Jump to content

S9203 Site to Host Consist Report Decoder Reference: Difference between revisions

From DocDepot
DGast (talk | contribs)
Created page with "Implementation notes for parsing AAR MSRP Standard S-9203A.V1.1 consist reports ("T-94"). Covers the four segments required to walk a file end to end: *AEM*, *RRE*, *EOT*, *EOC*. Field semantics are summarized for implementation only — refer to the controlling AAR standard for authoritative definitions and before publishing. h2. 1. File Model A T-94 file is a sequence of fixed-order, line-oriented segments. Each segment begins with a 3-character segment ID. * *AEM*..."
 
DGast (talk | contribs)
No edit summary
Line 1: Line 1:
Implementation notes for parsing AAR MSRP Standard S-9203A.V1.1 consist reports ("T-94"). Covers the four segments required to walk a file end to end: *AEM*, *RRE*, *EOT*, *EOC*.
Implementation notes for parsing AAR MSRP Standard S-9203A.V1.1 consist reports ("T-94"). Covers the four segments required to walk a file end to end: *AEM*, *RRE*, *EOT*, *EOC*.
Field semantics are summarized for implementation only — refer to the controlling AAR standard for authoritative definitions and before publishing.


h2. 1. File Model
Field semantics are summarized for implementation only refer to the controlling AAR standard for authoritative definitions and before publishing.


A T-94 file is a sequence of fixed-order, line-oriented segments. Each segment begins with a 3-character segment ID.
== File Model ==
A T-94 file is a sequence of fixed-order, line-oriented segments. Each segment begins with a 3-character segment ID.  This guide covers the basic consist level segments.  For a full implementation, refer to AAR S9203.


* *AEM* — one per consist, always first. Train-level header.
* *AEM* — one per consist, always first. Train-level header.
* *DCL* — zero or more, immediately after AEM (GPS / site asset inventory). At least one required in V1.1.
* *RRE* — one per rail vehicle, always the first segment for that vehicle. Opens the per-vehicle loop.
* *RRE* — one per rail vehicle, always the first segment for that vehicle. Opens the per-vehicle loop.
* Per-vehicle children (TDM, RTD, CEQ, EOT, XSC, THI, HAT, DYx, GEN, ALM, MMR, TST) follow their RRE.
* *EOC* — one per consist, always last. Carries the byte count.
* *EOC* — one per consist, always last. Carries the byte count.


Maintenance reports (AMH / MTS / EMS) are a separate message that may arrive attached to the consist or standalone.
== Encoding Variants — Detect Before Parsing ==
 
Three output modes are user-selectable at the reader, Comet will set up the delimiter character as a default.
h3. 1.1 Encoding Variants — Detect Before Parsing
 
Three output modes are user-selectable at the reader, so a decoder must handle all three:


|| Mode || Field widths || Delimiter || Parse strategy ||
|| Mode || Field widths || Delimiter || Parse strategy ||
Line 23: Line 18:
| 3 | Variable | present | Split on delimiter, index by position |
| 3 | Variable | present | Split on delimiter, index by position |


{warning:title=Detection heuristic}
Read the first line. If characters 4 and 10 (1-based) are the delimiter candidate and total line length equals the fixed-width sum plus delimiter count, treat as Mode 2. If total length equals the bare fixed-width sum, Mode 1. Otherwise Mode 3. Sample the {{*}} character explicitly.
Read the first line. If characters 4 and 10 (1-based) are the delimiter candidate and total line length equals the fixed-width sum plus delimiter count, treat as Mode 2. If total length equals the bare fixed-width sum, Mode 1. Otherwise Mode 3. Sample the {{*}} character explicitly — V1.1 examples for DCL/TDM use asterisk delimiting, older examples use space.
{warning}


{note}
{note}
Line 31: Line 24:
{note}
{note}


h2. 2. AEM — Consist Header
== AEM — Consist Header ==
 
One per consist. Establishes reporting site, timestamp, direction, speeds, counts, and site health.
One per consist. Establishes reporting site, timestamp, direction, speeds, counts, and site health.


