Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/IEC101_TRACE_MULTILINE_CONTRACT_20261029.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# IEC-101 Frame Trace — contract: exact ASE-style multilines (2026-10-29)

Screenshot/telegram-driven requirement: **one physical FT1.2 frame = one compact four-part block**. Do not regress to a one-line summary. 1. `⇧/⇩ [PC time] human-friendly event name`; 2. `Data Link:` service, all defined bit flags and link address (+ L for variable); 3. when a decoded ASDU exists, separate `ASDU:`, `Cause of transmission:`, `Comm Addr`, and one line per information object; 4. `Hex Frame:` all original bytes.

## Examples, protocol-correct
```
⇧ [03:39:37] Class 2 request
Data Link: Class 2 (Respond) [PRM:1,FCV:0] [FCB:0] Data Link Addr 105
Hex Frame: 10 4B 69 B4 16

⇩ [03:39:37] Class 2 Data Not Available, Class 1 Data Available!
Data Link: Nack (Data Not Available) [PRM:0,ACD:1,DFC:0] Data Link Addr 105
Hex Frame: 10 29 69 92 16
```

For valid Type 30 data, present e.g. `ASDU: Type ID 30 <Single-point with date/time> Count 1`, `Cause of transmission: 20 (...) | Origin: 0`, `Comm Addr 1`, `SI 8388716 1 [Valid] 10/28/26 20:39:28.460 [Valid time,Standard time]` and the whole physical frame. Multiple IOAs all belong in that one frame block; object-specific Value/Event evidence still flows independently. Open the inspector to analyze individual bit/byte groups.

**Bit-level correction:** control 0x4B (and 0x6B) has FCV bit 4 = 0, not 1. Source HEX takes precedence over a copied human label. Control 0x5A/0x7A for Class 1 has FCV=1.\n\n**Strict integrity:** the provided example variable frame `68 14 14 68 ... 85 16` is 25 bytes long although L=20 requires 26. Its checksum does not match supplied bytes and `64 FC` in its CP56 milliseconds field exceeds 59,999 ms/minute. It MUST NOT be silently accepted as `[Valid]` just to mimic an example. Synthetic regression instead creates a valid Type 30 frame, correct checksum, CP56Time2a, QDS/SIQ, COT, origin and CA. Time-zone origin/genuine-status not encoded in CP56 alone; show validity and summer/standard flags only, never invent authenticity.

## Architecture and performance
- Pure `Iec101TraceNarrator` receives **already-decoded typed evidence**, formats exactly once per physical frame; no new regex parser, no serial I/O or history mutation.
- `EvidenceRow` renders label, newline-separated details and full raw HEX in 3 compact WPF text rows; TX blue/RX green lightweight data triggers. No large cards.
- Frame Trace suppresses redundant **secondary per-object evidence rows** only. They remain in Values and Event Log. Primary physical event includes the entire `AsduDetails.Objects` list.
- Initial WPF blank tab fix and source-separated M1/M2 SSOT remain unchanged. Existing bounded ring 1200 rows, 75ms UI batched flush, and passive zero-TX protection retained.

## FAT gates
Inspect real 1200-bps 8E1 SICAM link: raw FC10/FC11, FC9 with ACD/DFC, FC8 Type 30 multi-object, GI ACTCON/ACTTERM and link E5. Confirm each exact original checksum, length, time flags and link address. Test Connect without switching tabs, inactive tab retention, selection/Auto, performance under long sessions and multi-COM passive. CI passing is not a substitute for real FAT. Also verify IEC-103/104 and imported old .ariec separate from IEC-101 on-wire formatting; do not blindly interpret their raw as IEC-101 FT1.2.
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,14 @@ private TraceVerbosityMode GetTraceVerbosityMode()

