Initial release
This commit is contained in:
+18
@@ -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
@@ -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 |
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
@@ -0,0 +1,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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>>=</option>
|
||||
<option><</option><option><=</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 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>
|
||||
· <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 <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><</option><option><=</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>
|
||||
· <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>
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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; } }
|
||||
+2
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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;}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
@@ -0,0 +1,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
@@ -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 |
@@ -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>
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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; } }
|
||||
+2
File diff suppressed because one or more lines are too long
+1
@@ -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;}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
@@ -0,0 +1,383 @@
|
||||
package 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()
|
||||
}
|
||||
}
|
||||
@@ -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:])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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 |
|
||||
@@ -0,0 +1,208 @@
|
||||
# UDPStreamer Wire Protocol
|
||||
|
||||
This document specifies the binary protocol used between UDPStreamer (server) and any
|
||||
compatible client (the included Go WebUI, a Python script, etc.).
|
||||
|
||||
All multi-byte integers are **little-endian**.
|
||||
|
||||
---
|
||||
|
||||
## Packet Header (17 bytes, packed)
|
||||
|
||||
Every datagram begins with a 17-byte header:
|
||||
|
||||
```
|
||||
Offset Size Type Field
|
||||
────── ──── ────── ────────────────────────────────────────────────────
|
||||
0 4 uint32 magic = 0x53504455 ('UDPS' LE)
|
||||
4 1 uint8 type see Packet Types below
|
||||
5 4 uint32 counter per-update sequence number
|
||||
(same across all fragments of one update)
|
||||
9 2 uint16 fragmentIdx 0-based index of this fragment
|
||||
11 2 uint16 totalFragments number of fragments for this update
|
||||
13 4 uint32 payloadBytes bytes of payload following this header
|
||||
```
|
||||
|
||||
**Total header size:** 17 bytes
|
||||
**Magic:** `0x55 0x44 0x50 0x53` (`UDPS`)
|
||||
|
||||
---
|
||||
|
||||
## Packet Types
|
||||
|
||||
| Value | Direction | Name | Description |
|
||||
|-------|-----------|------|-------------|
|
||||
| 0 | Server → Client | DATA | Signal data (may be fragmented) |
|
||||
| 1 | Server → Client | CONFIG | Signal metadata sent on connect |
|
||||
| 2 | Client → Server | ACK | Acknowledge a data counter (reserved) |
|
||||
| 3 | Client → Server | CONNECT | Request a session |
|
||||
| 4 | Client → Server | DISCONNECT | End the session |
|
||||
|
||||
---
|
||||
|
||||
## Session Flow
|
||||
|
||||
```
|
||||
Client Server
|
||||
────── ──────
|
||||
CONNECT (type=3) →
|
||||
← CONFIG (type=1)
|
||||
← DATA (type=0) ┐
|
||||
← DATA (type=0) │ repeated every RT cycle
|
||||
← DATA (type=0) ┘
|
||||
DISCONNECT (type=4) →
|
||||
```
|
||||
|
||||
1. Client sends a 17-byte CONNECT packet (`payloadBytes = 0`).
|
||||
2. Server responds immediately with one or more CONFIG fragments describing all signals.
|
||||
3. Server sends DATA fragments on every `Synchronise()` call while a client is connected.
|
||||
4. Client sends DISCONNECT to terminate cleanly. A new CONNECT replaces an existing session.
|
||||
|
||||
---
|
||||
|
||||
## CONFIG Payload
|
||||
|
||||
The CONFIG payload is sent as one or more fragmented packets (`type = 1`).
|
||||
After reassembly the layout is:
|
||||
|
||||
```
|
||||
Offset Size Type Field
|
||||
────── ──── ─────── ────────────────────────────────────
|
||||
0 4 uint32 numSignals
|
||||
── for each signal (136 bytes) ──────────────────────────────
|
||||
0 64 char[64] name null-terminated
|
||||
64 1 uint8 typeCode see Type Codes
|
||||
65 1 uint8 quantType see Quantization Types
|
||||
66 1 uint8 numDimensions 0 = scalar, 1 = 1-D array, 2 = matrix
|
||||
67 4 uint32 numRows 0 or 1 for scalar/1-D
|
||||
71 4 uint32 numCols number of elements along fastest axis
|
||||
75 8 float64 rangeMin
|
||||
83 8 float64 rangeMax
|
||||
91 1 uint8 timeMode see Time Modes
|
||||
92 8 float64 samplingRate Hz (0 if PacketTime)
|
||||
100 4 uint32 timeSignalIdx index of the time-reference signal;
|
||||
0xFFFFFFFF = PacketTime (no reference)
|
||||
104 32 char[32] unit null-terminated physical unit string
|
||||
── (total per signal: 136 bytes) ────────────────────────────
|
||||
```
|
||||
|
||||
### Type Codes
|
||||
|
||||
| Code | C type | Bytes/element |
|
||||
|------|--------|---------------|
|
||||
| 0 | uint8 | 1 |
|
||||
| 1 | int8 | 1 |
|
||||
| 2 | uint16 | 2 |
|
||||
| 3 | int16 | 2 |
|
||||
| 4 | uint32 | 4 |
|
||||
| 5 | int32 | 4 |
|
||||
| 6 | uint64 | 8 |
|
||||
| 7 | int64 | 8 |
|
||||
| 8 | float32 | 4 |
|
||||
| 9 | float64 | 8 |
|
||||
|
||||
### Quantization Type Codes (wire side)
|
||||
|
||||
| Code | Wire type | Description |
|
||||
|------|-----------|-------------|
|
||||
| 0 | — | No quantization; raw type as above |
|
||||
| 1 | uint8 | Linear map `[rangeMin, rangeMax]` → `[0, 255]` |
|
||||
| 2 | int8 | Linear map `[rangeMin, rangeMax]` → `[-127, 127]` |
|
||||
| 3 | uint16 | Linear map `[rangeMin, rangeMax]` → `[0, 65535]` |
|
||||
| 4 | int16 | Linear map `[rangeMin, rangeMax]` → `[-32767, 32767]` |
|
||||
|
||||
### Time Mode Codes
|
||||
|
||||
| Code | Name | Meaning |
|
||||
|------|------|---------|
|
||||
| 0 | PacketTime | HRT timestamp at `Synchronise()` — see DATA payload |
|
||||
| 1 | FullArray | `timeSignalIdx` signal has same `numElements`; element `[k]` time = `timeSignal[k]` |
|
||||
| 2 | FirstSample | `timeSignalIdx` is scalar; `t[k] = t[0] + k / samplingRate` |
|
||||
| 3 | LastSample | `timeSignalIdx` is scalar; `t[k] = t[N-1] - (N-1-k) / samplingRate` |
|
||||
|
||||
---
|
||||
|
||||
## DATA Payload
|
||||
|
||||
After reassembly, the DATA payload layout is:
|
||||
|
||||
```
|
||||
Offset Size Type Field
|
||||
────── ──── ────── ────────────────────────────────────────────────────
|
||||
0 8 uint64 hrtTimestamp hardware reference timer count at Synchronise()
|
||||
── for each signal (in config order) ────────────────────────────────────
|
||||
varies N×sz — signal data N = numRows×numCols, sz = element size
|
||||
(wire size if quantized, raw size otherwise)
|
||||
```
|
||||
|
||||
Signal data for quantized signals uses the wire element size (see Quantization Type Codes),
|
||||
not the original MARTe2 type size.
|
||||
|
||||
### Dequantization
|
||||
|
||||
To recover physical values from quantized integers:
|
||||
|
||||
```
|
||||
// uint16 → float
|
||||
span = rangeMax - rangeMin
|
||||
physical = rangeMin + (wire_uint16 / 65535.0) × span
|
||||
|
||||
// int16 → float
|
||||
physical = rangeMin + ((wire_int16 + 32767) / 65534.0) × span
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fragmentation
|
||||
|
||||
When a payload exceeds `MaxPayloadSize` bytes, it is split into fragments:
|
||||
|
||||
```
|
||||
chunkSize = MaxPayloadSize - 17 // usable bytes per datagram
|
||||
numFragments = ceil(payloadSize / chunkSize)
|
||||
```
|
||||
|
||||
Fragment `i` carries bytes `[i × chunkSize .. min((i+1) × chunkSize, payloadSize))`.
|
||||
All fragments share the same `counter`; `fragmentIdx` and `totalFragments` allow
|
||||
the client to reassemble them in any order.
|
||||
|
||||
**Example:** `MaxPayloadSize = 1400`, payload = 8016 B
|
||||
→ `chunkSize = 1383`, `numFragments = ceil(8016/1383) = 6`
|
||||
|
||||
---
|
||||
|
||||
## Minimal Python Client Example
|
||||
|
||||
```python
|
||||
import socket, struct, time
|
||||
|
||||
MAGIC = 0x53504455
|
||||
HDR_FMT = '<IBHHI' # magic, type, counter, fragIdx, totalFrags, payloadBytes
|
||||
HDR_SIZE = 17
|
||||
|
||||
def build_connect():
|
||||
return struct.pack(HDR_FMT, MAGIC, 3, 0, 0, 1, 0)
|
||||
|
||||
def parse_header(data):
|
||||
return struct.unpack_from(HDR_FMT, data)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(('', 44900))
|
||||
sock.sendto(build_connect(), ('127.0.0.1', 44500))
|
||||
sock.settimeout(5.0)
|
||||
|
||||
fragments = {}
|
||||
while True:
|
||||
data, _ = sock.recvfrom(65536)
|
||||
magic, ptype, counter, frag_idx, total_frags, payload_bytes = parse_header(data)
|
||||
payload = data[HDR_SIZE:]
|
||||
|
||||
if ptype == 1: # CONFIG
|
||||
print(f"CONFIG fragment {frag_idx+1}/{total_frags}")
|
||||
elif ptype == 0: # DATA
|
||||
fragments.setdefault(counter, {})[frag_idx] = payload
|
||||
if len(fragments[counter]) == total_frags:
|
||||
full = b''.join(fragments.pop(counter)[i] for i in range(total_frags))
|
||||
hrt = struct.unpack_from('<Q', full)[0]
|
||||
print(f"DATA counter={counter} hrt={hrt} payload={len(full)}B")
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
# SineArrayGAM
|
||||
|
||||
`SineArrayGAM` is a helper GAM bundled with the UDPStreamer library. It fills a `float32`
|
||||
output array with a continuous sinusoidal waveform, maintaining phase continuity across
|
||||
RT cycles. It is intended for testing and demonstrating the UDPStreamer's packed-burst
|
||||
signal capability.
|
||||
|
||||
> **Library:** The class is compiled into `UDPStreamer.so` alongside the UDPStreamer
|
||||
> DataSource. A `SineArrayGAM.so → UDPStreamer.so` symlink is required for MARTe2's
|
||||
> auto-loader (created automatically by `Test/MARTeApp/run.sh`).
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```
|
||||
+Ch1GAM = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0 // Signal frequency [Hz] (default: 1.0)
|
||||
Amplitude = 1.0 // Peak amplitude (default: 1.0)
|
||||
Offset = 0.0 // DC offset (default: 0.0)
|
||||
Phase = 0.0 // Initial phase [radians] (default: 0.0)
|
||||
SamplingRate = 1000000.0 // Sample rate [Hz] (default: 1000000.0)
|
||||
// Must match the SamplingRate declared in the
|
||||
// UDPStreamer signal config.
|
||||
|
||||
OutputSignals = {
|
||||
Ch1 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000 // N samples per RT cycle
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `Frequency` | float64 | 1.0 | Signal frequency in Hz |
|
||||
| `Amplitude` | float64 | 1.0 | Peak amplitude (half peak-to-peak) |
|
||||
| `Offset` | float64 | 0.0 | DC offset added to every sample |
|
||||
| `Phase` | float64 | 0.0 | Phase shift in radians |
|
||||
| `SamplingRate` | float64 | 1 000 000.0 | Sample rate in Hz; must be > 0 |
|
||||
|
||||
### Output signal
|
||||
|
||||
Exactly **one** output signal of type **`float32`** is required. The signal must have
|
||||
`NumberOfDimensions = 1` and `NumberOfElements = N` where N ≥ 1.
|
||||
|
||||
---
|
||||
|
||||
## Waveform formula
|
||||
|
||||
Each `Execute()` call fills elements `[0 .. N-1]` using:
|
||||
|
||||
```
|
||||
v[k] = Amplitude × sin(2π × Frequency × (offset_total + k) / SamplingRate + Phase) + Offset
|
||||
```
|
||||
|
||||
where `offset_total` is a cumulative sample counter that increments by `N` after each call.
|
||||
This guarantees a gapless, phase-continuous waveform across RT cycles.
|
||||
|
||||
---
|
||||
|
||||
## Bandwidth and fragmentation
|
||||
|
||||
For a 1 MSps burst at 10 kHz RT rate with 1000 samples per cycle:
|
||||
|
||||
```
|
||||
N = SamplingRate / RT_rate = 1 000 000 / 1000 = 1000 samples per cycle
|
||||
Wire bytes = 1000 × 4 (float32) = 4000 B per channel per cycle
|
||||
At 10 kHz: 4000 × 10 000 = 40 MB/s raw (UDP, before quantization)
|
||||
```
|
||||
|
||||
Using `QuantizedType = uint16` halves the wire bandwidth to 20 MB/s.
|
||||
With `MaxPayloadSize = 1400` the 4 000-byte payload spans 3 UDP datagrams
|
||||
(`ceil(4008 / 1383) = 3`).
|
||||
|
||||
---
|
||||
|
||||
## Example: two-channel quadrature pair
|
||||
|
||||
```
|
||||
+Ch1GAM = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0; Amplitude = 1.0; Phase = 0.0; SamplingRate = 10000000.0
|
||||
OutputSignals = {
|
||||
Ch1 = { DataSource = DDB; Type = float32; NumberOfDimensions = 1; NumberOfElements = 1000 }
|
||||
}
|
||||
}
|
||||
|
||||
+Ch2GAM = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0; Amplitude = 1.0; Phase = 1.5708; SamplingRate = 10000000.0
|
||||
OutputSignals = {
|
||||
Ch2 = { DataSource = DDB; Type = float32; NumberOfDimensions = 1; NumberOfElements = 1000 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This produces two signals 90° out of phase, useful for verifying IQ-style signal chains.
|
||||
@@ -0,0 +1,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 |
|
||||
@@ -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
@@ -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 (0–100%) |
|
||||
| **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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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, ×tamp, 8);
|
||||
WriteToBuffer(&tempWrite, &size, 4);
|
||||
WriteToBuffer(&tempWrite, data, size);
|
||||
|
||||
writeIndex = tempWrite;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Pop(uint32 &signalID, uint64 ×tamp, 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
|
||||
@@ -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
|
||||
@@ -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 CONNECT→DATA→DISCONNECT 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_ */
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Main_TestAll.cpp
|
||||
*
|
||||
* Created on: 31/10/2016
|
||||
* Author: Andre Neto
|
||||
*/
|
||||
|
||||
#include "ErrorManagement.h"
|
||||
#include "Object.h"
|
||||
#include "StreamString.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include <limits.h>
|
||||
|
||||
void MainGTestComponentsErrorProcessFunction(const MARTe::ErrorManagement::ErrorInformation& errorInfo,
|
||||
const char* const errorDescription)
|
||||
{
|
||||
MARTe::StreamString errorCodeStr;
|
||||
MARTe::ErrorManagement::ErrorCodeToStream(errorInfo.header.errorType, errorCodeStr);
|
||||
printf("[%s - %s:%d]: %s\n", errorCodeStr.Buffer(), errorInfo.fileName, errorInfo.header.lineNumber, errorDescription);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
|
||||
|
||||
SetErrorProcessFunction(&MainGTestComponentsErrorProcessFunction);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
include Makefile.inc
|
||||
|
||||
LIBRARIES += $(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")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
include Makefile.inc
|
||||
@@ -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)
|
||||
@@ -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
Reference in New Issue
Block a user