h3. 2.1 Offset Table (Mode 1, 0-based, total 78 bytes)
=== Offset Table (Mode 1, 0-based, total 78 bytes) ===
 
|| Ref || Off || Len || Type || Field ||
|| Ref || Off || Len || Type || Field ||
| AEM00 | 0 | 3 | AN | Segment ID = "AEM" |
| AEM00 | 0 | 3 | AN | Segment ID = "AEM" |
Line 68: Line 59:
| AEM28 | 74 | 4 | N | Total Axle Count |
| AEM28 | 74 | 4 | N | Total Axle Count |


h3. 2.2 Implied Decimals
=== Implied Decimals ===
 
{code}
{code}
AEM06  Time Zone      "##.#" decimal assumed  ->  "080" = 8.0 hrs west of GMT
AEM06  Time Zone      "##.#" decimal assumed  ->  "080" = 8.0 hrs west of GMT
Line 80: Line 70:
Gate version-specific parsing on digit 1 of AEM08. A change in digit 1 may not be backward compatible; digits 2 and 3 are safe to ignore.
Gate version-specific parsing on digit 1 of AEM08. A change in digit 1 may not be backward compatible; digits 2 and 3 are safe to ignore.


h3. 2.3 Code Lookups
=== Code Lookups ===
 
*AEM10 / AEM11 — Conversion Status (confidence)*
*AEM10 / AEM11 — Conversion Status (confidence)*
|| Code || Meaning ||
|| Code || Meaning ||
Line 160: Line 149:
This field is the natural hook for automated site-health alerting: map to a severity enum on ingest and raise on anything in {B, F, I, Q, S, U, V, M}.
This field is the natural hook for automated site-health alerting: map to a severity enum on ingest and raise on anything in {B, F, I, Q, S, U, V, M}.


h3. 2.4 Derived Read-Rate Metrics
=== Derived Read-Rate Metrics ===
 
{code}
{code}
loco_read_rate    = AEM25 / AEM24      (Locomotives Tagged / Locomotive Count)
loco_read_rate    = AEM25 / AEM24      (Locomotives Tagged / Locomotive Count)
Line 168: Line 156:
Counts include untagged equipment; "Tagged" counts only valid tags. Guard against divide-by-zero — a locomotive-only or railcar-only consist yields a zero denominator on the other pair.
Counts include untagged equipment; "Tagged" counts only valid tags. Guard against divide-by-zero — a locomotive-only or railcar-only consist yields a zero denominator on the other pair.


h2. 3. RRE — Rail Equipment
== RRE — Rail Equipment ==
 
Opens the per-vehicle loop. At least one RRE per rail vehicle, tagged or not. Always the first segment reported for a vehicle.
Opens the per-vehicle loop. At least one RRE per rail vehicle, tagged or not. Always the first segment reported for a vehicle.


h3. 3.1 Offset Table (Mode 1, 0-based, total 38 bytes)
=== Offset Table (Mode 1, 0-based, total 38 bytes) ===
 
|| Ref || Off || Len || Type || Field ||
|| Ref || Off || Len || Type || Field ||
| RRE00 | 0 | 3 | AN | Segment ID = "RRE" |
| RRE00 | 0 | 3 | AN | Segment ID = "RRE" |
Line 191: Line 177:
| RRE14 | 36 | 2 | N | Platform Count |
| RRE14 | 36 | 2 | N | Platform Count |


h3. 3.2 Code Lookups
=== Code Lookups ===
 
*RRE02 — Equipment Group Code:* D = Locomotive, R = Railcar (includes former non-revenue rail), ? = Unknown. Derived from axle pattern when the vehicle is untagged.
*RRE02 — Equipment Group Code:* D = Locomotive, R = Railcar (includes former non-revenue rail), ? = Unknown. Derived from axle pattern when the vehicle is untagged.


Line 228: Line 213:
{warning}
{warning}


h3. 3.3 The Duplicate-Sequence Rule
=== The Duplicate-Sequence Rule ===
 
