refactor: fronthaul, telemetry, radio packages; system harness dir

- Add fiwicontrol.fronthaul (FrontHaul link) and fiwicontrol.telemetry
  (FrontHaulTelemetry + retimer/SFP structs; common placeholder).
- Move RadioHead to fiwicontrol.radio; lab is discovery + INI only.
- Rename FrontHaulSnapshot to FrontHaulTelemetry; FrontHaul.snapshot
  to telemetry field.
- Add scripts/system/ for long-running hardware harnesses (vs pytest).
- README and pyproject describe the new layout; Power docstring path fix.

Made-with: Cursor
This commit is contained in:
Robert McMahon 2026-04-12 12:27:51 -07:00
parent ed8020ecc3
commit 7b57568176
13 changed files with 378 additions and 12 deletions

View File

@ -10,21 +10,24 @@ This repository ships that distribution (**`fiwicontrol`** on PyPI / `pip`) with
1. **`fiwicontrol.commands`** — run commands on remote rigs via OpenSSH or **`ush`** (`Command`, `CommandManager`, `ssh_node`). Remote Pi bootstrap: **`python3 -m fiwicontrol.commands <ip> --remote-repo …`** (see **`docs/install.md`**). Must not import **`fiwicontrol.power`**.
2. **`fiwicontrol.lab`** — USB discovery (Acroname / Monsoon) and **`configs/*.ini`** inventory load + verify. Imports **`fiwicontrol.commands`** for SSH discovery only.
3. **`fiwicontrol.power`** — **`Power`** (``.on()`` / ``.off()`` / ``.voltage()`` / …) via **`AcronamePower`** and **`MonsoonPower`**, plus a small CLI (**`--discovery-json`**, **`--list-power-devices`**, **`--verify-inventory`** with **`-c`**). Re-exports **`fiwicontrol.lab`** discovery/inventory for compatibility. May import **`fiwicontrol.commands`** and **`fiwicontrol.lab`**; the reverse is forbidden.
4. **`fiwicontrol.flows`** — async **iperf**-driven traffic **flows** over **`ssh`** (remote server/client processes, sampling, histogram / KS tooling). Lives in **`flows/flows.py`**, structured for **Python 3.11** (**`asyncio.timeout`**, **`asyncio.gather`**, **`await iperf_flow.run_traffic`** and related coroutines; sync **`run`** / **`commence`** / … call **`asyncio.run`** only when no loop is already running). Depends on **SciPy**, **NumPy**, **Matplotlib**, and a compatible **iperf** binary on endpoints—use **`pip install -e ".[flows]"`** when you need it; **`import fiwicontrol.flows`** alone stays lightweight (lazy load). Not yet integrated with **`ssh_node`** or **`iperf 2` on Pi 5** roadmap text above—that wiring is follow-on work.
5. **`fiwicontrol.spc`** — **SPC** primitives: **`ShewhartControlChart`** (individuals / MR limits) and **`HotellingT2`** (Phase I meancovariance, *T*² and UCL). Under **`spc/`** (not **`flows/`**). Optional **`pip install -e ".[spc]"`** (**NumPy**, **SciPy**); **`import fiwicontrol.spc`** is lazy until you reference a class.
3. **`fiwicontrol.fronthaul`** — concentrator→RRH **fronthaul link** identity (**`FrontHaul`**: medium, PCIe-style ids, link state); used in **lab and production**.
4. **`fiwicontrol.telemetry`** — telemetry **schemas split by domain**; today **`telemetry.fronthaul`** exposes **`FrontHaulTelemetry`** (retimer board / PCIe slot / cable / SFP / rails, e.g. **Adnacom Monitor** fields). Future: WiFi / FiWiwide modules alongside **`common`** for shared envelopes when needed.
5. **`fiwicontrol.radio`** — logical **RRH / AP** aggregate (**`RadioHead`**: WiFi, hostapd, clients, **`FrontHaul`**, optional **`Power`**); **lab and production**.
6. **`fiwicontrol.power`** — **`Power`** (``.on()`` / ``.off()`` / ``.voltage()`` / …) via **`AcronamePower`** and **`MonsoonPower`**, plus a small CLI (**`--discovery-json`**, **`--list-power-devices`**, **`--verify-inventory`** with **`-c`**). Re-exports **`fiwicontrol.lab`** discovery/inventory for compatibility. May import **`fiwicontrol.commands`** and **`fiwicontrol.lab`**; the reverse is forbidden.
7. **`fiwicontrol.flows`** — async **iperf**-driven traffic **flows** over **`ssh`** (remote server/client processes, sampling, histogram / KS tooling). Lives in **`flows/flows.py`**, structured for **Python 3.11** (**`asyncio.timeout`**, **`asyncio.gather`**, **`await iperf_flow.run_traffic`** and related coroutines; sync **`run`** / **`commence`** / … call **`asyncio.run`** only when no loop is already running). Depends on **SciPy**, **NumPy**, **Matplotlib**, and a compatible **iperf** binary on endpoints—use **`pip install -e ".[flows]"`** when you need it; **`import fiwicontrol.flows`** alone stays lightweight (lazy load). Not yet integrated with **`ssh_node`** or **`iperf 2` on Pi 5** roadmap text above—that wiring is follow-on work.
8. **`fiwicontrol.spc`** — **SPC** primitives: **`ShewhartControlChart`** (individuals / MR limits) and **`HotellingT2`** (Phase I meancovariance, *T*² and UCL). Under **`spc/`** (not **`flows/`**). Optional **`pip install -e ".[spc]"`** (**NumPy**, **SciPy**); **`import fiwicontrol.spc`** is lazy until you reference a class.
## Relationship to the FiWi architecture spec
The spec (**`html/Fi-Wi-L4S.html`**) describes the **FiWi system**: the **Umber concentrator** as the centralized **control, queueing, and time** plane, **RRHs** on the **PCIe / fronthaul** fabric, and the **L4S-oriented** latency model that depends on that split. FiWiControl does not implement the datapath or MAC; it is the **concentrator-resident (and dev-workstation) Python layer** that **manages and validates** the deployed system around that architecture.
Concretely, it is how we keep **what we think is connected** aligned with **what is actually cabled and powered**: **INI inventory and verification** (**`fiwicontrol.lab`**, **`fiwicontrol.power`**), **scripted remote work** over the same **SSH / `ush`** paths we use in production (**`fiwicontrol.commands`**), **programmable power and USB** so bring-up and PCIe **hot-plug** sequences are repeatable, **async iperf flows** for load and measurement against RRHs or lab stand-ins (**`fiwicontrol.flows`**), and **SPC** when we need statistical discipline across long campaigns (**`fiwicontrol.spc`**). Read the spec for **why** the topology and timing model look the way they do; read this repo and **`docs/`** for **how** we install, verify, and operate it day to day.
Concretely, it is how we keep **what we think is connected** aligned with **what is actually cabled and powered**: **INI inventory and verification** (**`fiwicontrol.lab`**, **`fiwicontrol.power`**), **scripted remote work** over the same **SSH / `ush`** paths we use in production (**`fiwicontrol.commands`**), **programmable power and USB** so bring-up and **fronthaul hot-plug** sequences are repeatable (PCIe is the fronthaul medium today), **fronthaul telemetry** (**`fiwicontrol.telemetry`**) for host retimer readouts, **async iperf flows** for load and measurement against RRHs or lab stand-ins (**`fiwicontrol.flows`**), and **SPC** when we need statistical discipline across long campaigns (**`fiwicontrol.spc`**). Read the spec for **why** the topology and timing model look the way they do; read this repo and **`docs/`** for **how** we install, verify, and operate it day to day.
## Near-term focus: PCIe hot-swap testing
## Near-term focus: fronthaul hot-swap testing (PCIe today)
A **first concrete goal** for this stack is to support **PCIe hot-swap (hot-plug) testing** in the lab: controlled **remove / restore** of the link, predictable **enumeration** and driver behavior, and **repeatable** runs across builds and rigs.
A **first concrete goal** for this stack is to support **fronthaul hot-swap (hot-plug) testing** in the lab—**PCIe** for now: controlled **remove / restore** of the link, predictable **enumeration** and driver behavior, and **repeatable** runs across builds and rigs.
That work fails if the bench is informal—wrong port, wrong power path, or no shared picture of what was connected when. FiWiControl targets that gap by combining **documented inventory** (INI + **`--verify-inventory`**), **remote automation** (**`ssh_node`**, **`python3 -m fiwicontrol.commands`** to bring up rigs), and **programmable power / USB paths** (**`fiwicontrol.power`**) so sequences are **scripted and checkable** before the PCIe harness runs. **`fiwicontrol.spc`** is aimed at **many hot-plug cycles**—spotting drift in failure rates or side metrics—not only general FiWi bring-up (see **`docs/spc.md`**).
That work fails if the bench is informal—wrong port, wrong power path, or no shared picture of what was connected when. FiWiControl targets that gap by combining **documented inventory** (INI + **`--verify-inventory`**), **remote automation** (**`ssh_node`**, **`python3 -m fiwicontrol.commands`** to bring up rigs), and **programmable power / USB paths** (**`fiwicontrol.power`**) so sequences are **scripted and checkable** before the fronthaul harness runs. **`fiwicontrol.spc`** is aimed at **many hot-plug cycles**—spotting drift in failure rates or side metrics—not only general FiWi bring-up (see **`docs/spc.md`**).
## Lab fleet: Raspberry Pi 5 and iperf 2 (planned)
@ -32,7 +35,7 @@ We expect a **fleet of Raspberry Pi 5** boards to act as **controllers and actua
## ESP32, IEEE 802.11, and advanced telemetry (planned)
Separately from the Pi 5 **`iperf`** plane, we plan **ESP32**-based nodes for **advanced telemetry**, including rich use of **IEEE 802.11 (WiFi)**: airlink statistics, channel / PHY-adjacent metrics, retries, timing, and correlation with harness and inventory state. The goal is lab- and field-grade **802.11 telemetry**, not only GPIO or serial counters. **This stack is not implemented yet** in **`fiwicontrol`**; todays code is still the commands / lab / power packages above.
Separately from the Pi 5 **`iperf`** plane, we plan **ESP32**-based nodes for **advanced telemetry**, including rich use of **IEEE 802.11 (WiFi)**: airlink statistics, channel / PHY-adjacent metrics, retries, timing, and correlation with harness and inventory state. The goal is lab- and field-grade **802.11 telemetry**, not only GPIO or serial counters. **This stack is not implemented yet** in **`fiwicontrol`**; todays packages are **commands**, **lab**, **fronthaul**, **telemetry** (fronthaul slice), **radio**, **power**, **flows**, and **spc** above.
## `ush` expansion (planned)
@ -55,6 +58,9 @@ FiWiControl/
├── LICENSE
├── README.md
├── pyproject.toml
├── scripts/
│ ├── setup_pi_power.sh
│ └── system/
├── html/
│ └── Fi-Wi-L4S.html
├── docs/
@ -70,11 +76,21 @@ FiWiControl/
│ │ ├── __main__.py
│ │ ├── node_control.py
│ │ └── remote_setup.py
│ ├── fronthaul/
│ │ ├── __init__.py
│ │ └── link.py
│ ├── lab/
│ │ ├── __init__.py
│ │ ├── discovery.py
│ │ ├── inventory_config.py
│ │ └── inventory_verify.py
│ ├── radio/
│ │ ├── __init__.py
│ │ └── radiohead.py
│ ├── telemetry/
│ │ ├── __init__.py
│ │ ├── common.py
│ │ └── fronthaul.py
│ ├── power/
│ │ ├── __init__.py
│ │ ├── __main__.py
@ -122,6 +138,10 @@ from fiwicontrol.commands import ssh_node, Command, CommandManager
## Tests
**`tests/`** — **`pytest`** package tests (CI-oriented, import smoke, gated remote tests, etc.).
**`scripts/system/`** — **manual or long-running harness scripts** that exercise real hardware (e.g. fronthaul **PCIe hot-swap** campaigns). Not part of the default **`pytest`** tree; run explicitly when the bench is wired.
After install, use the ordered **verification checklist** and **INI reference** in **`docs/power-control-and-inventory.md`** (sections **“Verification checklist (after install)”** and **“Lab INI file reference”**).
Commands, example **`pytest`** output, and **`unittest`** entry points for **`node_control`**: **`docs/node-control-asyncio-design.md`** → section **“Running tests”** (top-level `##` heading near the top of the file).

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "fiwicontrol"
version = "0.1.0"
description = "FiWiControl repo: commands (SSH/ush), lab, power, flows (async iperf), spc (Shewhart / Hotelling)."
description = "FiWiControl repo: commands (SSH/ush), lab, fronthaul, telemetry, radio, power, flows (async iperf), spc (Shewhart / Hotelling)."
readme = "README.md"
requires-python = ">=3.11"
license = { file = "LICENSE" }

