Skip to content

Commit e9d1789

Browse files
committed
Refined schema editing and telemetry ingestion and implemented global error handling.
1 parent 9b09a8b commit e9d1789

45 files changed

Lines changed: 2608 additions & 2214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>0.2.3.4</Version>
3+
<Version>0.2.3.5</Version>
44
<Authors>TekLot</Authors>
55
<Product>SignalBench</Product>
66
</PropertyGroup>

README.md

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**A professional-grade telemetry decoding and analysis workbench for satellite, aerospace, automotive, and industrial test engineers.**
44

5-
SignalBench is a high-performance, engineer-grade telemetry workbench designed for mission-critical test campaigns. It supports everything from CubeSat missions to complex flight test systems, providing robust decoding for CSV, binary logs, and live network or serial streams. Decode, visualize, and analyze telemetry without the need for custom scripting.
5+
SignalBench is a high-performance, engineer-grade telemetry workbench designed for mission-critical test campaigns. It supports everything from CubeSat missions to complex flight test systems, providing robust decoding for delimited text files (e.g., CSV, TSV), binary logs, and live network or serial streams. Decode, visualize, and analyze telemetry without the need for custom scripting.
66

77
## 🏗️ Project Structure
88

@@ -24,8 +24,8 @@ The project has recently undergone a major architectural overhaul to support a "
2424
## 🚀 Features
2525

2626
- **Workspace-Centric UI**: Top-level tabbed architecture. Each tab is a complete workspace with its own independent data source (File, Serial, or Network), signal selection sidebar, and plot configuration.
27-
- **Intelligent Data Import**: Specialized, format-aware dialogs for CSV and Binary data. Includes live data previews, delimiter/header configuration, and validation to prevent malformed imports.
28-
- **Binary Telemetry Mapping**: Full control over binary decoding via YAML-defined packet schemas. Supports custom timestamp field selection from any numeric field in the schema.
27+
- **Intelligent Data Import**: Specialized, format-aware dialogs for Delimited Text (CSV, TSV, etc.) and Binary data. Includes live data previews, custom delimiter/header configuration, and validation.
28+
- **DSDL-Lite Binary Decoding**: Full control over binary decoding via YAML-defined packet schemas with support for scaling, units, and nested fields.
2929
- **Live Network Streaming**: Connect via **TCP (Client)** or **UDP (Listener)** to decode and visualize data in real-time.
3030
- **Live Serial Streaming**: Connect to COM ports with full control over Baud Rate, Parity, and Stop Bits.
3131
- **High Performance**: Handles large files (> 500K+ records) and high-frequency streams efficiently using a Hybrid (In-Memory/SQLite) data store.
@@ -34,6 +34,50 @@ The project has recently undergone a major architectural overhaul to support a "
3434
- **Data Logging**: Record raw network or serial streams directly to disk while visualizing.
3535
- **Session Management**: Save and restore workspace sessions (`.sbs` files). Supports multi-tab persistence, embedded schemas, and **automatic restoration** of the last session on startup.
3636

37+
## 📄 DSDL-Lite Packet Schema
38+
39+
SignalBench uses a professional-grade YAML schema format inspired by industry standards like DSDL (Data Structure Definition Language). This allows you to define complex binary packets with support for nested groups, linear transformations, and categorical mappings.
40+
41+
### Key Capabilities
42+
- **Recursive Namespacing**: Group related signals into hierarchies (e.g., `Battery/Cell1/Voltage`).
43+
- **Linear Transformation**: Automatically convert raw integers to physical values using `scale` and `offset` (`PhysicalValue = (Raw * Scale) + Offset`).
44+
- **Categorical Lookup**: Map numeric status codes to human-readable strings (Enums).
45+
- **Physical Metadata**: Assign units (V, A, Hz, °C) that are displayed throughout the UI and statistics panels.
46+
47+
### Comprehensive Schema Example
48+
```yaml
49+
packet:
50+
name: "FlightController"
51+
endianness: little
52+
fields:
53+
- name: "system_status"
54+
type: uint8
55+
lookup:
56+
0: "BOOTING"
57+
1: "READY"
58+
2: "FAULT"
59+
60+
- name: "battery"
61+
fields: # Nested Group
62+
- name: "voltage"
63+
type: uint16
64+
scale: 0.001 # Convert mV to V
65+
unit: "V"
66+
- name: "current"
67+
type: int16
68+
scale: 0.1
69+
unit: "A"
70+
71+
- name: "environment"
72+
fields:
73+
- name: "temperature"
74+
type: int16
75+
scale: 0.01
76+
offset: -40.0
77+
unit: "°C"
78+
description: "Internal ambient temperature"
79+
```
80+
3781
## 🚀 Getting Started (Quick Start)
3882
3983
### 1. Load a Packet Schema
@@ -42,8 +86,8 @@ Before loading binary data or streaming, the app needs to know the structure of
4286
* Define or select a `.yaml` schema file.
4387