{warning}
{warning}
*Sequence Number is not a unique key.* When two tags are found on one vehicle with differing IDs (an ID mismatch), *two RRE segments are emitted with the same RRE01*, both carrying RRE08 = "M". The primary key for a vehicle row must be (consist key, RRE01, occurrence) — or (consist key, RRE01, RRE03, RRE04). A naive upsert on sequence number silently drops the second tag and destroys the mismatch evidence.
*Sequence Number is not a unique key.* When two tags are found on one vehicle with differing IDs (an ID mismatch), *two RRE segments are emitted with the same RRE01*, both carrying RRE08 = "M". The primary key for a vehicle row must be (consist key, RRE01, occurrence) — or (consist key, RRE01, RRE03, RRE04). A naive upsert on sequence number silently drops the second tag and destroys the mismatch evidence.
Line 380: Line 364:
* DCL and TDM are marked *Proposed* in the standard while RTD is mandatory in V1.1; verify current adoption state against the latest published revision before treating any of the three as guaranteed.
* DCL and TDM are marked *Proposed* in the standard while RTD is mandatory in V1.1; verify current adoption state against the latest published revision before treating any of the three as guaranteed.


h2. 9. References
== References ==
 
* AAR Manual of Standards and Recommended Practices, Standard *S-9203A.V1.1* — AEI Site-to-Host Consist Report Format (formerly S-918A). Adopted 2003, revised 2020.
* AAR Manual of Standards and Recommended Practices, Standard *S-9203A.V1.1* — AEI Site-to-Host Consist Report Format (formerly S-918A). Adopted 2003, revised 2020.
* AAR MSRP Standard *S-9203* (latest version) — tag data formats. Appendix A/B for RRE, Appendix E for EOT, Appendix P for raw reader-to-processor data (RTD).
* AAR MSRP Standard *S-9203* (latest version) — tag data formats. Appendix A/B for RRE, Appendix E for EOT, Appendix P for raw reader-to-processor data (RTD).
* Appendix A §1.0 of S-9203A — Equipment Group Code values.
* Appendix A §1.0 of S-9203A — Equipment Group Code values.
 
This page is an implementation summary. The AAR MSRP is licensed, copyrighted material — link to the controlled copy rather than attaching or transcribing the source document into the wiki.
{note:title=Do not host the standard here}
This page is an implementation summary. The AAR MSRP is licensed, copyrighted material — link to the controlled copy rather than attaching or transcribing the source document into the wiki.
{note}

Revision as of 16:14, 29 July 2026

Implementation notes for parsing AAR MSRP Standard S-9203A.V1.1 consist reports ("T-94"). Covers the four segments required to walk a file end to end: *AEM*, *RRE*, *EOT*, *EOC*.

Field semantics are summarized for implementation only refer to the controlling AAR standard for authoritative definitions and before publishing.

File Model

A T-94 file is a sequence of fixed-order, line-oriented segments. Each segment begins with a 3-character segment ID. This guide covers the basic consist level segments. For a full implementation, refer to AAR S9203.

  • *AEM* — one per consist, always first. Train-level header.
  • *RRE* — one per rail vehicle, always the first segment for that vehicle. Opens the per-vehicle loop.
  • *EOC* — one per consist, always last. Carries the byte count.

Encoding Variants — Detect Before Parsing

Three output modes are user-selectable at the reader, Comet will set up the delimiter character as a default.

|| Mode || Field widths || Delimiter || Parse strategy || | 1 | Fixed | none | Slice by offset table | | 2 | Fixed | present (space or Template:*) | Slice by offset, skip 1 delimiter char between fields | | 3 | Variable | present | Split on delimiter, index by position |

Read the first line. If characters 4 and 10 (1-based) are the delimiter candidate and total line length equals the fixed-width sum plus delimiter count, treat as Mode 2. If total length equals the bare fixed-width sum, Mode 1. Otherwise Mode 3. Sample the Template:* character explicitly.

