OSDP.Net 5.2.1
A .NET library for implementing the Open Supervised Device Protocol (OSDP)
Loading...
Searching...
No Matches
LineQualityMarkdownReport.cs
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Text;
5
7{
18 {
20 public string TesterName { get; set; }
21
23 public string InstallationLocation { get; set; }
24
26 public string CableDescription { get; set; }
27
29 public string AcuDescription { get; set; }
30
32 public string PdDescription { get; set; }
33
35 public string AdapterDescription { get; set; }
36
44 public bool? AdapterLatencyTimerAdjusted { get; set; }
45
47 public string Notes { get; set; }
48 }
49
54 public static class LineQualityMarkdownReport
55 {
63 public static string Render(LineQualityReport report, LineQualityReportMetadata metadata = null)
64 {
65 if (report == null) throw new ArgumentNullException(nameof(report));
66
67 var builder = new StringBuilder();
68 metadata = metadata ?? new LineQualityReportMetadata();
69
70 WriteHeader(builder, report, metadata);
71 WriteSummary(builder, report);
72 WriteFailureBreakdown(builder, report);
73 WriteDetailedResults(builder, report);
74 WriteMeasurementNotes(builder, report);
75 WriteRecommendations(builder, report, metadata);
76
77 return builder.ToString();
78 }
79
80 private static void WriteHeader(StringBuilder builder, LineQualityReport report,
82 {
83 builder.AppendLine("# OSDP Line Quality Test Report");
84 builder.AppendLine();
85 builder.AppendLine("## Test Information");
86 builder.AppendLine();
87 builder.AppendLine("| Field | Value |");
88 builder.AppendLine("|---|---|");
89
90 AppendRow(builder, "Test Date",
91 report.StartedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture));
92 AppendRow(builder, "Tester Name", metadata.TesterName);
93 AppendRow(builder, "Installation Location", metadata.InstallationLocation);
94 AppendRow(builder, "ACU Model/Firmware", metadata.AcuDescription);
95 AppendRow(builder, "PD Model/Firmware", metadata.PdDescription);
96 AppendRow(builder, "Cable Type/Length", metadata.CableDescription);
97 AppendRow(builder, "Connection", report.Connection);
98 AppendRow(builder, "Responder Address",
99 $"{report.Address} (0x{report.Address:X2})");
100 AppendRow(builder, "Test Profile",
101 $"{report.Profile} ({report.IterationsPerCombination} iterations per combination)");
102 AppendRow(builder, "Duration", FormatDuration(report.Duration));
103 AppendRow(builder, "Controller Platform / Adapter", metadata.AdapterDescription);
104 AppendRow(builder, "Adapter Latency Timer Adjusted",
105 metadata.AdapterLatencyTimerAdjusted.HasValue
106 ? metadata.AdapterLatencyTimerAdjusted.Value ? "Yes" : "No"
107 : null);
108
109 builder.AppendLine();
110 }
111
112 private static void WriteSummary(StringBuilder builder, LineQualityReport report)
113 {
114 builder.AppendLine("## Summary");
115 builder.AppendLine();
116 builder.AppendLine($"**Overall result: {Describe(report.OverallVerdict)}**");
117 builder.AppendLine();
118 builder.AppendLine(report.RecommendedBaudRate.HasValue
119 ? $"**Recommended operating baud rate: {report.RecommendedBaudRate.Value}**"
120 : "**Recommended operating baud rate: none — no rate passed cleanly.**");
121 builder.AppendLine();
122
123 builder.AppendLine(
124 "| Baud Rate | Sent | Failures | Success Rate | Detection Limit | Max Response (ms) | Result |");
125 builder.AppendLine("|---:|---:|---:|---:|---:|---:|:---|");
126
127 foreach (var result in report.BaudRates)
128 {
129 if (!result.WasTested)
130 {
131 builder.AppendLine(
132 $"| {result.BaudRate} | — | — | — | — | — | {Describe(result.Verdict)} |");
133 continue;
134 }
135
136 builder.AppendLine(
137 $"| {result.BaudRate} | {result.PacketsSent} | {result.Failures} | " +
138 $"{Percent(result.SuccessRatePercent)} | {Percent(result.DetectionLimitPercent)} | " +
139 $"{result.MaxResponseTimeMs:F1} | {Describe(result.Verdict)} |");
140 }
141
142 builder.AppendLine();
143
144 // The single most important caveat in the whole document: a pass bounds the loss rate,
145 // it does not prove the absence of loss.
146 builder.AppendLine(
147 "> A **PASS** establishes only that the packet loss rate is below the detection " +
148 "limit shown for that rate. It does not establish that loss is zero. The detection " +
149 "limit is set by how many packets the profile sent (see Measurement Notes).");
150 builder.AppendLine();
151
152 WriteSkipped(builder, report);
153 }
154
155 private static void WriteSkipped(StringBuilder builder, LineQualityReport report)
156 {
157 var failed = new List<BaudRateResult>();
158 var skipped = new List<BaudRateResult>();
159
160 foreach (var result in report.BaudRates)
161 {
162 if (result.FailureReason != null) failed.Add(result);
163 else if (result.SkipReason != null) skipped.Add(result);
164 }
165
166 if (failed.Count > 0)
167 {
168 builder.AppendLine("### Rate Changes That Did Not Complete");
169 builder.AppendLine();
170 foreach (var result in failed)
171 {
172 builder.AppendLine($"- **{result.BaudRate}** — {result.FailureReason}");
173 }
174
175 builder.AppendLine();
176 builder.AppendLine(
177 "> These count as failures of the rate rather than untested rates: the line " +
178 "could not carry the transition, or could not sustain the rate once there.");
179 builder.AppendLine();
180 }
181
182 if (skipped.Count == 0) return;
183
184 builder.AppendLine("### Rates Not Tested");
185 builder.AppendLine();
186 foreach (var result in skipped)
187 {
188 builder.AppendLine($"- **{result.BaudRate}** — {result.SkipReason}");
189 }
190
191 builder.AppendLine();
192 }
193
194 private static void WriteFailureBreakdown(StringBuilder builder, LineQualityReport report)
195 {
196 var withFailures = new List<BaudRateResult>();
197 foreach (var result in report.BaudRates)
198 {
199 if (result.WasTested && result.Failures > 0) withFailures.Add(result);
200 }
201
202 if (withFailures.Count == 0) return;
203
204 builder.AppendLine("## Failure Breakdown");
205 builder.AppendLine();
206 builder.AppendLine(
207 "| Baud Rate | Timeouts | Integrity Errors | NAKs | Pattern Mismatches |");
208 builder.AppendLine("|---:|---:|---:|---:|---:|");
209
210 foreach (var result in withFailures)
211 {
212 builder.AppendLine(
213 $"| {result.BaudRate} | {result.Timeouts} | {result.IntegrityErrors} | " +
214 $"{result.Naks} | {result.PatternMismatches} |");
215 }
216
217 builder.AppendLine();
218 }
219
220 private static void WriteDetailedResults(StringBuilder builder, LineQualityReport report)
221 {
222 builder.AppendLine("## Detailed Results");
223 builder.AppendLine();
224
225 foreach (var result in report.BaudRates)
226 {
227 if (!result.WasTested) continue;
228
229 builder.AppendLine($"### {result.BaudRate} baud — {Describe(result.Verdict)}");
230 builder.AppendLine();
231 builder.AppendLine(
232 "| Pattern | Size | Sent | Received | Timeouts | Integrity | NAKs | " +
233 "Mismatches | Success % | Avg ms | Max ms |");
234 builder.AppendLine("|:---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|");
235
236 foreach (var combination in result.Combinations)
237 {
238 builder.AppendLine(
239 $"| {combination.Pattern} | {combination.PayloadLength} | " +
240 $"{combination.PacketsSent} | {combination.PacketsReceived} | " +
241 $"{combination.Timeouts} | {combination.IntegrityErrors} | " +
242 $"{combination.Naks} | {combination.PatternMismatches} | " +
243 $"{Percent(combination.SuccessRatePercent)} | " +
244 $"{combination.AverageResponseTimeMs:F1} | {combination.MaxResponseTimeMs:F1} |");
245 }
246
247 builder.AppendLine();
248 }
249 }
250
251 private static void WriteMeasurementNotes(StringBuilder builder, LineQualityReport report)
252 {
253 builder.AppendLine("## Measurement Notes");
254 builder.AppendLine();
255
256 builder.AppendLine(
257 $"- **Profile:** {report.Profile}, {report.IterationsPerCombination} iterations of each " +
258 "of the 16 pattern and size combinations per baud rate.");
259 builder.AppendLine(
260 "- **Detection limit:** with no observed failures, the 95% upper bound on the loss " +
261 "rate is approximately 3/N for N packets. Only the Extended profile (3200 packets " +
262 "per rate) can substantiate a 99.9% success claim.");
263 builder.AppendLine(
264 "- **Response time** is measured from the end of the command to the first byte of " +
265 "the reply, matching REPLY_DELAY in OSDP section 5.7.");
266 builder.AppendLine(
267 "- **Timing resolution on a PC is limited by the serial adapter**, not the cable. " +
268 "FTDI parts default to a 16 ms latency timer, which at high baud rates exceeds the " +
269 "entire exchange. Treat the 200 ms reply window as a pass/fail gate; treat average " +
270 "and maximum times as indicative only unless the latency timer was lowered.");
271 builder.AppendLine();
272
273 builder.AppendLine("Failure categories point at different physical causes:");
274 builder.AppendLine();
275 builder.AppendLine("| Category | Usual cause |");
276 builder.AppendLine("|:---|:---|");
277 builder.AppendLine("| Timeouts | Open or unpowered line, wrong address, termination fault |");
278 builder.AppendLine("| Integrity errors | Marginal signal integrity, EMI, reflections |");
279 builder.AppendLine("| NAKs | Responder rejected the command; check the error code in the log |");
280 builder.AppendLine("| Pattern mismatches | Bit error that survived the CRC, or a responder defect |");
281 builder.AppendLine();
282 }
283
284 private static void WriteRecommendations(StringBuilder builder, LineQualityReport report,
285 LineQualityReportMetadata metadata)
286 {
287 builder.AppendLine("## Recommendations");
288 builder.AppendLine();
289
290 if (report.RecommendedBaudRate.HasValue)
291 {
292 builder.AppendLine(
293 $"- **Recommended operating baud rate:** {report.RecommendedBaudRate.Value}");
294
295 if (report.Profile != TestProfile.Extended)
296 {
297 builder.AppendLine(
298 "- Re-run with `--profile extended` at the selected rate before recording " +
299 "this as a commissioning result.");
300 }
301 }
302 else
303 {
304 builder.AppendLine(
305 "- **Recommended operating baud rate:** none. No rate passed cleanly; " +
306 "investigate the failures above before selecting a rate.");
307 }
308
309 builder.AppendLine("- **Installation quality assessment:**");
310 builder.AppendLine("- **Remediation actions (if any):**");
311 builder.AppendLine();
312
313 if (string.IsNullOrWhiteSpace(metadata.Notes)) return;
314
315 builder.AppendLine("### Notes");
316 builder.AppendLine();
317 builder.AppendLine(metadata.Notes);
318 builder.AppendLine();
319 }
320
321 private static void AppendRow(StringBuilder builder, string field, string value) =>
322 builder.AppendLine($"| {field} | {(string.IsNullOrWhiteSpace(value) ? string.Empty : value)} |");
323
324 private static string Percent(double value) =>
325 value.ToString("F2", CultureInfo.InvariantCulture) + "%";
326
327 private static string Describe(LineQualityVerdict verdict)
328 {
329 switch (verdict)
330 {
331 case LineQualityVerdict.Pass: return "PASS";
332 case LineQualityVerdict.Marginal: return "MARGINAL";
333 case LineQualityVerdict.Fail: return "FAIL";
334 default: return "UNTESTED";
335 }
336 }
337
338 private static string FormatDuration(TimeSpan duration) =>
339 duration.TotalMinutes >= 1
340 ? $"{(int)duration.TotalMinutes}m {duration.Seconds}s"
341 : duration.TotalSeconds.ToString("F1", CultureInfo.InvariantCulture) + "s";
342 }
343}
Renders a LineQualityReport as the Markdown report described in section 7 of the OSDP Line Quality Te...
static string Render(LineQualityReport report, LineQualityReportMetadata metadata=null)
Renders the report.
Details about an installation that the test cannot discover for itself, used to fill in the header of...
string AdapterDescription
Gets or sets the host platform and serial adapter used.
string AcuDescription
Gets or sets the model and firmware of the controller side.
string InstallationLocation
Gets or sets where the installation is.
bool? AdapterLatencyTimerAdjusted
Gets or sets whether the serial adapter's latency timer was lowered before the run.
string CableDescription
Gets or sets the cable type and length.
string PdDescription
Gets or sets the model and firmware of the responder side.
string Notes
Gets or sets free-form notes to include in the report.
The complete results of a line quality test run.
string Connection
Gets a description of the connection tested.
TimeSpan Duration
Gets how long the run took.
DateTime StartedUtc
Gets when the run started.
TestProfile
Test profiles from section 3.10 of the Line Quality Test Procedure.
LineQualityVerdict
The overall assessment of a baud rate or of a whole test run.