4488
### 2. Import Static Telemetry Data
45-
* **CSV Import**: Click the **CSV File icon** in the **DATA FILE** group.
46-
* Select your file, configure the **Delimiter** and **Has Header** settings.
89+
* **Delimited Text File Import**: Click the **Text File icon** in the **DATA FILE** group.
90+
* Select your file, configure the **Delimiter** (Comma, Semicolon, Pipe, Tab, etc.) and **Has Header** settings.
4791
* The preview grid will update automatically to validate your settings.
4892
* **Binary Import**: Click the **Binary File icon** (identified by the '101' badge).
4993
* Select your file and a corresponding schema.
@@ -70,11 +114,9 @@ Streaming settings are **per-workspace (per-tab)**. You can stream from multiple
70114

71115
- **Platform**: Windows, Linux, macOS (Cross-platform via Avalonia)
72116
- **Runtime**: .NET 9.0 SDK
73-
- **Hardware**: Serial port access or Network (Ethernet/WiFi) connectivity
74117
- **Dependencies**:
75118
- Avalonia UI
76119
- ScottPlot (v5.1+)
77-
- System.IO.Ports
78120
- YamlDotNet
79121
- Microsoft.Data.Sqlite
80122
- NCalcSync

SignalBench.Core/Decoding/BinaryDecoder.cs

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,29 @@ public sealed class BinaryDecoder
99
public DecodedPacket Decode(ReadOnlySpan<byte> data, PacketSchema schema)
1010
{
1111
var fields = new Dictionary<string, object>();
12+
DecodeFields(data, schema.Fields, schema.Endianness, fields, "");
1213

13-
foreach (var field in schema.Fields)
14+
return new DecodedPacket
15+
{
16+
SchemaName = schema.Name,
17+
Timestamp = DateTime.Now,
18+
Fields = fields
19+
};
20+
}
21+
22+
private void DecodeFields(ReadOnlySpan<byte> data, IEnumerable<FieldDefinition> fieldDefs, Endianness endian, Dictionary<string, object> results, string prefix)
23+
{
24+
foreach (var field in fieldDefs)
1425
{
26+
string fullName = string.IsNullOrEmpty(prefix) ? field.Name : $"{prefix}/{field.Name}";
27+
28+
if (field.Fields != null && field.Fields.Count > 0)
29+
{
30+
// Recursive call for nested fields
31+
DecodeFields(data, field.Fields, endian, results, fullName);
32+
continue;
33+
}
34+
1535
int byteOffset = field.BitOffset / 8;
1636
int bitOffsetInByte = field.BitOffset % 8;
1737

@@ -20,43 +40,38 @@ public DecodedPacket Decode(ReadOnlySpan<byte> data, PacketSchema schema)
2040
int neededBytes = GetTypeSize(field.Type, field.BitLength);
2141
if (byteOffset + neededBytes > data.Length) continue;
2242

23-
object value = field.Type switch
43+
double rawValue = field.Type switch
2444
{
25-
FieldType.Uint8 => (object)ExtractBits(data[byteOffset], bitOffsetInByte, field.BitLength),
26-
FieldType.Uint16 => (object)ReadUInt16(data, byteOffset, bitOffsetInByte, field.BitLength, schema.Endianness),
27-
FieldType.Uint32 => (object)ReadUInt32(data, byteOffset, bitOffsetInByte, field.BitLength, schema.Endianness),
28-
FieldType.Uint64 => (object)(schema.Endianness == Endianness.Little
45+
FieldType.Uint8 => ExtractBits(data[byteOffset], bitOffsetInByte, field.BitLength),
46+
FieldType.Uint16 => ReadUInt16(data, byteOffset, bitOffsetInByte, field.BitLength, endian),
47+
FieldType.Uint32 => ReadUInt32(data, byteOffset, bitOffsetInByte, field.BitLength, endian),
48+
FieldType.Uint64 => (double)(endian == Endianness.Little
2949
? BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(byteOffset, 8))
3050
: BinaryPrimitives.ReadUInt64BigEndian(data.Slice(byteOffset, 8))),
31-
FieldType.Int8 => (object)unchecked((sbyte)data[byteOffset]),
32-
FieldType.Int16 => (object)(schema.Endianness == Endianness.Little
51+
FieldType.Int8 => unchecked((sbyte)data[byteOffset]),
52+
FieldType.Int16 => (endian == Endianness.Little
3353
? BinaryPrimitives.ReadInt16LittleEndian(data.Slice(byteOffset, 2))
3454
: BinaryPrimitives.ReadInt16BigEndian(data.Slice(byteOffset, 2))),
35-
FieldType.Int32 => (object)(schema.Endianness == Endianness.Little
55+
FieldType.Int32 => (endian == Endianness.Little
3656
? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(byteOffset, 4))
3757
: BinaryPrimitives.ReadInt32BigEndian(data.Slice(byteOffset, 4))),
38-
FieldType.Int64 => (object)(schema.Endianness == Endianness.Little
58+
FieldType.Int64 => (double)(endian == Endianness.Little
3959
? BinaryPrimitives.ReadInt64LittleEndian(data.Slice(byteOffset, 8))
4060
: BinaryPrimitives.ReadInt64BigEndian(data.Slice(byteOffset, 8))),
41-
FieldType.Float32 => (object)(schema.Endianness == Endianness.Little
61+
FieldType.Float32 => (endian == Endianness.Little
4262
? BinaryPrimitives.ReadSingleLittleEndian(data.Slice(byteOffset, 4))
4363
: BinaryPrimitives.ReadSingleBigEndian(data.Slice(byteOffset, 4))),
44-
FieldType.Float64 => (object)(schema.Endianness == Endianness.Little
64+
FieldType.Float64 => (endian == Endianness.Little
4565
? BinaryPrimitives.ReadDoubleLittleEndian(data.Slice(byteOffset, 8))
4666
: BinaryPrimitives.ReadDoubleBigEndian(data.Slice(byteOffset, 8))),
47-
FieldType.Bool => (object)(data[byteOffset] != 0),
48-
_ => (object)0
67+
FieldType.Bool => (data[byteOffset] != 0 ? 1.0 : 0.0),
68+
_ => 0.0
4969
};
5070

51-
fields[field.Name] = value;
71+
// Apply Scale and Offset
72+
double finalValue = (rawValue * field.Scale) + field.Offset;
73+
results[fullName] = finalValue;
5274
}
53-
54-
return new DecodedPacket
55-
{
56-
SchemaName = schema.Name,
57-
Timestamp = DateTime.Now,
58-
Fields = fields
59-
};
6075
}
6176

6277
private uint ExtractBits(byte b, int offset, int length)

SignalBench.Core/Models/Schema/FieldType.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,3 @@ public enum FieldType
1414
Float64,
1515
Bool
1616
}
17-
18-
public enum SchemaType
19-
{
20-
Binary,
21-
Streaming,
22-
Csv
23-
}

SignalBench.Core/Models/Schema/PacketSchema.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ public class FieldDefinition
66
public FieldType Type { get; set; }
77
public int BitOffset { get; set; }
88
public int BitLength { get; set; }
9+
10+
// Metadata & Transformation
11+
public double Scale { get; set; } = 1.0;
12+
public double Offset { get; set; } = 0.0;
13+
public string? Unit { get; set; }
14+
public string? Description { get; set; }
15+
16+
// Categorical Mapping
17+
public Dictionary<double, string>? Lookup { get; set; }
18+
19+
// Nested Fields support
20+
public List<FieldDefinition>? Fields { get; set; }
921
}
1022

1123
public enum Endianness
@@ -17,7 +29,6 @@ public enum Endianness
1729
public class PacketSchema
1830
{
1931
public string Name { get; set; } = string.Empty;
20-
public SchemaType Type { get; set; } = SchemaType.Binary;
2132
public uint? SyncWord { get; set; }
2233
public Endianness Endianness { get; set; } = Endianness.Little;
2334
public List<FieldDefinition> Fields { get; set; } = [];

SignalBench.SDK/Interfaces/ITabFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace SignalBench.SDK.Interfaces;
22

3-
public interface ITabFactory
3+
public interface ITabFactory : IPlugin
44
{
55
/// <summary>
66
/// Unique ID for the tab type (e.g. "SignalBench.Plot")

SignalBench/App.axaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,13 @@
6363
<Style Selector="Button.toolbar:disabled /template/ ContentPresenter">
6464
<Setter Property="Background" Value="Transparent"/>
6565
</Style>
66+
67+
<!-- Global Tab Header Styling -->
68+
<Style Selector="TabStripItem, TabItem">
69+
<Setter Property="FontSize" Value="10"/>
70+
<Setter Property="MinHeight" Value="24"/>
71+
<Setter Property="Padding" Value="8,0"/>
72+
<Setter Property="VerticalContentAlignment" Value="Center"/>
73+
</Style>
6674
</Application.Styles>
6775
</Application>

SignalBench/App.axaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ private void ConfigureServices(IServiceCollection services)
8686
return new HybridDataStore(mode);
8787
});
8888
services.AddSingleton<MainWindowViewModel>();
89-
services.AddTransient<SettingsViewModel>();
89+
services.AddTransient<SettingsDialogViewModel>();
9090
}
9191

9292
private void DisableAvaloniaDataAnnotationValidation()

SignalBench/Converters/GeneralConverters.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,19 @@ public class SignalItemToNameConverter : IValueConverter
6060
return value?.ToString();
6161
}
6262
}
63+
64+
public class ValueWithUnitConverter : IMultiValueConverter
65+
{
66+
public static readonly ValueWithUnitConverter Instance = new();
67+
public object? Convert(System.Collections.Generic.IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
68+
{
69+
if (values.Count < 1 || values[0] == null || values[0] is not double d) return "n/a";
70+
71+
string val = d.ToString("G5");
72+
if (values.Count > 1 && values[1] is string unit && !string.IsNullOrEmpty(unit))
73+
{
74+
return $"{val} {unit}";
75+
}
76+
return val;
77+
}
78+
}

SignalBench/Program.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Avalonia;
2+
using MsBox.Avalonia;
3+
using MsBox.Avalonia.Enums;
24

35
namespace SignalBench;
46

@@ -10,8 +12,49 @@ sealed class Program
1012
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
1113
// yet and stuff might break.
1214
[STAThread]
13-
public static void Main(string[] args) => BuildAvaloniaApp()
14-
.StartWithClassicDesktopLifetime(args);
15+
public static void Main(string[] args)
16+
{
17+
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
18+
{
19+
var ex = e.ExceptionObject as Exception;
20+
ShowError("Critical Unhandled Error", ex?.Message ?? "Unknown Error", ex?.ToString());
21+
};
22+
23+
TaskScheduler.UnobservedTaskException += (s, e) =>
24+
{
25+
ShowError("Unobserved Task Error", e.Exception.Message, e.Exception.ToString());
26+
e.SetObserved();
27+
};
28+
29+
try
30+
{
31+
BuildAvaloniaApp()
32+
.StartWithClassicDesktopLifetime(args);
33+
}
34+
catch (Exception ex)
35+
{
36+
ShowError("Application Crash", ex.Message, ex.ToString());
37+
}
38+
}
39+
40+
private static void ShowError(string title, string message, string? detail = null)
41+
{
42+
// Log the detail for debugging/diagnostics, but do not show to end user
43+
System.Diagnostics.Debug.WriteLine($"[CRITICAL ERROR] {title}: {message}\n{detail}");
44+
45+
var box = MessageBoxManager.GetMessageBoxStandard(title, message);
46+
47+
// If we are already on UI thread, we can show it, otherwise we might need to use Post
48+
if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess())
49+
{
50+
box.ShowAsync();
51+
}
52+
else
53+
{
54+
// Try to show it on UI thread if possible
55+
Avalonia.Threading.Dispatcher.UIThread.Post(() => box.ShowAsync());
56+
}
57+
}
1558

1659
// Avalonia configuration, don't remove; also used by visual designer.
1760
public static AppBuilder BuildAvaloniaApp()

0 commit comments

Comments
 (0)