{note}

  • Elements and whole segments may be turned off* by site configuration. Turning off elements forces a delimited format. Never assume a fixed line length without validating it; treat a short line as "elements suppressed," not as corrupt data.

{note}

AEM — Consist Header

One per consist. Establishes reporting site, timestamp, direction, speeds, counts, and site health.

Offset Table (Mode 1, 0-based, total 78 bytes)

|| Ref || Off || Len || Type || Field || | AEM00 | 0 | 3 | AN | Segment ID = "AEM" | | AEM01 | 3 | 5 | AN | AAR Billing Code | | AEM02 | 8 | 7 | AN | Site ID | | AEM03 | 15 | 6 | N | Event Start Date (YYMMDD) | | AEM04 | 21 | 4 | N | Event Start Time (HHMM, local) | | AEM05 | 25 | 4 | N | Event Stop Time (HHMM) | | AEM06 | 29 | 3 | N | Time Zone offset from GMT | | AEM07 | 32 | 1 | A | DST Indicator (Y/N) | | AEM08 | 33 | 3 | AN | Data Format Version | | AEM09 | 36 | 4 | N | Train Sequence Number | | AEM10 | 40 | 1 | AN | Locomotive Conversion Status | | AEM11 | 41 | 1 | AN | Railcar Conversion Status | | AEM12 | 42 | 1 | A | Direction of Travel | | AEM13 | 43 | 1 | N | Switch / Direction Indicator | | AEM14 | 44 | 1 | A | Units of Measure (E/M) | | AEM15 | 45 | 3 | N | Maximum Speed | | AEM16 | 48 | 3 | N | Minimum Speed | | AEM17 | 51 | 3 | N | Average Speed | | AEM18 | 54 | 1 | AN | Movement Status | | AEM19 | 55 | 1 | AN | Termination Status | | AEM20 | 56 | 1 | AN | Transmission Type | | AEM21 | 57 | 1 | AN | Adjacent Track Occupied (Y/N) | | AEM22 | 58 | 5 | N | Train Length | | AEM23 | 63 | 1 | AN | Equipment Status Code | | AEM24 | 64 | 2 | N | Locomotive Count | | AEM25 | 66 | 2 | N | Locomotives Tagged | | AEM26 | 68 | 3 | N | Railcar Count | | AEM27 | 71 | 3 | N | Railcars Tagged | | AEM28 | 74 | 4 | N | Total Axle Count |

Implied Decimals

{code} AEM06 Time Zone "##.#" decimal assumed -> "080" = 8.0 hrs west of GMT

                     5=Eastern 6=Central 7=Mountain 8=Pacific

AEM08 Format Version "#.##" decimal assumed -> "400" = 4.00

                     digit1 = breaking change, digit2 = compatible enhancement,
                     digit3 = processing upgrade

{code}

Gate version-specific parsing on digit 1 of AEM08. A change in digit 1 may not be backward compatible; digits 2 and 3 are safe to ignore.

Code Lookups

  • AEM10 / AEM11 — Conversion Status (confidence)*

|| Code || Meaning || | G | Good, no significant problems | | A | At least one questionable axle pattern | | D | Excessive disqualifiers (≥25% tag/axle inconsistency) | | M | Both A and D | | ? | Other |

  • AEM12 — Direction of Travel:* N, S, E, W
  • AEM13 — Switch / Direction Indicator:* 0–9, owner-defined. Reflects local switch position so the host can infer route where multiple routes exist. Do not hardcode meanings; make it a per-site lookup.
  • AEM14 — Units of Measure:* E = English, M = Metric. Applies to the *entire report*. Latch this value and use it for all downstream unit conversions (see §6).
  • AEM18 — Movement Status*

|| Code || Meaning || | A | Through movement, over 5 mph | | B | Through movement, under 5 mph | | C | Through movement, stop and go | | D | Through movement, switching (direction changes detected) | | E | Simulation (software-generated train) | | F | Reverse exit (pull by) | | R | Arrival listing | | ? | Other |

{tip} Filter *E* (simulation) out of production analytics. It is a software-generated train, not traffic. {tip}

  • AEM19 — Termination Status*

