Initial release

This commit is contained in:
Martino Ferrari
2026-05-29 13:29:59 +02:00
commit 617b5bd712
110 changed files with 29234 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
Build/
dependency/
*.o
*.a
*.so
*.d
compile_commands.json
*.gcov
*.gcda
*.gcno
# Go binaries
Client/debugger/marte2debugger
Client/udpstreamer/udpstreamer-webui
Client/*/bin/
# Standalone webui (integrated into Client/udpstreamer)
udp_standalone_webui/
+240
View File
@@ -0,0 +1,240 @@
# Architecture
This document describes the internal architecture of the MARTe2 Integrated Components.
---
## 1. Repository Overview
```
┌────────────────────────────────────────────────────────────────────┐
│ MARTe2 Application │
│ ┌──────────┐ ┌───────────────────────┐ ┌──────────────────┐ │
│ │ GAM(s) │ │ DebugBrokerWrapper │ │ FastScheduler │ │
│ │ │◄──│ (Registry-patched) │ │ (unmodified) │ │
│ └──────────┘ └──────────┬────────────┘ └──────────────────┘ │
│ │ RT-path API │
└────────────────────────────┼───────────────────────────────────────┘
┌────────▼────────┐
│ DebugServiceI │ ← Abstract singleton interface
└────────┬────────┘
┌────────▼────────┐
│ DebugService │
│ TCP/UDP │
└────────┬────────┘
┌───────────────┼──────────────┐
│ │ │
TCP 8080 UDP 8081 TCP 8082
(commands) (telemetry) (TcpLogger)
│ │
┌────────▼───────────────▼──────┐
│ Client/debugger │
│ Go web client (browser) │
└───────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ MARTe2 Application │
│ ┌─────────────────────────────────────────────┐ │
│ │ UDPStreamer DataSource │ │
│ │ - Receives signals from GAMs via IOGAM │ │
│ │ - Serialises to UDPS binary protocol │ │
│ │ - Manages per-client sessions │ │
│ └──────────────────────┬──────────────────────┘ │
└─────────────────────────┼──────────────────────────────────────────┘
UDP 44500
┌───────────▼──────────────┐
│ Common/Client/go │
│ udpsprotocol package │
│ (decode CONFIG+DATA) │
└───────────┬──────────────┘
│ WebSocket
┌───────────▼──────────────┐
│ Browser oscilloscope │
│ (Chart.js plots) │
└──────────────────────────┘
```
---
## 2. UDPS Binary Protocol
Defined in `Common/UDP/UDPSProtocol.h` and shared between:
- `UDPStreamer` (C++ producer)
- `DebugService` (C++ producer for trace packets)
- `Common/Client/go/udpsprotocol` (Go decoder)
### Packet Header (17 bytes, little-endian, packed)
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 4 | uint32 | `magic` | Always `0x53504455` ('UDPS' LE) |
| 4 | 1 | uint8 | `type` | Packet type (DATA=0, CONFIG=1, ACK=2, CONNECT=3, DISCONNECT=4) |
| 5 | 4 | uint32 | `counter` | Per-update sequence number (same across all fragments) |
| 9 | 2 | uint16 | `fragmentIdx` | 0-based fragment index |
| 11 | 2 | uint16 | `totalFragments` | Total fragments for this update |
| 13 | 4 | uint32 | `payloadBytes` | Bytes of payload following this header |
### CONFIG Payload
Sent when the signal set changes or a client connects:
```
[uint32 numSigs]
numSigs × UDPSSignalDescriptor (136 bytes each, packed)
[uint8 publishMode]
```
### DATA Payload (Strict / Decimate modes)
```
[uint64 HRT timestamp]
per-signal data in CONFIG order (quantised or raw, no inter-signal padding)
```
### DATA Payload (Accumulate mode)
```
[uint64 HRT timestamp]
[uint32 numSamples]
for each signal: if scalar → numSamples elements; else → NumElements once
```
### Quantization
When `QuantizedType` is set on a signal, the server maps `[RangeMin, RangeMax]` to the
full integer range of the quantized type before transmission. The Go client reverses this
using the `Scale` and `Offset` fields from `UDPSSignalDescriptor`.
---
## 3. DebugService Architecture
### 3.1 Registry Patching (Zero-Code-Change Instrumentation)
`DebugService::PatchRegistry()` replaces the `ObjectBuilder` for all `MemoryMap*Broker`
types in the MARTe2 `ClassRegistryDatabase`. Any subsequent `ConfigureApplication()` call
will instantiate `DebugBrokerWrapper<T>` objects instead of the originals. No application
source changes are needed.
Wrapped types: `MemoryMapInputBroker`, `MemoryMapOutputBroker`,
`MemoryMapSynchronisedInputBroker`, `MemoryMapSynchronisedOutputBroker`,
`MemoryMapMultiBufferBroker`, `MemoryMapMultiBufferOutputBroker`,
`MemoryMapAsynchronousInputBroker`, `MemoryMapAsynchronousOutputBroker`,
`MemoryMapInterpolatedInputBroker`, `MemoryMapStatefulOutputBroker`,
`MemoryMapStatefulInputBroker`.
### 3.2 Signal Registration
```
ConfigureApplication()
└─► DebugBrokerWrapper::Init()
└─► DebugBrokerHelper::InitSignals()
├─► DebugServiceI::RegisterSignal() (canonical + GAM alias)
└─► DebugServiceI::RegisterBroker()
```
Each signal is registered twice:
1. **Canonical**: `<DataSourcePath>.<SignalName>` (e.g. `App.Data.DDB.Counter`)
2. **GAM alias**: `<GAMPath>.In.<SignalName>` or `<GAMPath>.Out.<SignalName>`
Both map to the same `DebugSignalInfo*`. `AliasMatch()` in `DebugServiceBase.cpp`
performs bidirectional suffix matching so short unqualified names work in commands.
### 3.3 RT Hot Path
```
RealTimeThread::Execute()
└─► DebugBrokerWrapper::Execute()
├─► Base::Execute() (actual data movement)
└─► DebugBrokerHelper::Process()
├─► For each active signal:
│ └─► DebugServiceI::ProcessSignal()
│ ├─► if isForcing: memcpy forcedValue → signal memory
│ ├─► if isTracing & decimation fires: push to TraceRingBuffer
│ └─► if breakOp set: evaluate condition → SetPaused(true)
└─► (output brokers only) ConsumeStepIfNeeded()
```
### 3.4 TraceRingBuffer
Single-producer/single-consumer circular byte buffer (4 MB default).
Entry format: `[ID:4][Timestamp:8][Size:4][Data:N]`.
- `Push()` serialised by `tracePushMutex` (multiple RT threads may write)
- `Pop()` called exclusively by the Streamer thread
- Corrupt entries detected by `size >= bufferSize`; discarded gracefully
### 3.5 DebugService Threads
| Thread | Role |
|---|---|
| `Server()` | Accepts one TCP client; reads text commands; writes JSON/text responses |
| `Streamer()` | Drains `TraceRingBuffer`; assembles/sends UDP datagrams; polls monitored signals |
### 3.6 DebugServiceI Abstraction
`DebugServiceI.h` defines a pure-virtual singleton so transport implementations
(`DebugService` TCP/UDP, `WebDebugService` HTTP/SSE) share the same broker injection layer.
```cpp
DebugServiceI::SetInstance(this); // concrete implementation registers itself
DebugServiceI *svc = DebugServiceI::GetInstance(); // broker wrapper retrieves it
```
---
## 4. UDPStreamer DataSource
`UDPStreamer` is a standard MARTe2 `DataSourceI` that:
1. Maintains a list of registered client sessions (UDP source address + port)
2. On each `Synchronise()` call (once per RT cycle):
- Serialises all configured signals into one or more UDPS DATA packets
- Sends to all connected clients
3. Handles `CONNECT` / `DISCONNECT` / `ACK` packets from clients
4. Sends `CONFIG` packets when the signal list changes or a new client connects
5. Applies quantization (`QuantizedType`, `RangeMin`, `RangeMax`) per signal
### Packed Signals (Accumulate mode)
When a signal has `NumberOfElements > 1` and `SamplingRate` is configured:
- `TimeMode = FirstSample`: the `TimeSignal` value anchors the burst timestamp
- The Go client interpolates per-sample timestamps as `t0 + e/SamplingRate * dt`
- `UDPStreamer` packs all elements contiguously with no per-element header
---
## 5. Go Client Packages
### `Common/Client/go/udpsprotocol`
Pure Go decoder for UDPS packets:
- `Decoder` — reassembles fragments; returns complete CONFIG or DATA payloads
- `SignalDescriptor` — mirrors `UDPSSignalDescriptor` from C++
- `DataPacket` — decoded signal values with per-sample timestamps
### `Common/Client/go/wshub`
WebSocket broadcast hub:
- Multiple browser clients share one UDP → WebSocket relay
- JSON-encodes signal samples and broadcasts to all connected browsers
### `Client/debugger`
Go HTTP server providing the debug web UI:
- `main.go` — CLI entry point; starts TCP relay and HTTP server
- `martecontrol.go` — TCP client to `DebugService`; routes commands from browser
- `static/` — Single-page application (HTML/JS/CSS) with Chart.js plots
---
## 6. Key Design Decisions
| Decision | Rationale |
|---|---|
| Shared `UDPSProtocol.h` in `Common/UDP/` | Eliminates duplication between `UDPStreamer` and any future UDP producer; Go packages decode the same binary layout |
| `DebugServiceI` abstract interface | Allows `DebugService` (TCP/UDP) and `WebDebugService` (HTTP/SSE) to share broker injection without coupling |
| No STL in C++ components | MARTe2 coding convention; `Vec<T>` used instead of `std::vector` |
| `FastPollingMutexSem` on hot path | Lowest-latency synchronisation primitive available in MARTe2; avoids OS scheduler involvement |
| Separate `TCPLogger` component | Decoupled from `DebugService`; can be used with any MARTe2 application independently |
| Go for clients | Minimal dependencies, single binary, easy cross-compilation; no Node/npm toolchain required |
+12
View File
@@ -0,0 +1,12 @@
module marte2debugger
go 1.21
require marte2/common v0.0.0
require (
github.com/gorilla/websocket v1.5.1 // indirect
golang.org/x/net v0.17.0 // indirect
)
replace marte2/common => ../../Common/Client/go
+4
View File
@@ -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=
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"embed"
"errors"
"flag"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"marte2/common/wshub"
)
var buildVersion = "dev"
//go:embed static
var staticFiles embed.FS
func main() {
addr := flag.String("addr", ":7777", "HTTP listen address")
sourcesFile := flag.String("sources-file", "", "JSON file for persistent source list")
flag.Parse()
hub := wshub.NewHub()
sm := wshub.NewSourceManager(hub, *sourcesFile)
hub.SetSourceManager(sm)
ctrl := NewMarteController(hub)
go hub.Run()
// Load persisted sources
if *sourcesFile != "" {
if err := sm.Load(*sourcesFile); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("sources-file load: %v", err)
}
}
// Register the debug source (always present; state managed by MarteController)
hub.AddSource("debug", "MARTe2 Debug", "")
// Forward browser debug commands to MarteController
go func() {
for msg := range hub.DebugCh {
ctrl.HandleBrowserCommand(msg)
}
}()
sub, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatalf("static sub-fs: %v", err)
}
http.Handle("/", http.FileServer(http.FS(sub)))
http.HandleFunc("/ws", hub.HandleWebSocket)
http.HandleFunc("/api/zoom", hub.HandleZoom)
http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, buildVersion)
})
log.Printf("MARTe2 Integrated Client listening on %s (build=%s)", *addr, buildVersion)
if err := http.ListenAndServe(*addr, nil); err != nil {
log.Fatalf("http: %v", err)
}
}
+895
View File
@@ -0,0 +1,895 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"log"
"net"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"marte2/common/udpsprotocol"
"marte2/common/wshub"
)
// ---------------------------------------------------------------------------
// Signal metadata (populated by DISCOVER)
// ---------------------------------------------------------------------------
type SignalMeta struct {
Name string `json:"name"`
ID uint32 `json:"id"`
Type string `json:"type"`
Dimensions uint8 `json:"dimensions"`
Elements uint32 `json:"elements"`
Names []string // canonical + alias names mapping to this ID
}
// ---------------------------------------------------------------------------
// Outbound broadcast helpers
// ---------------------------------------------------------------------------
func broadcastHub(hub *wshub.Hub, v any) {
b, err := json.Marshal(v)
if err != nil {
return
}
hub.Broadcast(b)
}
// ---------------------------------------------------------------------------
// MarteController
// ---------------------------------------------------------------------------
type MarteController struct {
hub *wshub.Hub
mu sync.Mutex
tcpConn net.Conn
writer *bufio.Writer
cmdMu sync.Mutex // serialise TCP writes
sigMu sync.RWMutex
signals map[uint32]*SignalMeta // id -> meta
tracedMu sync.RWMutex
tracedNames map[string]bool // user-visible names currently being traced
// Persistent forced-signal state: replayed to new browser clients so the
// forced-signals panel stays correct across page reloads.
forcedMu sync.RWMutex
forcedState map[string]string // signal key (may include [i]) → forced value
baseTsSet bool
basesMu sync.Mutex
connected int32 // atomic bool
lastWriteMs int64 // atomic; updated on every TCP write, used by keepalive
stopCh chan struct{}
// accumulates signals across DISCOVER_PART chunks; merged on final DISCOVER
discoverAcc []discoverSignalJSON
// cached last-known ports (updated by SERVICE_INFO)
host string
cmdPort int
udpPort int
logPort int
}
// NewMarteController creates a MarteController bound to the given hub.
func NewMarteController(hub *wshub.Hub) *MarteController {
mc := &MarteController{
hub: hub,
signals: make(map[uint32]*SignalMeta),
tracedNames: make(map[string]bool),
forcedState: make(map[string]string),
stopCh: make(chan struct{}),
}
// Register the new-client hook so connection + forced/traced state is
// replayed to any browser that connects (or reconnects) while the server
// already holds a live MARTe2 TCP session.
hub.SetOnClientConnect(mc.replayStateToClient)
return mc
}
func (m *MarteController) IsConnected() bool {
return atomic.LoadInt32(&m.connected) == 1
}
// replayStateToClient is called by the hub whenever a new WebSocket client
// connects. It sends the current MARTe2 connection status and any persistent
// forced/traced signal state so the browser UI is always consistent.
func (m *MarteController) replayStateToClient(send func([]byte)) {
if !m.IsConnected() {
return
}
// Re-send "connected" so the browser updates its UI state.
if b, err := json.Marshal(map[string]any{"type": "connected"}); err == nil {
send(b)
}
// Replay forced signals.
m.forcedMu.RLock()
forced := make(map[string]string, len(m.forcedState))
for k, v := range m.forcedState {
forced[k] = v
}
m.forcedMu.RUnlock()
if len(forced) > 0 {
if b, err := json.Marshal(map[string]any{"type": "forced_state", "signals": forced}); err == nil {
send(b)
}
}
// Replay traced signal names.
m.tracedMu.RLock()
traced := make([]string, 0, len(m.tracedNames))
for n := range m.tracedNames {
traced = append(traced, n)
}
m.tracedMu.RUnlock()
if len(traced) > 0 {
if b, err := json.Marshal(map[string]any{"type": "traced_state", "signals": traced}); err == nil {
send(b)
}
}
}
// ---------------------------------------------------------------------------
// Connect / Disconnect
// ---------------------------------------------------------------------------
func (m *MarteController) Connect(host string, cmdPort, udpPort, logPort int) {
m.Disconnect()
m.mu.Lock()
m.host = host
m.cmdPort = cmdPort
m.udpPort = udpPort
m.logPort = logPort
m.stopCh = make(chan struct{})
m.mu.Unlock()
// Update source state so the browser shows "connecting".
m.hub.SetSourceState("debug", "connecting")
broadcastHub(m.hub, map[string]any{
"type": "log", "time": time.Now().Format("15:04:05.000"),
"level": "INFO", "message": fmt.Sprintf("Connecting to %s cmd=%d udp=%d log=%d", host, cmdPort, udpPort, logPort),
})
go m.runTCP(host, cmdPort)
go m.runDebugUDP(host, udpPort)
go m.runLog(host, logPort)
}
func (m *MarteController) Disconnect() {
m.mu.Lock()
select {
case <-m.stopCh:
// already closed
default:
close(m.stopCh)
}
if m.tcpConn != nil {
m.tcpConn.Close()
m.tcpConn = nil
}
m.mu.Unlock()
atomic.StoreInt32(&m.connected, 0)
m.basesMu.Lock()
m.baseTsSet = false
m.basesMu.Unlock()
m.discoverAcc = nil
m.hub.SetSourceState("debug", "disconnected")
}
func (m *MarteController) stopped() bool {
select {
case <-m.stopCh:
return true
default:
return false
}
}
// ---------------------------------------------------------------------------
// HandleBrowserCommand — dispatch JSON commands from browser
// ---------------------------------------------------------------------------
func (m *MarteController) HandleBrowserCommand(msg []byte) {
var env map[string]interface{}
if err := json.Unmarshal(msg, &env); err != nil {
return
}
t, _ := env["type"].(string)
switch t {
case "connect":
data, _ := env["data"].(map[string]interface{})
if data == nil {
return
}
host, _ := data["host"].(string)
portF, _ := data["port"].(float64)
udpPortF, _ := data["udp_port"].(float64)
logPortF, _ := data["log_port"].(float64)
if host == "" {
host = "127.0.0.1"
}
port := int(portF)
if port == 0 {
port = 8080
}
udpPort := int(udpPortF)
if udpPort == 0 {
udpPort = port + 1
}
logPort := int(logPortF)
if logPort == 0 {
logPort = port + 2
}
m.Connect(host, port, udpPort, logPort)
case "disconnect":
m.Disconnect()
case "cmd":
data, _ := env["data"].(map[string]interface{})
if data == nil {
return
}
cmd, _ := data["cmd"].(string)
if cmd != "" {
m.trackForcedCmd(cmd)
m.SendCommand(cmd)
}
}
}
// ---------------------------------------------------------------------------
// TCP command channel
// ---------------------------------------------------------------------------
func (m *MarteController) runTCP(host string, port int) {
addr := fmt.Sprintf("%s:%d", host, port)
for !m.stopped() {
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
broadcastHub(m.hub, map[string]any{
"type": "log", "time": time.Now().Format("15:04:05.000"),
"level": "WARNING", "message": fmt.Sprintf("TCP %s: %v — retrying…", addr, err),
})
time.Sleep(2 * time.Second)
continue
}
conn.(*net.TCPConn).SetNoDelay(true)
m.mu.Lock()
m.tcpConn = conn
m.writer = bufio.NewWriter(conn)
m.mu.Unlock()
atomic.StoreInt32(&m.connected, 1)
broadcastHub(m.hub, map[string]any{"type": "connected"})
// Send SERVICE_INFO to auto-discover ports
m.writeCmd("SERVICE_INFO")
go m.runKeepalive()
m.readLoop(conn)
atomic.StoreInt32(&m.connected, 0)
broadcastHub(m.hub, map[string]any{"type": "disconnected"})
m.mu.Lock()
m.tcpConn = nil
m.writer = nil
m.mu.Unlock()
if !m.stopped() {
time.Sleep(2 * time.Second)
}
}
}
func (m *MarteController) writeCmd(cmd string) {
m.cmdMu.Lock()
defer m.cmdMu.Unlock()
m.mu.Lock()
w := m.writer
m.mu.Unlock()
if w == nil {
return
}
// Suppress high-frequency polling commands from both terminal and browser logs.
silent := cmd == "STEP_STATUS" || cmd == "INFO"
if !silent {
log.Printf("[→MARTe] %s", cmd)
broadcastHub(m.hub, map[string]any{
"type": "log", "time": time.Now().Format("15:04:05.000"),
"level": "CMD", "message": fmt.Sprintf("→ %s", cmd),
})
}
w.WriteString(cmd + "\n")
w.Flush()
atomic.StoreInt64(&m.lastWriteMs, time.Now().UnixMilli())
}
// runKeepalive sends INFO every 20 s when idle.
func (m *MarteController) runKeepalive() {
ticker := time.NewTicker(20 * time.Second)
defer ticker.Stop()
for {
select {
case <-m.stopCh:
return
case <-ticker.C:
if !m.IsConnected() {
continue
}
idleMs := time.Now().UnixMilli() - atomic.LoadInt64(&m.lastWriteMs)
if idleMs >= 20_000 {
m.writeCmd("INFO")
}
}
}
}
// trackForcedCmd intercepts FORCE/UNFORCE commands to maintain the persistent
// forced-signal state that is replayed to new browser clients.
func (m *MarteController) trackForcedCmd(cmd string) {
parts := strings.Fields(cmd)
if len(parts) < 2 {
return
}
switch strings.ToUpper(parts[0]) {
case "FORCE":
if len(parts) >= 3 {
key := parts[1]
val := strings.Join(parts[2:], " ")
m.forcedMu.Lock()
m.forcedState[key] = val
m.forcedMu.Unlock()
}
case "UNFORCE":
key := parts[1]
m.forcedMu.Lock()
// Remove the exact key and any element keys that share the same base name
// (e.g., UNFORCE Foo removes Foo, Foo[0], Foo[1], …).
delete(m.forcedState, key)
prefix := key + "["
for k := range m.forcedState {
if strings.HasPrefix(k, prefix) {
delete(m.forcedState, k)
}
}
m.forcedMu.Unlock()
}
}
func (m *MarteController) SendCommand(cmd string) {
// Track TRACE enable/disable so translateSignalNames can pick the right alias.
if strings.HasPrefix(cmd, "TRACE ") {
parts := strings.Fields(cmd)
if len(parts) == 3 {
name := parts[1]
enable := parts[2] == "1"
m.tracedMu.Lock()
if enable {
m.tracedNames[name] = true
} else {
delete(m.tracedNames, name)
}
m.tracedMu.Unlock()
}
}
m.writeCmd(cmd)
}
func (m *MarteController) readLoop(conn net.Conn) {
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, 8*1024*1024), 8*1024*1024)
var jsonAcc strings.Builder
inJSON := false
for scanner.Scan() {
if m.stopped() {
return
}
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
// Detect start of JSON block
if !inJSON && strings.HasPrefix(trimmed, "{") {
inJSON = true
jsonAcc.Reset()
}
if inJSON {
jsonAcc.WriteString(trimmed)
tag, done := detectJSONDone(trimmed)
if done {
inJSON = false
raw := jsonAcc.String()
// Strip trailing sentinel
idx := strings.Index(raw, "OK "+tag)
if idx >= 0 {
raw = strings.TrimSpace(raw[:idx])
}
m.handleJSONResponse(tag, raw)
jsonAcc.Reset()
}
} else {
m.handleTextLine(trimmed)
}
}
}
func detectJSONDone(line string) (string, bool) {
tags := []string{
"DISCOVER_PART", "DISCOVER", "TREE", "INFO", "CONFIG", "STEP_STATUS",
"VALUE", "MSG", "TRACE", "FORCE", "UNFORCE", "BREAK",
"PAUSE", "RESUME", "STEP", "MONITOR", "UNMONITOR", "LS",
}
for _, t := range tags {
if line == "OK "+t {
return t, true
}
}
return "", false
}
func (m *MarteController) handleJSONResponse(tag, data string) {
silent := tag == "STEP_STATUS" || tag == "INFO"
if !silent {
log.Printf("[←MARTe] %s %d bytes", tag, len(data))
broadcastHub(m.hub, map[string]any{
"type": "log", "time": time.Now().Format("15:04:05.000"),
"level": "RESP", "message": fmt.Sprintf("← %s (%d B)", tag, len(data)),
})
}
switch tag {
case "DISCOVER_PART":
var resp discoverResp
if err := json.Unmarshal([]byte(data), &resp); err != nil {
log.Printf("[DISCOVER_PART] parse error: %v", err)
return
}
m.discoverAcc = append(m.discoverAcc, resp.Signals...)
log.Printf("[DISCOVER_PART] accumulated %d signals (total so far: %d)",
len(resp.Signals), len(m.discoverAcc))
return
case "DISCOVER":
var resp discoverResp
if err := json.Unmarshal([]byte(data), &resp); err != nil {
log.Printf("[DISCOVER] parse error: %v", err)
return
}
all := append(m.discoverAcc, resp.Signals...)
m.discoverAcc = nil
m.parseDiscoverSignals(all)
// Synthesize SignalInfo for the hub so the plot system gets a config
m.synthesizeHubConfig(all)
// Re-marshal the merged list so the browser gets a single consistent blob.
merged, _ := json.Marshal(discoverResp{Signals: all})
broadcastHub(m.hub, map[string]any{
"type": "response", "tag": "DISCOVER", "data": string(merged),
})
return
case "TREE":
broadcastHub(m.hub, map[string]any{
"type": "tree_node",
"data": data,
})
return
}
broadcastHub(m.hub, map[string]any{
"type": "response",
"tag": tag,
"data": data,
})
}
func (m *MarteController) handleTextLine(line string) {
if strings.HasPrefix(line, "OK SERVICE_INFO") {
parts := strings.Fields(line)
newUDP, newLog := 0, 0
for _, p := range parts {
if strings.HasPrefix(p, "UDP_STREAM:") {
fmt.Sscanf(p[11:], "%d", &newUDP)
}
if strings.HasPrefix(p, "TCP_LOG:") {
fmt.Sscanf(p[8:], "%d", &newLog)
}
}
broadcastHub(m.hub, map[string]any{
"type": "response",
"tag": "SERVICE_INFO",
"data": line[len("OK SERVICE_INFO "):],
})
if newUDP > 0 || newLog > 0 {
broadcastHub(m.hub, map[string]any{
"type": "service_config",
"udp_port": newUDP,
"log_port": newLog,
})
m.mu.Lock()
host := m.host
oldUDP := m.udpPort
oldLog := m.logPort
if newUDP > 0 {
m.udpPort = newUDP
}
if newLog > 0 {
m.logPort = newLog
}
m.mu.Unlock()
if newUDP > 0 && newUDP != oldUDP {
go m.runDebugUDP(host, newUDP)
}
if newLog > 0 && newLog != oldLog {
go m.runLog(host, newLog)
}
}
}
broadcastHub(m.hub, map[string]any{
"type": "text_line",
"data": line,
})
}
// ---------------------------------------------------------------------------
// DISCOVER parsing
// ---------------------------------------------------------------------------
type discoverSignalJSON struct {
Name string `json:"name"`
ID uint32 `json:"id"`
Type string `json:"type"`
Dimensions uint8 `json:"dimensions"`
Elements uint32 `json:"elements"`
}
type discoverResp struct {
Signals []discoverSignalJSON `json:"Signals"`
}
func (m *MarteController) parseDiscoverSignals(sigs []discoverSignalJSON) {
m.sigMu.Lock()
defer m.sigMu.Unlock()
m.signals = make(map[uint32]*SignalMeta, len(sigs))
for _, s := range sigs {
el := s.Elements
if el == 0 {
el = 1
}
if existing, ok := m.signals[s.ID]; ok {
existing.Names = append(existing.Names, s.Name)
continue
}
meta := &SignalMeta{
Name: s.Name,
ID: s.ID,
Type: s.Type,
Dimensions: s.Dimensions,
Elements: el,
Names: []string{s.Name},
}
m.signals[s.ID] = meta
}
log.Printf("[DISCOVER] registered %d unique signals from %d entries", len(m.signals), len(sigs))
}
// translateSignalNames maps UDPS signal names (DS canonical, e.g. "DDB1.Sine1")
// to the preferred GAM-path alias (e.g. "App.Functions.SineGAM1.Out.Sine1").
//
// DebugService always sets signals[i]->name to the first registered name, which
// is the DataSource canonical path. The user-facing name in the tree and the
// tracedSet is the GAM alias (contains ".Out." or ".In."). Without this
// translation the buffer key created from UDPS CONFIG ("debug:DDB1.Sine1")
// never matches the buffer key the tracedTab looks up ("debug:App…Out.Sine1").
func (m *MarteController) translateSignalNames(sigs []udpsprotocol.SignalInfo) []udpsprotocol.SignalInfo {
m.sigMu.RLock()
defer m.sigMu.RUnlock()
if len(m.signals) == 0 {
return sigs // no DISCOVER data yet — return as-is
}
// Build a reverse map: every known alias name → SignalMeta
nameToMeta := make(map[string]*SignalMeta, len(m.signals)*2)
for _, meta := range m.signals {
for _, n := range meta.Names {
nameToMeta[n] = meta
}
}
// Snapshot the user's currently-traced names for matching.
m.tracedMu.RLock()
traced := make(map[string]bool, len(m.tracedNames))
for k, v := range m.tracedNames {
traced[k] = v
}
m.tracedMu.RUnlock()
result := make([]udpsprotocol.SignalInfo, len(sigs))
for i, sig := range sigs {
result[i] = sig
meta, ok := nameToMeta[sig.Name]
if !ok {
continue
}
best := sig.Name
// Priority 1: find a traced name that alias-matches this signal (mirrors
// C++ AliasMatch: exact or suffix in either direction, dot-boundary).
outer:
for _, n := range meta.Names {
for tracedName := range traced {
if aliasMatch(n, tracedName) {
best = tracedName // use the name the user sees in the tree
break outer
}
}
}
// Priority 2 (fallback): longest alias with ".Out." or ".In." (GAM path).
if best == sig.Name {
for _, n := range meta.Names {
if (strings.Contains(n, ".Out.") || strings.Contains(n, ".In.")) && len(n) > len(best) {
best = n
}
}
}
if best != sig.Name {
log.Printf("[debug-udp] rename %q → %q", sig.Name, best)
result[i].Name = best
}
}
return result
}
// aliasMatch mirrors C++ AliasMatch: returns true if a and b are equal or one
// is a dot-boundary suffix of the other.
func aliasMatch(a, b string) bool {
if a == b {
return true
}
return suffixMatchDot(a, b) || suffixMatchDot(b, a)
}
func suffixMatchDot(str, suffix string) bool {
if len(suffix) > len(str) {
return false
}
tail := str[len(str)-len(suffix):]
if tail != suffix {
return false
}
return len(str) == len(suffix) || str[len(str)-len(suffix)-1] == '.'
}
// typeCodeFromString maps a MARTe2 type string to a UDPS type code.
func typeCodeFromString(t string) uint8 {
switch strings.ToLower(t) {
case "uint8":
return 0
case "int8":
return 1
case "uint16":
return 2
case "int16":
return 3
case "uint32":
return 4
case "int32":
return 5
case "uint64":
return 6
case "int64":
return 7
case "float32":
return 8
case "float64":
return 9
default:
return 4 // default to uint32
}
}
// synthesizeHubConfig converts DISCOVER signals to udpsprotocol.SignalInfo and
// sends them to the hub so the signal list panel is populated before UDP data arrives.
func (m *MarteController) synthesizeHubConfig(sigs []discoverSignalJSON) {
seen := make(map[uint32]bool)
var sigInfos []udpsprotocol.SignalInfo
for _, s := range sigs {
if seen[s.ID] {
continue
}
seen[s.ID] = true
el := s.Elements
if el == 0 {
el = 1
}
numRows := el
numCols := uint32(1)
if s.Dimensions >= 2 {
numCols = el
numRows = 1
}
si := udpsprotocol.SignalInfo{
Name: s.Name,
TypeCode: typeCodeFromString(s.Type),
QuantType: 0,
NumDimensions: s.Dimensions,
NumRows: numRows,
NumCols: numCols,
RangeMin: 0,
RangeMax: 0,
TimeMode: udpsprotocol.TimeModePacket,
SamplingRate: 0,
TimeSignalIdx: udpsprotocol.NoTimeSignal,
Unit: "",
}
sigInfos = append(sigInfos, si)
}
m.hub.UpdateConfigForSource("debug", sigInfos)
}
// ---------------------------------------------------------------------------
// Debug UDP receiver — receives UDPS packets from DebugService
// ---------------------------------------------------------------------------
func (m *MarteController) runDebugUDP(host string, port int) {
addr := fmt.Sprintf("0.0.0.0:%d", port)
// Use SO_REUSEPORT so we can bind the same port that DebugService already
// holds open. Without this the second bind fails with EADDRINUSE and we
// receive nothing.
lc := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
const SO_REUSEPORT = 0xf // Linux SO_REUSEPORT = 15
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, SO_REUSEPORT, 1); err != nil {
log.Printf("[debug-udp] SO_REUSEPORT: %v", err)
}
})
},
}
pc, err := lc.ListenPacket(context.Background(), "udp4", addr)
if err != nil {
msg := fmt.Sprintf("UDP bind on %s failed: %v — rebuild DebugService C++ and restart", addr, err)
log.Printf("[debug-udp] %s", msg)
broadcastHub(m.hub, map[string]any{
"type": "log", "time": time.Now().Format("15:04:05.000"),
"level": "ERROR", "message": msg,
})
return
}
conn := pc.(*net.UDPConn)
defer conn.Close()
conn.SetReadBuffer(10 * 1024 * 1024)
log.Printf("[debug-udp] listening on %s for UDPS packets", addr)
broadcastHub(m.hub, map[string]any{
"type": "log", "time": time.Now().Format("15:04:05.000"),
"level": "INFO", "message": fmt.Sprintf("UDP listener bound on %s", addr),
})
reassembler := udpsprotocol.NewReassembler(2 * time.Second)
buf := make([]byte, 65535)
var currentSigs []udpsprotocol.SignalInfo
var currentPublishMode uint8
var pktCount int64
for !m.stopped() {
conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
n, _, err := conn.ReadFromUDP(buf)
arrivalTime := time.Now()
if err != nil {
continue
}
pktCount++
if n < udpsprotocol.HeaderSize {
continue
}
hdr, err := udpsprotocol.ParseHeader(buf[:n])
if err != nil {
continue
}
payload := make([]byte, n-udpsprotocol.HeaderSize)
copy(payload, buf[udpsprotocol.HeaderSize:n])
complete, ok := reassembler.AddFragment(hdr, payload)
if !ok {
continue
}
switch hdr.Type {
case udpsprotocol.PktConfig:
sigs, pm, err := udpsprotocol.ParseConfig(complete)
if err != nil {
log.Printf("[debug-udp] parse config: %v", err)
continue
}
sigs = m.translateSignalNames(sigs)
currentSigs = sigs
currentPublishMode = pm
m.hub.UpdateConfigForSource("debug", sigs)
m.hub.SetSourceState("debug", "connected")
case udpsprotocol.PktData:
if len(currentSigs) == 0 {
continue
}
samples, err := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
if err != nil {
log.Printf("[debug-udp] parse data: %v", err)
continue
}
for _, s := range samples {
m.hub.PushDataForSource("debug", s)
}
}
}
log.Printf("[debug-udp] stopped")
}
// ---------------------------------------------------------------------------
// TCP log channel
// ---------------------------------------------------------------------------
func (m *MarteController) runLog(host string, port int) {
addr := fmt.Sprintf("%s:%d", host, port)
for !m.stopped() {
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
time.Sleep(2 * time.Second)
continue
}
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
if m.stopped() {
conn.Close()
return
}
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "LOG ") {
rest := line[4:]
idx := strings.Index(rest, " ")
if idx < 0 {
continue
}
level := rest[:idx]
msg := rest[idx+1:]
broadcastHub(m.hub, map[string]any{
"type": "log",
"time": time.Now().Format("15:04:05.000"),
"level": level,
"message": msg,
})
}
}
conn.Close()
if !m.stopped() {
time.Sleep(2 * time.Second)
}
}
}
File diff suppressed because it is too large Load Diff
+514
View File
@@ -0,0 +1,514 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MARTe2 Integrated Client</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/uPlot.min.css">
<link rel="stylesheet" href="/style.css">
<script src="/uPlot.iife.min.js"></script>
</head>
<body>
<!-- ── Top bar ───────────────────────────────────────────────── -->
<div id="topbar">
<span id="app-title">MARTe2</span>
<div class="topbar-vsep"></div>
<!-- Debug connection menu -->
<div class="menu-wrap">
<button class="ctrl-btn" id="conn-menu-btn" onclick="toggleMenu('conn-dropdown')">
<span id="conn-status"></span>
Debug ▾
</button>
<div class="dropdown" id="conn-dropdown">
<div class="menu-row">
<input type="text" id="host" value="127.0.0.1" placeholder="host" style="flex:2">
<input type="number" id="port" value="8080" placeholder="ctrl" style="width:60px">
<button id="btn-connect" onclick="toggleConnect()">Connect</button>
</div>
<div class="menu-row" id="port-manual-row" style="display:none">
<label style="color:#a6adc8;font-size:11px;flex-shrink:0">UDP:</label>
<input type="number" id="udp-port" value="8081" placeholder="udp" style="width:72px">
<label style="color:#a6adc8;font-size:11px;flex-shrink:0">Log:</label>
<input type="number" id="log-port" value="8082" placeholder="log" style="width:72px">
</div>
<div class="menu-row">
<label class="form-check" style="margin:0;font-size:11px">
<input type="checkbox" id="auto-ports" checked onchange="toggleAutoPorts(this.checked)">
<span style="color:#a6adc8">Auto ports (SERVICE_INFO)</span>
</label>
</div>
<hr class="menu-sep">
<div class="menu-btn-row">
<button onclick="discoverCmd()">Discover</button>
<button onclick="treeCmd()">Tree</button>
<button onclick="serviceInfoCmd()">Info</button>
</div>
</div>
</div>
<button id="btn-pause" class="ctrl-btn" onclick="togglePause()">⏸ Pause</button>
<button class="ctrl-btn" onclick="openStepDialog()">⚙ Step</button>
<button class="ctrl-btn" onclick="openForceDialog()">⚡ Force</button>
<button class="ctrl-btn" onclick="openBreakDialog()">Break</button>
<button class="ctrl-btn" onclick="openMsgDialog()">✉ Msg</button>
<div class="topbar-vsep"></div>
<!-- Scope controls -->
<button id="btn-layout" class="ctrl-btn layout-toggle" title="Select layout">⊞ 1×1 ▾</button>
<div class="topbar-sep"></div>
<div id="cursor-readout">
<span id="cur-ta">A: —</span><span class="cur-sep"></span>
<span id="cur-tb">B: —</span><span class="cur-sep"></span>
<span id="cur-dt">ΔT: —</span>
</div>
<span class="ctrl-label" id="lbl-window">Window:</span>
<select id="window-select" class="ctrl-select">
<option value="1">1 s</option><option value="5" selected>5 s</option>
<option value="10">10 s</option><option value="30">30 s</option>
<option value="60">60 s</option>
</select>
<button id="btn-cursor" class="ctrl-btn" style="display:none">Cursor</button>
<button id="btn-zoom-back" class="ctrl-btn" style="display:none">← Back</button>
<button id="btn-zoom-fit" class="ctrl-btn">Fit</button>
<button id="btn-csv-all" class="ctrl-btn" title="Export all signals to CSV">⬇ CSV</button>
<button id="btn-sync-resume" class="ctrl-btn resume-btn" style="display:none">↺ Auto</button>
<button id="btn-trigger" class="ctrl-btn">⚡ Trigger</button>
<button id="btn-pause-global" class="ctrl-btn">⏸ Pause</button>
<div class="topbar-vsep"></div>
<span id="udp-stats" style="color:#585b70;font-size:11px">0 pkts</span>
<div id="status-led"></div>
<span id="status-text" style="font-size:11px;color:#a6adc8">Disconnected</span>
<span id="sb-tsage" style="font-size:11px;color:#585b70"></span>
<button id="btn-stats" class="ctrl-btn" style="height:20px;padding:0 7px;font-size:10px;line-height:1">Stats</button>
<span id="build-version" style="font-size:10px;color:#585b70;margin-left:4px"></span>
</div>
<!-- ── Trigger bar ───────────────────────────────────────────── -->
<div id="trigbar">
<div class="trig-group">
<span class="trig-label">Signal</span>
<select id="trig-signal" class="trig-select"><option value="">— none —</option></select>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Edge</span>
<select id="trig-edge" class="trig-select">
<option value="rising">Rising ↑</option>
<option value="falling">Falling ↓</option>
<option value="both">Both ↕</option>
</select>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Threshold</span>
<input id="trig-threshold" class="trig-input" type="number" value="0" step="any">
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Window</span>
<select id="trig-window" class="trig-select">
<option value="0.0001">100 μs</option><option value="0.001">1 ms</option>
<option value="0.01">10 ms</option><option value="0.1">100 ms</option>
<option value="0.5">500 ms</option><option value="1" selected>1 s</option>
<option value="5">5 s</option><option value="10">10 s</option>
</select>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Pre</span>
<input id="trig-pre" class="trig-range" type="range" min="0" max="100" value="20">
<span class="trig-range-val" id="trig-pre-val">20%</span>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Mode</span>
<select id="trig-mode" class="trig-select">
<option value="normal">Normal</option>
<option value="single">Single</option>
</select>
</div>
<div class="trig-sep"></div>
<div class="trig-group" style="gap:8px">
<span id="trig-status-badge">IDLE</span>
<button id="btn-trig-stop" style="display:none">Stop</button>
<button id="btn-trig-rearm">Rearm</button>
</div>
</div>
<!-- ── Body ─────────────────────────────────────────────────── -->
<div id="body">
<!-- ── Step status bar (inside body, visible when paused) ── -->
<div id="step-bar">
<span>⏹ PAUSED at <b id="paused-gam"></b></span>
<span id="step-remaining"></span>
<select id="step-thread" style="width:120px"></select>
<button onclick="step(1)">Step 1</button>
<button onclick="step(5)">Step 5</button>
<button onclick="step(20)">Step 20</button>
<button class="ok" onclick="togglePause()">▶ Resume</button>
</div>
<div id="body-row">
<!-- LEFT: signal list (scope) + object tree (debugger) -->
<div id="sidebar">
<div class="panel-tabs">
<div class="panel-tab active" onclick="switchSidebarTab('tree')">Object Tree</div>
</div>
<!-- Signals tab (hidden — signals come from Traced panel in debugger) -->
<div id="sidebar-signals" class="sidebar-tab-body" style="display:none">
<div id="signal-list"></div>
</div>
<!-- Object tree tab -->
<div id="sidebar-tree" class="sidebar-tab-body active">
<div class="panel-search">
<input id="tree-search" oninput="filterTree(this.value)" placeholder="Search tree…">
</div>
<div class="panel-body" id="tree-body">
<div class="empty-hint">Connect to MARTe2 then click Tree</div>
</div>
</div>
</div>
<!-- Left resize/collapse strip -->
<div id="left-strip" class="panel-strip" title="Drag to resize · Click to collapse"></div>
<!-- CENTER: plots -->
<div id="main">
<div id="plot-grid" class="l1x1"></div>
</div>
<!-- Right resize/collapse strip -->
<div id="right-strip" class="panel-strip" title="Drag to resize · Click to collapse"></div>
<!-- RIGHT: debug tabs (Traced, Forced, Breaks, Msgs) -->
<div id="right-panel">
<div class="tabs">
<div class="tab active" onclick="switchTab('traced')">Traced</div>
<div class="tab" onclick="switchTab('forced')">Forced</div>
<div class="tab" onclick="switchTab('breaks')">Breaks</div>
<div class="tab" onclick="switchTab('msgs')">Msgs</div>
</div>
<div class="tab-content active" id="tab-traced">
<div class="empty-hint" id="no-traced-hint">No signals traced</div>
</div>
<div class="tab-content" id="tab-forced">
<div class="empty-hint" id="no-forced-hint">No signals forced</div>
</div>
<div class="tab-content" id="tab-breaks">
<div class="empty-hint" id="no-breaks-hint">No breakpoints set</div>
</div>
<div class="tab-content" id="tab-msgs">
<div class="empty-hint" id="no-msgs-hint">No messages sent</div>
</div>
</div>
</div><!-- #body-row -->
<!-- ── Bottom: Logs ─────────────────────────────────────────── -->
<div id="log-panel">
<div id="log-toolbar">
<button class="panel-toggle" title="Collapse logs" onclick="togglePanel('log-panel','▼','▲',this)" id="log-toggle-btn"></button>
<span style="font-weight:600;color:#89b4fa">Logs</span>
<label><input type="checkbox" id="lf-service" onchange="renderLogs()"> Service</label>
<label><input type="checkbox" id="lf-debug" checked onchange="renderLogs()"> Debug</label>
<label><input type="checkbox" id="lf-info" checked onchange="renderLogs()"> Info</label>
<label><input type="checkbox" id="lf-warn" checked onchange="renderLogs()"> Warn</label>
<label><input type="checkbox" id="lf-error" checked onchange="renderLogs()"> Error</label>
<input type="text" id="log-filter" placeholder="Filter…" oninput="renderLogs()" style="width:150px">
<button onclick="logs=[];renderLogs()" style="margin-left:auto">Clear</button>
<label><input type="checkbox" id="log-autoscroll" checked> Auto-scroll</label>
</div>
<div id="log-body"></div>
</div><!-- #log-panel -->
</div><!-- #body -->
<!-- ── Stats panel ─────────────────────────────────────────── -->
<div id="stats-panel">
<div id="stats-panel-hdr">
<span class="stats-hdr-label">Source Statistics</span>
<select id="stats-source-sel" class="stats-source-sel"></select>
<button id="btn-stats-close"></button>
</div>
<div id="stats-body"></div>
</div>
<div id="layout-menu"></div>
<!-- ── Signal style context menu ─────────────────────────────── -->
<div id="sig-ctx-menu" style="display:none">
<div class="ctx-menu-header">Style:
<span id="ctx-menu-key" class="ctx-menu-key"></span></div>
<div class="ctx-row">
<label>Color</label>
<input type="color" id="ctx-color">
</div>
<div class="ctx-row">
<label>Width</label>
<div class="ctx-btns" id="ctx-width-btns">
<button class="ctx-btn" data-w="1">1px</button>
<button class="ctx-btn active" data-w="1.5">1.5</button>
<button class="ctx-btn" data-w="2">2px</button>
<button class="ctx-btn" data-w="3">3px</button>
</div>
</div>
<div class="ctx-row">
<label>Line</label>
<div class="ctx-btns" id="ctx-dash-btns">
<button class="ctx-btn active" data-dash="solid">——</button>
<button class="ctx-btn" data-dash="dashed">╌╌</button>
<button class="ctx-btn" data-dash="dotted">·····</button>
</div>
</div>
<div class="ctx-row">
<label>Marker</label>
<div class="ctx-btns" id="ctx-marker-btns">
<button class="ctx-btn active" data-marker="none">none</button>
<button class="ctx-btn" data-marker="circle"></button>
<button class="ctx-btn" data-marker="square"></button>
<button class="ctx-btn" data-marker="cross"></button>
<button class="ctx-btn" data-marker="diamond"></button>
</div>
</div>
<div class="ctx-row">
<label>Size</label>
<input type="range" class="ctx-range" id="ctx-marker-size" min="2" max="10" value="4">
<span class="ctx-range-val" id="ctx-marker-size-val">4px</span>
</div>
</div>
<!-- ── Array index picker (trigger signal) ──────────────────── -->
<div id="array-idx-picker" style="display:none">
<div class="ctx-menu-header">Element index: <span id="aip-sig" class="ctx-menu-key"></span></div>
<div class="ctx-row">
<label>Index</label>
<input type="number" id="aip-idx" class="ctx-num" min="0" step="1" value="0">
<span id="aip-range" style="font-size:10px;color:var(--overlay0)"></span>
</div>
<div class="ctx-row" style="justify-content:flex-end;gap:6px">
<button class="ctx-btn" id="aip-cancel">Cancel</button>
<button class="ctx-btn active" id="aip-ok">OK</button>
</div>
</div>
<!-- ── VScale toolbar (moved into plot card when active) ─────── -->
<div id="vscale-menu" style="display:none">
<div class="vstb-header">
<span class="vstb-label">V-Scale: <span id="vscale-menu-key" class="ctx-menu-key"></span></span>
<div class="ctx-btns" id="vscale-mode-btns">
<button class="ctx-btn active" data-mode="auto">Auto</button>
<button class="ctx-btn" data-mode="range">Range</button>
<button class="ctx-btn" data-mode="manual">Manual</button>
</div>
<div id="vscale-manual-row" style="display:none;align-items:center;gap:4px">
<label class="vstb-lbl">V/div</label>
<input type="number" id="vscale-vdiv" class="ctx-num" min="1e-30" step="any" value="1">
</div>
<div id="vscale-pos-row" style="display:none;align-items:center;gap:4px">
<label class="vstb-lbl">Pos</label>
<input type="number" id="vscale-pos" class="ctx-num" step="0.1" value="0">
</div>
<div id="vscale-type-row" style="display:none;align-items:center;gap:4px">
<label class="vstb-lbl">Type</label>
<div class="ctx-btns" id="vscale-type-btns">
<button class="ctx-btn active" data-type="analog">Analog</button>
<button class="ctx-btn" data-type="digital">Digital</button>
</div>
</div>
<button id="btn-vscale-close" class="vstb-close" title="Close"></button>
</div>
</div>
<!-- ═══ DIALOGS ═══ -->
<!-- Force dialog -->
<div class="dialog-overlay" id="dlg-force" style="display:none" onclick="if(event.target===this)closeDlg('dlg-force')">
<div class="dialog">
<h3>Force Signal</h3>
<label>Signal</label>
<input id="force-sig" list="force-sig-list" placeholder="Signal path…">
<datalist id="force-sig-list"></datalist>
<label>Value</label>
<input id="force-val" placeholder="e.g. 42">
<div class="btns">
<button onclick="closeDlg('dlg-force')">Cancel</button>
<button class="active" onclick="doForce()">Force</button>
</div>
</div>
</div>
<!-- Unforce confirm -->
<div class="dialog-overlay" id="dlg-unforce" style="display:none" onclick="if(event.target===this)closeDlg('dlg-unforce')">
<div class="dialog">
<h3>Remove Force</h3>
<p id="unforce-msg" style="margin-bottom:12px;color:#cdd6f4"></p>
<div class="btns">
<button onclick="closeDlg('dlg-unforce')">Cancel</button>
<button class="danger" onclick="doUnforce()">Unforce</button>
</div>
</div>
</div>
<!-- Break dialog -->
<div class="dialog-overlay" id="dlg-break" style="display:none" onclick="if(event.target===this)closeDlg('dlg-break')">
<div class="dialog">
<h3>Set Breakpoint</h3>
<label>Signal</label>
<input id="break-sig" list="break-sig-list" placeholder="Signal path…">
<datalist id="break-sig-list"></datalist>
<div class="form-row">
<div>
<label>Operator</label>
<select id="break-op">
<option>></option><option>&gt;=</option>
<option>&lt;</option><option>&lt;=</option>
<option>==</option><option>!=</option>
</select>
</div>
<div>
<label>Threshold</label>
<input id="break-thresh" placeholder="0">
</div>
</div>
<div class="btns">
<button onclick="closeDlg('dlg-break')">Cancel</button>
<button class="active" onclick="doBreak()">Set Break</button>
</div>
</div>
</div>
<!-- Message dialog -->
<div class="dialog-overlay" id="dlg-msg" style="display:none" onclick="if(event.target===this)closeDlg('dlg-msg')">
<div class="dialog">
<h3>Send Message</h3>
<label>Destination</label>
<input id="msg-dest" list="msg-dest-list" placeholder="e.g. App.Functions.GAM1">
<datalist id="msg-dest-list"></datalist>
<label>Function</label>
<input id="msg-func" placeholder="FunctionName">
<label>Payload (key=value lines)</label>
<textarea id="msg-payload" placeholder="Key = Value&#10;Key2 = Value2"></textarea>
<div class="form-check">
<input type="checkbox" id="msg-wait">
<label for="msg-wait">Wait for reply</label>
</div>
<div class="btns">
<button onclick="closeDlg('dlg-msg')">Cancel</button>
<button class="active" onclick="doSendMsg()">Send</button>
</div>
</div>
</div>
<!-- Array signal dialog (Trace / Force / Break with index/range selection) -->
<div class="dialog-overlay" id="dlg-array" style="display:none" onclick="if(event.target===this)closeDlg('dlg-array')">
<div class="dialog" style="min-width:360px">
<h3 id="arr-dlg-title">Array Signal</h3>
<p style="font-size:11px;color:#a6adc8;margin-bottom:14px">
<b id="arr-dlg-sig" style="color:#cdd6f4"></b>
&nbsp;·&nbsp;<span id="arr-dlg-n" style="color:#89b4fa"></span> elements
</p>
<label style="margin-bottom:6px">Apply to</label>
<div class="arr-seg">
<button id="arr-sel-all" class="active" onclick="setArrSel('all')">All</button>
<button id="arr-sel-idx" onclick="setArrSel('idx')">Index</button>
<button id="arr-sel-rng" onclick="setArrSel('rng')">Range</button>
</div>
<div id="arr-idx-sect" style="display:none">
<label>Element index &nbsp;<span style="color:#585b70;font-weight:normal">(0 <span id="arr-idx-max"></span>)</span></label>
<input id="arr-idx" type="number" min="0" value="0">
</div>
<div id="arr-rng-sect" style="display:none">
<div class="form-row">
<div>
<label>From index</label>
<input id="arr-r0" type="number" min="0" value="0">
</div>
<div>
<label>To index <span style="color:#585b70;font-weight:normal">inclusive</span></label>
<input id="arr-r1" type="number" min="0" value="0">
</div>
</div>
</div>
<div id="arr-force-sect" style="display:none">
<label>Force value</label>
<input id="arr-force-val" placeholder="e.g. 0">
</div>
<div id="arr-break-sect" style="display:none">
<div class="form-row">
<div>
<label>Condition</label>
<select id="arr-break-op">
<option>></option><option>>=</option>
<option>&lt;</option><option>&lt;=</option>
<option>==</option><option>!=</option>
</select>
</div>
<div>
<label>Threshold</label>
<input id="arr-break-thr" placeholder="0">
</div>
</div>
</div>
<div class="btns">
<button onclick="closeDlg('dlg-array')">Cancel</button>
<button id="arr-ok-btn" class="active" onclick="doArrayOp()">Apply</button>
</div>
</div>
</div>
<!-- Array → plot mode dialog (Sequential / Waterfall) -->
<div class="dialog-overlay" id="dlg-arr-plot" style="display:none" onclick="if(event.target===this)closeDlg('dlg-arr-plot')">
<div class="dialog" style="min-width:360px">
<h3>Plot Array Signal</h3>
<p style="font-size:11px;color:#a6adc8;margin-bottom:14px">
<b id="arrp-name" style="color:#cdd6f4"></b>
&nbsp;·&nbsp;<span id="arrp-n" style="color:#89b4fa"></span> elements
</p>
<div class="arr-seg" style="margin-bottom:8px">
<button id="arrp-sel-sequential" class="active" onclick="setArrpMode('sequential')">Sequential</button>
<button id="arrp-sel-waterfall" onclick="setArrpMode('waterfall')">Waterfall</button>
</div>
<p id="arrp-desc" style="font-size:10px;color:#a6adc8;margin-bottom:16px;min-height:28px"></p>
<div class="btns">
<button onclick="closeDlg('dlg-arr-plot')">Cancel</button>
<button class="active" onclick="doArrayPlotMode()">Add</button>
</div>
</div>
</div>
<!-- Info dialog -->
<div class="dialog-overlay" id="dlg-info" style="display:none" onclick="if(event.target===this)closeDlg('dlg-info')">
<div class="dialog" style="max-width:600px;width:90vw">
<h3 id="info-title">Info</h3>
<pre id="info-body" style="background:#11111b;padding:8px;border-radius:4px;overflow:auto;max-height:400px;font-size:11px;color:#cdd6f4;white-space:pre-wrap"></pre>
<div class="btns" style="margin-top:12px">
<button onclick="closeDlg('dlg-info')">Close</button>
</div>
</div>
</div>
<!-- Step dialog -->
<div class="dialog-overlay" id="dlg-step" style="display:none" onclick="if(event.target===this)closeDlg('dlg-step')">
<div class="dialog">
<h3>Step Execution</h3>
<div class="form-row">
<div>
<label>Count</label>
<input type="number" id="step-count" value="1" min="1">
</div>
<div>
<label>Thread (optional)</label>
<input id="step-thread-inp" list="step-thread-list" placeholder="all">
<datalist id="step-thread-list"></datalist>
</div>
</div>
<div class="btns">
<button onclick="closeDlg('dlg-step')">Cancel</button>
<button class="active" onclick="doStep()">Step</button>
</div>
</div>
</div>
<script src="/app.js"></script>
</body>
</html>
+39
View File
@@ -0,0 +1,39 @@
'use strict';
// LTTB (Largest Triangle Three Buckets) decimation — O(n).
// Runs off-main-thread to avoid blocking the render loop.
function lttb(t, v, threshold) {
const len = t.length;
if (len <= threshold) {
// Copy to new arrays so we can transfer them back without detaching the input.
return { t: new Float64Array(t), v: new Float64Array(v) };
}
const outT = new Float64Array(threshold);
const outV = new Float64Array(threshold);
outT[0] = t[0]; outV[0] = v[0];
outT[threshold - 1] = t[len - 1]; outV[threshold - 1] = v[len - 1];
const every = (len - 2) / (threshold - 2);
let a = 0;
for (let i = 0; i < threshold - 2; i++) {
const avgS = Math.floor((i + 1) * every) + 1;
const avgE = Math.min(Math.floor((i + 2) * every) + 1, len);
let avgT = 0, avgV = 0, n = 0;
for (let j = avgS; j < avgE; j++) { avgT += t[j]; avgV += v[j]; n++; }
if (n) { avgT /= n; avgV /= n; }
const rS = Math.floor(i * every) + 1;
const rE = Math.min(Math.floor((i + 1) * every) + 1, len);
let maxA = -1, next = rS;
const aT = t[a], aV = v[a];
for (let j = rS; j < rE; j++) {
const area = Math.abs((aT - avgT) * (v[j] - aV) - (aT - t[j]) * (avgV - aV));
if (area > maxA) { maxA = area; next = j; }
}
outT[i + 1] = t[next]; outV[i + 1] = v[next]; a = next;
}
return { t: outT, v: outV };
}
self.onmessage = function({ data: { id, t, v, threshold } }) {
const result = lttb(t, v, threshold);
// Transfer the output buffers back to the main thread zero-copy.
self.postMessage({ id, t: result.t, v: result.v }, [result.t.buffer, result.v.buffer]);
};
+711
View File
@@ -0,0 +1,711 @@
/* ── Catppuccin Mocha palette ──────────────────────────────── */
:root {
--bg: #1e1e2e; --mantle: #181825; --crust: #11111b;
--surface0: #313244; --surface1: #45475a; --surface2: #585b70;
--overlay0: #6c7086; --overlay1: #7f849c; --text: #cdd6f4;
--subtext0: #a6adc8; --subtext1: #bac2de;
--accent: #89b4fa; --green: #a6e3a1; --red: #f38ba8;
--yellow: #f9e2af; --peach: #fab387; --mauve: #cba6f7;
--teal: #94e2d5; --sky: #89dceb; --lavender: #b4befe;
--pink: #f5c2e7;
--radius: 8px; --sidebar-w: 280px; --topbar-h: 52px;
--trigbar-h: 0px; --statusbar-h: 0px; --transition: 0.18s ease;
--border: var(--surface0); --fg: var(--text); --fg2: var(--subtext0);
--bg2: var(--mantle); --bg3: var(--surface0);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height:100%; background:var(--bg); color:var(--text);
font-family:'Segoe UI',system-ui,sans-serif; font-size:14px; overflow:hidden; }
::-webkit-scrollbar { width:6px; }
::-webkit-scrollbar-track { background:var(--mantle); }
::-webkit-scrollbar-thumb { background:var(--surface1); border-radius:3px; }
/* ── Global element overrides ────────────────────────────── */
button { background:var(--surface0); border:1px solid var(--surface1); color:var(--text); padding:2px 8px; border-radius:4px; cursor:pointer; white-space:nowrap; }
button:hover { background:var(--surface1); }
button.active { background:var(--accent); color:var(--crust); border-color:var(--accent); }
button.danger { background:var(--red); color:var(--crust); border-color:var(--red); }
button.warn { background:var(--peach); color:var(--crust); border-color:var(--peach); }
button.ok { background:var(--green); color:var(--crust); border-color:var(--green); }
select { background:var(--surface0); border:1px solid var(--surface1); color:var(--text); padding:2px 4px; border-radius:4px; outline:none; cursor:pointer; }
select:hover { border-color:var(--accent); }
input[type=text], input[type=number] {
background:var(--surface0); border:1px solid var(--surface1); color:var(--text);
padding:2px 6px; border-radius:4px; outline:none;
}
input[type=text]:focus, input[type=number]:focus { border-color:var(--accent); }
/* ── uPlot overrides ─────────────────────────────────────────── */
.uplot { background:transparent !important; }
.uplot .u-over { cursor: crosshair; }
.uplot .u-cursor-x, .uplot .u-cursor-y { border-color: #585b70 !important; }
.uplot .u-select { background: rgba(137,180,250,0.1) !important; border: 1px solid rgba(137,180,250,0.4) !important; }
/* ── Top bar ─────────────────────────────────────────────────── */
#topbar {
position:fixed; top:0; left:0; right:0; height:var(--topbar-h);
background:var(--mantle); border-bottom:1px solid var(--surface0);
display:flex; align-items:center; gap:10px; padding:0 14px;
z-index:100; box-shadow:0 2px 8px rgba(0,0,0,0.4);
}
#app-title { font-weight:700; font-size:15px; color:var(--accent);
white-space:nowrap; letter-spacing:0.3px; flex-shrink:0; }
.topbar-sep { flex:1; min-width:4px; }
#status-led { width:8px; height:8px; border-radius:50%;
background:var(--red); flex-shrink:0; transition:background var(--transition); }
#status-led.green { background:var(--green); animation:pulse-green 2s infinite; }
#status-led.orange { background:var(--yellow); animation:pulse-orange 1.5s infinite; }
@keyframes pulse-green { 0%,100%{box-shadow:0 0 0 0 rgba(166,227,161,0.4)} 50%{box-shadow:0 0 0 4px rgba(166,227,161,0)} }
@keyframes pulse-orange { 0%,100%{box-shadow:0 0 0 0 rgba(249,226,175,0.4)} 50%{box-shadow:0 0 0 4px rgba(249,226,175,0)} }
#status-text { font-size:11px; color:var(--subtext0); white-space:nowrap; }
#sb-tsage { font-size:11px; color:var(--overlay0); white-space:nowrap; font-family:monospace; }
#cursor-readout {
display:none; align-items:center; gap:8px;
font-size:11px; font-family:monospace;
background:var(--surface0); border:1px solid var(--surface1);
border-radius:5px; padding:3px 8px; white-space:nowrap; flex-shrink:0;
}
#cursor-readout.visible { display:flex; }
#cur-ta { color:var(--sky); } #cur-tb { color:var(--yellow); }
#cur-dt { color:var(--subtext1); } .cur-sep { color:var(--surface2); }
.topbar-vsep { width:1px; height:22px; background:var(--surface0); flex-shrink:0; margin:0 2px; }
#layout-btns { display:flex; gap:2px; align-items:center; flex-shrink:0; }
.ctrl-label { font-size:12px; color:var(--subtext0); white-space:nowrap; flex-shrink:0; }
select.ctrl-select {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:var(--radius);
padding:4px 6px; font-size:12px; cursor:pointer; outline:none; flex-shrink:0;
}
select.ctrl-select:hover { border-color:var(--accent); }
button.ctrl-btn {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:var(--radius);
padding:4px 12px; font-size:12px; cursor:pointer; white-space:nowrap; flex-shrink:0;
transition:background var(--transition),border-color var(--transition);
}
button.ctrl-btn:hover { background:var(--surface1); border-color:var(--accent); }
button.ctrl-btn.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
button.ctrl-btn.trig-active { background:rgba(203,166,247,0.15); border-color:var(--mauve); color:var(--mauve); }
button.ctrl-btn.cursor-a { border-color:var(--sky); color:var(--sky); }
button.ctrl-btn.cursor-b { border-color:var(--yellow); color:var(--yellow); }
button.ctrl-btn.resume-btn { border-color:var(--teal); color:var(--teal); }
/* ── Connection dropdown (debug menu) ─────────────────────── */
.menu-wrap { position:relative; }
.dropdown {
position:absolute; top:calc(100% + 4px); left:0; z-index:300;
background:var(--mantle); border:1px solid var(--surface1);
border-radius:var(--radius); box-shadow:0 6px 20px rgba(0,0,0,.5);
padding:6px; min-width:280px; display:none; flex-direction:column; gap:6px;
}
.dropdown.open { display:flex; }
.menu-row { display:flex; align-items:center; gap:6px; }
.menu-row input[type=text], .menu-row input[type=number] { font-size:11px; }
.menu-row label { font-size:11px; }
.menu-btn-row { display:flex; gap:4px; }
.menu-btn-row button { flex:1; font-size:11px; }
.menu-sep { border:none; border-top:1px solid var(--surface1); margin:2px 0; }
#conn-status { width:8px; height:8px; border-radius:50%; background:var(--red); display:inline-block; flex-shrink:0; vertical-align:middle; }
#conn-status.ok { background:var(--green); }
/* ── Trigger bar ──────────────────────────────────────────────── */
#trigbar {
position:fixed; top:var(--topbar-h); left:0; right:0;
background:var(--crust); border-bottom:1px solid var(--surface0);
display:flex; align-items:center; flex-wrap:wrap; gap:10px;
padding:0 16px; z-index:99;
height:0; overflow:hidden;
transition:height var(--transition),padding var(--transition);
}
#trigbar.open { height:48px; padding:0 16px; }
.trig-group { display:flex; align-items:center; gap:6px; }
.trig-sep { width:1px; height:24px; background:var(--surface0); flex-shrink:0; }
.trig-label { font-size:11px; color:var(--subtext0); white-space:nowrap; }
select.trig-select {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:5px;
padding:3px 6px; font-size:12px; cursor:pointer; outline:none;
}
select.trig-select:hover { border-color:var(--mauve); }
input.trig-input {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:5px;
padding:3px 6px; font-size:12px; outline:none; width:80px;
}
input.trig-input:focus { border-color:var(--mauve); }
input[type=range].trig-range {
-webkit-appearance:none; width:90px; height:4px;
background:var(--surface1); border-radius:2px; outline:none; cursor:pointer;
}
input[type=range].trig-range::-webkit-slider-thumb {
-webkit-appearance:none; width:12px; height:12px;
border-radius:50%; background:var(--mauve); cursor:pointer;
}
.trig-range-val { font-size:11px; color:var(--mauve); min-width:28px; }
#trig-status-badge {
font-size:11px; font-weight:700; letter-spacing:0.8px;
padding:3px 10px; border-radius:12px;
border:1px solid var(--surface1); background:var(--surface0); color:var(--subtext0);
min-width:80px; text-align:center; white-space:nowrap;
}
#trig-status-badge.armed { background:rgba(166,227,161,0.12); border-color:var(--green); color:var(--green); }
#trig-status-badge.waiting { background:rgba(249,226,175,0.12); border-color:var(--yellow); color:var(--yellow); }
#trig-status-badge.triggered { background:rgba(203,166,247,0.15); border-color:var(--mauve); color:var(--mauve); }
#btn-trig-rearm, #btn-trig-stop {
border:none; border-radius:5px;
padding:4px 12px; font-size:12px; font-weight:600; cursor:pointer; display:none;
}
#btn-trig-rearm { background:var(--mauve); color:var(--crust); }
#btn-trig-stop { background:var(--surface1); color:var(--yellow); border:1px solid var(--yellow); }
#btn-trig-rearm:hover, #btn-trig-stop:hover { opacity:0.85; }
/* ── Body: outer flex-column container ───────────────────── */
#body {
position:fixed;
top:calc(var(--topbar-h) + var(--trigbar-h));
left:0; right:0; bottom:0;
display:flex; flex-direction:column; overflow:hidden;
transition:top var(--transition);
}
/* ── Step status bar (inside #body, at top) ──────────────── */
#step-bar {
background:rgba(45,43,69,0.95); border-bottom:1px solid var(--surface0);
padding:4px 8px; display:none; align-items:center; gap:8px;
flex-shrink:0; font-size:11px;
}
#step-bar.visible { display:flex; }
#step-bar button { font-size:11px; }
#step-bar button.ok { background:var(--green); color:var(--crust); border-color:var(--green); }
/* ── Inner horizontal row: sidebar | strip | main | strip | right-panel */
#body-row {
display:flex; flex-direction:row; flex:1; min-height:0; overflow:hidden;
}
/* ── Sidebar ──────────────────────────────────────────────────── */
#sidebar {
width:var(--sidebar-w); min-width:var(--sidebar-w);
background:var(--mantle); border-right:1px solid var(--surface0);
display:flex; flex-direction:column;
transition:width var(--transition),min-width var(--transition); overflow:hidden;
}
#sidebar.collapsed { width:0; min-width:0; }
#sidebar-header {
display:flex; align-items:center; justify-content:space-between;
padding:10px 14px; border-bottom:1px solid var(--surface0);
font-weight:600; color:var(--subtext1); font-size:12px;
text-transform:uppercase; letter-spacing:0.8px; flex-shrink:0;
}
/* ── Sidebar dual-tab (Signals / Object Tree) ─────────────── */
.panel-tabs {
display:flex; border-bottom:1px solid var(--surface0); flex-shrink:0;
}
.panel-tab {
flex:1; padding:4px 6px; text-align:center; cursor:pointer;
color:var(--subtext0); font-size:11px; font-weight:500;
border-bottom:2px solid transparent;
transition:color var(--transition), border-color var(--transition);
}
.panel-tab:hover { color:var(--subtext1); }
.panel-tab.active { color:var(--accent); border-bottom-color:var(--accent); }
.sidebar-tab-body { display:none; flex:1; flex-direction:column; overflow:hidden; }
.sidebar-tab-body.active { display:flex; }
.panel-search { padding:4px; flex-shrink:0; border-bottom:1px solid var(--surface0); }
.panel-search input { width:100%; font-size:11px; }
.panel-body { flex:1; overflow-y:auto; padding:4px; }
/* ── Signal list ──────────────────────────────────────────── */
#signal-list { flex:1; overflow-y:auto; padding:8px 0; }
.sig-item {
padding:6px 14px; cursor:grab; border-radius:6px; margin:1px 6px;
transition:background var(--transition); display:flex; align-items:center; gap:8px;
user-select:none;
}
.sig-item:hover { background:var(--surface0); }
.sig-item:active { cursor:grabbing; }
.sig-item.dragging { opacity:0.4; }
.sig-name { flex:1; font-size:13px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sig-unit { font-size:11px; color:var(--subtext0); font-style:italic; }
.type-badge { font-size:10px; background:var(--surface1); color:var(--subtext1); padding:1px 5px; border-radius:3px; white-space:nowrap; }
.array-group {}
.array-header {
padding:6px 14px 6px 10px; cursor:pointer; border-radius:6px; margin:1px 6px;
transition:background var(--transition); display:flex; align-items:center; gap:6px; user-select:none;
}
.array-header:hover { background:var(--surface0); }
.array-arrow { font-size:10px; color:var(--subtext0); transition:transform var(--transition); display:inline-block; }
.array-header.open .array-arrow { transform:rotate(90deg); }
.array-children { display:none; padding-left:16px; }
.array-header.open + .array-children { display:block; }
.array-child {
padding:4px 14px 4px 8px; cursor:grab; border-radius:6px; margin:1px 6px;
transition:background var(--transition); display:flex; align-items:center; gap:8px;
user-select:none; color:var(--subtext1); font-size:12px;
}
.array-child:hover { background:var(--surface0); }
.array-child:active { cursor:grabbing; }
/* ── Resize / collapse strips ─────────────────────────────── */
.panel-strip {
width:6px; flex-shrink:0; cursor:col-resize;
background:var(--mantle); border:none; position:relative;
transition:background 0.1s;
}
.panel-strip::after {
content:''; position:absolute; top:50%; left:50%;
transform:translate(-50%,-50%); width:2px; height:32px;
background:var(--surface1); border-radius:1px; pointer-events:none;
}
.panel-strip:hover { background:var(--surface0); }
.panel-strip:hover::after { background:var(--accent); }
/* ── Main area ────────────────────────────────────────────────── */
#main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
/* Layout toggle button in topbar */
#btn-layout { display:flex; align-items:center; gap:4px; font-size:11px; padding:3px 8px; }
#btn-layout svg { flex-shrink:0; }
#btn-layout span { flex-shrink:0; }
.layout-toggle { font-size:11px; }
/* Layout dropdown menu */
#layout-menu {
position:fixed; z-index:200;
background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius);
box-shadow:0 6px 20px rgba(0,0,0,0.5);
display:none; grid-template-columns:1fr 1fr; gap:4px; padding:6px;
}
#layout-menu.open { display:grid; }
.layout-menu-item {
display:flex; flex-direction:column; align-items:center; gap:3px;
padding:5px 10px; cursor:pointer;
background:var(--surface0); border:1px solid var(--surface1); border-radius:6px;
color:var(--subtext1); font-size:10px; font-family:monospace;
transition:background var(--transition),border-color var(--transition),color var(--transition);
}
.layout-menu-item:hover { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
.layout-menu-item.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
/* ── Plot grid ────────────────────────────────────────────────── */
#plot-grid {
flex:1; min-height:0; display:grid; gap:0; padding:0; overflow:hidden;
border-top:1px solid var(--surface0); border-left:1px solid var(--surface0);
position:relative;
}
.resize-handle-v {
position:absolute; top:0; bottom:0; width:6px; cursor:col-resize; z-index:20;
transform:translateX(-50%); background:transparent; transition:background 0.15s;
}
.resize-handle-h {
position:absolute; left:0; right:0; height:6px; cursor:row-resize; z-index:20;
transform:translateY(-50%); background:transparent; transition:background 0.15s;
}
.resize-handle-v:hover,.resize-handle-v.dragging,
.resize-handle-h:hover,.resize-handle-h.dragging {
background:rgba(137,180,250,0.35);
}
#plot-grid.l1x1 { grid-template-columns:1fr; grid-template-rows:1fr; }
#plot-grid.l2x1 { grid-template-columns:1fr 1fr; grid-template-rows:1fr; }
#plot-grid.l1x2 { grid-template-columns:1fr; grid-template-rows:1fr 1fr; }
#plot-grid.l2x2 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; }
#plot-grid.l3x1 { grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr; }
#plot-grid.l1x3 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr; }
#plot-grid.l3x2 { grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr 1fr; }
#plot-grid.l2x3 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr 1fr; }
#plot-grid.l1x4 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr 1fr; }
#plot-grid.l4x1 { grid-template-columns:1fr 1fr 1fr 1fr; grid-template-rows:1fr; }
/* ── Plot card ────────────────────────────────────────────────── */
.plot-card {
background:var(--bg);
border-right:1px solid var(--surface0); border-bottom:1px solid var(--surface0);
border-radius:0; display:flex; flex-direction:column;
min-height:0; position:relative; overflow:hidden;
}
.plot-card.drag-over { background:rgba(137,180,250,0.04); box-shadow:inset 0 0 0 2px var(--accent); }
.plot-card-header {
z-index:5; flex-shrink:0;
background:rgba(17,17,27,0.88); backdrop-filter:blur(6px);
border-bottom:1px solid var(--surface1);
display:flex; align-items:center; gap:5px;
padding:3px 8px; min-height:26px; overflow:hidden;
}
.plot-title {
font-size:11px; color:var(--subtext1); font-weight:600;
cursor:pointer; border:1px solid transparent; border-radius:3px;
padding:1px 4px; background:transparent; white-space:nowrap; user-select:none;
flex-shrink:0; max-width:100px; overflow:hidden; text-overflow:ellipsis;
transition:border-color 0.15s, background 0.15s;
}
.plot-title:hover { border-color:var(--surface1); background:rgba(88,91,112,0.4); }
.plot-cfg-bar {
flex-shrink:0; background:rgba(17,17,27,0.92); border-top:1px solid var(--surface1);
padding:3px 8px;
}
.sig-badges { display:flex; flex-wrap:nowrap; gap:3px; flex:1; overflow:hidden; min-width:0; }
.sig-badge {
display:inline-flex; align-items:center; gap:3px;
background:rgba(69,71,90,0.6); color:var(--subtext1);
border-radius:10px; padding:1px 6px 1px 4px;
font-size:10px; white-space:nowrap; flex-shrink:0; cursor:pointer;
}
.trace-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; display:inline-block; }
.sig-badge-x {
cursor:pointer; color:var(--overlay0); font-size:11px; line-height:1; margin-left:1px;
transition:color var(--transition);
}
.sig-badge-x:hover { color:var(--red); }
.sig-badge-active { outline:1px solid rgba(255,255,255,0.35); background:rgba(88,91,112,0.9); }
.sig-badge-active .vscale-info { color:var(--subtext0); }
.plot-body { flex:1; position:relative; min-height:0; overflow:hidden; }
.drop-hint {
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
color:var(--overlay0); font-size:13px; pointer-events:none;
}
.trig-collect-overlay {
position:absolute; inset:0; display:none;
align-items:center; justify-content:center; pointer-events:none; z-index:10;
}
.plot-card.trig-collecting .trig-collect-overlay { display:flex; }
.trig-collect-text {
background:rgba(49,50,68,0.82); color:var(--mauve);
font-size:11px; font-weight:600; padding:4px 12px; border-radius:20px;
border:1px solid var(--mauve);
}
/* ── Signal style context menu ────────────────────────────────── */
#sig-ctx-menu {
position:fixed; z-index:300;
background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius);
box-shadow:0 8px 24px rgba(0,0,0,0.6); padding:10px; min-width:210px;
}
.ctx-menu-header {
font-size:11px; color:var(--subtext0); margin-bottom:8px;
padding-bottom:6px; border-bottom:1px solid var(--surface0);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.ctx-menu-key { color:var(--accent); font-weight:600; }
.ctx-row { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.ctx-row label { font-size:11px; color:var(--subtext0); width:42px; flex-shrink:0; }
.ctx-btns { display:flex; gap:3px; flex-wrap:wrap; }
.ctx-btn {
background:var(--surface0); border:1px solid var(--surface1); border-radius:4px;
color:var(--subtext1); font-size:11px; padding:2px 7px; cursor:pointer;
transition:background var(--transition),border-color var(--transition),color var(--transition);
}
.ctx-btn:hover { background:var(--surface1); border-color:var(--accent); }
.ctx-btn.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
#ctx-color {
width:28px; height:22px; border:1px solid var(--surface1); border-radius:4px;
background:transparent; cursor:pointer; padding:1px;
}
.ctx-range { width:80px; }
.ctx-range-val { font-size:11px; color:var(--mauve); min-width:26px; }
.ctx-num {
width:90px; background:var(--surface0); border:1px solid var(--surface1); border-radius:4px;
color:var(--text); font-size:11px; padding:2px 6px;
}
.ctx-num:focus { outline:none; border-color:var(--accent); }
.ctx-btn:disabled { opacity:0.35; cursor:not-allowed; border-color:var(--surface1); }
/* ── Array index picker ─────────────────────────────────────────── */
#array-idx-picker {
position:fixed; z-index:300;
background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius);
box-shadow:0 8px 24px rgba(0,0,0,0.6); padding:10px; min-width:200px;
}
/* ── VScale toolbar (embedded in plot card) ──────────────────────── */
#vscale-menu {
flex-shrink:0; background:rgba(17,17,27,0.92); border-top:1px solid var(--surface1);
padding:3px 8px;
}
.vstb-header {
display:flex; align-items:center; gap:8px; flex-wrap:nowrap; overflow-x:auto;
scrollbar-width:none;
}
.vstb-header::-webkit-scrollbar { display:none; }
.vstb-label { font-size:11px; color:var(--subtext0); white-space:nowrap; flex-shrink:0; }
.vstb-lbl { font-size:10px; color:var(--overlay0); white-space:nowrap; }
.vstb-close {
margin-left:auto; flex-shrink:0;
background:transparent; border:none; color:var(--overlay0);
cursor:pointer; font-size:11px; padding:0 3px; line-height:1;
transition:color var(--transition);
}
.vstb-close:hover { color:var(--red); }
.plot-vscale-bar { display:none; }
/* ── Per-plot cursor value readout ────────────────────────────── */
.plot-cursor-ro {
margin-left:auto; flex-shrink:0;
align-items:center; gap:5px;
font-size:10px; font-family:monospace;
background:var(--surface0); border:1px solid var(--surface1);
border-radius:4px; padding:1px 7px; white-space:nowrap; overflow:hidden;
}
.pcur-a { color:var(--sky); }
.pcur-b { color:var(--yellow); }
.pcur-dv { color:var(--subtext1); }
.pcur-sep { color:var(--surface2); }
/* ── Badge vscale info & active state ───────────────────────────── */
.vscale-info {
font-size:9px; color:var(--overlay0); font-family:monospace; margin-left:2px; white-space:nowrap;
}
/* ── Source groups ────────────────────────────────────────────── */
.source-group { margin-bottom:2px; }
.source-group-header {
display:flex; align-items:center; gap:5px;
padding:5px 8px 5px 10px; margin:4px 6px 2px;
background:var(--surface0); border-radius:6px;
}
.source-state-dot {
width:7px; height:7px; border-radius:50%; flex-shrink:0;
background:var(--overlay0); transition:background var(--transition);
}
.source-state-dot.connected { background:var(--green); }
.source-state-dot.connecting { background:var(--yellow); animation:pulse-orange 1.5s infinite; }
.source-state-dot.disconnected { background:var(--red); }
.source-name {
font-size:11px; font-weight:600; color:var(--subtext1); flex:1;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.source-addr {
font-size:10px; color:var(--overlay0); font-family:monospace;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:90px;
}
.source-remove-btn {
background:none; border:none; color:var(--overlay0);
cursor:pointer; font-size:15px; line-height:1; padding:0 2px; flex-shrink:0;
transition:color var(--transition);
}
.source-remove-btn:hover { color:var(--red); }
/* ── Add source section ───────────────────────────────────────── */
.add-source-section { border-top:1px solid var(--surface0); margin-top:4px; }
.add-source-title {
display:flex; align-items:center; gap:5px;
padding:6px 10px; cursor:pointer; user-select:none;
font-size:11px; color:var(--overlay0);
transition:color var(--transition);
}
.add-source-title:hover { color:var(--subtext1); }
.add-src-arrow { font-size:9px; display:inline-block; transition:transform var(--transition); }
.add-source-body { display:none; flex-direction:column; gap:5px; padding:0 10px 10px; }
.add-source-section.open .add-source-body { display:flex; }
.add-src-input {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:5px;
padding:4px 8px; font-size:12px; outline:none; width:100%;
}
.add-src-input:focus { border-color:var(--accent); }
.add-src-btn {
background:var(--surface0); color:var(--accent);
border:1px solid var(--surface1); border-radius:5px;
padding:4px 10px; font-size:12px; cursor:pointer; width:100%;
transition:background var(--transition),border-color var(--transition);
}
.add-src-btn:hover { background:rgba(137,180,250,0.15); border-color:var(--accent); }
.save-src-btn { color:var(--green); }
.save-src-btn:hover { background:rgba(166,227,161,0.1); border-color:var(--green); }
/* ── Right panel (debug: Traced/Forced/Breaks/Msgs) ────────── */
#right-panel {
width:260px; min-width:180px; max-width:420px;
display:flex; flex-direction:column;
border-left:1px solid var(--surface0); overflow:hidden;
background:var(--mantle);
}
#right-panel.collapsed { width:0; min-width:0; border:none; overflow:hidden; }
.tabs { display:flex; border-bottom:1px solid var(--surface0); flex-shrink:0; }
.tab {
flex:1; padding:4px; text-align:center; cursor:pointer;
color:var(--overlay0); font-size:11px;
border-bottom:2px solid transparent;
transition:color var(--transition),border-color var(--transition);
}
.tab:hover { color:var(--subtext1); }
.tab.active { color:var(--accent); border-bottom-color:var(--accent); }
.tab-content { display:none; flex:1; overflow-y:auto; flex-direction:column; }
.tab-content.active { display:flex; }
/* ── Traced / Forced signal rows ─────────────────────────── */
.traced-row {
display:flex; align-items:center; gap:4px;
padding:3px 6px; border-bottom:1px solid var(--crust); font-size:11px;
}
.traced-name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--accent); }
.traced-val { font-family:monospace; color:var(--green); font-size:11px; min-width:60px; text-align:right; }
.forced-row { display:flex; align-items:center; gap:4px; padding:3px 6px; border-bottom:1px solid var(--crust); font-size:11px; }
.forced-name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--peach); }
.forced-val { font-family:monospace; color:var(--yellow); font-size:11px; }
.break-item { display:flex; align-items:center; gap:4px; padding:3px 6px; border-bottom:1px solid var(--crust); font-size:11px; }
.break-sig { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--mauve); }
.msg-item { padding:3px 6px; border-bottom:1px solid var(--crust); font-size:11px; }
.empty-hint { padding:16px; color:var(--overlay0); text-align:center; font-size:12px; }
/* ── Panel collapse toggle ────────────────────────────────── */
.panel-toggle {
background:transparent; border:none; color:var(--subtext0);
padding:0 4px; font-size:11px; cursor:pointer; flex-shrink:0;
transition:color var(--transition);
}
.panel-toggle:hover { color:var(--accent); }
/* ── Log panel ────────────────────────────────────────────── */
#log-panel {
height:140px; min-height:26px; display:flex; flex-direction:column;
border-top:1px solid var(--surface0); background:var(--crust);
flex-shrink:0; overflow:hidden;
transition:height var(--transition);
}
#log-panel.collapsed { height:26px; }
#log-panel.collapsed #log-body { display:none; }
#log-toolbar {
display:flex; align-items:center; gap:8px;
padding:3px 8px; background:var(--mantle);
border-bottom:1px solid var(--surface0); flex-shrink:0; height:26px;
}
#log-toolbar label { display:flex; align-items:center; gap:3px; font-size:11px; color:var(--subtext0); cursor:pointer; }
#log-toolbar input[type=text] { font-size:11px; background:var(--surface0); border:1px solid var(--surface1); border-radius:3px; color:var(--text); padding:1px 4px; }
#log-body { flex:1; overflow-y:auto; font-family:monospace; font-size:11px; padding:2px 0; }
.log-line { padding:1px 8px; display:flex; gap:8px; }
.log-time { color:var(--overlay0); flex-shrink:0; font-family:monospace; }
.log-lvl { flex-shrink:0; min-width:50px; font-weight:600; }
.log-msg { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; }
.log-line.DEBUG .log-lvl { color:var(--accent); }
.log-line.INFO .log-lvl { color:var(--green); }
.log-line.WARNING .log-lvl { color:var(--peach); }
.log-line.ERROR .log-lvl { color:var(--red); }
.log-line.CMD .log-lvl { color:var(--teal); }
.log-line.RESP .log-lvl { color:var(--sky); }
.log-hidden { display:none; }
/* ── Stats panel ─────────────────────────────────────────────── */
#stats-panel {
position:fixed; bottom:0; left:0; right:0;
height:0; overflow:hidden;
background:var(--mantle); border-top:2px solid var(--surface0);
z-index:89;
transition:height var(--transition);
}
#stats-panel.open { height:290px; }
#stats-panel-hdr {
display:flex; align-items:center; gap:8px;
padding:4px 10px; border-bottom:1px solid var(--surface0); flex-shrink:0;
font-size:11px; font-weight:700; color:var(--subtext1); letter-spacing:0.5px; text-transform:uppercase;
}
.stats-hdr-label { flex-shrink:0; }
.stats-source-sel {
flex:1; min-width:0;
background:var(--surface0); border:1px solid var(--surface1); border-radius:var(--radius);
color:var(--text); font-size:11px; padding:1px 5px; cursor:pointer;
}
.stats-source-sel:focus { outline:none; border-color:var(--accent); }
#btn-stats-close {
background:none; border:none; color:var(--overlay0); flex-shrink:0;
cursor:pointer; font-size:14px; line-height:1; padding:2px;
transition:color var(--transition);
}
#btn-stats-close:hover { color:var(--red); }
#stats-body {
overflow-x:hidden; overflow-y:auto;
display:flex; flex-direction:column; gap:0;
padding:8px 18px;
height:calc(290px - 30px); box-sizing:border-box;
}
.stats-section { display:flex; flex-direction:column; gap:5px; padding:4px 0; }
.stats-section-grow { flex:1; }
.stats-empty { font-size:11px; color:var(--overlay0); }
.stats-section-label { font-size:9px; color:var(--overlay0); text-transform:uppercase; letter-spacing:0.6px; }
.stats-row { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-end; }
.stats-kv { display:flex; flex-direction:column; gap:1px; min-width:70px; }
.stats-k { font-size:9px; color:var(--overlay0); text-transform:uppercase; letter-spacing:0.5px; }
.stats-v { font-size:12px; color:var(--text); font-family:monospace; font-weight:600; }
.stats-v.warn { color:var(--yellow); }
.stats-v.ok { color:var(--green); }
.stats-sep { border:none; border-top:1px solid var(--surface0); margin:2px 0; }
.stats-hist { width:100%; }
.hist-bars { display:flex; align-items:flex-end; gap:1px; height:72px; background:var(--crust); border-radius:3px; padding:2px 3px; }
.hist-bar { flex:1; background:var(--accent); border-radius:1px 1px 0 0; min-height:1px; opacity:0.65; transition:opacity 0.1s; }
.hist-bar:hover { opacity:1; }
.hist-labels { display:flex; justify-content:space-between; font-size:9px; color:var(--overlay0); font-family:monospace; margin-top:2px; }
/* ── Dialogs ──────────────────────────────────────────────── */
.dialog-overlay {
position:fixed; inset:0; background:rgba(0,0,0,.6);
display:flex; align-items:center; justify-content:center; z-index:200;
}
.dialog {
background:var(--mantle); border:1px solid var(--surface1);
border-radius:var(--radius); padding:16px; min-width:320px; max-width:500px;
}
.dialog h3 { margin-bottom:12px; color:var(--accent); }
.dialog label { display:block; margin-bottom:4px; color:var(--subtext0); font-size:13px; }
.dialog input, .dialog select, .dialog textarea {
width:100%; background:var(--surface0); border:1px solid var(--surface1);
color:var(--text); padding:4px 8px; border-radius:4px; margin-bottom:10px;
}
.dialog textarea { height:80px; resize:vertical; font-family:monospace; font-size:12px; }
.dialog .btns { display:flex; gap:8px; justify-content:flex-end; margin-top:4px; }
.dialog select option { background:var(--mantle); }
.form-row { display:flex; gap:8px; }
.form-row > * { flex:1; }
.form-check { display:flex; align-items:center; gap:8px; margin-bottom:10px; }
.form-check input[type=checkbox] { width:auto; margin:0; }
.form-check label { margin:0; color:var(--subtext0); display:inline; }
/* ── Array selection segment ──────────────────────────────── */
.arr-seg {
display:flex; gap:0; margin-bottom:12px;
border-radius:4px; overflow:hidden; border:1px solid var(--surface1);
}
.arr-seg button {
flex:1; border:none; border-radius:0; border-right:1px solid var(--surface1);
color:var(--subtext0); background:var(--surface0); padding:4px 0; font-size:11px;
}
.arr-seg button:last-child { border-right:none; }
.arr-seg button.active { background:var(--accent); color:var(--crust); }
.arr-seg button:hover:not(.active) { background:var(--surface1); color:var(--text); }
/* ── Tree (Object Tree tab) ───────────────────────────────── */
.tree-node { padding:1px 0; }
.tree-leaf { display:flex; align-items:center; gap:4px; padding:2px 6px; cursor:default; user-select:none; font-size:11px; }
.tree-leaf:hover { background:var(--surface0); }
.tree-leaf.selected { background:var(--surface1); }
.tree-name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.tree-class { color:var(--overlay0); font-size:10px; flex-shrink:0; }
details > summary { list-style:none; cursor:pointer; padding:2px 6px; display:flex; align-items:center; gap:4px; user-select:none; font-size:11px; }
details > summary:hover { background:var(--surface0); }
details > summary::before { content:'▶'; font-size:9px; color:var(--overlay0); width:10px; flex-shrink:0; }
details[open] > summary::before { content:'▼'; }
details > .children { padding-left:14px; }
.tree-loading { padding:2px 4px; color:var(--overlay0); font-size:10px; font-style:italic; }
.tree-btn {
background:transparent; border:1px solid var(--surface1); color:var(--subtext0);
padding:0 4px; border-radius:3px; cursor:pointer; font-size:10px; line-height:14px;
}
.tree-btn:hover { background:var(--surface1); color:var(--text); }
.tree-btn.t { border-color:var(--accent); color:var(--accent); }
.tree-btn.f { border-color:var(--green); color:var(--green); }
.tree-btn.b { border-color:var(--peach); color:var(--peach); }
/* ── Empty state (center area hint) ─────────────────────── */
#empty-state {
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
text-align:center; color:var(--subtext0); pointer-events:none; display:none;
}
#empty-state.visible { display:block; }
#empty-state h2 { font-size:20px; margin-bottom:8px; color:var(--surface2); }
#empty-state p { font-size:13px; }
/* ── UDP stats ────────────────────────────────────────────── */
#udp-stats { color:var(--overlay0); font-size:11px; }
@media (max-width:700px) { #sidebar { width:0; min-width:0; } :root { --sidebar-w:240px; } }
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
+283
View File
@@ -0,0 +1,283 @@
'use strict';
/* ════════════════════════════════════════════════════════════════
Web Worker buffer management, binary parsing, LTTB
════════════════════════════════════════════════════════════════ */
const TEMPORAL_CAP = 600_000;
const DEFAULT_CAP = 10_000;
// Circular buffers: key → {t:Float64Array, v:Float64Array, head, size, cap}
const buffers = {};
function makeBuffer(cap) {
return { t: new Float64Array(cap), v: new Float64Array(cap), head: 0, size: 0, cap };
}
function pushBuffer(buf, t, v) {
buf.t[buf.head] = t; buf.v[buf.head] = v;
buf.head = (buf.head + 1) % buf.cap;
if (buf.size < buf.cap) buf.size++;
}
// ─── Binary frame parser ─────────────────────────────────────────────
// Format (little-endian):
// uint8 version (1)
// uint8 sourceIdLen
// UTF-8 sourceId
// uint32 numSignals
// for each signal:
// uint16 keyLen
// UTF-8 key (relative to source)
// uint32 pairCount N
// float64[N] t values
// float64[N] v values
function parseBinaryFrame(buf) {
const dv = new DataView(buf);
let off = 0;
if (dv.getUint8(off) !== 1) { console.warn('[worker] bad binary version'); return; }
off += 1;
const srcIdLen = dv.getUint8(off); off += 1;
const srcId = new TextDecoder().decode(new Uint8Array(buf, off, srcIdLen));
off += srcIdLen;
const prefix = srcId + ':';
const numSigs = dv.getUint32(off, true); off += 4;
for (let s = 0; s < numSigs; s++) {
const keyLen = dv.getUint16(off, true); off += 2;
const key = new TextDecoder().decode(new Uint8Array(buf, off, keyLen));
off += keyLen;
const fullKey = prefix + key;
const n = dv.getUint32(off, true); off += 4;
let bufObj = buffers[fullKey];
if (!bufObj) {
// Auto-create buffer with reasonable capacity
const cap = n > 100 ? TEMPORAL_CAP : DEFAULT_CAP;
bufObj = makeBuffer(cap);
buffers[fullKey] = bufObj;
}
// Read t values
for (let i = 0; i < n; i++) {
const t = dv.getFloat64(off, true); off += 8;
const v = dv.getFloat64(off + n * 8, true); // v array starts after t array
pushBuffer(bufObj, t, v);
}
off += n * 8; // skip v array (already read inline above)
}
}
// ─── Range slice from circular buffer ────────────────────────────────
function getBufferSliceRange(bufObj, t0, t1) {
const { cap, size, head } = bufObj;
if (size === 0) return { t: new Float64Array(0), v: new Float64Array(0) };
const start = (size === cap) ? head : 0;
const physAt = k => (start + k) % cap;
let lo = 0, hi = size;
while (lo < hi) { const m = (lo + hi) >>> 1; if (bufObj.t[physAt(m)] < t0) lo = m + 1; else hi = m; }
const kStart = lo;
lo = kStart; hi = size;
while (lo < hi) { const m = (lo + hi) >>> 1; if (bufObj.t[physAt(m)] <= t1) lo = m + 1; else hi = m; }
const kEnd = lo, len = kEnd - kStart;
if (len <= 0) return { t: new Float64Array(0), v: new Float64Array(0) };
const outT = new Float64Array(len), outV = new Float64Array(len);
const physStart = physAt(kStart), tail = cap - physStart;
if (tail >= len) {
outT.set(bufObj.t.subarray(physStart, physStart + len));
outV.set(bufObj.v.subarray(physStart, physStart + len));
} else {
outT.set(bufObj.t.subarray(physStart, physStart + tail));
outT.set(bufObj.t.subarray(0, len - tail), tail);
outV.set(bufObj.v.subarray(physStart, physStart + tail));
outV.set(bufObj.v.subarray(0, len - tail), tail);
}
return { t: outT, v: outV };
}
// ─── LTTB decimation ─────────────────────────────────────────────────
function lttb(t, v, threshold) {
const len = t.length;
if (len <= threshold || threshold < 3) return { t, v };
const outT = new Float64Array(threshold), outV = new Float64Array(threshold);
outT[0] = t[0]; outV[0] = v[0];
outT[threshold - 1] = t[len - 1]; outV[threshold - 1] = v[len - 1];
const every = (len - 2) / (threshold - 2);
let a = 0;
for (let i = 0; i < threshold - 2; i++) {
const avgS = Math.floor((i + 1) * every) + 1, avgE = Math.min(Math.floor((i + 2) * every) + 1, len);
let avgT = 0, avgV = 0, n = 0;
for (let j = avgS; j < avgE; j++) { avgT += t[j]; avgV += v[j]; n++; }
if (n) { avgT /= n; avgV /= n; }
const rS = Math.floor(i * every) + 1, rE = Math.min(Math.floor((i + 1) * every) + 1, len);
let maxA = -1, next = rS;
const aT = t[a], aV = v[a];
for (let j = rS; j < rE; j++) {
const area = Math.abs((aT - avgT) * (v[j] - aV) - (aT - t[j]) * (avgV - aV));
if (area > maxA) { maxA = area; next = j; }
}
outT[i + 1] = t[next]; outV[i + 1] = v[next]; a = next;
}
return { t: outT, v: outV };
}
// ─── Linear resampling ───────────────────────────────────────────────
function resampleLinear(tSrc, vSrc, tDst) {
const n = tDst.length;
const out = new Float64Array(n);
if (tSrc.length === 0) return out;
if (tSrc.length === 1) { out.fill(vSrc[0]); return out; }
let j = 0;
for (let i = 0; i < n; i++) {
const td = tDst[i];
while (j < tSrc.length - 2 && tSrc[j + 1] < td) j++;
if (td <= tSrc[0]) { out[i] = vSrc[0]; }
else if (td >= tSrc[tSrc.length - 1]) { out[i] = vSrc[vSrc.length - 1]; }
else {
const t0 = tSrc[j], t1 = tSrc[j + 1];
const frac = (td - t0) / (t1 - t0);
out[i] = vSrc[j] + frac * (vSrc[j + 1] - vSrc[j]);
}
}
return out;
}
// ─── Master time grid selection ──────────────────────────────────────
// samplingRates: key → rate (Hz), provided by main thread on init
const samplingRates = {};
function pickMasterKey(keys) {
let bestKey = keys[0], bestRate = -1;
for (const k of keys) {
const rate = samplingRates[k] || 0;
if (rate > bestRate) { bestRate = rate; bestKey = k; }
}
return bestKey;
}
// ─── Build uPlot-compatible data arrays ──────────────────────────────
function buildRenderData(keys, t0, t1, targetPts) {
if (!keys || keys.length === 0) return [new Float64Array(0)];
const slices = {};
let masterKey = pickMasterKey(keys), masterCount = -1;
for (const key of keys) {
const bufObj = buffers[key];
if (!bufObj || bufObj.size === 0) continue;
const sl = getBufferSliceRange(bufObj, t0, t1);
slices[key] = sl;
if (sl.t.length > masterCount) { masterCount = sl.t.length; masterKey = key; }
}
const masterRaw = slices[masterKey];
if (!masterRaw || masterRaw.t.length === 0)
return [new Float64Array(0), ...keys.map(() => new Float64Array(0))];
const dec = lttb(masterRaw.t, masterRaw.v, targetPts);
const sharedT = dec.t;
const yArrays = [];
for (const key of keys) {
if (key === masterKey) { yArrays.push(dec.v); continue; }
const sl = slices[key];
if (!sl || sl.t.length === 0) { yArrays.push(new Float64Array(sharedT.length)); continue; }
yArrays.push(resampleLinear(sl.t, sl.v, sharedT));
}
const result = [sharedT, ...yArrays];
// Transfer ownership of the Float64Arrays to main thread
const transferList = result.map(a => a.buffer);
return { data: result, transfer: transferList };
}
// ─── Message handler ─────────────────────────────────────────────────
self.onmessage = function(e) {
const msg = e.data;
switch (msg.type) {
case 'initSignals': {
// {signals: [{key, cap}]}
const sigs = msg.signals || [];
sigs.forEach(s => {
if (!buffers[s.key]) {
buffers[s.key] = makeBuffer(s.cap || DEFAULT_CAP);
}
if (s.samplingRate !== undefined) {
samplingRates[s.key] = s.samplingRate;
}
});
break;
}
case 'binaryData': {
// {buffer: ArrayBuffer} — transferred from main thread
parseBinaryFrame(msg.buffer);
self.postMessage({ type: 'dataReady' });
break;
}
case 'requestData': {
// {id, t0, t1, targetPts, keys}
const { id, t0, t1, targetPts, keys } = msg;
const { data, transfer } = buildRenderData(keys, t0, t1, targetPts);
self.postMessage({ type: 'renderData', id, data }, transfer);
break;
}
case 'clearSource': {
const prefix = msg.prefix;
Object.keys(buffers).forEach(k => {
if (k.startsWith(prefix)) delete buffers[k];
});
Object.keys(samplingRates).forEach(k => {
if (k.startsWith(prefix)) delete samplingRates[k];
});
break;
}
case 'getBufferNow': {
// Returns newest timestamp across given keys
const keys = msg.keys || [];
let latest = -Infinity;
keys.forEach(key => {
const bufObj = buffers[key];
if (bufObj && bufObj.size > 0) {
const t = bufObj.t[(bufObj.head - 1 + bufObj.cap) % bufObj.cap];
if (t > latest) latest = t;
}
});
self.postMessage({ type: 'bufferNow', id: msg.id, now: isFinite(latest) ? latest : null });
break;
}
case 'getBufferForTrig': {
// Returns full buffer contents for a single key (used for trigger check)
const key = msg.key;
const bufObj = buffers[key];
if (!bufObj || bufObj.size === 0) {
self.postMessage({ type: 'trigBuf', id: msg.id, key, size: 0 });
break;
}
// Copy out all data
const { cap, size, head } = bufObj;
const start = (size === cap) ? head : 0;
const t = new Float64Array(size), v = new Float64Array(size);
const physAt = k => (start + k) % cap;
for (let i = 0; i < size; i++) {
const p = physAt(i);
t[i] = bufObj.t[p];
v[i] = bufObj.v[p];
}
self.postMessage({
type: 'trigBuf', id: msg.id, key, size,
t, v
}, [t.buffer, v.buffer]);
break;
}
}
};
+12
View File
@@ -0,0 +1,12 @@
module udpstreamer-webui
go 1.21
require marte2/common v0.0.0
require (
github.com/gorilla/websocket v1.5.1 // indirect
golang.org/x/net v0.17.0 // indirect
)
replace marte2/common => ../../Common/Client/go
+4
View File
@@ -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=
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"embed"
"errors"
"flag"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"marte2/common/wshub"
)
var buildVersion = "dev"
//go:embed static
var staticFiles embed.FS
// multiFlag allows a flag to be repeated: --source a --source b
type multiFlag []string
func (f *multiFlag) String() string { return fmt.Sprintf("%v", []string(*f)) }
func (f *multiFlag) Set(v string) error { *f = append(*f, v); return nil }
func main() {
var sourceArgs multiFlag
flag.Var(&sourceArgs, "source", `Data source in the form [label@]host:port[/multicastGroup:dataPort] (repeatable)`)
sourcesFile := flag.String("sources-file", "", "JSON file for persistent source list (load on start, save target)")
listenAddr := flag.String("addr", ":8080", "HTTP listen address")
flag.Parse()
hub := wshub.NewHub()
sm := wshub.NewSourceManager(hub, *sourcesFile)
hub.SetSourceManager(sm)
go hub.Run()
// Load sources from file first (if specified), then add any CLI --source flags.
if *sourcesFile != "" {
if err := sm.Load(*sourcesFile); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("sources-file load: %v", err)
}
}
for _, arg := range sourceArgs {
label, addr, mcastGroup, dataPort := wshub.ParseSourceArgFull(arg)
sm.Add(label, addr, mcastGroup, dataPort)
}
sub, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatalf("static sub-fs: %v", err)
}
http.Handle("/", http.FileServer(http.FS(sub)))
http.HandleFunc("/ws", hub.HandleWebSocket)
http.HandleFunc("/api/zoom", hub.HandleZoom)
http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, buildVersion)
})
log.Printf("UDPStreamer WebUI listening on %s (build=%s)", *listenAddr, buildVersion)
if err := http.ListenAndServe(*listenAddr, nil); err != nil {
log.Fatalf("http: %v", err)
}
}
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<rect width="32" height="32" rx="6" fill="#1e1e2e"/>
<polyline points="2,16 7,16 9,8 11,24 14,6 17,26 20,12 23,20 25,16 30,16"
fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

+203
View File
@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UDP Scope</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/uPlot.min.css">
<link rel="stylesheet" href="/style.css">
<script src="/uPlot.iife.min.js"></script>
</head>
<body>
<!-- ── Top bar ───────────────────────────────────────────────── -->
<div id="topbar">
<button id="btn-sidebar" class="ctrl-btn active" title="Toggle sidebar"></button>
<span id="app-title">UDP Scope</span>
<div class="topbar-vsep"></div>
<button id="btn-layout" class="ctrl-btn layout-toggle" title="Select layout">⊞ 1×1 ▾</button>
<div class="topbar-sep"></div>
<div id="cursor-readout">
<span id="cur-ta">A: —</span><span class="cur-sep"></span>
<span id="cur-tb">B: —</span><span class="cur-sep"></span>
<span id="cur-dt">ΔT: —</span>
</div>
<span class="ctrl-label" id="lbl-window">Window:</span>
<select id="window-select" class="ctrl-select">
<option value="1">1 s</option><option value="5" selected>5 s</option>
<option value="10">10 s</option><option value="30">30 s</option>
<option value="60">60 s</option>
</select>
<button id="btn-cursor" class="ctrl-btn" style="display:none">Cursor</button>
<button id="btn-zoom-back" class="ctrl-btn" style="display:none">← Back</button>
<button id="btn-zoom-fit" class="ctrl-btn">Fit</button>
<button id="btn-csv-all" class="ctrl-btn" title="Export all signals to CSV">⬇ CSV</button>
<button id="btn-sync-resume" class="ctrl-btn resume-btn" style="display:none">↺ Auto</button>
<button id="btn-trigger" class="ctrl-btn">⚡ Trigger</button>
<button id="btn-pause-global" class="ctrl-btn">⏸ Pause</button>
</div>
<!-- ── Trigger bar ───────────────────────────────────────────── -->
<div id="trigbar">
<div class="trig-group">
<span class="trig-label">Signal</span>
<select id="trig-signal" class="trig-select"><option value="">— none —</option></select>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Edge</span>
<select id="trig-edge" class="trig-select">
<option value="rising">Rising ↑</option>
<option value="falling">Falling ↓</option>
<option value="both">Both ↕</option>
</select>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Threshold</span>
<input id="trig-threshold" class="trig-input" type="number" value="0" step="any">
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Window</span>
<select id="trig-window" class="trig-select">
<option value="0.0001">100 μs</option><option value="0.001">1 ms</option>
<option value="0.01">10 ms</option><option value="0.1">100 ms</option>
<option value="0.5">500 ms</option><option value="1" selected>1 s</option>
<option value="5">5 s</option><option value="10">10 s</option>
</select>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Pre</span>
<input id="trig-pre" class="trig-range" type="range" min="0" max="100" value="20">
<span class="trig-range-val" id="trig-pre-val">20%</span>
</div>
<div class="trig-sep"></div>
<div class="trig-group">
<span class="trig-label">Mode</span>
<select id="trig-mode" class="trig-select">
<option value="normal">Normal</option>
<option value="single">Single</option>
</select>
</div>
<div class="trig-sep"></div>
<div class="trig-group" style="gap:8px">
<span id="trig-status-badge">IDLE</span>
<button id="btn-trig-stop" style="display:none">Stop</button>
<button id="btn-trig-rearm">Rearm</button>
</div>
</div>
<!-- ── Body ─────────────────────────────────────────────────── -->
<div id="body">
<div id="sidebar">
<div id="sidebar-header">Signals</div>
<div id="signal-list"></div>
</div>
<div id="main">
<div id="plot-grid" class="l1x1"></div>
</div>
</div>
<!-- ── Stats panel ─────────────────────────────────────────────── -->
<div id="stats-panel">
<div id="stats-panel-hdr">
<span class="stats-hdr-label">Source Statistics</span>
<select id="stats-source-sel" class="stats-source-sel"></select>
<button id="btn-stats-close"></button>
</div>
<div id="stats-body"></div>
</div>
<!-- ── Status bar ─────────────────────────────────────────────── -->
<div id="statusbar">
<div id="sb-left">
<div id="status-led"></div>
<span id="status-text">Disconnected</span>
<span id="sb-tsage"></span>
<button id="btn-stats" class="ctrl-btn" style="height:16px;padding:0 7px;font-size:10px;line-height:1">📊 Stats</button>
</div>
<span id="build-version"></span>
</div>
<div id="layout-menu"></div>
<!-- ── Signal style context menu ─────────────────────────────── -->
<div id="sig-ctx-menu" style="display:none">
<div class="ctx-menu-header">Style:
<span id="ctx-menu-key" class="ctx-menu-key"></span></div>
<div class="ctx-row">
<label>Color</label>
<input type="color" id="ctx-color">
</div>
<div class="ctx-row">
<label>Width</label>
<div class="ctx-btns" id="ctx-width-btns">
<button class="ctx-btn" data-w="1">1px</button>
<button class="ctx-btn active" data-w="1.5">1.5</button>
<button class="ctx-btn" data-w="2">2px</button>
<button class="ctx-btn" data-w="3">3px</button>
</div>
</div>
<div class="ctx-row">
<label>Line</label>
<div class="ctx-btns" id="ctx-dash-btns">
<button class="ctx-btn active" data-dash="solid">——</button>
<button class="ctx-btn" data-dash="dashed">╌╌</button>
<button class="ctx-btn" data-dash="dotted">·····</button>
</div>
</div>
<div class="ctx-row">
<label>Marker</label>
<div class="ctx-btns" id="ctx-marker-btns">
<button class="ctx-btn active" data-marker="none">none</button>
<button class="ctx-btn" data-marker="circle"></button>
<button class="ctx-btn" data-marker="square"></button>
<button class="ctx-btn" data-marker="cross"></button>
<button class="ctx-btn" data-marker="diamond"></button>
</div>
</div>
<div class="ctx-row">
<label>Size</label>
<input type="range" class="ctx-range" id="ctx-marker-size" min="2" max="10" value="4">
<span class="ctx-range-val" id="ctx-marker-size-val">4px</span>
</div>
</div>
<!-- ── Array index picker (trigger signal) ──────────────────────── -->
<div id="array-idx-picker" style="display:none">
<div class="ctx-menu-header">Element index: <span id="aip-sig" class="ctx-menu-key"></span></div>
<div class="ctx-row">
<label>Index</label>
<input type="number" id="aip-idx" class="ctx-num" min="0" step="1" value="0">
<span id="aip-range" style="font-size:10px;color:var(--overlay0)"></span>
</div>
<div class="ctx-row" style="justify-content:flex-end;gap:6px">
<button class="ctx-btn" id="aip-cancel">Cancel</button>
<button class="ctx-btn active" id="aip-ok">OK</button>
</div>
</div>
<!-- ── VScale toolbar (moved into plot card when active) ─────────── -->
<div id="vscale-menu" style="display:none">
<div class="vstb-header">
<span class="vstb-label">V-Scale: <span id="vscale-menu-key" class="ctx-menu-key"></span></span>
<div class="ctx-btns" id="vscale-mode-btns">
<button class="ctx-btn active" data-mode="auto">Auto</button>
<button class="ctx-btn" data-mode="range">Range</button>
<button class="ctx-btn" data-mode="manual">Manual</button>
</div>
<div id="vscale-manual-row" style="display:none;align-items:center;gap:4px">
<label class="vstb-lbl">V/div</label>
<input type="number" id="vscale-vdiv" class="ctx-num" min="1e-30" step="any" value="1">
</div>
<div id="vscale-pos-row" style="display:none;align-items:center;gap:4px">
<label class="vstb-lbl">Pos</label>
<input type="number" id="vscale-pos" class="ctx-num" step="0.1" value="0">
</div>
<div id="vscale-type-row" style="display:none;align-items:center;gap:4px">
<label class="vstb-lbl">Type</label>
<div class="ctx-btns" id="vscale-type-btns">
<button class="ctx-btn active" data-type="analog">Analog</button>
<button class="ctx-btn" data-type="digital">Digital</button>
</div>
</div>
<button id="btn-vscale-close" class="vstb-close" title="Close"></button>
</div>
</div>
<script src="/app.js"></script>
</body>
</html>
+39
View File
@@ -0,0 +1,39 @@
'use strict';
// LTTB (Largest Triangle Three Buckets) decimation — O(n).
// Runs off-main-thread to avoid blocking the render loop.
function lttb(t, v, threshold) {
const len = t.length;
if (len <= threshold) {
// Copy to new arrays so we can transfer them back without detaching the input.
return { t: new Float64Array(t), v: new Float64Array(v) };
}
const outT = new Float64Array(threshold);
const outV = new Float64Array(threshold);
outT[0] = t[0]; outV[0] = v[0];
outT[threshold - 1] = t[len - 1]; outV[threshold - 1] = v[len - 1];
const every = (len - 2) / (threshold - 2);
let a = 0;
for (let i = 0; i < threshold - 2; i++) {
const avgS = Math.floor((i + 1) * every) + 1;
const avgE = Math.min(Math.floor((i + 2) * every) + 1, len);
let avgT = 0, avgV = 0, n = 0;
for (let j = avgS; j < avgE; j++) { avgT += t[j]; avgV += v[j]; n++; }
if (n) { avgT /= n; avgV /= n; }
const rS = Math.floor(i * every) + 1;
const rE = Math.min(Math.floor((i + 1) * every) + 1, len);
let maxA = -1, next = rS;
const aT = t[a], aV = v[a];
for (let j = rS; j < rE; j++) {
const area = Math.abs((aT - avgT) * (v[j] - aV) - (aT - t[j]) * (avgV - aV));
if (area > maxA) { maxA = area; next = j; }
}
outT[i + 1] = t[next]; outV[i + 1] = v[next]; a = next;
}
return { t: outT, v: outV };
}
self.onmessage = function({ data: { id, t, v, threshold } }) {
const result = lttb(t, v, threshold);
// Transfer the output buffers back to the main thread zero-copy.
self.postMessage({ id, t: result.t, v: result.v }, [result.t.buffer, result.v.buffer]);
};
+527
View File
@@ -0,0 +1,527 @@
/* ── Catppuccin Mocha palette ──────────────────────────────── */
:root {
--bg: #1e1e2e; --mantle: #181825; --crust: #11111b;
--surface0: #313244; --surface1: #45475a; --surface2: #585b70;
--overlay0: #6c7086; --overlay1: #7f849c; --text: #cdd6f4;
--subtext0: #a6adc8; --subtext1: #bac2de;
--accent: #89b4fa; --green: #a6e3a1; --red: #f38ba8;
--yellow: #f9e2af; --peach: #fab387; --mauve: #cba6f7;
--teal: #94e2d5; --sky: #89dceb; --lavender: #b4befe;
--pink: #f5c2e7;
--radius: 8px; --sidebar-w: 280px; --topbar-h: 52px;
--trigbar-h: 0px; --statusbar-h: 20px; --transition: 0.18s ease;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height:100%; background:var(--bg); color:var(--text);
font-family:'Segoe UI',system-ui,sans-serif; font-size:14px; overflow:hidden; }
::-webkit-scrollbar { width:6px; }
::-webkit-scrollbar-track { background:var(--mantle); }
::-webkit-scrollbar-thumb { background:var(--surface1); border-radius:3px; }
/* ── uPlot overrides ─────────────────────────────────────────── */
.uplot { background:transparent !important; }
.uplot .u-over { cursor: crosshair; }
.uplot .u-cursor-x, .uplot .u-cursor-y { border-color: #585b70 !important; }
.uplot .u-select { background: rgba(137,180,250,0.1) !important; border: 1px solid rgba(137,180,250,0.4) !important; }
/* ── Top bar ─────────────────────────────────────────────────── */
#topbar {
position:fixed; top:0; left:0; right:0; height:var(--topbar-h);
background:var(--mantle); border-bottom:1px solid var(--surface0);
display:flex; align-items:center; gap:10px; padding:0 14px;
z-index:100; box-shadow:0 2px 8px rgba(0,0,0,0.4); overflow:hidden;
}
#app-title { font-weight:700; font-size:15px; color:var(--accent);
white-space:nowrap; letter-spacing:0.3px; flex-shrink:0; }
.topbar-sep { flex:1; min-width:4px; }
#status-led { width:8px; height:8px; border-radius:50%;
background:var(--red); flex-shrink:0; transition:background var(--transition); }
#status-led.green { background:var(--green); animation:pulse-green 2s infinite; }
#status-led.orange { background:var(--yellow); animation:pulse-orange 1.5s infinite; }
@keyframes pulse-green { 0%,100%{box-shadow:0 0 0 0 rgba(166,227,161,0.4)} 50%{box-shadow:0 0 0 4px rgba(166,227,161,0)} }
@keyframes pulse-orange { 0%,100%{box-shadow:0 0 0 0 rgba(249,226,175,0.4)} 50%{box-shadow:0 0 0 4px rgba(249,226,175,0)} }
#status-text { font-size:11px; color:var(--subtext0); white-space:nowrap; }
#sb-tsage { font-size:11px; color:var(--overlay0); white-space:nowrap; font-family:monospace; }
#cursor-readout {
display:none; align-items:center; gap:8px;
font-size:11px; font-family:monospace;
background:var(--surface0); border:1px solid var(--surface1);
border-radius:5px; padding:3px 8px; white-space:nowrap; flex-shrink:0;
}
#cursor-readout.visible { display:flex; }
#cur-ta { color:var(--sky); } #cur-tb { color:var(--yellow); }
#cur-dt { color:var(--subtext1); } .cur-sep { color:var(--surface2); }
.topbar-vsep { width:1px; height:22px; background:var(--surface0); flex-shrink:0; margin:0 2px; }
#layout-btns { display:flex; gap:2px; align-items:center; flex-shrink:0; }
.ctrl-label { font-size:12px; color:var(--subtext0); white-space:nowrap; flex-shrink:0; }
select.ctrl-select {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:var(--radius);
padding:4px 6px; font-size:12px; cursor:pointer; outline:none; flex-shrink:0;
}
select.ctrl-select:hover { border-color:var(--accent); }
button.ctrl-btn {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:var(--radius);
padding:4px 12px; font-size:12px; cursor:pointer; white-space:nowrap; flex-shrink:0;
transition:background var(--transition),border-color var(--transition);
}
button.ctrl-btn:hover { background:var(--surface1); border-color:var(--accent); }
button.ctrl-btn.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
button.ctrl-btn.trig-active { background:rgba(203,166,247,0.15); border-color:var(--mauve); color:var(--mauve); }
button.ctrl-btn.cursor-a { border-color:var(--sky); color:var(--sky); }
button.ctrl-btn.cursor-b { border-color:var(--yellow); color:var(--yellow); }
button.ctrl-btn.resume-btn { border-color:var(--teal); color:var(--teal); }
/* ── Trigger bar ──────────────────────────────────────────────── */
#trigbar {
position:fixed; top:var(--topbar-h); left:0; right:0;
background:var(--crust); border-bottom:1px solid var(--surface0);
display:flex; align-items:center; flex-wrap:wrap; gap:10px;
padding:0 16px; z-index:99;
height:0; overflow:hidden;
transition:height var(--transition),padding var(--transition);
}
#trigbar.open { height:48px; padding:0 16px; }
.trig-group { display:flex; align-items:center; gap:6px; }
.trig-sep { width:1px; height:24px; background:var(--surface0); flex-shrink:0; }
.trig-label { font-size:11px; color:var(--subtext0); white-space:nowrap; }
select.trig-select {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:5px;
padding:3px 6px; font-size:12px; cursor:pointer; outline:none;
}
select.trig-select:hover { border-color:var(--mauve); }
input.trig-input {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:5px;
padding:3px 6px; font-size:12px; outline:none; width:80px;
}
input.trig-input:focus { border-color:var(--mauve); }
input[type=range].trig-range {
-webkit-appearance:none; width:90px; height:4px;
background:var(--surface1); border-radius:2px; outline:none; cursor:pointer;
}
input[type=range].trig-range::-webkit-slider-thumb {
-webkit-appearance:none; width:12px; height:12px;
border-radius:50%; background:var(--mauve); cursor:pointer;
}
.trig-range-val { font-size:11px; color:var(--mauve); min-width:28px; }
#trig-status-badge {
font-size:11px; font-weight:700; letter-spacing:0.8px;
padding:3px 10px; border-radius:12px;
border:1px solid var(--surface1); background:var(--surface0); color:var(--subtext0);
min-width:80px; text-align:center; white-space:nowrap;
}
#trig-status-badge.armed { background:rgba(166,227,161,0.12); border-color:var(--green); color:var(--green); }
#trig-status-badge.waiting { background:rgba(249,226,175,0.12); border-color:var(--yellow); color:var(--yellow); }
#trig-status-badge.triggered { background:rgba(203,166,247,0.15); border-color:var(--mauve); color:var(--mauve); }
#btn-trig-rearm, #btn-trig-stop {
border:none; border-radius:5px;
padding:4px 12px; font-size:12px; font-weight:600; cursor:pointer; display:none;
}
#btn-trig-rearm { background:var(--mauve); color:var(--crust); }
#btn-trig-stop { background:var(--surface1); color:var(--yellow); border:1px solid var(--yellow); }
#btn-trig-rearm:hover, #btn-trig-stop:hover { opacity:0.85; }
/* ── Status bar ───────────────────────────────────────────────── */
#statusbar {
position:fixed; bottom:0; left:0; right:0; height:var(--statusbar-h);
background:var(--crust); border-top:1px solid var(--surface0);
display:flex; align-items:center; justify-content:space-between;
padding:0 10px; z-index:100;
}
#sb-left { display:flex; align-items:center; gap:6px; }
#build-version { font-size:10px; color:var(--overlay0); font-family:monospace; white-space:nowrap; }
/* ── Body ─────────────────────────────────────────────────────── */
#body {
position:fixed; top:calc(var(--topbar-h) + var(--trigbar-h));
left:0; right:0; bottom:var(--statusbar-h); display:flex; overflow:hidden;
transition:top var(--transition);
}
/* ── Sidebar ──────────────────────────────────────────────────── */
#sidebar {
width:var(--sidebar-w); min-width:var(--sidebar-w);
background:var(--mantle); border-right:1px solid var(--surface0);
display:flex; flex-direction:column;
transition:width var(--transition),min-width var(--transition); overflow:hidden;
}
#sidebar.collapsed { width:0; min-width:0; }
#sidebar-header {
display:flex; align-items:center; justify-content:space-between;
padding:10px 14px; border-bottom:1px solid var(--surface0);
font-weight:600; color:var(--subtext1); font-size:12px;
text-transform:uppercase; letter-spacing:0.8px; flex-shrink:0;
}
#signal-list { flex:1; overflow-y:auto; padding:8px 0; }
.sig-item {
padding:6px 14px; cursor:grab; border-radius:6px; margin:1px 6px;
transition:background var(--transition); display:flex; align-items:center; gap:8px;
user-select:none;
}
.sig-item:hover { background:var(--surface0); }
.sig-item:active { cursor:grabbing; }
.sig-item.dragging { opacity:0.4; }
.sig-name { flex:1; font-size:13px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sig-unit { font-size:11px; color:var(--subtext0); font-style:italic; }
.type-badge { font-size:10px; background:var(--surface1); color:var(--subtext1); padding:1px 5px; border-radius:3px; white-space:nowrap; }
.array-group {}
.array-header {
padding:6px 14px 6px 10px; cursor:pointer; border-radius:6px; margin:1px 6px;
transition:background var(--transition); display:flex; align-items:center; gap:6px; user-select:none;
}
.array-header:hover { background:var(--surface0); }
.array-arrow { font-size:10px; color:var(--subtext0); transition:transform var(--transition); display:inline-block; }
.array-header.open .array-arrow { transform:rotate(90deg); }
.array-children { display:none; padding-left:16px; }
.array-header.open + .array-children { display:block; }
.array-child {
padding:4px 14px 4px 8px; cursor:grab; border-radius:6px; margin:1px 6px;
transition:background var(--transition); display:flex; align-items:center; gap:8px;
user-select:none; color:var(--subtext1); font-size:12px;
}
.array-child:hover { background:var(--surface0); }
.array-child:active { cursor:grabbing; }
/* ── Main area ────────────────────────────────────────────────── */
#main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
/* Layout toggle button in topbar */
#btn-layout {
display:flex; align-items:center; gap:4px;
font-size:11px; padding:3px 8px;
}
#btn-layout svg { flex-shrink:0; }
#btn-layout span { flex-shrink:0; }
/* Layout dropdown menu */
#layout-menu {
position:fixed; z-index:200;
background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius);
box-shadow:0 6px 20px rgba(0,0,0,0.5);
display:none; grid-template-columns:1fr 1fr; gap:4px; padding:6px;
}
#layout-menu.open { display:grid; }
.layout-menu-item {
display:flex; flex-direction:column; align-items:center; gap:3px;
padding:5px 10px; cursor:pointer;
background:var(--surface0); border:1px solid var(--surface1); border-radius:6px;
color:var(--subtext1); font-size:10px; font-family:monospace;
transition:background var(--transition),border-color var(--transition),color var(--transition);
}
.layout-menu-item:hover { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
.layout-menu-item.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
/* ── Plot grid ────────────────────────────────────────────────── */
#plot-grid {
flex:1; min-height:0; display:grid; gap:0; padding:0; overflow:hidden;
border-top:1px solid var(--surface0); border-left:1px solid var(--surface0);
position:relative;
}
.resize-handle-v {
position:absolute; top:0; bottom:0; width:6px; cursor:col-resize; z-index:20;
transform:translateX(-50%); background:transparent; transition:background 0.15s;
}
.resize-handle-h {
position:absolute; left:0; right:0; height:6px; cursor:row-resize; z-index:20;
transform:translateY(-50%); background:transparent; transition:background 0.15s;
}
.resize-handle-v:hover,.resize-handle-v.dragging,
.resize-handle-h:hover,.resize-handle-h.dragging {
background:rgba(137,180,250,0.35);
}
#plot-grid.l1x1 { grid-template-columns:1fr; grid-template-rows:1fr; }
#plot-grid.l2x1 { grid-template-columns:1fr 1fr; grid-template-rows:1fr; }
#plot-grid.l1x2 { grid-template-columns:1fr; grid-template-rows:1fr 1fr; }
#plot-grid.l2x2 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; }
#plot-grid.l3x1 { grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr; }
#plot-grid.l1x3 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr; }
#plot-grid.l3x2 { grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr 1fr; }
#plot-grid.l2x3 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr 1fr; }
#plot-grid.l1x4 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr 1fr; }
#plot-grid.l4x1 { grid-template-columns:1fr 1fr 1fr 1fr; grid-template-rows:1fr; }
/* ── Plot card ────────────────────────────────────────────────── */
.plot-card {
background:var(--bg);
border-right:1px solid var(--surface0); border-bottom:1px solid var(--surface0);
border-radius:0; display:flex; flex-direction:column;
min-height:0; position:relative; overflow:hidden;
}
.plot-card.drag-over { background:rgba(137,180,250,0.04); box-shadow:inset 0 0 0 2px var(--accent); }
/* Header: always visible, part of normal card flex flow */
.plot-card-header {
z-index:5; flex-shrink:0;
background:rgba(17,17,27,0.88); backdrop-filter:blur(6px);
border-bottom:1px solid var(--surface1);
display:flex; align-items:center; gap:5px;
padding:3px 8px; min-height:26px; overflow:hidden;
}
.plot-title {
font-size:11px; color:var(--subtext1); font-weight:600;
cursor:pointer; border:1px solid transparent; border-radius:3px;
padding:1px 4px; background:transparent; white-space:nowrap; user-select:none;
flex-shrink:0; max-width:100px; overflow:hidden; text-overflow:ellipsis;
transition:border-color 0.15s, background 0.15s;
}
.plot-title:hover { border-color:var(--surface1); background:rgba(88,91,112,0.4); }
.plot-cfg-bar {
flex-shrink:0; background:rgba(17,17,27,0.92); border-top:1px solid var(--surface1);
padding:3px 8px;
}
.sig-badges { display:flex; flex-wrap:nowrap; gap:3px; flex:1; overflow:hidden; min-width:0; }
.sig-badge {
display:inline-flex; align-items:center; gap:3px;
background:rgba(69,71,90,0.6); color:var(--subtext1);
border-radius:10px; padding:1px 6px 1px 4px;
font-size:10px; white-space:nowrap; flex-shrink:0;
}
.trace-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; display:inline-block; }
.sig-badge-x {
cursor:pointer; color:var(--overlay0); font-size:11px; line-height:1; margin-left:1px;
transition:color var(--transition);
}
.sig-badge-x:hover { color:var(--red); }
.plot-body { flex:1; position:relative; min-height:0; overflow:hidden; }
.drop-hint {
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
color:var(--overlay0); font-size:13px; pointer-events:none;
}
.trig-collect-overlay {
position:absolute; inset:0; display:none;
align-items:center; justify-content:center; pointer-events:none; z-index:10;
}
.plot-card.trig-collecting .trig-collect-overlay { display:flex; }
.trig-collect-text {
background:rgba(49,50,68,0.82); color:var(--mauve);
font-size:11px; font-weight:600; padding:4px 12px; border-radius:20px;
border:1px solid var(--mauve);
}
/* ── Signal style context menu ────────────────────────────────── */
#sig-ctx-menu {
position:fixed; z-index:300;
background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius);
box-shadow:0 8px 24px rgba(0,0,0,0.6); padding:10px; min-width:210px;
}
.ctx-menu-header {
font-size:11px; color:var(--subtext0); margin-bottom:8px;
padding-bottom:6px; border-bottom:1px solid var(--surface0);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.ctx-menu-key { color:var(--accent); font-weight:600; }
.ctx-row { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.ctx-row label { font-size:11px; color:var(--subtext0); width:42px; flex-shrink:0; }
.ctx-btns { display:flex; gap:3px; flex-wrap:wrap; }
.ctx-btn {
background:var(--surface0); border:1px solid var(--surface1); border-radius:4px;
color:var(--subtext1); font-size:11px; padding:2px 7px; cursor:pointer;
transition:background var(--transition),border-color var(--transition),color var(--transition);
}
.ctx-btn:hover { background:var(--surface1); border-color:var(--accent); }
.ctx-btn.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); }
#ctx-color {
width:28px; height:22px; border:1px solid var(--surface1); border-radius:4px;
background:transparent; cursor:pointer; padding:1px;
}
.ctx-range { width:80px; }
.ctx-range-val { font-size:11px; color:var(--mauve); min-width:26px; }
.ctx-num {
width:90px; background:var(--surface0); border:1px solid var(--surface1); border-radius:4px;
color:var(--text); font-size:11px; padding:2px 6px;
}
.ctx-num:focus { outline:none; border-color:var(--accent); }
.ctx-btn:disabled { opacity:0.35; cursor:not-allowed; border-color:var(--surface1); }
/* ── Array index picker ─────────────────────────────────────────── */
#array-idx-picker {
position:fixed; z-index:300;
background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius);
box-shadow:0 8px 24px rgba(0,0,0,0.6); padding:10px; min-width:200px;
}
/* ── VScale toolbar (embedded in plot card) ──────────────────────── */
#vscale-menu {
flex-shrink:0; background:rgba(17,17,27,0.92); border-top:1px solid var(--surface1);
padding:3px 8px;
}
.vstb-header {
display:flex; align-items:center; gap:8px; flex-wrap:nowrap; overflow-x:auto;
scrollbar-width:none;
}
.vstb-header::-webkit-scrollbar { display:none; }
.vstb-label { font-size:11px; color:var(--subtext0); white-space:nowrap; flex-shrink:0; }
.vstb-lbl { font-size:10px; color:var(--overlay0); white-space:nowrap; }
.vstb-close {
margin-left:auto; flex-shrink:0;
background:transparent; border:none; color:var(--overlay0);
cursor:pointer; font-size:11px; padding:0 3px; line-height:1;
transition:color var(--transition);
}
.vstb-close:hover { color:var(--red); }
.plot-vscale-bar { display:none; }
/* ── Per-plot cursor value readout (in plot card header) ────────── */
.plot-cursor-ro {
margin-left:auto; flex-shrink:0;
align-items:center; gap:5px;
font-size:10px; font-family:monospace;
background:var(--surface0); border:1px solid var(--surface1);
border-radius:4px; padding:1px 7px; white-space:nowrap;
overflow:hidden;
}
.pcur-a { color:var(--sky); }
.pcur-b { color:var(--yellow); }
.pcur-dv { color:var(--subtext1); }
.pcur-sep { color:var(--surface2); }
/* ── Badge vscale info & active state ───────────────────────────── */
.vscale-info {
font-size:9px; color:var(--overlay0); font-family:monospace; margin-left:2px; white-space:nowrap;
}
.sig-badge { cursor:pointer; }
.sig-badge-active { outline:1px solid rgba(255,255,255,0.35); background:rgba(88,91,112,0.9); }
.sig-badge-active .vscale-info { color:var(--subtext0); }
/* ── Source groups ────────────────────────────────────────────── */
.source-group { margin-bottom:2px; }
.source-group-header {
display:flex; align-items:center; gap:5px;
padding:5px 8px 5px 10px; margin:4px 6px 2px;
background:var(--surface0); border-radius:6px;
}
.source-state-dot {
width:7px; height:7px; border-radius:50%; flex-shrink:0;
background:var(--overlay0); transition:background var(--transition);
}
.source-state-dot.connected { background:var(--green); }
.source-state-dot.connecting { background:var(--yellow); animation:pulse-orange 1.5s infinite; }
.source-state-dot.disconnected { background:var(--red); }
.source-name {
font-size:11px; font-weight:600; color:var(--subtext1); flex:1;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.source-addr {
font-size:10px; color:var(--overlay0); font-family:monospace;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:90px;
}
.source-remove-btn {
background:none; border:none; color:var(--overlay0);
cursor:pointer; font-size:15px; line-height:1; padding:0 2px; flex-shrink:0;
transition:color var(--transition);
}
.source-remove-btn:hover { color:var(--red); }
/* ── Add source section ───────────────────────────────────────── */
.add-source-section {
border-top:1px solid var(--surface0); margin-top:4px;
}
.add-source-title {
display:flex; align-items:center; gap:5px;
padding:6px 10px; cursor:pointer; user-select:none;
font-size:11px; color:var(--overlay0);
transition:color var(--transition);
}
.add-source-title:hover { color:var(--subtext1); }
.add-src-arrow {
font-size:9px; display:inline-block;
transition:transform var(--transition);
}
.add-source-body {
display:none; flex-direction:column; gap:5px;
padding:0 10px 10px;
}
.add-source-section.open .add-source-body { display:flex; }
.add-src-input {
background:var(--surface0); color:var(--text);
border:1px solid var(--surface1); border-radius:5px;
padding:4px 8px; font-size:12px; outline:none; width:100%;
}
.add-src-input:focus { border-color:var(--accent); }
.add-src-btn {
background:var(--surface0); color:var(--accent);
border:1px solid var(--surface1); border-radius:5px;
padding:4px 10px; font-size:12px; cursor:pointer; width:100%;
transition:background var(--transition),border-color var(--transition);
}
.add-src-btn:hover { background:rgba(137,180,250,0.15); border-color:var(--accent); }
.save-src-btn { color:var(--green); }
.save-src-btn:hover { background:rgba(166,227,161,0.1); border-color:var(--green); }
/* ── Stats panel ─────────────────────────────────────────────── */
#stats-panel {
position:fixed; bottom:var(--statusbar-h); left:0; right:0;
height:0; overflow:hidden;
background:var(--mantle); border-top:2px solid var(--surface0);
z-index:89; /* below topbar / sidebar */
transition:height var(--transition);
}
#stats-panel.open { height:290px; }
#stats-panel-hdr {
display:flex; align-items:center; gap:8px;
padding:4px 10px; border-bottom:1px solid var(--surface0); flex-shrink:0;
font-size:11px; font-weight:700; color:var(--subtext1); letter-spacing:0.5px; text-transform:uppercase;
}
.stats-hdr-label { flex-shrink:0; }
.stats-source-sel {
flex:1; min-width:0;
background:var(--surface0); border:1px solid var(--surface1); border-radius:var(--radius);
color:var(--text); font-size:11px; padding:1px 5px; cursor:pointer;
}
.stats-source-sel:focus { outline:none; border-color:var(--accent); }
#btn-stats-close {
background:none; border:none; color:var(--overlay0); flex-shrink:0;
cursor:pointer; font-size:14px; line-height:1; padding:2px;
transition:color var(--transition);
}
#btn-stats-close:hover { color:var(--red); }
#stats-body {
overflow-x:hidden; overflow-y:auto;
display:flex; flex-direction:column; gap:0;
padding:8px 18px;
height:calc(290px - 30px); box-sizing:border-box;
}
.stats-section {
display:flex; flex-direction:column; gap:5px; padding:4px 0;
}
.stats-section-grow { flex:1; }
.stats-empty { font-size:11px; color:var(--overlay0); }
.stats-section-label {
font-size:9px; color:var(--overlay0); text-transform:uppercase; letter-spacing:0.6px;
}
.stats-row { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-end; }
.stats-kv { display:flex; flex-direction:column; gap:1px; min-width:70px; }
.stats-k { font-size:9px; color:var(--overlay0); text-transform:uppercase; letter-spacing:0.5px; }
.stats-v { font-size:12px; color:var(--text); font-family:monospace; font-weight:600; }
.stats-v.warn { color:var(--yellow); }
.stats-v.ok { color:var(--green); }
.stats-sep { border:none; border-top:1px solid var(--surface0); margin:2px 0; }
/* Histogram */
.stats-hist { width:100%; }
.hist-bars {
display:flex; align-items:flex-end; gap:1px; height:72px;
background:var(--crust); border-radius:3px; padding:2px 3px;
}
.hist-bar {
flex:1; background:var(--accent); border-radius:1px 1px 0 0; min-height:1px;
opacity:0.65; transition:opacity 0.1s;
}
.hist-bar:hover { opacity:1; }
.hist-labels {
display:flex; justify-content:space-between;
font-size:9px; color:var(--overlay0); font-family:monospace; margin-top:2px;
}
/* ── Empty state ──────────────────────────────────────────────── */
#empty-state {
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
text-align:center; color:var(--subtext0); pointer-events:none; display:none;
}
#empty-state.visible { display:block; }
#empty-state h2 { font-size:20px; margin-bottom:8px; color:var(--surface2); }
#empty-state p { font-size:13px; }
@media (max-width:700px) { #sidebar { width:0; min-width:0; } :root { --sidebar-w:240px; } }
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
+283
View File
@@ -0,0 +1,283 @@
'use strict';
/* ════════════════════════════════════════════════════════════════
Web Worker buffer management, binary parsing, LTTB
════════════════════════════════════════════════════════════════ */
const TEMPORAL_CAP = 600_000;
const DEFAULT_CAP = 10_000;
// Circular buffers: key → {t:Float64Array, v:Float64Array, head, size, cap}
const buffers = {};
function makeBuffer(cap) {
return { t: new Float64Array(cap), v: new Float64Array(cap), head: 0, size: 0, cap };
}
function pushBuffer(buf, t, v) {
buf.t[buf.head] = t; buf.v[buf.head] = v;
buf.head = (buf.head + 1) % buf.cap;
if (buf.size < buf.cap) buf.size++;
}
// ─── Binary frame parser ─────────────────────────────────────────────
// Format (little-endian):
// uint8 version (1)
// uint8 sourceIdLen
// UTF-8 sourceId
// uint32 numSignals
// for each signal:
// uint16 keyLen
// UTF-8 key (relative to source)
// uint32 pairCount N
// float64[N] t values
// float64[N] v values
function parseBinaryFrame(buf) {
const dv = new DataView(buf);
let off = 0;
if (dv.getUint8(off) !== 1) { console.warn('[worker] bad binary version'); return; }
off += 1;
const srcIdLen = dv.getUint8(off); off += 1;
const srcId = new TextDecoder().decode(new Uint8Array(buf, off, srcIdLen));
off += srcIdLen;
const prefix = srcId + ':';
const numSigs = dv.getUint32(off, true); off += 4;
for (let s = 0; s < numSigs; s++) {
const keyLen = dv.getUint16(off, true); off += 2;
const key = new TextDecoder().decode(new Uint8Array(buf, off, keyLen));
off += keyLen;
const fullKey = prefix + key;
const n = dv.getUint32(off, true); off += 4;
let bufObj = buffers[fullKey];
if (!bufObj) {
// Auto-create buffer with reasonable capacity
const cap = n > 100 ? TEMPORAL_CAP : DEFAULT_CAP;
bufObj = makeBuffer(cap);
buffers[fullKey] = bufObj;
}
// Read t values
for (let i = 0; i < n; i++) {
const t = dv.getFloat64(off, true); off += 8;
const v = dv.getFloat64(off + n * 8, true); // v array starts after t array
pushBuffer(bufObj, t, v);
}
off += n * 8; // skip v array (already read inline above)
}
}
// ─── Range slice from circular buffer ────────────────────────────────
function getBufferSliceRange(bufObj, t0, t1) {
const { cap, size, head } = bufObj;
if (size === 0) return { t: new Float64Array(0), v: new Float64Array(0) };
const start = (size === cap) ? head : 0;
const physAt = k => (start + k) % cap;
let lo = 0, hi = size;
while (lo < hi) { const m = (lo + hi) >>> 1; if (bufObj.t[physAt(m)] < t0) lo = m + 1; else hi = m; }
const kStart = lo;
lo = kStart; hi = size;
while (lo < hi) { const m = (lo + hi) >>> 1; if (bufObj.t[physAt(m)] <= t1) lo = m + 1; else hi = m; }
const kEnd = lo, len = kEnd - kStart;
if (len <= 0) return { t: new Float64Array(0), v: new Float64Array(0) };
const outT = new Float64Array(len), outV = new Float64Array(len);
const physStart = physAt(kStart), tail = cap - physStart;
if (tail >= len) {
outT.set(bufObj.t.subarray(physStart, physStart + len));
outV.set(bufObj.v.subarray(physStart, physStart + len));
} else {
outT.set(bufObj.t.subarray(physStart, physStart + tail));
outT.set(bufObj.t.subarray(0, len - tail), tail);
outV.set(bufObj.v.subarray(physStart, physStart + tail));
outV.set(bufObj.v.subarray(0, len - tail), tail);
}
return { t: outT, v: outV };
}
// ─── LTTB decimation ─────────────────────────────────────────────────
function lttb(t, v, threshold) {
const len = t.length;
if (len <= threshold || threshold < 3) return { t, v };
const outT = new Float64Array(threshold), outV = new Float64Array(threshold);
outT[0] = t[0]; outV[0] = v[0];
outT[threshold - 1] = t[len - 1]; outV[threshold - 1] = v[len - 1];
const every = (len - 2) / (threshold - 2);
let a = 0;
for (let i = 0; i < threshold - 2; i++) {
const avgS = Math.floor((i + 1) * every) + 1, avgE = Math.min(Math.floor((i + 2) * every) + 1, len);
let avgT = 0, avgV = 0, n = 0;
for (let j = avgS; j < avgE; j++) { avgT += t[j]; avgV += v[j]; n++; }
if (n) { avgT /= n; avgV /= n; }
const rS = Math.floor(i * every) + 1, rE = Math.min(Math.floor((i + 1) * every) + 1, len);
let maxA = -1, next = rS;
const aT = t[a], aV = v[a];
for (let j = rS; j < rE; j++) {
const area = Math.abs((aT - avgT) * (v[j] - aV) - (aT - t[j]) * (avgV - aV));
if (area > maxA) { maxA = area; next = j; }
}
outT[i + 1] = t[next]; outV[i + 1] = v[next]; a = next;
}
return { t: outT, v: outV };
}
// ─── Linear resampling ───────────────────────────────────────────────
function resampleLinear(tSrc, vSrc, tDst) {
const n = tDst.length;
const out = new Float64Array(n);
if (tSrc.length === 0) return out;
if (tSrc.length === 1) { out.fill(vSrc[0]); return out; }
let j = 0;
for (let i = 0; i < n; i++) {
const td = tDst[i];
while (j < tSrc.length - 2 && tSrc[j + 1] < td) j++;
if (td <= tSrc[0]) { out[i] = vSrc[0]; }
else if (td >= tSrc[tSrc.length - 1]) { out[i] = vSrc[vSrc.length - 1]; }
else {
const t0 = tSrc[j], t1 = tSrc[j + 1];
const frac = (td - t0) / (t1 - t0);
out[i] = vSrc[j] + frac * (vSrc[j + 1] - vSrc[j]);
}
}
return out;
}
// ─── Master time grid selection ──────────────────────────────────────
// samplingRates: key → rate (Hz), provided by main thread on init
const samplingRates = {};
function pickMasterKey(keys) {
let bestKey = keys[0], bestRate = -1;
for (const k of keys) {
const rate = samplingRates[k] || 0;
if (rate > bestRate) { bestRate = rate; bestKey = k; }
}
return bestKey;
}
// ─── Build uPlot-compatible data arrays ──────────────────────────────
function buildRenderData(keys, t0, t1, targetPts) {
if (!keys || keys.length === 0) return [new Float64Array(0)];
const slices = {};
let masterKey = pickMasterKey(keys), masterCount = -1;
for (const key of keys) {
const bufObj = buffers[key];
if (!bufObj || bufObj.size === 0) continue;
const sl = getBufferSliceRange(bufObj, t0, t1);
slices[key] = sl;
if (sl.t.length > masterCount) { masterCount = sl.t.length; masterKey = key; }
}
const masterRaw = slices[masterKey];
if (!masterRaw || masterRaw.t.length === 0)
return [new Float64Array(0), ...keys.map(() => new Float64Array(0))];
const dec = lttb(masterRaw.t, masterRaw.v, targetPts);
const sharedT = dec.t;
const yArrays = [];
for (const key of keys) {
if (key === masterKey) { yArrays.push(dec.v); continue; }
const sl = slices[key];
if (!sl || sl.t.length === 0) { yArrays.push(new Float64Array(sharedT.length)); continue; }
yArrays.push(resampleLinear(sl.t, sl.v, sharedT));
}
const result = [sharedT, ...yArrays];
// Transfer ownership of the Float64Arrays to main thread
const transferList = result.map(a => a.buffer);
return { data: result, transfer: transferList };
}
// ─── Message handler ─────────────────────────────────────────────────
self.onmessage = function(e) {
const msg = e.data;
switch (msg.type) {
case 'initSignals': {
// {signals: [{key, cap}]}
const sigs = msg.signals || [];
sigs.forEach(s => {
if (!buffers[s.key]) {
buffers[s.key] = makeBuffer(s.cap || DEFAULT_CAP);
}
if (s.samplingRate !== undefined) {
samplingRates[s.key] = s.samplingRate;
}
});
break;
}
case 'binaryData': {
// {buffer: ArrayBuffer} — transferred from main thread
parseBinaryFrame(msg.buffer);
self.postMessage({ type: 'dataReady' });
break;
}
case 'requestData': {
// {id, t0, t1, targetPts, keys}
const { id, t0, t1, targetPts, keys } = msg;
const { data, transfer } = buildRenderData(keys, t0, t1, targetPts);
self.postMessage({ type: 'renderData', id, data }, transfer);
break;
}
case 'clearSource': {
const prefix = msg.prefix;
Object.keys(buffers).forEach(k => {
if (k.startsWith(prefix)) delete buffers[k];
});
Object.keys(samplingRates).forEach(k => {
if (k.startsWith(prefix)) delete samplingRates[k];
});
break;
}
case 'getBufferNow': {
// Returns newest timestamp across given keys
const keys = msg.keys || [];
let latest = -Infinity;
keys.forEach(key => {
const bufObj = buffers[key];
if (bufObj && bufObj.size > 0) {
const t = bufObj.t[(bufObj.head - 1 + bufObj.cap) % bufObj.cap];
if (t > latest) latest = t;
}
});
self.postMessage({ type: 'bufferNow', id: msg.id, now: isFinite(latest) ? latest : null });
break;
}
case 'getBufferForTrig': {
// Returns full buffer contents for a single key (used for trigger check)
const key = msg.key;
const bufObj = buffers[key];
if (!bufObj || bufObj.size === 0) {
self.postMessage({ type: 'trigBuf', id: msg.id, key, size: 0 });
break;
}
// Copy out all data
const { cap, size, head } = bufObj;
const start = (size === cap) ? head : 0;
const t = new Float64Array(size), v = new Float64Array(size);
const physAt = k => (start + k) % cap;
for (let i = 0; i < size; i++) {
const p = physAt(i);
t[i] = bufObj.t[p];
v[i] = bufObj.v[p];
}
self.postMessage({
type: 'trigBuf', id: msg.id, key, size,
t, v
}, [t.buffer, v.buffer]);
break;
}
}
};
+7
View File
@@ -0,0 +1,7 @@
module marte2/common
go 1.21
require github.com/gorilla/websocket v1.5.1
require golang.org/x/net v0.17.0 // indirect
+4
View File
@@ -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=
+383
View File
@@ -0,0 +1,383 @@
package udpsprotocol
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
// TimeMode values must match UDPStreamerTimeMode enum in UDPStreamer.h
TimeModePacket uint8 = 0 // use wall-clock packet arrival time
TimeModeFullArray uint8 = 1 // TimeSignal has same N elements; not expanded here
TimeModeFirstSample uint8 = 2 // TimeSignal scalar = time of element [0]
TimeModeLastSample uint8 = 3 // TimeSignal scalar = time of element [N-1]
// PublishMode values must match UDPStreamerPublishMode enum in UDPStreamer.h
PublishModeStrict uint8 = 0 // one packet per Synchronise() call
PublishModeAccumulate uint8 = 1 // variable batch; DATA has [8 HRT][4 numSamples][signals...]
PublishModeDecimate uint8 = 2 // one packet every Ratio calls
)
// ─── 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.
// Returns the signal list, the publishing mode byte (PublishMode*), and any error.
func ParseConfig(payload []byte) ([]SignalInfo, uint8, error) {
if len(payload) < 4 {
return nil, 0, 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, 0, 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
}
// Trailing publish-mode byte (added after signal descriptors).
publishMode := PublishModeStrict
if offset < len(payload) {
publishMode = payload[offset]
}
return sigs, publishMode, 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
}
// parseElems reads n elements for sig from payload at offset, advancing offset.
// Returns the slice of float64 values and the new offset.
func parseElems(payload []byte, offset, n int, sig SignalInfo) ([]float64, int, error) {
elems := make([]float64, n)
if sig.QuantType == QuantNone {
sz := rawTypeSize(sig.TypeCode)
needed := n * sz
if offset+needed > len(payload) {
return nil, offset, 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 nil, offset, 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
}
return elems, offset, nil
}
// ParseData decodes a fully-reassembled DATA payload using the provided signal config
// and publishing mode. arrivalTime is the wall-clock time at which the packet arrived.
//
// For PublishModeAccumulate the payload format is:
//
// [8 HRT][4 numSamples][for each signal: accumulated scalars → numSamples elems; arrays → NumElements elems]
//
// The function returns one DataSample per accumulated snapshot so the hub can
// process each slot independently with its own timestamp.
func ParseData(payload []byte, sigs []SignalInfo, publishMode uint8, arrivalTime time.Time) ([]DataSample, error) {
if len(payload) < 8 {
return nil, fmt.Errorf("data payload too short")
}
hrt := binary.LittleEndian.Uint64(payload[0:8])
offset := 8
if publishMode == PublishModeAccumulate {
if len(payload) < 12 {
return nil, fmt.Errorf("accumulate data payload too short (missing numSamples)")
}
numSamples := int(binary.LittleEndian.Uint32(payload[8:12]))
offset = 12
if numSamples == 0 {
return []DataSample{}, nil
}
// Parse per-signal data blocks (all slots for a signal are contiguous).
accumVals := make(map[string][]float64, len(sigs)) // scalars: numSamples values
fixedVals := make(map[string][]float64, len(sigs)) // arrays: NumElements values
for _, sig := range sigs {
n := sig.NumElements()
if n == 1 {
// Accumulated scalar: read numSamples back-to-back elements.
elems, newOff, err := parseElems(payload, offset, numSamples, sig)
if err != nil {
return nil, err
}
offset = newOff
accumVals[sig.Name] = elems
} else {
// Fixed array (non-accumulated): one set of NumElements values.
elems, newOff, err := parseElems(payload, offset, n, sig)
if err != nil {
return nil, err
}
offset = newOff
fixedVals[sig.Name] = elems
}
}
// Build one DataSample per slot.
samples := make([]DataSample, numSamples)
for k := 0; k < numSamples; k++ {
vals := make(map[string][]float64, len(sigs))
for sigName, av := range accumVals {
vals[sigName] = []float64{av[k]}
}
for sigName, fv := range fixedVals {
vals[sigName] = fv // shared read-only reference; hub does not modify
}
samples[k] = DataSample{HRTTimestamp: hrt, WallTime: arrivalTime, Values: vals}
}
return samples, nil
}
// Strict / Decimate: single snapshot, one element set per signal.
vals := make(map[string][]float64, len(sigs))
for _, sig := range sigs {
n := sig.NumElements()
elems, newOff, err := parseElems(payload, offset, n, sig)
if err != nil {
return nil, err
}
offset = newOff
vals[sig.Name] = elems
}
return []DataSample{{HRTTimestamp: hrt, WallTime: arrivalTime, Values: vals}}, nil
}
@@ -0,0 +1,105 @@
package udpsprotocol
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()
}
}
+903
View File
@@ -0,0 +1,903 @@
package wshub
import (
"encoding/binary"
"encoding/json"
"log"
"math"
"net/http"
"strconv"
"strings"
"sync"
"time"
"unsafe"
"github.com/gorilla/websocket"
"marte2/common/udpsprotocol"
)
// ─── WebSocket client ─────────────────────────────────────────────────────────
type wsMessage struct {
msgType int
data []byte
}
type wsClient struct {
hub *Hub
conn *websocket.Conn
send chan wsMessage
}
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(msg.msgType, msg.data); 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
}
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 <- wsMessage{websocket.TextMessage, resp}:
default:
}
case "addSource":
label, _ := env["label"].(string)
addr, _ := env["addr"].(string)
mcastGroup, _ := env["multicastGroup"].(string)
dataPortF, _ := env["dataPort"].(float64)
if addr != "" {
select {
case c.hub.commandCh <- hubCmd{
op: "wsAddSource", label: label, addr: addr,
multicastGroup: mcastGroup, dataPort: int(dataPortF),
}:
default:
}
}
case "removeSource":
id, _ := env["id"].(string)
if id != "" {
select {
case c.hub.commandCh <- hubCmd{op: "wsRemoveSource", sourceID: id}:
default:
}
}
case "saveSources":
select {
case c.hub.commandCh <- hubCmd{op: "wsSaveSources"}:
default:
}
default:
// Unrecognized message type — forward to DebugCh
select {
case c.hub.DebugCh <- msg:
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 },
}
// sourceHubState holds all data for one active data source.
// Only accessed from the Run() goroutine.
type sourceHubState struct {
id, label, addr, connState string
signals []udpsprotocol.SignalInfo
configJS []byte
// Time-signal calibration — only accessed from Run() goroutine.
timeSigCalib map[string]float64
configSeq uint64
configSeqAtCalib uint64
}
// taggedSample is a DataSample annotated with its source ID.
type taggedSample struct {
sourceID string
sample udpsprotocol.DataSample
}
// hubCmd carries a command to the Run() goroutine.
type hubCmd struct {
op string // "addSource","removeSource","setSourceState","updateConfig",
// "wsAddSource","wsRemoveSource","wsSaveSources"
sourceID string
label string
addr string
state string
sigs []udpsprotocol.SignalInfo
multicastGroup string
dataPort int
}
// Hub is the central broker between UDP clients and WebSocket clients.
// All map state is accessed exclusively from the Run() goroutine, except
// ringsMu/rings which are also read by HTTP handler goroutines.
type Hub struct {
clients map[*wsClient]bool
register chan *wsClient
unregister chan *wsClient
broadcastCh chan []byte
dataCh chan taggedSample
commandCh chan hubCmd
// DebugCh receives raw browser messages whose type is not handled by the hub.
DebugCh chan []byte
sm *SourceManager // set after construction; used for WS-initiated source changes
// Ring buffers for hi-res zoom data.
// ringsMu protects the map structure; each sigRing has its own RWMutex for data.
ringsMu sync.RWMutex
rings map[string]*sigRing // "sourceId:signalKey" → ring
// lastZoomAt tracks the last time a zoom request was served.
// Ring buffer writes are skipped when no zoom has been requested
// in the last 10 s, saving substantial CPU on LTTB + ring writes.
lastZoomAt time.Time
zoomAtMu sync.Mutex
statsMu sync.RWMutex
statsMap map[string]*SourceStat
// onClientConnect, if set, is called each time a new WebSocket client
// registers. The callback receives a send function that delivers a message
// directly to that client. It is invoked synchronously from Run(), so it
// must not block.
onClientConnectMu sync.RWMutex
onClientConnect func(send func([]byte))
}
// 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, 256),
dataCh: make(chan taggedSample, 65536), // large buffer: absorbs bursts at high sample rates
commandCh: make(chan hubCmd, 64),
DebugCh: make(chan []byte, 256),
rings: make(map[string]*sigRing),
statsMap: make(map[string]*SourceStat),
}
}
// SetOnClientConnect registers a callback invoked synchronously (from Run())
// each time a new WebSocket client connects. The callback receives a send
// function that enqueues one message to that specific client.
func (h *Hub) SetOnClientConnect(fn func(send func([]byte))) {
h.onClientConnectMu.Lock()
h.onClientConnect = fn
h.onClientConnectMu.Unlock()
}
// SetSourceManager sets the SourceManager associated with the Hub.
func (h *Hub) SetSourceManager(sm *SourceManager) {
h.sm = sm
}
// getRing returns the ring buffer for a fully-prefixed signal key, or nil.
func (h *Hub) getRing(key string) *sigRing {
h.ringsMu.RLock()
rb := h.rings[key]
h.ringsMu.RUnlock()
return rb
}
// shouldWriteRing returns true if zoom was requested within the last 10 seconds.
func (h *Hub) shouldWriteRing() bool {
h.zoomAtMu.Lock()
ok := time.Since(h.lastZoomAt) < 10*time.Second
h.zoomAtMu.Unlock()
return ok
}
// HandleZoom serves GET /api/zoom?... It also records the access time
// so the ring buffer knows zoom is active and worth populating.
func (h *Hub) HandleZoom(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
t0, err0 := strconv.ParseFloat(q.Get("t0"), 64)
t1, err1 := strconv.ParseFloat(q.Get("t1"), 64)
if err0 != nil || err1 != nil || t1 <= t0 {
http.Error(w, "invalid t0/t1", http.StatusBadRequest)
return
}
var n int
if nStr := q.Get("n"); nStr == "" {
n = 2400
} else {
n, _ = strconv.Atoi(nStr)
if n <= 0 {
n = 1 << 30 // no decimation
} else if n < 10 {
n = 2400
}
}
if n > 0 {
h.zoomAtMu.Lock()
h.lastZoomAt = time.Now()
h.zoomAtMu.Unlock()
}
keys := strings.Split(q.Get("signals"), ",")
h.ringsMu.RLock()
refs := make(map[string]*sigRing, len(keys))
for _, k := range keys {
k = strings.TrimSpace(k)
if k == "" {
continue
}
if rb, ok := h.rings[k]; ok {
refs[k] = rb
}
}
h.ringsMu.RUnlock()
result := make(map[string]sigData, len(refs))
for k, rb := range refs {
rt, rv := rb.slice(t0, t1)
if len(rt) == 0 {
continue
}
dt, dv := lttbDecimate(rt, rv, n)
result[k] = sigData{T: dt, V: dv}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(map[string]any{
"type": "zoom",
"signals": result,
}); err != nil {
log.Printf("hub: zoom encode: %v", err)
}
}
// AddSource notifies the Hub that a new source has been registered.
func (h *Hub) AddSource(id, label, addr string) {
select {
case h.commandCh <- hubCmd{op: "addSource", sourceID: id, label: label, addr: addr}:
default:
}
}
// RemoveSource notifies the Hub that a source has been removed.
func (h *Hub) RemoveSource(id string) {
select {
case h.commandCh <- hubCmd{op: "removeSource", sourceID: id}:
default:
}
}
// SetSourceState updates the connection state of a source.
func (h *Hub) SetSourceState(id, state string) {
select {
case h.commandCh <- hubCmd{op: "setSourceState", sourceID: id, state: state}:
default:
}
}
// UpdateConfigForSource stores a new signal config for a source and broadcasts it.
func (h *Hub) UpdateConfigForSource(sourceID string, sigs []udpsprotocol.SignalInfo) {
select {
case h.commandCh <- hubCmd{op: "updateConfig", sourceID: sourceID, sigs: sigs}:
default:
}
}
// PushDataForSource enqueues a data sample from a specific source.
func (h *Hub) PushDataForSource(sourceID string, s udpsprotocol.DataSample) {
select {
case h.dataCh <- taggedSample{sourceID: sourceID, sample: s}:
default:
}
}
// broadcast enqueues a message for delivery to all WebSocket clients.
func (h *Hub) broadcast(msg []byte) {
select {
case h.broadcastCh <- msg:
default:
}
}
// Broadcast is the exported wrapper for broadcast.
func (h *Hub) Broadcast(msg []byte) {
h.broadcast(msg)
}
// 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 wsMessage, 64)}
h.register <- c
go c.writePump()
go c.readPump()
}
// buildSourcesMsg serialises the current source list as a JSON "sources" message.
func buildSourcesMsg(sm map[string]*sourceHubState) []byte {
type srcInfo struct {
ID string `json:"id"`
Label string `json:"label"`
Addr string `json:"addr"`
State string `json:"state"`
}
list := make([]srcInfo, 0, len(sm))
for _, src := range sm {
list = append(list, srcInfo{ID: src.id, Label: src.label, Addr: src.addr, State: src.connState})
}
msg, _ := json.Marshal(map[string]interface{}{"type": "sources", "sources": list})
return msg
}
// Run is the hub's main goroutine. Must be started with go hub.Run().
func (h *Hub) Run() {
ticker := time.NewTicker(time.Second / 30)
defer ticker.Stop()
statsTicker := time.NewTicker(time.Second)
defer statsTicker.Stop()
sourcesMap := make(map[string]*sourceHubState)
var sourcesMsg []byte
// pending[sourceID] accumulates samples between 30 Hz ticks.
pending := make(map[string][]udpsprotocol.DataSample)
rebuildSources := func() {
sourcesMsg = buildSourcesMsg(sourcesMap)
h.broadcast(sourcesMsg)
}
for {
select {
case c := <-h.register:
h.clients[c] = true
// Send current state to the new client.
if sourcesMsg != nil {
select { case c.send <- wsMessage{websocket.TextMessage, sourcesMsg}: default: }
}
for _, src := range sourcesMap {
if src.configJS != nil {
select { case c.send <- wsMessage{websocket.TextMessage, src.configJS}: default: }
}
}
// Notify the application layer so it can replay any persistent state
// (e.g., MARTe2 connection status, forced/traced signals).
h.onClientConnectMu.RLock()
fn := h.onClientConnect
h.onClientConnectMu.RUnlock()
if fn != nil {
fn(func(msg []byte) {
select { case c.send <- wsMessage{websocket.TextMessage, msg}: default: }
})
}
case c := <-h.unregister:
if _, ok := h.clients[c]; ok {
delete(h.clients, c)
close(c.send)
}
case msg := <-h.broadcastCh:
for c := range h.clients {
select { case c.send <- wsMessage{websocket.TextMessage, msg}: default: }
}
case cmd := <-h.commandCh:
switch cmd.op {
case "addSource":
sourcesMap[cmd.sourceID] = &sourceHubState{
id: cmd.sourceID,
label: cmd.label,
addr: cmd.addr,
connState: "connecting",
timeSigCalib: make(map[string]float64),
}
h.statsMu.Lock()
h.statsMap[cmd.sourceID] = &SourceStat{}
h.statsMu.Unlock()
rebuildSources()
case "removeSource":
delete(sourcesMap, cmd.sourceID)
delete(pending, cmd.sourceID)
pfxDel := cmd.sourceID + ":"
h.ringsMu.Lock()
for k := range h.rings {
if strings.HasPrefix(k, pfxDel) {
delete(h.rings, k)
}
}
h.ringsMu.Unlock()
h.statsMu.Lock()
delete(h.statsMap, cmd.sourceID)
h.statsMu.Unlock()
rebuildSources()
case "setSourceState":
if src, ok := sourcesMap[cmd.sourceID]; ok {
src.connState = cmd.state
rebuildSources()
}
case "updateConfig":
src, ok := sourcesMap[cmd.sourceID]
if !ok {
continue
}
src.signals = cmd.sigs
src.configSeq++
cfgMsg, err := json.Marshal(map[string]any{
"type": "config",
"sourceId": cmd.sourceID,
"signals": cmd.sigs,
})
if err != nil {
log.Printf("hub: marshal config: %v", err)
continue
}
src.configJS = cfgMsg
h.broadcast(cfgMsg)
// Rebuild ring buffers for this source.
pfxUpd := cmd.sourceID + ":"
h.ringsMu.Lock()
for k := range h.rings {
if strings.HasPrefix(k, pfxUpd) {
delete(h.rings, k)
}
}
for _, sig := range cmd.sigs {
ne := sig.NumElements()
isTemporal := ne > 1 && sig.TimeMode != udpsprotocol.TimeModePacket
if isTemporal {
h.rings[pfxUpd+sig.Name] = newSigRing(ringCapTemporal)
} else {
// Both scalar (ne==1) and snapshot-waveform (ne>1, TimeModePacket)
// are stored under the base signal name.
h.rings[pfxUpd+sig.Name] = newSigRing(ringCapScalar)
}
}
h.ringsMu.Unlock()
case "wsAddSource":
if h.sm != nil {
go func(label, addr, mcastGroup string, dataPort int) {
h.sm.Add(label, addr, mcastGroup, dataPort)
}(cmd.label, cmd.addr, cmd.multicastGroup, cmd.dataPort)
}
case "wsRemoveSource":
if h.sm != nil {
go func(id string) { h.sm.Remove(id) }(cmd.sourceID)
}
case "wsSaveSources":
if h.sm != nil {
if err := h.sm.Save(); err != nil {
log.Printf("hub: save sources: %v", err)
}
}
}
case ts := <-h.dataCh:
pending[ts.sourceID] = append(pending[ts.sourceID], ts.sample)
case <-ticker.C:
for srcID, samples := range pending {
if len(samples) == 0 {
continue
}
src, ok := sourcesMap[srcID]
if !ok || len(src.signals) == 0 || len(h.clients) == 0 {
pending[srcID] = pending[srcID][:0]
continue
}
msg := h.buildBinaryDataMessageForSource(src, samples)
pending[srcID] = pending[srcID][:0]
if msg != nil {
for c := range h.clients {
select {
case c.send <- wsMessage{websocket.BinaryMessage, msg}:
default:
}
}
}
}
case <-statsTicker.C:
h.statsMu.RLock()
snap := make(map[string]StatInfo, len(h.statsMap))
for id, st := range h.statsMap {
snap[id] = st.Snapshot()
}
h.statsMu.RUnlock()
if len(snap) > 0 {
msg, _ := json.Marshal(map[string]any{"type": "stats", "sources": snap})
h.broadcast(msg)
}
}
}
}
// float64ToBytes reinterprets a []float64 as []byte without copying.
func float64ToBytes(f []float64) []byte {
if len(f) == 0 {
return nil
}
return unsafe.Slice((*byte)(unsafe.Pointer(&f[0])), len(f)*8)
}
// writeFloat64s encodes a []float64 as little-endian bytes into buf at offset
// and returns the new offset.
func writeFloat64s(buf []byte, off int, f []float64) int {
copy(buf[off:], float64ToBytes(f))
return off + len(f)*8
}
// ─── Data serialisation ───────────────────────────────────────────────────────
const maxPushPoints = 50
const maxRingPoints = 20_000
const ringCapTemporal = 6_000_000
const ringCapScalar = 100_000
// lttbDecimate reduces (tIn, vIn) to at most threshold representative points
// using the Largest-Triangle-Three-Buckets algorithm.
func lttbDecimate(tIn, vIn []float64, threshold int) ([]float64, []float64) {
n := len(tIn)
if n <= threshold || threshold < 3 {
return tIn, vIn
}
outT := make([]float64, threshold)
outV := make([]float64, threshold)
outT[0], outV[0] = tIn[0], vIn[0]
outT[threshold-1], outV[threshold-1] = tIn[n-1], vIn[n-1]
every := float64(n-2) / float64(threshold-2)
a := 0
for i := 0; i < threshold-2; i++ {
avgS := int(float64(i+1)*every) + 1
avgE := int(float64(i+2)*every) + 1
if avgE > n {
avgE = n
}
avgT, avgV, cnt := 0.0, 0.0, 0
for j := avgS; j < avgE; j++ {
avgT += tIn[j]; avgV += vIn[j]; cnt++
}
if cnt > 0 {
avgT /= float64(cnt); avgV /= float64(cnt)
}
rS := int(float64(i)*every) + 1
rE := int(float64(i+1)*every) + 1
if rE > n {
rE = n
}
maxArea, next := -1.0, rS
aT, aV := tIn[a], vIn[a]
for j := rS; j < rE; j++ {
area := math.Abs((aT-avgT)*(vIn[j]-aV) - (aT-tIn[j])*(avgV-aV))
if area > maxArea {
maxArea = area; next = j
}
}
outT[i+1], outV[i+1] = tIn[next], vIn[next]
a = next
}
return outT, outV
}
type sigData struct {
T []float64 `json:"t"`
V []float64 `json:"v"`
}
type dataMsg struct {
Type string `json:"type"`
SourceID string `json:"sourceId"`
Signals map[string]sigData `json:"signals"`
}
// buildBinaryDataMessageForSource encodes a batch of samples as a compact binary frame.
func (h *Hub) buildBinaryDataMessageForSource(src *sourceHubState, batch []udpsprotocol.DataSample) []byte {
if len(batch) == 0 {
return nil
}
if src.configSeq != src.configSeqAtCalib {
src.configSeqAtCalib = src.configSeq
src.timeSigCalib = make(map[string]float64)
}
sigs := src.signals
pfx := src.id + ":"
writeRing := h.shouldWriteRing()
type pairBuf struct {
t, v []float64
}
pairs := make(map[string]pairBuf, len(sigs)*2)
for _, sig := range sigs {
n := sig.NumElements()
switch {
case n > 1 && (sig.TimeMode == udpsprotocol.TimeModeFirstSample || sig.TimeMode == udpsprotocol.TimeModeLastSample):
hasTimeSig := sig.TimeSignalIdx != udpsprotocol.NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs)
var timeSigName string
timerToSec := 1e-6
if hasTimeSig {
ts := sigs[sig.TimeSignalIdx]
timeSigName = ts.Name
if ts.TypeCode == 6 {
timerToSec = 1e-9
}
}
dt := 0.0
if sig.SamplingRate > 0 {
dt = 1.0 / sig.SamplingRate
}
allT := make([]float64, 0, len(batch)*n)
allV := make([]float64, 0, len(batch)*n)
for _, s := range batch {
vals, ok := s.Values[sig.Name]
if !ok || len(vals) < n {
continue
}
var anchorTime float64
anchorIsFirstSample := sig.TimeMode == udpsprotocol.TimeModeFirstSample
if hasTimeSig {
tVals, tOk := s.Values[timeSigName]
if tOk && len(tVals) >= 1 {
timerS := tVals[0] * timerToSec
wallT := float64(s.WallTime.UnixNano()) / 1e9
if _, exists := src.timeSigCalib[timeSigName]; !exists {
src.timeSigCalib[timeSigName] = wallT - timerS
}
anchorTime = src.timeSigCalib[timeSigName] + timerS
} else {
anchorTime = float64(s.WallTime.UnixNano()) / 1e9
anchorIsFirstSample = false
}
} else {
anchorTime = float64(s.WallTime.UnixNano()) / 1e9
anchorIsFirstSample = false
}
for k := 0; k < n; k++ {
var t float64
if anchorIsFirstSample {
t = anchorTime + float64(k)*dt
} else {
t = anchorTime - float64(n-1-k)*dt
}
allT = append(allT, t)
allV = append(allV, vals[k])
}
}
if writeRing {
ringT, ringV := lttbDecimate(allT, allV, maxRingPoints)
if rb := h.getRing(pfx + sig.Name); rb != nil {
rb.write(ringT, ringV)
}
}
decimT, decimV := lttbDecimate(allT, allV, maxPushPoints)
pairs[sig.Name] = pairBuf{t: decimT, v: decimV}
case sig.TimeMode == udpsprotocol.TimeModeFullArray:
hasTimeSig := sig.TimeSignalIdx != udpsprotocol.NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs)
var timeSigName string
timerToSec := 1e-6
if hasTimeSig {
ts := sigs[sig.TimeSignalIdx]
timeSigName = ts.Name
if ts.TypeCode == 6 {
timerToSec = 1e-9
}
}
allT := make([]float64, 0, len(batch)*n)
allV := make([]float64, 0, len(batch)*n)
for _, s := range batch {
vals, ok := s.Values[sig.Name]
if !ok || len(vals) < n {
continue
}
if hasTimeSig {
tVals, tOk := s.Values[timeSigName]
if tOk && len(tVals) >= n {
if _, exists := src.timeSigCalib[timeSigName]; !exists {
wallT := float64(s.WallTime.UnixNano()) / 1e9
src.timeSigCalib[timeSigName] = wallT - tVals[0]*timerToSec
}
calib := src.timeSigCalib[timeSigName]
for k := 0; k < n; k++ {
allT = append(allT, calib+tVals[k]*timerToSec)
allV = append(allV, vals[k])
}
continue
}
}
wallT := float64(s.WallTime.UnixNano()) / 1e9
for k := 0; k < n; k++ {
allT = append(allT, wallT)
allV = append(allV, vals[k])
}
}
if writeRing {
ringT, ringV := lttbDecimate(allT, allV, maxRingPoints)
if rb := h.getRing(pfx + sig.Name); rb != nil {
rb.write(ringT, ringV)
}
}
decimT, decimV := lttbDecimate(allT, allV, maxPushPoints)
pairs[sig.Name] = pairBuf{t: decimT, v: decimV}
case n == 1:
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])
}
if writeRing {
if rb := h.getRing(pfx + sig.Name); rb != nil {
rb.write(ts, vs)
}
}
pairs[sig.Name] = pairBuf{t: ts, v: vs}
default:
// n > 1, TimeModePacket: no samplingRate from C++, so we interpolate
// timestamps from wall-clock differences between consecutive packets.
// Each packet covers n elements; dt per element ≈ (T_{i+1}-T_i)/n.
allT := make([]float64, 0, len(batch)*n)
allV := make([]float64, 0, len(batch)*n)
for bi, s := range batch {
vals, ok := s.Values[sig.Name]
if !ok || len(vals) < n {
continue
}
wallSec := float64(s.WallTime.UnixNano()) / 1e9
var dtSec float64
if bi+1 < len(batch) {
dtSec = (float64(batch[bi+1].WallTime.UnixNano())-float64(s.WallTime.UnixNano()))/1e9/float64(n)
} else if bi > 0 {
dtSec = (float64(s.WallTime.UnixNano())-float64(batch[bi-1].WallTime.UnixNano()))/1e9/float64(n)
} else {
dtSec = 1.0 / float64(n) // single-sample fallback
}
for j := 0; j < n; j++ {
allT = append(allT, wallSec+float64(j)*dtSec)
allV = append(allV, vals[j])
}
}
if len(allT) > 0 {
if writeRing {
ringT, ringV := lttbDecimate(allT, allV, maxRingPoints)
if rb := h.getRing(pfx + sig.Name); rb != nil {
rb.write(ringT, ringV)
}
}
decimT, decimV := lttbDecimate(allT, allV, maxPushPoints)
pairs[sig.Name] = pairBuf{t: decimT, v: decimV}
}
}
}
// Compute total size and serialize
totalSize := 1 + 1 + len(src.id) + 4
for key, p := range pairs {
totalSize += 2 + len(key) + 4
totalSize += len(p.t)*8 + len(p.v)*8
}
buf := make([]byte, totalSize)
buf[0] = 1 // version
buf[1] = byte(len(src.id))
copy(buf[2:], src.id)
off := 2 + len(src.id)
binary.LittleEndian.PutUint32(buf[off:], uint32(len(pairs)))
off += 4
for key, p := range pairs {
binary.LittleEndian.PutUint16(buf[off:], uint16(len(key)))
off += 2
copy(buf[off:], key)
off += len(key)
binary.LittleEndian.PutUint32(buf[off:], uint32(len(p.t)))
off += 4
off = writeFloat64s(buf, off, p.t)
off = writeFloat64s(buf, off, p.v)
}
return buf
}
// RecordDataFragment is called by UDPClient for every incoming DATA datagram.
func (h *Hub) RecordDataFragment(sourceID string, counter uint32, nBytes int, arrivalNs int64, complete bool) {
h.statsMu.RLock()
st := h.statsMap[sourceID]
h.statsMu.RUnlock()
if st != nil {
st.RecordFragment(counter, nBytes, arrivalNs, complete)
}
}
// 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:])
}
+88
View File
@@ -0,0 +1,88 @@
package wshub
import "sync"
// sigRing is a fixed-capacity circular buffer storing (time, value) pairs.
// Writes come from the Hub.Run() goroutine; reads come from HTTP handler goroutines.
// The embedded RWMutex protects concurrent access.
type sigRing struct {
mu sync.RWMutex
t, v []float64
cap int
head, size int // next write position; current fill
}
func newSigRing(capacity int) *sigRing {
return &sigRing{
t: make([]float64, capacity),
v: make([]float64, capacity),
cap: capacity,
}
}
// write appends (tArr[i], vArr[i]) pairs, overwriting oldest entries when full.
func (rb *sigRing) write(tArr, vArr []float64) {
rb.mu.Lock()
defer rb.mu.Unlock()
for i := 0; i < len(tArr); i++ {
rb.t[rb.head] = tArr[i]
rb.v[rb.head] = vArr[i]
rb.head = (rb.head + 1) % rb.cap
if rb.size < rb.cap {
rb.size++
}
}
}
// slice returns copies of all (t, v) pairs whose timestamp falls in [t0, t1].
// The returned slices are safe to use after the call without holding any lock.
func (rb *sigRing) slice(t0, t1 float64) ([]float64, []float64) {
rb.mu.RLock()
defer rb.mu.RUnlock()
if rb.size == 0 {
return nil, nil
}
start := 0
if rb.size == rb.cap {
start = rb.head
}
physAt := func(k int) int { return (start + k) % rb.cap }
// Binary search for t0
lo, hi := 0, rb.size
for lo < hi {
m := (lo + hi) >> 1
if rb.t[physAt(m)] < t0 {
lo = m + 1
} else {
hi = m
}
}
kStart := lo
// Binary search for t1
lo, hi = kStart, rb.size
for lo < hi {
m := (lo + hi) >> 1
if rb.t[physAt(m)] <= t1 {
lo = m + 1
} else {
hi = m
}
}
kEnd := lo
n := kEnd - kStart
if n <= 0 {
return nil, nil
}
outT := make([]float64, n)
outV := make([]float64, n)
for i := 0; i < n; i++ {
p := physAt(kStart + i)
outT[i] = rb.t[p]
outV[i] = rb.v[p]
}
return outT, outV
}
+483
View File
@@ -0,0 +1,483 @@
package wshub
import (
"encoding/json"
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"marte2/common/udpsprotocol"
)
// ─── Source configuration ─────────────────────────────────────────────────────
// SourceConfig is the serialisable description of one data source (for file save/load).
type SourceConfig struct {
Label string `json:"label"`
Addr string `json:"addr"`
MulticastGroup string `json:"multicastGroup,omitempty"`
DataPort int `json:"dataPort,omitempty"`
}
// managedSource is the SourceManager's view of one running source.
type managedSource struct {
id string
label string
addr string
multicastGroup string
dataPort int
client *UDPClient
}
// SourceManager owns the lifecycle of all active data sources.
type SourceManager struct {
mu sync.RWMutex
sources map[string]*managedSource
hub *Hub
filePath string
nextID atomic.Int32
}
// NewSourceManager creates a SourceManager bound to the given hub.
func NewSourceManager(hub *Hub, filePath string) *SourceManager {
return &SourceManager{
sources: make(map[string]*managedSource),
hub: hub,
filePath: filePath,
}
}
func (sm *SourceManager) genID() string {
return fmt.Sprintf("s%d", sm.nextID.Add(1))
}
// Add creates a new source and starts connecting.
func (sm *SourceManager) Add(label, addr, multicastGroup string, dataPort int) string {
if label == "" {
label = addr
}
id := sm.genID()
c := NewUDPClient(addr, id, sm.hub, multicastGroup, dataPort)
ms := &managedSource{
id: id, label: label, addr: addr,
multicastGroup: multicastGroup, dataPort: dataPort,
client: c,
}
sm.mu.Lock()
sm.sources[id] = ms
sm.mu.Unlock()
sm.hub.AddSource(id, label, addr)
go c.Run()
return id
}
// Remove stops the source and removes it from the hub.
func (sm *SourceManager) Remove(id string) {
sm.mu.Lock()
ms, ok := sm.sources[id]
if ok {
delete(sm.sources, id)
}
sm.mu.Unlock()
if ok {
ms.client.Stop()
sm.hub.RemoveSource(id)
}
}
// Save writes the current source list to filePath.
func (sm *SourceManager) Save() error {
if sm.filePath == "" {
return fmt.Errorf("no sources-file configured")
}
sm.mu.RLock()
cfgs := make([]SourceConfig, 0, len(sm.sources))
for _, ms := range sm.sources {
cfgs = append(cfgs, SourceConfig{
Label: ms.label,
Addr: ms.addr,
MulticastGroup: ms.multicastGroup,
DataPort: ms.dataPort,
})
}
sm.mu.RUnlock()
data, err := json.MarshalIndent(cfgs, "", " ")
if err != nil {
return err
}
return os.WriteFile(sm.filePath, data, 0644)
}
// Load reads sources from path and adds them.
func (sm *SourceManager) Load(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
var cfgs []SourceConfig
if err := json.Unmarshal(data, &cfgs); err != nil {
return err
}
sm.filePath = path
for _, cfg := range cfgs {
sm.Add(cfg.Label, cfg.Addr, cfg.MulticastGroup, cfg.DataPort)
}
return nil
}
// ParseSourceArg parses "label@host:port" or "host:port".
func ParseSourceArg(s string) (label, addr string) {
label, addr, _, _ = ParseSourceArgFull(s)
return
}
// ParseSourceArgFull parses "[label@]host:port[/multicastGroup:dataPort]".
func ParseSourceArgFull(s string) (label, addr, multicastGroup string, dataPort int) {
s = strings.TrimSpace(s)
rest := s
if idx := strings.Index(s, "@"); idx >= 0 {
label = strings.TrimSpace(s[:idx])
rest = strings.TrimSpace(s[idx+1:])
}
if idx := strings.Index(rest, "/"); idx >= 0 {
addr = strings.TrimSpace(rest[:idx])
mcastPart := strings.TrimSpace(rest[idx+1:])
if lastColon := strings.LastIndex(mcastPart, ":"); lastColon >= 0 {
multicastGroup = strings.TrimSpace(mcastPart[:lastColon])
dataPort, _ = strconv.Atoi(strings.TrimSpace(mcastPart[lastColon+1:]))
} else {
multicastGroup = mcastPart
}
} else {
addr = rest
}
return
}
// ─── UDPClient ────────────────────────────────────────────────────────────────
const (
silenceTimeout = 5 * time.Second
reconnectDelay = 2 * time.Second
readBufSize = 65536
udpRcvBufSize = 8 * 1024 * 1024
)
// UDPClient manages the connection to one MARTe2 streamer source.
type UDPClient struct {
serverAddr string
sourceID string
hub *Hub
multicastGroup string
dataPort int
stopCh chan struct{}
}
// NewUDPClient creates a UDPClient bound to a specific source ID.
func NewUDPClient(serverAddr, sourceID string, hub *Hub, multicastGroup string, dataPort int) *UDPClient {
return &UDPClient{
serverAddr: serverAddr,
sourceID: sourceID,
hub: hub,
multicastGroup: multicastGroup,
dataPort: dataPort,
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:
}
u.hub.SetSourceState(u.sourceID, "connecting")
log.Printf("[%s] connecting to %s", u.sourceID, u.serverAddr)
var err error
if u.multicastGroup != "" {
err = u.runMulticastSession()
} else {
err = u.runSession()
}
if err != nil {
log.Printf("[%s] session ended: %v", u.sourceID, err)
}
u.hub.SetSourceState(u.sourceID, "disconnected")
select {
case <-u.stopCh:
return
case <-time.After(reconnectDelay):
}
}
}
// runSession opens a UDP socket, sends CONNECT, reads data until silent or error.
func (u *UDPClient) runSession() error {
conn, err := net.ListenUDP("udp4", &net.UDPAddr{})
if err != nil {
return err
}
defer conn.Close()
if err := conn.SetReadBuffer(udpRcvBufSize); err != nil {
log.Printf("[%s] udp: SetReadBuffer: %v", u.sourceID, err)
}
serverAddr, err := net.ResolveUDPAddr("udp4", u.serverAddr)
if err != nil {
return err
}
if _, err := conn.WriteToUDP(udpsprotocol.BuildConnectPacket(), serverAddr); err != nil {
return err
}
log.Printf("[%s] udp: sent CONNECT", u.sourceID)
reassembler := udpsprotocol.NewReassembler(2 * time.Second)
buf := make([]byte, readBufSize)
var currentSigs []udpsprotocol.SignalInfo
var currentPublishMode uint8
for {
conn.SetReadDeadline(time.Now().Add(silenceTimeout))
n, _, err := conn.ReadFromUDP(buf)
arrivalTime := time.Now()
if err != nil {
conn.WriteToUDP(udpsprotocol.BuildDisconnectPacket(), serverAddr)
return err
}
if n < udpsprotocol.HeaderSize {
log.Printf("[%s] udp: short datagram (%d bytes), skipping", u.sourceID, n)
continue
}
hdr, err := udpsprotocol.ParseHeader(buf[:n])
if err != nil {
log.Printf("[%s] udp: parse header: %v", u.sourceID, err)
continue
}
payload := make([]byte, n-udpsprotocol.HeaderSize)
copy(payload, buf[udpsprotocol.HeaderSize:n])
complete, ok := reassembler.AddFragment(hdr, payload)
if hdr.Type == udpsprotocol.PktData {
u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok)
}
if !ok {
continue
}
switch hdr.Type {
case udpsprotocol.PktConfig:
sigs, pm, err := udpsprotocol.ParseConfig(complete)
if err != nil {
log.Printf("[%s] udp: parse config: %v", u.sourceID, err)
continue
}
currentSigs = sigs
currentPublishMode = pm
log.Printf("[%s] udp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(sigs), pm)
u.hub.SetSourceState(u.sourceID, "connected")
u.hub.UpdateConfigForSource(u.sourceID, sigs)
case udpsprotocol.PktData:
if len(currentSigs) == 0 {
continue
}
samples, err := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
if err != nil {
log.Printf("[%s] udp: parse data: %v", u.sourceID, err)
continue
}
for _, s := range samples {
u.hub.PushDataForSource(u.sourceID, s)
}
case udpsprotocol.PktACK:
log.Printf("[%s] udp: received ACK (counter=%d)", u.sourceID, hdr.Counter)
case udpsprotocol.PktDisconnect:
log.Printf("[%s] udp: server sent DISCONNECT", u.sourceID)
return nil
default:
log.Printf("[%s] udp: unknown packet type %d", u.sourceID, hdr.Type)
}
select {
case <-u.stopCh:
conn.WriteToUDP(udpsprotocol.BuildDisconnectPacket(), serverAddr)
return nil
default:
}
}
}
// runMulticastSession handles the multicast mode session.
func (u *UDPClient) runMulticastSession() error {
tcpAddr, err := net.ResolveTCPAddr("tcp4", u.serverAddr)
if err != nil {
return err
}
tcpConn, err := net.DialTCP("tcp4", nil, tcpAddr)
if err != nil {
return err
}
defer tcpConn.Close()
if _, err := tcpConn.Write(udpsprotocol.BuildConnectPacket()); err != nil {
return err
}
log.Printf("[%s] tcp: sent CONNECT to %s", u.sourceID, u.serverAddr)
hdrBuf := make([]byte, udpsprotocol.HeaderSize)
if _, err := io.ReadFull(tcpConn, hdrBuf); err != nil {
return err
}
cfgHdr, err := udpsprotocol.ParseHeader(hdrBuf)
if err != nil {
return err
}
if cfgHdr.Type != udpsprotocol.PktConfig {
return net.ErrClosed
}
cfgPayload := make([]byte, cfgHdr.PayloadBytes)
if cfgHdr.PayloadBytes > 0 {
if _, err := io.ReadFull(tcpConn, cfgPayload); err != nil {
return err
}
}
currentSigs, currentPublishMode, err := udpsprotocol.ParseConfig(cfgPayload)
if err != nil {
return err
}
log.Printf("[%s] tcp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(currentSigs), currentPublishMode)
u.hub.SetSourceState(u.sourceID, "connected")
u.hub.UpdateConfigForSource(u.sourceID, currentSigs)
mcastPort := u.dataPort
if mcastPort == 0 {
mcastPort = tcpAddr.Port + 1
}
mcastIP := net.ParseIP(u.multicastGroup)
if mcastIP == nil {
return &net.AddrError{Err: "invalid multicast group IP", Addr: u.multicastGroup}
}
mcastAddr := &net.UDPAddr{IP: mcastIP, Port: mcastPort}
mcastConn, err := net.ListenMulticastUDP("udp4", nil, mcastAddr)
if err != nil {
return err
}
defer mcastConn.Close()
if err := mcastConn.SetReadBuffer(udpRcvBufSize); err != nil {
log.Printf("[%s] multicast SetReadBuffer: %v", u.sourceID, err)
}
log.Printf("[%s] joined multicast %s:%s", u.sourceID, u.multicastGroup, strconv.Itoa(mcastPort))
tcpDone := make(chan error, 1)
go func() {
buf := make([]byte, udpsprotocol.HeaderSize+64)
for {
n, readErr := tcpConn.Read(buf)
if readErr != nil {
tcpDone <- readErr
return
}
if n >= udpsprotocol.HeaderSize {
hdr, parseErr := udpsprotocol.ParseHeader(buf[:n])
if parseErr == nil && hdr.Type == udpsprotocol.PktDisconnect {
tcpDone <- nil
return
}
}
}
}()
reassembler := udpsprotocol.NewReassembler(2 * time.Second)
buf := make([]byte, readBufSize)
for {
mcastConn.SetReadDeadline(time.Now().Add(silenceTimeout))
n, _, readErr := mcastConn.ReadFromUDP(buf)
arrivalTime := time.Now()
if readErr != nil {
select {
case <-tcpDone:
return nil
default:
}
tcpConn.Write(udpsprotocol.BuildDisconnectPacket())
return readErr
}
if n < udpsprotocol.HeaderSize {
continue
}
hdr, parseErr := udpsprotocol.ParseHeader(buf[:n])
if parseErr != nil {
log.Printf("[%s] multicast: parse header: %v", u.sourceID, parseErr)
continue
}
payload := make([]byte, n-udpsprotocol.HeaderSize)
copy(payload, buf[udpsprotocol.HeaderSize:n])
complete, ok := reassembler.AddFragment(hdr, payload)
if hdr.Type == udpsprotocol.PktData {
u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok)
}
if !ok {
continue
}
if hdr.Type == udpsprotocol.PktData {
if len(currentSigs) == 0 {
continue
}
samples, parseErr := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
if parseErr != nil {
log.Printf("[%s] multicast: parse data: %v", u.sourceID, parseErr)
continue
}
for _, s := range samples {
u.hub.PushDataForSource(u.sourceID, s)
}
}
select {
case <-u.stopCh:
tcpConn.Write(udpsprotocol.BuildDisconnectPacket())
return nil
case tcpErr := <-tcpDone:
log.Printf("[%s] tcp control closed: %v", u.sourceID, tcpErr)
return nil
default:
}
}
}
+155
View File
@@ -0,0 +1,155 @@
package wshub
import (
"math"
"sync"
)
const statRingSize = 512
// SourceStat accumulates per-source UDP performance metrics.
// Thread-safe; RecordFragment is called from the UDPClient goroutine.
type SourceStat struct {
mu sync.Mutex
seenFirst bool
lastCounter uint32
TotalRx uint64
TotalLost uint64
// Cycle-time ring (seconds between consecutive DATA completions)
ctRing [statRingSize]float64
ctHead int
ctFull bool
lastRxNs int64
// Per-cycle accumulators (reset after each DATA completion)
fragCount int
byteCount int
fragRing [statRingSize]int
byteRing [statRingSize]int
}
// RecordFragment is called for every UDP datagram of a DATA packet.
// complete: this fragment completed the DATA reassembly.
// nBytes: raw datagram size (header+payload).
func (s *SourceStat) RecordFragment(counter uint32, nBytes int, arrivalNs int64, complete bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.fragCount++
s.byteCount += nBytes
if !complete {
return
}
s.TotalRx++
if s.seenFirst {
if delta := counter - s.lastCounter; delta > 1 {
s.TotalLost += uint64(delta - 1)
}
} else {
s.seenFirst = true
}
s.lastCounter = counter
if s.lastRxNs != 0 {
idx := s.ctHead
s.ctRing[idx] = float64(arrivalNs-s.lastRxNs) * 1e-9
s.fragRing[idx] = s.fragCount
s.byteRing[idx] = s.byteCount
s.ctHead = (s.ctHead + 1) % statRingSize
if s.ctHead == 0 {
s.ctFull = true
}
}
s.lastRxNs = arrivalNs
s.fragCount = 0
s.byteCount = 0
}
// Snapshot computes and returns a StatInfo for broadcast.
func (s *SourceStat) Snapshot() StatInfo {
s.mu.Lock()
defer s.mu.Unlock()
n := s.ctHead
if s.ctFull {
n = statRingSize
}
si := StatInfo{TotalReceived: s.TotalRx, TotalLost: s.TotalLost}
if n == 0 {
return si
}
sum, sumSq := 0.0, 0.0
minV, maxV := math.MaxFloat64, 0.0
fragSum, byteSum := 0, 0
for i := 0; i < n; i++ {
v := s.ctRing[i]
sum += v
sumSq += v * v
if v < minV {
minV = v
}
if v > maxV {
maxV = v
}
fragSum += s.fragRing[i]
byteSum += s.byteRing[i]
}
avg := sum / float64(n)
variance := sumSq/float64(n) - avg*avg
if variance < 0 {
variance = 0
}
stdv := math.Sqrt(variance)
si.CycleAvgMs = avg * 1e3
si.CycleStdMs = stdv * 1e3
si.CycleMinMs = minV * 1e3
si.CycleMaxMs = maxV * 1e3
si.RateHz = 1.0 / avg
si.RateStdHz = stdv / (avg * avg)
si.FragsPerCycle = float64(fragSum) / float64(n)
si.BytesPerCycle = float64(byteSum) / float64(n)
const nBins = 20
si.CycleHistMin = minV * 1e3
si.CycleHistMax = maxV * 1e3
si.CycleHist = make([]int, nBins)
span := maxV - minV
for i := 0; i < n; i++ {
var bin int
if span > 0 {
bin = int((s.ctRing[i] - minV) / span * float64(nBins))
if bin >= nBins {
bin = nBins - 1
}
} else {
bin = nBins / 2
}
si.CycleHist[bin]++
}
return si
}
// StatInfo is the JSON snapshot for one source sent to the frontend.
type StatInfo struct {
TotalReceived uint64 `json:"totalReceived"`
TotalLost uint64 `json:"totalLost"`
RateHz float64 `json:"rateHz"`
RateStdHz float64 `json:"rateStdHz"`
FragsPerCycle float64 `json:"fragsPerCycle"`
BytesPerCycle float64 `json:"bytesPerCycle"`
CycleAvgMs float64 `json:"cycleAvgMs"`
CycleStdMs float64 `json:"cycleStdMs"`
CycleMinMs float64 `json:"cycleMinMs"`
CycleMaxMs float64 `json:"cycleMaxMs"`
CycleHist []int `json:"cycleHist"`
CycleHistMin float64 `json:"cycleHistMin"`
CycleHistMax float64 `json:"cycleHistMax"`
}
+234
View File
@@ -0,0 +1,234 @@
/**
* @file UDPSProtocol.h
* @brief Shared UDPS binary streaming protocol definitions.
*
* This header is free of MARTe2-specific types (other than CompilerTypes.h)
* so it can be included by both UDPStreamer (DataSource) and DebugService
* (Interface), as well as any future component that needs to produce or
* consume UDPS packets.
*
* Protocol overview
* -----------------
* Every packet starts with a 17-byte packed header (UDPSPacketHeader).
* Large payloads are split into multiple fragments sharing the same counter.
*
* Packet types:
* DATA (0) - signal sample values, Server → Client
* CONFIG (1) - signal metadata, Server → Client (sent when set changes)
* ACK (2) - counter acknowledgment, Client → Server (optional)
* CONNECT (3) - session initiation, Client → Server
* DISCONNECT (4) - session teardown, Client → Server or Server → Client
*
* CONFIG payload:
* [uint32 numSigs]
* numSigs × UDPSSignalDescriptor (136 bytes each, packed)
* [uint8 publishMode] (PublishModeStrict / Accumulate / Decimate)
*
* DATA payload (Strict / Decimate):
* [uint64 HRT timestamp]
* per-signal data in CONFIG order (quantised or raw, no padding)
*
* DATA payload (Accumulate):
* [uint64 HRT timestamp]
* [uint32 numSamples]
* for each signal: if scalar → numSamples elements; else → NumElements once
*/
#ifndef UDPS_PROTOCOL_H_
#define UDPS_PROTOCOL_H_
#include "CompilerTypes.h"
namespace MARTe {
/*---------------------------------------------------------------------------*/
/* Magic and packet types */
/*---------------------------------------------------------------------------*/
/** Magic number: ASCII 'UDPS' stored little-endian (0x53504455). */
static const uint32 UDPS_MAGIC = 0x53504455u;
/** Packet type constants — must match the Go-side PktXxx constants. */
static const uint8 UDPS_TYPE_DATA = 0u; ///< Server → Client: signal data
static const uint8 UDPS_TYPE_CONFIG = 1u; ///< Server → Client: signal config
static const uint8 UDPS_TYPE_ACK = 2u; ///< Client → Server: ack counter
static const uint8 UDPS_TYPE_CONNECT = 3u; ///< Client → Server: connect request
static const uint8 UDPS_TYPE_DISCONNECT = 4u; ///< Client → Server: disconnect
/*---------------------------------------------------------------------------*/
/* Wire packet header (17 bytes) */
/*---------------------------------------------------------------------------*/
/**
* @brief 17-byte packed UDP packet header, little-endian.
*
* All fields use unsigned integer types; no padding. Placed at offset 0
* of every UDPS datagram.
*/
#pragma pack(push, 1)
struct UDPSPacketHeader {
uint32 magic; ///< Must equal UDPS_MAGIC
uint8 type; ///< One of UDPS_TYPE_*
uint32 counter; ///< Per-update sequence counter (same for all fragments)
uint16 fragmentIdx; ///< 0-based index of this fragment
uint16 totalFragments; ///< Total fragments for this update (1 = no fragmentation)
uint32 payloadBytes; ///< Payload bytes immediately following this header
};
#pragma pack(pop)
static const uint32 UDPS_HEADER_SIZE = 17u; ///< sizeof(UDPSPacketHeader)
/*---------------------------------------------------------------------------*/
/* Signal type codes (CONFIG wire format) */
/*---------------------------------------------------------------------------*/
/** Compact type codes stored in UDPSSignalDescriptor::typeCode. */
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;
/*---------------------------------------------------------------------------*/
/* Quantisation and time-mode codes */
/*---------------------------------------------------------------------------*/
/** Quantisation codes stored in UDPSSignalDescriptor::quantType. */
static const uint8 UDPS_QUANT_NONE = 0u; ///< No quantisation; raw wire format
static const uint8 UDPS_QUANT_UINT8 = 1u; ///< Map to uint8 [0, 255]
static const uint8 UDPS_QUANT_INT8 = 2u; ///< Map to int8 [-127, 127]
static const uint8 UDPS_QUANT_UINT16 = 3u; ///< Map to uint16 [0, 65535]
static const uint8 UDPS_QUANT_INT16 = 4u; ///< Map to int16 [-32767, 32767]
/** Time-mode codes stored in UDPSSignalDescriptor::timeMode. */
static const uint8 UDPS_TIMEMODE_PACKET = 0u; ///< Use packet-level HRT timestamp
static const uint8 UDPS_TIMEMODE_FULL_ARRAY = 1u; ///< TimeSignal has NumElements timestamps
static const uint8 UDPS_TIMEMODE_FIRST_SAMPLE = 2u; ///< TimeSignal scalar = timestamp of element [0]
static const uint8 UDPS_TIMEMODE_LAST_SAMPLE = 3u; ///< TimeSignal scalar = timestamp of element [N-1]
/** Sentinel value for UDPSSignalDescriptor::timeSignalIdx when no time signal. */
static const uint32 UDPS_NO_TIME_SIGNAL = 0xFFFFFFFFu;
/*---------------------------------------------------------------------------*/
/* Publish mode codes (CONFIG trailing byte) */
/*---------------------------------------------------------------------------*/
static const uint8 UDPS_PUBLISH_STRICT = 0u; ///< One packet per Synchronise() call
static const uint8 UDPS_PUBLISH_ACCUMULATE = 1u; ///< Variable batch; flush on size or time
static const uint8 UDPS_PUBLISH_DECIMATE = 2u; ///< One packet per Ratio calls
/*---------------------------------------------------------------------------*/
/* CONFIG payload — per-signal descriptor */
/*---------------------------------------------------------------------------*/
/** Maximum length of signal name and unit strings in the CONFIG payload. */
static const uint32 UDPS_MAX_SIGNAL_NAME = 64u;
static const uint32 UDPS_MAX_UNIT_LEN = 32u;
/** Byte size of one serialised UDPSSignalDescriptor on the wire. */
static const uint32 UDPS_SIGNAL_DESC_SIZE = 136u;
/**
* @brief Wire-format per-signal descriptor embedded in the CONFIG payload.
*
* Contains only plain C types so it can be memcpy'd directly to/from
* the network buffer (little-endian on all supported platforms).
*
* Size: 64 + 1 + 1 + 1 + 4 + 4 + 8 + 8 + 1 + 8 + 4 + 32 = 136 bytes.
*/
#pragma pack(push, 1)
struct UDPSSignalDescriptor {
char8 name[UDPS_MAX_SIGNAL_NAME]; ///< Null-terminated signal name
uint8 typeCode; ///< UDPS_TYPECODE_*
uint8 quantType; ///< UDPS_QUANT_*
uint8 numDimensions; ///< 0=scalar, 1=array, 2=matrix
uint32 numRows; ///< Number of rows (or elements for 1D)
uint32 numCols; ///< Number of columns (1 for 1D/scalar)
float64 rangeMin; ///< Physical range minimum (for quantisation)
float64 rangeMax; ///< Physical range maximum (for quantisation)
uint8 timeMode; ///< UDPS_TIMEMODE_*
float64 samplingRate; ///< Hz; used for FirstSample/LastSample modes
uint32 timeSignalIdx; ///< Index of time-ref signal; UDPS_NO_TIME_SIGNAL if none
char8 unit[UDPS_MAX_UNIT_LEN]; ///< Null-terminated physical unit string
};
#pragma pack(pop)
/*---------------------------------------------------------------------------*/
/* TypeDescriptor → UDPS type-code mapping */
/*---------------------------------------------------------------------------*/
/**
* @brief Maps a MARTe2 TypeDescriptor to the compact UDPS_TYPECODE_* value.
*
* Returns UDPS_TYPECODE_UNKNOWN for types that have no UDPS equivalent.
* Defined inline to avoid a separate translation unit for this trivial table.
*/
inline uint8 UDPSTypeDescriptorToCode(TypeDescriptor td) {
if (td == UnsignedInteger8Bit) return UDPS_TYPECODE_UINT8;
if (td == SignedInteger8Bit) return UDPS_TYPECODE_INT8;
if (td == UnsignedInteger16Bit) return UDPS_TYPECODE_UINT16;
if (td == SignedInteger16Bit) return UDPS_TYPECODE_INT16;
if (td == UnsignedInteger32Bit) return UDPS_TYPECODE_UINT32;
if (td == SignedInteger32Bit) return UDPS_TYPECODE_INT32;
if (td == UnsignedInteger64Bit) return UDPS_TYPECODE_UINT64;
if (td == SignedInteger64Bit) return UDPS_TYPECODE_INT64;
if (td == Float32Bit) return UDPS_TYPECODE_FLOAT32;
if (td == Float64Bit) return UDPS_TYPECODE_FLOAT64;
return UDPS_TYPECODE_UNKNOWN;
}
/**
* @brief Returns the wire byte size of one element for the given type code.
* Returns 0 for UDPS_TYPECODE_UNKNOWN.
*/
inline uint32 UDPSTypeCodeByteSize(uint8 typeCode) {
switch (typeCode) {
case UDPS_TYPECODE_UINT8: case UDPS_TYPECODE_INT8: return 1u;
case UDPS_TYPECODE_UINT16: case UDPS_TYPECODE_INT16: return 2u;
case UDPS_TYPECODE_UINT32: case UDPS_TYPECODE_INT32:
case UDPS_TYPECODE_FLOAT32: return 4u;
case UDPS_TYPECODE_UINT64: case UDPS_TYPECODE_INT64:
case UDPS_TYPECODE_FLOAT64: return 8u;
default: return 0u;
}
}
/*---------------------------------------------------------------------------*/
/* Inline packet-building helpers */
/*---------------------------------------------------------------------------*/
/**
* @brief Serialise a UDPSPacketHeader into the first 17 bytes of @p buf.
* @param buf Must have room for at least UDPS_HEADER_SIZE bytes.
* @param type Packet type (UDPS_TYPE_*).
* @param counter Per-update sequence counter.
* @param fragIdx 0-based fragment index.
* @param totalFrags Total fragments for this update.
* @param payloadBytes Bytes of payload that follow this header.
*/
inline void UDPSBuildHeader(uint8 *buf,
uint8 type,
uint32 counter,
uint16 fragIdx,
uint16 totalFrags,
uint32 payloadBytes)
{
UDPSPacketHeader hdr;
hdr.magic = UDPS_MAGIC;
hdr.type = type;
hdr.counter = counter;
hdr.fragmentIdx = fragIdx;
hdr.totalFragments = totalFrags;
hdr.payloadBytes = payloadBytes;
memcpy(buf, &hdr, UDPS_HEADER_SIZE);
}
} /* namespace MARTe */
#endif /* UDPS_PROTOCOL_H_ */
+241
View File
@@ -0,0 +1,241 @@
# DebugService
`DebugService` is a MARTe2 `Object` that instruments a running MARTe2 application for
real-time signal tracing, forcing, and breakpoints — **without modifying the application
source code**.
It lives at `Source/Components/Interfaces/DebugService/` in this repository and is built
alongside `TCPLogger` at `Source/Components/Interfaces/TCPLogger/`.
---
## Architecture
### Core Mechanism: Registry Patching
On `Initialise()`, `PatchRegistry()` replaces the `ObjectBuilder` for every standard
`MemoryMap*Broker` class in the `ClassRegistryDatabase`. When
`RealTimeApplication::ConfigureApplication()` runs afterward it instantiates wrapped broker
objects instead of the originals. The application sees no difference.
Patched broker types:
| Class | Wrapper |
|---|---|
| `MemoryMapInputBroker` | `DebugBrokerWrapper<MemoryMapInputBroker>` |
| `MemoryMapOutputBroker` | `DebugBrokerWrapper<MemoryMapOutputBroker>` |
| `MemoryMapSynchronisedInputBroker` | `DebugBrokerWrapper<MemoryMapSynchronisedInputBroker>` |
| `MemoryMapSynchronisedOutputBroker` | `DebugBrokerWrapper<MemoryMapSynchronisedOutputBroker>` |
| `MemoryMapMultiBufferBroker` | `DebugBrokerWrapper<MemoryMapMultiBufferBroker>` |
| `MemoryMapMultiBufferOutputBroker` | `DebugBrokerWrapper<MemoryMapMultiBufferOutputBroker>` |
| `MemoryMapAsynchronousInputBroker` | `DebugBrokerWrapperAsync<MemoryMapAsynchronousInputBroker>` |
| `MemoryMapAsynchronousOutputBroker` | `DebugBrokerWrapperAsync<MemoryMapAsynchronousOutputBroker>` |
| `MemoryMapInterpolatedInputBroker` | `DebugBrokerWrapper<MemoryMapInterpolatedInputBroker>` |
| `MemoryMapStatefulOutputBroker` | `DebugBrokerWrapper<MemoryMapStatefulOutputBroker>` |
| `MemoryMapStatefulInputBroker` | `DebugBrokerWrapper<MemoryMapStatefulInputBroker>` |
### DebugServiceI Abstraction
`DebugServiceI.h` defines an abstract singleton interface decoupling the broker injection
layer from the transport:
- **RT-path API** (called every RT cycle): `RegisterSignal`, `ProcessSignal`, `RegisterBroker`,
`IsPaused`/`SetPaused`, `ConsumeStepIfNeeded`
- **Control-path API**: `ForceSignal`/`UnforceSignal`, `TraceSignal`, `SetBreak`/`ClearBreak`,
`RegisterMonitorSignal`/`UnmonitorSignal`
### Signal Registration and Aliases
Each signal is registered with two names:
1. **Canonical**: `<DataSourcePath>.<SignalName>` (e.g. `App.Data.DDB.Counter`)
2. **GAM alias**: `<GAMPath>.In.<SignalName>` or `<GAMPath>.Out.<SignalName>`
Both map to the same `DebugSignalInfo*`. Alias lookup is bidirectional — a short unqualified
name matches any longer canonical or alias path.
### Communication Channels
| Port | Protocol | Purpose |
|------|----------|---------|
| 8080 (default) | TCP text | Command and control |
| 8081 (default) | UDP binary | High-speed telemetry stream |
| 8082 (default) | TCP stream | Log forwarding via `TcpLogger` |
### TraceRingBuffer
Single-producer/single-consumer circular byte buffer (4 MB default) defined in `DebugCore.h`.
Buffer layout per entry: `[ID:4][Timestamp:8][Size:4][Data:N]`.
`Push()` is serialised by `tracePushMutex` (multiple RT threads write).
`Pop()` is called exclusively by the Streamer thread.
---
## Configuration
Add `DebugService` as a **sibling** of `+App` (not inside it):
```text
+DebugService = {
Class = DebugService
ControlPort = 8080
UdpPort = 8081
LogPort = 8082
}
+Logger = {
Class = TcpLogger
Port = 8082
}
+App = {
Class = RealTimeApplication
...
}
```
Call `SetFullConfig(cdb)` after `ConfigureApplication()` to enable the `CONFIG` and `INFO`
commands.
---
## TCP Command Interface (port 8080)
Commands are newline-terminated (`\n`) UTF-8 text strings. One client is served at a time.
Rate limit: 100 commands/second. Idle timeout: 30 seconds.
### `DISCOVER`
List all registered signals.
```
Request: DISCOVER\n
Response: {"Signals":[{"id":<n>,"name":"…","alias":"…","type":"…","elements":<n>},...]}
OK DISCOVER
```
### `TREE`
Return the live `ObjectRegistryDatabase` hierarchy as JSON.
```
Request: TREE\n
Response: {"name":"Root","children":[…]}
OK TREE
```
### `INFO <path>`
Return metadata for a specific ORD node, enriched with config fields.
### `LS [path]`
List the immediate children of an ORD node.
### `TRACE <name> <0|1> [decimation]`
Enable or disable high-speed tracing. Optional decimation controls how many RT cycles are
skipped between samples (default 1 = every cycle).
```
Request: TRACE App.Data.DDB.Counter 1\n
Response: OK TRACE <match_count>\n
```
### `FORCE <name> <value>`
Inject a persistent value into a signal's memory location every RT cycle.
```
Request: FORCE App.Data.DDB.Counter 9999\n
Response: OK FORCE <match_count>\n
```
### `UNFORCE <name>`
Remove a forced value; the signal resumes normal data-flow.
### `BREAK <name> <op> <threshold>`
Set a conditional breakpoint. Supported operators: `>`, `<`, `==`, `>=`, `<=`, `!=`, `OFF`.
```
Request: BREAK App.Data.DDB.Counter > 1000\n
Response: OK BREAK <match_count>\n
```
### `PAUSE` / `RESUME`
Halt or release all patched RT threads.
### `STEP <n> [thread]`
Resume for exactly `n` RT output-broker cycles, then pause again.
### `VALUE <name>`
Read the current raw value of a signal. Arrays capped at 256 elements.
### `CONFIG`
Return the full application configuration as JSON (requires `SetFullConfig()`).
### `MSG <object> <function> [key=value …]`
Send a MARTe2 `Message` to any object in the ORD.
### `SERVICE_INFO`
Return metadata about the debug service (ports, transport type).
---
## UDP Telemetry Format (port 8081)
Packets are little-endian and packed.
### `TraceHeader` (20 bytes)
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | uint32 | `magic` | Always `0xDA7A57AD` |
| 4 | uint32 | `seq` | Monotonically incrementing sequence number |
| 8 | uint64 | `timestamp` | High-resolution hardware timestamp |
| 16 | uint32 | `count` | Number of samples in this datagram |
### Sample Entry (per signal)
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | uint32 | `id` | Signal ID from `DISCOVER` |
| 4 | uint64 | `timestamp` | Per-sample RT timestamp |
| 12 | uint32 | `size` | Payload size in bytes |
| 16 | bytes | `data` | Raw signal memory |
Multiple samples are packed into one datagram up to `STREAMER_MTU = 1400` bytes.
---
## Log Forwarding (TcpLogger, port 8082)
`TcpLogger` connects to the MARTe2 global `LoggerI` and forwards every `REPORT_ERROR` call:
```
<level>|<object>|<function>|<message>\n
```
Up to 8 simultaneous log clients supported.
---
## Performance Hardening
| Fix | Change |
|---|---|
| Multi-producer ring | `tracePushMutex` serialises `TraceRingBuffer::Push()` |
| Break evaluation | Break indices copied to stack-local array before evaluating outside the lock |
| Decimation counter | `Atomic::Add` for lock-free per-signal decimation in multi-producer `ProcessSignal` |
| Ring corruption | `Pop()` discards all entries only when `size >= bufferSize` |
| TCP rate limit | Disconnect clients sending > 100 commands/second |
| Idle timeout | Disconnect active TCP client after 30 s of silence |
| TCP framing | `inputBuffer` accumulates partial commands across `Read()` calls; bounded at 8 KiB |
| VALUE output size | Caps output at 256 elements; includes `"Truncated"` flag |
---
## Key Source Files
| File | Purpose |
|---|---|
| `DebugCore.h` | `DebugSignalInfo` struct, `TraceRingBuffer`, `BreakOp` enum |
| `DebugServiceI.h` | Abstract singleton interface, `SignalAlias`, `BrokerInfo` |
| `DebugBrokerWrapper.h` | `DebugBrokerHelper`, wrapper templates, `DebugBrokerBuilder` |
| `DebugService.h/.cpp` | TCP/UDP transport implementation; `Server()` and `Streamer()` threads |
| `DebugServiceBase.h/.cpp` | Shared signal registry and alias matching logic |
| `TcpLogger.h/.cpp` | `LoggerConsumerI` forwarding `REPORT_ERROR` events to TCP clients |
+208
View File
@@ -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")
```
+104
View File
@@ -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.
+493
View File
@@ -0,0 +1,493 @@
# Tutorial: MARTe2 Integrated Components
This guide covers two complementary use-cases:
- **Part A** — Real-time signal streaming with `UDPStreamer` and the web client
- **Part B** — Signal tracing, forcing, and breakpoints with `DebugService`
---
## Part A: Streaming Signals with UDPStreamer
### A.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).
---
---
## Part B: Debugging with DebugService
### B.1 Add DebugService to Your Config
Add it as a **sibling** of the `+App` node (not inside it):
```text
+DebugService = {
Class = DebugService
ControlPort = 8080
UdpPort = 8081
LogPort = 8082
}
+Logger = {
Class = TcpLogger
Port = 8082
}
+App = {
Class = RealTimeApplication
...
}
```
Call `SetFullConfig(cdb)` after `ConfigureApplication()` to enable the `CONFIG` and `INFO` commands.
### B.2 Start the Debug Web Client
```bash
cd Client/debugger
go build ./...
./debugger --listen :9090
```
Open `http://localhost:9090` in any browser.
### B.3 Exploring the Object Tree
The **Application Tree** panel on the left mirrors your live MARTe2 `ObjectRegistryDatabase`.
1. Expand `Root → App → Data` to find data sources.
2. Click **Info** next to any node to see its class, config, and signals.
3. Click **List** to show immediate children.
### B.4 Real-Time Signal Tracing
1. Locate a signal, e.g. `Root.App.Data.Timer.Counter`.
2. Click **Trace**. The signal appears in the **Traced Signals** list with its live last value.
3. Click **Plot** to open it in the real-time graph. Use **Follow** to keep the time axis scrolling.
4. To set decimation (e.g. every 10th sample):
```
TRACE App.Data.Timer.Counter 1 10
```
5. Click **Trace** again (or send `TRACE … 0`) to stop.
### B.5 Signal Forcing
1. Find a signal, e.g. `Root.App.Data.DDB.Counter`.
2. Click **Force**, enter a value (e.g. `9999`), click **Apply**.
3. The signal is locked at that value every RT cycle.
4. Click **Unforce** to release.
### B.6 Conditional Breakpoints
1. Click **Break** next to a signal.
2. Select an operator (`>`, `<`, `==`, `>=`, `<=`, `!=`) and enter a threshold.
3. When the condition fires, the application pauses. The status bar shows **PAUSED**.
4. Use **Step** to advance one cycle at a time, or **Resume** to continue.
5. Click **Break OFF** to clear.
### B.7 Execution Stepping
While paused (after a breakpoint or manual **Pause**):
1. Enter a step count (e.g. `5`) and click **Step**.
2. The RT loop runs exactly 5 output-broker cycles, then pauses again.
3. The status SSE event (`{"type":"status","remaining":…}`) keeps the UI updated.
### B.8 Scripted / Programmatic Access
Both `DebugService` and the web client accept plain-text TCP commands:
```bash
# Direct TCP
echo -e "DISCOVER\nTRACE App.Data.DDB.Counter 1" | nc localhost 8080
# Via web client API
curl -s -X POST http://localhost:9090/api/command \
-H "Content-Type: text/plain" \
-d "DISCOVER"
```
See `Docs/DebugService.md` for the full command reference.
---
## Part C: Using UDPStreamer and DebugService Together
Both can run simultaneously in the same application:
```text
+DebugService = { Class = DebugService; ControlPort = 8080; UdpPort = 8081; LogPort = 8082 }
+Logger = { Class = TcpLogger; Port = 8082 }
+App = {
Class = RealTimeApplication
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
+DDB = { Class = GAMDataSource }
+Streamer = { Class = UDPStreamer; Port = 44500; MaxPayloadSize = 1400; ... }
+Timings = { Class = TimingDataSource }
}
...
}
```
- `UDPStreamer` provides continuous high-speed streaming of selected signals.
- `DebugService` provides on-demand tracing, forcing, and breakpoints for any signal.
- Both use the UDPS binary protocol format (see `Docs/Protocol.md`).
---
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| `Failed dlopen(): UDPStreamer.so` | Library not in `LD_LIBRARY_PATH` | Source `env.sh`; add build dir |
| `Failed dlopen(): SineArrayGAM.so` | Symlink missing | `ln -sf UDPStreamer.so SineArrayGAM.so` in build dir |
| WebUI shows "No data" | UDPStreamer not running / wrong port | Check port numbers; check MARTe2 logs |
| Plots only show ~167 ms of HF data | Browser buffer too small | Reduce decimation or increase `TEMPORAL_CAP` in JS |
| Fragmentation error / missing data | MTU too small | Reduce `MaxPayloadSize` to 1200 or smaller |
| DebugService: DISCOVER returns empty | `PatchRegistry` called too late | Ensure `DebugService` is initialised before `ConfigureApplication()` |
| Integration tests timeout | MARTe2 libs not on `LD_LIBRARY_PATH` | Source `env.sh` before running tests |
+200
View File
@@ -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
```
+300
View File
@@ -0,0 +1,300 @@
# 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 [uPlot](https://github.com/leeoniya/uPlot).
```
MARTe2 RT app
│ UDP (binary protocol)
udpstreamer-webui (Go)
│ WebSocket (binary frames)
Browser (index.html + uPlot)
```
---
## 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
```
┌───────────────────────────────────────────────────────────────────────────────┐
│ ☰ UDP Scope │ ⊞ 1×1 ▾ │ A: — B: — ΔT: — │ Window: [5s▾] Fit ⬇ CSV ⚡ Trigger ⏸ Pause │
├──────────────────┬────────────────────────────────────────────────────────────┤
│ Signals │ [Plot title] ○ ○ ○ ← signal badges │
│──────────────────│────────────────────────────────────────────────────────── │
│ Counter · u32 │ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ Time · f64 │ │ Plot 1 │ │ Plot 2 │ │
│ Sine1 · f32 │ │ (drop signals here) │ │ (drop signals here) │ │
│ Sine2 · f32 │ │ │ │ │ │
│ Ch1 · [1000] f32 │ └─────────────────────┘ └─────────────────────────────┘ │
└──────────────────┴────────────────────────────────────────────────────────────┘
│ ● Streaming [📊 Stats] v1.0.0 │
└───────────────────────────────────────────────────────────────────────────────┘
```
### 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.
Click the sidebar toggle button (☰) to collapse/expand the signal list.
### Adding Plots
1. Select a layout from the layout menu (⊞ button in the top bar).
2. Drag a signal from the sidebar onto a plot panel.
3. Multiple signals can be overlaid on the same plot.
4. Click the **×** inside a signal badge to remove a trace.
### Plot Layout
The plot grid supports multiple layouts selectable from the layout menu (⊞):
| Layout | Description |
|--------|-------------|
| 1×1 | Single plot |
| 2×1 | Two columns |
| 1×2 | Two rows |
| 2×2 | Four plots |
| 3×1 | Three columns |
| … | More layouts available |
**Resizing plots**: When multiple plots are shown, drag the dividers between them
to resize. Vertical dividers resize column widths; horizontal dividers resize row
heights. Sizes are stored as fractional grid units (fr).
### Plot Configuration
Click the **plot title** to open the configuration toolbar:
- **Title** — edit the plot's display name.
- **Mode** — select the plot display mode:
- **Normal** — standard time-series oscilloscope view (default).
- **Mixed** — signals are arranged in horizontal bands (like digital mode), but
each signal can independently be displayed as analog (auto-scaled within its band)
or digital (quantized to high/low within its band).
- **Digital** — logic-analyzer style; each signal occupies a fixed horizontal band
and is quantized to high/low based on its data range midpoint as threshold.
### Signal Badges and Selection
Each signal assigned to a plot appears as a colored badge in the plot header.
- **Click a badge** — selects the signal and opens the V-Scale toolbar for that
signal. The selected signal is drawn on top with increased line width.
- **Click again** — deselects the signal and closes the V-Scale toolbar.
- **Right-click a badge** — opens the style context menu (color, line width, dash
style, markers).
- **Click ×** on a badge — removes the signal from the plot.
### V-Scale Toolbar
When a signal is selected (via badge click), an inline toolbar appears below the
plot header showing per-signal vertical scale controls:
| Control | Description |
|---------|-------------|
| **Auto** | Automatically fits the signal vertically |
| **Range** | Shows V/div and Pos controls for manual positioning |
| **Manual** | Fixed V/div with free positioning |
| **V/div** | Volts (or units) per division |
| **Pos (div)** | Screen position in divisions (draggable offset marker on Y axis) |
| **Type** (Mixed mode only) | Toggle between **Analog** and **Digital** for this signal |
| **✕** | Close the toolbar and deselect the signal |
Offset markers (small triangles on the Y axis) show each signal's position and can
be dragged to reposition signals without opening the toolbar.
### 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** (top bar) | Pause all plots simultaneously |
| **↺ Auto** (top bar) | Resume all paused plots and snap back to live view |
| **Window** (top bar) | Adjust the rolling time window (1 s 60 s) |
| **Fit** (top bar) | Fit all plots to their current data range |
| **⬇ CSV** (top bar) | Export all signals from all plots to CSV |
| Layout button | Switch between grid layouts |
| Sidebar **☰** | Toggle the signal list panel |
### Cursor System
Two vertical cursors (A and B) can be placed on plots:
- Enable with the **Cursor** button (shown when a plot is paused/zoomed).
- The top bar readout shows **A**, **B**, and **ΔT** (time between cursors).
- Cursors follow the mouse within the plot area.
### Zoom
- **Drag** left or right on a paused plot to zoom into a time range.
- **← Back** button steps back through zoom history.
- **Fit** returns to the full data view.
- When zooming into a region with few or no data points, the nearest data points
outside the zoom window are included so that connecting lines are drawn across
the view rather than showing blank space.
### Trigger System
Click **⚡ Trigger** in the top bar to open the trigger bar:
| Control | Description |
|---------|-------------|
| **Signal** | Select the trigger source signal |
| **Edge** | Rising ↑, Falling ↓, or Both ↕ |
| **Threshold** | Trigger level |
| **Window** | Capture duration after trigger (100 µs 10 s) |
| **Pre** | Pre-trigger buffer percentage (0100%) |
| **Mode** | **Normal** (re-arms automatically) or **Single** (fires once) |
| **Rearm** | Manually re-arm the trigger |
| **Stop** | Cancel waiting trigger |
For array signals, clicking the trigger signal selector prompts for the element
index to use as the trigger source.
### Status Bar
The status bar at the bottom shows:
- **LED indicator**: red (disconnected), orange pulsing (connected, no data), green pulsing (streaming).
- **Status text**: connection state and data age.
- **📊 Stats** button: opens the source statistics panel (packet counts, rates, errors).
- **Build version**: server build tag.
---
## Data Buffering
Signal buffers are sized based on the signal's configured sampling rate from the
CONFIG packet:
```
capacity = min(600 000, ceil(samplingRate × 60 s × 1.5))
```
This ensures up to 60 seconds of history is retained for any signal, regardless of
sample rate. If a signal's sampling rate is not configured, a default capacity of
100 000 samples is used. Temporal arrays (burst signals) use a fixed capacity of
500 000 samples.
Buffers grow automatically during streaming if a higher effective sample rate is
detected. Existing data is preserved during growth.
---
## 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 static files
└── GET /ws ← upgrades to WebSocket; launches per-client read/write pumps
```
### WebSocket Message Format
Two message types are sent from server to browser.
#### `config` message (JSON)
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": 1000.0,
"timeSignalIdx": 4294967295,
"unit": "V"
}
]
}
```
#### `data` message (binary)
Sent at ≤ 30 Hz, batching all UDP packets received since the last tick.
Uses a compact binary format: a fixed header followed by per-signal blocks.
Each signal block contains:
- Signal name (length-prefixed)
- Number of samples
- Timestamp array (float64 LE, Unix seconds)
- Value array (float64 LE)
For **temporal arrays**, each packet contributes `N` samples (one per array element).
For **spatial arrays**, 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.
+32
View File
@@ -0,0 +1,32 @@
#############################################################
# MARTe2 Integrated Components
#############################################################
TARGET?=x86-linux
MAKEDEFAULTDIR?=$(MARTe2_DIR)/MakeDefaults
export TARGET
export MAKEDEFAULTDIR
all: core test
echo done all
core:
$(MAKE) -C Source/Components/DataSources/UDPStreamer -f Makefile.gcc
$(MAKE) -C Source/Components/GAMs/SineArrayGAM -f Makefile.gcc
$(MAKE) -C Source/Components/GAMs/TimeArrayGAM -f Makefile.gcc
$(MAKE) -C Source/Components/Interfaces/TCPLogger -f Makefile.gcc
$(MAKE) -C Source/Components/Interfaces/DebugService -f Makefile.gcc
test:
$(MAKE) -C Test/GTest -f Makefile.gcc
$(MAKE) -C Test/Integration -f Makefile.gcc
clean:
$(MAKE) -C Source/Components/DataSources/UDPStreamer -f Makefile.gcc clean
$(MAKE) -C Source/Components/GAMs/SineArrayGAM -f Makefile.gcc clean
$(MAKE) -C Source/Components/GAMs/TimeArrayGAM -f Makefile.gcc clean
$(MAKE) -C Source/Components/Interfaces/TCPLogger -f Makefile.gcc clean
$(MAKE) -C Source/Components/Interfaces/DebugService -f Makefile.gcc clean
$(MAKE) -C Test/GTest -f Makefile.gcc clean
$(MAKE) -C Test/Integration -f Makefile.gcc clean
+7
View File
@@ -0,0 +1,7 @@
#############################################################
# MARTe2 Integrated Components — top-level shared settings
#############################################################
ROOT_DIR ?= .
MAKEDEFAULTDIR ?= $(MARTe2_DIR)/MakeDefaults
TARGET ?= x86-linux
BUILD_DIR ?= $(ROOT_DIR)/Build/$(TARGET)
+228
View File
@@ -0,0 +1,228 @@
# MARTe2 Integrated Components
A unified MARTe2 library providing real-time signal streaming and on-the-fly debugging
for control applications built with [MARTe2](https://vcis.f4e.europa.eu/marte2-docs/).
---
## Overview
This repository integrates two complementary capabilities:
| Capability | Component | Purpose |
|---|---|---|
| **Signal streaming** | `UDPStreamer` DataSource | Continuously stream selected signals to a browser-based oscilloscope over UDP |
| **Signal debugging** | `DebugService` Interface | On-demand signal tracing, value forcing, and conditional breakpoints — zero application code changes required |
| **Sine generation** | `SineArrayGAM` | Generate continuous sine-wave arrays for testing and simulation |
| **Time stamping** | `TimeArrayGAM` | Provide time-reference arrays aligned to an RT cycle |
| **Log forwarding** | `TCPLogger` Interface | Forward `REPORT_ERROR` log events to TCP clients in real time |
| **Integrated client** | `Common/Client/go` | Go packages for UDPS protocol and WebSocket hub |
| **Debug web client** | `Client/debugger` | Browser-based debug UI communicating with `DebugService` |
---
## Repository Structure
```
MARTe_Integrated_components/
├── Common/
│ ├── UDP/ UDPS binary protocol header (shared by all components)
│ └── Client/go/ Go packages: udpsprotocol, wshub
├── Source/Components/
│ ├── DataSources/UDPStreamer/ Real-time UDP signal streaming DataSource
│ ├── GAMs/SineArrayGAM/ Sine-wave array generator GAM
│ ├── GAMs/TimeArrayGAM/ Time-reference array GAM
│ ├── Interfaces/DebugService/ Signal tracing/forcing/breakpoint Interface
│ └── Interfaces/TCPLogger/ TCP log-forwarding LoggerConsumerI
├── Test/
│ ├── GTest/ GTest harness for UDPStreamer unit tests
│ ├── Integration/ Integration tests for DebugService
│ ├── Components/DataSources/UDPStreamer/ UDPStreamer unit tests
│ └── Configurations/ MARTe2 config files for tests and demos
├── Client/debugger/ Go web client for DebugService
└── Docs/ Documentation
```
---
## Components
### UDPStreamer DataSource
Streams MARTe2 signals over UDP using the UDPS binary protocol. Clients register by
sending a `CONNECT` packet; the server then sends `CONFIG` (signal metadata) and continuous
`DATA` packets. Features:
- Optional 16-bit quantization (configurable per signal: `QuantizedType`)
- Packed high-frequency bursts (`NumberOfElements > 1` with `SamplingRate`)
- Automatic packet fragmentation for payloads exceeding `MaxPayloadSize`
See `Docs/UDPStreamer.md` and `Docs/Protocol.md`.
### SineArrayGAM
Generates a continuous float32 sine-wave array every RT cycle. Used as a signal
source for testing and demo applications. Configurable: `Frequency`, `Amplitude`,
`Phase`, `SamplingRate`, `NumberOfElements`.
See `Docs/SineArrayGAM.md`.
### TimeArrayGAM
Generates a time-reference float64 array. Each element holds the timestamp of the
corresponding sample in a packed burst, computed from the RT cycle timestamp and the
configured `SamplingRate`.
### DebugService Interface
Instruments a running MARTe2 application **without modifying its source code**. On
`Initialise()` it patches the `ClassRegistryDatabase` to wrap all standard
`MemoryMap*Broker` types. When `RealTimeApplication::ConfigureApplication()` runs
afterward the application transparently uses the wrapped brokers.
Capabilities accessible over TCP (port 8080 by default):
- `DISCOVER` — enumerate all signals with type and alias metadata
- `TRACE` — enable/disable high-speed UDP telemetry per signal (with decimation)
- `FORCE` / `UNFORCE` — inject persistent values into signals on the RT path
- `BREAK` — set conditional breakpoints (`>`, `<`, `==`, etc.)
- `PAUSE` / `RESUME` / `STEP` — execution stepping
- `TREE` / `INFO` / `LS` — live ORD navigation
- `VALUE` — read current signal value on demand
- `MSG` — send MARTe2 `Message` to any ORD object
High-speed signal telemetry is streamed as UDP binary datagrams (port 8081).
Logs are forwarded via `TcpLogger` (port 8082).
See `Docs/DebugService.md`.
### TCPLogger Interface
A `LoggerConsumerI` that forwards every MARTe2 `REPORT_ERROR` call to up to 8 TCP
clients on a configurable port. Works as a sidecar to `DebugService`.
### UDPS Protocol
The `Common/UDP/UDPSProtocol.h` header defines the shared binary wire format used by
both `UDPStreamer` and `DebugService`. It is intentionally free of MARTe2-specific
dependencies so it can also be used by Go clients (via `Common/Client/go/udpsprotocol`).
See `Docs/Protocol.md`.
---
## Build
### Prerequisites
- MARTe2 built and installed (set `MARTe2_DIR`)
- MARTe2-components built and installed (set `MARTe2_Components_DIR`)
- GCC toolchain, make
- Go 1.21+ (for clients)
### Setup
```bash
# Edit env.sh to set MARTe2_DIR and MARTe2_Components_DIR, then:
source env.sh
```
### Build all C++ components
```bash
make -f Makefile.gcc core
```
### Build and run tests
```bash
make -f Makefile.gcc test
source env.sh
./Build/x86-linux/Test/Integration/IntegrationTests
```
### Build Go clients
```bash
# Integrated UDPS web client
cd Common/Client/go && go build ./...
# Debug web client
cd Client/debugger && go build ./...
```
### Clean
```bash
make -f Makefile.gcc clean
```
---
## Quick Start
### 1. Stream signals with UDPStreamer
Add to your MARTe2 config:
```text
+Streamer = {
Class = UDPStreamer
Port = 44500
MaxPayloadSize = 1400
Signals = {
Voltage = { Type = float32; Unit = "V" }
}
}
```
Launch the web client:
```bash
cd Common/Client/go
./udpstreamer-webui --streamer 127.0.0.1:44500 --listen :8080
```
Open `http://localhost:8080` and drag signals onto plots.
### 2. Debug a running application with DebugService
Add to your MARTe2 config (as a sibling of `+App`):
```text
+DebugService = {
Class = DebugService
ControlPort = 8080
UdpPort = 8081
LogPort = 8082
}
+Logger = { Class = TcpLogger; Port = 8082 }
```
Launch the debug client:
```bash
cd Client/debugger
./debugger --listen :9090
```
Open `http://localhost:9090`, explore the object tree, trace signals, force values.
---
## Documentation
| Document | Contents |
|---|---|
| `Docs/Protocol.md` | UDPS binary wire protocol specification |
| `Docs/UDPStreamer.md` | UDPStreamer DataSource configuration reference |
| `Docs/SineArrayGAM.md` | SineArrayGAM configuration reference |
| `Docs/DebugService.md` | DebugService TCP API and architecture |
| `Docs/Tutorial.md` | Step-by-step tutorial covering both components |
| `Docs/WebUI.md` | Web client user guide |
| `ARCHITECTURE.md` | System architecture overview |
---
## License
Licensed under the EUPL v1.1 (see individual source files for copyright notices).
@@ -0,0 +1,7 @@
all:
$(MAKE) -C UDPStreamer -f Makefile.gcc
clean:
$(MAKE) -C UDPStreamer -f Makefile.gcc clean
.PHONY: all clean
@@ -0,0 +1 @@
# DataSources intermediate Makefile.inc
@@ -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
PACKAGE=Components/DataSources
ROOT_DIR=../../../../
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
INCLUDES += -I.
INCLUDES += -I$(ROOT_DIR)/Common/UDP
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)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,472 @@
/**
* @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 "BasicTCPSocket.h"
#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 Publishing mode for the background sender thread.
*
* - Strict: send one packet every Synchronise() call (default).
* - Accumulate: buffer N successive snapshots and flush either when the next
* snapshot would exceed MaxPayloadSize or when 1/MinRefreshRate
* has elapsed. Scalars expand to arrays; the first scalar with
* Unit="us"/"ns" is auto-promoted as the FullArray time reference.
* - Decimate: send one packet every Ratio Synchronise() calls.
*/
typedef enum {
UDPStreamerPublishStrict = 0u, /**< Send on every RT cycle (default) */
UDPStreamerPublishAccumulate = 1u, /**< Accumulate until size/time limit; then flush */
UDPStreamerPublishDecimate = 2u /**< Send 1 packet every Ratio cycles */
} UDPStreamerPublishMode;
/**
* @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 */
bool accumulated; /**< True when this scalar was expanded to flushCount elements in Auto accumulation mode */
};
/**
* @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 UDP clients.
*
* @details This output DataSource accepts signals from GAMs and forwards them
* asynchronously over the network. A dedicated background thread handles all
* network I/O so that the real-time thread is only blocked by a fast spinlock
* and a memcpy during Synchronise().
*
* Two operating modes are selected by the presence or absence of MulticastGroup:
*
* @par Unicast mode (default — no MulticastGroup)
* The server opens a single UDP socket on Port. The client initiates the session
* by sending a CONNECT packet to that port. The server replies with a CONFIG
* packet on the same socket and subsequently sends DATA packets directly to the
* client's address. Any number of clients may connect sequentially (one at a
* time); a new CONNECT evicts the previous client.
*
* @par Multicast mode (MulticastGroup specified)
* The server opens a TCP listener on Port for control traffic and a UDP socket
* aimed at MulticastGroup:DataPort for data traffic. The client:
* 1. Connects to Port via TCP and sends a CONNECT packet.
* 2. Receives the CONFIG packet over TCP.
* 3. Joins the multicast group (MulticastGroup:DataPort) to receive DATA packets.
* Multiple clients may receive data simultaneously by joining the same group.
* A new TCP CONNECT evicts the previous client from the control channel; the
* multicast data stream continues regardless.
*
* @par Packet protocol (both modes)
* - Client → Server: CONNECT (initiates session), DISCONNECT (terminates session),
* ACK (optional, for packet-loss monitoring).
* - Server → Client: CONFIG (signal metadata), DATA (signal values, possibly
* fragmented into multiple datagrams if payload exceeds MaxPayloadSize).
*
* @par Top-level configuration parameters
* | Parameter | Type | Default | Description |
* |-----------------|---------|---------|-------------|
* | Port | uint16 | 44500 | TCP control port (multicast) or UDP server port (unicast). Values ≤ 1024 produce a warning. |
* | MulticastGroup | string | *(absent)* | **Enables multicast mode.** IPv4 multicast address, e.g. `"239.0.0.1"`. Must be in 224.0.0.0/4. Absent or empty = unicast. |
* | DataPort | uint16 | Port+1 | UDP port for multicast DATA datagrams. Ignored in unicast mode. Must be non-zero and differ from Port. |
* | MaxPayloadSize | uint32 | 1400 | Maximum bytes of signal payload per UDP datagram (excluding the 17-byte header). Larger signals are fragmented. |
* | PublishingMode | string | Strict | `Strict`: send one packet every Synchronise() call. `Auto`: rate-limited; flush only when MinRefreshRate interval has elapsed. |
* | MinRefreshRate | float64 | — | Required when PublishingMode = Auto. Flush frequency in Hz (e.g. 120.0). |
* | MaxBatchSize | uint32 | 1 | Optional when PublishingMode = Auto. Number of RT cycles to accumulate before flushing one packet. Scalar signals are expanded to arrays of MaxBatchSize elements; the first scalar with Unit="us" or "ns" is auto-promoted as the per-sample FullArray timestamp reference for all other scalars. When omitted or 1, the most-recent single value is sent at MinRefreshRate. |
* | CPUMask | uint32 | 0xFFFFFFFF | CPU affinity bitmask for the background thread. |
* | StackSize | uint32 | (MARTe2 default) | Stack size in bytes for the background thread. |
*
* @par Per-signal configuration parameters
* | Parameter | Type | Default | Description |
* |------------------|---------|-------------|-------------|
* | Type | string | — | MARTe2 type name (uint8, int16, float32, float64, …). Mandatory. |
* | NumberOfDimensions | uint8 | 0 | 0 = scalar, 1 = array, 2 = matrix. |
* | NumberOfElements | uint32 | 1 | Total element count (rows × cols for matrices). |
* | Unit | string | "" | Physical unit label, e.g. `"Pa"` or `"m/s"`. Transmitted in CONFIG for display purposes. |
* | RangeMin | float64 | 0.0 | Minimum of the physical range. Required when QuantizedType is not `none`. |
* | RangeMax | float64 | 1.0 | Maximum of the physical range. Required when QuantizedType is not `none`. |
* | QuantizedType | string | none | Wire quantization for float signals: `none` \| `uint8` \| `int8` \| `uint16` \| `int16`. Reduces wire bandwidth at the cost of precision. Only valid for float32/float64 signals. |
* | TimeMode | string | PacketTime | How the signal's time axis is encoded (see below). |
* | TimeSignal | string | — | Name of the signal that carries timestamps. Required when TimeMode ≠ PacketTime. |
* | SamplingRate | float64 | — | Signal sampling rate in Hz. Required when TimeMode = FirstSample or LastSample. |
*
* @par TimeMode values
* | Value | Description |
* |--------------|-------------|
* | PacketTime | Uses the HRT counter captured at Synchronise() time as the single packet timestamp. No dedicated time signal needed. |
* | FullArray | TimeSignal has the same NumberOfElements as this signal; one timestamp per element. |
* | FirstSample | TimeSignal is a scalar = timestamp of the first element; subsequent elements are spaced by 1/SamplingRate. |
* | LastSample | TimeSignal is a scalar = timestamp of the last element; elements are spaced backwards by 1/SamplingRate. |
*
* @par Example — unicast mode
* <pre>
* +Streamer = {
* Class = UDPStreamer
* Port = 44500
* MaxPayloadSize = 1400
* PublishingMode = "Strict"
* Signals = {
* Time = {
* Type = uint64
* }
* Pressure = {
* Type = float32
* NumberOfDimensions = 1
* NumberOfElements = 100
* Unit = "Pa"
* RangeMin = 0.0
* RangeMax = 1000000.0
* QuantizedType = uint16
* TimeMode = LastSample
* TimeSignal = Time
* SamplingRate = 10000.0
* }
* Temperature = {
* Type = float64
* Unit = "degC"
* TimeMode = PacketTime
* }
* }
* }
* </pre>
*
* @par Example — multicast mode
* <pre>
* +Streamer = {
* Class = UDPStreamer
* Port = 44500 // TCP control port
* MulticastGroup = "239.0.0.1" // Enables multicast mode
* DataPort = 44501 // UDP data port (default: Port+1)
* MaxPayloadSize = 1400
* PublishingMode = "Auto"
* MinRefreshRate = 60
* Signals = {
* Time = {
* Type = uint64
* }
* Voltage = {
* Type = float32
* NumberOfDimensions = 1
* NumberOfElements = 1000
* Unit = "V"
* RangeMin = -10.0
* RangeMax = 10.0
* QuantizedType = int16
* TimeMode = FirstSample
* TimeSignal = Time
* SamplingRate = 100000.0
* }
* }
* }
* </pre>
*/
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.
* @details Reads Port, MaxPayloadSize, CPUMask, StackSize, PublishingMode,
* MinRefreshRate, MulticastGroup, and DataPort from the configuration node.
* Sets useMulticast and dataPort when MulticastGroup is present and valid.
* @return true if all mandatory parameters are valid and consistent.
*/
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;
/**
* @brief Returns true when multicast mode is active (MulticastGroup was set in config).
*/
bool IsMulticast() 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 (unicast mode).
*/
void HandleClientCommand(const uint8 *buf, uint32 size);
/**
* @brief Handles a CONNECT received on the TCP control socket (multicast mode).
* @details Validates magic+type, sets clientConnected, builds and sends CONFIG via tcpClient.
*/
void HandleTCPConnect(const uint8 *buf, uint32 size);
/**
* @brief Serializes an accumulated batch into wireBuffer.
* @param src Flat buffer [numSamples × totalSrcBytes], slot 0 = oldest.
* @param timestamps HRT counters per slot; timestamps[0] is the packet timestamp.
* @param numSamples Actual number of filled slots to serialize.
*/
void SerializeAccumulated(const uint8 *src, const uint64 *timestamps, uint32 numSamples);
/**
* @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 */
UDPStreamerPublishMode publishMode; /**< Strict or Auto publishing mode */
float64 minRefreshRate; /**< Minimum flush rate (Hz) for Auto mode */
uint64 flushPeriodTicks; /**< HRT ticks per flush interval (computed from minRefreshRate) */
/* Accumulate mode — dynamic batch parameters */
uint32 maxBatchCount; /**< Max snapshots that fit in MaxPayloadSize (Accumulate) */
uint32 singleCycleWireBytes; /**< Wire bytes for all accumulated signals per snapshot */
uint32 fixedWireBytes; /**< Wire bytes for non-accumulated signals (arrays, once per packet) */
volatile uint64 lastPublishTs; /**< HRT counter of last successful flush (Accumulate mode) */
uint8 *accumBuffer; /**< Heap: [maxBatchCount × totalSrcBytes] linear fill */
uint64 *accumTimestamps; /**< Heap: [maxBatchCount] HRT counter per snapshot */
uint32 accumFill; /**< Slots filled in accumBuffer (0..maxBatchCount) */
uint64 *readyTimestamps; /**< Heap: [maxBatchCount] HRT for completed ready batch */
uint64 *scratchTimestamps; /**< Heap: [maxBatchCount] background-thread local copy */
uint32 readyFill; /**< Snapshot count in the ready batch */
/* Decimate mode */
uint32 decimateRatio; /**< Send 1 packet every decimateRatio Synchronise() calls */
uint32 decimateCounter; /**< Current decimate cycle counter */
/* 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 — unicast mode */
BasicUDPSocket serverSocket; /**< Bound to port; receives client commands (unicast) */
BasicUDPSocket clientSocket; /**< Configured to send to the connected client (unicast) */
volatile bool clientConnected; /**< True when a client has successfully connected */
/* Networking — multicast mode (only used when useMulticast == true) */
StreamString multicastGroup; /**< Multicast group IP, e.g. "239.0.0.1"; empty = unicast */
uint16 dataPort; /**< UDP port for DATA datagrams in multicast mode */
bool useMulticast; /**< Derived from multicastGroup.Size() > 0 in Initialise() */
BasicTCPSocket tcpListener; /**< TCP server socket: accepts control connections */
BasicTCPSocket *tcpClient; /**< Accepted TCP control connection (heap by WaitConnection) */
BasicUDPSocket dataSocket; /**< UDP socket aimed at multicastGroup:dataPort */
/* 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,142 @@
../../../..//Build/x86-linux/Components/DataSources/UDPStreamer/UDPStreamer.o: UDPStreamer.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapSynchronisedOutputBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapOutputBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/BrokerI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/ExecutableI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
UDPStreamer.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicTCPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetHostCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetMulticastCore.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HandleI.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/SocketCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/EventSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryDataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h
@@ -0,0 +1,142 @@
UDPStreamer.o: UDPStreamer.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapSynchronisedOutputBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapOutputBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/BrokerI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/ExecutableI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
UDPStreamer.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicTCPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetHostCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetMulticastCore.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HandleI.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/SocketCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/EventSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryDataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h
+9
View File
@@ -0,0 +1,9 @@
all:
$(MAKE) -C SineArrayGAM -f Makefile.gcc
$(MAKE) -C TimeArrayGAM -f Makefile.gcc
clean:
$(MAKE) -C SineArrayGAM -f Makefile.gcc clean
$(MAKE) -C TimeArrayGAM -f Makefile.gcc clean
.PHONY: all clean
+1
View File
@@ -0,0 +1 @@
# GAMs intermediate Makefile.inc
@@ -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,52 @@
#############################################################
#
# 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 = SineArrayGAM.x
PACKAGE=Components/GAMs
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
all: $(OBJS) \
$(BUILD_DIR)/SineArrayGAM$(LIBEXT) \
$(BUILD_DIR)/SineArrayGAM$(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,113 @@
../../../..//Build/x86-linux/Components/GAMs/SineArrayGAM/SineArrayGAM.o: SineArrayGAM.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
SineArrayGAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/GAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/ExecutableI.h
@@ -0,0 +1,113 @@
SineArrayGAM.o: SineArrayGAM.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
SineArrayGAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/GAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/ExecutableI.h
@@ -0,0 +1 @@
include Makefile.inc
@@ -0,0 +1,28 @@
OBJSX = TimeArrayGAM.x
PACKAGE=Components/GAMs
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
all: $(OBJS) \
$(BUILD_DIR)/TimeArrayGAM$(LIBEXT) \
$(BUILD_DIR)/TimeArrayGAM$(DLLEXT)
echo $(OBJS)
-include depends.$(TARGET)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -0,0 +1,108 @@
/**
* @file TimeArrayGAM.cpp
* @brief Source file for class TimeArrayGAM
* @date 19/05/2026
* @author Martino Ferrari
*/
#define DLL_API
#include "AdvancedErrorManagement.h"
#include "TimeArrayGAM.h"
namespace MARTe {
TimeArrayGAM::TimeArrayGAM() :
GAM(),
samplingRate(1000000.0),
anchorIsFirst(true),
nElements(0u),
inputTime(NULL_PTR(uint32 *)),
outputBuf(NULL_PTR(uint64 *)) {
}
TimeArrayGAM::~TimeArrayGAM() {
}
bool TimeArrayGAM::Initialise(StructuredDataI &data) {
bool ok = GAM::Initialise(data);
if (ok) {
if (!data.Read("SamplingRate", samplingRate) || samplingRate <= 0.0) {
REPORT_ERROR(ErrorManagement::InitialisationError,
"TimeArrayGAM: SamplingRate > 0 is required.");
ok = false;
}
}
if (ok) {
StreamString anchor;
(void) data.Read("Anchor", anchor);
if (anchor.Size() == 0u || anchor == "FirstSample") {
anchorIsFirst = true; /* default */
}
else if (anchor == "LastSample") {
anchorIsFirst = false;
}
else {
REPORT_ERROR(ErrorManagement::InitialisationError,
"TimeArrayGAM: Anchor must be 'FirstSample' or 'LastSample'.");
ok = false;
}
}
return ok;
}
bool TimeArrayGAM::Setup() {
bool ok = (GetNumberOfInputSignals() == 1u) && (GetNumberOfOutputSignals() == 1u);
if (!ok) {
REPORT_ERROR(ErrorManagement::InitialisationError,
"TimeArrayGAM: exactly one input and one output signal are required.");
return false;
}
inputTime = reinterpret_cast<uint32 *>(GetInputSignalMemory(0u));
outputBuf = reinterpret_cast<uint64 *>(GetOutputSignalMemory(0u));
ok = (inputTime != NULL_PTR(uint32 *)) && (outputBuf != NULL_PTR(uint64 *));
if (!ok) {
REPORT_ERROR(ErrorManagement::InitialisationError,
"TimeArrayGAM: failed to resolve signal memory.");
return false;
}
uint32 sz = 0u;
ok = GetSignalByteSize(OutputSignals, 0u, sz);
if (ok) {
nElements = sz / static_cast<uint32>(sizeof(uint32));
ok = (nElements > 0u);
}
if (!ok) {
REPORT_ERROR(ErrorManagement::InitialisationError,
"TimeArrayGAM: output signal must be a non-empty uint32 array.");
}
return ok;
}
bool TimeArrayGAM::Execute() {
/* Period in nanoseconds — uint64 preserves sub-microsecond resolution
* even at sampling rates > 1 MHz where the µs period would be < 1. */
uint64 periodNs = static_cast<uint64>(1000000000.0 / samplingRate + 0.5);
/* Input is uint32 microseconds (LinuxTimer); convert to nanoseconds. */
uint64 anchorNs = static_cast<uint64>(*inputTime) * 1000u;
if (anchorIsFirst) {
/* out[k] = anchorNs + k * periodNs */
for (uint32 k = 0u; k < nElements; k++) {
outputBuf[k] = anchorNs + static_cast<uint64>(k) * periodNs;
}
}
else {
/* out[k] = anchorNs - (N-1-k) * periodNs */
for (uint32 k = 0u; k < nElements; k++) {
outputBuf[k] = anchorNs - static_cast<uint64>(nElements - 1u - k) * periodNs;
}
}
return true;
}
CLASS_REGISTER(TimeArrayGAM, "1.0")
} /* namespace MARTe */
@@ -0,0 +1,64 @@
/**
* @file TimeArrayGAM.h
* @brief GAM that expands a scalar timer value into a per-sample uint32 time array.
* @date 19/05/2026
* @author Martino Ferrari
*
* @details Each Execute() call reads one uint32 scalar input (time in microseconds,
* e.g. from LinuxTimer) and fills a uint32[N] output array where element[k] holds
* the reconstructed timestamp of sample k:
*
* Anchor = FirstSample: out[k] = input + k * period_us
* Anchor = LastSample: out[k] = input - (N-1-k) * period_us
*
* The resulting time array is suitable as the TimeSignal for a UDPStreamer signal
* configured with TimeMode = FullArray, providing exact per-sample timestamps.
*
* Configuration:
* <pre>
* +TimeArrayGAM1 = {
* Class = TimeArrayGAM
* SamplingRate = 1000000.0 // Sample rate in Hz (must match data signal)
* Anchor = FirstSample // FirstSample (default) or LastSample
* InputSignals = {
* Time = { DataSource = DDB; Type = uint32 }
* }
* OutputSignals = {
* TimeArray = { DataSource = DDB; Type = uint32; NumberOfElements = 1000 }
* }
* }
* </pre>
*
* Exactly one uint32 input signal and one uint32 output array are required.
*/
#ifndef TIMEARRAYGAM_H_
#define TIMEARRAYGAM_H_
#include "CompilerTypes.h"
#include "GAM.h"
namespace MARTe {
class TimeArrayGAM : public GAM {
public:
CLASS_REGISTER_DECLARATION()
TimeArrayGAM();
virtual ~TimeArrayGAM();
virtual bool Initialise(StructuredDataI &data);
virtual bool Setup();
virtual bool Execute();
private:
float64 samplingRate; /**< Sample rate [Hz] */
bool anchorIsFirst; /**< true = FirstSample anchor, false = LastSample */
uint32 nElements; /**< Number of output elements */
uint32 *inputTime; /**< Pointer to scalar input (microseconds, uint32 from LinuxTimer) */
uint64 *outputBuf; /**< Pointer to output array (nanoseconds, uint64) */
};
} /* namespace MARTe */
#endif /* TIMEARRAYGAM_H_ */
@@ -0,0 +1,113 @@
../../../..//Build/x86-linux/Components/GAMs/TimeArrayGAM/TimeArrayGAM.o: TimeArrayGAM.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
TimeArrayGAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/GAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/ExecutableI.h
@@ -0,0 +1,113 @@
TimeArrayGAM.o: TimeArrayGAM.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
TimeArrayGAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/GAM.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/ExecutableI.h
@@ -0,0 +1,590 @@
#ifndef DEBUGBROKERWRAPPER_H
#define DEBUGBROKERWRAPPER_H
#include "BrokerI.h"
#include "DataSourceI.h"
#include "DebugServiceI.h"
#include "FastPollingMutexSem.h"
#include "HighResolutionTimer.h"
#include "MemoryMapBroker.h"
#include "ObjectBuilder.h"
#include "ObjectRegistryDatabase.h"
#include "Threads.h"
// Original broker headers
#include "MemoryMapAsyncOutputBroker.h"
#include "MemoryMapAsyncTriggerOutputBroker.h"
#include "MemoryMapInputBroker.h"
#include "MemoryMapInterpolatedInputBroker.h"
#include "MemoryMapMultiBufferInputBroker.h"
#include "MemoryMapMultiBufferOutputBroker.h"
#include "MemoryMapOutputBroker.h"
#include "MemoryMapSynchronisedInputBroker.h"
#include "MemoryMapSynchronisedMultiBufferInputBroker.h"
#include "MemoryMapSynchronisedMultiBufferOutputBroker.h"
#include "MemoryMapSynchronisedOutputBroker.h"
namespace MARTe {
// Recursive search for any object with a given name anywhere in the registry.
// Used to find GAMs regardless of nesting level.
static Reference FindByNameRecursive(ReferenceContainer *container,
const char8 *name) {
if (container == NULL_PTR(ReferenceContainer *))
return Reference();
uint32 n = container->Size();
for (uint32 i = 0; i < n; i++) {
Reference child = container->Get(i);
if (!child.IsValid())
continue;
if (StringHelper::Compare(child->GetName(), name) == 0)
return child;
ReferenceContainer *sub =
dynamic_cast<ReferenceContainer *>(child.operator->());
if (sub != NULL_PTR(ReferenceContainer *)) {
Reference found = FindByNameRecursive(sub, name);
if (found.IsValid())
return found;
}
}
return Reference();
}
/**
* @brief Helper for optimized signal processing within brokers.
*/
class DebugBrokerHelper {
public:
// Evaluate a break condition against the current live value of a signal.
// Returns true if the condition is met and execution should be paused.
// Only called when signal->breakOp != BREAK_OFF; reads memoryAddress directly.
static bool EvaluateBreak(const DebugSignalInfo *s) {
if (s->memoryAddress == NULL_PTR(void *)) return false;
float64 val = 0.0;
const TypeDescriptor &t = s->type;
if (t == Float64Bit) val = *static_cast<const float64 *>(s->memoryAddress);
else if (t == Float32Bit) val = *static_cast<const float32 *>(s->memoryAddress);
else if (t == UnsignedInteger32Bit) val = *static_cast<const uint32 *>(s->memoryAddress);
else if (t == SignedInteger32Bit) val = *static_cast<const int32 *>(s->memoryAddress);
else if (t == UnsignedInteger64Bit) val = static_cast<float64>(*static_cast<const uint64 *>(s->memoryAddress));
else if (t == SignedInteger64Bit) val = static_cast<float64>(*static_cast<const int64 *>(s->memoryAddress));
else if (t == UnsignedInteger16Bit) val = *static_cast<const uint16 *>(s->memoryAddress);
else if (t == SignedInteger16Bit) val = *static_cast<const int16 *>(s->memoryAddress);
else if (t == UnsignedInteger8Bit) val = *static_cast<const uint8 *>(s->memoryAddress);
else if (t == SignedInteger8Bit) val = *static_cast<const int8 *>(s->memoryAddress);
else return false; // unsupported type — skip
const float64 thr = s->breakThreshold;
switch (s->breakOp) {
case BREAK_GT: return val > thr;
case BREAK_LT: return val < thr;
case BREAK_EQ: return val == thr;
case BREAK_GEQ: return val >= thr;
case BREAK_LEQ: return val <= thr;
case BREAK_NEQ: return val != thr;
default: return false;
}
}
// Spin-wait point for output brokers — called from Execute() AFTER Process().
// Spins while paused, then consumes one step counter tick (if stepping).
// Input brokers must NOT call this — they complete normally to avoid blocking
// cross-thread EventSem posts (RealTimeThreadSynchBroker would time out).
static void OutputPauseAndStep(DebugServiceI *service, const char8 *gamName) {
if (service == NULL_PTR(DebugServiceI *)) return;
// Fast path: nothing to do when neither paused nor stepping.
// Avoids Threads::Name() lookup (and its mutex) on every 1000 Hz cycle.
if (!service->IsPaused() && !service->IsStepPending()) return;
// Wait if already paused (manual PAUSE or breakpoint from a previous cycle)
while (service->IsPaused()) Sleep::MSec(10);
// Pass the OS thread name so per-thread step filtering works.
const char8 *tName = Threads::Name(Threads::Id());
service->ConsumeStepIfNeeded(gamName, tName);
while (service->IsPaused()) Sleep::MSec(10);
}
static void Process(DebugServiceI *service,
DebugSignalInfo **signalInfoPointers,
Vector<uint32> &activeIndices, Vector<uint32> &activeSizes,
FastPollingMutexSem &activeMutex,
volatile bool *anyBreakFlag,
Vector<uint32> *breakIndices) {
if (service == NULL_PTR(DebugServiceI *))
return;
// NOTE: No spin here. Spinning for paused state is handled in Execute() of
// OUTPUT brokers only (see OutputPauseAndStep). Input brokers must not block
// because that prevents cross-thread EventSem posts from completing.
activeMutex.FastLock();
uint32 n = activeIndices.GetNumberOfElements();
if (n > 0 && signalInfoPointers != NULL_PTR(DebugSignalInfo **)) {
// Capture timestamp ONCE per broker cycle for lowest impact
uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() *
HighResolutionTimer::Period() * 1.0e9);
for (uint32 i = 0; i < n; i++) {
uint32 idx = activeIndices[i];
uint32 size = activeSizes[i];
DebugSignalInfo *s = signalInfoPointers[idx];
if (s != NULL_PTR(DebugSignalInfo *)) {
service->ProcessSignal(s, size, ts);
}
}
}
// FIX #3: copy break indices under lock into a local stack array, then
// release activeMutex BEFORE evaluating. EvaluateBreak reads signal
// memory which can stall (cache miss); holding activeMutex during that
// stall would block UpdateBrokersBreakStatus() in the Server thread and
// cause unnecessary priority inversion on the RT path.
bool shouldCheckBreak = (*anyBreakFlag && !service->IsPaused() &&
breakIndices != NULL_PTR(Vector<uint32> *) &&
signalInfoPointers != NULL_PTR(DebugSignalInfo **));
static const uint32 MAX_BREAK_INDICES = 64u;
uint32 localBreakIdx[MAX_BREAK_INDICES];
uint32 nb = 0u;
if (shouldCheckBreak) {
nb = breakIndices->GetNumberOfElements();
if (nb > MAX_BREAK_INDICES) nb = MAX_BREAK_INDICES;
for (uint32 i = 0; i < nb; i++) {
localBreakIdx[i] = (*breakIndices)[i];
}
}
activeMutex.FastUnLock();
// Evaluate break conditions outside the lock — safe because
// EvaluateBreak only reads signalInfoPointers[idx]->memoryAddress,
// which is RT data-bus memory and is never freed during the RT cycle.
if (shouldCheckBreak && nb > 0u) {
for (uint32 i = 0; i < nb; i++) {
uint32 idx = localBreakIdx[i];
DebugSignalInfo *s = signalInfoPointers[idx];
if (s != NULL_PTR(DebugSignalInfo *) && s->breakOp != BREAK_OFF &&
EvaluateBreak(s)) {
service->SetPaused(true);
break;
}
}
}
}
// Pass numCopies explicitly so we can mock it
static void
InitSignals(BrokerI *broker, DataSourceI &dataSourceIn,
DebugServiceI *&service, DebugSignalInfo **&signalInfoPointers,
uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable,
const char8 *functionName, SignalDirection direction,
volatile bool *anyActiveFlag, Vector<uint32> *activeIndices,
Vector<uint32> *activeSizes, FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag, Vector<uint32> *breakIndices) {
if (numCopies > 0) {
signalInfoPointers = new DebugSignalInfo *[numCopies];
for (uint32 i = 0; i < numCopies; i++)
signalInfoPointers[i] = NULL_PTR(DebugSignalInfo *);
}
// Use the singleton registered by DebugService::Initialise() — no ORD
// search on the Init path, and no dependency on a concrete type.
service = DebugServiceI::GetInstance();
if (service && (copyTable != NULL_PTR(MemoryMapBrokerCopyTableEntry *))) {
StreamString dsPath;
DebugServiceI::GetFullObjectName(dataSourceIn, dsPath);
fprintf(stderr, ">> %s broker for %s [%d]\n",
direction == InputSignals ? "Input" : "Output", dsPath.Buffer(),
numCopies);
MemoryMapBroker *mmb = dynamic_cast<MemoryMapBroker *>(broker);
if (mmb == NULL_PTR(MemoryMapBroker *)) {
fprintf(stderr, ">> Impossible to get broker pointer!!\n");
}
for (uint32 i = 0; i < numCopies; i++) {
void *addr = copyTable[i].dataSourcePointer;
TypeDescriptor type = copyTable[i].type;
uint32 dsIdx = i;
if (mmb != NULL_PTR(MemoryMapBroker *)) {
dsIdx = mmb->GetDSCopySignalIndex(i);
}
StreamString signalName;
if (!dataSourceIn.GetSignalName(dsIdx, signalName))
signalName = "Unknown";
fprintf(stderr, ">> registering %s.%s [%p]\n", dsPath.Buffer(),
signalName.Buffer(), mmb);
uint8 dims = 0;
uint32 elems = 1;
(void)dataSourceIn.GetSignalNumberOfDimensions(dsIdx, dims);
(void)dataSourceIn.GetSignalNumberOfElements(dsIdx, elems);
// Register canonical name
StreamString dsFullName;
dsFullName.Printf("%s.%s", dsPath.Buffer(), signalName.Buffer());
service->RegisterSignal(addr, type, dsFullName.Buffer(), dims, elems);
// Register alias
if (functionName != NULL_PTR(const char8 *)) {
StreamString gamFullName;
const char8 *dirStr =
(direction == InputSignals) ? "InputSignals" : "OutputSignals";
const char8 *dirStrShort = (direction == InputSignals) ? "In" : "Out";
// Search recursively through the entire registry for the GAM by name.
// Direct Find("GAM1") only checks top-level; the GAM may be nested
// several levels deep inside a RealTimeApplication container.
Reference gamRef =
FindByNameRecursive(ObjectRegistryDatabase::Instance(),
functionName);
fprintf(stderr, ">> GAM lookup '%s': %s\n", functionName,
gamRef.IsValid() ? "FOUND" : "NOT FOUND");
if (gamRef.IsValid()) {
StreamString absGamPath;
DebugServiceI::GetFullObjectName(*(gamRef.operator->()), absGamPath);
// Register short path (In/Out) for GUI compatibility
gamFullName.Printf("%s.%s.%s", absGamPath.Buffer(), dirStrShort,
signalName.Buffer());
signalInfoPointers[i] =
service->RegisterSignal(addr, type, gamFullName.Buffer(), dims, elems);
} else {
// Fallback to short form
gamFullName.Printf("%s.%s.%s", functionName, dirStrShort,
signalName.Buffer());
signalInfoPointers[i] =
service->RegisterSignal(addr, type, gamFullName.Buffer(), dims, elems);
}
} else {
signalInfoPointers[i] =
service->RegisterSignal(addr, type, dsFullName.Buffer(), dims, elems);
}
}
// Register broker in DebugService for optimized control
bool isOutputBroker = (direction == OutputSignals);
service->RegisterBroker(signalInfoPointers, numCopies, mmb, anyActiveFlag,
activeIndices, activeSizes, activeMutex,
anyBreakFlag, breakIndices,
(functionName != NULL_PTR(const char8 *)) ? functionName : dsPath.Buffer(),
isOutputBroker);
}
}
};
/**
* @brief Template class to instrument any MARTe2 Broker.
*/
template <typename BaseClass> class DebugBrokerWrapper : public BaseClass {
public:
DebugBrokerWrapper() : BaseClass() {
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
anyBreakActive = false;
isOutput = false;
gamName[0] = '\0';
}
virtual ~DebugBrokerWrapper() {
if (signalInfoPointers)
delete[] signalInfoPointers;
}
virtual bool Execute() {
bool ret = BaseClass::Execute();
if (ret && (anyActive || anyBreakActive)) {
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
activeSizes, activeMutex,
&anyBreakActive, &breakIndices);
}
// Output brokers are the safe pause point: base Execute has already
// committed data / posted any cross-thread EventSems.
if (ret && isOutput) {
DebugBrokerHelper::OutputPauseAndStep(service, gamName);
}
return ret;
}
virtual bool Init(SignalDirection direction, DataSourceI &ds,
const char8 *const name, void *gamMem) {
bool ret = BaseClass::Init(direction, ds, name, gamMem);
fprintf(stderr, ">> INIT BROKER %s %s\n", name,
direction == InputSignals ? "In" : "Out");
if (ret) {
numSignals = this->GetNumberOfCopies();
isOutput = (direction == OutputSignals);
StringHelper::CopyN(gamName, name, 255u);
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
numSignals, this->copyTable, name,
direction, &anyActive, &activeIndices,
&activeSizes, &activeMutex,
&anyBreakActive, &breakIndices);
}
return ret;
}
virtual bool Init(SignalDirection direction, DataSourceI &ds,
const char8 *const name, void *gamMem, const bool optim) {
bool ret = BaseClass::Init(direction, ds, name, gamMem, false);
fprintf(stderr, ">> INIT optimized BROKER %s %s\n", name,
direction == InputSignals ? "In" : "Out");
if (ret) {
numSignals = this->GetNumberOfCopies();
isOutput = (direction == OutputSignals);
StringHelper::CopyN(gamName, name, 255u);
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
numSignals, this->copyTable, name,
direction, &anyActive, &activeIndices,
&activeSizes, &activeMutex,
&anyBreakActive, &breakIndices);
}
return ret;
}
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
volatile bool anyBreakActive;
bool isOutput;
char8 gamName[256];
Vector<uint32> activeIndices;
Vector<uint32> activeSizes;
Vector<uint32> breakIndices;
FastPollingMutexSem activeMutex;
};
template <typename BaseClass>
class DebugBrokerWrapperNoOptim : public BaseClass {
public:
DebugBrokerWrapperNoOptim() : BaseClass() {
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
anyBreakActive = false;
isOutput = false;
gamName[0] = '\0';
}
virtual ~DebugBrokerWrapperNoOptim() {
if (signalInfoPointers)
delete[] signalInfoPointers;
}
virtual bool Execute() {
bool ret = BaseClass::Execute();
if (ret && (anyActive || anyBreakActive)) {
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
activeSizes, activeMutex,
&anyBreakActive, &breakIndices);
}
if (ret && isOutput) {
DebugBrokerHelper::OutputPauseAndStep(service, gamName);
}
return ret;
}
virtual bool Init(SignalDirection direction, DataSourceI &ds,
const char8 *const name, void *gamMem) {
bool ret = BaseClass::Init(direction, ds, name, gamMem);
if (ret) {
numSignals = this->GetNumberOfCopies();
isOutput = (direction == OutputSignals);
StringHelper::CopyN(gamName, name, 255u);
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
numSignals, this->copyTable, name,
direction, &anyActive, &activeIndices,
&activeSizes, &activeMutex,
&anyBreakActive, &breakIndices);
}
return ret;
}
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
volatile bool anyBreakActive;
bool isOutput;
char8 gamName[256];
Vector<uint32> activeIndices;
Vector<uint32> activeSizes;
Vector<uint32> breakIndices;
FastPollingMutexSem activeMutex;
};
class DebugMemoryMapAsyncOutputBroker : public MemoryMapAsyncOutputBroker {
public:
DebugMemoryMapAsyncOutputBroker() : MemoryMapAsyncOutputBroker() {
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
anyBreakActive = false;
gamName[0] = '\0';
}
virtual ~DebugMemoryMapAsyncOutputBroker() {
if (signalInfoPointers)
delete[] signalInfoPointers;
}
virtual bool Execute() {
bool ret = MemoryMapAsyncOutputBroker::Execute();
if (ret && (anyActive || anyBreakActive)) {
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
activeSizes, activeMutex,
&anyBreakActive, &breakIndices);
}
// Async output brokers are always output direction
if (ret) {
DebugBrokerHelper::OutputPauseAndStep(service, gamName);
}
return ret;
}
virtual bool InitWithBufferParameters(const SignalDirection direction,
DataSourceI &dataSourceIn,
const char8 *const functionName,
void *const gamMemoryAddress,
const uint32 numberOfBuffersIn,
const ProcessorType &cpuMaskIn,
const uint32 stackSizeIn) {
bool ret = MemoryMapAsyncOutputBroker::InitWithBufferParameters(
direction, dataSourceIn, functionName, gamMemoryAddress,
numberOfBuffersIn, cpuMaskIn, stackSizeIn);
if (ret) {
numSignals = this->GetNumberOfCopies();
StringHelper::CopyN(gamName, (functionName != NULL_PTR(const char8 *)) ? functionName : "", 255u);
DebugBrokerHelper::InitSignals(
this, dataSourceIn, service, signalInfoPointers, numSignals,
this->copyTable, functionName, direction, &anyActive, &activeIndices,
&activeSizes, &activeMutex, &anyBreakActive, &breakIndices);
}
return ret;
}
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
volatile bool anyBreakActive;
char8 gamName[256];
Vector<uint32> activeIndices;
Vector<uint32> activeSizes;
Vector<uint32> breakIndices;
FastPollingMutexSem activeMutex;
};
class DebugMemoryMapAsyncTriggerOutputBroker
: public MemoryMapAsyncTriggerOutputBroker {
public:
DebugMemoryMapAsyncTriggerOutputBroker()
: MemoryMapAsyncTriggerOutputBroker() {
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
anyBreakActive = false;
gamName[0] = '\0';
}
virtual ~DebugMemoryMapAsyncTriggerOutputBroker() {
if (signalInfoPointers)
delete[] signalInfoPointers;
}
virtual bool Execute() {
bool ret = MemoryMapAsyncTriggerOutputBroker::Execute();
if (ret && (anyActive || anyBreakActive)) {
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
activeSizes, activeMutex,
&anyBreakActive, &breakIndices);
}
if (ret) {
DebugBrokerHelper::OutputPauseAndStep(service, gamName);
}
return ret;
}
virtual bool InitWithTriggerParameters(
const SignalDirection direction, DataSourceI &dataSourceIn,
const char8 *const functionName, void *const gamMemoryAddress,
const uint32 numberOfBuffersIn, const uint32 preTriggerBuffersIn,
const uint32 postTriggerBuffersIn, const ProcessorType &cpuMaskIn,
const uint32 stackSizeIn) {
bool ret = MemoryMapAsyncTriggerOutputBroker::InitWithTriggerParameters(
direction, dataSourceIn, functionName, gamMemoryAddress,
numberOfBuffersIn, preTriggerBuffersIn, postTriggerBuffersIn, cpuMaskIn,
stackSizeIn);
if (ret) {
numSignals = this->GetNumberOfCopies();
StringHelper::CopyN(gamName, (functionName != NULL_PTR(const char8 *)) ? functionName : "", 255u);
DebugBrokerHelper::InitSignals(
this, dataSourceIn, service, signalInfoPointers, numSignals,
this->copyTable, functionName, direction, &anyActive, &activeIndices,
&activeSizes, &activeMutex, &anyBreakActive, &breakIndices);
}
return ret;
}
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
volatile bool anyBreakActive;
char8 gamName[256];
Vector<uint32> activeIndices;
Vector<uint32> activeSizes;
Vector<uint32> breakIndices;
FastPollingMutexSem activeMutex;
};
template <typename T> class DebugBrokerBuilder : public ObjectBuilder {
public:
virtual Object *Build(HeapI *const heap) const { return new (heap) T(); }
};
typedef DebugBrokerWrapper<MemoryMapInputBroker> DebugMemoryMapInputBroker;
// LCOV_EXCL_START
typedef DebugBrokerWrapper<MemoryMapOutputBroker> DebugMemoryMapOutputBroker;
typedef DebugBrokerWrapper<MemoryMapSynchronisedInputBroker>
DebugMemoryMapSynchronisedInputBroker;
typedef DebugBrokerWrapper<MemoryMapSynchronisedOutputBroker>
DebugMemoryMapSynchronisedOutputBroker;
typedef DebugBrokerWrapperNoOptim<MemoryMapInterpolatedInputBroker>
DebugMemoryMapInterpolatedInputBroker;
typedef DebugBrokerWrapper<MemoryMapMultiBufferInputBroker>
DebugMemoryMapMultiBufferInputBroker;
typedef DebugBrokerWrapper<MemoryMapMultiBufferOutputBroker>
DebugMemoryMapMultiBufferOutputBroker;
typedef DebugBrokerWrapper<MemoryMapSynchronisedMultiBufferInputBroker>
DebugMemoryMapSynchronisedMultiBufferInputBroker;
typedef DebugBrokerWrapper<MemoryMapSynchronisedMultiBufferOutputBroker>
DebugMemoryMapSynchronisedMultiBufferOutputBroker;
// LCOV_EXCL_STOP
typedef DebugBrokerBuilder<DebugMemoryMapInputBroker>
DebugMemoryMapInputBrokerBuilder;
// LCOV_EXCL_START
typedef DebugBrokerBuilder<DebugMemoryMapOutputBroker>
DebugMemoryMapOutputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapSynchronisedInputBroker>
DebugMemoryMapSynchronisedInputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapSynchronisedOutputBroker>
DebugMemoryMapSynchronisedOutputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapInterpolatedInputBroker>
DebugMemoryMapInterpolatedInputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapMultiBufferInputBroker>
DebugMemoryMapMultiBufferInputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapMultiBufferOutputBroker>
DebugMemoryMapMultiBufferOutputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapSynchronisedMultiBufferInputBroker>
DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapSynchronisedMultiBufferOutputBroker>
DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapAsyncOutputBroker>
DebugMemoryMapAsyncOutputBrokerBuilder;
typedef DebugBrokerBuilder<DebugMemoryMapAsyncTriggerOutputBroker>
DebugMemoryMapAsyncTriggerOutputBrokerBuilder;
// LCOV_EXCL_STOP
} // namespace MARTe
#endif
@@ -0,0 +1,188 @@
#ifndef DEBUGCORE_H
#define DEBUGCORE_H
#include "CompilerTypes.h"
#include "TypeDescriptor.h"
#include "StreamString.h"
#include <string.h>
namespace MARTe {
// Break condition operators stored in DebugSignalInfo::breakOp
enum BreakOp {
BREAK_OFF = 0,
BREAK_GT = 1, // >
BREAK_LT = 2, // <
BREAK_EQ = 3, // ==
BREAK_GEQ = 4, // >=
BREAK_LEQ = 5, // <=
BREAK_NEQ = 6 // !=
};
struct DebugSignalInfo {
void* memoryAddress;
TypeDescriptor type;
StreamString name;
uint8 numberOfDimensions;
uint32 numberOfElements;
volatile bool isTracing;
volatile bool isForcing;
uint8 forcedValue[1024];
uint8 forcedMask[32]; // bit e set → element e is forced; supports up to 256 elements
uint32 internalID;
volatile uint32 decimationFactor;
volatile uint32 decimationCounter;
// Conditional break fields (zero-cost when breakOp == BREAK_OFF)
volatile uint8 breakOp; // BreakOp enum value
float64 breakThreshold; // comparison threshold
};
#pragma pack(push, 1)
struct TraceHeader {
uint32 magic; // 0xDA7A57AD
uint32 seq; // Sequence number
uint64 timestamp; // HighRes timestamp
uint32 count; // Number of samples in payload
};
#pragma pack(pop)
/**
* @brief Ring buffer for high-frequency signal tracing.
* @details New format per sample: [ID:4][Timestamp:8][Size:4][Data:N]
*/
class TraceRingBuffer {
public:
TraceRingBuffer() {
bufferSize = 0;
buffer = NULL_PTR(uint8*);
readIndex = 0;
writeIndex = 0;
}
~TraceRingBuffer() {
if (buffer != NULL_PTR(uint8*)) {
delete[] buffer;
}
}
bool Init(uint32 size) {
if (buffer != NULL_PTR(uint8*)) {
delete[] buffer;
}
bufferSize = size;
buffer = new uint8[bufferSize];
readIndex = 0;
writeIndex = 0;
return (buffer != NULL_PTR(uint8*));
}
bool Push(uint32 signalID, uint64 timestamp, void* data, uint32 size) {
uint32 packetSize = 4 + 8 + 4 + size; // ID + TS + Size + Data
uint32 read = readIndex;
uint32 write = writeIndex;
uint32 available = 0;
if (read <= write) {
available = bufferSize - (write - read) - 1;
} else {
available = read - write - 1;
}
if (available < packetSize) return false;
uint32 tempWrite = write;
WriteToBuffer(&tempWrite, &signalID, 4);
WriteToBuffer(&tempWrite, &timestamp, 8);
WriteToBuffer(&tempWrite, &size, 4);
WriteToBuffer(&tempWrite, data, size);
writeIndex = tempWrite;
return true;
}
bool Pop(uint32 &signalID, uint64 &timestamp, void* dataBuffer, uint32 &size, uint32 maxSize) {
uint32 read = readIndex;
uint32 write = writeIndex;
if (read == write) return false;
uint32 tempRead = read;
uint32 tempId = 0;
uint64 tempTs = 0;
uint32 tempSize = 0;
ReadFromBuffer(&tempRead, &tempId, 4);
ReadFromBuffer(&tempRead, &tempTs, 8);
ReadFromBuffer(&tempRead, &tempSize, 4);
if (tempSize > maxSize) {
// FIX #5: Skip only the current entry rather than discarding the
// entire ring buffer. tempRead is already past the 16-byte header;
// advancing by tempSize lands at the start of the next entry.
//
// Safety fallback: if tempSize >= bufferSize the stored size field
// is corrupt (it can never be that large). In that case we cannot
// locate the next entry safely, so fall back to discarding everything
// to avoid reading garbage as sample headers on future Pop() calls.
if (tempSize >= bufferSize) {
readIndex = write; // corrupt ring — discard all
} else {
readIndex = (tempRead + tempSize) % bufferSize;
}
return false;
}
ReadFromBuffer(&tempRead, dataBuffer, tempSize);
signalID = tempId;
timestamp = tempTs;
size = tempSize;
readIndex = tempRead;
return true;
}
uint32 Count() {
uint32 read = readIndex;
uint32 write = writeIndex;
if (write >= read) return write - read;
return bufferSize - (read - write);
}
private:
void WriteToBuffer(uint32 *idx, void* src, uint32 count) {
uint32 current = *idx;
uint32 spaceToEnd = bufferSize - current;
if (count <= spaceToEnd) {
memcpy(&buffer[current], src, count);
*idx = (current + count) % bufferSize;
} else {
memcpy(&buffer[current], src, spaceToEnd);
uint32 remaining = count - spaceToEnd;
memcpy(&buffer[0], (uint8*)src + spaceToEnd, remaining);
*idx = remaining;
}
}
void ReadFromBuffer(uint32 *idx, void* dst, uint32 count) {
uint32 current = *idx;
uint32 spaceToEnd = bufferSize - current;
if (count <= spaceToEnd) {
memcpy(dst, &buffer[current], count);
*idx = (current + count) % bufferSize;
} else {
memcpy(dst, &buffer[current], spaceToEnd);
uint32 remaining = count - spaceToEnd;
memcpy((uint8*)dst + spaceToEnd, &buffer[0], remaining);
*idx = remaining;
}
}
volatile uint32 readIndex;
volatile uint32 writeIndex;
uint32 bufferSize;
uint8 *buffer;
};
}
#endif
@@ -0,0 +1,618 @@
#include "BasicTCPSocket.h"
#include "ConfigurationDatabase.h"
#include "DebugService.h"
#include "GlobalObjectsDatabase.h"
#include "HighResolutionTimer.h"
#include "LoggerService.h"
#include "Message.h"
#include "ObjectRegistryDatabase.h"
#include "ReferenceT.h"
#include "Sleep.h"
#include "StreamString.h"
#include "StringHelper.h"
#include "Threads.h"
#include "TimeoutType.h"
#include "UDPSProtocol.h"
namespace MARTe {
CLASS_REGISTER(DebugService, "1.0")
// C++98 ODR definitions for static constants
const uint32 DebugService::CMD_RATE_LIMIT;
const uint32 DebugService::CLIENT_IDLE_TIMEOUT_MS;
const uint32 DebugService::INPUT_BUFFER_MAX;
// ---------------------------------------------------------------------------
// Constructor / Destructor
// ---------------------------------------------------------------------------
DebugService::DebugService()
: DebugServiceBase(),
EmbeddedServiceMethodBinderI(),
binderServer(this, ServiceBinder::ServerType),
binderStreamer(this, ServiceBinder::StreamerType),
threadService(binderServer),
streamerService(binderStreamer) {
controlPort = 0u;
streamPort = 8081u;
logPort = 8082u;
streamIP = "127.0.0.1";
isServer = false;
suppressTimeoutLogs = true;
activeClient = NULL_PTR(BasicTCPSocket *);
cmdCountInWindow = 0u;
cmdWindowStartMs = 0u;
lastDataTimeMs = 0u;
inputBuffer = "";
// UDPS members
udpsNumSlots = 0u;
udpsDataPayload = NULL_PTR(uint8 *);
udpsDataPayloadSize = 0u;
udpsPacketCounter = 0u;
udpsConfigPending = false;
}
DebugService::~DebugService() {
if (DebugServiceI::GetInstance() == this) {
DebugServiceI::SetInstance(NULL_PTR(DebugServiceI *));
}
threadService.Stop();
streamerService.Stop();
tcpServer.Close();
udpSocket.Close();
if (activeClient != NULL_PTR(BasicTCPSocket *)) {
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
}
if (udpsDataPayload != NULL_PTR(uint8 *)) {
delete[] udpsDataPayload;
udpsDataPayload = NULL_PTR(uint8 *);
}
// signals owned by DebugServiceBase destructor
}
// ---------------------------------------------------------------------------
// Initialise
// ---------------------------------------------------------------------------
bool DebugService::Initialise(StructuredDataI &data) {
if (!ReferenceContainer::Initialise(data)) return false;
uint32 port = 0u;
if (data.Read("ControlPort", port)) {
controlPort = (uint16)port;
} else {
(void)data.Read("TcpPort", port);
controlPort = (uint16)port;
}
if (controlPort > 0u) {
isServer = true;
DebugServiceI::SetInstance(this);
}
port = 8081u;
if (data.Read("StreamPort", port)) {
streamPort = (uint16)port;
} else {
(void)data.Read("UdpPort", port);
streamPort = (uint16)port;
}
port = 8082u;
if (data.Read("LogPort", port)) {
logPort = (uint16)port;
} else {
(void)data.Read("TcpLogPort", port);
logPort = (uint16)port;
}
StreamString tempIP;
if (data.Read("StreamIP", tempIP)) {
streamIP = tempIP;
} else {
streamIP = "127.0.0.1";
}
uint32 suppress = 1u;
if (data.Read("SuppressTimeoutLogs", suppress)) {
suppressTimeoutLogs = (suppress == 1u);
}
// Capture only the local subtree — do NOT call MoveToRoot() on the shared CDB.
(void)data.Copy(fullConfig);
if (isServer) {
if (!traceBuffer.Init(8 * 1024 * 1024)) return false;
PatchRegistry();
ConfigurationDatabase threadData;
threadData.Write("Timeout", (uint32)1000);
threadService.Initialise(threadData);
streamerService.Initialise(threadData);
if (!tcpServer.Open()) return false;
if (!tcpServer.Listen(controlPort)) return false;
if (!udpSocket.Open()) return false;
// Note: do NOT bind udpSocket to streamPort here. The Go client
// must own that port to receive streamed data. The socket sends
// via an ephemeral source port, which is fine for UDP.
if (threadService.Start() != ErrorManagement::NoError) return false;
if (streamerService.Start() != ErrorManagement::NoError) return false;
// Send initial (empty) CONFIG so clients know the schema version
SendUDPSConfig();
}
return true;
}
// ---------------------------------------------------------------------------
// Transport config hook (called by RebuildConfigFromRegistry in base)
// ---------------------------------------------------------------------------
void DebugService::RebuildTransportConfig() {
const char8 *myName = GetName();
if (myName != NULL_PTR(const char8 *)) {
if (fullConfig.MoveRelative(myName)) {
(void)fullConfig.Write("ControlPort", static_cast<uint32>(controlPort));
(void)fullConfig.Write("UdpPort", static_cast<uint32>(streamPort));
(void)fullConfig.Write("LogPort", static_cast<uint32>(logPort));
if (streamIP.Size() > 0u)
(void)fullConfig.Write("StreamIP", streamIP.Buffer());
(void)fullConfig.MoveToAncestor(1u);
}
}
}
// ---------------------------------------------------------------------------
// SERVICE_INFO hook
// ---------------------------------------------------------------------------
void DebugService::GetServiceInfo(StreamString &out) {
out.Printf("OK SERVICE_INFO TCP_CTRL:%u UDP_STREAM:%u TCP_LOG:%u STATE:%s\n",
controlPort, streamPort, logPort,
isPaused ? "PAUSED" : "RUNNING");
}
// ---------------------------------------------------------------------------
// Execute / HandleMessage (framework boilerplate)
// ---------------------------------------------------------------------------
ErrorManagement::ErrorType DebugService::Execute(ExecutionInfo &info) {
(void)info;
return ErrorManagement::FatalError;
}
ErrorManagement::ErrorType DebugService::HandleMessage(ReferenceT<Message> &data) {
(void)data;
return ErrorManagement::NoError;
}
// ---------------------------------------------------------------------------
// TraceSignal override — sets config-pending flag after base handling
// ---------------------------------------------------------------------------
uint32 DebugService::TraceSignal(const char8 *name, bool enable, uint32 decimation) {
uint32 ret = DebugServiceBase::TraceSignal(name, enable, decimation);
udpsConfigPending = true;
return ret;
}
// ---------------------------------------------------------------------------
// InjectTcpLoggerIfNeeded
// ---------------------------------------------------------------------------
void DebugService::InjectTcpLoggerIfNeeded() {
if (logPort == 0u) return;
Reference existing = ObjectRegistryDatabase::Instance()->Find("LoggerService");
if (existing.IsValid()) {
ReferenceContainer *rc =
dynamic_cast<ReferenceContainer *>(existing.operator->());
if (rc != NULL_PTR(ReferenceContainer *)) {
for (uint32 i = 0u; i < rc->Size(); i++) {
Reference child = rc->Get(i);
// Check by class name to avoid a direct symbol dependency on TcpLogger.so
if (child.IsValid()) {
const ClassProperties *cp = child->GetClassProperties();
if (cp != NULL_PTR(const ClassProperties *)) {
if (StringHelper::Compare(cp->GetName(), "TcpLogger") == 0) {
return; // already has a TcpLogger
}
}
}
}
}
}
ConfigurationDatabase lsCdb;
(void)lsCdb.Write("Class", "LoggerService");
uint32 cpus = 1u;
(void)lsCdb.Write("CPUs", cpus);
if (lsCdb.CreateRelative("+DebugConsumer")) {
(void)lsCdb.Write("Class", "TcpLogger");
uint32 p = static_cast<uint32>(logPort);
(void)lsCdb.Write("Port", p);
(void)lsCdb.MoveToAncestor(1u);
}
(void)lsCdb.MoveToRoot();
ReferenceT<LoggerService> ls(
"LoggerService", GlobalObjectsDatabase::Instance()->GetStandardHeap());
if (!ls.IsValid()) return;
ls->SetName("LoggerService");
if (!ls->Initialise(lsCdb)) return;
(void)ObjectRegistryDatabase::Instance()->Insert(ls);
}
// ---------------------------------------------------------------------------
// Server thread
// ---------------------------------------------------------------------------
ErrorManagement::ErrorType DebugService::Server(ExecutionInfo &info) {
if (info.GetStage() == ExecutionInfo::TerminationStage)
return ErrorManagement::NoError;
if (info.GetStage() == ExecutionInfo::StartupStage) {
serverThreadId = Threads::Id();
Sleep::MSec(500u);
InjectTcpLoggerIfNeeded();
return ErrorManagement::NoError;
}
uint64 nowMs = (uint64)((float64)HighResolutionTimer::Counter() *
HighResolutionTimer::Period() * 1000.0);
if (activeClient == NULL_PTR(BasicTCPSocket *)) {
BasicTCPSocket *newClient = tcpServer.WaitConnection(TimeoutType(100));
if (newClient != NULL_PTR(BasicTCPSocket *)) {
clientMutex.FastLock();
activeClient = newClient;
clientMutex.FastUnLock();
cmdCountInWindow = 0u;
cmdWindowStartMs = nowMs;
lastDataTimeMs = nowMs;
}
} else {
if (nowMs - lastDataTimeMs > CLIENT_IDLE_TIMEOUT_MS) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"Server: TCP client idle for >%u ms — closing connection.",
CLIENT_IDLE_TIMEOUT_MS);
inputBuffer = "";
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
cmdCountInWindow = 0u;
} else if (!activeClient->IsConnected()) {
inputBuffer = "";
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
} else {
char buffer[1024];
uint32 size = 1024u;
if (activeClient->Read(buffer, size)) {
if (size > 0u) {
lastDataTimeMs = nowMs;
if (nowMs - cmdWindowStartMs >= 1000u) {
cmdWindowStartMs = nowMs;
cmdCountInWindow = 0u;
}
if (inputBuffer.Size() + (uint32)size > INPUT_BUFFER_MAX) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"Server: input buffer overflow (>%u bytes without newline) "
"— disconnecting.", INPUT_BUFFER_MAX);
inputBuffer = "";
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
cmdCountInWindow = 0u;
} else {
(void)inputBuffer.Seek(inputBuffer.Size());
uint32 writeSize = (uint32)size;
inputBuffer.Write(buffer, writeSize);
const char8 *raw = inputBuffer.Buffer();
uint32 total = (uint32)inputBuffer.Size();
uint32 lineStart = 0u;
bool rateLimitExceeded = false;
for (uint32 pos = 0u; pos < total && !rateLimitExceeded; pos++) {
if (raw[pos] != '\n') continue;
uint32 len = pos - lineStart;
if (len > 0u && raw[lineStart + len - 1u] == '\r') len--;
if (len > 0u) {
cmdCountInWindow++;
if (cmdCountInWindow > CMD_RATE_LIMIT) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"Server: client exceeded rate limit (%u cmd/s) "
"— disconnecting.", CMD_RATE_LIMIT);
rateLimitExceeded = true;
break;
}
StreamString command;
uint32 cmdLen = len;
command.Write(raw + lineStart, cmdLen);
// Dispatch via base HandleCommand, write response to socket.
StreamString out;
HandleCommand(command, out);
if (out.Size() > 0u) {
const char8 *wPtr = out.Buffer();
uint32 remaining = (uint32)out.Size();
lastDataTimeMs = (uint64)((float64)HighResolutionTimer::Counter() *
HighResolutionTimer::Period() * 1000.0);
while (remaining > 0u) {
uint32 wrote = remaining;
if (!activeClient->Write(wPtr, wrote) || wrote == 0u) {
break;
}
wPtr += wrote;
remaining -= wrote;
lastDataTimeMs = (uint64)((float64)HighResolutionTimer::Counter() *
HighResolutionTimer::Period() * 1000.0);
}
}
}
lineStart = pos + 1u;
}
if (rateLimitExceeded) {
inputBuffer = "";
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
cmdCountInWindow = 0u;
} else {
StreamString newInputBuffer;
if (lineStart < total) {
uint32 remLen = total - lineStart;
newInputBuffer.Write(raw + lineStart, remLen);
}
inputBuffer = newInputBuffer;
}
}
}
} else {
inputBuffer = "";
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
}
}
}
return ErrorManagement::NoError;
}
// ---------------------------------------------------------------------------
// Streamer thread — UDPS binary telemetry
// ---------------------------------------------------------------------------
ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) {
if (info.GetStage() == ExecutionInfo::TerminationStage) {
if (udpsDataPayload != NULL_PTR(uint8 *)) {
delete[] udpsDataPayload;
udpsDataPayload = NULL_PTR(uint8 *);
}
return ErrorManagement::NoError;
}
if (info.GetStage() == ExecutionInfo::StartupStage) {
streamerThreadId = Threads::Id();
return ErrorManagement::NoError;
}
// Set UDP destination
InternetHost dest(streamPort, streamIP.Buffer());
(void)udpSocket.SetDestination(dest);
// a) If config has changed, rebuild and send CONFIG packet
if (udpsConfigPending) {
SendUDPSConfig();
udpsConfigPending = false;
}
// b) Drain traceBuffer — pack each sample into udpsDataPayload
bool anyData = false;
uint32 id, size;
uint64 ts;
uint8 udpsSampleBuf[UDPS_MAX_SAMPLE_BYTES];
while (traceBuffer.Pop(id, ts, udpsSampleBuf, size, UDPS_MAX_SAMPLE_BYTES)) {
// Find matching slot by internalID
for (uint32 i = 0u; i < udpsNumSlots; i++) {
if (udpsSlots[i].internalID == id) {
if ((udpsDataPayload != NULL_PTR(uint8 *)) &&
(8u + udpsSlots[i].wireOffset + udpsSlots[i].wireSize <= udpsDataPayloadSize)) {
uint32 copySize = size;
if (copySize > udpsSlots[i].wireSize) copySize = udpsSlots[i].wireSize;
memcpy(udpsDataPayload + 8u + udpsSlots[i].wireOffset, udpsSampleBuf, copySize);
udpsSlots[i].everFilled = true;
}
anyData = true;
break;
}
}
}
// c) If we have data, stamp with HRT and send
if (anyData && udpsNumSlots > 0u && udpsDataPayload != NULL_PTR(uint8 *)) {
uint64 hrt = HighResolutionTimer::Counter();
memcpy(udpsDataPayload, &hrt, 8u);
SendUDPSFragmented(UDPS_TYPE_DATA, udpsDataPayload, udpsDataPayloadSize);
udpsPacketCounter++;
}
if (!anyData) {
Sleep::MSec(1u);
}
return ErrorManagement::NoError;
}
// ---------------------------------------------------------------------------
// SendUDPSConfig — build and send a UDPS CONFIG packet
// ---------------------------------------------------------------------------
bool DebugService::SendUDPSConfig() {
// Snapshot traced signals under mutex
mutex.FastLock();
// Count traced signals
uint32 tracedCount = 0u;
for (uint32 i = 0u; i < signals.GetNumberOfElements(); i++) {
if (signals[i]->isTracing) {
tracedCount++;
}
}
// Build CONFIG payload into udpsTxBuf starting at UDPS_HEADER_SIZE offset
uint8 *payload = udpsTxBuf + UDPS_HEADER_SIZE;
// Write numTraced (uint32 LE)
memcpy(payload, &tracedCount, 4u);
uint32 payloadOffset = 4u;
// Build slot table in parallel
uint32 currentOffset = 0u;
uint32 newNumSlots = 0u;
uint32 totalWireBytes = 0u;
for (uint32 i = 0u; i < signals.GetNumberOfElements(); i++) {
if (!signals[i]->isTracing) continue;
if (newNumSlots >= UDPS_MAX_SLOTS) break;
uint8 typeCode = UDPSTypeDescriptorToCode(signals[i]->type);
if (typeCode == UDPS_TYPECODE_UNKNOWN) {
// Skip unsupported types
continue;
}
uint32 numElements = signals[i]->numberOfElements;
if (numElements == 0u) numElements = 1u;
uint32 numRows, numCols;
if (signals[i]->numberOfDimensions >= 2u) {
// For matrix: approximate square root
numCols = numElements;
numRows = 1u;
} else if (signals[i]->numberOfDimensions == 1u) {
numRows = numElements;
numCols = 1u;
} else {
numRows = 1u;
numCols = 1u;
}
uint32 wireSize = UDPSTypeCodeByteSize(typeCode) * numElements;
// Write signal descriptor (136 bytes) to payload
if (payloadOffset + UDPS_SIGNAL_DESC_SIZE <= sizeof(udpsTxBuf) - UDPS_HEADER_SIZE - 1u) {
UDPSSignalDescriptor *desc = reinterpret_cast<UDPSSignalDescriptor *>(payload + payloadOffset);
memset(desc, 0, UDPS_SIGNAL_DESC_SIZE);
if (signals[i]->name.Size() > 0u) {
strncpy(desc->name, signals[i]->name.Buffer(), UDPS_MAX_SIGNAL_NAME - 1u);
}
desc->typeCode = typeCode;
desc->quantType = UDPS_QUANT_NONE;
desc->numDimensions = signals[i]->numberOfDimensions;
desc->numRows = numRows;
desc->numCols = numCols;
desc->rangeMin = 0.0;
desc->rangeMax = 0.0;
desc->timeMode = UDPS_TIMEMODE_PACKET;
desc->samplingRate = 0.0;
desc->timeSignalIdx = UDPS_NO_TIME_SIGNAL;
// unit stays zero
payloadOffset += UDPS_SIGNAL_DESC_SIZE;
}
// Fill slot table entry
udpsSlots[newNumSlots].internalID = signals[i]->internalID;
udpsSlots[newNumSlots].wireOffset = currentOffset;
udpsSlots[newNumSlots].wireSize = wireSize;
udpsSlots[newNumSlots].everFilled = false;
newNumSlots++;
currentOffset += wireSize;
totalWireBytes += wireSize;
}
// Write publish mode byte
if (payloadOffset < sizeof(udpsTxBuf) - UDPS_HEADER_SIZE) {
payload[payloadOffset] = UDPS_PUBLISH_STRICT;
payloadOffset++;
}
udpsNumSlots = newNumSlots;
mutex.FastUnLock();
// Reallocate data payload buffer
if (udpsDataPayload != NULL_PTR(uint8 *)) {
delete[] udpsDataPayload;
udpsDataPayload = NULL_PTR(uint8 *);
}
udpsDataPayloadSize = 8u + totalWireBytes; // 8 bytes HRT + signal data
if (udpsDataPayloadSize > 0u) {
udpsDataPayload = new uint8[udpsDataPayloadSize];
memset(udpsDataPayload, 0, udpsDataPayloadSize);
}
// Send CONFIG packet
SendUDPSFragmented(UDPS_TYPE_CONFIG, payload, payloadOffset);
return true;
}
// ---------------------------------------------------------------------------
// SendUDPSFragmented — fragment and send a UDPS packet
// ---------------------------------------------------------------------------
bool DebugService::SendUDPSFragmented(uint8 type, const uint8 *payload, uint32 payloadSize) {
if (payloadSize <= UDPS_MAX_PAYLOAD) {
// Single datagram
UDPSBuildHeader(udpsTxBuf, type, udpsPacketCounter, 0u, 1u, payloadSize);
if (payload != (udpsTxBuf + UDPS_HEADER_SIZE)) {
memcpy(udpsTxBuf + UDPS_HEADER_SIZE, payload, payloadSize);
}
uint32 toWrite = UDPS_HEADER_SIZE + payloadSize;
(void)udpSocket.Write(reinterpret_cast<char8 *>(udpsTxBuf), toWrite);
} else {
// Fragmented
uint32 numFrags = (payloadSize + UDPS_MAX_PAYLOAD - 1u) / UDPS_MAX_PAYLOAD;
uint32 offset = 0u;
for (uint32 i = 0u; i < numFrags; i++) {
uint32 chunkSize = UDPS_MAX_PAYLOAD;
if (offset + chunkSize > payloadSize) {
chunkSize = payloadSize - offset;
}
UDPSBuildHeader(udpsTxBuf, type, udpsPacketCounter,
static_cast<uint16>(i),
static_cast<uint16>(numFrags),
chunkSize);
memcpy(udpsTxBuf + UDPS_HEADER_SIZE, payload + offset, chunkSize);
uint32 toWrite = UDPS_HEADER_SIZE + chunkSize;
(void)udpSocket.Write(reinterpret_cast<char8 *>(udpsTxBuf), toWrite);
offset += chunkSize;
}
}
return true;
}
} // namespace MARTe
@@ -0,0 +1,183 @@
#ifndef DEBUGSERVICE_H
#define DEBUGSERVICE_H
#include "BasicTCPSocket.h"
#include "BasicUDPSocket.h"
#include "DebugServiceBase.h"
#include "EmbeddedServiceMethodBinderI.h"
#include "MessageI.h"
#include "ReferenceT.h"
#include "SingleThreadService.h"
#include "UDPSProtocol.h"
namespace MARTe {
/**
* @brief TCP/UDP implementation of DebugServiceI (via DebugServiceBase).
*
* Signal tracing now uses the UDPS binary protocol (same wire format as
* UDPStreamer) so that the same Go web client can consume both sources.
*
* Flow:
* 1. The Go client listens on udpPort.
* 2. DebugService binds the same port on its end (or uses the client's
* configured IP:port as destination for the old push model).
* 3. On startup and on every TRACE change, DebugService sends a CONFIG
* packet listing the currently-traced signals.
* 4. For each RT drain cycle that contains new data, DebugService sends
* one DATA packet (Strict mode) with the most-recent value per signal.
*/
class DebugService : public DebugServiceBase,
public MessageI,
public EmbeddedServiceMethodBinderI {
public:
friend class DebugServiceTest;
CLASS_REGISTER_DECLARATION()
DebugService();
virtual ~DebugService();
virtual bool Initialise(StructuredDataI &data);
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
virtual ErrorManagement::ErrorType HandleMessage(ReferenceT<Message> &data);
/**
* @brief Override TraceSignal to trigger a CONFIG re-send after each change.
*/
virtual uint32 TraceSignal(const char8 *name, bool enable, uint32 decimation = 1u);
protected:
virtual void GetServiceInfo(StreamString &out);
virtual void RebuildTransportConfig();
private:
void InjectTcpLoggerIfNeeded();
ErrorManagement::ErrorType Server (ExecutionInfo &info);
ErrorManagement::ErrorType Streamer(ExecutionInfo &info);
/**
* @brief Build and transmit a CONFIG packet for the current traced-signal set.
* @details Iterates over all registered signals, selects those with
* isTracing == true, builds UDPSSignalDescriptor array, and sends
* a (possibly fragmented) UDPS CONFIG packet. Also rebuilds the
* udpsSlots table and udpsWirePayload buffer.
* @return true on success.
*/
bool SendUDPSConfig();
/**
* @brief Fragment and send a payload as one or more UDPS datagrams.
* @param type Packet type (UDPS_TYPE_CONFIG or UDPS_TYPE_DATA).
* @param payload Pointer to fully-built payload.
* @param payloadSize Byte count of payload.
* @return true if all datagrams were written without error.
*/
bool SendUDPSFragmented(uint8 type, const uint8 *payload, uint32 payloadSize);
// -----------------------------------------------------------------------
// TCP/UDP transport configuration
// -----------------------------------------------------------------------
uint16 controlPort;
uint16 streamPort;
uint16 logPort;
StreamString streamIP;
bool isServer;
bool suppressTimeoutLogs;
BasicTCPSocket tcpServer;
BasicUDPSocket udpSocket;
// -----------------------------------------------------------------------
// Server-thread helper class
// -----------------------------------------------------------------------
class ServiceBinder : public EmbeddedServiceMethodBinderI {
public:
enum ServiceType { ServerType, StreamerType };
ServiceBinder(DebugService *parent, ServiceType type)
: parent(parent), type(type) {}
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info) {
if (type == StreamerType) return parent->Streamer(info);
return parent->Server(info);
}
private:
DebugService *parent;
ServiceType type;
};
ServiceBinder binderServer;
ServiceBinder binderStreamer;
SingleThreadService threadService;
SingleThreadService streamerService;
ThreadIdentifier serverThreadId;
ThreadIdentifier streamerThreadId;
FastPollingMutexSem clientMutex;
BasicTCPSocket *activeClient;
// -----------------------------------------------------------------------
// TCP server rate limiting and idle detection
// -----------------------------------------------------------------------
static const uint32 CMD_RATE_LIMIT = 100u;
static const uint32 CLIENT_IDLE_TIMEOUT_MS = 120000u;
static const uint32 INPUT_BUFFER_MAX = 8192u;
uint32 cmdCountInWindow;
uint64 cmdWindowStartMs;
uint64 lastDataTimeMs;
StreamString inputBuffer;
// -----------------------------------------------------------------------
// UDPS streaming state (replaces the old custom-format streamer buffers)
// -----------------------------------------------------------------------
/**
* @brief Per-slot descriptor for DATA packet assembly.
* One slot per currently-traced signal; index mirrors CONFIG order.
*/
struct UDPSSlot {
uint32 internalID; ///< DebugSignalInfo::internalID
uint32 wireOffset; ///< Byte offset in udpsDataPayload (after 8-byte HRT)
uint32 wireSize; ///< Bytes occupied by this signal in the DATA payload
bool everFilled; ///< True once at least one sample has been placed
};
/** Maximum number of simultaneously traced signals. */
static const uint32 UDPS_MAX_SLOTS = 512u;
/** Maximum payload per UDP datagram (signal data bytes, excluding header). */
static const uint32 UDPS_MAX_PAYLOAD = 1400u;
/** Hard cap on a single signal's data: 65535 - header(17) - HRT(8). */
static const uint32 UDPS_MAX_SAMPLE_BYTES = 65510u;
UDPSSlot udpsSlots[UDPS_MAX_SLOTS]; ///< Slot table (valid for [0, udpsNumSlots))
uint32 udpsNumSlots; ///< Number of active slots
/** Heap-allocated DATA payload buffer: [HRT:8][sig0_data]...[sigN_data].
* Re-allocated by SendUDPSConfig(). NULL when no signals are traced. */
uint8 *udpsDataPayload;
uint32 udpsDataPayloadSize;
/** Staging buffer: a single sample popped from traceBuffer. */
uint8 udpsSampleBuf[65535u];
/** General-purpose TX buffer for CONFIG and DATA packet assembly. */
uint8 udpsTxBuf[65535u];
/** Packet sequence counter (incremented per DATA/CONFIG datagram group). */
uint32 udpsPacketCounter;
/**
* @brief Set to true by TraceSignal() to request a CONFIG re-send.
* Read and cleared by the Streamer thread.
*/
volatile bool udpsConfigPending;
};
} // namespace MARTe
#endif // DEBUGSERVICE_H
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,214 @@
#ifndef DEBUGSERVICEBASE_H
#define DEBUGSERVICEBASE_H
/**
* @file DebugServiceBase.h
* @brief Shared base class for DebugService and WebDebugService.
*
* Extracts all signal-management, command-handling and config logic that is
* common to both the TCP/UDP transport (DebugService) and the HTTP/SSE
* transport (WebDebugService), eliminating the previous code duplication.
*/
#include "ConfigurationDatabase.h"
#include "DataSourceI.h"
#include "DebugServiceI.h"
#include "FastPollingMutexSem.h"
#include "ReferenceContainer.h"
#include "ReferenceT.h"
#include "StreamString.h"
namespace MARTe {
/**
* @brief Intermediate base class that holds all shared debugging logic.
*
* Inherits from ReferenceContainer (to be a MARTe2 Object) and DebugServiceI
* (to expose the RT-path / control-path interface). Transport subclasses
* (DebugService, WebDebugService) inherit from this class and add only their
* transport-specific threading and socket logic.
*/
class DebugServiceBase : public ReferenceContainer, public DebugServiceI {
public:
friend class DebugServiceTest;
DebugServiceBase();
virtual ~DebugServiceBase();
// =========================================================================
// DebugServiceI RT-path overrides (shared implementation)
// =========================================================================
virtual DebugSignalInfo *RegisterSignal(void *memoryAddress,
TypeDescriptor type,
const char8 *name,
uint8 numberOfDimensions = 0,
uint32 numberOfElements = 1);
virtual void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
uint64 timestamp);
virtual void RegisterBroker(DebugSignalInfo **signalPointers,
uint32 numSignals,
MemoryMapBroker *broker,
volatile bool *anyActiveFlag,
Vector<uint32> *activeIndices,
Vector<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag,
Vector<uint32> *breakIndices,
const char8 *gamName = NULL_PTR(const char8 *),
bool isOutput = false);
virtual bool IsPaused() const { return isPaused; }
virtual void SetPaused(bool paused) { isPaused = paused; }
virtual bool IsStepPending() const { return stepRemaining > 0u; }
virtual void ConsumeStepIfNeeded(const char8 *gamName,
const char8 *threadName = NULL_PTR(const char8 *));
// =========================================================================
// DebugServiceI control-path overrides (shared implementation)
// =========================================================================
virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr);
virtual uint32 UnforceSignal(const char8 *name);
virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1);
virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold);
virtual uint32 ClearBreak (const char8 *name);
virtual bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable);
virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs);
virtual uint32 UnmonitorSignal (const char8 *path);
// =========================================================================
// Shared control-path methods (used by transport subclasses)
// =========================================================================
/**
* @brief Parse and dispatch a debug command; write text response to @p out.
*
* Handles: TRACE, FORCE, UNFORCE, BREAK, PAUSE, RESUME, STEP, STEP_STATUS,
* VALUE, DISCOVER, TREE, INFO, LS, CONFIG, MONITOR, UNMONITOR,
* SERVICE_INFO, MSG.
*
* SERVICE_INFO delegates to GetServiceInfo() so each transport can fill in
* its own port/state information.
*/
void HandleCommand(const StreamString &cmdIn, StreamString &out);
void GetStepStatus(StreamString &out);
void GetSignalValue(const char8 *name, StreamString &out);
void Discover (StreamString &out);
void InfoNode (const char8 *path, StreamString &out);
void ListNodes (const char8 *path, StreamString &out);
void ServeConfig (StreamString &out);
void SetFullConfig(ConfigurationDatabase &config);
void RebuildConfigFromRegistry();
// =========================================================================
// Struct shared by both transports
// =========================================================================
struct MonitoredSignal {
ReferenceT<DataSourceI> dataSource;
uint32 signalIdx;
uint32 internalID;
uint32 periodMs;
uint64 lastPollTime;
uint32 size;
StreamString path;
};
// =========================================================================
// Constants
// =========================================================================
static const uint32 GET_VALUE_MAX_ELEMENTS = 256u;
/**
* @brief Pre-build DISCOVER and TREE response caches.
*
* Called automatically by SetFullConfig() once the application is fully
* initialised. After this point, DISCOVER and TREE are served from the
* pre-built string with no mutex contention and no JSON generation cost.
* Both caches are invalidated whenever a new signal is registered (which
* normally only happens during broker init, before SetFullConfig).
*/
void BuildDiscoverCache();
void BuildTreeCache();
// Number of signals per DISCOVER_PART TCP chunk. Responses larger than
// this are split so the Go client can start parsing immediately.
static const uint32 DISCOVER_CHUNK_SIGNALS = 256u;
protected:
// =========================================================================
// Virtual hooks for transport subclasses
// =========================================================================
/**
* @brief Fill the SERVICE_INFO response text (transport-specific).
*
* Called by HandleCommand when the SERVICE_INFO command is received.
* The base implementation writes nothing; subclasses override to append
* e.g. "OK SERVICE_INFO TCP_CTRL:8080 UDP_STREAM:8081 STATE:RUNNING\n".
*/
virtual void GetServiceInfo(StreamString &out) = 0;
/**
* @brief Write back transport-specific config keys after RebuildConfigFromRegistry().
*
* Called at the end of RebuildConfigFromRegistry() so that port numbers
* and other transport parameters that were read in Initialise() are
* reflected back into fullConfig (ExportData on ReferenceContainers
* doesn't re-emit config-file parameters).
*/
virtual void RebuildTransportConfig() {}
// =========================================================================
// Shared protected helpers
// =========================================================================
void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *));
void UpdateBrokersActiveStatus();
void UpdateBrokersBreakStatus();
void PatchRegistry();
uint32 ExportTree(ReferenceContainer *container, StreamString &json,
const char8 *pathPrefix);
void ExportTreeNode(const char8 *path, StreamString &out);
void EnrichWithConfig(const char8 *path, StreamString &json);
static void JsonifyDatabase(ConfigurationDatabase &db, StreamString &json);
// =========================================================================
// Shared data members
// =========================================================================
Vector<DebugSignalInfo *> signals;
Vector<SignalAlias> aliases;
Vector<BrokerInfo> brokers;
Vector<MonitoredSignal> monitoredSignals;
FastPollingMutexSem mutex;
FastPollingMutexSem tracePushMutex;
TraceRingBuffer traceBuffer;
volatile bool isPaused;
volatile uint32 stepRemaining;
StreamString pausedAtGam;
StreamString stepThreadFilter;
ConfigurationDatabase fullConfig;
bool manualConfigSet;
// Pre-built response caches. Guarded by mutex (brief lock for swap,
// none needed for reads once cacheValid is true and construction is done).
StreamString discoverCache; // full chunked DISCOVER_PART+DISCOVER payload
StreamString treeCache; // full TREE payload including sentinel
volatile bool discoverCacheValid;
volatile bool treeCacheValid;
};
} // namespace MARTe
#endif // DEBUGSERVICEBASE_H
@@ -0,0 +1,188 @@
#ifndef DEBUGSERVICEI_H
#define DEBUGSERVICEI_H
/**
* @file DebugServiceI.h
* @brief Abstract interface for the MARTe2 debug/instrumentation service.
*
* All broker wrappers (DebugBrokerWrapper) depend solely on this interface,
* decoupling them from the concrete TCP/UDP implementation. Alternative
* transports TTY, WebSocket, shared-memory ring, etc. can be plugged in
* by providing a new concrete implementation without touching any broker or
* application code.
*
* The interface is split into two logical groups:
*
* RT-path API
* Called from broker threads on every RT cycle. Implementations must
* keep these methods as cheap as possible; the only permissible
* synchronisation primitive is a fast spinlock (FastPollingMutexSem).
*
* Control-path API
* Called from server / command-handler threads (TCP, TTY, Web ).
* Latency here is acceptable; correctness and thread-safety matter.
*
* Concrete implementations register themselves during Initialise() via
* SetInstance(). Broker wrappers retrieve the active instance via
* GetInstance(); there is no ORD search on the hot path.
*/
#include "DebugCore.h"
#include "FastPollingMutexSem.h"
#include "Object.h"
#include "StreamString.h"
#include "TypeDescriptor.h"
namespace MARTe {
// Forward declarations — concrete types are only needed in the implementation.
class MemoryMapBroker;
struct SignalAlias {
StreamString name;
uint32 signalIndex;
};
struct BrokerInfo {
DebugSignalInfo **signalPointers;
uint32 numSignals;
MemoryMapBroker *broker;
volatile bool *anyActiveFlag;
Vector<uint32> *activeIndices;
Vector<uint32> *activeSizes;
FastPollingMutexSem *activeMutex;
volatile bool *anyBreakFlag;
Vector<uint32> *breakIndices;
StreamString gamName;
bool isOutput;
};
/**
* @brief Abstract debug-service interface.
*/
class DebugServiceI {
public:
// -------------------------------------------------------------------------
// Static instance registry
// -------------------------------------------------------------------------
/**
* @brief Return the currently registered debug-service instance, or NULL.
*
* Called on every broker Init() path and from OutpautPauseAndStep().
* Returns NULL when no debug service has been initialised, in which case
* all instrumentation is a no-op.
*/
static DebugServiceI *GetInstance() { return instance; }
/**
* @brief Register @p inst as the global debug-service.
*
* Concrete implementations call this from their Initialise() method.
* Passing NULL deregisters the current instance (called from the
* destructor so dangling pointers are never visible to broker threads).
*/
static void SetInstance(DebugServiceI *inst) { instance = inst; }
virtual ~DebugServiceI() {}
// =========================================================================
// RT-path API (called from broker execute threads every cycle)
// =========================================================================
/**
* @brief Register a signal memory region with the debug service.
*
* Called once per signal during broker Init(). Returns a pointer to the
* internal DebugSignalInfo that the broker caches for use on the hot path.
* Thread-safe; must not be called after the RT loop has started.
*/
virtual DebugSignalInfo *RegisterSignal(void *memoryAddress,
TypeDescriptor type,
const char8 *name,
uint8 numberOfDimensions = 0,
uint32 numberOfElements = 1) = 0;
/**
* @brief Process one signal on the RT path.
*
* Applies forced values (memcpy into signal memory) and, when tracing is
* enabled and the decimation counter fires, pushes a sample to the trace
* ring buffer. Called under the broker's activeMutex; implementations
* must not acquire any lock that is also held by the Server thread.
*/
virtual void ProcessSignal(DebugSignalInfo *signalInfo,
uint32 size,
uint64 timestamp) = 0;
/**
* @brief Register a broker so the service can push active/break index
* updates to it without iterating every signal.
*/
virtual void RegisterBroker(DebugSignalInfo **signalPointers,
uint32 numSignals,
MemoryMapBroker *broker,
volatile bool *anyActiveFlag,
Vector<uint32> *activeIndices,
Vector<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag,
Vector<uint32> *breakIndices,
const char8 *gamName = NULL_PTR(const char8 *),
bool isOutput = false) = 0;
/** @brief Return true if the RT loop is currently held at a pause/breakpoint. */
virtual bool IsPaused() const = 0;
/** @brief Set or clear the paused state (called by break-condition logic). */
virtual void SetPaused(bool paused) = 0;
/** @brief Return true if a step count is pending (stepRemaining > 0). */
virtual bool IsStepPending() const = 0;
/**
* @brief Consume one step credit for the current output-broker cycle.
*
* Called by every output broker after Execute(). No-op when stepRemaining
* is zero (the common case); only acquires a mutex when stepping is active.
*/
virtual void ConsumeStepIfNeeded(
const char8 *gamName,
const char8 *threadName = NULL_PTR(const char8 *)) = 0;
// =========================================================================
// Control-path API (called from server / command-handler threads)
// =========================================================================
virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr) = 0;
virtual uint32 UnforceSignal(const char8 *name) = 0;
virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1) = 0;
virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold) = 0;
virtual uint32 ClearBreak (const char8 *name) = 0;
virtual bool IsInstrumented(const char8 *fullPath,
bool &traceable, bool &forcable) = 0;
virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs) = 0;
virtual uint32 UnmonitorSignal (const char8 *path) = 0;
// =========================================================================
// Utility (implementation-agnostic, defined in DebugService.cpp)
// =========================================================================
/**
* @brief Resolve the fully-qualified ORD path of @p obj into @p fullPath.
*
* Static so it can be called without a service instance. All concrete
* implementations (and InitSignals in DebugBrokerWrapper.h) use this
* to build canonical signal names. The definition lives in
* DebugService.cpp alongside FindPathInContainer().
*/
static bool GetFullObjectName(const Object &obj, StreamString &fullPath);
protected:
/** Pointer to the single active debug-service instance. */
static DebugServiceI *instance;
};
} // namespace MARTe
#endif // DEBUGSERVICEI_H
@@ -0,0 +1 @@
include Makefile.inc
@@ -0,0 +1,36 @@
#############################################################
# MARTe2 Integrated Components — DebugService
#############################################################
OBJSX=DebugService.x DebugServiceBase.x
PACKAGE=Components/Interfaces
ROOT_DIR=../../../../
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
# Shared UDPS protocol header (from Common/)
INCLUDES += -I$(ROOT_DIR)/Common/UDP
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger
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/L4Logger
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/L4LoggerService
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) $(SUBPROJ) \
$(BUILD_DIR)/DebugService$(LIBEXT) \
$(BUILD_DIR)/DebugService$(DLLEXT)
echo $(OBJS)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -0,0 +1,9 @@
all:
$(MAKE) -C TCPLogger -f Makefile.gcc
$(MAKE) -C DebugService -f Makefile.gcc
clean:
$(MAKE) -C TCPLogger -f Makefile.gcc clean
$(MAKE) -C DebugService -f Makefile.gcc clean
.PHONY: all clean
@@ -0,0 +1 @@
# Interfaces intermediate Makefile.inc
@@ -0,0 +1 @@
include Makefile.inc
@@ -0,0 +1,31 @@
OBJSX=TcpLogger.x
PACKAGE=Components/Interfaces
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/L4Logger
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/L4LoggerService
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) $(SUBPROJ) \
$(BUILD_DIR)/TcpLogger$(LIBEXT) \
$(BUILD_DIR)/TcpLogger$(DLLEXT)
echo $(OBJS)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -0,0 +1,163 @@
#include "TcpLogger.h"
#include "Threads.h"
#include "StringHelper.h"
#include "ConfigurationDatabase.h"
namespace MARTe {
CLASS_REGISTER(TcpLogger, "1.0")
TcpLogger::TcpLogger() :
ReferenceContainer(), LoggerConsumerI(), EmbeddedServiceMethodBinderI(),
service(*this)
{
port = 8082;
readIdx = 0;
writeIdx = 0;
workerThreadId = InvalidThreadIdentifier;
for (uint32 i=0; i<MAX_CLIENTS; i++) {
activeClients[i] = NULL_PTR(BasicTCPSocket*);
}
}
TcpLogger::~TcpLogger() {
service.Stop();
server.Close();
clientsMutex.FastLock();
for (uint32 i=0; i<MAX_CLIENTS; i++) {
if (activeClients[i] != NULL_PTR(BasicTCPSocket*)) {
activeClients[i]->Close();
delete activeClients[i];
activeClients[i] = NULL_PTR(BasicTCPSocket*);
}
}
clientsMutex.FastUnLock();
}
bool TcpLogger::ExportData(StructuredDataI & data) {
bool ok = data.Write("Port", static_cast<uint32>(port));
return ok;
}
bool TcpLogger::Initialise(StructuredDataI & data) {
if (!ReferenceContainer::Initialise(data)) return false;
if (!data.Read("Port", port)) {
(void)data.Read("TcpPort", port);
}
(void)eventSem.Create();
ConfigurationDatabase threadData;
threadData.Write("Timeout", (uint32)1000);
if (!service.Initialise(threadData)) return false;
if (!server.Open()) return false;
if (!server.Listen(port)) {
return false;
}
printf("[TcpLogger] Listening on port %u\n", port);
if (service.Start() != ErrorManagement::NoError) {
return false;
}
return true;
}
void TcpLogger::ConsumeLogMessage(LoggerPage *logPage) {
if (logPage == NULL_PTR(LoggerPage*)) return;
// 1. Mirror to stdout
StreamString levelStr;
ErrorManagement::ErrorCodeToStream(logPage->errorInfo.header.errorType, levelStr);
printf("[%s] %s\n", levelStr.Buffer(), logPage->errorStrBuffer);
fflush(stdout);
// 2. Queue for TCP
InsertLogIntoQueue(logPage->errorInfo, logPage->errorStrBuffer);
}
void TcpLogger::InsertLogIntoQueue(const ErrorManagement::ErrorInformation &info, const char8 * const description) {
uint32 next = (writeIdx + 1) % QUEUE_SIZE;
if (next != readIdx) {
TcpLogEntry &entry = queue[writeIdx % QUEUE_SIZE];
entry.info = info;
StringHelper::Copy(entry.description, description);
writeIdx = next;
(void)eventSem.Post();
}
}
ErrorManagement::ErrorType TcpLogger::Execute(ExecutionInfo & info) {
if (info.GetStage() == ExecutionInfo::TerminationStage) return ErrorManagement::NoError;
if (info.GetStage() == ExecutionInfo::StartupStage) {
workerThreadId = Threads::Id();
return ErrorManagement::NoError;
}
// Each Execute() call does one cycle. The MARTe2 framework loops Execute()
// so we must NOT spin in an infinite internal loop here — doing so prevents
// the framework from ever delivering the TerminationStage and causes
// Stop() to time out, leaving threads running after the destructor.
// 1. Check for new connections (1 ms timeout → returns promptly)
BasicTCPSocket *newClient = server.WaitConnection(1);
if (newClient != NULL_PTR(BasicTCPSocket *)) {
clientsMutex.FastLock();
bool added = false;
for (uint32 i=0; i<MAX_CLIENTS; i++) {
if (activeClients[i] == NULL_PTR(BasicTCPSocket*)) {
activeClients[i] = newClient;
added = true;
break;
}
}
clientsMutex.FastUnLock();
if (!added) {
newClient->Close();
delete newClient;
} else {
(void)newClient->SetBlocking(false);
}
}
// 2. Stream queued entries to clients
bool hadData = false;
while (readIdx != writeIdx) {
hadData = true;
uint32 idx = readIdx % QUEUE_SIZE;
TcpLogEntry &entry = queue[idx];
StreamString level;
ErrorManagement::ErrorCodeToStream(entry.info.header.errorType, level);
StreamString packet;
packet.Printf("LOG %s %s\n", level.Buffer(), entry.description);
uint32 size = packet.Size();
clientsMutex.FastLock();
for (uint32 j=0; j<MAX_CLIENTS; j++) {
if (activeClients[j] != NULL_PTR(BasicTCPSocket*)) {
uint32 s = size;
if (!activeClients[j]->Write(packet.Buffer(), s)) {
activeClients[j]->Close();
delete activeClients[j];
activeClients[j] = NULL_PTR(BasicTCPSocket*);
}
}
}
clientsMutex.FastUnLock();
readIdx = (readIdx + 1) % QUEUE_SIZE;
}
if (!hadData) {
// Brief wait so we don't busy-spin; return so Stop() can take effect
(void)eventSem.Wait(TimeoutType(10));
eventSem.Reset();
}
return ErrorManagement::NoError;
}
}
@@ -0,0 +1,67 @@
#ifndef TCPLOGGER_H
#define TCPLOGGER_H
#include "LoggerConsumerI.h"
#include "ReferenceContainer.h"
#include "EmbeddedServiceMethodBinderI.h"
#include "BasicTCPSocket.h"
#include "SingleThreadService.h"
#include "FastPollingMutexSem.h"
#include "EventSem.h"
namespace MARTe {
struct TcpLogEntry {
ErrorManagement::ErrorInformation info;
char8 description[MAX_ERROR_MESSAGE_SIZE];
};
/**
* @brief Logger consumer that publishes framework logs to TCP and stdout.
* @details Implements LoggerConsumerI to be used inside a LoggerService.
*/
class TcpLogger : public ReferenceContainer, public LoggerConsumerI, public EmbeddedServiceMethodBinderI {
public:
CLASS_REGISTER_DECLARATION()
TcpLogger();
virtual ~TcpLogger();
virtual bool Initialise(StructuredDataI & data);
virtual bool ExportData(StructuredDataI & data);
/**
* @brief Implementation of LoggerConsumerI.
* Called by LoggerService.
*/
virtual void ConsumeLogMessage(LoggerPage *logPage);
/**
* @brief Worker thread method for TCP streaming.
*/
virtual ErrorManagement::ErrorType Execute(ExecutionInfo & info);
private:
void InsertLogIntoQueue(const ErrorManagement::ErrorInformation &info, const char8 * const description);
uint16 port;
BasicTCPSocket server;
static const uint32 MAX_CLIENTS = 8;
BasicTCPSocket* activeClients[MAX_CLIENTS];
FastPollingMutexSem clientsMutex;
SingleThreadService service;
ThreadIdentifier workerThreadId;
static const uint32 QUEUE_SIZE = 1024;
TcpLogEntry queue[QUEUE_SIZE];
volatile uint32 readIdx;
volatile uint32 writeIdx;
EventSem eventSem;
};
}
#endif
+11
View File
@@ -0,0 +1,11 @@
all:
$(MAKE) -C DataSources -f Makefile.gcc
$(MAKE) -C GAMs -f Makefile.gcc
$(MAKE) -C Interfaces -f Makefile.gcc
clean:
$(MAKE) -C DataSources -f Makefile.gcc clean
$(MAKE) -C GAMs -f Makefile.gcc clean
$(MAKE) -C Interfaces -f Makefile.gcc clean
.PHONY: all clean
+1
View File
@@ -0,0 +1 @@
# Components intermediate Makefile.inc
@@ -0,0 +1 @@
include Makefile.inc
@@ -0,0 +1,58 @@
#############################################################
#
# 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$(ROOT_DIR)/Common/UDP
INCLUDES += -I$(ROOT_DIR)/Source/Components/DataSources/UDPStreamer
all: $(OBJS) \
$(BUILD_DIR)/UDPStreamerTest$(LIBEXT)
echo $(OBJS)
include depends.$(TARGET)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -0,0 +1,227 @@
/**
* @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, TestInitialise_AutoMode_Valid) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestInitialise_AutoMode_Valid());
}
TEST(UDPStreamerGTest, TestInitialise_AutoMode_MissingRefreshRate) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestInitialise_AutoMode_MissingRefreshRate());
}
TEST(UDPStreamerGTest, TestInitialise_UnknownPublishingMode) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestInitialise_UnknownPublishingMode());
}
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());
}
TEST(UDPStreamerGTest, TestInitialise_MulticastMode_Valid) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestInitialise_MulticastMode_Valid());
}
TEST(UDPStreamerGTest, TestInitialise_MulticastMode_DefaultDataPort) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestInitialise_MulticastMode_DefaultDataPort());
}
TEST(UDPStreamerGTest, TestInitialise_MulticastMode_InvalidDataPort) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestInitialise_MulticastMode_InvalidDataPort());
}
TEST(UDPStreamerGTest, TestPrepareNextState_Multicast) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestPrepareNextState_Multicast());
}
TEST(UDPStreamerGTest, TestExecute_MulticastConnectDataDisconnect) {
UDPStreamerTest test;
ASSERT_TRUE(test.TestExecute_MulticastConnectDataDisconnect());
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,239 @@
/**
* @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 PublishingMode = "Auto" with a valid MinRefreshRate.
*/
bool TestInitialise_AutoMode_Valid();
/**
* @brief Tests that PublishingMode = "Auto" without MinRefreshRate is rejected.
*/
bool TestInitialise_AutoMode_MissingRefreshRate();
/**
* @brief Tests that an unknown PublishingMode string is rejected.
*/
bool TestInitialise_UnknownPublishingMode();
/**
* @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 CONNECTDATADISCONNECT cycle with high-frequency packed signals.
* Reassembles all fragments and verifies the total payload size.
*/
bool TestHighFrequency_DataIntegrity();
/**
* @brief Tests Initialise with MulticastGroup and explicit DataPort.
*/
bool TestInitialise_MulticastMode_Valid();
/**
* @brief Tests that DataPort defaults to Port+1 when MulticastGroup is set but DataPort is absent.
*/
bool TestInitialise_MulticastMode_DefaultDataPort();
/**
* @brief Tests that DataPort equal to Port is rejected.
*/
bool TestInitialise_MulticastMode_InvalidDataPort();
/**
* @brief Tests PrepareNextState in multicast mode: TCP listener opens, data socket connects.
*/
bool TestPrepareNextState_Multicast();
/**
* @brief Tests full TCP CONNECT CONFIG DATA via multicast DISCONNECT on loopback.
*/
bool TestExecute_MulticastConnectDataDisconnect();
};
#endif /* UDPSTREAMERTEST_H_ */
+569
View File
@@ -0,0 +1,569 @@
/**
* Test MARTe2 application for UDPStreamer DataSource.
*
* Three independent RT threads demonstrate all four UDPStreamer time modes:
*
* Thread1 "Streamer" port 44500 TCP control / 44503 UDP multicast 239.0.0.1
* (scalar signals, auto-accumulated to arrays[10], multicast mode,
* 100 Hz flush = 10 samples per packet, 1 kHz source)
* Counter uint32 cycle counter
* Time uint32 time in microseconds (LinuxTimer, 1 kHz)
* Sine1 float32, 1 Hz, quantised to uint16 on wire (PacketTime)
* Sine2 float32, 0.3 Hz, raw float32 on wire (PacketTime)
*
* Thread2 "FastStreamer" port 44501 (packed arrays, FirstSample + LastSample)
* Time uint32 scalar anchor (5 kHz LinuxTimer)
* Ch1 float32[1000], 1 kHz sine (TimeMode = FirstSample)
* Ch2 float32[1000], 10 kHz sine (TimeMode = LastSample)
* Both channels use Time as the anchor and SamplingRate = 5 000 000 Hz so
* the WebUI reconstructs the 200 ns per-sample timestamps.
*
* Thread3 "FullArrStreamer" port 44502 (packed arrays, FullArray)
* TimeArray uint64[1000] per-sample timestamps in ns generated by TimeArrayGAM
* Ch3 float32[1000], 3 kHz sine (TimeMode = FullArray)
* Ch4 float32[1000], 500 Hz sine (TimeMode = FullArray)
* The WebUI uses the explicit timestamp for each sample rather than
* reconstructing them from a scalar anchor.
*/
$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
}
}
}
// ── 5 kHz fast timer for packed-array threads ────────────────────────
+FastTimerGAM = {
Class = IOGAM
InputSignals = {
Time = {
Frequency = 5000
DataSource = FastTimer
Type = uint32
}
}
OutputSignals = {
Time = {
DataSource = DDB2
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 (FirstSample anchor) ───────────────
+SineGAM3 = {
Class = SineArrayGAM
Frequency = 1000.0
Amplitude = 1.0
Phase = 0.0
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch1 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
// ── 10 kHz sine burst channel 2 (LastSample anchor) ───────────────
+SineGAM4 = {
Class = SineArrayGAM
Frequency = 10000.0
Amplitude = 0.5
Phase = 1.5708
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch2 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
// ── Route scalar signals → Streamer ──────────────────────────────────
+StreamerGAM = {
Class = IOGAM
InputSignals = {
Counter = {
DataSource = DDB
Type = uint32
}
Time = {
DataSource = DDB
Type = uint32
}
Sine1 = {
DataSource = DDB
Type = float32
}
Sine2 = {
DataSource = DDB
Type = float32
}
}
OutputSignals = {
Counter = {
DataSource = Streamer
Type = uint32
}
Time = {
DataSource = Streamer
Type = uint32
}
Sine1 = {
DataSource = Streamer
Type = float32
}
Sine2 = {
DataSource = Streamer
Type = float32
}
}
}
// ── Route packed arrays → FastStreamer (FirstSample + LastSample) ────
+FastStreamerGAM = {
Class = IOGAM
InputSignals = {
Time = {
DataSource = DDB2
Type = uint32
}
Ch1 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch2 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
OutputSignals = {
Time = {
DataSource = FastStreamer
Type = uint32
}
Ch1 = {
DataSource = FastStreamer
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch2 = {
DataSource = FastStreamer
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
// ── 3 kHz sine burst channel 3 (FullArray anchor) ─────────────────
+SineGAM5 = {
Class = SineArrayGAM
Frequency = 3000.0
Amplitude = 2.0
Phase = 0.0
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch3 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
// ── 500 Hz sine burst channel 4 (FullArray anchor) ─────────────────
+SineGAM6 = {
Class = SineArrayGAM
Frequency = 500.0
Amplitude = 3.0
Phase = 0.7854
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch4 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
// ── Build per-sample time array for FullArray channels ────────────────
// TimeArrayGAM expands the scalar LinuxTimer Time (uint32, µs) into a
// uint64[1000] array in nanoseconds where element[k] = Time_ns + k * period_ns.
// Using ns preserves sub-µs resolution at sampling rates > 1 MHz.
+TimeArrayGAM1 = {
Class = TimeArrayGAM
SamplingRate = 5000000.0
Anchor = FirstSample
InputSignals = {
Time = {
DataSource = DDB3
Type = uint32
}
}
OutputSignals = {
TimeArray = {
DataSource = DDB3
Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
// ── Fast timer for FullArray thread ───────────────────────────────────
+FullArrTimerGAM = {
Class = IOGAM
InputSignals = {
Time = {
Frequency = 5000
DataSource = FullArrTimer
Type = uint32
}
}
OutputSignals = {
Time = {
DataSource = DDB3
Type = uint32
}
}
}
// ── Route FullArray channels → FullArrStreamer ─────────────────────
+FullArrStreamerGAM = {
Class = IOGAM
InputSignals = {
TimeArray = {
DataSource = DDB3
Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch3 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch4 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
OutputSignals = {
TimeArray = {
DataSource = FullArrStreamer
Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch3 = {
DataSource = FullArrStreamer
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch4 = {
DataSource = FullArrStreamer
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
+DDB = {
Class = GAMDataSource
}
+DDB2 = {
Class = GAMDataSource
}
+DDB3 = {
Class = GAMDataSource
}
// ── 1 kHz real-time clock (Thread1) ──────────────────────────────────
+Timer = {
Class = LinuxTimer
SleepNature = "Default"
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
// ── 5 kHz fast timer (Thread2 FirstSample / LastSample) ────────────
+FastTimer = {
Class = LinuxTimer
SleepNature = "Default"
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
// ── 5 kHz fast timer (Thread3 FullArray) ───────────────────────────
+FullArrTimer = {
Class = LinuxTimer
SleepNature = "Default"
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
// ── Streamer: scalar signals, PacketTime (port 44500) ─────────────────
// Multicast mode: clients connect via TCP on port 44500 to receive the
// CONFIG packet, then join 239.0.0.1:44503 to receive DATA datagrams.
// Auto publishing ensures data is sent every RT cycle (1 kHz = 1 ms
// temporal resolution) but never faster, so the rate is bounded.
+Streamer = {
Class = UDPStreamer
Port = 44500
MulticastGroup = "239.0.0.1"
DataPort = 44503
MaxPayloadSize = 1400
PublishingMode = "Accumulate"
MinRefreshRate = 100
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
}
}
}
// ── FastStreamer: packed arrays, FirstSample + LastSample (port 44501)
//
// Ch1 uses TimeMode = FirstSample:
// Time is the timestamp of sample [0]; later samples are extrapolated
// forward: t[k] = Time + k / SamplingRate.
//
// Ch2 uses TimeMode = LastSample:
// Time is the timestamp of sample [N-1]; earlier samples are
// extrapolated backward: t[k] = Time - (N-1-k) / SamplingRate.
//
// Both modes produce identical wall-clock placements for a fixed-rate
// signal and are shown here side-by-side for comparison.
+FastStreamer = {
Class = UDPStreamer
Port = 44501
MaxPayloadSize = 1400
Signals = {
Time = {
Type = uint32
Unit = "us"
}
Ch1 = {
Type = float32
Unit = "V"
NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = FirstSample
TimeSignal = Time
SamplingRate = 5000000.0
}
Ch2 = {
Type = float32
Unit = "V"
NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = LastSample
TimeSignal = Time
SamplingRate = 5000000.0
}
}
}
// ── FullArrStreamer: packed arrays, FullArray (port 44502) ─────────────
//
// TimeMode = FullArray: the TimeSignal (TimeArray) has the same
// NumberOfElements as the data channel. Each sample pair
// (TimeArray[k], Ch3[k]) provides its own independent timestamp.
// This mode handles non-uniform sampling and explicit per-sample clocks.
+FullArrStreamer = {
Class = UDPStreamer
Port = 44502
MaxPayloadSize = 1400
Signals = {
TimeArray = {
Type = uint64
Unit = "ns"
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch3 = {
Type = float32
Unit = "V"
NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = FullArray
TimeSignal = TimeArray
}
Ch4 = {
Type = float32
Unit = "V"
NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = FullArray
TimeSignal = TimeArray
}
}
}
+Timings = {
Class = TimingDataSource
}
}
+States = {
Class = ReferenceContainer
+Running = {
Class = RealTimeState
+Threads = {
Class = ReferenceContainer
// Thread1: scalar signals at 1 kHz
+Thread1 = {
Class = RealTimeThread
CPUs = 0x1
Functions = {TimerGAM SineGAM1 SineGAM2 StreamerGAM}
}
// Thread2: packed arrays at 5 kHz (FirstSample + LastSample)
+Thread2 = {
Class = RealTimeThread
CPUs = 0x2
Functions = {FastTimerGAM SineGAM3 SineGAM4 FastStreamerGAM}
}
// Thread3: packed arrays at 5 kHz (FullArray with explicit timestamps)
+Thread3 = {
Class = RealTimeThread
CPUs = 0x4
Functions = {FullArrTimerGAM SineGAM5 SineGAM6 TimeArrayGAM1 FullArrStreamerGAM}
}
}
}
}
+Scheduler = {
Class = GAMScheduler
TimingDataSource = Timings
}
}
+384
View File
@@ -0,0 +1,384 @@
/**
* Combined integration test configuration.
*
* Exercises all components in the repo simultaneously:
* - SineArrayGAM — array sine wave generation
* - TimeArrayGAM — per-sample timestamp arrays
* - UDPStreamer — UDPS wire protocol (ports 44500, 44501, 44502)
* - DebugService — live signal tracing / forcing / breakpoints (ports 8080-8082)
* TcpLogger is auto-injected by DebugService on port 8082 (log port).
*
* Three real-time threads:
*
* Thread1 1 kHz — scalar signals → UDPStreamer port 44500 (Accumulate/multicast)
* Counter, Time, Sine1 (1 Hz), Sine2 (0.3 Hz)
*
* Thread2 5 kHz — packed arrays → UDPStreamer port 44501
* Ch1 float32[1000] @ 1 kHz (TimeMode = FirstSample)
* Ch2 float32[1000] @ 10 kHz (TimeMode = LastSample)
*
* Thread3 5 kHz — FullArray + explicit timestamps → UDPStreamer port 44502
* Ch3 float32[1000] @ 3 kHz (TimeMode = FullArray)
* Ch4 float32[1000] @ 500 Hz (TimeMode = FullArray)
*/
$App = {
Class = RealTimeApplication
+Functions = {
Class = ReferenceContainer
// ── Thread1: 1 kHz scalar signals ────────────────────────────────────
+TimerGAM = {
Class = IOGAM
InputSignals = {
Counter = {
DataSource = Timer
Type = uint32
Frequency = 1000
}
Time = {
DataSource = Timer
Type = uint32
}
}
OutputSignals = {
Counter = { DataSource = DDB1 Type = uint32 }
Time = { DataSource = DDB1 Type = uint32 }
}
}
+SineGAM1 = {
Class = SineArrayGAM
Frequency = 1.0
Amplitude = 5.0
Phase = 0.0
Offset = 0.0
SamplingRate = 1000.0
OutputSignals = {
Sine1 = { DataSource = DDB1 Type = float32 }
}
}
+SineGAM2 = {
Class = SineArrayGAM
Frequency = 0.3
Amplitude = 3.0
Phase = 1.0472
Offset = 0.0
SamplingRate = 1000.0
OutputSignals = {
Sine2 = { DataSource = DDB1 Type = float32 }
}
}
+StreamerGAM1 = {
Class = IOGAM
InputSignals = {
Counter = { DataSource = DDB1 Type = uint32 }
Time = { DataSource = DDB1 Type = uint32 }
Sine1 = { DataSource = DDB1 Type = float32 }
Sine2 = { DataSource = DDB1 Type = float32 }
}
OutputSignals = {
Counter = { DataSource = Streamer1 Type = uint32 }
Time = { DataSource = Streamer1 Type = uint32 }
Sine1 = { DataSource = Streamer1 Type = float32 }
Sine2 = { DataSource = Streamer1 Type = float32 }
}
}
// ── Thread2: 5 kHz packed arrays (FirstSample / LastSample) ──────────
+FastTimerGAM = {
Class = IOGAM
InputSignals = {
Time = {
DataSource = FastTimer
Type = uint32
Frequency = 5000
}
}
OutputSignals = {
Time = { DataSource = DDB2 Type = uint32 }
}
}
+SineGAM3 = {
Class = SineArrayGAM
Frequency = 1000.0
Amplitude = 1.0
Phase = 0.0
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch1 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
+SineGAM4 = {
Class = SineArrayGAM
Frequency = 10000.0
Amplitude = 0.5
Phase = 1.5708
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch2 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
+StreamerGAM2 = {
Class = IOGAM
InputSignals = {
Time = { DataSource = DDB2 Type = uint32 }
Ch1 = { DataSource = DDB2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Ch2 = { DataSource = DDB2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
}
OutputSignals = {
Time = { DataSource = Streamer2 Type = uint32 }
Ch1 = { DataSource = Streamer2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Ch2 = { DataSource = Streamer2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
}
}
// ── Thread3: 5 kHz FullArray with per-sample timestamps ──────────────
+FullArrTimerGAM = {
Class = IOGAM
InputSignals = {
Time = {
DataSource = FullArrTimer
Type = uint32
Frequency = 5000
}
}
OutputSignals = {
Time = { DataSource = DDB3 Type = uint32 }
}
}
+SineGAM5 = {
Class = SineArrayGAM
Frequency = 3000.0
Amplitude = 2.0
Phase = 0.0
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch3 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
+SineGAM6 = {
Class = SineArrayGAM
Frequency = 500.0
Amplitude = 3.0
Phase = 0.7854
Offset = 0.0
SamplingRate = 5000000.0
OutputSignals = {
Ch4 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
+TimeArrayGAM1 = {
Class = TimeArrayGAM
SamplingRate = 5000000.0
Anchor = FirstSample
InputSignals = {
Time = { DataSource = DDB3 Type = uint32 }
}
OutputSignals = {
TimeArray = {
DataSource = DDB3
Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
+StreamerGAM3 = {
Class = IOGAM
InputSignals = {
TimeArray = { DataSource = DDB3 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 }
Ch3 = { DataSource = DDB3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Ch4 = { DataSource = DDB3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
}
OutputSignals = {
TimeArray = { DataSource = Streamer3 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 }
Ch3 = { DataSource = Streamer3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Ch4 = { DataSource = Streamer3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
}
}
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB1
+DDB1 = { Class = GAMDataSource }
+DDB2 = { Class = GAMDataSource }
+DDB3 = { Class = GAMDataSource }
// ── Timers (one per thread) ───────────────────────────────────────────
+Timer = {
Class = LinuxTimer
SleepNature = "Default"
Signals = {
Counter = { Type = uint32 }
Time = { Type = uint32 }
}
}
+FastTimer = {
Class = LinuxTimer
SleepNature = "Default"
Signals = {
Counter = { Type = uint32 }
Time = { Type = uint32 }
}
}
+FullArrTimer = {
Class = LinuxTimer
SleepNature = "Default"
Signals = {
Counter = { Type = uint32 }
Time = { Type = uint32 }
}
}
// ── UDPStreamer instances ─────────────────────────────────────────────
// Thread1: scalar signals, Accumulate mode, multicast output
+Streamer1 = {
Class = UDPStreamer
Port = 44500
MulticastGroup = "239.0.0.1"
DataPort = 44503
MaxPayloadSize = 1400
PublishingMode = "Accumulate"
MinRefreshRate = 100
Signals = {
Counter = { Type = uint32 }
Time = { Type = uint32 Unit = "us" }
Sine1 = { Type = float32 Unit = "V" RangeMin = -5.0 RangeMax = 5.0 }
Sine2 = { Type = float32 Unit = "V" RangeMin = -3.0 RangeMax = 3.0 }
}
}
// Thread2: packed arrays, FirstSample + LastSample
+Streamer2 = {
Class = UDPStreamer
Port = 44501
MaxPayloadSize = 1400
Signals = {
Time = { Type = uint32 Unit = "us" }
Ch1 = {
Type = float32 Unit = "V"
NumberOfDimensions = 1 NumberOfElements = 1000
TimeMode = FirstSample TimeSignal = Time SamplingRate = 5000000.0
}
Ch2 = {
Type = float32 Unit = "V"
NumberOfDimensions = 1 NumberOfElements = 1000
TimeMode = LastSample TimeSignal = Time SamplingRate = 5000000.0
}
}
}
// Thread3: packed arrays, FullArray (per-sample timestamps)
+Streamer3 = {
Class = UDPStreamer
Port = 44502
MaxPayloadSize = 1400
Signals = {
TimeArray = {
Type = uint64 Unit = "ns"
NumberOfDimensions = 1 NumberOfElements = 1000
}
Ch3 = {
Type = float32 Unit = "V"
NumberOfDimensions = 1 NumberOfElements = 1000
TimeMode = FullArray TimeSignal = TimeArray
}
Ch4 = {
Type = float32 Unit = "V"
NumberOfDimensions = 1 NumberOfElements = 1000
TimeMode = FullArray TimeSignal = TimeArray
}
}
}
+Timings = { Class = TimingDataSource }
}
+States = {
Class = ReferenceContainer
+Running = {
Class = RealTimeState
+Threads = {
Class = ReferenceContainer
+Thread1 = {
Class = RealTimeThread
CPUs = 0x1
Functions = {TimerGAM SineGAM1 SineGAM2 StreamerGAM1}
}
+Thread2 = {
Class = RealTimeThread
CPUs = 0x2
Functions = {FastTimerGAM SineGAM3 SineGAM4 StreamerGAM2}
}
+Thread3 = {
Class = RealTimeThread
CPUs = 0x4
Functions = {FullArrTimerGAM SineGAM5 SineGAM6 TimeArrayGAM1 StreamerGAM3}
}
}
}
}
+Scheduler = {
Class = GAMScheduler
TimingDataSource = Timings
}
}
// ── DebugService ──────────────────────────────────────────────────────────────
// Patches the broker registry at startup so every signal is traceable.
// TcpLogger is auto-injected on LogPort (9090) — no explicit DataSource needed.
// Connect the debugger web UI (./run_combined_test.sh -d) to:
// Host=127.0.0.1 TCP=8080 UDP=8081 Log=9090
+DebugService = {
Class = DebugService
ControlPort = 8080
StreamPort = 8081
LogPort = 9090
StreamIP = "127.0.0.1"
}
+189
View File
@@ -0,0 +1,189 @@
+App = {
Class = RealTimeApplication
+Functions = {
Class = ReferenceContainer
+GAM1 = {
Class = IOGAM
InputSignals = {
Counter = {
DataSource = Timer
Type = uint32
Frequency = 1000
}
Time = {
DataSource = Timer
Type = uint32
}
}
OutputSignals = {
Counter = {
DataSource = SyncDB
Type = uint32
}
Time = {
DataSource = DDB
Type = uint32
}
}
}
+CGAM = {
Class = ConstantGAM
OutputSignals = {
Test = {
DataSource = DDB2
Type = float32
Default = 0.123
}
}
}
+GAM2 = {
Class = IOGAM
InputSignals = {
Counter = {
DataSource = TimerSlow
Frequency = 1
}
Time = {
DataSource = TimerSlow
}
Test = {
DataSource = DDB2
Type = float32
}
}
OutputSignals = {
Counter = {
Type = uint32
DataSource = Logger
}
Time = {
Type = uint32
DataSource = Logger
}
ConstOut = {
DataSource = Logger
Type = float32
}
}
}
+GAM3 = {
Class = IOGAM
InputSignals = {
Counter = {
Frequency = 1
Samples = 100
Type = uint32
DataSource = SyncDB
}
}
OutputSignals = {
Counter = {
DataSource = DDB3
NumberOfElements = 100
Type = uint32
}
}
}
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
+SyncDB = {
Class = RealTimeThreadSynchronisation
Timeout = 200
Signals = {
Counter = {
Type = uint32
}
}
}
+Timer = {
Class = LinuxTimer
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
+TimerSlow = {
Class = LinuxTimer
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
+Logger = {
Class = LoggerDataSource
Signals = {
CounterCopy = {
Type = uint32
}
TimeCopy = {
Type = uint32
}
}
}
+DDB = {
AllowNoProducer = 1
Class = GAMDataSource
Signals = {
Counter= {
Type = uint32
}
}
}
+DDB2 = {
AllowNoProducer = 1
Class = GAMDataSource
}
+DDB3 = {
AllowNoProducer = 1
Class = GAMDataSource
}
+DAMS = {
Class = TimingDataSource
}
}
+States = {
Class = ReferenceContainer
+State1 = {
Class = RealTimeState
+Threads = {
Class = ReferenceContainer
+Thread1 = {
Class = RealTimeThread
Functions = {GAM1}
}
+Thread2 = {
Class = RealTimeThread
Functions = {GAM2 CGAM}
}
+Thread3 = {
Class = RealTimeThread
Functions = {GAM3}
}
}
}
}
+Scheduler = {
Class = GAMScheduler
TimingDataSource = DAMS
}
}
+DebugService = {
Class = DebugService
ControlPort = 8080
UdpPort = 8081
LogPort = 8082
StreamIP = "127.0.0.1"
}
+186
View File
@@ -0,0 +1,186 @@
+App = {
Class = RealTimeApplication
+Functions = {
Class = ReferenceContainer
+GAM1 = {
Class = IOGAM
InputSignals = {
Counter = {
DataSource = Timer
Type = uint32
Frequency = 1000
}
Time = {
DataSource = Timer
Type = uint32
}
}
OutputSignals = {
Counter = {
DataSource = SyncDB
Type = uint32
}
Time = {
DataSource = DDB
Type = uint32
}
}
}
+CGAM = {
Class = ConstantGAM
OutputSignals = {
Test = {
DataSource = DDB2
Type = float32
Default = 0.123
}
}
}
+GAM2 = {
Class = IOGAM
InputSignals = {
Counter = {
DataSource = TimerSlow
Frequency = 1
}
Time = {
DataSource = TimerSlow
}
Test = {
DataSource = DDB2
Type = float32
}
}
OutputSignals = {
Counter = {
Type = uint32
DataSource = Logger
}
Time = {
Type = uint32
DataSource = Logger
}
ConstOut = {
DataSource = Logger
Type = float32
}
}
}
+GAM3 = {
Class = IOGAM
InputSignals = {
Counter = {
Frequency = 1
Samples = 100
Type = uint32
DataSource = SyncDB
}
}
OutputSignals = {
Counter = {
DataSource = DDB3
NumberOfElements = 100
Type = uint32
}
}
}
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
+SyncDB = {
Class = RealTimeThreadSynchronisation
Timeout = 200
Signals = {
Counter = {
Type = uint32
}
}
}
+Timer = {
Class = LinuxTimer
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
+TimerSlow = {
Class = LinuxTimer
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
+Logger = {
Class = LoggerDataSource
Signals = {
CounterCopy = {
Type = uint32
}
TimeCopy = {
Type = uint32
}
}
}
+DDB = {
AllowNoProducer = 1
Class = GAMDataSource
Signals = {
Counter= {
Type = uint32
}
}
}
+DDB2 = {
AllowNoProducer = 1
Class = GAMDataSource
}
+DDB3 = {
AllowNoProducer = 1
Class = GAMDataSource
}
+DAMS = {
Class = TimingDataSource
}
}
+States = {
Class = ReferenceContainer
+State1 = {
Class = RealTimeState
+Threads = {
Class = ReferenceContainer
+Thread1 = {
Class = RealTimeThread
Functions = {GAM1}
}
+Thread2 = {
Class = RealTimeThread
Functions = {GAM2 CGAM}
}
+Thread3 = {
Class = RealTimeThread
Functions = {GAM3}
}
}
}
}
+Scheduler = {
Class = GAMScheduler
TimingDataSource = DAMS
}
}
+DebugService = {
Class = WebDebugService
HttpPort = 8090
}
+29
View File
@@ -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();
}
+34
View File
@@ -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 += $(MARTe2_DIR)/Lib/gtest-1.7.0/libgtest.a -lpthread
LIBRARIES += -L$(MARTe2_DIR)/Build/$(TARGET)/Core -lMARTe2
LIBRARIES += -ldl
#Look for all the statically linked files
LIBRARIES_STATIC+=$(shell find $(ROOT_DIR)/Build/$(TARGET) -name "*.a")
+55
View File
@@ -0,0 +1,55 @@
#############################################################
#
# 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$(ROOT_DIR)/Common/UDP
INCLUDES += -I$(ROOT_DIR)/Source/Components/DataSources/UDPStreamer
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/DebugService
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger
INCLUDES += -I$(ROOT_DIR)/Test/Components/DataSources/UDPStreamer
all: $(BUILD_DIR)/MainGTest$(EXEEXT)
include depends.$(TARGET)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
+101
View File
@@ -0,0 +1,101 @@
../../Build/x86-linux//GTest/MainGTest.o: MainGTest.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-internal.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-port.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-message.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-string.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-filepath.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-type-util.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-death-test.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-death-test-internal.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-param-test.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-param-util.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-linked_ptr.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-printers.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-param-util-generated.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest_prod.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-test-part.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-typed-test.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest_pred_impl.h
+101
View File
@@ -0,0 +1,101 @@
MainGTest.o: MainGTest.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-internal.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-port.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-message.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-string.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-filepath.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-type-util.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-death-test.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-death-test-internal.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-param-test.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-param-util.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-linked_ptr.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-printers.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/internal/gtest-param-util-generated.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest_prod.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-test-part.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest-typed-test.h \
/home/martino/workspace/MARTe2/Lib/gtest-1.7.0/include/gtest/gtest_pred_impl.h
+161
View File
@@ -0,0 +1,161 @@
#include "BasicTCPSocket.h"
#include "DebugService.h"
#include "ObjectRegistryDatabase.h"
#include "StandardParser.h"
#include "StreamString.h"
#include "GlobalObjectsDatabase.h"
#include "RealTimeApplication.h"
#include <assert.h>
#include <stdio.h>
using namespace MARTe;
const char8 * const config_command_text =
"DebugService = {"
" Class = DebugService "
" ControlPort = 8100 "
" UdpPort = 8101 "
" StreamIP = \"127.0.0.1\" "
" MyCustomField = \"HelloConfig\" "
"}"
"App = {"
" Class = RealTimeApplication "
" +Functions = {"
" Class = ReferenceContainer "
" +GAM1 = {"
" Class = IOGAM "
" CustomGAMField = \"GAMValue\" "
" InputSignals = {"
" Counter = { DataSource = Timer Type = uint32 Frequency = 1000 PVName = \"PROC:VAR:1\" }"
" Time = { DataSource = Timer Type = uint32 }"
" }"
" OutputSignals = {"
" Counter = { DataSource = DDB Type = uint32 }"
" Time = { DataSource = DDB Type = uint32 }"
" }"
" }"
" }"
" +Data = {"
" Class = ReferenceContainer "
" +Timer = { Class = LinuxTimer SleepTime = 1000 Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }"
" +DDB = { Class = GAMDataSource Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }"
" +DAMS = { Class = TimingDataSource }"
" }"
" +States = {"
" Class = ReferenceContainer "
" +State1 = { Class = RealTimeState +Threads = { Class = ReferenceContainer +Thread1 = { Class = RealTimeThread Functions = {GAM1} } } }"
" }"
" +Scheduler = { Class = GAMScheduler TimingDataSource = DAMS }"
"}";
static bool SendCommandAndGetReply(uint16 port, const char8* cmd, StreamString &reply) {
BasicTCPSocket client;
if (!client.Open()) return false;
if (!client.Connect("127.0.0.1", port)) return false;
uint32 s = StringHelper::Length(cmd);
if (!client.Write(cmd, s)) return false;
char buffer[4096];
uint32 size = 4096;
TimeoutType timeout(2000000); // 2s
if (client.Read(buffer, size, timeout)) {
reply.Write(buffer, size);
client.Close();
return true;
}
client.Close();
return false;
}
void TestConfigCommands() {
printf("--- MARTe2 Config & Metadata Enrichment Test ---\n");
ObjectRegistryDatabase::Instance()->Purge();
ConfigurationDatabase cdb;
StreamString ss = config_command_text;
ss.Seek(0);
StandardParser parser(ss, cdb);
assert(parser.Parse());
cdb.MoveToRoot();
uint32 n = cdb.GetNumberOfChildren();
for (uint32 i=0; i<n; i++) {
const char8* name = cdb.GetChildName(i);
ConfigurationDatabase child;
cdb.MoveRelative(name);
cdb.Copy(child);
cdb.MoveToAncestor(1u);
StreamString className;
child.Read("Class", className);
Reference ref(className.Buffer(), GlobalObjectsDatabase::Instance()->GetStandardHeap());
ref->SetName(name);
assert(ref->Initialise(child));
ObjectRegistryDatabase::Instance()->Insert(ref);
}
printf("Application and DebugService (port 8100) initialised.\n");
// Start the application to trigger broker execution and signal registration
ReferenceT<RealTimeApplication> app = ObjectRegistryDatabase::Instance()->Find("App");
assert(app.IsValid());
assert(app->ConfigureApplication());
assert(app->PrepareNextState("State1") == ErrorManagement::NoError);
assert(app->StartNextStateExecution() == ErrorManagement::NoError);
printf("Application started (for signal registration).\n");
Sleep::MSec(500); // Wait for some cycles
ReferenceT<DebugService> service = ObjectRegistryDatabase::Instance()->Find("DebugService");
if (service.IsValid()) {
service->SetFullConfig(cdb);
}
Sleep::MSec(1000);
// 1. Test CONFIG command
{
printf("Testing CONFIG command...\n");
StreamString reply;
assert(SendCommandAndGetReply(8100, "CONFIG\n", reply));
printf("\n%s\n", reply.Buffer());
// Verify it contains some key parts of the config
assert(StringHelper::SearchString(reply.Buffer(), "MyCustomField") != NULL_PTR(const char8*));
assert(StringHelper::SearchString(reply.Buffer(), "HelloConfig") != NULL_PTR(const char8*));
assert(StringHelper::SearchString(reply.Buffer(), "PROC:VAR:1") != NULL_PTR(const char8*));
assert(StringHelper::SearchString(reply.Buffer(), "OK CONFIG") != NULL_PTR(const char8*));
printf("SUCCESS: CONFIG command validated.\n");
}
// 2. Test INFO on object with enrichment
{
printf("Testing INFO on App.Functions.GAM1...\n");
StreamString reply;
assert(SendCommandAndGetReply(8100, "INFO App.Functions.GAM1\n", reply));
// Check standard MARTe fields (Name, Class)
assert(StringHelper::SearchString(reply.Buffer(), "\"Name\": \"GAM1\"") != NULL_PTR(const char8*));
// Check enriched fields from fullConfig
assert(StringHelper::SearchString(reply.Buffer(), "\"CustomGAMField\": \"GAMValue\"") != NULL_PTR(const char8*));
assert(StringHelper::SearchString(reply.Buffer(), "OK INFO") != NULL_PTR(const char8*));
printf("SUCCESS: Object metadata enrichment validated.\n");
}
// 3. Test INFO on signal with enrichment
{
printf("Testing INFO on App.Functions.GAM1.In.Counter...\n");
StreamString reply;
assert(SendCommandAndGetReply(8100, "INFO App.Functions.GAM1.In.Counter\n", reply));
// Check enriched fields from signal configuration
assert(StringHelper::SearchString(reply.Buffer(), "\"Frequency\": \"1000\"") != NULL_PTR(const char8*));
assert(StringHelper::SearchString(reply.Buffer(), "\"PVName\": \"PROC:VAR:1\"") != NULL_PTR(const char8*));
assert(StringHelper::SearchString(reply.Buffer(), "OK INFO") != NULL_PTR(const char8*));
printf("SUCCESS: Signal metadata enrichment validated.\n");
}
if (app.IsValid()) {
app->StopCurrentStateExecution();
}
ObjectRegistryDatabase::Instance()->Purge();
}
+252
View File
@@ -0,0 +1,252 @@
#include "ClassRegistryDatabase.h"
#include "ConfigurationDatabase.h"
#include "DebugService.h"
#include "ObjectRegistryDatabase.h"
#include "ErrorManagement.h"
#include "BasicTCPSocket.h"
#include "BasicUDPSocket.h"
#include "RealTimeApplication.h"
#include "StandardParser.h"
#include "TestCommon.h"
#include <stdio.h>
using namespace MARTe;
#include <signal.h>
#include <unistd.h>
void timeout_handler(int sig) {
printf("Test timed out!\n");
_exit(1);
}
void ErrorProcessFunction(const MARTe::ErrorManagement::ErrorInformation &errorInfo, const char8 * const errorDescription) {
printf("[MARTe Error] %s: %s\n", errorInfo.className, errorDescription);
}
// Forward declarations of other tests
void TestSchedulerControl();
void TestFullTracePipeline();
void RunValidationTest();
void TestConfigCommands();
void TestGAMSignalTracing();
void TestTreeCommand();
int main() {
signal(SIGALRM, timeout_handler);
alarm(180);
MARTe::ErrorManagement::SetErrorProcessFunction(&ErrorProcessFunction);
printf("MARTe2 Debug Suite Integration Tests\n");
printf("\n--- Test 1: Registry Patching ---\n");
{
ObjectRegistryDatabase::Instance()->Purge();
DebugService service;
ConfigurationDatabase serviceData;
serviceData.Write("ControlPort", (uint32)9090);
service.Initialise(serviceData);
printf("DebugService initialized and Registry Patched.\n");
ClassRegistryItem *item =
ClassRegistryDatabase::Instance()->Find("MemoryMapInputBroker");
if (item != NULL_PTR(ClassRegistryItem *)) {
Object *obj = item->GetObjectBuilder()->Build(
GlobalObjectsDatabase::Instance()->GetStandardHeap());
if (obj != NULL_PTR(Object *)) {
printf("Instantiated Broker Class: %s\n",
obj->GetClassProperties()->GetName());
printf("Success: Broker patched and instantiated.\n");
} else {
printf("Failed to build broker\n");
}
} else {
printf("MemoryMapInputBroker not found in registry\n");
}
}
Sleep::MSec(1000);
printf("\n--- Test 2: Full Trace Pipeline ---\n");
TestFullTracePipeline();
Sleep::MSec(1000);
printf("\n--- Test 3: Scheduler Control ---\n");
TestSchedulerControl();
Sleep::MSec(1000);
printf("\n--- Test 4: 1kHz Lossless Trace Validation ---\n");
RunValidationTest();
Sleep::MSec(1000);
printf("\n--- Test 5: Config & Metadata Enrichment ---\n");
TestConfigCommands();
Sleep::MSec(1000);
printf("\n--- Test 6: TREE Command Enhancement ---\n");
TestTreeCommand();
Sleep::MSec(1000);
printf("\n--- Test 7: Custom MARTe Message (MSG) ---\n");
TestMessageCommand();
Sleep::MSec(1000);
printf("\nAll Integration Tests Finished.\n");
return 0;
}
// --- Test Implementation ---
void TestGAMSignalTracing() {
printf("--- Test: GAM Signal Tracing Issue ---\n");
ObjectRegistryDatabase::Instance()->Purge();
ConfigurationDatabase cdb;
StreamString ss = debug_test_config;
ss.Seek(0);
StandardParser parser(ss, cdb);
if (!parser.Parse()) {
printf("ERROR: Failed to parse config\n");
return;
}
cdb.MoveToRoot();
uint32 n = cdb.GetNumberOfChildren();
for (uint32 i=0; i<n; i++) {
const char8* name = cdb.GetChildName(i);
ConfigurationDatabase child;
cdb.MoveRelative(name);
cdb.Copy(child);
cdb.MoveToAncestor(1u);
StreamString className;
child.Read("Class", className);
Reference ref(className.Buffer(), GlobalObjectsDatabase::Instance()->GetStandardHeap());
if (!ref.IsValid()) {
printf("ERROR: Could not create object %s of class %s\n", name, className.Buffer());
continue;
}
ref->SetName(name);
if (!ref->Initialise(child)) {
printf("ERROR: Failed to initialise object %s\n", name);
continue;
}
ObjectRegistryDatabase::Instance()->Insert(ref);
}
ReferenceT<DebugService> service = ObjectRegistryDatabase::Instance()->Find("DebugService");
if (!service.IsValid()) {
printf("ERROR: DebugService not found\n");
return;
}
service->SetFullConfig(cdb);
ReferenceT<RealTimeApplication> app = ObjectRegistryDatabase::Instance()->Find("App");
if (!app.IsValid()) {
printf("ERROR: App not found\n");
return;
}
if (!app->ConfigureApplication()) {
printf("ERROR: ConfigureApplication failed.\n");
return;
}
if (app->PrepareNextState("State1") != ErrorManagement::NoError) {
printf("ERROR: PrepareNextState failed.\n");
return;
}
if (app->StartNextStateExecution() != ErrorManagement::NoError) {
printf("ERROR: StartNextStateExecution failed.\n");
return;
}
printf("Application started.\n");
Sleep::MSec(1000);
// Step 1: Discover signals
{
StreamString reply;
if (SendCommandGAM(8095, "DISCOVER\n", reply)) {
printf("DISCOVER response received (len=%llu)\n", reply.Size());
} else {
printf("ERROR: DISCOVER failed\n");
}
}
Sleep::MSec(500);
// Step 2: Trace a DataSource signal (Timer.Counter)
printf("\n--- Step 1: Trace DataSource signal (Timer.Counter) ---\n");
{
StreamString reply;
if (SendCommandGAM(8095, "TRACE App.Data.Timer.Counter 1\n", reply)) {
printf("TRACE response: %s", reply.Buffer());
} else {
printf("ERROR: TRACE failed\n");
}
}
Sleep::MSec(500);
// Step 3: Trace a GAM input signal (GAM1.In.Counter)
printf("\n--- Step 2: Trace GAM input signal (GAM1.In.Counter) ---\n");
{
StreamString reply;
if (SendCommandGAM(8095, "TRACE App.Functions.GAM1.In.Counter 1\n", reply)) {
printf("TRACE response: %s", reply.Buffer());
} else {
printf("ERROR: TRACE failed\n");
}
}
Sleep::MSec(500);
// Step 4: Try to trace another DataSource signal (TimerSlow.Counter)
printf("\n--- Step 3: Try to trace another signal (TimerSlow.Counter) ---\n");
{
StreamString reply;
if (SendCommandGAM(8095, "TRACE App.Data.TimerSlow.Counter 1\n", reply)) {
printf("TRACE response: %s", reply.Buffer());
} else {
printf("ERROR: TRACE failed\n");
}
}
Sleep::MSec(500);
// Step 5: Check if we can still trace more signals
printf("\n--- Step 4: Try to trace Logger.Counter ---\n");
{
StreamString reply;
if (SendCommandGAM(8095, "TRACE App.Data.Logger.Counter 1\n", reply)) {
printf("TRACE response: %s", reply.Buffer());
} else {
printf("ERROR: TRACE failed\n");
}
}
Sleep::MSec(500);
// Verify UDP is still receiving data
BasicUDPSocket listener;
listener.Open();
listener.Listen(8096);
char buffer[1024];
uint32 size = 1024;
TimeoutType timeout(1000);
int packetCount = 0;
while (listener.Read(buffer, size, timeout)) {
packetCount++;
size = 1024;
}
printf("\n--- Results ---\n");
if (packetCount > 0) {
printf("SUCCESS: Received %d UDP packets.\n", packetCount);
} else {
printf("FAILURE: No UDP packets received. Possible deadlock or crash.\n");
}
app->StopCurrentStateExecution();
}
+1
View File
@@ -0,0 +1 @@
include Makefile.inc
+43
View File
@@ -0,0 +1,43 @@
OBJSX = SchedulerTest.x TraceTest.x ValidationTest.x ConfigCommandTest.x TreeCommandTest.x MessageCommandTest.x TestCommon.x
PACKAGE = Test/Integration
ROOT_DIR = ../..
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
INCLUDES += -I$(ROOT_DIR)/Common/UDP
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/DebugService
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger
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/L4Logger
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L6App
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/L4LoggerService
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_Components_DIR)/Source/Components/GAMs/IOGAM
INCLUDES += -I$(MARTe2_Components_DIR)/Source/Components/DataSources/LinuxTimer
LIBRARIES += -L$(MARTe2_DIR)/Build/$(TARGET)/Core -lMARTe2
LIBRARIES += -L$(MARTe2_Components_DIR)/Build/$(TARGET)/Components/DataSources/LinuxTimer -lLinuxTimer
LIBRARIES += -L$(MARTe2_Components_DIR)/Build/$(TARGET)/Components/GAMs/IOGAM -lIOGAM
LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/DebugService -lDebugService
LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/TCPLogger -lTcpLogger
all: $(OBJS) $(BUILD_DIR)/IntegrationTests$(EXEEXT)
echo $(OBJS)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
+72
View File
@@ -0,0 +1,72 @@
#include "TestCommon.h"
#include "ObjectRegistryDatabase.h"
#include "DebugService.h"
#include "StandardParser.h"
#include "GlobalObjectsDatabase.h"
#include <stdio.h>
using namespace MARTe;
namespace MARTe {
void TestMessageCommand() {
printf("--- Test: Custom MARTe Message (MSG) ---\n");
ObjectRegistryDatabase::Instance()->Purge();
Sleep::MSec(1000);
ConfigurationDatabase cdb;
const char8 * const msg_test_config =
"DebugService = {"
" Class = DebugService "
" ControlPort = 8120 "
" UdpPort = 8121 "
" StreamIP = \"127.0.0.1\" "
"}";
StreamString ss = msg_test_config;
ss.Seek(0);
StandardParser parser(ss, cdb);
if (!parser.Parse()) {
printf("ERROR: Failed to parse config\n");
return;
}
// Initialize Service
ReferenceT<DebugService> service("DebugService", GlobalObjectsDatabase::Instance()->GetStandardHeap());
service->SetName("DebugService");
cdb.MoveToRoot();
if (cdb.MoveRelative("DebugService")) {
if (!service->Initialise(cdb)) {
printf("ERROR: Failed to initialize DebugService\n");
return;
}
}
ObjectRegistryDatabase::Instance()->Insert(service);
if (ObjectRegistryDatabase::Instance()->Find("DebugService").IsValid()) {
printf("DebugService successfully registered in ORD.\n");
} else {
printf("ERROR: DebugService NOT found in ORD.\n");
}
printf("Service initialized on port 8120.\n");
Sleep::MSec(500);
StreamString reply;
if (SendCommandGAM(8120, "MSG DebugService UnknownFunc 0 Key1=Val1\\nKey2=Val2\n", reply)) {
printf("MSG response received: %s", reply.Buffer());
if (StringHelper::SearchString(reply.Buffer(), "OK MSG") != NULL_PTR(const char8 *)) {
printf("SUCCESS: Asynchronous message dispatched correctly.\n");
} else {
printf("FAILURE: MSG command returned error.\n");
}
} else {
printf("ERROR: MSG command communication failed\n");
}
ObjectRegistryDatabase::Instance()->Purge();
}
} // namespace MARTe

Some files were not shown because too many files have changed in this diff Show More