private bool ShouldShowInFrameTrace(Iec103MasterEvidenceEvent item, EvidenceRow row)
{
// Multiple IOAs from one received ASDU are individually routed to Values
// and Event Log, but their shared physical telegram is displayed ONCE
// in Trace. The single block enumerates every decoded ASDU object.
if (item.ProtocolMode == Iec60870ProtocolMode.Iec101 &&
item.AsduDetails is not null &&
item.InformationObjectDetails is { Index: > 1 })
return false;
Comment on lines +760 to +763

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the required output policy for the trace changes

This commit changes Frame Trace output and retention behavior by introducing multiline blocks and suppressing secondary per-object trace rows, but only adds a standalone contract document. AGENTS.md specifically requires updating docs/OUTPUT_AND_PERFORMANCE_POLICY.md when output, retention, or rendering behavior changes; that policy is untouched. Update the canonical policy to describe physical-frame deduplication and the new presentation contract so its documented behavior remains consistent with the implementation.

AGENTS.md reference: AGENTS.md:L194-L194

Useful? React with 👍 / 👎.


// Trace is the byte-for-byte communication line monitor, not a proof
// summary. A valid TX/RX raw frame MUST be inspectable even if the user
// is currently in Values, Events or Report. The bounded ring limits RAM.
Expand Down
45 changes: 27 additions & 18 deletions src/ARIEC60870.Desktop/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -519,11 +519,25 @@
<TextBlock Text="{Binding ProtocolTraceTitle, Mode=OneWay}"
FontFamily="Sometype Mono, Cascadia Mono, Consolas"
FontSize="11.6"
FontWeight="SemiBold"
FontWeight="Medium"
LineHeight="15"
TextWrapping="Wrap"
TextTrimming="None"
TextOptions.TextFormattingMode="Display" />
TextOptions.TextFormattingMode="Display">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{StaticResource Ink900Brush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Direction}" Value="TX">
<Setter Property="Foreground" Value="#2563EB" />
</DataTrigger>
<DataTrigger Binding="{Binding Direction}" Value="RX">
<Setter Property="Foreground" Value="#059669" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Grid.Column="1"
Text="{Binding ProtocolTraceMeta, Mode=OneWay}"
FontFamily="Sometype Mono, Cascadia Mono, Consolas"
Expand All @@ -547,22 +561,17 @@
TextOptions.TextFormattingMode="Display"
Margin="0,3,0,0" />

<Border Grid.Row="2"
Background="#F8FAFC"
BorderBrush="#E2E8F0"
BorderThickness="1"
CornerRadius="7"
Padding="7,5"
Margin="0,5,0,0">
<TextBlock Text="{Binding ProtocolTraceRaw, Mode=OneWay}"
FontFamily="Sometype Mono, Cascadia Mono, Consolas"
FontSize="10.55"
LineHeight="14.2"
Foreground="{StaticResource Ink600Brush}"
TextWrapping="Wrap"
TextTrimming="None"
TextOptions.TextFormattingMode="Display" />
</Border>
<!-- One compact HEX line per physical frame, not another card. -->
<TextBlock Grid.Row="2"
Text="{Binding ProtocolTraceRaw, Mode=OneWay}"
FontFamily="Sometype Mono, Cascadia Mono, Consolas"
FontSize="10.55"
LineHeight="14.2"
Foreground="{StaticResource Ink600Brush}"
TextWrapping="Wrap"
TextTrimming="None"
TextOptions.TextFormattingMode="Display"
Margin="0,5,0,2" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
Expand Down
23 changes: 22 additions & 1 deletion src/ARIEC60870.Desktop/ViewModels/EvidenceRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public EvidenceRow(Iec103MasterEvidenceEvent item, Iec10xPointMappingEntry? ioaP
ProtocolTraceTitle = narration?.Title ?? BuildProtocolTraceTitle();
ProtocolTraceMeaning = narration?.Meaning ?? BuildProtocolTraceMeaning();
ProtocolTraceRaw = BuildProtocolTraceRaw();
ProtocolTraceMeta = $"#{Sequence} {Time} {ProtocolName}";
ProtocolTraceMeta = narration is null ? $"#{Sequence} {Time} {ProtocolName}" : string.Empty;
TrafficTone = ResolveTrafficTone();
}

Expand Down Expand Up @@ -280,6 +280,27 @@ private string BuildProtocolTraceMeaning()
private string BuildProtocolTraceRaw()
{
var raw = CleanTracePart(RawHex);
if (ProtocolMode == "101")
{
// Raw bytes always come from captured frames. Passive replay keeps a
// compact hexadecimal string; normalize it once for readable byte groups.
if (raw != "-" && !raw.Contains(' ') && Source.Frame?.RawBytes is { Count: > 0 } bytes)
{
raw = string.Create(bytes.Count * 3 - 1, bytes, static (chars, source) =>
{
const string digits = "0123456789ABCDEF";
for (var i = 0; i < source.Count; i++)
{
var value = source[i];
chars[i * 3] = digits[value >> 4];
chars[i * 3 + 1] = digits[value & 15];
if (i + 1 < source.Count) chars[i * 3 + 2] = ' ';
}
});
}
return "Hex Frame: " + raw;
}

var prefix = CleanTracePart(LinkFlags);
var rawLine = raw == "-" ? "RAW -" : "RAW " + raw;
return prefix == "-" ? rawLine : $"LINK {prefix} · {rawLine}";
Expand Down
Loading
Loading