|| Code || Meaning || Decoder action || | N | Normal | Accept | | T | Time-out; train stopped, partial consist reported | Hold as fragment; expect an appended retransmission | | O | Presence forcibly cleared (failed presence circuit) | Accept, flag data quality | | P | Processing limitation | Accept, flag data quality | | ? | Other | Accept, flag |

  • AEM20 — Transmission Type*

|| Code || Meaning || Decoder action || | F | First transmission | Insert | | R | Retransmission | Idempotent upsert on (Billing Code, Site ID, Sequence, Start Date/Time) | | S | Summary | Upsert | | A | Appended | *Replace* the prior fragment entirely | | ? | Other | Log |

{warning:title=Time-out / append pairing — the main state-machine trap} When AEM19 = *T*, the site reports a partial train. When movement resumes the train is reported again with the new cars appended, carrying *the same train sequence number and the same start date/time* as the original, and AEM20 = *A*. The appended report *replaces* the earlier fragment — it is not a delta. Key your idempotency on (AEM01, AEM02, AEM09, AEM03, AEM04) and overwrite rather than merge. {warning}

  • AEM23 — Equipment Status Code (site health)*

Reflects abnormal site activity during or before this train. Detail lives in the maintenance report.

|| Code || Condition || Severity || | G | No problems detected | OK | | A | Antenna warning — low tag count on ≥1 antenna | Warning | | B | Antenna fatal — ≥1 antenna no longer reading | Fatal | | C | Communication error with external components | Error | | E | Intertrack comms warning (recovered) | Warning | | F | Intertrack comms fatal — cannot reach external processor | Fatal | | H | Power supply warning — charger temporarily lost power | Warning | | I | Power supply fatal — charger lost power | Fatal | | K | Software setup failure — parameter set inaccurately | Error | | P | Presence circuit warning (recovered) | Warning | | Q | Presence circuit fatal — no longer functional | Fatal | | R | Reader / RF warning (recovered) | Warning | | S | Reader / RF fatal — ≥1 reader no longer functional | Fatal | | T | Wheel transducer warning (recovered) | Warning | | U | Wheel transducer fatal | Fatal | | V | No wheel transducer input | Fatal | | W | External detector trigger (hut door, thermocouple) | Info | | X | External processor comms warning (recovered) | Warning | | Y | External processor comms fatal | Fatal | | Z | Security — excessive failed logon attempts | Security | | M | Multiple problems | Escalate | | ? | Other | Warning |

This field is the natural hook for automated site-health alerting: map to a severity enum on ingest and raise on anything in {B, F, I, Q, S, U, V, M}.

Derived Read-Rate Metrics

{code} loco_read_rate = AEM25 / AEM24 (Locomotives Tagged / Locomotive Count) railcar_read_rate = AEM27 / AEM26 (Railcars Tagged / Railcar Count) {code} Counts include untagged equipment; "Tagged" counts only valid tags. Guard against divide-by-zero — a locomotive-only or railcar-only consist yields a zero denominator on the other pair.

RRE — Rail Equipment

Opens the per-vehicle loop. At least one RRE per rail vehicle, tagged or not. Always the first segment reported for a vehicle.

Offset Table (Mode 1, 0-based, total 38 bytes)

|| Ref || Off || Len || Type || Field || | RRE00 | 0 | 3 | AN | Segment ID = "RRE" | | RRE01 | 3 | 3 | N | Sequence Number (standing order, 1 = first) | | RRE02 | 6 | 1 | A | Equipment Group Code | | RRE03 | 7 | 4 | A | Owner Code (SCAC) | | RRE04 | 11 | 10 | N | Owner Equipment Number | | RRE05 | 21 | 1 | A | Orientation | | RRE06 | 22 | 1 | AN | Reserved | | RRE07 | 23 | 1 | A | Axle Conversion Code | | RRE08 | 24 | 1 | A | Tag Status | | RRE09 | 25 | 1 | A | Tag Detail Status | | RRE10 | 26 | 2 | N | Handshakes Antenna 0 | | RRE11 | 28 | 2 | N | Handshakes Antenna 1 | | RRE12 | 30 | 3 | N | Speed of Vehicle | | RRE13 | 33 | 3 | N | Axles counted by wheel sensor | | RRE14 | 36 | 2 | N | Platform Count |

