diff --git a/setup.py b/setup.py index cb5de2303..8350763e9 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ 'sonic_platform_base.sonic_xcvr.mem_maps.public.cmis.pages', 'sonic_platform_base.sonic_xcvr.mem_maps.public.cmis.elsfp', 'sonic_platform_base.sonic_xcvr.mem_maps.public.cmis.elsfp.pages', + 'sonic_platform_base.sonic_xcvr.mem_maps.public.cmis_pages', 'sonic_platform_base.sonic_xcvr.api', 'sonic_platform_base.sonic_xcvr.api.public', 'sonic_platform_base.sonic_xcvr.api.broadcom', @@ -68,8 +69,12 @@ 'sonic_platform_base.sonic_xcvr.mem_maps.amphenol', 'sonic_platform_base.sonic_xcvr.api.broadcom', 'sonic_platform_base.sonic_xcvr.mem_maps.broadcom', + 'sonic_platform_base.sonic_xcvr.api.nvidia', + 'sonic_platform_base.sonic_xcvr.codes.nvidia', + 'sonic_platform_base.sonic_xcvr.mem_maps.nvidia', 'sonic_platform_base.sonic_xcvr.cdb', 'sonic_platform_base.sonic_xcvr.cpo', + 'sonic_platform_base.sonic_xcvr.cdb.nvidia', 'sonic_psu', 'sonic_sfp', 'sonic_thermal', diff --git a/sonic_platform_base/sonic_xcvr/api/cdb_capable_mixin.py b/sonic_platform_base/sonic_xcvr/api/cdb_capable_mixin.py new file mode 100644 index 000000000..e2fde7a64 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/api/cdb_capable_mixin.py @@ -0,0 +1,26 @@ +"""Mixin granting an XcvrApi subclass a lazy ``cdb_handler`` backed by an injected CdbMemMap. + +Subclasses must call ``self._init_cdb_mem_map(cdb_mem_map)`` from their own ``__init__``; +the mixin does not own ``__init__`` so it composes cleanly with any primary base. +""" + +from ..cdb.cdb import CdbCmdHandler + + +class CdbCapableMixin(object): + _cdb_mem_map = None + _cdb_handler = None + + def _init_cdb_mem_map(self, cdb_mem_map=None): + self._cdb_mem_map = cdb_mem_map + self._cdb_handler = None + + @property + def cdb_handler(self): + if getattr(self, '_cdb_mem_map', None) is None: + return None + if self._cdb_handler is None: + self._cdb_handler = CdbCmdHandler(self.xcvr_eeprom.reader, + self.xcvr_eeprom.writer, + self._cdb_mem_map) + return self._cdb_handler diff --git a/sonic_platform_base/sonic_xcvr/api/nvidia/__init__.py b/sonic_platform_base/sonic_xcvr/api/nvidia/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/api/nvidia/__init__.py @@ -0,0 +1 @@ + diff --git a/sonic_platform_base/sonic_xcvr/api/nvidia/cpo_els.py b/sonic_platform_base/sonic_xcvr/api/nvidia/cpo_els.py new file mode 100644 index 000000000..600a11442 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/api/nvidia/cpo_els.py @@ -0,0 +1,274 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + + +import logging + +from ..public.elsfp_cmis import ElsfpCmisApi +from ...fields import consts +from ...mem_maps.nvidia.cpo_els import ( + NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD, + NVIDIA_ELS_CUSTOM_MON_THRESHOLDS_FIELD, + NVIDIA_ELS_VOLTAGE_FIELD, + NVIDIA_ELS_VOLTAGE_THRESHOLDS_FIELD, +) +from ...cdb.nvidia.cpo_els_memmap import ( + CDB_READ_ELS_LASER_MONITORING_CMD, + ELS_LASER_MONITORING_CAP_MASK_ALL, + NUM_LASERS, + NVIDIA_CPO_ELS_LASER_HEALTH, + NVIDIA_CPO_ELS_LASER_MPD, + NVIDIA_CPO_ELS_MODULE_POWER, + NVIDIA_CPO_ELS_TEC_HEALTH, + NVIDIA_CPO_ELS_TEC_VOLTAGE, +) + +# Maps each per-laser CDB 0x9018 reply field-name prefix (0-indexed _ suffix) +# to the spec's els_* DOM_SENSOR prefix (1-indexed lane suffix, no underscore). +_ELS_LASER_DOM_SENSOR_RENAMES = ( + (NVIDIA_CPO_ELS_LASER_MPD, 'els_laser_mpd'), + (NVIDIA_CPO_ELS_TEC_VOLTAGE, 'els_tec_voltage_laser'), + (NVIDIA_CPO_ELS_LASER_HEALTH, 'els_health_value_laser'), + (NVIDIA_CPO_ELS_TEC_HEALTH, 'els_tec_health_value_laser'), +) + +logger = logging.getLogger(__name__) +logger.addHandler(logging.NullHandler()) + + +class NvidiaCpoElsCmisApi(ElsfpCmisApi): + + def get_els_status(self): + """ELS module state, fault cause, and VCC for TRANSCEIVER_STATUS. + + Sources: NVIDIA B0 mirror (state / fault cause / VCC at 0xB0:144). + """ + result = {} + + status = self.xcvr_eeprom.read(consts.TRANS_MODULE_STATUS_FIELD) + if status: + result['els_module_state'] = status.get(consts.MODULE_STATE, 'N/A') + result['els_module_fault_cause'] = status.get(consts.MODULE_FAULT_CAUSE, 'N/A') + else: + result['els_module_state'] = 'Unknown' + result['els_module_fault_cause'] = 'Unknown' + + vcc = self.xcvr_eeprom.read(consts.VOLTAGE_FIELD) + result['els_vcc'] = vcc if vcc is not None else 'N/A' + + return result + + def get_els_dom_sensors(self): + """ELS temperature + voltage from the standard CMIS slots (page 0x00). + + The B0 mirror is for the get_els_status / els_vcc path; these come + from the actual standard slots (CUSTOM_MON @ 24 and VOLTAGE @ 16). + """ + result = {} + + mons = self.xcvr_eeprom.read(NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD) + if mons: + result['els_temperature'] = mons.get(consts.CUSTOM_MON, 'N/A') + + volts = self.xcvr_eeprom.read(NVIDIA_ELS_VOLTAGE_FIELD) + if volts: + result['els_voltage'] = volts.get(consts.VOLTAGE_FIELD, 'N/A') + + return result + + def get_els_cmon_temp_thresholds(self): + """ELS temperature thresholds from the standard Custom Monitor slot (0x02:168-175).""" + thresholds = self.xcvr_eeprom.read(NVIDIA_ELS_CUSTOM_MON_THRESHOLDS_FIELD) + if not thresholds: + return { + 'els_temphighalarm': 'N/A', + 'els_templowalarm': 'N/A', + 'els_temphighwarning': 'N/A', + 'els_templowwarning': 'N/A', + } + + return { + 'els_temphighalarm': thresholds.get(consts.CUSTOM_MON_HIGH_ALARM, 'N/A'), + 'els_templowalarm': thresholds.get(consts.CUSTOM_MON_LOW_ALARM, 'N/A'), + 'els_temphighwarning': thresholds.get(consts.CUSTOM_MON_HIGH_WARN, 'N/A'), + 'els_templowwarning': thresholds.get(consts.CUSTOM_MON_LOW_WARN, 'N/A'), + } + + def get_els_dom_flags(self): + """ELS custom-monitor alarm/warning flag bits (B0:139, MODULE_FLAG_BYTE3).""" + flag_byte = self.xcvr_eeprom.read(consts.MODULE_FLAG_BYTE3) + if flag_byte is None: + return {} + return { + 'els_custom_mon_high_alarm': bool((flag_byte >> 4) & 1), + 'els_custom_mon_low_alarm': bool((flag_byte >> 5) & 1), + 'els_custom_mon_high_warning': bool((flag_byte >> 6) & 1), + 'els_custom_mon_low_warning': bool((flag_byte >> 7) & 1), + } + + def get_els_info(self): + """ELS identity (B1), type info (B0/B1), and HW rev (B2).""" + result = {} + + admin = self.xcvr_eeprom.read(consts.ADMIN_INFO_FIELD) + if admin: + result['els_manufacturer'] = admin.get(consts.VENDOR_NAME_FIELD, 'N/A') + result['els_vendor_oui'] = admin.get(consts.VENDOR_OUI_FIELD, 'N/A') + result['els_model'] = admin.get(consts.VENDOR_PART_NO_FIELD, 'N/A') + result['els_vendor_rev'] = admin.get(consts.VENDOR_REV_FIELD, 'N/A') + result['els_serial'] = admin.get(consts.VENDOR_SERIAL_NO_FIELD, 'N/A') + result['els_vendor_date'] = admin.get(consts.VENDOR_DATE_FIELD, 'N/A') + result['els_connector'] = admin.get(consts.CONNECTOR_FIELD, 'N/A') + result['els_type'] = admin.get(consts.ID_FIELD, 'N/A') + result['els_type_abbrv_name'] = admin.get(consts.ID_ABBRV_FIELD, 'N/A') + major = admin.get(consts.CMIS_MAJOR_REVISION) + minor = admin.get(consts.CMIS_MINOR_REVISION) + result['els_cmis_rev'] = ( + f"{major}.{minor}" if major is not None and minor is not None else 'N/A') + + + ext_id = admin.get(consts.EXT_ID_FIELD) + if isinstance(ext_id, dict): + result['els_ext_identifier'] = ext_id.get(consts.POWER_CLASS_FIELD, 'N/A') + else: + result['els_ext_identifier'] = 'N/A' + + # cable_type has no standard CMIS counterpart and is not on the B1 mirror. + result['els_cable_type'] = 'N/A' + else: + result['els_manufacturer'] = 'Unknown' + result['els_vendor_oui'] = 'Unknown' + result['els_model'] = 'Unknown' + result['els_vendor_rev'] = 'Unknown' + result['els_serial'] = 'Unknown' + result['els_vendor_date'] = 'Unknown' + result['els_connector'] = 'Unknown' + result['els_type'] = 'Unknown' + result['els_type_abbrv_name'] = 'Unknown' + result['els_cmis_rev'] = 'Unknown' + result['els_ext_identifier'] = 'Unknown' + result['els_cable_type'] = 'Unknown' + result['els_hardware_rev'] = 'Unknown' + + adv = self.xcvr_eeprom.read(consts.ADVERTISING_FIELD) + if adv: + major = adv.get(consts.HW_MAJOR_REV, 0) + minor = adv.get(consts.HW_MINOR_REV, 0) + result['els_hardware_rev'] = f"{major}.{minor}" + else: + result['els_hardware_rev'] = 'Unknown' + + return result + + def get_els_thresholds(self): + """ELS supply-voltage thresholds from the standard CMIS slot (0x02:136-143).""" + data = self.xcvr_eeprom.read(NVIDIA_ELS_VOLTAGE_THRESHOLDS_FIELD) + if not data: + return { + 'els_vcchighalarm': 'N/A', + 'els_vcclowalarm': 'N/A', + 'els_vcchighwarning': 'N/A', + 'els_vcclowwarning': 'N/A', + } + return { + 'els_vcchighalarm': data.get(consts.VOLTAGE_HIGH_ALARM_FIELD, 'N/A'), + 'els_vcclowalarm': data.get(consts.VOLTAGE_LOW_ALARM_FIELD, 'N/A'), + 'els_vcchighwarning': data.get(consts.VOLTAGE_HIGH_WARNING_FIELD, 'N/A'), + 'els_vcclowwarning': data.get(consts.VOLTAGE_LOW_WARNING_FIELD, 'N/A'), + } + + def get_els_laser_monitoring(self, cap_mask=ELS_LASER_MONITORING_CAP_MASK_ALL, + laser_mask=0x00): + """CDB 0x9018: read NVIDIA ELS laser monitoring block (raw decoded reply).""" + if self.cdb_handler is None: + return {} + + payload = { + "cap_mask": cap_mask, + "bank_id": self.bank_id, + "laser_mask": laser_mask, + } + + try: + ok = self.cdb_handler.send_cmd(CDB_READ_ELS_LASER_MONITORING_CMD, + payload=payload) + except Exception: + logger.exception("CDB 0x%04x send failed", + CDB_READ_ELS_LASER_MONITORING_CMD) + return {} + if ok is not True: + logger.warning("CDB 0x%04x returned non-success: %r", + CDB_READ_ELS_LASER_MONITORING_CMD, ok) + return {} + + try: + return self.cdb_handler.read_reply(CDB_READ_ELS_LASER_MONITORING_CMD) or {} + except Exception: + logger.exception("CDB 0x%04x read_reply failed", + CDB_READ_ELS_LASER_MONITORING_CMD) + return {} + + def get_els_laser_dom_sensors(self): + """Spec-aligned DOM_SENSOR projection of the CDB 0x9018 reply. + + Renames per-laser CDB field-name prefixes to els_* spec prefixes + (lane index 1-indexed, no underscore) and surfaces module power + as ``els_power_consumption``. Cap/bank/mask framing bytes are dropped. + """ + raw = self.get_els_laser_monitoring() + if not raw: + return {} + result = {} + for src_prefix, dst_prefix in _ELS_LASER_DOM_SENSOR_RENAMES: + for lane in range(NUM_LASERS): + src_key = f"{src_prefix}_{lane}" + if src_key in raw: + result[f"{dst_prefix}{lane + 1}"] = raw[src_key] + if NVIDIA_CPO_ELS_MODULE_POWER in raw: + result['els_power_consumption'] = raw[NVIDIA_CPO_ELS_MODULE_POWER] + return result + + def get_transceiver_info(self): + result = super().get_transceiver_info() + result.update(self.get_els_info()) + return result + + def get_transceiver_dom_real_value(self): + result = super().get_transceiver_dom_real_value() + result.update(self.get_els_dom_sensors()) + result.update(self.get_els_laser_dom_sensors()) + return result + + def get_transceiver_threshold_info(self): + result = super().get_transceiver_threshold_info() + result.update(self.get_els_thresholds()) + result.update(self.get_els_cmon_temp_thresholds()) + return result + + def get_transceiver_dom_flags(self): + result = super().get_transceiver_dom_flags() + result.update(self.get_els_dom_flags()) + return result + + def get_transceiver_status(self): + result = super().get_transceiver_status() + result.update(self.get_els_status()) + return result diff --git a/sonic_platform_base/sonic_xcvr/api/nvidia/cpo_oe.py b/sonic_platform_base/sonic_xcvr/api/nvidia/cpo_oe.py new file mode 100644 index 000000000..9db202797 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/api/nvidia/cpo_oe.py @@ -0,0 +1,74 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + + +import logging + +from ..public.cmis import CmisApi +from ...cdb.nvidia.cpo_oe_memmap import ( + CDB_READ_OE_TELEMETRY_CMD, + OE_TELEMETRY_REQUEST_MASK_ALL, +) + +logger = logging.getLogger(__name__) +logger.addHandler(logging.NullHandler()) + + +class NvidiaCpoOeCmisApi(CmisApi): + + def _get_bank_id(self): + return getattr(self.xcvr_eeprom.mem_map, 'bank', 0) + + def get_oe_telemetry(self, request_mask=OE_TELEMETRY_REQUEST_MASK_ALL): + """CDB 0x9030: read NVIDIA OE telemetry block. + + Returns the raw decoded reply dict (per-lane fields are suffixed + ``_0``..``_7``). Cap/valid-bit interpretation is the consumer's job. + Returns ``None`` if CDB is unavailable or the command fails. + """ + if self.cdb_handler is None: + return None + + payload = {"bank_id": self._get_bank_id(), "request_mask": request_mask} + + try: + ok = self.cdb_handler.send_cmd(CDB_READ_OE_TELEMETRY_CMD, payload=payload) + except Exception: + logger.exception("CDB 0x%04x send failed", CDB_READ_OE_TELEMETRY_CMD) + return None + if ok is not True: + logger.warning("CDB 0x%04x returned non-success: %r", + CDB_READ_OE_TELEMETRY_CMD, ok) + return None + + try: + return self.cdb_handler.read_reply(CDB_READ_OE_TELEMETRY_CMD) + except Exception: + logger.exception("CDB 0x%04x read_reply failed", CDB_READ_OE_TELEMETRY_CMD) + return None + + def get_transceiver_vdm_real_value(self): + """Standard CMIS VDM real values overlaid with the NVIDIA OE telemetry block.""" + result = super().get_transceiver_vdm_real_value() or {} + oe = self.get_oe_telemetry() + if oe: + result.update(oe) + return result or None diff --git a/sonic_platform_base/sonic_xcvr/api/public/cmis.py b/sonic_platform_base/sonic_xcvr/api/public/cmis.py index 9e0784a45..fbc7c08f4 100644 --- a/sonic_platform_base/sonic_xcvr/api/public/cmis.py +++ b/sonic_platform_base/sonic_xcvr/api/public/cmis.py @@ -14,6 +14,11 @@ from ...codes.public.cdb import CdbCodes from ...codes.public.sff8024 import Sff8024 from ...mem_maps.public.cmis.cdb import CdbMemMap +from ...fields import consts +from ...cdb.cdb_fw import CdbFwHandler as CdbFw +from ..xcvr_api import XcvrApi +from ..cdb_capable_mixin import CdbCapableMixin +from .cmisCDB import CmisCdbApi from .cmisVDM import CmisVdmApi import time import copy @@ -55,6 +60,9 @@ class VdmSubtypeIndex(Enum): } CMIS_VDM_KEY_TO_DB_PREFIX_KEY_MAP = { + "Laser Age [%]" : "laser_age", + "TEC Current [%]" : "tec_current", + "Laser Frequency Error [MHz]" : "laser_frequency_err", "Laser Temperature [C]" : "laser_temperature_media", "eSNR Media Input [dB]" : "esnr_media_input", "PAM4 Level Transition Parameter Media Input [dB]" : "pam4_level_transition_media_input", @@ -75,7 +83,8 @@ class VdmSubtypeIndex(Enum): "Errored Frames Minimum Host Input" : "errored_frames_min_host_input", "Errored Frames Maximum Host Input" : "errored_frames_max_host_input", "Errored Frames Average Host Input" : "errored_frames_avg_host_input", - "Errored Frames Current Value Host Input" : "errored_frames_curr_host_input" + "Errored Frames Current Value Host Input" : "errored_frames_curr_host_input", + "ELS Input Power [dBm]" : "els_input_power" } CMIS_XCVR_INFO_DEFAULT_DICT = { @@ -105,7 +114,7 @@ class VdmSubtypeIndex(Enum): "vdm_supported": "N/A" } -class CmisApi(CmisCdbFw, XcvrApi): +class CmisApi(XcvrApi, CdbCapableMixin): NUM_CHANNELS = 8 LowPwrRequestSW = 4 LowPwrAllowRequestHW = 6 @@ -134,8 +143,9 @@ def set_cache_enabled(cls, enabled: bool): """ cls.cache_enabled = bool(enabled) - def __init__(self, xcvr_eeprom, init_cdb_fw_handler=False): + def __init__(self, xcvr_eeprom, init_cdb_fw_handler=False, cdb_mem_map=None): super(CmisApi, self).__init__(xcvr_eeprom) + self._init_cdb_mem_map(cdb_mem_map) self.vdm = CmisVdmApi(xcvr_eeprom) if not self.is_flat_memory() else None self._init_cdb_fw_handler = init_cdb_fw_handler self._cdb_fw_hdlr = None diff --git a/sonic_platform_base/sonic_xcvr/api/public/cpo.py b/sonic_platform_base/sonic_xcvr/api/public/cpo.py new file mode 100644 index 000000000..80d6beee1 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/api/public/cpo.py @@ -0,0 +1,44 @@ +"""XcvrApi for Co-Packaged Optics (CPO) modules: aggregates an OE bank and an ELS bank.""" + +from .cmis import CmisApi + + +class CpoApi(CmisApi): + """CPO XcvrApi composing an optical-engine API with an external-laser-source API. + + Each ``get_transceiver_*`` method merges OE and ELS results into a freshly + owned dict (a shallow copy of the OE result) so the backend dicts are never + mutated; ELS keys win on collisions. ``None`` from either bank is treated as + no contribution (so a missing CMIS page on one side does not suppress the + other). + """ + + def __init__(self, optical_engine_xcvr_api, external_laser_source_xcvr_api) -> None: + super().__init__(optical_engine_xcvr_api.xcvr_eeprom) + self.optical_engine_xcvr_api = optical_engine_xcvr_api + self.external_laser_source_xcvr_api = external_laser_source_xcvr_api + + def _merge(self, method_name): + result = dict(getattr(self.optical_engine_xcvr_api, method_name)() or {}) + els_data = getattr(self.external_laser_source_xcvr_api, method_name)() + if els_data: + result.update(els_data) + return result + + def get_transceiver_info(self): + return self._merge('get_transceiver_info') + + def get_transceiver_dom_real_value(self): + return self._merge('get_transceiver_dom_real_value') + + def get_transceiver_threshold_info(self): + return self._merge('get_transceiver_threshold_info') + + def get_transceiver_dom_flags(self): + return self._merge('get_transceiver_dom_flags') + + def get_transceiver_status(self): + return self._merge('get_transceiver_status') + + def get_transceiver_status_flags(self): + return self._merge('get_transceiver_status_flags') diff --git a/sonic_platform_base/sonic_xcvr/api/public/elsfp_cmis.py b/sonic_platform_base/sonic_xcvr/api/public/elsfp_cmis.py new file mode 100644 index 000000000..0125179a1 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/api/public/elsfp_cmis.py @@ -0,0 +1,236 @@ +"""XcvrApi for the ELSFP (External Laser Source) device, per OIF-CMIS-ELSFP. + +Strict ELSFP-only: inherits XcvrApi (not CmisApi). Vendor subclasses chain super() +on each aggregator and overlay vendor-specific extensions. All getters return dicts +with ``els_*``-prefixed keys so a joint-mode merge with an OE-side CmisApi is a +disjoint union. +""" + +import logging + +from ...fields import elsfp_consts as ec +from ..cdb_capable_mixin import CdbCapableMixin +from ..xcvr_api import XcvrApi + +logger = logging.getLogger(__name__) +logger.addHandler(logging.NullHandler()) + +NUM_ELSFP_LANES = 8 + + +class ElsfpCmisApi(XcvrApi, CdbCapableMixin): + NUM_CHANNELS = NUM_ELSFP_LANES + + def __init__(self, xcvr_eeprom, bank_id=0, cdb_mem_map=None): + super(ElsfpCmisApi, self).__init__(xcvr_eeprom) + self._init_cdb_mem_map(cdb_mem_map) + self.bank_id = bank_id + + @staticmethod + def _nested_get(decoded, group_key, leaf_key, default='N/A'): + """Fetch a per-lane leaf from a decoded RegGroupField. + + The ELSFP pages compose several groups as a RegGroupField whose members + are themselves RegGroupFields, so RegGroupField.decode returns a nested + dict (``decoded[group_key][leaf_key]``) rather than a flat one. This + helper resolves the leaf through the nested sub-dict, falling back to a + flat ``decoded[leaf_key]`` so callers stay correct regardless of whether + the leaf is nested or surfaced directly. + """ + sub = decoded.get(group_key) + if isinstance(sub, dict): + return sub.get(leaf_key, decoded.get(leaf_key, default)) + return decoded.get(leaf_key, default) + + def get_elsfp_lane_monitors(self): + """ELSFP 0x1B per-lane monitors for TRANSCEIVER_DOM_SENSOR.""" + monitors = self.xcvr_eeprom.read(ec.ELSFP_MONITORS_FIELD) + if monitors is None: + return None + result = {} + for lane in range(1, self.NUM_CHANNELS + 1): + mon_bias = f"{ec.BIAS_CURRENT_MONITOR_FIELD}{lane}" + mon_power = f"{ec.OPT_POWER_MONITOR_FIELD}{lane}" + mon_voltage = f"{ec.VOLTAGE_MONITOR_FIELD}{lane}" + result[f"els_bias_current_monitor{lane}"] = self._nested_get( + monitors, ec.BIAS_CURRENT_MONITOR_FIELD, mon_bias) + result[f"els_opt_power_monitor{lane}"] = self._nested_get( + monitors, ec.OPT_POWER_MONITOR_FIELD, mon_power) + result[f"els_voltage_monitor{lane}"] = self._nested_get( + monitors, ec.VOLTAGE_MONITOR_FIELD, mon_voltage) + result['els_icc'] = monitors.get(ec.ICC_MONITOR, 'N/A') + return result + + def get_elsfp_lane_thresholds(self): + """ELSFP 0x1A bias/power thresholds (Table 4).""" + adv = self.xcvr_eeprom.read(ec.ELSFP_MODULE_ADVERTISEMENTS_FIELD) + if adv is None: + return None + result = {} + fields = [ + ['els_bias', ec.BIAS_HIGH_ALARM, ec.BIAS_LOW_ALARM, ec.BIAS_HIGH_WARN, ec.BIAS_LOW_WARN, False], + ['els_optpower', ec.OPT_POWER_HIGH_ALARM, ec.OPT_POWER_LOW_ALARM, ec.OPT_POWER_HIGH_WARN, ec.OPT_POWER_LOW_WARN, True], + ] + + for item in fields: + prefix, const_ha, const_la, const_hw, const_lw, convert_dbm = item + ha = adv.get(const_ha, 'N/A') + la = adv.get(const_la, 'N/A') + hw = adv.get(const_hw, 'N/A') + lw = adv.get(const_lw, 'N/A') + if convert_dbm: + suffix_val_list = [ + ('highalarm', ha), + ('lowalarm', la), + ('highwarning', hw), + ('lowwarning', lw) + ] + for suffix, val in suffix_val_list: + if val != 'N/A' and val is not None: + result[f'{prefix}{suffix}'] = float("{:.3f}".format(self.mw_to_dbm(val))) + else: + result[f'{prefix}{suffix}'] = 'N/A' + else: + result[f'{prefix}highalarm'] = ha + result[f'{prefix}lowalarm'] = la + result[f'{prefix}highwarning'] = hw + result[f'{prefix}lowwarning'] = lw + + return result + + def get_elsfp_lane_flags(self): + """ELSFP 0x1A per-lane indexed alarm/warning flags (Table 7).""" + masks = self.xcvr_eeprom.read(ec.ELSFP_ALARMS_WARNINGS_MASKS_FIELD) + if masks is None: + return None + result = {} + flag_map = [ + (ec.HIGH_BIAS_ALARM_INDEXED_FIELD, f'els_HighBiasAlarm'), + (ec.LOW_BIAS_ALARM_INDEXED_FIELD, f'els_LowBiasAlarm'), + (ec.HIGH_BIAS_WARN_INDEXED_FIELD, f'els_HighBiasWarn'), + (ec.LOW_BIAS_WARN_INDEXED_FIELD, f'els_LowBiasWarn'), + (ec.HIGH_POWER_ALARM_INDEXED_FIELD, f'els_HighPowerAlarm'), + (ec.LOW_POWER_ALARM_INDEXED_FIELD, f'els_LowPowerAlarm'), + (ec.HIGH_POWER_WARN_INDEXED_FIELD, f'els_HighPowerWarn'), + (ec.LOW_POWER_WARN_INDEXED_FIELD, f'els_LowPowerWarn'), + ] + for field_const, key_template in flag_map: + byte_val = masks.get(field_const, 0) + for lane in range(1, self.NUM_CHANNELS + 1): + result[f"{key_template}{lane}"] = bool((byte_val >> (lane - 1)) & 0x1) + return result + + def get_elsfp_lane_state(self): + """ELSFP 0x1A per-lane enable + state (Table 8) for TRANSCEIVER_STATUS.""" + ctrl = self.xcvr_eeprom.read(ec.ELSFP_LANE_CONTROLS_FIELD) + if ctrl is None: + return None + result = {} + enable_byte = ctrl.get(ec.LANE_ENABLE_FIELD, 0) + for lane in range(1, self.NUM_CHANNELS + 1): + result[f'els_lane_enable{lane}'] = (enable_byte >> (lane - 1)) & 1 + result[f'els_lane_state{lane}'] = self._nested_get( + ctrl, ec.LANE_STATE_FIELD, f"{ec.LANE_STATE_FIELD}{lane}") + return result + + def get_elsfp_output_fiber_checked(self): + """ELSFP 0x1A output-fiber-checked per-laser flags (byte 223).""" + fiber = self.xcvr_eeprom.read(ec.ELSFP_OUTPUT_FIBER_CHECKED_FIELD) + if fiber is None: + return None + fiber_byte = fiber.get(ec.OUTPUT_FIBER_CHECKED_FLAG_LANE_FIELD, 0) + return { + f'els_output_fiber_checked_flag_lane{lane}': + (fiber_byte >> (lane - 1)) & 1 + for lane in range(1, self.NUM_CHANNELS + 1) + } + + def get_elsfp_fault_warning_codes(self): + """ELSFP 0x1A per-lane 4-bit fault/warning codes (Table 7).""" + masks = self.xcvr_eeprom.read(ec.ELSFP_ALARMS_WARNINGS_MASKS_FIELD) + if masks is None: + return None + result = {} + for lane in range(1, self.NUM_CHANNELS + 1): + result[f'els_fault_code{lane}'] = self._nested_get( + masks, ec.FAULT_CODE_FIELD, f"{ec.FAULT_CODE_FIELD}{lane}") + result[f'els_warning_code{lane}'] = self._nested_get( + masks, ec.WARNING_CODE_FIELD, f"{ec.WARNING_CODE_FIELD}{lane}") + return result + + def get_elsfp_lane_setpoints(self): + """ELSFP 0x1B per-lane bias-current / opt-power setpoints for TRANSCEIVER_DOM_THRESHOLD_INFO.""" + sp = self.xcvr_eeprom.read(ec.ELSFP_SETPOINTS_FIELD) + if sp is None: + return None + result = {} + for lane in range(1, self.NUM_CHANNELS + 1): + result[f'els_bias_current_setpoint{lane}'] = self._nested_get( + sp, ec.BIAS_CURRENT_SETPOINT_FIELD, + f"{ec.BIAS_CURRENT_SETPOINT_FIELD}{lane}") + result[f'els_opt_power_setpoint{lane}'] = self._nested_get( + sp, ec.OPT_POWER_SETPOINT_FIELD, + f"{ec.OPT_POWER_SETPOINT_FIELD}{lane}") + return result + + def get_elsfp_control_mode(self): + """ELSFP 0x1A APC/ACC control mode for TRANSCEIVER_STATUS.""" + adv = self.xcvr_eeprom.read(ec.ELSFP_MODULE_ADVERTISEMENTS_FIELD) + if not adv: + return { + 'els_control_mode_APCACC': 'N/A' + } + return { + 'els_control_mode_APCACC': self._nested_get( + adv, ec.CONTROL_MODE_AND_LANE_COUNT, ec.CONTROL_MODE_APC_ACC), + } + + def get_elsfp_status_flags(self): + """ELSFP 0x1A non-banked summary fault/warn flag bytes for this bank. + + Bytes 166-169 hold per-lane fault flags for absolute lanes 1-32; bytes + 174-177 the matching warning flags. The relevant 8 lanes are selected + by self.bank_id and remapped to relative lanes 1-8 in the result. + """ + faults = self.xcvr_eeprom.read(ec.ELSFP_LANE_FAULTS_WARNINGS_FIELD) + if faults is None: + return None + result = {} + lane_start = self.bank_id * 8 + 1 + lane_end = lane_start + 8 + for abs_lane in range(lane_start, lane_end): + rel_lane = abs_lane - lane_start + 1 + fault_key = f"FaultFlagLane{abs_lane}" + warn_key = f"WarnFlagLane{abs_lane}" + result[f'els_fault_flag_lane{rel_lane}'] = self._nested_get( + faults, ec.FAULT_FLAG_LANE_FIELD, fault_key) + result[f'els_warn_flag_lane{rel_lane}'] = self._nested_get( + faults, ec.WARN_FLAG_LANE_FIELD, warn_key) + return result + + def get_transceiver_info(self): + return {} + + def get_transceiver_dom_real_value(self): + result = self.get_elsfp_lane_monitors() or {} + return result + + def get_transceiver_threshold_info(self): + result = self.get_elsfp_lane_thresholds() or {} + result.update(self.get_elsfp_lane_setpoints() or {}) + return result + + def get_transceiver_dom_flags(self): + result = self.get_elsfp_lane_flags() or {} + return result + + def get_transceiver_status(self): + result = self.get_elsfp_lane_state() or {} + result.update(self.get_elsfp_control_mode() or {}) + result.update(self.get_elsfp_fault_warning_codes() or {}) + return result + + def get_transceiver_status_flags(self): + result = self.get_elsfp_status_flags() or {} + result.update(self.get_elsfp_output_fiber_checked() or {}) + return result diff --git a/sonic_platform_base/sonic_xcvr/cdb/nvidia/__init__.py b/sonic_platform_base/sonic_xcvr/cdb/nvidia/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_els_codes.py b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_els_codes.py new file mode 100644 index 000000000..57d4aeb45 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_els_codes.py @@ -0,0 +1,26 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +from ...codes.public.cdb import CdbCodes + + +class NvidiaCpoElsCdbCodes(CdbCodes): + pass diff --git a/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_els_memmap.py b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_els_memmap.py new file mode 100644 index 000000000..6d5bb8b99 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_els_memmap.py @@ -0,0 +1,182 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +import struct + +from ...fields import cdb_consts +from ...fields.scale_consts import ( + SCALE_0P1W_TO_W, + SCALE_5MV_TO_MV, + SCALE_UA_TO_MA, +) +from ...fields.xcvr_field import ( + NumberRegField, + RegGroupField, +) +from ...mem_maps.public.cdb import CDBCommand, CdbMemMap + + +CDB_READ_ELS_LASER_MONITORING_CMD = 0x9018 + +NVIDIA_CPO_ELS_LASER_MONITORING_REPLY = "NvidiaCpoElsLaserMonitoringReply" + +NVIDIA_CPO_ELS_LASER_MON_CAP = "NvidiaCpoElsLaserMonCap" +NVIDIA_CPO_ELS_LASER_MON_BANK = "NvidiaCpoElsLaserMonBank" +NVIDIA_CPO_ELS_LASER_MON_MASK = "NvidiaCpoElsLaserMonMask" +NVIDIA_CPO_ELS_MODULE_POWER = "NvidiaCpoElsModulePower" + +NVIDIA_CPO_ELS_LASER_MPD = "NvidiaCpoElsLaserMpd" +NVIDIA_CPO_ELS_TEC_VOLTAGE = "NvidiaCpoElsTecVoltage" +NVIDIA_CPO_ELS_LASER_HEALTH = "NvidiaCpoElsLaserHealth" +NVIDIA_CPO_ELS_TEC_HEALTH = "NvidiaCpoElsTecHealth" + +# LPL reply offsets relative to RPL_DATA_START_OFFSET (page 0x9F byte 136). +_OFF_MON_CAP = 0 +_OFF_MON_BANK = 1 +_OFF_MON_MASK = 2 +# offset 3: reserved +_OFF_LASER_MPD = 4 +_OFF_TEC_VOLTAGE = 20 +_OFF_LASER_HEALTH = 36 +_OFF_TEC_HEALTH = 44 +_OFF_MODULE_POWER = 52 + +NUM_LASERS = 8 + +ELS_LASER_MON_CAP_BIT_LASER_MPD = 0x01 +ELS_LASER_MON_CAP_BIT_TEC_VOLTAGE = 0x02 +ELS_LASER_MON_CAP_BIT_LASER_HEALTH = 0x04 +ELS_LASER_MON_CAP_BIT_TEC_HEALTH = 0x08 +ELS_LASER_MON_CAP_BIT_MODULE_POWER = 0x10 + +ELS_LASER_MONITORING_CAP_MASK_ALL = ( + ELS_LASER_MON_CAP_BIT_LASER_MPD + | ELS_LASER_MON_CAP_BIT_TEC_VOLTAGE + | ELS_LASER_MON_CAP_BIT_LASER_HEALTH + | ELS_LASER_MON_CAP_BIT_TEC_HEALTH + | ELS_LASER_MON_CAP_BIT_MODULE_POWER +) + +# Scale factors are named constants in fields.scale_consts. +# CDB 0x9018 reply layout: +# laser MPD -> SCALE_UA_TO_MA (1 uA/LSB, decoded in mA) +# TEC voltage -> no scale (raw u16; unit under review) +# laser health -> SCALE_5MV_TO_MV (5 mV/LSB, decoded in mV) +# TEC health -> SCALE_5MV_TO_MV (5 mV/LSB, decoded in mV) +# module power -> SCALE_0P1W_TO_W (0.1 W/LSB, decoded in W) + + +def _lane_name(prefix, lane): + return f"{prefix}_{lane}" + + +def _lpl_addr(getaddr, byte_offset): + return getaddr(cdb_consts.LPL_PAGE, + cdb_consts.RPL_DATA_START_OFFSET + byte_offset) + + +def _build_u16_per_lane_fields(getaddr, prefix, base_offset, scale=None): + return [ + NumberRegField( + _lane_name(prefix, lane), + _lpl_addr(getaddr, base_offset + lane * 2), + size=2, format=">H", + scale=scale, + ) + for lane in range(NUM_LASERS) + ] + + +def _build_u8_per_lane_fields(getaddr, prefix, base_offset, scale=None): + return [ + NumberRegField( + _lane_name(prefix, lane), + _lpl_addr(getaddr, base_offset + lane), + size=1, format="B", + scale=scale, + ) + for lane in range(NUM_LASERS) + ] + + +class CdbReadElsLaserMonitoring(CDBCommand): + """CDB 0x9018 -- read ELS laser monitoring (LPL reply only). + + Payload contract: ``send_cmd(cmd_id, payload={"cap_mask": int, "bank_id": int, "laser_mask": int})``. + Per-lane reply keys use the internal ``_`` (0-indexed) suffix. + """ + def __init__(self, + cmd_id=CDB_READ_ELS_LASER_MONITORING_CMD, + reply_field=NVIDIA_CPO_ELS_LASER_MONITORING_REPLY): + super(CdbReadElsLaserMonitoring, self).__init__( + cmd_id, + epl=0, + lpl=3, + rpl_field=reply_field, + ) + + def encode(self, payload): + cap_mask = payload.get("cap_mask", ELS_LASER_MONITORING_CAP_MASK_ALL) & 0xFF + bank_id = payload.get("bank_id", 0) & 0xFF + laser_mask = payload.get("laser_mask", 0) & 0xFF + lpl_data = struct.pack("BBB", cap_mask, bank_id, laser_mask) + return super(CdbReadElsLaserMonitoring, self).encode(payload=lpl_data) + + +class NvidiaCpoElsCdbMemMap(CdbMemMap): + def __init__(self, codes): + super(NvidiaCpoElsCdbMemMap, self).__init__(codes) + + getaddr = self.getaddr + + reply_fields = [ + NumberRegField(NVIDIA_CPO_ELS_LASER_MON_CAP, + _lpl_addr(getaddr, _OFF_MON_CAP), + size=1, format="B"), + NumberRegField(NVIDIA_CPO_ELS_LASER_MON_BANK, + _lpl_addr(getaddr, _OFF_MON_BANK), + size=1, format="B"), + NumberRegField(NVIDIA_CPO_ELS_LASER_MON_MASK, + _lpl_addr(getaddr, _OFF_MON_MASK), + size=1, format="B"), + ] + reply_fields += _build_u16_per_lane_fields( + getaddr, NVIDIA_CPO_ELS_LASER_MPD, _OFF_LASER_MPD, + scale=SCALE_UA_TO_MA) + reply_fields += _build_u16_per_lane_fields( + getaddr, NVIDIA_CPO_ELS_TEC_VOLTAGE, _OFF_TEC_VOLTAGE) + reply_fields += _build_u8_per_lane_fields( + getaddr, NVIDIA_CPO_ELS_LASER_HEALTH, _OFF_LASER_HEALTH, + scale=SCALE_5MV_TO_MV) + reply_fields += _build_u8_per_lane_fields( + getaddr, NVIDIA_CPO_ELS_TEC_HEALTH, _OFF_TEC_HEALTH, + scale=SCALE_5MV_TO_MV) + reply_fields += [ + NumberRegField(NVIDIA_CPO_ELS_MODULE_POWER, + _lpl_addr(getaddr, _OFF_MODULE_POWER), + size=2, format=">H", + scale=SCALE_0P1W_TO_W), + ] + + self.nvidia_cpo_els_laser_monitoring_reply = RegGroupField( + NVIDIA_CPO_ELS_LASER_MONITORING_REPLY, *reply_fields) + + self.nvidia_cpo_els_read_laser_monitoring_cmd = CdbReadElsLaserMonitoring() diff --git a/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_oe_codes.py b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_oe_codes.py new file mode 100644 index 000000000..02d9d69f5 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_oe_codes.py @@ -0,0 +1,26 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +from ...codes.public.cdb import CdbCodes + + +class NvidiaCpoOeCdbCodes(CdbCodes): + pass diff --git a/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_oe_memmap.py b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_oe_memmap.py new file mode 100644 index 000000000..b147e87cd --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/cdb/nvidia/cpo_oe_memmap.py @@ -0,0 +1,204 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +import struct + +from ...fields import cdb_consts +from ...fields.xcvr_field import ( + NumberRegField, + RegBitsField, + RegGroupField, +) +from ...mem_maps.public.cdb import CDBCommand, CdbMemMap + + +CDB_READ_OE_TELEMETRY_CMD = 0x9030 +CDB_READ_OE_TELEMETRY_EPL_LEN = 168 + +NVIDIA_CPO_OE_TELEMETRY_REPLY = "NvidiaCpoOeTelemetryReply" + +NVIDIA_CPO_OE_TLM_CAPABILITY = "NvidiaCpoOeTlmCapability" +NVIDIA_CPO_OE_TLM_VALID = "NvidiaCpoOeTlmValid" +NVIDIA_CPO_OE_TLM_LANE_BANK_ECHO = "NvidiaCpoOeTlmLaneBankEcho" +NVIDIA_CPO_OE_TLM_VER_MAJOR = "NvidiaCpoOeTlmVerMajor" +NVIDIA_CPO_OE_TLM_VER_MINOR = "NvidiaCpoOeTlmVerMinor" +NVIDIA_CPO_OE_TLM_VER_RC = "NvidiaCpoOeTlmVerRc" + +NVIDIA_CPO_OE_TLM_TX_PS_LANE_STATE = "NvidiaCpoOeTlmTxPsLaneState" +NVIDIA_CPO_OE_TLM_RX_PS_LANE_STATE = "NvidiaCpoOeTlmRxPsLaneState" +NVIDIA_CPO_OE_TLM_TX_PS_LANE_SUB_STATE = "NvidiaCpoOeTlmTxPsLaneSubState" +NVIDIA_CPO_OE_TLM_RX_PS_LANE_SUB_STATE = "NvidiaCpoOeTlmRxPsLaneSubState" +NVIDIA_CPO_OE_TLM_TX_HS_DATA_RATE = "NvidiaCpoOeTlmTxHsDataRate" +NVIDIA_CPO_OE_TLM_RX_HS_DATA_RATE = "NvidiaCpoOeTlmRxHsDataRate" +NVIDIA_CPO_OE_TLM_LAST_REASON_OPCODE = "NvidiaCpoOeTlmLastReasonOpcode" + +# EPL absolute offsets (from EPL byte 0 = page 0xA0 byte 128) of each block. +_OFF_CAPABILITY = 0 +_OFF_VALID = 4 +_OFF_LANE_BANK_ECHO = 8 +_OFF_TX_PS_LANE_STATE = 43 +_OFF_RX_PS_LANE_STATE = 47 +_OFF_TX_PS_LANE_SUB_STATE = 51 +_OFF_RX_PS_LANE_SUB_STATE = 67 +_OFF_TX_HS_DATA_RATE = 83 +_OFF_RX_HS_DATA_RATE = 87 +_OFF_LAST_REASON_OPCODE = 154 +_OFF_VER_MAJOR = 162 +_OFF_VER_MINOR = 164 +_OFF_VER_RC = 166 + +NUM_LANES = 8 + +# Per-field cap-bit metadata gating each field. Cap-bit interpretation of the +# decoded reply is the consumer's job (cap & valid -> use value). +# Bits >= 18 are firmware-version slots that the MCU populates unconditionally. +_OE_TELEMETRY_CAP_BITS = (4, 5, 6, 7, 8, 9, 18, 19, 20, 21) + +OE_TELEMETRY_REQUEST_MASK_ALL = 0 +for _bit in _OE_TELEMETRY_CAP_BITS: + OE_TELEMETRY_REQUEST_MASK_ALL |= (1 << _bit) + + +def _lane_name(prefix, lane): + return f"{prefix}_{lane}" + + +def _build_nibble_packed_lane_fields(getaddr, prefix, base_offset, bit_width): + """4 NumberRegField parents (1 byte each) holding 2 RegBitsField lanes per byte. + + Even lanes occupy bits 0..bit_width-1; odd lanes bits 4..4+bit_width-1. + """ + fields = [] + for byte_idx in range(NUM_LANES // 2): + even_lane = byte_idx * 2 + odd_lane = byte_idx * 2 + 1 + fields.append( + NumberRegField( + f"{prefix}_byte{byte_idx}", + getaddr(cdb_consts.EPL_PAGE, 128 + base_offset + byte_idx), + RegBitsField(_lane_name(prefix, even_lane), bitpos=0, size=bit_width), + RegBitsField(_lane_name(prefix, odd_lane), bitpos=4, size=bit_width), + bitdecode=True, + ) + ) + return fields + + +def _build_u16_per_lane_fields(getaddr, prefix, base_offset): + return [ + NumberRegField( + _lane_name(prefix, lane), + getaddr(cdb_consts.EPL_PAGE, 128 + base_offset + lane * 2), + size=2, format=">H", + ) + for lane in range(NUM_LANES) + ] + + +def _build_u8_per_lane_fields(getaddr, prefix, base_offset): + return [ + NumberRegField( + _lane_name(prefix, lane), + getaddr(cdb_consts.EPL_PAGE, 128 + base_offset + lane), + size=1, format="B", + ) + for lane in range(NUM_LANES) + ] + + +class CdbReadOeTelemetry(CDBCommand): + """CDB 0x9030 -- read OE telemetry block (EPL reply). + + LPL request payload (5 bytes): [bank_id_u8] + [request_mask_u32_be]. + Payload contract: ``send_cmd(cmd_id, payload={"bank_id": int, "request_mask": int})``; + request_mask defaults to ``OE_TELEMETRY_REQUEST_MASK_ALL`` when omitted. + """ + def __init__(self, + cmd_id=CDB_READ_OE_TELEMETRY_CMD, + reply_field=NVIDIA_CPO_OE_TELEMETRY_REPLY): + super(CdbReadOeTelemetry, self).__init__( + cmd_id, + epl=CDB_READ_OE_TELEMETRY_EPL_LEN, + lpl=5, + rpl_field=reply_field, + ) + + def encode(self, payload): + bank_id = payload.get("bank_id", 0) & 0xFF + request_mask = payload.get("request_mask", OE_TELEMETRY_REQUEST_MASK_ALL) & 0xFFFFFFFF + lpl_data = struct.pack("B", bank_id) + struct.pack(">I", request_mask) + return super(CdbReadOeTelemetry, self).encode(payload=lpl_data) + + +class NvidiaCpoOeCdbMemMap(CdbMemMap): + def __init__(self, codes): + super(NvidiaCpoOeCdbMemMap, self).__init__(codes) + + getaddr = self.getaddr + + reply_fields = [ + NumberRegField(NVIDIA_CPO_OE_TLM_CAPABILITY, + getaddr(cdb_consts.EPL_PAGE, 128 + _OFF_CAPABILITY), + size=4, format=">I"), + NumberRegField(NVIDIA_CPO_OE_TLM_VALID, + getaddr(cdb_consts.EPL_PAGE, 128 + _OFF_VALID), + size=4, format=">I"), + NumberRegField(NVIDIA_CPO_OE_TLM_LANE_BANK_ECHO, + getaddr(cdb_consts.EPL_PAGE, 128 + _OFF_LANE_BANK_ECHO), + size=1, format="B"), + ] + reply_fields += _build_nibble_packed_lane_fields( + getaddr, NVIDIA_CPO_OE_TLM_TX_PS_LANE_STATE, + _OFF_TX_PS_LANE_STATE, bit_width=4) + reply_fields += _build_nibble_packed_lane_fields( + getaddr, NVIDIA_CPO_OE_TLM_RX_PS_LANE_STATE, + _OFF_RX_PS_LANE_STATE, bit_width=4) + reply_fields += _build_u16_per_lane_fields( + getaddr, NVIDIA_CPO_OE_TLM_TX_PS_LANE_SUB_STATE, + _OFF_TX_PS_LANE_SUB_STATE) + reply_fields += _build_u16_per_lane_fields( + getaddr, NVIDIA_CPO_OE_TLM_RX_PS_LANE_SUB_STATE, + _OFF_RX_PS_LANE_SUB_STATE) + reply_fields += _build_nibble_packed_lane_fields( + getaddr, NVIDIA_CPO_OE_TLM_TX_HS_DATA_RATE, + _OFF_TX_HS_DATA_RATE, bit_width=3) + reply_fields += _build_nibble_packed_lane_fields( + getaddr, NVIDIA_CPO_OE_TLM_RX_HS_DATA_RATE, + _OFF_RX_HS_DATA_RATE, bit_width=3) + reply_fields += _build_u8_per_lane_fields( + getaddr, NVIDIA_CPO_OE_TLM_LAST_REASON_OPCODE, + _OFF_LAST_REASON_OPCODE) + reply_fields += [ + NumberRegField(NVIDIA_CPO_OE_TLM_VER_MAJOR, + getaddr(cdb_consts.EPL_PAGE, 128 + _OFF_VER_MAJOR), + size=2, format=">H"), + NumberRegField(NVIDIA_CPO_OE_TLM_VER_MINOR, + getaddr(cdb_consts.EPL_PAGE, 128 + _OFF_VER_MINOR), + size=2, format=">H"), + NumberRegField(NVIDIA_CPO_OE_TLM_VER_RC, + getaddr(cdb_consts.EPL_PAGE, 128 + _OFF_VER_RC), + size=2, format=">H"), + ] + + self.nvidia_cpo_oe_telemetry_reply = RegGroupField( + NVIDIA_CPO_OE_TELEMETRY_REPLY, *reply_fields) + + self.nvidia_cpo_oe_read_telemetry_cmd = CdbReadOeTelemetry() diff --git a/sonic_platform_base/sonic_xcvr/codes/nvidia/__init__.py b/sonic_platform_base/sonic_xcvr/codes/nvidia/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/codes/nvidia/__init__.py @@ -0,0 +1 @@ + diff --git a/sonic_platform_base/sonic_xcvr/codes/nvidia/cpo_els.py b/sonic_platform_base/sonic_xcvr/codes/nvidia/cpo_els.py new file mode 100644 index 000000000..adf7866d9 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/codes/nvidia/cpo_els.py @@ -0,0 +1,26 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +from ..public.cmis import CmisCodes + + +class NvidiaCpoElsCodes(CmisCodes): + pass diff --git a/sonic_platform_base/sonic_xcvr/codes/nvidia/cpo_oe.py b/sonic_platform_base/sonic_xcvr/codes/nvidia/cpo_oe.py new file mode 100644 index 000000000..3fcf8262f --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/codes/nvidia/cpo_oe.py @@ -0,0 +1,26 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +from ..public.cmis import CmisCodes + + +class NvidiaCpoOeCodes(CmisCodes): + pass diff --git a/sonic_platform_base/sonic_xcvr/codes/public/cmis.py b/sonic_platform_base/sonic_xcvr/codes/public/cmis.py index ca67d43df..8de880fba 100644 --- a/sonic_platform_base/sonic_xcvr/codes/public/cmis.py +++ b/sonic_platform_base/sonic_xcvr/codes/public/cmis.py @@ -181,4 +181,54 @@ class CmisCodes(Sff8024): 2: 4 } + CONTROL_MODE = { + 0: 'ACC', # Automatic Current Control + 1: 'APC', # Automatic Power Control + } + + LANE_FAULT_CODE = { + 0: 'No alarm detected', + 1: 'Automatic Power Control (APC) control loop failure', + 2: 'Automatic Current Control (ACC) control loop failure', + 3: 'Reserved', + 4: 'Reserved', + 5: 'Reserved', + 6: 'Reserved', + 7: 'Reserved', + 8: 'Reserved', + 9: 'Vendor specific fault', + 10: 'Vendor specific fault', + 11: 'Vendor specific fault', + 12: 'Vendor specific fault', + 13: 'Vendor specific fault', + 14: 'Vendor specific fault', + 15: 'Vendor specific fault', + } + + LANE_WARNING_CODE = { + 0: 'No warning detected', + 1: 'Automatic Power Control (APC) control loop warning', + 2: 'Automatic Current Control (ACC) control loop warning', + 3: 'Reserved', + 4: 'Reserved', + 5: 'Reserved', + 6: 'Reserved', + 7: 'Reserved', + 8: 'Reserved', + 9: 'Vendor specific warning', + 10: 'Vendor specific warning', + 11: 'Vendor specific warning', + 12: 'Vendor specific warning', + 13: 'Vendor specific warning', + 14: 'Vendor specific warning', + 15: 'Vendor specific warning', + } + + LANE_STATE = { + 0: 'Lane Output off', + 1: 'Lane Output ramping', + 2: 'Lane Output on', + 3: 'Reserved', + } + # TODO: Add other codes diff --git a/sonic_platform_base/sonic_xcvr/fields/consts.py b/sonic_platform_base/sonic_xcvr/fields/consts.py index c4881da34..c3b4867b8 100644 --- a/sonic_platform_base/sonic_xcvr/fields/consts.py +++ b/sonic_platform_base/sonic_xcvr/fields/consts.py @@ -229,6 +229,10 @@ AUX3_LOW_ALARM = "Aux3MonitorLowAlarmThreshold" AUX3_HIGH_WARN = "Aux3MonitorHighWarningThreshold" AUX3_LOW_WARN = "Aux3MonitorLowWarningThreshold" +CUSTOM_MON_HIGH_ALARM = "CustomMonHighAlarmThreshold" +CUSTOM_MON_LOW_ALARM = "CustomMonLowAlarmThreshold" +CUSTOM_MON_HIGH_WARN = "CustomMonHighWarningThreshold" +CUSTOM_MON_LOW_WARN = "CustomMonLowWarningThreshold" TX_POWER_HIGH_ALARM = "TxOpticalPowerHighAlarmThreshold" TX_POWER_LOW_ALARM = "TxOpticalPowerLowAlarmThreshold" TX_POWER_HIGH_WARN = "TxOpticalPowerHighWarningThreshold" @@ -507,3 +511,8 @@ VENDOR_CUSTOM = "VendorCustom" TARGET_MODE = "TargetMode" CARTRDIGE_SLOT_ID = "CartridgeSlotID" + +# ELSFP field-name constants now live in fields/elsfp_consts.py (imported +# verbatim from upstream nexthop-ai/sonic-platform-common#2). Use: +# from . import elsfp_consts as ec +# ec.ELSFP_MONITORS_FIELD, ec.BIAS_HIGH_ALARM, ... diff --git a/sonic_platform_base/sonic_xcvr/fields/scale_consts.py b/sonic_platform_base/sonic_xcvr/fields/scale_consts.py new file mode 100644 index 000000000..9d60b3765 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/fields/scale_consts.py @@ -0,0 +1,58 @@ +"""Named scale factors for :class:`NumberRegField`. + +``NumberRegField.decode()`` returns ``raw / scale``. Names below encode the +*raw LSB* and the *target unit* of the decoded float, so a reader can tell +at a glance which unit the field ends up in. + +Convention: ``SCALE__TO_``. + +Multiple fields can share the same numeric value with different semantics +(e.g. ``SCALE_100UA_TO_A`` and ``SCALE_100UV_TO_V`` are both ``10000.0``); +use the name that matches the field's physical quantity, not the number. +""" + +# ---------- Current ---------- +# Raw stored as 1 uA per LSB, decoded in mA (CDB laser MPD). +SCALE_UA_TO_MA = 1000.0 + +# Raw stored as 100 uA per LSB, decoded in A. +SCALE_100UA_TO_A = 10000.0 + +# Raw stored as 100 uA per LSB, decoded in mA +# (ELSFP bias thresholds / setpoints / monitors, ELSFP max/min bias). +SCALE_100UA_TO_MA = 10.0 + +# Raw stored as 200 uA per LSB, decoded in mA (ELSFP module ICC monitor). +SCALE_200UA_TO_MA = 5.0 + +# ---------- Optical power (linear) ---------- +# Raw stored as 10 uW per LSB, decoded in mW +# (ELSFP opt-power thresholds / setpoints / monitors, ELSFP max/min opt power). +SCALE_10UW_TO_MW = 100.0 + +# ---------- Voltage ---------- +# Raw stored as 100 uV per LSB, decoded in V +# (standard CMIS VOLTAGE_FIELD, NVIDIA ELS voltage on page 0x00/0x02). +SCALE_100UV_TO_V = 10000.0 + +# Raw stored as 15 mV per LSB, decoded in V (ELSFP per-lane VCC monitor). +SCALE_15MV_TO_V = 1000.0 / 15.0 + +# Raw stored as 5 mV per LSB, decoded in mV +# (NVIDIA CPO ELS laser + TEC health values on CDB 0x9018). +SCALE_5MV_TO_MV = 0.2 + +# ---------- Module power ---------- +# Raw stored as 0.1 W per LSB, decoded in W (CDB module power consumption). +SCALE_0P1W_TO_W = 10.0 + +# ---------- Temperature ---------- +# Raw stored as 1/256 deg C per LSB (signed int16, aka Q8.8 fixed-point: +# upper byte = integer deg C, lower byte = fractional deg C in 1/256 steps), +# decoded in deg C. Used by standard CMIS temperature and NVIDIA ELS +# custom-monitor temperature value + thresholds. +SCALE_1_OVER_256C_TO_C = 256.0 + +# ---------- Frequency ---------- +# Raw stored as 5 GHz per LSB, decoded in GHz (ELSFP per-lane laser frequency). +SCALE_5GHZ_TO_GHZ = 0.2 diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/__init__.py b/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/__init__.py @@ -0,0 +1 @@ + diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/cpo_els.py b/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/cpo_els.py new file mode 100644 index 000000000..2c77ad404 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/cpo_els.py @@ -0,0 +1,108 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +from ..public.cmis_pages.base import CmisPage +from ..public.cmis_pages.cmis_pages import ( + CmisAdministrativeLowerPage, + CmisAdministrativeUpperPage, + CmisAdvertisingPage, +) +from ..public.elsfp_cmis import ElsfpCmisMemMap +from ...fields import consts +from ...fields.scale_consts import ( + SCALE_100UV_TO_V, + SCALE_1_OVER_256C_TO_C, +) +from ...fields.xcvr_field import NumberRegField + +NVIDIA_ELS_MODULE_STATUS_PAGE = 0xB0 +NVIDIA_ELS_IDENTITY_PAGE = 0xB1 +NVIDIA_ELS_ADVERTISING_PAGE = 0xB2 +NVIDIA_ELS_THRESHOLDS_PAGE = 0xB3 + +NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD = "NvidiaElsCustomMonValue" +NVIDIA_ELS_CUSTOM_MON_THRESHOLDS_FIELD = "NvidiaElsCustomMonThresholds" +NVIDIA_ELS_VOLTAGE_FIELD = "NvidiaElsVoltage" +NVIDIA_ELS_VOLTAGE_THRESHOLDS_FIELD = "NvidiaElsVoltageThresholds" + + +class NvidiaCpoElsCustomMonValuePage(CmisPage): + """ELS Custom Monitor value at page 0x00 byte 24 (int16, 1/256 deg C per LSB).""" + + def __init__(self, codes, bank=0): + super(NvidiaCpoElsCustomMonValuePage, self).__init__(codes, page=0, bank=bank) + self.fields[NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD] = [ + NumberRegField(consts.CUSTOM_MON, self.getaddr(24), + size=2, format=">h", scale=SCALE_1_OVER_256C_TO_C), + ] + + +class NvidiaCpoElsCustomMonThresholdsPage(CmisPage): + """ELS Custom Monitor thresholds at page 0x02 bytes 168-175 (int16, 1/256 deg C per LSB).""" + + def __init__(self, codes, bank=0): + super(NvidiaCpoElsCustomMonThresholdsPage, self).__init__(codes, page=0x02, bank=bank) + self.fields[NVIDIA_ELS_CUSTOM_MON_THRESHOLDS_FIELD] = [ + NumberRegField(consts.CUSTOM_MON_HIGH_ALARM, self.getaddr(168), size=2, format=">h", scale=SCALE_1_OVER_256C_TO_C), + NumberRegField(consts.CUSTOM_MON_LOW_ALARM, self.getaddr(170), size=2, format=">h", scale=SCALE_1_OVER_256C_TO_C), + NumberRegField(consts.CUSTOM_MON_HIGH_WARN, self.getaddr(172), size=2, format=">h", scale=SCALE_1_OVER_256C_TO_C), + NumberRegField(consts.CUSTOM_MON_LOW_WARN, self.getaddr(174), size=2, format=">h", scale=SCALE_1_OVER_256C_TO_C), + ] + + +class NvidiaCpoElsVoltagePage(CmisPage): + """ELS Voltage at page 0x00 byte 16 (u16, scale 10000 -> Volts).""" + + def __init__(self, codes, bank=0): + super(NvidiaCpoElsVoltagePage, self).__init__(codes, page=0, bank=bank) + self.fields[NVIDIA_ELS_VOLTAGE_FIELD] = [ + NumberRegField(consts.VOLTAGE_FIELD, self.getaddr(16), + size=2, format=">H", scale=SCALE_100UV_TO_V), + ] + + +class NvidiaCpoElsVoltageThresholdsPage(CmisPage): + """ELS Voltage thresholds at page 0x02 bytes 136-143 (u16, scale 10000 -> Volts).""" + + def __init__(self, codes, bank=0): + super(NvidiaCpoElsVoltageThresholdsPage, self).__init__(codes, page=0x02, bank=bank) + self.fields[NVIDIA_ELS_VOLTAGE_THRESHOLDS_FIELD] = [ + NumberRegField(consts.VOLTAGE_HIGH_ALARM_FIELD, self.getaddr(136), size=2, format=">H", scale=SCALE_100UV_TO_V), + NumberRegField(consts.VOLTAGE_LOW_ALARM_FIELD, self.getaddr(138), size=2, format=">H", scale=SCALE_100UV_TO_V), + NumberRegField(consts.VOLTAGE_HIGH_WARNING_FIELD, self.getaddr(140), size=2, format=">H", scale=SCALE_100UV_TO_V), + NumberRegField(consts.VOLTAGE_LOW_WARNING_FIELD, self.getaddr(142), size=2, format=">H", scale=SCALE_100UV_TO_V), + ] + + +class NvidiaCpoElsCmisMemMap(ElsfpCmisMemMap): + def _build_pages(self, codes): + pages = super(NvidiaCpoElsCmisMemMap, self)._build_pages(codes) + pages.extend([ + CmisAdministrativeLowerPage(codes, page=NVIDIA_ELS_MODULE_STATUS_PAGE, bank=self.bank), + CmisAdministrativeUpperPage(codes, page=NVIDIA_ELS_IDENTITY_PAGE, bank=self.bank), + CmisAdvertisingPage(codes, page=NVIDIA_ELS_ADVERTISING_PAGE, bank=self.bank), + # B3 thresholds-mirror intentionally NOT registered (see module docstring). + NvidiaCpoElsCustomMonValuePage(codes, bank=self.bank), + NvidiaCpoElsCustomMonThresholdsPage(codes, bank=self.bank), + NvidiaCpoElsVoltagePage(codes, bank=self.bank), + NvidiaCpoElsVoltageThresholdsPage(codes, bank=self.bank), + ]) + return pages diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/cpo_oe.py b/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/cpo_oe.py new file mode 100644 index 000000000..a2a870bf0 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/nvidia/cpo_oe.py @@ -0,0 +1,26 @@ +# +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################# +# Mellanox +############################################################################# + +from ..public.cmis import CmisMemMap + + +class NvidiaCpoOeMemMap(CmisMemMap): + pass diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/public/c_cmis.py b/sonic_platform_base/sonic_xcvr/mem_maps/public/c_cmis.py new file mode 100644 index 000000000..9f91bd48f --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/public/c_cmis.py @@ -0,0 +1,81 @@ +""" + c_cmis.py + + Implementation of XcvrMemMap for C-CMIS Rev 1.1 +""" + +from ...fields.xcvr_field import ( + NumberRegField, + RegGroupField +) +from ...fields import consts +from .cmis import CmisMemMap + +class CCmisMemMap(CmisMemMap): + def __init__(self, codes, bank=0): + super(CCmisMemMap, self).__init__(codes, bank=bank) + + self.MODULE_CONFIG_SUPPORT = RegGroupField(consts.MODULE_CONFIG_SUPPORT_FIELD, + NumberRegField(consts.SUPPORT_GRID, self.getaddr(0x4, 128)), + NumberRegField(consts.LOW_CHANNEL, self.getaddr(0x4, 158), format=">h", size=2), + NumberRegField(consts.HIGH_CHANNEL, self.getaddr(0x4, 160), format=">h", size=2), + NumberRegField(consts.MIN_PROG_OUTPUT_POWER, self.getaddr(0x4, 198), format=">h", size=2, scale = 100.0), + NumberRegField(consts.MAX_PROG_OUTPUT_POWER, self.getaddr(0x4, 200), format=">h", size=2, scale = 100.0) + ) + + self.MEDIA_LANE_FEC_PM = RegGroupField(consts.MEDIA_LANE_FEC_PM, + NumberRegField(consts.RX_BITS_PM, self.getaddr(0x34, 128), format=">Q", size=8), + NumberRegField(consts.RX_BITS_SUB_INTERVAL_PM, self.getaddr(0x34, 136), format=">Q", size=8), + NumberRegField(consts.RX_CORR_BITS_PM, self.getaddr(0x34, 144), format=">Q", size=8), + NumberRegField(consts.RX_MIN_CORR_BITS_SUB_INTERVAL_PM, self.getaddr(0x34, 152), format=">Q", size=8), + NumberRegField(consts.RX_MAX_CORR_BITS_SUB_INTERVAL_PM, self.getaddr(0x34, 160), format=">Q", size=8), + NumberRegField(consts.RX_FRAMES_PM, self.getaddr(0x34, 168), format=">I", size=4), + NumberRegField(consts.RX_FRAMES_SUB_INTERVAL_PM, self.getaddr(0x34, 172), format=">I", size=4), + NumberRegField(consts.RX_FRAMES_UNCORR_ERR_PM, self.getaddr(0x34, 176), format=">I", size=4), + NumberRegField(consts.RX_MIN_FRAMES_UNCORR_ERR_SUB_INTERVAL_PM, self.getaddr(0x34, 180), format=">I", size=4), + NumberRegField(consts.RX_MAX_FRAMES_UNCORR_ERR_SUB_INTERVAL_PM, self.getaddr(0x34, 184), format=">I", size=4), + ) + + # MEDIA_LANE_LINK_PM block corresponds to all the coherent PMs in C-CMIS spec reported in Page 35h. + self.MEDIA_LANE_LINK_PM = RegGroupField(consts.MEDIA_LANE_LINK_PM, + NumberRegField(consts.RX_AVG_CD_PM, self.getaddr(0x35, 128), format=">i", size=4), + NumberRegField(consts.RX_MIN_CD_PM, self.getaddr(0x35, 132), format=">i", size=4), + NumberRegField(consts.RX_MAX_CD_PM, self.getaddr(0x35, 136), format=">i", size=4), + NumberRegField(consts.RX_AVG_DGD_PM, self.getaddr(0x35, 140), format=">H", size=2, scale=100.0), + NumberRegField(consts.RX_MIN_DGD_PM, self.getaddr(0x35, 142), format=">H", size=2, scale=100.0), + NumberRegField(consts.RX_MAX_DGD_PM, self.getaddr(0x35, 144), format=">H", size=2, scale=100.0), + NumberRegField(consts.RX_AVG_SOPMD_PM, self.getaddr(0x35, 146), format=">H", size=2, scale=100.0), + NumberRegField(consts.RX_MIN_SOPMD_PM, self.getaddr(0x35, 148), format=">H", size=2, scale=100.0), + NumberRegField(consts.RX_MAX_SOPMD_PM, self.getaddr(0x35, 150), format=">H", size=2, scale=100.0), + NumberRegField(consts.RX_AVG_PDL_PM, self.getaddr(0x35, 152), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MIN_PDL_PM, self.getaddr(0x35, 154), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MAX_PDL_PM, self.getaddr(0x35, 156), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_AVG_OSNR_PM, self.getaddr(0x35, 158), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MIN_OSNR_PM, self.getaddr(0x35, 160), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MAX_OSNR_PM, self.getaddr(0x35, 162), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_AVG_ESNR_PM, self.getaddr(0x35, 164), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MIN_ESNR_PM, self.getaddr(0x35, 166), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MAX_ESNR_PM, self.getaddr(0x35, 168), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_AVG_CFO_PM, self.getaddr(0x35, 170), format=">h", size=2), + NumberRegField(consts.RX_MIN_CFO_PM, self.getaddr(0x35, 172), format=">h", size=2), + NumberRegField(consts.RX_MAX_CFO_PM, self.getaddr(0x35, 174), format=">h", size=2), + NumberRegField(consts.RX_AVG_EVM_PM, self.getaddr(0x35, 176), format=">H", size=2, scale=655.35), + NumberRegField(consts.RX_MIN_EVM_PM, self.getaddr(0x35, 178), format=">H", size=2, scale=655.35), + NumberRegField(consts.RX_MAX_EVM_PM, self.getaddr(0x35, 180), format=">H", size=2, scale=655.35), + NumberRegField(consts.TX_AVG_POWER_PM, self.getaddr(0x35,182), format=">h", size=2, scale=100.0), + NumberRegField(consts.TX_MIN_POWER_PM, self.getaddr(0x35,184), format=">h", size=2, scale=100.0), + NumberRegField(consts.TX_MAX_POWER_PM, self.getaddr(0x35,186), format=">h", size=2, scale=100.0), + NumberRegField(consts.RX_AVG_POWER_PM, self.getaddr(0x35,188), format=">h", size=2, scale=100.0), + NumberRegField(consts.RX_MIN_POWER_PM, self.getaddr(0x35,190), format=">h", size=2, scale=100.0), + NumberRegField(consts.RX_MAX_POWER_PM, self.getaddr(0x35,192), format=">h", size=2, scale=100.0), + NumberRegField(consts.RX_AVG_SIG_POWER_PM, self.getaddr(0x35,194), format=">h", size=2, scale=100.0), + NumberRegField(consts.RX_MIN_SIG_POWER_PM, self.getaddr(0x35,196), format=">h", size=2, scale=100.0), + NumberRegField(consts.RX_MAX_SIG_POWER_PM, self.getaddr(0x35,198), format=">h", size=2, scale=100.0), + NumberRegField(consts.RX_AVG_SOPROC_PM, self.getaddr(0x35,200), format=">H", size=2), + NumberRegField(consts.RX_MIN_SOPROC_PM, self.getaddr(0x35,202), format=">H", size=2), + NumberRegField(consts.RX_MAX_SOPROC_PM, self.getaddr(0x35,204), format=">H", size=2), + NumberRegField(consts.RX_AVG_MER_PM, self.getaddr(0x35,206), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MIN_MER_PM, self.getaddr(0x35,208), format=">H", size=2, scale=10.0), + NumberRegField(consts.RX_MAX_MER_PM, self.getaddr(0x35,210), format=">H", size=2, scale=10.0), + # TODO: add others PMs... + ) diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis.py b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis.py new file mode 100755 index 000000000..8512a1eb3 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis.py @@ -0,0 +1,72 @@ +""" + cmis.py + + Implementation of XcvrMemMap for CMIS Rev 5.0 + + The actual field definitions live in mem_maps/public/cmis_pages/cmis_pages.py + as CmisPage subclasses (see nexthop-ai/sonic-platform-common#1, imported as + a temporary scaffold). This file is now a thin composer that wires the page + classes onto CmisFlatMemMap / CmisMemMap via CmisPage.register_fields, with + cross-page RegGroupField merges (ADMIN_INFO, ADVERTISING, MODULE_MONITORS, + LANE_DATAPATH_STATUS, TRANS_CDB) handled automatically. + + The legacy 2-arg getaddr(page, offset) is preserved on this class so + downstream subclasses that still add fields directly here (CCmisMemMap, + CmisTargetFWUpgradeMemMap, AmphBackplaneMemMap, NvidiaCpoOeMemMap) + continue to work unchanged. New code should define fields inside + CmisPage subclasses and use page.getaddr(offset) instead. +""" + +from ..xcvr_mem_map import XcvrMemMap +from ...fields import consts # noqa: F401 -- preserved for downstream `cmis.consts` access +from ...fields.consts import * # noqa: F401,F403 -- preserved re-export of CMIS field constants +from .cmis_pages import ( + CmisAdministrativeLowerPage, + CmisAdministrativeUpperPage, + CmisAdvertisingPage, + CmisThresholdsPage, + CmisLaneDatapathConfigPage, + CmisLaneDatapathStatusPage, + CmisTunableLaserCtrlStatusPage, + CmisModulePerfDiagCtrlPage, + CmisVdmAdvertisingCtrlPage, + CmisCdbMessagePage, +) + + +class CmisFlatMemMap(XcvrMemMap): + """Memory map for CMIS flat memory (Lower page and Upper page 0h ONLY).""" + + def __init__(self, codes, bank=0): + super(CmisFlatMemMap, self).__init__(codes) + self.bank = bank + for page in self._build_pages(codes): + page.register_fields(self) + + def _build_pages(self, codes): + """Return the list of CmisPage instances composed onto this memmap. + Subclasses extend this list to layer additional pages on top. + """ + return [ + CmisAdministrativeLowerPage(codes), + CmisAdministrativeUpperPage(codes), + ] + + def getaddr(self, page, offset, page_size=128): + return page * page_size + offset + + +class CmisMemMap(CmisFlatMemMap): + """Full CMIS memory map (lower + all standard upper pages).""" + + def _build_pages(self, codes): + return super(CmisMemMap, self)._build_pages(codes) + [ + CmisAdvertisingPage(codes), # 0x01 + CmisThresholdsPage(codes), # 0x02 + CmisLaneDatapathConfigPage(codes), # 0x10 + CmisLaneDatapathStatusPage(codes), # 0x11 + CmisTunableLaserCtrlStatusPage(codes), # 0x12 + CmisModulePerfDiagCtrlPage(codes), # 0x13 + CmisVdmAdvertisingCtrlPage(codes), # 0x2F + CmisCdbMessagePage(codes), # 0x9F + ] diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/__init__.py b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/__init__.py new file mode 100644 index 000000000..94f7ed102 --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/__init__.py @@ -0,0 +1,47 @@ +# ============================================================================= +# TEMPORARY: scaffold derived from upstream PRs +# nexthop-ai/sonic-platform-common#1 (Refactor CMIS memory map into pages) +# nexthop-ai/sonic-platform-common#2 (ELSFP pages and Memory Map) +# +# Re-export surface for the page-class composition pattern. Upstream keeps +# each page in its own pg_NN_*.py file; downstream we collapse the standard +# CMIS pages into cmis_pages.py and the ELSFP pages into elsfp_pages.py +# (one file per logical memory map). When upstream merges, replace the two +# consolidated files with the upstream pg_NN_*.py files and update the +# imports below accordingly. +# ============================================================================= +from .base import CmisPage +from .cmis_pages import ( + CmisAdministrativeLowerPage, + CmisAdministrativeUpperPage, + CmisAdvertisingPage, + CmisThresholdsPage, + CmisLaneDatapathConfigPage, + CmisLaneDatapathStatusPage, + CmisTunableLaserCtrlStatusPage, + CmisModulePerfDiagCtrlPage, + CmisVdmAdvertisingCtrlPage, + CmisCdbMessagePage, +) +from .elsfp_pages import ( + ElsfpAdvertisementsFlagsCtrlPage, + ElsfpSetpointsMonitorsPage, +) + +__all__ = [ + "CmisPage", + # Standard CMIS pages + "CmisAdministrativeLowerPage", + "CmisAdministrativeUpperPage", + "CmisAdvertisingPage", + "CmisThresholdsPage", + "CmisLaneDatapathConfigPage", + "CmisLaneDatapathStatusPage", + "CmisTunableLaserCtrlStatusPage", + "CmisModulePerfDiagCtrlPage", + "CmisVdmAdvertisingCtrlPage", + "CmisCdbMessagePage", + # ELSFP pages + "ElsfpAdvertisementsFlagsCtrlPage", + "ElsfpSetpointsMonitorsPage", +] diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/base.py b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/base.py new file mode 100644 index 000000000..f461d9faa --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/base.py @@ -0,0 +1,71 @@ +""" + base.py + + Abstract class for CMIS pages +""" + +from ...xcvr_mem_map import XcvrMemMap +from ....fields.xcvr_field import RegField, RegGroupField +from .cmis_page_consts import CMIS_NUM_BANKED_PAGES, CMIS_ARCH_PAGES + +def get_field_from_pages(field_name, *pages): + fields = [] + for page in pages: + if hasattr(page, 'fields') and field_name in page.fields: + fields.extend(page.fields[field_name]) + return fields + +class CmisPage(XcvrMemMap): + + def __init__(self, codes, page, bank=0): + super(CmisPage, self).__init__(codes) + self._page = page + self._bank = bank + self.fields = {} + + @property + def page(self): + """Returns the page number (read-only).""" + return self._page + + @property + def bank(self): + """Returns the bank number (read-only).""" + return self._bank + + def getaddr(self, offset, page_size=128): + """Return a linear address for this page that encodes the bank. + + Page 0 is non-banked (CMIS spec) so bank is dropped. Upper pages encode + bank as a 32KB-per-bank stride: ``(bank * CMIS_ARCH_PAGES + page) * 128 + offset``. + Platform readers extract the bank from the upper bits of this address. + """ + if self._page == 0: + return offset + return (self._bank * CMIS_ARCH_PAGES + self._page) * page_size + offset + + def get_field_values(self, field: str): + return self.fields[field] + + def register_fields(self, memmap): + """Compose this page's fields onto the memory map. + + Each key in self.fields becomes both the setattr name on memmap and the + RegGroupField.name. If memmap already carries a RegGroupField under the + same key (from a previously-registered page), this page's contributions + are merged in and the field list is re-sorted by offset to preserve the + RegGroupField invariant (first member must have the smallest offset). + """ + for key, contribs in self.fields.items(): + if not contribs: + continue + existing = getattr(memmap, key, None) + field_key = key + field_values = contribs + if isinstance(existing, RegGroupField): + field_key = existing.name + field_values = sorted( + list(existing.fields) + list(contribs), + key=lambda f: f.get_offset(), + ) + setattr(memmap, key, RegGroupField(field_key, *field_values)) diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/cmis_page_consts.py b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/cmis_page_consts.py new file mode 100644 index 000000000..7b3ed1b2d --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/cmis_page_consts.py @@ -0,0 +1,26 @@ +""" + cmis_page_consts.py + + Constants for CMIS memory map pages +""" + +# Constants matching optoe driver +CMIS_EEPROM_PAGE_SIZE = 128 +CMIS_NUM_NON_BANKED_PAGES = 16 # pages 00h-0Fh +CMIS_NUM_BANKED_PAGES = 240 # pages 10h-FFh +CMIS_ARCH_PAGES = 256 # architectural pages per bank (matches OPTOE_ARCH_PAGES) + +# CMIS page number constants +ADMINISTRATIVE_PAGE = 0x00 +ADVERTISING_PAGE = 0x01 +THRESHOLDS_PAGE = 0x02 +LANE_DATAPATH_CONFIG_PAGE = 0x10 +LANE_DATAPATH_STATUS_PAGE = 0x11 +TUNABLE_LASER_CTRL_STATUS_PAGE = 0x12 +MODULE_PERF_DIAG_CTRL_PAGE = 0x13 +VDM_ADVERTISING_CTRL_PAGE = 0x2F +CDB_MESSAGE_PAGE = 0x9F + +# ELSFP-specific CMIS pages +ELSFP_ADVERTISEMENTS_FLAGS_CTRL_PAGE = 0x1A +ELSFP_SETPOINTS_MON_PAGE = 0x1B diff --git a/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/cmis_pages.py b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/cmis_pages.py new file mode 100644 index 000000000..9156dc67d --- /dev/null +++ b/sonic_platform_base/sonic_xcvr/mem_maps/public/cmis_pages/cmis_pages.py @@ -0,0 +1,778 @@ +"""cmis_pages.py + +CmisPage subclasses for the standard CMIS Rev 5.0 memory map. Each class +populates self.fields with const-string keys; CmisPage.register_fields then +composes them onto a CmisMemMap (or any XcvrMemMap subclass) with cross-page +group merging handled by the base class. +""" + +from .base import CmisPage +from .cmis_page_consts import ( + ADMINISTRATIVE_PAGE, + ADVERTISING_PAGE, + THRESHOLDS_PAGE, + LANE_DATAPATH_CONFIG_PAGE, + LANE_DATAPATH_STATUS_PAGE, + TUNABLE_LASER_CTRL_STATUS_PAGE, + MODULE_PERF_DIAG_CTRL_PAGE, + VDM_ADVERTISING_CTRL_PAGE, + CDB_MESSAGE_PAGE, +) +from ....fields.xcvr_field import ( + CodeRegField, + DateField, + HexRegField, + NumberRegField, + RegBitField, + RegBitsField, + RegGroupField, + StringRegField, +) +from ....fields import consts +from ....fields.public.cmis import CableLenField + + +# --------------------------------------------------------------------------- +# Page 0x00 -- Administrative (lower memory, offsets 0-127) +# --------------------------------------------------------------------------- +class CmisAdministrativeLowerPage(CmisPage): + def __init__(self, codes, page=ADMINISTRATIVE_PAGE, bank=0): + super(CmisAdministrativeLowerPage, self).__init__( + codes, page=page, bank=bank) + + self.fields[consts.MGMT_CHAR_FIELD] = [ + NumberRegField(consts.MGMT_CHAR_MISC_FIELD, self.getaddr(2), + RegBitField(consts.FLAT_MEM_FIELD, 7) + ), + ] + + self.fields[consts.ADMIN_INFO_FIELD] = [ + CodeRegField(consts.ID_FIELD, self.getaddr(0), self.codes.XCVR_IDENTIFIERS), + NumberRegField(consts.CMIS_MAJOR_REVISION, self.getaddr(1), + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ), + NumberRegField(consts.CMIS_MINOR_REVISION, self.getaddr(1), + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ), + NumberRegField(consts.ACTIVE_FW_MAJOR_REV, self.getaddr(39), format="B", size=1), + NumberRegField(consts.ACTIVE_FW_MINOR_REV, self.getaddr(40), format="B", size=1), + CodeRegField(consts.MEDIA_TYPE_FIELD, self.getaddr(85), self.codes.MODULE_MEDIA_TYPE), + CodeRegField(consts.HOST_ELECTRICAL_INTERFACE, self.getaddr(86), self.codes.HOST_ELECTRICAL_INTERFACE), + CodeRegField(consts.MODULE_MEDIA_INTERFACE_850NM, self.getaddr(87), self.codes.NM_850_MEDIA_INTERFACE), + CodeRegField(consts.MODULE_MEDIA_INTERFACE_SM, self.getaddr(87), self.codes.SM_MEDIA_INTERFACE), + CodeRegField(consts.MODULE_MEDIA_INTERFACE_PASSIVE_COPPER, self.getaddr(87), self.codes.PASSIVE_COPPER_MEDIA_INTERFACE), + CodeRegField(consts.MODULE_MEDIA_INTERFACE_ACTIVE_CABLE, self.getaddr(87), self.codes.ACTIVE_CABLE_MEDIA_INTERFACE), + CodeRegField(consts.MODULE_MEDIA_INTERFACE_BASE_T, self.getaddr(87), self.codes.BASE_T_MEDIA_INTERFACE), + NumberRegField(consts.MEDIA_LANE_COUNT, self.getaddr(88), + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ), + NumberRegField(consts.HOST_LANE_COUNT, self.getaddr(88), + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ), + NumberRegField(consts.HOST_LANE_ASSIGNMENT_OPTION, self.getaddr(89), format="B", size=1), + ] + + self.fields[consts.APPLS_ADVT_FIELD] = [ + *(CodeRegField(f"{consts.HOST_ELECTRICAL_INTERFACE}_{app}", self.getaddr(86 + 4 * (app - 1)), + self.codes.HOST_ELECTRICAL_INTERFACE) for app in range(1, 9)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_850NM}_{app}", self.getaddr(87 + 4 * (app - 1)), + self.codes.NM_850_MEDIA_INTERFACE) for app in range(1, 9)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_SM}_{app}", self.getaddr(87 + 4 * (app - 1)), + self.codes.SM_MEDIA_INTERFACE) for app in range(1, 9)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_PASSIVE_COPPER}_{app}", self.getaddr(87 + 4 * (app - 1)), + self.codes.PASSIVE_COPPER_MEDIA_INTERFACE) for app in range(1, 9)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_ACTIVE_CABLE}_{app}", self.getaddr(87 + 4 * (app - 1)), + self.codes.ACTIVE_CABLE_MEDIA_INTERFACE) for app in range(1, 9)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_BASE_T}_{app}", self.getaddr(87 + 4 * (app - 1)), + self.codes.BASE_T_MEDIA_INTERFACE) for app in range(1, 9)), + + *(NumberRegField(f"{consts.MEDIA_LANE_COUNT}_{lane}", self.getaddr(88 + 4 * (lane - 1)), + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ) for lane in range(1, 9)), + + *(NumberRegField(f"{consts.HOST_LANE_COUNT}_{lane}", self.getaddr(88 + 4 * (lane - 1)), + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ) for lane in range(1, 9)), + + *(NumberRegField(f"{consts.HOST_LANE_ASSIGNMENT_OPTION}_{lane}", self.getaddr(89 + 4 * (lane - 1)), + format="B", size=1) for lane in range(1, 9)), + ] + + self.fields[consts.MODULE_MONITORS_PAGE0_FIELD] = [ + NumberRegField(consts.TEMPERATURE_FIELD, self.getaddr(14), size=2, format=">h", scale=256.0), + NumberRegField(consts.VOLTAGE_FIELD, self.getaddr(16), size=2, format=">H", scale=10000.0), + NumberRegField(consts.AUX1_MON, self.getaddr(18), format=">h", size=2), + NumberRegField(consts.AUX2_MON, self.getaddr(20), format=">h", size=2), + NumberRegField(consts.AUX3_MON, self.getaddr(22), format=">h", size=2), + NumberRegField(consts.CUSTOM_MON, self.getaddr(24), size=2, format=">h", scale=256.0), + ] + + self.fields[consts.TRANS_MODULE_STATUS_FIELD] = [ + CodeRegField(consts.MODULE_STATE, self.getaddr(3), self.codes.MODULE_STATE, + *(RegBitField(f"Bit{bit}", bit) for bit in range(1, 4)) + ), + NumberRegField(consts.MODULE_FIRMWARE_FAULT_INFO, self.getaddr(8), size=1), + NumberRegField(consts.MODULE_FLAG_BYTE1, self.getaddr(9), size=1), + NumberRegField(consts.MODULE_FLAG_BYTE2, self.getaddr(10), size=1), + NumberRegField(consts.MODULE_FLAG_BYTE3, self.getaddr(11), size=1), + NumberRegField(consts.CDB1_STATUS, self.getaddr(37), size=1), + CodeRegField(consts.MODULE_FAULT_CAUSE, self.getaddr(41), self.codes.MODULE_FAULT_CAUSE), + ] + + self.fields[consts.TRANS_CONFIG_FIELD] = [ + NumberRegField(consts.MODULE_LEVEL_CONTROL, self.getaddr(26), size=1, ro=False), + ] + + def getaddr(self, offset, page_size=128): + if self._page != ADMINISTRATIVE_PAGE and offset < 128: + offset += 128 + return super(CmisAdministrativeLowerPage, self).getaddr(offset, page_size) + + +# --------------------------------------------------------------------------- +# Page 0x00 -- Administrative (upper memory, offsets 128-255) +# --------------------------------------------------------------------------- +class CmisAdministrativeUpperPage(CmisPage): + def __init__(self, codes, page=ADMINISTRATIVE_PAGE, bank=0): + super(CmisAdministrativeUpperPage, self).__init__( + codes, page=page, bank=bank) + + self.fields[consts.ADMIN_INFO_FIELD] = [ + CodeRegField(consts.ID_ABBRV_FIELD, self.getaddr(128), self.codes.XCVR_IDENTIFIER_ABBRV), + StringRegField(consts.VENDOR_NAME_FIELD, self.getaddr(129), size=16), + HexRegField(consts.VENDOR_OUI_FIELD, self.getaddr(145), size=3), + StringRegField(consts.VENDOR_PART_NO_FIELD, self.getaddr(148), size=16), + StringRegField(consts.VENDOR_REV_FIELD, self.getaddr(164), size=2), + StringRegField(consts.VENDOR_SERIAL_NO_FIELD, self.getaddr(166), size=16), + DateField(consts.VENDOR_DATE_FIELD, self.getaddr(182), size=8), + RegGroupField(consts.EXT_ID_FIELD, + CodeRegField(consts.POWER_CLASS_FIELD, self.getaddr(200), self.codes.POWER_CLASSES, + *(RegBitField(f"{consts.POWER_CLASS_FIELD}_{bit}", bit) for bit in range(5, 8)) + ), + NumberRegField(consts.MAX_POWER_FIELD, self.getaddr(201), scale=4.0), + ), + NumberRegField(consts.LEN_MULT_FIELD, self.getaddr(202), + *(RegBitField(f"{consts.LEN_MULT_FIELD}_{bit}", bit) for bit in range(6, 8)) + ), + CableLenField(consts.LENGTH_ASSEMBLY_FIELD, self.getaddr(202), + *(RegBitField(f"{consts.LENGTH_ASSEMBLY_FIELD}_{bit}", bit) for bit in range(0, 6)) + ), + CodeRegField(consts.CONNECTOR_FIELD, self.getaddr(203), self.codes.CONNECTORS), + CodeRegField(consts.MEDIA_INTERFACE_TECH, self.getaddr(212), self.codes.MEDIA_INTERFACE_TECH), + ] + + +# --------------------------------------------------------------------------- +# Page 0x01 -- Advertising +# --------------------------------------------------------------------------- +class CmisAdvertisingPage(CmisPage): + def __init__(self, codes, page=ADVERTISING_PAGE, bank=0): + super(CmisAdvertisingPage, self).__init__( + codes, page=page, bank=bank) + + self.fields[consts.ADVERTISING_FIELD] = [ + NumberRegField(consts.INACTIVE_FW_MAJOR_REV, self.getaddr(128), format="B", size=1), + NumberRegField(consts.INACTIVE_FW_MINOR_REV, self.getaddr(129), format="B", size=1), + NumberRegField(consts.HW_MAJOR_REV, self.getaddr(130), size=1), + NumberRegField(consts.HW_MINOR_REV, self.getaddr(131), size=1), + CodeRegField(consts.DP_PATH_INIT_DURATION, self.getaddr(144), self.codes.DP_PATH_TIMINGS, + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ), + CodeRegField(consts.DP_PATH_DEINIT_DURATION, self.getaddr(144), self.codes.DP_PATH_TIMINGS, + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ), + CodeRegField(consts.MODULE_PWRUP_DURATION, self.getaddr(167), self.codes.DP_PATH_TIMINGS, + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ), + CodeRegField(consts.MODULE_PWRDN_DURATION, self.getaddr(167), self.codes.DP_PATH_TIMINGS, + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ), + CodeRegField(consts.DP_TX_TURNON_DURATION, self.getaddr(168), self.codes.DP_PATH_TIMINGS, + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ), + CodeRegField(consts.DP_TX_TURNOFF_DURATION, self.getaddr(168), self.codes.DP_PATH_TIMINGS, + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ), + NumberRegField(consts.MEDIA_LANE_ASSIGNMENT_OPTION, self.getaddr(176), format="B", size=1), + + RegGroupField(consts.APPLS_ADVT_FIELD_PAGE01, + *(NumberRegField(f"{consts.MEDIA_LANE_ASSIGNMENT_OPTION}_{app}", self.getaddr(176 + (app - 1)), + format="B", size=1) for app in range(1, 16)), + + *(CodeRegField(f"{consts.HOST_ELECTRICAL_INTERFACE}_{app}", self.getaddr(223 + 4 * (app - 9)), + self.codes.HOST_ELECTRICAL_INTERFACE) for app in range(9, 16)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_850NM}_{app}", self.getaddr(224 + 4 * (app - 9)), + self.codes.NM_850_MEDIA_INTERFACE) for app in range(9, 16)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_SM}_{app}", self.getaddr(224 + 4 * (app - 9)), + self.codes.SM_MEDIA_INTERFACE) for app in range(9, 16)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_PASSIVE_COPPER}_{app}", self.getaddr(224 + 4 * (app - 9)), + self.codes.PASSIVE_COPPER_MEDIA_INTERFACE) for app in range(9, 16)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_ACTIVE_CABLE}_{app}", self.getaddr(224 + 4 * (app - 9)), + self.codes.ACTIVE_CABLE_MEDIA_INTERFACE) for app in range(9, 16)), + + *(CodeRegField(f"{consts.MODULE_MEDIA_INTERFACE_BASE_T}_{app}", self.getaddr(224 + 4 * (app - 9)), + self.codes.BASE_T_MEDIA_INTERFACE) for app in range(9, 16)), + + *(NumberRegField(f"{consts.MEDIA_LANE_COUNT}_{lane}", self.getaddr(225 + 4 * (lane - 9)), + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ) for lane in range(9, 16)), + + *(NumberRegField(f"{consts.HOST_LANE_COUNT}_{lane}", self.getaddr(225 + 4 * (lane - 9)), + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ) for lane in range(9, 16)), + + *(NumberRegField(f"{consts.HOST_LANE_ASSIGNMENT_OPTION}_{app}", self.getaddr(226 + 4 * (app - 9)), + format="B", size=1) for app in range(9, 16)), + ), + ] + + self.fields[consts.MODULE_MONITORS_FIELD] = [ + NumberRegField(consts.AUX_MON_TYPE, self.getaddr(145), size=1), + ] + + self.fields[consts.MODULE_CHAR_ADVT_FIELD] = [ + NumberRegField(consts.PAGE_SUPPORT_ADVT_FIELD, self.getaddr(142), + RegBitField(consts.VDM_SUPPORTED, 6), + RegBitField(consts.DIAG_PAGE_SUPPORT_ADVT_FIELD, 5), + ), + NumberRegField(consts.TX_INPUT_EQ_MAX, self.getaddr(153), + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ), + NumberRegField(consts.RX_OUTPUT_LEVEL_SUPPORT, self.getaddr(153), + RegBitField(consts.RX_OUTPUT_LEVEL_0_SUPPORTED, 4), + RegBitField(consts.RX_OUTPUT_LEVEL_1_SUPPORTED, 5), + RegBitField(consts.RX_OUTPUT_LEVEL_2_SUPPORTED, 6), + RegBitField(consts.RX_OUTPUT_LEVEL_3_SUPPORTED, 7), + ), + NumberRegField(consts.RX_OUTPUT_EQ_PRE_CURSOR_MAX, self.getaddr(154), + *(RegBitField(f"Bit{bit}", bit) for bit in range(0, 4)) + ), + NumberRegField(consts.RX_OUTPUT_EQ_POST_CURSOR_MAX, self.getaddr(154), + *(RegBitField(f"Bit{bit}", bit) for bit in range(4, 8)) + ), + NumberRegField(consts.CTRLS_ADVT_FIELD, self.getaddr(155), + RegBitField(consts.TX_DISABLE_SUPPORT_FIELD, 1), + size=2, format=" 0xB0:142. + field = self.mm.get_field(consts.TEMPERATURE_FIELD) + assert field.get_offset() == NVIDIA_ELS_MODULE_STATUS_PAGE * 128 + 142 + + def test_voltage_high_alarm_resolves_to_standard_page2_byte136(self): + field = self.mm.get_field(consts.VOLTAGE_HIGH_ALARM_FIELD) + assert field.get_offset() == 0x02 * 128 + 136 + + def test_custom_mon_value_resolves_to_standard_page0_byte24(self): + rg = self.mm.get_field(NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD) + assert rg.get_offset() == 24 + assert any(f.name == consts.CUSTOM_MON for f in rg.fields) + + def test_custom_mon_thresholds_resolve_to_standard_page2(self): + rg = self.mm.get_field(NVIDIA_ELS_CUSTOM_MON_THRESHOLDS_FIELD) + assert rg.get_offset() == 0x02 * 128 + 168 + names = {f.name for f in rg.fields} + assert names == { + consts.CUSTOM_MON_HIGH_ALARM, + consts.CUSTOM_MON_LOW_ALARM, + consts.CUSTOM_MON_HIGH_WARN, + consts.CUSTOM_MON_LOW_WARN, + } + + def test_voltage_resolves_to_standard_page0_byte16(self): + rg = self.mm.get_field(NVIDIA_ELS_VOLTAGE_FIELD) + assert rg.get_offset() == 16 + assert any(f.name == consts.VOLTAGE_FIELD for f in rg.fields) + + def test_voltage_thresholds_resolve_to_standard_page2_bytes_136_to_143(self): + rg = self.mm.get_field(NVIDIA_ELS_VOLTAGE_THRESHOLDS_FIELD) + assert rg.get_offset() == 0x02 * 128 + 136 + names = {f.name for f in rg.fields} + assert names == { + consts.VOLTAGE_HIGH_ALARM_FIELD, + consts.VOLTAGE_LOW_ALARM_FIELD, + consts.VOLTAGE_HIGH_WARNING_FIELD, + consts.VOLTAGE_LOW_WARNING_FIELD, + } + + def test_extension_pages_are_distinct_groups_from_b0_b3(self): + """Verifies the Custom-Mon extension is not merged with the B0/B3 mirrors + (otherwise the RegGroupField would span kilobytes and break reads).""" + custom_mon_value = self.mm.get_field(NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD) + b0_mirror_monitors = self.mm.get_field(consts.MODULE_MONITORS_PAGE0_FIELD) + assert custom_mon_value.get_offset() != b0_mirror_monitors.get_offset() + b0_field_names = {f.name for f in b0_mirror_monitors.fields} + assert consts.CUSTOM_MON not in b0_field_names \ + or b0_mirror_monitors.get_offset() >= NVIDIA_ELS_MODULE_STATUS_PAGE * 128 + + +class TestExtensionPageStandalone: + + def test_value_page_addresses(self): + page = NvidiaCpoElsCustomMonValuePage(NvidiaCpoElsCodes) + assert page.page == 0 + fields = page.fields[NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD] + assert len(fields) == 1 + assert fields[0].name == consts.CUSTOM_MON + assert fields[0].get_offset() == 24 + + def test_thresholds_page_addresses(self): + page = NvidiaCpoElsCustomMonThresholdsPage(NvidiaCpoElsCodes) + assert page.page == 0x02 + fields = page.fields[NVIDIA_ELS_CUSTOM_MON_THRESHOLDS_FIELD] + assert [f.name for f in fields] == [ + consts.CUSTOM_MON_HIGH_ALARM, + consts.CUSTOM_MON_LOW_ALARM, + consts.CUSTOM_MON_HIGH_WARN, + consts.CUSTOM_MON_LOW_WARN, + ] + for i, f in enumerate(fields): + assert f.get_offset() == 0x02 * 128 + 168 + i * 2 + + def test_voltage_page_addresses(self): + page = NvidiaCpoElsVoltagePage(NvidiaCpoElsCodes) + assert page.page == 0 + fields = page.fields[NVIDIA_ELS_VOLTAGE_FIELD] + assert len(fields) == 1 + assert fields[0].name == consts.VOLTAGE_FIELD + assert fields[0].get_offset() == 16 + + def test_voltage_thresholds_page_addresses(self): + page = NvidiaCpoElsVoltageThresholdsPage(NvidiaCpoElsCodes) + assert page.page == 0x02 + fields = page.fields[NVIDIA_ELS_VOLTAGE_THRESHOLDS_FIELD] + assert [f.name for f in fields] == [ + consts.VOLTAGE_HIGH_ALARM_FIELD, + consts.VOLTAGE_LOW_ALARM_FIELD, + consts.VOLTAGE_HIGH_WARNING_FIELD, + consts.VOLTAGE_LOW_WARNING_FIELD, + ] + for i, f in enumerate(fields): + assert f.get_offset() == 0x02 * 128 + 136 + i * 2 + + +class TestCdbReadElsLaserMonitoring: + + def test_init_defaults(self): + cmd = CdbReadElsLaserMonitoring() + assert cmd.cmd_id == CDB_READ_ELS_LASER_MONITORING_CMD + assert cmd.epl == 0 + assert cmd.lpl == 3 + assert cmd.rpl_field == NVIDIA_CPO_ELS_LASER_MONITORING_REPLY + + def test_encode_packs_payload_dict_into_3_byte_lpl(self): + cmd = CdbReadElsLaserMonitoring() + encoded = cmd.encode({"cap_mask": 0x1F, "bank_id": 2, "laser_mask": 0xAA}) + + # CDBCommand.encode prepends an 8-byte header (id|epl|lpl|cksum|rpl). + assert encoded[:2] == b"\x90\x18" + assert encoded[4] == 3 + assert encoded[8] == 0x1F + assert encoded[9] == 0x02 + assert encoded[10] == 0xAA + + def test_encode_defaults_cap_mask_to_all(self): + cmd = CdbReadElsLaserMonitoring() + encoded = cmd.encode({"bank_id": 1}) + assert encoded[8] == ELS_LASER_MONITORING_CAP_MASK_ALL + assert encoded[9] == 1 + assert encoded[10] == 0 + + def test_encode_defaults_bank_id_to_zero(self): + cmd = CdbReadElsLaserMonitoring() + encoded = cmd.encode({"cap_mask": 0x05}) + assert encoded[8] == 0x05 + assert encoded[9] == 0 + assert encoded[10] == 0 + + def test_encode_truncates_oversize_values_to_one_byte(self): + cmd = CdbReadElsLaserMonitoring() + encoded = cmd.encode({"cap_mask": 0x1FF, "bank_id": 0x100, "laser_mask": 0xABCD}) + assert encoded[8] == 0xFF + assert encoded[9] == 0x00 + assert encoded[10] == 0xCD + + +class TestNvidiaCpoElsCdbMemMap: + + def setup_method(self): + self.mm = NvidiaCpoElsCdbMemMap(NvidiaCpoElsCdbCodes) + + def test_inherits_standard_cdb_commands(self): + from sonic_platform_base.sonic_xcvr.fields import cdb_consts as cc + assert self.mm.get_cdb_cmd(cc.CDB_QUERY_STATUS_CMD) is not None + + def test_registers_laser_monitoring_command(self): + cmd = self.mm.get_cdb_cmd(CDB_READ_ELS_LASER_MONITORING_CMD) + assert cmd is not None + assert isinstance(cmd, CdbReadElsLaserMonitoring) + + def test_reply_field_is_registered_at_lpl_offset(self): + rg = self.mm.get_field(NVIDIA_CPO_ELS_LASER_MONITORING_REPLY) + assert rg is not None + assert rg.get_offset() == _LPL_REPLY_BASE + 0 + # Last field is u16 module-power at offset 52. + assert rg.get_size() == 54 + + def test_reply_decode_full(self): + reply = bytearray(54) + reply[0] = 0x1F # cap mask + reply[1] = 0x05 # bank echo + reply[2] = 0xAA # laser mask echo + # offset 3 reserved + for laser in range(NUM_LASERS): + # u16 BE: laser_mpd (offset 4), tec_voltage (offset 20) + reply[4 + laser * 2:4 + laser * 2 + 2] = (1000 + laser).to_bytes(2, "big") + reply[20 + laser * 2:20 + laser * 2 + 2] = (2000 + laser).to_bytes(2, "big") + # u8: laser_health (offset 36), tec_health (offset 44) + reply[36 + laser] = 100 + laser + reply[44 + laser] = 200 + laser + # u16 BE module power at offset 52. + reply[52:54] = (3050).to_bytes(2, "big") + + rg = self.mm.get_field(NVIDIA_CPO_ELS_LASER_MONITORING_REPLY) + decoded = rg.decode(bytes(reply)) + + assert decoded[NVIDIA_CPO_ELS_LASER_MON_CAP] == 0x1F + assert decoded[NVIDIA_CPO_ELS_LASER_MON_BANK] == 0x05 + assert decoded[NVIDIA_CPO_ELS_LASER_MON_MASK] == 0xAA + assert decoded[NVIDIA_CPO_ELS_MODULE_POWER] == 305.0 + + for laser in range(NUM_LASERS): + assert decoded["%s_%d" % (NVIDIA_CPO_ELS_LASER_MPD, laser)] == \ + (1000 + laser) / 1000.0 + assert decoded["%s_%d" % (NVIDIA_CPO_ELS_TEC_VOLTAGE, laser)] == \ + 2000 + laser + assert decoded["%s_%d" % (NVIDIA_CPO_ELS_LASER_HEALTH, laser)] == \ + (100 + laser) * 5.0 + assert decoded["%s_%d" % (NVIDIA_CPO_ELS_TEC_HEALTH, laser)] == \ + (200 + laser) * 5.0 + + +class TestGetElsStatus: + def test_full_status(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.side_effect = lambda f: { + consts.TRANS_MODULE_STATUS_FIELD: { + consts.MODULE_STATE: "ModuleReady", + consts.MODULE_FAULT_CAUSE: "NoFault", + }, + consts.VOLTAGE_FIELD: 3.31, + }.get(f) + out = api.get_els_status() + assert out == { + "els_module_state": "ModuleReady", + "els_module_fault_cause": "NoFault", + "els_vcc": 3.31, + } + + def test_status_partial_when_status_read_fails(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.side_effect = lambda f: 3.4 if f == consts.VOLTAGE_FIELD else None + out = api.get_els_status() + # Status group failed -> 'Unknown'; vcc succeeded independently. + assert out["els_module_state"] == "Unknown" + assert out["els_module_fault_cause"] == "Unknown" + assert out["els_vcc"] == 3.4 + + def test_status_all_unknown_when_both_reads_fail(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = None + assert api.get_els_status() == { + "els_module_state": "Unknown", + "els_module_fault_cause": "Unknown", + "els_vcc": "N/A", + } + + +class TestGetElsDomSensors: + @staticmethod + def _read_side_effect(per_field): + def fake(field): + return per_field.get(field) + return fake + + def test_returns_temperature_and_voltage(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.side_effect = self._read_side_effect({ + NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD: {consts.CUSTOM_MON: 47.5}, + NVIDIA_ELS_VOLTAGE_FIELD: {consts.VOLTAGE_FIELD: 3.4}, + }) + out = api.get_els_dom_sensors() + assert out == {"els_temperature": 47.5, "els_voltage": 3.4} + + def test_reads_only_from_nvidia_extension_groups(self): + """Verifies dispatch via NVIDIA-named groups, not via the B0 mirror.""" + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = None + api.get_els_dom_sensors() + assert api.xcvr_eeprom.read.call_args_list == [ + call(NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD), + call(NVIDIA_ELS_VOLTAGE_FIELD), + ] + + def test_omits_voltage_when_only_temperature_succeeds(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.side_effect = self._read_side_effect({ + NVIDIA_ELS_CUSTOM_MON_VALUE_FIELD: {consts.CUSTOM_MON: 30.0}, + }) + assert api.get_els_dom_sensors() == {"els_temperature": 30.0} + + def test_omits_temperature_when_only_voltage_succeeds(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.side_effect = self._read_side_effect({ + NVIDIA_ELS_VOLTAGE_FIELD: {consts.VOLTAGE_FIELD: 3.3}, + }) + assert api.get_els_dom_sensors() == {"els_voltage": 3.3} + + def test_returns_empty_when_both_reads_fail(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = None + assert api.get_els_dom_sensors() == {} + + +class TestGetElsCmonTempThresholds: + + def test_full_decode(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = { + consts.CUSTOM_MON_HIGH_ALARM: 80.0, + consts.CUSTOM_MON_LOW_ALARM: -5.0, + consts.CUSTOM_MON_HIGH_WARN: 70.0, + consts.CUSTOM_MON_LOW_WARN: 0.0, + } + out = api.get_els_cmon_temp_thresholds() + assert out == { + "els_temphighalarm": 80.0, + "els_templowalarm": -5.0, + "els_temphighwarning": 70.0, + "els_templowwarning": 0.0, + } + + def test_reads_from_nvidia_extension_group(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = {} + api.get_els_cmon_temp_thresholds() + api.xcvr_eeprom.read.assert_called_once_with(NVIDIA_ELS_CUSTOM_MON_THRESHOLDS_FIELD) + + def test_all_na_on_read_failure(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = None + assert api.get_els_cmon_temp_thresholds() == { + "els_temphighalarm": "N/A", + "els_templowalarm": "N/A", + "els_temphighwarning": "N/A", + "els_templowwarning": "N/A", + } + + +class TestGetElsDomFlags: + def test_decode_all_flags_set(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = 0xF0 # bits 4..7 all set + out = api.get_els_dom_flags() + assert out == { + "els_custom_mon_high_alarm": True, + "els_custom_mon_low_alarm": True, + "els_custom_mon_high_warning": True, + "els_custom_mon_low_warning": True, + } + + def test_decode_individual_bits(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = 1 << 6 # high warning only + out = api.get_els_dom_flags() + assert out["els_custom_mon_high_warning"] is True + assert out["els_custom_mon_high_alarm"] is False + assert out["els_custom_mon_low_alarm"] is False + assert out["els_custom_mon_low_warning"] is False + + def test_returns_none_when_byte_unreadable(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = None + assert api.get_els_dom_flags() == {} + + +class TestGetElsInfo: + def _admin_dict(self): + return { + consts.VENDOR_NAME_FIELD: "Nvidia", + consts.VENDOR_OUI_FIELD: "00-1c-23", + consts.VENDOR_PART_NO_FIELD: "ELSPN-001", + consts.VENDOR_REV_FIELD: "A1", + consts.VENDOR_SERIAL_NO_FIELD: "SN0001", + consts.VENDOR_DATE_FIELD: "2026-04-01", + consts.CONNECTOR_FIELD: "MPO", + consts.ID_FIELD: "CPO ELS", + consts.ID_ABBRV_FIELD: "ELS", + consts.CMIS_MAJOR_REVISION: 5, + consts.CMIS_MINOR_REVISION: 0, + consts.EXT_ID_FIELD: { + consts.POWER_CLASS_FIELD: "Power Class 5", + consts.MAX_POWER_FIELD: 12.5, + }, + } + + def test_full_info(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.side_effect = lambda f: { + consts.ADMIN_INFO_FIELD: self._admin_dict(), + consts.ADVERTISING_FIELD: { + consts.HW_MAJOR_REV: 1, consts.HW_MINOR_REV: 2, + }, + }.get(f) + + out = api.get_els_info() + assert out["els_manufacturer"] == "Nvidia" + assert out["els_model"] == "ELSPN-001" + assert out["els_serial"] == "SN0001" + assert out["els_cmis_rev"] == "5.0" + assert out["els_ext_identifier"] == "Power Class 5" + assert out["els_hardware_rev"] == "1.2" + # No CMIS analog for cable_type yet. + assert out["els_cable_type"] == "N/A" + + def test_missing_advertising_uses_admin_only(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.side_effect = lambda f: ( + self._admin_dict() if f == consts.ADMIN_INFO_FIELD else None) + out = api.get_els_info() + # Admin group succeeded; advertising failed -> hardware_rev='Unknown'. + assert out["els_hardware_rev"] == "Unknown" + assert out["els_manufacturer"] == "Nvidia" + + def test_missing_ext_id_falls_back_to_na(self): + api = _new_els_api_stub() + admin = self._admin_dict() + admin[consts.EXT_ID_FIELD] = None + api.xcvr_eeprom.read.side_effect = lambda f: ( + admin if f == consts.ADMIN_INFO_FIELD else None) + out = api.get_els_info() + assert out["els_ext_identifier"] == "N/A" + + def test_missing_cmis_rev_falls_back_to_na(self): + api = _new_els_api_stub() + admin = self._admin_dict() + admin[consts.CMIS_MAJOR_REVISION] = None + admin[consts.CMIS_MINOR_REVISION] = None + api.xcvr_eeprom.read.side_effect = lambda f: ( + admin if f == consts.ADMIN_INFO_FIELD else None) + out = api.get_els_info() + assert out["els_cmis_rev"] == "N/A" + + def test_all_unknown_when_both_reads_fail(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = None + # Both admin + advertising groups failed -> every els_* key is 'Unknown'. + assert api.get_els_info() == { + "els_manufacturer": "Unknown", + "els_vendor_oui": "Unknown", + "els_model": "Unknown", + "els_vendor_rev": "Unknown", + "els_serial": "Unknown", + "els_vendor_date": "Unknown", + "els_connector": "Unknown", + "els_type": "Unknown", + "els_type_abbrv_name": "Unknown", + "els_cmis_rev": "Unknown", + "els_ext_identifier": "Unknown", + "els_cable_type": "Unknown", + "els_hardware_rev": "Unknown", + } + + +class TestGetElsThresholds: + """Verifies the els_vcc* contribution from the standard CMIS Voltage thresholds slot.""" + + def test_full_decode(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = { + consts.VOLTAGE_HIGH_ALARM_FIELD: 3.6, + consts.VOLTAGE_LOW_ALARM_FIELD: 3.0, + consts.VOLTAGE_HIGH_WARNING_FIELD: 3.5, + consts.VOLTAGE_LOW_WARNING_FIELD: 3.1, + } + out = api.get_els_thresholds() + assert out == { + "els_vcchighalarm": 3.6, + "els_vcclowalarm": 3.0, + "els_vcchighwarning": 3.5, + "els_vcclowwarning": 3.1, + } + + def test_reads_from_nvidia_extension_group(self): + """Verifies dispatch via the NVIDIA-named voltage-thresholds group, not the B3 mirror.""" + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = {} + api.get_els_thresholds() + api.xcvr_eeprom.read.assert_called_once_with(NVIDIA_ELS_VOLTAGE_THRESHOLDS_FIELD) + + def test_all_na_on_read_failure(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = None + assert api.get_els_thresholds() == { + "els_vcchighalarm": "N/A", + "els_vcclowalarm": "N/A", + "els_vcchighwarning": "N/A", + "els_vcclowwarning": "N/A", + } + + def test_missing_subfields_become_na(self): + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = { + consts.VOLTAGE_HIGH_ALARM_FIELD: 3.6, + } + out = api.get_els_thresholds() + assert out["els_vcchighalarm"] == 3.6 + assert out["els_vcclowwarning"] == "N/A" + + def test_does_not_emit_temp_or_power_or_bias_or_rx_keys(self): + """Spec-alignment guard: temp / power / bias come from sibling getters.""" + api = _new_els_api_stub() + api.xcvr_eeprom.read.return_value = { + consts.VOLTAGE_HIGH_ALARM_FIELD: 3.6, + } + out = api.get_els_thresholds() + forbidden_substrings = ("temp", "txpower", "txbias", "rxpower", "optpower", "bias") + for k in out: + assert not any(sub in k for sub in forbidden_substrings), \ + "get_els_thresholds leaked non-vcc key: %s" % k + + +class TestGetElsLaserMonitoring: + def _decoded_reply(self, cap=0x1F): + raw = { + NVIDIA_CPO_ELS_LASER_MON_CAP: cap, + NVIDIA_CPO_ELS_LASER_MON_BANK: 0, + NVIDIA_CPO_ELS_LASER_MON_MASK: 0, + NVIDIA_CPO_ELS_MODULE_POWER: 30.5, + } + for laser in range(NUM_LASERS): + raw["%s_%d" % (NVIDIA_CPO_ELS_LASER_MPD, laser)] = (1500 + laser) / 1000.0 + raw["%s_%d" % (NVIDIA_CPO_ELS_TEC_VOLTAGE, laser)] = 2500 + laser + raw["%s_%d" % (NVIDIA_CPO_ELS_LASER_HEALTH, laser)] = (100 + laser) * 5.0 + raw["%s_%d" % (NVIDIA_CPO_ELS_TEC_HEALTH, laser)] = (50 + laser) * 5.0 + return raw + + def test_returns_none_when_no_cdb_handler(self): + api = _new_els_api_stub(cdb_handler=None) + assert api.get_els_laser_monitoring() == {} + + def test_returns_none_when_send_returns_false(self): + cdb = MagicMock() + cdb.send_cmd.return_value = False + api = _new_els_api_stub(cdb_handler=cdb) + assert api.get_els_laser_monitoring() == {} + assert not cdb.read_reply.called + + def test_returns_none_when_send_raises(self): + cdb = MagicMock() + cdb.send_cmd.side_effect = RuntimeError("boom") + api = _new_els_api_stub(cdb_handler=cdb) + assert api.get_els_laser_monitoring() == {} + + def test_returns_none_when_read_reply_raises(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.side_effect = RuntimeError("bad reply") + api = _new_els_api_stub(cdb_handler=cdb) + assert api.get_els_laser_monitoring() == {} + + def test_returns_none_when_read_reply_is_none(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = None + api = _new_els_api_stub(cdb_handler=cdb) + assert api.get_els_laser_monitoring() == {} + + def test_payload_is_dict_with_full_cap_mask_and_bank(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = self._decoded_reply() + api = _new_els_api_stub(cdb_handler=cdb, bank_id=2) + api.get_els_laser_monitoring() + + cmd_id = cdb.send_cmd.call_args[0][0] + payload = cdb.send_cmd.call_args[1]["payload"] + assert cmd_id == CDB_READ_ELS_LASER_MONITORING_CMD + assert payload == { + "cap_mask": ELS_LASER_MONITORING_CAP_MASK_ALL, + "bank_id": 2, + "laser_mask": 0x00, + } + + def test_payload_propagates_caller_overrides(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = self._decoded_reply() + api = _new_els_api_stub(cdb_handler=cdb, bank_id=1) + api.get_els_laser_monitoring(cap_mask=0x05, laser_mask=0xAA) + payload = cdb.send_cmd.call_args[1]["payload"] + assert payload == {"cap_mask": 0x05, "bank_id": 1, "laser_mask": 0xAA} + + def test_returns_raw_decoded_dict_unchanged(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + decoded = self._decoded_reply() + cdb.read_reply.return_value = decoded + api = _new_els_api_stub(cdb_handler=cdb) + + out = api.get_els_laser_monitoring() + assert out is decoded + assert out[NVIDIA_CPO_ELS_LASER_MON_CAP] == 0x1F + assert out[NVIDIA_CPO_ELS_MODULE_POWER] == 30.5 + for laser in range(NUM_LASERS): + assert out["%s_%d" % (NVIDIA_CPO_ELS_LASER_MPD, laser)] == \ + (1500 + laser) / 1000.0 + + +class TestGetElsLaserDomSensors: + """Verifies the spec-aligned projection of CDB 0x9018 onto els_* DOM_SENSOR keys.""" + + def _decoded_reply(self, cap=0x1F): + raw = { + NVIDIA_CPO_ELS_LASER_MON_CAP: cap, + NVIDIA_CPO_ELS_LASER_MON_BANK: 0, + NVIDIA_CPO_ELS_LASER_MON_MASK: 0, + NVIDIA_CPO_ELS_MODULE_POWER: 30.5, + } + for laser in range(NUM_LASERS): + raw["%s_%d" % (NVIDIA_CPO_ELS_LASER_MPD, laser)] = (1500 + laser) / 1000.0 + raw["%s_%d" % (NVIDIA_CPO_ELS_TEC_VOLTAGE, laser)] = 2500 + laser + raw["%s_%d" % (NVIDIA_CPO_ELS_LASER_HEALTH, laser)] = (100 + laser) * 5.0 + raw["%s_%d" % (NVIDIA_CPO_ELS_TEC_HEALTH, laser)] = (50 + laser) * 5.0 + return raw + + def _api_with_raw(self, raw): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = raw + return _new_els_api_stub(cdb_handler=cdb) + + def test_returns_empty_when_raw_empty(self): + cdb = MagicMock() + cdb.send_cmd.return_value = False + api = _new_els_api_stub(cdb_handler=cdb) + assert api.get_els_laser_dom_sensors() == {} + + def test_returns_empty_when_no_cdb_handler(self): + api = _new_els_api_stub(cdb_handler=None) + assert api.get_els_laser_dom_sensors() == {} + + def test_full_decode_renames_to_spec_keys(self): + api = self._api_with_raw(self._decoded_reply()) + out = api.get_els_laser_dom_sensors() + + assert out["els_power_consumption"] == 30.5 + + # 0-indexed CDB suffix _0 -> 1-indexed spec suffix concatenated to the prefix. + for laser in range(NUM_LASERS): + n = laser + 1 + assert out["els_laser_mpd%d" % n] == (1500 + laser) / 1000.0 + assert out["els_tec_voltage_laser%d" % n] == 2500 + laser + assert out["els_health_value_laser%d" % n] == (100 + laser) * 5.0 + assert out["els_tec_health_value_laser%d" % n] == (50 + laser) * 5.0 + + def test_strips_cdb_framing_bytes(self): + """cap/bank/mask are CDB transport metadata and must not reach STATE_DB.""" + api = self._api_with_raw(self._decoded_reply()) + out = api.get_els_laser_dom_sensors() + for framing in (NVIDIA_CPO_ELS_LASER_MON_CAP, + NVIDIA_CPO_ELS_LASER_MON_BANK, + NVIDIA_CPO_ELS_LASER_MON_MASK): + assert framing not in out + + def test_emits_only_els_prefixed_keys(self): + """Spec-alignment guard: every emitted key must start with 'els_'.""" + api = self._api_with_raw(self._decoded_reply()) + out = api.get_els_laser_dom_sensors() + assert out, "expected non-empty projection from the full raw dict" + for k in out: + assert k.startswith("els_"), \ + "non-spec key leaked from get_els_laser_dom_sensors: %s" % k + assert not k.startswith("NvidiaCpoEls"), \ + "raw CDB key leaked from get_els_laser_dom_sensors: %s" % k + + def test_partial_reply_only_emits_present_fields(self): + """A partial raw reply emits only the keys present (no 'N/A' fillers).""" + raw = { + NVIDIA_CPO_ELS_MODULE_POWER: 12.0, + "%s_0" % NVIDIA_CPO_ELS_TEC_VOLTAGE: 2500, + "%s_3" % NVIDIA_CPO_ELS_LASER_HEALTH: 250.0, + } + api = self._api_with_raw(raw) + out = api.get_els_laser_dom_sensors() + assert out == { + "els_power_consumption": 12.0, + "els_tec_voltage_laser1": 2500, + "els_health_value_laser4": 250.0, + } + + +class TestAggregatorOverrides: + + def _api_with_stubbed_children(self, **child_returns): + api = _new_els_api_stub() + for name, value in child_returns.items(): + setattr(api, name, MagicMock(return_value=value)) + return api + + def test_info_returns_els_info_only(self): + api = self._api_with_stubbed_children( + get_els_info={ + "els_manufacturer": "Nvidia", + "els_model": "ELSPN-001", + "els_hardware_rev": "1.2", + }, + ) + out = api.get_transceiver_info() + assert out == { + "els_manufacturer": "Nvidia", + "els_model": "ELSPN-001", + "els_hardware_rev": "1.2", + } + + def test_info_returns_empty_when_els_info_empty(self): + api = self._api_with_stubbed_children(get_els_info={}) + assert api.get_transceiver_info() == {} + + def test_dom_real_value_merges_lane_temp_and_laser_monitoring(self): + """Unions ELSFP lane monitors, ELS dom sensors, and the CDB 0x9018 projection.""" + api = self._api_with_stubbed_children( + get_elsfp_lane_monitors={"els_bias_current_monitor1": 10}, + get_els_dom_sensors={"els_temperature": 50.0}, + get_els_laser_dom_sensors={ + "els_laser_mpd1": 1.5, + "els_power_consumption": 30.5, + }, + ) + out = api.get_transceiver_dom_real_value() + assert out == { + "els_bias_current_monitor1": 10, + "els_temperature": 50.0, + "els_laser_mpd1": 1.5, + "els_power_consumption": 30.5, + } + + def test_dom_real_value_returns_empty_when_all_children_empty(self): + api = self._api_with_stubbed_children( + get_elsfp_lane_monitors=None, + get_els_dom_sensors={}, + get_els_laser_dom_sensors={}, + ) + assert api.get_transceiver_dom_real_value() == {} + + def test_threshold_info_merges_elsfp_thresholds_setpoints_vcc_and_temp(self): + """Unions ELSFP bias/opt thresholds + per-lane setpoints, ELS vcc and temp thresholds.""" + api = self._api_with_stubbed_children( + get_elsfp_lane_thresholds={"els_biashighalarm": 1.0}, + get_elsfp_lane_setpoints={ + "els_bias_current_setpoint1": 20, + "els_opt_power_setpoint1": 1500, + }, + get_els_thresholds={"els_vcchighalarm": 3.6}, + get_els_cmon_temp_thresholds={"els_temphighalarm": 70.0}, + ) + out = api.get_transceiver_threshold_info() + assert out == { + "els_biashighalarm": 1.0, + "els_bias_current_setpoint1": 20, + "els_opt_power_setpoint1": 1500, + "els_vcchighalarm": 3.6, + "els_temphighalarm": 70.0, + } + + def test_dom_flags_merges_lane_flags_and_cmon(self): + """Unions ELSFP per-lane indexed flags and the NVIDIA Custom Monitor flag quartet.""" + api = self._api_with_stubbed_children( + get_elsfp_lane_flags={"els_HighBiasAlarm1": True}, + get_els_dom_flags={"els_custom_mon_high_alarm": False}, + ) + out = api.get_transceiver_dom_flags() + assert out == { + "els_HighBiasAlarm1": True, + "els_custom_mon_high_alarm": False, + } + + def test_dom_flags_does_not_emit_setpoints(self): + """Spec-alignment guard: setpoints belong in DOM_THRESHOLD_INFO, not DOM_FLAG.""" + api = self._api_with_stubbed_children( + get_elsfp_lane_flags={"els_HighBiasAlarm1": True}, + get_elsfp_lane_setpoints={ + "els_bias_current_setpoint1": 20, + "els_opt_power_setpoint1": 1500, + }, + get_els_dom_flags={"els_custom_mon_high_alarm": False}, + ) + out = api.get_transceiver_dom_flags() + for k in out: + assert "setpoint" not in k, \ + "lane setpoint leaked into get_transceiver_dom_flags: %s" % k + + def test_status_merges_lane_state_control_mode_codes_and_status(self): + """Unions ELSFP lane state + control mode + fault/warning codes and ELS module status.""" + api = self._api_with_stubbed_children( + get_elsfp_lane_state={ + "els_lane_enable1": 1, + "els_lane_state1": 2, + }, + get_elsfp_control_mode={"els_control_mode_APCACC": 0}, + get_elsfp_fault_warning_codes={ + "els_fault_code1": 0, + "els_warning_code1": 2, + }, + get_els_status={"els_module_state": "ModuleReady"}, + ) + out = api.get_transceiver_status() + assert out == { + "els_lane_enable1": 1, + "els_lane_state1": 2, + "els_control_mode_APCACC": 0, + "els_fault_code1": 0, + "els_warning_code1": 2, + "els_module_state": "ModuleReady", + } + + def test_status_does_not_emit_setpoint_or_output_fiber_keys(self): + """Spec-alignment guard: setpoints belong in DOM_THRESHOLD_INFO, output-fiber in STATUS_FLAGS.""" + api = self._api_with_stubbed_children( + get_elsfp_lane_state={"els_lane_enable1": 1}, + get_elsfp_control_mode={"els_control_mode_APCACC": 0}, + get_elsfp_lane_setpoints={"els_bias_current_setpoint1": 20}, + get_elsfp_output_fiber_checked={ + "els_output_fiber_checked_flag_lane1": 1, + }, + get_els_status={"els_module_state": "ModuleReady"}, + ) + out = api.get_transceiver_status() + for k in out: + assert "setpoint" not in k, \ + "lane setpoint leaked into get_transceiver_status: %s" % k + assert "output_fiber_checked" not in k, \ + "output-fiber-checked leaked into get_transceiver_status: %s" % k + + def test_status_flags_merges_per_lane_flags_and_output_fiber(self): + """Unions ELSFP per-lane fault/warn flag bytes and output-fiber-checked bits.""" + api = self._api_with_stubbed_children( + get_elsfp_status_flags={"els_fault_flag_lane1": True}, + get_elsfp_output_fiber_checked={ + "els_output_fiber_checked_flag_lane1": 1, + "els_output_fiber_checked_flag_lane2": 0, + }, + ) + out = api.get_transceiver_status_flags() + assert out == { + "els_fault_flag_lane1": True, + "els_output_fiber_checked_flag_lane1": 1, + "els_output_fiber_checked_flag_lane2": 0, + } + + def test_status_flags_does_not_emit_codes_or_laser_monitoring_keys(self): + """Spec-alignment guard: 4-bit codes are STATUS, laser monitoring is DOM_SENSOR.""" + api = self._api_with_stubbed_children( + get_elsfp_status_flags={"els_fault_flag_lane1": True}, + get_elsfp_output_fiber_checked={ + "els_output_fiber_checked_flag_lane1": 1, + }, + get_elsfp_fault_warning_codes={ + "els_fault_code1": 0, + "els_warning_code1": 2, + }, + get_els_laser_dom_sensors={ + "els_laser_mpd1": 1.5, + "els_power_consumption": 30.5, + }, + ) + out = api.get_transceiver_status_flags() + for k in out: + assert not k.endswith("_code1"), \ + "fault/warning code leaked into STATUS_FLAGS: %s" % k + assert "laser_mpd" not in k + assert "tec_voltage" not in k + assert "tec_health" not in k + assert "health_value" not in k + assert "power_consump" not in k + + def test_status_flags_returns_empty_when_all_children_empty(self): + api = self._api_with_stubbed_children( + get_elsfp_status_flags=None, + get_elsfp_output_fiber_checked=None, + ) + assert api.get_transceiver_status_flags() == {} + + +def test_factory_style_instantiation(): + """End-to-end: compose memmap + CDB memmap + API exactly as the factory does.""" + from sonic_platform_base.sonic_xcvr.xcvr_eeprom import XcvrEeprom + + reader = MagicMock(return_value=b"\x00") + writer = MagicMock(return_value=True) + + mm = NvidiaCpoElsCmisMemMap(NvidiaCpoElsCodes, bank=1) + eeprom = XcvrEeprom(reader, writer, mm) + cdb_mm = NvidiaCpoElsCdbMemMap(NvidiaCpoElsCdbCodes) + api = NvidiaCpoElsCmisApi(eeprom, cdb_mem_map=cdb_mm) + + assert api.cdb_handler is not None + assert api.cdb_handler.mem_map is cdb_mm + assert api.bank_id == 0 + + +class TestElsfpNestedGroupDecode: + """End-to-end regression for the nested RegGroupField decode. + + Several ELSFP 0x1A/0x1B groups are composed as a RegGroupField whose members + are themselves RegGroupFields, so RegGroupField.decode yields a nested dict. + The per-lane getters must resolve through that extra level; otherwise every + per-lane value comes back 'N/A' even when the EEPROM holds real data. These + tests run the real XcvrEeprom decode path with deterministic non-zero bytes + (the rest of the suite stubs the getters, which is why this slipped through). + """ + + @staticmethod + def _api_with_nonzero_eeprom(fill=0x12, bank_id=0): + from sonic_platform_base.sonic_xcvr.xcvr_eeprom import XcvrEeprom + from sonic_platform_base.sonic_xcvr.api.public.elsfp_cmis import ElsfpCmisApi + + mm = NvidiaCpoElsCmisMemMap(NvidiaCpoElsCodes) + eeprom = XcvrEeprom(lambda off, size: bytes([fill] * size), + lambda off, size, data: True, mm) + api = ElsfpCmisApi.__new__(ElsfpCmisApi) + api.xcvr_eeprom = eeprom + api.bank_id = bank_id + return api + + def _assert_no_na(self, getter_name, expected_len): + api = self._api_with_nonzero_eeprom() + out = getattr(api, getter_name)() + assert out is not None and len(out) == expected_len, \ + "%s returned %r" % (getter_name, out) + na = [k for k, v in out.items() if v == 'N/A'] + assert not na, "%s left these fields 'N/A': %s" % (getter_name, na) + + def test_lane_monitors_populated(self): + # 8 lanes x (bias, opt, voltage) + els_icc = 25 + self._assert_no_na("get_elsfp_lane_monitors", 25) + + def test_lane_setpoints_populated(self): + self._assert_no_na("get_elsfp_lane_setpoints", 16) + + def test_control_mode_populated(self): + self._assert_no_na("get_elsfp_control_mode", 1) + + def test_fault_warning_codes_populated(self): + self._assert_no_na("get_elsfp_fault_warning_codes", 16) + + def test_lane_state_populated(self): + # lane_enable is flat and already worked; lane_state was nested. + api = self._api_with_nonzero_eeprom() + out = api.get_elsfp_lane_state() + state_na = [k for k in out if k.startswith('els_lane_state') and out[k] == 'N/A'] + assert not state_na, "lane_state still 'N/A': %s" % state_na + + def test_status_flags_populated(self): + self._assert_no_na("get_elsfp_status_flags", 16) + + def test_icc_monitor_still_flat(self): + # els_icc comes from a flat member; guard against regressing it. + api = self._api_with_nonzero_eeprom() + assert api.get_elsfp_lane_monitors()['els_icc'] != 'N/A' diff --git a/tests/sonic_xcvr/test_nvidia_cpo_factory.py b/tests/sonic_xcvr/test_nvidia_cpo_factory.py new file mode 100644 index 000000000..1ca5f8616 --- /dev/null +++ b/tests/sonic_xcvr/test_nvidia_cpo_factory.py @@ -0,0 +1,144 @@ +"""Tests for the NVIDIA-CPO branches of XcvrApiFactory.""" +from unittest.mock import MagicMock, patch + +import pytest + +from sonic_platform_base.sonic_xcvr.api.public.cmis import CmisApi +from sonic_platform_base.sonic_xcvr.api.public.cpo import CpoApi +from sonic_platform_base.sonic_xcvr.api.public.elsfp_cmis import ElsfpCmisApi +from sonic_platform_base.sonic_xcvr.api.nvidia.cpo_oe import NvidiaCpoOeCmisApi +from sonic_platform_base.sonic_xcvr.api.nvidia.cpo_els import NvidiaCpoElsCmisApi +from sonic_platform_base.sonic_xcvr.mem_maps.nvidia.cpo_els import NvidiaCpoElsCmisMemMap +from sonic_platform_base.sonic_xcvr.mem_maps.nvidia.cpo_oe import NvidiaCpoOeMemMap +from sonic_platform_base.sonic_xcvr.cdb.nvidia.cpo_els_memmap import NvidiaCpoElsCdbMemMap +from sonic_platform_base.sonic_xcvr.cdb.nvidia.cpo_oe_memmap import NvidiaCpoOeCdbMemMap +from sonic_platform_base.sonic_xcvr.xcvr_api_factory import ( + NVIDIA_ELS_ADMIN_UPPER_PAGE, + NVIDIA_VENDOR_NAME, + XcvrApiFactory, +) + + +def _make_factory(): + reader = MagicMock(return_value=b"\x00") + writer = MagicMock(return_value=True) + return XcvrApiFactory(reader, writer) + + +class TestBuildCpoOeApi: + @patch.object(XcvrApiFactory, "_get_vendor_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + def test_nvidia_oe_gets_nvidia_class_and_cdb_memmap(self): + f = _make_factory() + api, els_admin_upper_page = f._build_cpo_oe_api(bank_id=1) + assert isinstance(api, NvidiaCpoOeCmisApi) + assert isinstance(api.xcvr_eeprom.mem_map, NvidiaCpoOeMemMap) + assert api.cdb_handler is not None + assert isinstance(api.cdb_handler.mem_map, NvidiaCpoOeCdbMemMap) + assert api.xcvr_eeprom.mem_map.bank == 1 + assert api._get_bank_id() == 1 + assert els_admin_upper_page == NVIDIA_ELS_ADMIN_UPPER_PAGE + + @patch.object(XcvrApiFactory, "_get_vendor_name", MagicMock(return_value="SomeOtherVendor")) + def test_unknown_oe_falls_back_to_generic_cmis(self): + f = _make_factory() + api, els_admin_upper_page = f._build_cpo_oe_api(bank_id=0) + assert isinstance(api, CmisApi) + assert not isinstance(api, NvidiaCpoOeCmisApi) + assert api.cdb_handler is None + assert els_admin_upper_page is None + + +class TestBuildCpoElsApi: + @patch.object(XcvrApiFactory, "_get_vendor_els_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + def test_nvidia_els_gets_nvidia_class_and_cdb_memmap(self): + f = _make_factory() + api = f._build_cpo_els_api(NVIDIA_ELS_ADMIN_UPPER_PAGE, bank_id=2) + assert isinstance(api, NvidiaCpoElsCmisApi) + assert isinstance(api.xcvr_eeprom.mem_map, NvidiaCpoElsCmisMemMap) + assert api.cdb_handler is not None + assert isinstance(api.cdb_handler.mem_map, NvidiaCpoElsCdbMemMap) + assert api.xcvr_eeprom.mem_map.bank == 2 + assert api.bank_id == 2 + + @patch.object(XcvrApiFactory, "_get_vendor_els_name", MagicMock(return_value="SomeOtherVendor")) + def test_unknown_els_falls_back_to_generic_elsfp(self): + f = _make_factory() + api = f._build_cpo_els_api(NVIDIA_ELS_ADMIN_UPPER_PAGE, bank_id=3) + assert isinstance(api, ElsfpCmisApi) + assert not isinstance(api, NvidiaCpoElsCmisApi) + assert api.cdb_handler is None + assert api.bank_id == 3 + + +class TestCreateCmisCpoApi: + @patch.object(XcvrApiFactory, "_get_vendor_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + @patch.object(XcvrApiFactory, "_get_vendor_els_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + def test_nvidia_oe_and_els_returns_cpo_api(self): + f = _make_factory() + api = f._create_cmis_cpo_api(bank_id=3) + assert isinstance(api, CpoApi) + assert isinstance(api.optical_engine_xcvr_api, NvidiaCpoOeCmisApi) + assert isinstance(api.external_laser_source_xcvr_api, NvidiaCpoElsCmisApi) + assert api.optical_engine_xcvr_api.xcvr_eeprom.mem_map.bank == 3 + assert api.optical_engine_xcvr_api._get_bank_id() == 3 + assert api.external_laser_source_xcvr_api.xcvr_eeprom.mem_map.bank == 3 + assert api.external_laser_source_xcvr_api.bank_id == 3 + + @patch.object(XcvrApiFactory, "_get_vendor_name", MagicMock(return_value="OtherOe")) + @patch.object(XcvrApiFactory, "_get_vendor_els_name", MagicMock(return_value="OtherEls")) + def test_unknown_vendors_still_produce_a_cpo_api(self): + f = _make_factory() + api = f._create_cmis_cpo_api(bank_id=0) + assert isinstance(api, CpoApi) + assert isinstance(api.optical_engine_xcvr_api, CmisApi) + assert not isinstance(api.optical_engine_xcvr_api, NvidiaCpoOeCmisApi) + assert isinstance(api.external_laser_source_xcvr_api, ElsfpCmisApi) + assert not isinstance(api.external_laser_source_xcvr_api, NvidiaCpoElsCmisApi) + + @patch.object(XcvrApiFactory, "_get_vendor_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + @patch.object(XcvrApiFactory, "_get_vendor_els_name", MagicMock(return_value="OtherEls")) + def test_mixed_vendors_each_half_picks_independently(self): + f = _make_factory() + api = f._create_cmis_cpo_api(bank_id=2) + assert isinstance(api.optical_engine_xcvr_api, NvidiaCpoOeCmisApi) + assert isinstance(api.external_laser_source_xcvr_api, ElsfpCmisApi) + assert not isinstance(api.external_laser_source_xcvr_api, NvidiaCpoElsCmisApi) + assert api.optical_engine_xcvr_api.xcvr_eeprom.mem_map.bank == 2 + assert api.optical_engine_xcvr_api._get_bank_id() == 2 + assert api.external_laser_source_xcvr_api.xcvr_eeprom.mem_map.bank == 2 + assert api.external_laser_source_xcvr_api.bank_id == 2 + + def test_bank_id_is_a_required_positional_argument(self): + f = _make_factory() + with pytest.raises(TypeError): + f._create_cmis_cpo_api() + + +class TestCreateXcvrApiCpoIdentifier: + @patch.object(XcvrApiFactory, "_get_vendor_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + @patch.object(XcvrApiFactory, "_get_vendor_els_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + def test_id_0x80_with_explicit_bank_id_propagates_to_memmaps(self): + f = _make_factory() + with patch.object(XcvrApiFactory, "_get_id", + MagicMock(return_value=0x80)): + api = f.create_xcvr_api(bank_id=2) + assert isinstance(api, CpoApi) + assert isinstance(api.optical_engine_xcvr_api, NvidiaCpoOeCmisApi) + assert isinstance(api.external_laser_source_xcvr_api, NvidiaCpoElsCmisApi) + assert api.optical_engine_xcvr_api.xcvr_eeprom.mem_map.bank == 2 + assert api.optical_engine_xcvr_api._get_bank_id() == 2 + assert api.external_laser_source_xcvr_api.xcvr_eeprom.mem_map.bank == 2 + assert api.external_laser_source_xcvr_api.bank_id == 2 + + @patch.object(XcvrApiFactory, "_get_vendor_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + @patch.object(XcvrApiFactory, "_get_vendor_els_name", MagicMock(return_value=NVIDIA_VENDOR_NAME)) + def test_id_0x80_without_bank_id_defaults_to_zero(self): + f = _make_factory() + with patch.object(XcvrApiFactory, "_get_id", + MagicMock(return_value=0x80)): + api = f.create_xcvr_api() + assert isinstance(api, CpoApi) + assert api.optical_engine_xcvr_api.xcvr_eeprom.mem_map.bank == 0 + assert api.optical_engine_xcvr_api._get_bank_id() == 0 + assert api.external_laser_source_xcvr_api.xcvr_eeprom.mem_map.bank == 0 + assert api.external_laser_source_xcvr_api.bank_id == 0 diff --git a/tests/sonic_xcvr/test_nvidia_cpo_oe.py b/tests/sonic_xcvr/test_nvidia_cpo_oe.py new file mode 100644 index 000000000..7a5e1601c --- /dev/null +++ b/tests/sonic_xcvr/test_nvidia_cpo_oe.py @@ -0,0 +1,325 @@ +"""Tests for the NVIDIA CPO Optical Engine (OE) memmaps and API.""" +import struct +from unittest.mock import MagicMock, patch + +from sonic_platform_base.sonic_xcvr.api.public.cmis import CmisApi +from sonic_platform_base.sonic_xcvr.codes.public.cmis import CmisCodes +from sonic_platform_base.sonic_xcvr.cdb.nvidia.cpo_oe_codes import NvidiaCpoOeCdbCodes +from sonic_platform_base.sonic_xcvr.fields import cdb_consts +from sonic_platform_base.sonic_xcvr.mem_maps.nvidia.cpo_oe import NvidiaCpoOeMemMap +from sonic_platform_base.sonic_xcvr.cdb.nvidia.cpo_oe_memmap import ( + CDB_READ_OE_TELEMETRY_CMD, + CDB_READ_OE_TELEMETRY_EPL_LEN, + NUM_LANES, + NVIDIA_CPO_OE_TELEMETRY_REPLY, + NVIDIA_CPO_OE_TLM_CAPABILITY, + NVIDIA_CPO_OE_TLM_LANE_BANK_ECHO, + NVIDIA_CPO_OE_TLM_LAST_REASON_OPCODE, + NVIDIA_CPO_OE_TLM_RX_HS_DATA_RATE, + NVIDIA_CPO_OE_TLM_RX_PS_LANE_STATE, + NVIDIA_CPO_OE_TLM_RX_PS_LANE_SUB_STATE, + NVIDIA_CPO_OE_TLM_TX_HS_DATA_RATE, + NVIDIA_CPO_OE_TLM_TX_PS_LANE_STATE, + NVIDIA_CPO_OE_TLM_TX_PS_LANE_SUB_STATE, + NVIDIA_CPO_OE_TLM_VALID, + NVIDIA_CPO_OE_TLM_VER_MAJOR, + NVIDIA_CPO_OE_TLM_VER_MINOR, + NVIDIA_CPO_OE_TLM_VER_RC, + OE_TELEMETRY_REQUEST_MASK_ALL, + CdbReadOeTelemetry, + NvidiaCpoOeCdbMemMap, +) +from sonic_platform_base.sonic_xcvr.api.nvidia.cpo_oe import NvidiaCpoOeCmisApi + + +def _epl_addr(byte_in_reply): + return cdb_consts.EPL_PAGE * 128 + 128 + byte_in_reply + + +def _make_blank_reply(): + return bytearray(CDB_READ_OE_TELEMETRY_EPL_LEN) + + +def _new_oe_api_stub(cdb_handler=None, mem_map_bank=0): + """Bypass NvidiaCpoOeCmisApi.__init__ and stick MagicMocks in place of EEPROM/CDB.""" + api = NvidiaCpoOeCmisApi.__new__(NvidiaCpoOeCmisApi) + eeprom = MagicMock() + eeprom.mem_map = MagicMock(bank=mem_map_bank) + api.xcvr_eeprom = eeprom + api._cdb_mem_map = MagicMock() if cdb_handler is not None else None + api._cdb_handler = cdb_handler + return api + + +class TestNvidiaCpoOeMemMap: + def test_instantiates_with_default_bank(self): + mm = NvidiaCpoOeMemMap(CmisCodes) + assert mm.bank == 0 + + def test_instantiates_with_explicit_bank(self): + mm = NvidiaCpoOeMemMap(CmisCodes, bank=2) + assert mm.bank == 2 + + def test_inherits_standard_cmis_fields(self): + mm = NvidiaCpoOeMemMap(CmisCodes) + from sonic_platform_base.sonic_xcvr.fields import consts + field = mm.get_field(consts.ADMIN_INFO_FIELD) + assert field is not None + + +class TestCdbReadOeTelemetry: + def test_init_defaults(self): + cmd = CdbReadOeTelemetry() + assert cmd.cmd_id == CDB_READ_OE_TELEMETRY_CMD + assert cmd.epl == CDB_READ_OE_TELEMETRY_EPL_LEN + assert cmd.lpl == 5 + assert cmd.rpl_field == NVIDIA_CPO_OE_TELEMETRY_REPLY + + def test_encode_packs_payload_dict_into_5_byte_lpl(self): + cmd = CdbReadOeTelemetry() + encoded = cmd.encode({"bank_id": 0x02, "request_mask": 0x000003F0}) + + # CDBCommand.encode prepends an 8-byte header (id|epl|lpl|cksum|rpl). + assert encoded[:2] == b"\x90\x30" + assert struct.unpack(">H", encoded[2:4])[0] == CDB_READ_OE_TELEMETRY_EPL_LEN + assert encoded[4] == 5 # lpl byte + # LPL payload starts at byte 8. + assert encoded[8] == 0x02 # bank_id + assert struct.unpack(">I", encoded[9:13])[0] == 0x000003F0 # request_mask + + def test_encode_defaults_request_mask_to_all(self): + cmd = CdbReadOeTelemetry() + encoded = cmd.encode({"bank_id": 1}) + assert encoded[8] == 1 + assert struct.unpack(">I", encoded[9:13])[0] == OE_TELEMETRY_REQUEST_MASK_ALL + + def test_encode_defaults_bank_id_to_zero(self): + cmd = CdbReadOeTelemetry() + encoded = cmd.encode({"request_mask": 0x12345678}) + assert encoded[8] == 0 + assert struct.unpack(">I", encoded[9:13])[0] == 0x12345678 + + def test_request_mask_all_covers_all_advertised_caps(self): + for bit in (4, 5, 6, 7, 8, 9, 18, 19, 20, 21): + assert OE_TELEMETRY_REQUEST_MASK_ALL & (1 << bit), \ + "cap bit %d should be in OE_TELEMETRY_REQUEST_MASK_ALL" % bit + + +class TestNvidiaCpoOeCdbMemMap: + def setup_method(self): + self.mm = NvidiaCpoOeCdbMemMap(NvidiaCpoOeCdbCodes) + + def test_inherits_standard_cdb_commands(self): + from sonic_platform_base.sonic_xcvr.fields import cdb_consts as cc + assert self.mm.get_cdb_cmd(cc.CDB_QUERY_STATUS_CMD) is not None + assert self.mm.get_cdb_cmd(cc.CDB_GET_FIRMWARE_INFO_CMD) is not None + + def test_registers_oe_telemetry_command(self): + cmd = self.mm.get_cdb_cmd(CDB_READ_OE_TELEMETRY_CMD) + assert cmd is not None + assert isinstance(cmd, CdbReadOeTelemetry) + assert cmd.get_reply_field() == NVIDIA_CPO_OE_TELEMETRY_REPLY + + def test_telemetry_reply_field_is_registered(self): + rg = self.mm.get_field(NVIDIA_CPO_OE_TELEMETRY_REPLY) + assert rg is not None + assert rg.get_offset() == _epl_addr(0) + # Last field is VER_RC (2 bytes at offset 166). + assert rg.get_size() == 168 + + def test_telemetry_reply_decode_full(self): + reply = _make_blank_reply() + + struct.pack_into(">I", reply, 0, 0xFFFFFFFF) + struct.pack_into(">I", reply, 4, 0xFFFFFFFF) + reply[8] = 0x07 + + # Per-lane state nibbles (4 bits per lane): even lane in bits 0..3, + # odd lane in bits 4..7. Lanes 0..7 carry values 1..8. + # Pack [1,2,3,4,5,6,7,8] -> bytes [0x21, 0x43, 0x65, 0x87]. + reply[43:47] = bytes([0x21, 0x43, 0x65, 0x87]) # tx_ps_lane_state + reply[47:51] = bytes([0x21, 0x43, 0x65, 0x87]) # rx_ps_lane_state + + for lane in range(NUM_LANES): + struct.pack_into(">H", reply, 51 + lane * 2, 0x0100 + lane) + struct.pack_into(">H", reply, 67 + lane * 2, 0x0200 + lane) + + # 3-bit RegBitsField: lanes 0..7 carry values 0..7 -> [0x10, 0x32, 0x54, 0x76]. + reply[83:87] = bytes([0x10, 0x32, 0x54, 0x76]) + reply[87:91] = bytes([0x10, 0x32, 0x54, 0x76]) + + reply[154:162] = bytes(range(8)) + + struct.pack_into(">H", reply, 162, 0x0102) + struct.pack_into(">H", reply, 164, 0x0304) + struct.pack_into(">H", reply, 166, 0x0506) + + rg = self.mm.get_field(NVIDIA_CPO_OE_TELEMETRY_REPLY) + decoded = rg.decode(bytes(reply)) + + assert decoded[NVIDIA_CPO_OE_TLM_CAPABILITY] == 0xFFFFFFFF + assert decoded[NVIDIA_CPO_OE_TLM_VALID] == 0xFFFFFFFF + assert decoded[NVIDIA_CPO_OE_TLM_LANE_BANK_ECHO] == 0x07 + + for lane in range(NUM_LANES): + byte_idx = lane // 2 + for prefix, expected in ( + (NVIDIA_CPO_OE_TLM_TX_PS_LANE_STATE, lane + 1), + (NVIDIA_CPO_OE_TLM_RX_PS_LANE_STATE, lane + 1), + (NVIDIA_CPO_OE_TLM_TX_HS_DATA_RATE, lane), + (NVIDIA_CPO_OE_TLM_RX_HS_DATA_RATE, lane), + ): + parent_key = "%s_byte%d" % (prefix, byte_idx) + lane_key = "%s_%d" % (prefix, lane) + assert decoded[parent_key][lane_key] == expected, \ + "%s lane %d: expected %d, got %r" % ( + prefix, lane, expected, decoded[parent_key][lane_key]) + + assert decoded["%s_%d" % (NVIDIA_CPO_OE_TLM_TX_PS_LANE_SUB_STATE, lane)] == 0x0100 + lane + assert decoded["%s_%d" % (NVIDIA_CPO_OE_TLM_RX_PS_LANE_SUB_STATE, lane)] == 0x0200 + lane + assert decoded["%s_%d" % (NVIDIA_CPO_OE_TLM_LAST_REASON_OPCODE, lane)] == lane + + assert decoded[NVIDIA_CPO_OE_TLM_VER_MAJOR] == 0x0102 + assert decoded[NVIDIA_CPO_OE_TLM_VER_MINOR] == 0x0304 + assert decoded[NVIDIA_CPO_OE_TLM_VER_RC] == 0x0506 + + +class TestGetOeTelemetry: + def _raw_reply_marker(self): + return {"sentinel": True} + + def test_returns_none_when_no_cdb_handler(self): + api = _new_oe_api_stub(cdb_handler=None) + assert api.get_oe_telemetry() is None + + def test_returns_none_when_send_returns_false(self): + cdb = MagicMock() + cdb.send_cmd.return_value = False + api = _new_oe_api_stub(cdb_handler=cdb) + assert api.get_oe_telemetry() is None + assert not cdb.read_reply.called + + def test_returns_none_when_send_raises(self): + cdb = MagicMock() + cdb.send_cmd.side_effect = RuntimeError("boom") + api = _new_oe_api_stub(cdb_handler=cdb) + assert api.get_oe_telemetry() is None + + def test_returns_none_when_read_reply_raises(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.side_effect = RuntimeError("bad reply") + api = _new_oe_api_stub(cdb_handler=cdb) + assert api.get_oe_telemetry() is None + + def test_returns_none_when_read_reply_is_none(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = None + api = _new_oe_api_stub(cdb_handler=cdb) + assert api.get_oe_telemetry() is None + + def test_returns_raw_decoded_reply_unchanged(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + sentinel = self._raw_reply_marker() + cdb.read_reply.return_value = sentinel + api = _new_oe_api_stub(cdb_handler=cdb) + assert api.get_oe_telemetry() is sentinel + + def test_send_cmd_invoked_with_payload_dict(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = self._raw_reply_marker() + api = _new_oe_api_stub(cdb_handler=cdb, mem_map_bank=2) + api.get_oe_telemetry() + + args, kwargs = cdb.send_cmd.call_args + assert args[0] == CDB_READ_OE_TELEMETRY_CMD + payload = kwargs["payload"] + assert payload == {"bank_id": 2, "request_mask": OE_TELEMETRY_REQUEST_MASK_ALL} + + def test_caller_can_override_request_mask(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = self._raw_reply_marker() + api = _new_oe_api_stub(cdb_handler=cdb, mem_map_bank=1) + api.get_oe_telemetry(request_mask=0x000000F0) + + payload = cdb.send_cmd.call_args[1]["payload"] + assert payload == {"bank_id": 1, "request_mask": 0x000000F0} + + def test_read_reply_invoked_with_cmd_id(self): + cdb = MagicMock() + cdb.send_cmd.return_value = True + cdb.read_reply.return_value = self._raw_reply_marker() + api = _new_oe_api_stub(cdb_handler=cdb) + api.get_oe_telemetry() + assert cdb.read_reply.call_args[0][0] == CDB_READ_OE_TELEMETRY_CMD + + +class TestGetTransceiverVdmRealValue: + + def _stub_get_oe_telemetry(self, api, value): + api.get_oe_telemetry = MagicMock(return_value=value) + + def test_merges_super_vdm_with_oe_telemetry(self): + api = _new_oe_api_stub() + self._stub_get_oe_telemetry(api, {"NvidiaCpoOeTlmVerMajor": 1, + "NvidiaCpoOeTlmVerMinor": 2}) + with patch.object(CmisApi, "get_transceiver_vdm_real_value", + return_value={"Laser Temperature [C]": 45.5}): + out = api.get_transceiver_vdm_real_value() + assert out == { + "Laser Temperature [C]": 45.5, + "NvidiaCpoOeTlmVerMajor": 1, + "NvidiaCpoOeTlmVerMinor": 2, + } + + def test_oe_telemetry_takes_precedence_on_key_collision(self): + api = _new_oe_api_stub() + self._stub_get_oe_telemetry(api, {"shared_key": "from_oe"}) + with patch.object(CmisApi, "get_transceiver_vdm_real_value", + return_value={"shared_key": "from_super"}): + out = api.get_transceiver_vdm_real_value() + assert out["shared_key"] == "from_oe" + + def test_returns_super_only_when_oe_telemetry_none(self): + api = _new_oe_api_stub() + self._stub_get_oe_telemetry(api, None) + with patch.object(CmisApi, "get_transceiver_vdm_real_value", + return_value={"x": 1}): + assert api.get_transceiver_vdm_real_value() == {"x": 1} + + def test_returns_oe_telemetry_only_when_super_none(self): + api = _new_oe_api_stub() + self._stub_get_oe_telemetry(api, {"NvidiaCpoOeTlmVerMajor": 7}) + with patch.object(CmisApi, "get_transceiver_vdm_real_value", + return_value=None): + assert api.get_transceiver_vdm_real_value() == { + "NvidiaCpoOeTlmVerMajor": 7, + } + + def test_returns_none_when_both_sources_empty(self): + api = _new_oe_api_stub() + self._stub_get_oe_telemetry(api, None) + with patch.object(CmisApi, "get_transceiver_vdm_real_value", + return_value=None): + assert api.get_transceiver_vdm_real_value() is None + + +def test_factory_style_instantiation(): + """End-to-end: build the OE memmap + CDB memmap + API the same way the factory does.""" + from sonic_platform_base.sonic_xcvr.xcvr_eeprom import XcvrEeprom + + reader = MagicMock(return_value=b"\x00") + writer = MagicMock(return_value=True) + + mm = NvidiaCpoOeMemMap(CmisCodes, bank=1) + eeprom = XcvrEeprom(reader, writer, mm) + cdb_mm = NvidiaCpoOeCdbMemMap(NvidiaCpoOeCdbCodes) + api = NvidiaCpoOeCmisApi(eeprom, cdb_mem_map=cdb_mm) + + assert api.cdb_handler is not None + assert api.cdb_handler.mem_map is cdb_mm + assert api._get_bank_id() == 1