65 if (report ==
null)
throw new ArgumentNullException(nameof(report));
67 var builder =
new StringBuilder();
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);
77 return builder.ToString();
83 builder.AppendLine(
"# OSDP Line Quality Test Report");
85 builder.AppendLine(
"## Test Information");
87 builder.AppendLine(
"| Field | Value |");
88 builder.AppendLine(
"|---|---|");
90 AppendRow(builder,
"Test Date",
91 report.
StartedUtc.ToLocalTime().ToString(
"yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture));
92 AppendRow(builder,
"Tester Name", metadata.
TesterName);
95 AppendRow(builder,
"PD Model/Firmware", metadata.
PdDescription);
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));
104 AppendRow(builder,
"Adapter Latency Timer Adjusted",
109 builder.AppendLine();
112 private static void WriteSummary(StringBuilder builder, LineQualityReport report)
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();
124 "| Baud Rate | Sent | Failures | Success Rate | Detection Limit | Max Response (ms) | Result |");
125 builder.AppendLine(
"|---:|---:|---:|---:|---:|---:|:---|");
127 foreach (var result
in report.BaudRates)
129 if (!result.WasTested)
132 $
"| {result.BaudRate} | — | — | — | — | — | {Describe(result.Verdict)} |");
137 $
"| {result.BaudRate} | {result.PacketsSent} | {result.Failures} | " +
138 $
"{Percent(result.SuccessRatePercent)} | {Percent(result.DetectionLimitPercent)} | " +
139 $
"{result.MaxResponseTimeMs:F1} | {Describe(result.Verdict)} |");
142 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();
152 WriteSkipped(builder, report);
155 private static void WriteSkipped(StringBuilder builder, LineQualityReport report)
157 var failed =
new List<BaudRateResult>();
158 var skipped =
new List<BaudRateResult>();
160 foreach (var result
in report.BaudRates)
162 if (result.FailureReason !=
null) failed.Add(result);
163 else if (result.SkipReason !=
null) skipped.Add(result);
166 if (failed.Count > 0)
168 builder.AppendLine(
"### Rate Changes That Did Not Complete");
169 builder.AppendLine();
170 foreach (var result
in failed)
172 builder.AppendLine($
"- **{result.BaudRate}** — {result.FailureReason}");
175 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();
182 if (skipped.Count == 0)
return;
184 builder.AppendLine(
"### Rates Not Tested");
185 builder.AppendLine();
186 foreach (var result
in skipped)
188 builder.AppendLine($
"- **{result.BaudRate}** — {result.SkipReason}");
191 builder.AppendLine();
194 private static void WriteFailureBreakdown(StringBuilder builder, LineQualityReport report)
196 var withFailures =
new List<BaudRateResult>();
197 foreach (var result
in report.BaudRates)
199 if (result.WasTested && result.Failures > 0) withFailures.Add(result);
202 if (withFailures.Count == 0)
return;
204 builder.AppendLine(
"## Failure Breakdown");
205 builder.AppendLine();
207 "| Baud Rate | Timeouts | Integrity Errors | NAKs | Pattern Mismatches |");
208 builder.AppendLine(
"|---:|---:|---:|---:|---:|");
210 foreach (var result
in withFailures)
213 $
"| {result.BaudRate} | {result.Timeouts} | {result.IntegrityErrors} | " +
214 $
"{result.Naks} | {result.PatternMismatches} |");
217 builder.AppendLine();
220 private static void WriteDetailedResults(StringBuilder builder, LineQualityReport report)
222 builder.AppendLine(
"## Detailed Results");
223 builder.AppendLine();
225 foreach (var result
in report.BaudRates)
227 if (!result.WasTested)
continue;
229 builder.AppendLine($
"### {result.BaudRate} baud — {Describe(result.Verdict)}");
230 builder.AppendLine();
232 "| Pattern | Size | Sent | Received | Timeouts | Integrity | NAKs | " +
233 "Mismatches | Success % | Avg ms | Max ms |");
234 builder.AppendLine(
"|:---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|");
236 foreach (var combination
in result.Combinations)
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} |");
247 builder.AppendLine();
251 private static void WriteMeasurementNotes(StringBuilder builder, LineQualityReport report)
253 builder.AppendLine(
"## Measurement Notes");
254 builder.AppendLine();
257 $
"- **Profile:** {report.Profile}, {report.IterationsPerCombination} iterations of each " +
258 "of the 16 pattern and size combinations per baud rate.");
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.");
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.");
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();
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();
284 private static void WriteRecommendations(StringBuilder builder, LineQualityReport report,
285 LineQualityReportMetadata metadata)
287 builder.AppendLine(
"## Recommendations");
288 builder.AppendLine();
290 if (report.RecommendedBaudRate.HasValue)
293 $
"- **Recommended operating baud rate:** {report.RecommendedBaudRate.Value}");
298 "- Re-run with `--profile extended` at the selected rate before recording " +
299 "this as a commissioning result.");
305 "- **Recommended operating baud rate:** none. No rate passed cleanly; " +
306 "investigate the failures above before selecting a rate.");
309 builder.AppendLine(
"- **Installation quality assessment:**");
310 builder.AppendLine(
"- **Remediation actions (if any):**");
311 builder.AppendLine();
313 if (
string.IsNullOrWhiteSpace(metadata.Notes))
return;
315 builder.AppendLine(
"### Notes");
316 builder.AppendLine();
317 builder.AppendLine(metadata.Notes);
318 builder.AppendLine();
321 private static void AppendRow(StringBuilder builder,
string field,
string value) =>
322 builder.AppendLine($
"| {field} | {(string.IsNullOrWhiteSpace(value) ? string.Empty : value)} |");
324 private static string Percent(
double value) =>
325 value.ToString(
"F2", CultureInfo.InvariantCulture) +
"%";
334 default:
return "UNTESTED";
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";