Code Lookups

  • RRE02 — Equipment Group Code:* D = Locomotive, R = Railcar (includes former non-revenue rail), ? = Unknown. Derived from axle pattern when the vehicle is untagged.
  • RRE05 — Orientation (which end faces forward)*

|| Code || Railcar || Locomotive || | A | "A" end | "F" end | | B | "B" (brake) end | "R" end | | U | Unknown / not determined | Unknown |

  • RRE07 — Axle Conversion Code:* G = good, no problem determining vehicles from axle input. B = bad, axle data may have created a phantom vehicle. S = sequence logically correct but software cannot determine vehicle type.
  • RRE08 — Tag Status (placement / read completeness)*

|| Code || Meaning || | G | Good — both right and left tags read | | M | Mismatched identification (owner code + equipment number differ between tags) | | L | Left tag missing | | R | Right tag missing | | N | No tag read | | ? | Other |

  • RRE09 — Tag Detail Status (tag content / programming)*

|| Code || Meaning || | K | OK | | A | Axle error — tag axle count differs from axle pattern | | E | EGC error — tag EGC differs from axle pattern | | L | Length error — tag length differs from axle pattern by >15% | | O | Placement error — wrong side or wrong end | | D | Unused tag read on equipment | | W | Window violation — tag outside the AAR acceptable application window | | M | Multiple errors | | ? | Other |

{warning:title=RRE09 = "M" triggers a required TDM} When Tag Detail Status is *M*, the RRE is followed by a *TDM* segment enumerating exactly which cross-checks failed. A decoder that ignores TDM loses all detail behind an "M". Also note a TDM is unconditionally required directly after the *first* RRE in every consist, which declares which cross-checks the site is capable of measuring at all — parse it before interpreting any RRE09 value. {warning}

The Duplicate-Sequence Rule

{warning}

  • Sequence Number is not a unique key.* When two tags are found on one vehicle with differing IDs (an ID mismatch), *two RRE segments are emitted with the same RRE01*, both carrying RRE08 = "M". The primary key for a vehicle row must be (consist key, RRE01, occurrence) — or (consist key, RRE01, RRE03, RRE04). A naive upsert on sequence number silently drops the second tag and destroys the mismatch evidence.

{warning}

h3. 3.4 Field Handling Notes

  • *RRE03 (Owner Code)* — 4 chars, *space-padded* SCAC ("SP  "). *Blank if not tagged.* Trim on read; distinguish blank-because-untagged from blank-because-suppressed by checking RRE08 = N.
  • *RRE04 (Equipment Number)* — 10 digits, leading zeros. *Zero if not tagged.* Store as string if you need to round-trip exactly; convert to integer only for matching.
  • *RRE10 / RRE11 (Handshakes)* — 2 digits, saturating: *99 means "99 or more"*, not literally 99. Do not compute averages across a saturated field without flagging. A zero on one antenna alongside a healthy count on the other is the normal signature of a missing left/right tag.
  • *RRE14 (Platform Count)* — always 1 for non-articulated cars; >1 indicates an articulated multi-platform car, which is what CEQ platform codes hang off.
  • Untagged vehicles still produce an RRE derived purely from axle pattern. Expect blank SCAC, zero number, RRE08 = N.

h2. 4. EOT — End of Train Device

One per EOT tag read. Usually the last tag in the consist, but can appear on any vehicle (e.g. dead-headed on a locomotive), so do not use EOT position as an end-of-train marker.

h3. 4.1 Offset Table (Mode 1, 0-based, total 26 bytes)

