Skip to content

Architecture

schoolbus starts with concrete protocol objects and grows abstractions only when they remove real repetition.

The learning ladder is:

flowchart TD bits["Bits"] --> fields["Fields"] fields --> units["Units"] units --> containers["Frames, words, or sentences"] containers --> messages["Messages or transactions"] messages --> signals["Signals"] signals --> observations["Observations"] observations --> state["State"] state --> inference["Inference"]
  • Bits are positions and masks.
  • Fields give names and meaning to ranges of bits.
  • Units describe interpreted values.
  • Words, frames, and sentences are the first useful protocol containers.
  • Messages and transactions connect containers into a protocol act.
  • Signals turn encoded values into engineering quantities.
  • Observations attach time, source, and quality.
  • State is what a passive observer can infer over time.

The preferred usage style is:

thing = ProtocolThing.from_raw(...)
thing.show_fields()
thing.explain()

Structural Contracts

Protocol objects use structural contracts rather than a required inheritance tree. Major public protocol objects should be explainable and inspectable:

  • explain() -> str returns plain-English teaching context.
  • show_fields() returns display-oriented decoded fields.

Other capabilities are optional and should be advertised by the protocol catalog when they exist:

  • raw construction or export
  • arbitration comparison
  • framing encode/decode
  • transaction grouping
  • signal extraction
  • observation generation

This keeps incompatible protocols from being forced into identical APIs.

Composition

Protocol layers should compose directly. J1939 is interpreted over a CAN identifier; it does not need to subclass a CAN frame:

from schoolbus.protocols.can import CanFrame
from schoolbus.protocols.j1939 import J1939Identifier

frame = CanFrame(identifier=0x0CF00400, data=b"", extended=True)
identifier = J1939Identifier(frame.identifier)
print(identifier.explain())

The same approach should apply to future decoders: ARINC 429 label-specific decoders can wrap an Arinc429Word, MIL-STD-1553 transactions can group command, status, and data words, and serial protocols can sit on top of UART framing.

Protocol Catalog

The protocol catalog is a static registry of implemented protocol support and synthetic tutorial examples. It is intentionally not a plugin framework, auto-detection engine, or universal decoder.

Use it for iteration and discoverability:

from schoolbus.protocols import iter_protocols, make_example

for descriptor in iter_protocols():
    print(descriptor.slug, descriptor.capabilities, descriptor.binder_path)

thing = make_example("can")
print(thing.explain())

The first implementation deliberately avoids a universal protocol factory. CAN frames, J1939 identifiers, ARINC 429 words, MIL-STD-1553 words, and UART framing each get direct models first.