Skip to content

alicatlib.firmware

FirmwareVersion and FirmwareFamily — family-aware parsing and ordering. Cross-family comparison raises TypeError by design; see Devices §Firmware families and Design §5.10.

alicatlib.firmware

Firmware version parsing and family-aware ordering.

Alicat firmware evolves in four distinct families (per the Alicat Serial Primer, p. 4): GP (oldest; no Nv number, requires $$ prefix on every command), 1v-7v, 8v-9v, and 10v. Cross-family ordering is meaningless — "GP supports X" is a separate fact from "10v05 supports X" — so this module models the family as a first-class enum and refuses to order versions across families. Attempting FirmwareVersion(GP, 0, 0) < FirmwareVersion(V10, 10, 5) raises TypeError at the comparison site; gating code in :class:alicatlib.devices.session.Session catches that and surfaces it as a typed :class:alicatlib.errors.AlicatFirmwareError with reason="family_not_supported".

Design reference: docs/design.md §5.10.

NUMERIC_FAMILIES module-attribute

NUMERIC_FAMILIES = frozenset({V1_V7, V8_V9, V10})

The non-GP families — useful when gating commands that require any Nv firmware (i.e. anything that ships with a numeric version and no $$ prefix).

FirmwareFamily

Bases: Enum

Hardware/firmware lineage. See module docstring and design §5.10.

FirmwareVersion dataclass

FirmwareVersion(family, major, minor, raw)

Family-scoped firmware version.

Warning — ordering is intentionally family-gated. __lt__ / __le__ / __gt__ / __ge__ raise :class:TypeError when the operands have different families. __eq__ returns False on family mismatch rather than raising (so sets and dict lookups stay well-behaved). This asymmetry is deliberate: silent cross-family comparison is the worse failure mode.

Canonical gating pattern (see :class:alicatlib.devices.session.Session)::

if cmd.firmware_families and fw.family not in cmd.firmware_families:
    raise AlicatFirmwareError(reason="family_not_supported", ...)
if cmd.min_firmware and fw < cmd.min_firmware:       # safe: same family
    raise AlicatFirmwareError(reason="firmware_too_old", ...)

Attributes:

Name Type Description
family FirmwareFamily

The firmware family (GP / V1_V7 / V8_V9 / V10).

major int

Numeric major; 0 for GP.

minor int

Numeric minor; 0 for GP.

raw str

The original string as reported by the device, preserved for diagnostics (e.g. "GP", "GP-10v05", "10v05").

parse classmethod

parse(software)

Parse software into a :class:FirmwareVersion.

Accepts any of the historical shapes: "GP", "GP-10v05", "1v00", "7v99", "10v05", "10v5", "10.05", or those substrings embedded in a longer response.

GP detection: if the string contains a standalone GP token, the family is :attr:FirmwareFamily.GP, regardless of any trailing Nv<major>v<minor> suffix. major / minor are 0 for GP (the Nv suffix, when present, is purely cosmetic on GP hardware).

Parameters:

Name Type Description Default
software str

Firmware string as reported by the device.

required

Returns:

Type Description
Self

The parsed version.

Raises:

Type Description
AlicatParseError

If software contains neither a GP token nor a recognisable <major>v<minor> / <major>.<minor> pair.

Source code in src/alicatlib/firmware.py
@classmethod
def parse(cls, software: str) -> Self:
    """Parse ``software`` into a :class:`FirmwareVersion`.

    Accepts any of the historical shapes: ``"GP"``, ``"GP-10v05"``,
    ``"1v00"``, ``"7v99"``, ``"10v05"``, ``"10v5"``, ``"10.05"``, or those
    substrings embedded in a longer response.

    GP detection: if the string contains a standalone ``GP`` token, the
    family is :attr:`FirmwareFamily.GP`, regardless of any trailing
    ``Nv<major>v<minor>`` suffix. ``major`` / ``minor`` are ``0`` for GP
    (the Nv suffix, when present, is purely cosmetic on GP hardware).

    Args:
        software: Firmware string as reported by the device.

    Returns:
        The parsed version.

    Raises:
        AlicatParseError: If ``software`` contains neither a ``GP`` token
            nor a recognisable ``<major>v<minor>`` / ``<major>.<minor>`` pair.
    """
    is_gp = _GP_PREFIX_RE.search(software) is not None
    numeric_match = _NUMERIC_RE.search(software)

    if is_gp:
        return cls(family=FirmwareFamily.GP, major=0, minor=0, raw=software)

    if numeric_match is None:
        raise AlicatParseError(
            f"Could not parse firmware from {software!r}",
            field_name="software",
            expected="GP or <major>v<minor>",
            actual=software,
        )

    major = int(numeric_match.group("major"))
    minor = int(numeric_match.group("minor"))
    family = _family_for_major(major)
    return cls(family=family, major=major, minor=minor, raw=software)