|| Ref || Off || Len || Type || Field || | EOT00 | 0 | 3 | AN | Segment ID = "EOT" | | EOT01 | 3 | 3 | N | Sequence Number of the *carrying vehicle* | | EOT02 | 6 | 1 | A | Equipment Group Code (from tag; "E" per EGC table) | | EOT03 | 7 | 4 | A | Owner Code (SCAC) | | EOT04 | 11 | 10 | N | Owner Equipment Number | | EOT05 | 21 | 2 | N | Handshakes Antenna 0 | | EOT06 | 23 | 2 | N | Handshakes Antenna 1 | | EOT07 | 25 | 1 | A | Tag Detail Status |

h3. 4.2 EOT07 — Tag Detail Status || Code || Meaning || | K | OK | | H | Performance — low handshakes | | W | Window violation — tag in wrong location | | ? | Other |

{note} Per the implementation-status notes in the standard, EOT Tag Detail Status is *always "K"* in practice; all other fields are complete. Treat non-K as an anomaly worth logging rather than a routine branch. {note}

Max occurrences per RRE loop: 99. EOT01 associates the device to its carrier — join on sequence number, do not assume the EOT belongs to the highest-numbered vehicle.

h2. 5. EOC — End of Consist

Mandatory terminating segment. Always the last segment of the consist report.

|| Ref || Off || Len || Type || Field || | EOC00 | 0 | 3 | AN | Segment ID = "EOC" | | EOC01 | 3 | 10 | N | Total Byte Count |

Total: 13 bytes.

{warning:title=Byte count semantics} EOC01 counts *from the consist header (AEM) up to but not including the EOC segment itself*. Use it as the integrity check on every consist:

{code} observed = byte_length(all bytes from start of AEM through end of segment preceding EOC) if observed != int(EOC01): reject_or_quarantine(consist) {code}

Two things to pin down against live data from each site before enabling hard rejection: whether line terminators (CR/LF) are included in the count, and whether trailing delimiters are counted. These vary by vendor implementation. Recommended rollout: log the delta in advisory mode first, then enforce once the per-vendor convention is confirmed. {warning}

Absence of EOC means a truncated transmission — quarantine, do not partially commit.

h2. 6. Units of Measure

AEM14 governs the whole report. Fields affected across the four segments in scope:

|| Segment || Field || E (English) || M (Metric) || | AEM | Max / Min / Avg Speed | mph | kph | | AEM | Train Length | feet | decimeters | | RRE | Speed | mph | kph |

{warning} Metric train length is in *decimeters*, not meters. This is a frequent off-by-10 source. Normalize to a single internal unit at ingest and store the original AEM14 value alongside it for audit. {warning}

Out of scope here but on the same latch: CEQ length (feet / decimeters), XSC weight (pounds / kilograms), DYL fuel volume (100 gal / 100 L) and cumulative energy (100 HP-hr / 100 kW-hr).

h2. 7. Parser Contract

h3. 7.1 Ordering and Looping

Segments must be transmitted in the standard T-94 sequence. Relevant structure for the segments in scope:

|| Segment || Loop L1 || Loop L2 || Status || Max Occurs || | AEM | 1 | 1 | Required | 1 | | DCL | 1 | 1 | Optional loop | 999 | | RRE | 2 | 1 | Optional — begins RRE loop | 999 | | TDM | 2 | 1 | Required after RRE01 / conditional | — | | EOT | 2 | 2 | Optional | 99 | | RTD | 2 | 6 | Mandatory in V1.1 | 99 | | CEQ | 2 | 13 | Optional — begins CEQ loop inside RRE loop | 999 | | MMR | 2 | 14 | Optional — begins MMR loop inside RRE loop | 999 | | EOC | 3 | 1 | Required | 1 |

Loop mechanics: the first segment of a loop appears exactly once per occurrence and never again inside that loop, so RRE reliably delimits vehicle boundaries and CEQ / MMR reliably delimit their nested occurrences.

h3. 7.2 Recommended State Machine

{code} EXPECT_AEM

 -> AEM              : latch units(AEM14), version(AEM08), consist key,
                       site health(AEM23); start byte counter