0
scripts/system/.gitkeep Normal file
View File

View File

@ -0,0 +1,9 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Fronthaul link model (concentrator ↔ RRH). Telemetry lives under :mod:`fiwicontrol.telemetry`."""
from fiwicontrol.fronthaul.link import FrontHaul
__all__ = ["FrontHaul"]

View File

@ -0,0 +1,41 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Logical concentrator→RRH fronthaul link identity (medium + PCIe ids today)."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from fiwicontrol.telemetry.fronthaul import FrontHaulTelemetry
@dataclass
class FrontHaul:
"""Physical / logical link from concentrator host toward the radio head.
FiWi uses a **fronthaul** between concentrator and RRH. Today that is **PCIe**;
other media can reuse this type with a different ``medium`` and id fields as
documented for that transport.
Attach :class:`~fiwicontrol.telemetry.fronthaul.FrontHaulTelemetry` for host
retimer / cable / SFP readouts (production or lab).
"""
#: Transport name, e.g. ``"pcie"`` (default); future values are host-defined.
medium: str = "pcie"
#: 16-bit vendor id when ``medium == "pcie"`` (configuration space).
vendor_id: int | None = None
#: 16-bit device id when ``medium == "pcie"``.
device_id: int | None = None
#: Subsystem vendor id when ``medium == "pcie"``.
subsystem_vendor_id: int | None = None
#: Subsystem device id when ``medium == "pcie"``.
subsystem_device_id: int | None = None
#: Link / power-management state strings (e.g. ``("L0", "D0")``); host-defined.
link_states: tuple[str, ...] = ()
#: Latest fronthaul telemetry sample from the host (optional).
telemetry: FrontHaulTelemetry | None = None

View File

@ -2,7 +2,11 @@
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Lab USB topology: BrainStem / Monsoon discovery and INI inventory."""
"""Lab-focused USB topology and INI inventory (discovery, load, verify).
Radio models live in :mod:`fiwicontrol.radio`. Fronthaul link and telemetry live in
:mod:`fiwicontrol.fronthaul` and :mod:`fiwicontrol.telemetry`.
"""
from fiwicontrol.lab.discovery import (
MONSOON_USB_PIDS,

View File

@ -14,8 +14,8 @@ class Power:
"""
Bench power facet: switch Acroname hub ports and read Monsoon electricals.
Intended for use as ``radiohead[id].power`` with methods ``.on()``, ``.off()``,
``.voltage()``, ``.current()``, ``.watts()``.
Attach to :class:`fiwicontrol.radio.radiohead.RadioHead` as ``head.power``; methods
``.on()``, ``.off()``, ``.voltage()``, ``.current()``, ``.watts()``.
"""
def __init__(

View File

@ -0,0 +1,9 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Logical radio / RRH models (lab and production concentrator)."""
from fiwicontrol.radio.radiohead import RadioHead
__all__ = ["RadioHead"]

View File

@ -0,0 +1,55 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Logical radio head: Wi-Fi, host integration, fronthaul, and optional bench ``Power``."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from fiwicontrol.fronthaul import FrontHaul
if TYPE_CHECKING:
from fiwicontrol.power.control import Power
@dataclass
class RadioHead:
"""Aggregate state for one logical FiWi radio / remote radio head (RRH-oriented).
Most fields are optional so probes and inventory can populate them incrementally.
Use :attr:`fronthaul` for the concentratorRRH link (PCIe today). Use :attr:`power`
to attach a :class:`fiwicontrol.power.Power` facade when this head maps to Acroname
/ Monsoon paths from the inventory INI.
"""
#: Stable logical id (e.g. inventory ``[node.*]`` key or RRH label).
radio_id: str
#: Patch panel port number for the copper/fiber hop to this head (plant layer).
patch_panel_port: int | None = None
#: Channel description from the driver or tooling (e.g. ``"6g80/160"``), if known.
chanspec: str | None = None
#: Center or channel carrier frequency in MHz, if known separately from ``chanspec``.
carrier_frequency_mhz: float | None = None
#: High-level power state (e.g. ``"on"``, ``"off"``, ``"psm"``); semantics are host-defined.
power_state: str | None = None
#: BSSID / AP MAC (often derived from the underlying 802.3-style station MAC).
bssid: str | None = None
#: Hostapd instance or control surface identifier (path, dbus name, etc.).
hostapd: str | None = None
#: Service set identifier broadcast for this BSS.
ssid: str | None = None
#: Associated client MAC addresses (or stable client ids) as reported by the AP stack.
associated_clients: list[str] = field(default_factory=list)
#: Vendor / chip name string from ``ethtool``, sysfs, or nl80211.
wifi_chip: str | None = None
#: WLAN firmware version string.
wifi_firmware: str | None = None
#: In-tree or staged driver name / version string.
wifi_driver: str | None = None
#: Concentrator→RRH fronthaul (PCIe link identity and state today).
fronthaul: FrontHaul | None = None
#: Optional bench power for this head (Acroname / Monsoon via :class:`~fiwicontrol.power.Power`).
power: Power | None = None

View File

@ -0,0 +1,29 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Telemetry schemas split by domain (fronthaul today; WiFi / FiWi later).
Import submodules explicitly (e.g. :mod:`fiwicontrol.telemetry.fronthaul`) or use
the re-exports below for the fronthaul bundle.
"""
from fiwicontrol.telemetry.fronthaul import (
CablePortStats,
FrontHaulTelemetry,
FronthaulBoardInfo,
PcieSlotTelemetry,
SfpModuleInventory,
SfpModuleTelemetry,
SystemPowerRails,
)
__all__ = [
"CablePortStats",
"FrontHaulTelemetry",
"FronthaulBoardInfo",
"PcieSlotTelemetry",
"SfpModuleInventory",
"SfpModuleTelemetry",
"SystemPowerRails",
]

View File

@ -0,0 +1,7 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Cross-domain telemetry helpers (timestamps, sources, envelopes) when shared code is needed."""
from __future__ import annotations

View File

@ -0,0 +1,127 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Fronthaul-side telemetry: retimer host readouts (e.g. Adnacom-style GUIs)."""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class FronthaulBoardInfo:
"""Device / retimer board row (e.g. ``1:0.0 H3`` header in Adnacom Monitor)."""
#: Linux PCI BDF of the bridge or retimer (e.g. ``"0000:01:00.0"``).
device_bdf: str | None = None
#: PCI class line as reported by tools (e.g. ``"PCI bridge: PLX Tech"``).
pci_class_text: str | None = None
board_name: str | None = None
#: e.g. ``"Host"`` vs ``"Device"`` mode string from firmware.
mode: str | None = None
hw_rev: str | None = None
fw_rev: str | None = None
board_serial: str | None = None
#: Switch ASIC string (e.g. ``"PEX8718"``).
pcie_switch: str | None = None
#: Optional 8-bit DIP pattern (LSB = switch 1); ``None`` if unknown.
dip_switch_mask: int | None = None
@dataclass
class PcieSlotTelemetry:
"""Upstream PCIe slot / edge status."""
port: int | None = None
capability: str | None = None
link_state: str | None = None
@dataclass
class CablePortStats:
"""One row of the **Cable** table (per fronthaul lane / retimer port)."""
port: int = 0
capability: str | None = None
status: str | None = None
training_ms: float | None = None
remote_up_ms: float | None = None
recovery: int | None = None
rx_errors: int | None = None
bad_tlp: int | None = None
bad_dllp: int | None = None
framing_error: int | None = None
retrain: int | None = None
link_down: int | None = None
@dataclass
class SfpModuleInventory:
"""Static / slow fields from the **SFP** inventory table."""
port: int = 0
present: bool | None = None
power_supply_v: float | None = None
power_supply_a: float | None = None
power_supply_w: float | None = None
state: str | None = None
rx_los: int | None = None
startup_ms: float | None = None
vendor: str | None = None
part_number: str | None = None
serial_number: str | None = None
technology: str | None = None
squelch_tx_implemented: bool | None = None
squelch_rx_implemented: bool | None = None
i2c_errors: int | None = None
@dataclass
class SfpModuleTelemetry:
"""Live optics fields (e.g. per-port gauges in Adnacom Monitor)."""
port: int = 0
temp_c: float | None = None
vcc_v: float | None = None
rx_power_uw: float | None = None
rx_power_dbm: float | None = None
tx_bias_ma: float | None = None
@dataclass
class SystemPowerRails:
"""Bottom **System** voltage / temperature row on the retimer card."""
tpex_c: float | None = None
topu_c: float | None = None
slot_3v3_v: float | None = None
slot_12v_v: float | None = None
slot_12v_a: float | None = None
slot_12v_w: float | None = None
slot_3v3aux_v: float | None = None
slot_3v3aux_a: float | None = None
slot_3v3aux_w: float | None = None
rail_3v3_v: float | None = None
rail_1v8sb_v: float | None = None
rail_1v8_v: float | None = None
rail_0v9_v: float | None = None
rail_0v9a_v: float | None = None
@dataclass
class FrontHaulTelemetry:
"""Host-side fronthaul telemetry (retimer GUI or JSON/API export).
Board / PCIe slot / cable / SFP / rail fields for one capture. Containers may
be empty when unknown. Intended for **lab and production** concentrator hosts.
"""
#: Provenance string (e.g. ``"Adnacom Monitor v1.0.0"``).
monitor_source: str | None = None
board: FronthaulBoardInfo | None = None
pcie_slot: PcieSlotTelemetry | None = None
cable_ports: tuple[CablePortStats, ...] = field(default_factory=tuple)
sfp_inventory: tuple[SfpModuleInventory, ...] = field(default_factory=tuple)
sfp_telemetry: tuple[SfpModuleTelemetry, ...] = field(default_factory=tuple)
system_power: SystemPowerRails | None = None

View File

@ -2,10 +2,75 @@ def test_import_subpackages() -> None:
import fiwicontrol
import fiwicontrol.commands
import fiwicontrol.flows
import fiwicontrol.fronthaul
import fiwicontrol.lab
import fiwicontrol.power
import fiwicontrol.radio
import fiwicontrol.spc
import fiwicontrol.telemetry
from fiwicontrol.fronthaul import FrontHaul
from fiwicontrol.radio import RadioHead
from fiwicontrol.telemetry import (
CablePortStats,
FrontHaulTelemetry,
FronthaulBoardInfo,
PcieSlotTelemetry,
SfpModuleInventory,
SfpModuleTelemetry,
SystemPowerRails,
)
assert fiwicontrol.__version__
assert "iperf" in (fiwicontrol.flows.__doc__ or "").lower()
assert "Shewhart" in (fiwicontrol.spc.__doc__ or "")
tel = FrontHaulTelemetry(
monitor_source="Adnacom Monitor v1.0.0",
board=FronthaulBoardInfo(
device_bdf="0000:01:00.0",
board_name="H3",
mode="Host",
hw_rev="1.0.1",
fw_rev="1.2.0",
board_serial="4C-0A-3D-62-13-13",
pcie_switch="PEX8718",
),
pcie_slot=PcieSlotTelemetry(port=0, capability="Gen3 x4", link_state="Gen3 x4"),
cable_ports=(
CablePortStats(port=1, capability="Gen2 x1", training_ms=17.0, remote_up_ms=3.0, recovery=3),
),
sfp_inventory=(
SfpModuleInventory(
port=1,
present=True,
power_supply_v=3.33,
state="On",
vendor="6COM",
part_number="6C-SFP+-LR, Rev.B",
serial_number="6C82510221225",
technology="1310 nm VCSEL",
),
),
sfp_telemetry=(SfpModuleTelemetry(port=1, temp_c=33.3, vcc_v=3.31, rx_power_uw=501.0, rx_power_dbm=-3.0, tx_bias_ma=28.9),),
system_power=SystemPowerRails(tpex_c=37.0, topu_c=31.0, slot_3v3_v=3.33, rail_3v3_v=3.33),
)
fh = FrontHaul(
medium="pcie",
vendor_id=0x1234,
device_id=0x5678,
link_states=("L0",),
telemetry=tel,
)
h = RadioHead(
radio_id="rrh-01",
patch_panel_port=17,
ssid="lab-bss",
bssid="02:00:00:00:01:00",
fronthaul=fh,
)
assert h.radio_id == "rrh-01"
assert h.patch_panel_port == 17
assert h.fronthaul is not None and h.fronthaul.medium == "pcie"
assert h.fronthaul.telemetry is not None
assert h.fronthaul.telemetry.monitor_source == "Adnacom Monitor v1.0.0"
assert h.fronthaul.telemetry.board is not None and h.fronthaul.telemetry.board.pcie_switch == "PEX8718"
assert h.associated_clients == []