EXPECT_DCL_OR_RRE

 -> DCL*             : accumulate site asset inventory
 -> RRE              : first RRE -> next segment MUST be TDM (capability decl.)

IN_VEHICLE

 -> RRE              : close prior vehicle, open new.
                       If RRE01 == previous RRE01 -> ID-mismatch pair, same vehicle
 -> TDM              : cross-check detail (required when prior RRE09 == 'M')
 -> RTD              : raw tag hex, 0..99 per vehicle
 -> EOT | CEQ | XSC | THI | HAT | DYx | GEN | ALM | MMR | TST
                     : attach to current RRE01
 -> EOC              : close vehicle, validate byte count, emit consist

DONE {code}

h3. 7.3 Defensive Rules

  1. *Dispatch on segment ID, never on line position.* Any segment may be disabled at the site.
  2. *Treat "?" as a first-class value in every enumerated field.* It means "other," not "invalid." Never throw on it.
  3. *Preserve raw lines.* Store the original segment text alongside the parsed row for the retention window. Reprocessing beats re-requesting from a Class I host.
  4. *Never reject a consist over an unknown code value.* Log unknown enum values and pass the record through with a data-quality flag. New codes get added to the standard.
  5. *Idempotency key:* (AEM01 Billing Code, AEM02 Site ID, AEM09 Sequence, AEM03 Start Date, AEM04 Start Time). AEM09 wraps 9999 → 1, and zero is invalid — sequence alone is not unique across a long feed.
  6. *Timestamps are local time.* AEM03/AEM04 plus AEM06 offset plus AEM07 DST flag. Convert to UTC on ingest and retain the original triple. Do not infer DST from the date; trust AEM07.
  7. *Fixed-width examples in the standard are not perfectly consistent* in their spacing. Build the parser off the field-size tables, not off the printed examples, and be tolerant of stray whitespace.
  8. *Numeric fields are zero-padded strings.* Strip leading zeros only at the point of use; equipment numbers in particular should round-trip as strings.

h3. 7.4 Validation Checklist per Consist

|| Check || Source || On failure || | Segment ID recognized | 3-char prefix | Log + skip segment, continue | | AEM present and first | State machine | Reject consist | | EOC present and last | State machine | Quarantine as truncated | | Byte count matches | EOC01 | Advisory, then reject once convention confirmed | | Format version major digit supported | AEM08 digit 1 | Route to quarantine for review | | RRE count reconciles to AEM24 + AEM26 | Counts | Data-quality flag, do not reject | | Tagged counts ≤ total counts | AEM24/25, AEM26/27 | Data-quality flag | | Site health nominal | AEM23 | Raise maintenance alert on fatal codes | | Every RRE01 in 1..AEM24+AEM26 | Sequence | Data-quality flag | | Duplicate RRE01 also has RRE08 = M | Mismatch rule | Log unexpected duplicate |

h2. 8. Open Items

  • Confirm per-vendor CR/LF and trailing-delimiter treatment in the EOC01 byte count before enforcing rejection.
  • Confirm whether source sites emit V1.1 mandatory RTD and TDM segments, or legacy output without them — this determines whether their absence is an error or an expected legacy condition.
  • Register the delimiter character actually used per site (space vs Template:*) in site configuration rather than detecting it per file.
  • DCL and TDM are marked *Proposed* in the standard while RTD is mandatory in V1.1; verify current adoption state against the latest published revision before treating any of the three as guaranteed.

References

  • AAR Manual of Standards and Recommended Practices, Standard *S-9203A.V1.1* — AEI Site-to-Host Consist Report Format (formerly S-918A). Adopted 2003, revised 2020.
  • AAR MSRP Standard *S-9203* (latest version) — tag data formats. Appendix A/B for RRE, Appendix E for EOT, Appendix P for raw reader-to-processor data (RTD).
  • Appendix A §1.0 of S-9203A — Equipment Group Code values.
This page is an implementation summary. The AAR MSRP is licensed, copyrighted material — link to the controlled copy rather than attaching or transcribing the source document into the wiki.