diff --git a/sonic_platform_base/sonic_xcvr/api/public/cmis.py b/sonic_platform_base/sonic_xcvr/api/public/cmis.py index 87f974430..e2b368018 100644 --- a/sonic_platform_base/sonic_xcvr/api/public/cmis.py +++ b/sonic_platform_base/sonic_xcvr/api/public/cmis.py @@ -167,6 +167,27 @@ def get_cdb_fw_handler(self): def _get_vdm_key_to_db_prefix_map(self): return CMIS_VDM_KEY_TO_DB_PREFIX_KEY_MAP + def _get_vdm_key_to_db_prefix_map_by_observable_type(self, observable_type): + """ + Returns the VDM key-to-DB-prefix map filtered by observable type, + using the 'B'/'S' classification from VDM_TYPE as the single source of truth. + + Args: + observable_type: 'B' for basic (instantaneous), 'S' for statistic (min/max/avg) + + Returns: + dict: Filtered subset of _get_vdm_key_to_db_prefix_map() + """ + vdm_type_dict = self.xcvr_eeprom.mem_map.codes.VDM_TYPE + matching_names = { + info[0] for info in vdm_type_dict.values() + if len(info) > 3 and info[3] == observable_type + } + return { + k: v for k, v in self._get_vdm_key_to_db_prefix_map().items() + if k in matching_names + } + @staticmethod def _strip_str(val): return val.rstrip() if isinstance(val, str) else val @@ -1678,20 +1699,45 @@ def is_cdb_supported(self): return False + @read_only_cached_api_return def is_transceiver_vdm_supported(self): ''' This function returns whether VDM is supported ''' return self.vdm is not None and self.xcvr_eeprom.read(consts.VDM_SUPPORTED) - def get_vdm(self, field_option=None): + @read_only_cached_api_return + def is_vdm_statistic_supported(self): + ''' + This function returns whether the optic advertises any VDM statistic + observable types (min/max/avg) in its VDM descriptor pages. + + Returns: + bool: True if at least one statistic observable type is advertised, False otherwise. ''' - This function returns all the VDM items, including real time monitor value, threholds and flags + if self.vdm is None: + return False + return self.vdm.is_vdm_statistic_supported() + + def get_vdm(self, field_option=None, observable_type=None): ''' + This function returns all the VDM items, including real time monitor value, thresholds and flags + + Args: + field_option: Bitmask to select real value, threshold, and/or flag fields. + Defaults to ALL_FIELD (all fields). + observable_type: Bitmask to filter by observable type. + VDM_OBSERVABLE_BASIC (0x1) for basic (instantaneous) types, + VDM_OBSERVABLE_STATISTIC (0x2) for statistic (min/max/avg) types, + VDM_OBSERVABLE_ALL (0x3) for both. Defaults to VDM_OBSERVABLE_ALL. + ''' + if self.vdm is None: + return {} if field_option is None: field_option = self.vdm.ALL_FIELD - vdm = self.vdm.get_vdm_allpage(field_option) if self.vdm is not None else {} - return vdm + if observable_type is None: + observable_type = self.vdm.VDM_OBSERVABLE_ALL + return self.vdm.get_vdm_allpage(field_option, observable_type) or {} def get_module_firmware_fault_state_changed(self): ''' @@ -2382,7 +2428,20 @@ def get_transceiver_loopback(self): def get_transceiver_vdm_real_value(self): """ - Retrieves VDM real value for this xcvr + Retrieves all VDM real values (both basic and statistic) for this xcvr. + This is a convenience method that merges basic and statistic results. + + Returns: + Dictionary with all VDM real values. + """ + result = {} + result.update(self.get_transceiver_vdm_real_value_basic()) + result.update(self.get_transceiver_vdm_real_value_statistic()) + return result + + def get_transceiver_vdm_real_value_basic(self): + """ + Retrieves basic VDM real values for this xcvr. Returns: A dict containing the following keys/values : @@ -2394,21 +2453,9 @@ def get_transceiver_vdm_real_value(self): esnr_host_input{lane_num} = FLOAT ; eSNR value in dB for host input pam4_level_transition_media_input{lane_num} = FLOAT ; PAM4 level transition parameter in dB for media input pam4_level_transition_host_input{lane_num} = FLOAT ; PAM4 level transition parameter in dB for host input - prefec_ber_min_media_input{lane_num} = FLOAT ; Pre-FEC BER minimum value for media input - prefec_ber_max_media_input{lane_num} = FLOAT ; Pre-FEC BER maximum value for media input - prefec_ber_avg_media_input{lane_num} = FLOAT ; Pre-FEC BER average value for media input prefec_ber_curr_media_input{lane_num} = FLOAT ; Pre-FEC BER current value for media input - prefec_ber_min_host_input{lane_num} = FLOAT ; Pre-FEC BER minimum value for host input - prefec_ber_max_host_input{lane_num} = FLOAT ; Pre-FEC BER maximum value for host input - prefec_ber_avg_host_input{lane_num} = FLOAT ; Pre-FEC BER average value for host input prefec_ber_curr_host_input{lane_num} = FLOAT ; Pre-FEC BER current value for host input - errored_frames_min_media_input{lane_num} = FLOAT ; Errored frames minimum value for media input - errored_frames_max_media_input{lane_num} = FLOAT ; Errored frames maximum value for media input - errored_frames_avg_media_input{lane_num} = FLOAT ; Errored frames average value for media input errored_frames_curr_media_input{lane_num} = FLOAT ; Errored frames current value for media input - errored_frames_min_host_input{lane_num} = FLOAT ; Errored frames minimum value for host input - errored_frames_max_host_input{lane_num} = FLOAT ; Errored frames maximum value for host input - errored_frames_avg_host_input{lane_num} = FLOAT ; Errored frames average value for host input errored_frames_curr_host_input{lane_num} = FLOAT ; Errored frames current value for host input ;C-CMIS specific fields @@ -2433,8 +2480,40 @@ def get_transceiver_vdm_real_value(self): ======================================================================== """ vdm_real_value_dict = dict() - vdm_raw_dict = self.get_vdm(self.vdm.VDM_REAL_VALUE) - for vdm_observable_type, db_key_name_prefix in self._get_vdm_key_to_db_prefix_map().items(): + vdm_raw_dict = self.get_vdm(self.vdm.VDM_REAL_VALUE, self.vdm.VDM_OBSERVABLE_BASIC) + for vdm_observable_type, db_key_name_prefix in self._get_vdm_key_to_db_prefix_map_by_observable_type('B').items(): + for lane in range(1, self.NUM_CHANNELS + 1): + db_key_name = f"{db_key_name_prefix}{lane}" + self._update_vdm_dict(vdm_real_value_dict, db_key_name, vdm_raw_dict, vdm_observable_type, + VdmSubtypeIndex.VDM_SUBTYPE_REAL_VALUE, lane) + return vdm_real_value_dict + + def get_transceiver_vdm_real_value_statistic(self): + """ + Retrieves statistic (min/max/avg) VDM real values for this xcvr. + + Returns: + A dict containing the following keys/values : + ======================================================================== + key = TRANSCEIVER_VDM_REAL_VALUE|ifname ; information module VDM sample on port + ; field = value + prefec_ber_min_media_input{lane_num} = FLOAT ; Pre-FEC BER minimum value for media input + prefec_ber_max_media_input{lane_num} = FLOAT ; Pre-FEC BER maximum value for media input + prefec_ber_avg_media_input{lane_num} = FLOAT ; Pre-FEC BER average value for media input + prefec_ber_min_host_input{lane_num} = FLOAT ; Pre-FEC BER minimum value for host input + prefec_ber_max_host_input{lane_num} = FLOAT ; Pre-FEC BER maximum value for host input + prefec_ber_avg_host_input{lane_num} = FLOAT ; Pre-FEC BER average value for host input + errored_frames_min_media_input{lane_num} = FLOAT ; Errored frames minimum value for media input + errored_frames_max_media_input{lane_num} = FLOAT ; Errored frames maximum value for media input + errored_frames_avg_media_input{lane_num} = FLOAT ; Errored frames average value for media input + errored_frames_min_host_input{lane_num} = FLOAT ; Errored frames minimum value for host input + errored_frames_max_host_input{lane_num} = FLOAT ; Errored frames maximum value for host input + errored_frames_avg_host_input{lane_num} = FLOAT ; Errored frames average value for host input + ======================================================================== + """ + vdm_real_value_dict = dict() + vdm_raw_dict = self.get_vdm(self.vdm.VDM_REAL_VALUE, self.vdm.VDM_OBSERVABLE_STATISTIC) + for vdm_observable_type, db_key_name_prefix in self._get_vdm_key_to_db_prefix_map_by_observable_type('S').items(): for lane in range(1, self.NUM_CHANNELS + 1): db_key_name = f"{db_key_name_prefix}{lane}" self._update_vdm_dict(vdm_real_value_dict, db_key_name, vdm_raw_dict, vdm_observable_type, @@ -2495,7 +2574,7 @@ def get_transceiver_vdm_thresholds(self): rxsigpower_xxx{lane_num} = FLOAT ; rx signal power in dbm (high/low alarm/warning) ======================================================================== """ vdm_thresholds_dict = dict() - vdm_raw_dict = self.get_vdm(self.vdm.VDM_THRESHOLD) + vdm_raw_dict = self.get_vdm(self.vdm.VDM_THRESHOLD, self.vdm.VDM_OBSERVABLE_ALL) for vdm_observable_type, db_key_name_prefix in self._get_vdm_key_to_db_prefix_map().items(): for lane in range(1, self.NUM_CHANNELS + 1): for vdm_threshold_type in range(VdmSubtypeIndex.VDM_SUBTYPE_HALARM_THRESHOLD.value, VdmSubtypeIndex.VDM_SUBTYPE_LWARN_THRESHOLD.value + 1): @@ -2562,7 +2641,7 @@ def get_transceiver_vdm_flags(self): rxsigpower_xxx{lane_num} = FLOAT ; rx signal power in dbm (high/low alarm/warning flag) """ vdm_flags_dict = dict() - vdm_raw_dict = self.get_vdm(self.vdm.VDM_FLAG) + vdm_raw_dict = self.get_vdm(self.vdm.VDM_FLAG, self.vdm.VDM_OBSERVABLE_ALL) for vdm_observable_type, db_key_name_prefix in self._get_vdm_key_to_db_prefix_map().items(): for lane in range(1, self.NUM_CHANNELS + 1): for vdm_flag_type in range(VdmSubtypeIndex.VDM_SUBTYPE_HALARM_FLAG.value, VdmSubtypeIndex.VDM_SUBTYPE_LWARN_FLAG.value + 1): diff --git a/sonic_platform_base/sonic_xcvr/api/public/cmisVDM.py b/sonic_platform_base/sonic_xcvr/api/public/cmisVDM.py index ebaef758d..e12f7fef9 100644 --- a/sonic_platform_base/sonic_xcvr/api/public/cmisVDM.py +++ b/sonic_platform_base/sonic_xcvr/api/public/cmisVDM.py @@ -24,6 +24,11 @@ class CmisVdmApi(XcvrApi): VDM_FLAG = 0x4 ALL_FIELD = 0xff + # Observable type filters + VDM_OBSERVABLE_BASIC = 0x1 # Basic (instantaneous) observable types + VDM_OBSERVABLE_STATISTIC = 0x2 # Statistic (min/max/avg) observable types + VDM_OBSERVABLE_ALL = 0x3 # Both basic and statistic + def __init__(self, xcvr_eeprom): super(CmisVdmApi, self).__init__(xcvr_eeprom) @@ -36,7 +41,7 @@ def get_F16(self, value): result = mantissa*10**(scale_exponent-24) return result - def get_vdm_page(self, page, VDM_flag_page, field_option=ALL_FIELD): + def get_vdm_page(self, page, VDM_flag_page, field_option=ALL_FIELD, observable_type=VDM_OBSERVABLE_ALL): ''' This function returns VDM items from a specific VDM page. Output format is a dictionary. Key is observable type; value is a dictionary. @@ -52,6 +57,15 @@ def get_vdm_page(self, page, VDM_flag_page, field_option=ALL_FIELD): vdm_high_warn_flag, vdm_low_warn_flag ] + + Args: + page: VDM descriptor page (0x20-0x23) + VDM_flag_page: Raw flag page data or None + field_option: Bitmask to select real value, threshold, and/or flag fields + observable_type: Bitmask to filter by observable type. + VDM_OBSERVABLE_BASIC (0x1) for basic (instantaneous) types, + VDM_OBSERVABLE_STATISTIC (0x2) for statistic (min/max/avg) types, + VDM_OBSERVABLE_ALL (0x3) for both. ''' if page not in [0x20, 0x21, 0x22, 0x23]: raise ValueError('Page not in VDM Descriptor range!') @@ -75,6 +89,14 @@ def get_vdm_page(self, page, VDM_flag_page, field_option=ALL_FIELD): continue vdm_info_dict = VDM_TYPE_DICT[typeID] + + # Filter by observable type (basic vs statistic) + vdm_obs_type = vdm_info_dict[3] if len(vdm_info_dict) > 3 else 'B' + if vdm_obs_type == 'B' and not (observable_type & self.VDM_OBSERVABLE_BASIC): + continue + if vdm_obs_type == 'S' and not (observable_type & self.VDM_OBSERVABLE_STATISTIC): + continue + thrshID = VDM_thresholdID[index] vdm_type = vdm_info_dict[0] vdm_format = vdm_info_dict[1] @@ -178,7 +200,7 @@ def get_vdm_page(self, page, VDM_flag_page, field_option=ALL_FIELD): vdm_low_warn_flag] return vdm_Page_data - def get_vdm_allpage(self, field_option=ALL_FIELD ): + def get_vdm_allpage(self, field_option=ALL_FIELD, observable_type=VDM_OBSERVABLE_ALL): ''' This function returns VDM items from all advertised VDM pages. Output format is a dictionary. Key is observable type; value is a dictionary. @@ -194,6 +216,13 @@ def get_vdm_allpage(self, field_option=ALL_FIELD ): vdm_high_warn_flag, vdm_low_warn_flag ] + + Args: + field_option: Bitmask to select real value, threshold, and/or flag fields + observable_type: Bitmask to filter by observable type. + VDM_OBSERVABLE_BASIC (0x1) for basic (instantaneous) types, + VDM_OBSERVABLE_STATISTIC (0x2) for statistic (min/max/avg) types, + VDM_OBSERVABLE_ALL (0x3) for both. ''' vdm_pages_supported = self.xcvr_eeprom.read(consts.VDM_SUPPORTED) if not vdm_pages_supported: @@ -210,6 +239,37 @@ def get_vdm_allpage(self, field_option=ALL_FIELD ): vdm_flag_page = None for page in range(VDM_START_PAGE, VDM_START_PAGE + vdm_groups_supported_raw + 1): - vdm_current_page = self.get_vdm_page(page, vdm_flag_page, field_option) + vdm_current_page = self.get_vdm_page(page, vdm_flag_page, field_option, observable_type) vdm.update(vdm_current_page) return vdm + + def is_vdm_statistic_supported(self): + ''' + Checks whether the optic advertises any VDM statistic observable types + by scanning the VDM descriptor pages for type IDs classified as 'S' in VDM_TYPE. + + Returns: + bool: True if at least one statistic observable type is advertised, False otherwise. + ''' + vdm_pages_supported = self.xcvr_eeprom.read(consts.VDM_SUPPORTED) + if not vdm_pages_supported: + return False + vdm_groups_supported_raw = self.xcvr_eeprom.read(consts.VDM_SUPPORTED_PAGE) + if vdm_groups_supported_raw is None: + return False + + VDM_START_PAGE = 0x20 + VDM_TYPE_DICT = self.xcvr_eeprom.mem_map.codes.VDM_TYPE + + for page in range(VDM_START_PAGE, VDM_START_PAGE + vdm_groups_supported_raw + 1): + vdm_descriptor = self.xcvr_eeprom.read_raw(page * PAGE_SIZE + PAGE_OFFSET, PAGE_SIZE) + if not vdm_descriptor: + continue + # Odd addresses contain the VDM observable type IDs + vdm_typeIDs = vdm_descriptor[1::2] + for typeID in vdm_typeIDs: + if typeID in VDM_TYPE_DICT: + info = VDM_TYPE_DICT[typeID] + if len(info) > 3 and info[3] == 'S': + return True + return False diff --git a/sonic_platform_base/sonic_xcvr/api/xcvr_api.py b/sonic_platform_base/sonic_xcvr/api/xcvr_api.py index f0879c7c8..51af230fe 100644 --- a/sonic_platform_base/sonic_xcvr/api/xcvr_api.py +++ b/sonic_platform_base/sonic_xcvr/api/xcvr_api.py @@ -222,13 +222,31 @@ def is_transceiver_vdm_supported(self): """ raise NotImplementedError + def is_vdm_statistic_supported(self): + """ + Retrieves whether the optic advertises any VDM statistic observables (applicable for CMIS and C-CMIS) + """ + raise NotImplementedError + def get_transceiver_vdm_real_value(self): """ - Retrieves VDM real (sample) values for this xcvr (applicable for CMIS and C-CMIS) + Retrieves all VDM real (sample) values for this xcvr (applicable for CMIS and C-CMIS) Specifically, it retrieves sample data from pages 24h to 27h """ raise NotImplementedError + def get_transceiver_vdm_real_value_basic(self): + """ + Retrieves basic (instantaneous) VDM real values for this xcvr (applicable for CMIS and C-CMIS) + """ + raise NotImplementedError + + def get_transceiver_vdm_real_value_statistic(self): + """ + Retrieves statistic (min/max/avg) VDM real values for this xcvr (applicable for CMIS and C-CMIS) + """ + raise NotImplementedError + def get_transceiver_vdm_thresholds(self): """ Retrieves VDM thresholds for this xcvr (applicable for CMIS and C-CMIS) diff --git a/sonic_platform_base/sonic_xcvr/codes/public/cmis.py b/sonic_platform_base/sonic_xcvr/codes/public/cmis.py index cd868717c..1c8e2aa0b 100644 --- a/sonic_platform_base/sonic_xcvr/codes/public/cmis.py +++ b/sonic_platform_base/sonic_xcvr/codes/public/cmis.py @@ -74,51 +74,52 @@ class CmisCodes(Sff8024): } VDM_TYPE = { - # VDM_ID: [VDM_NAME, DATA_TYPE, SCALE] - 1: ['Laser Age [%]', 'U16', 1], - 2: ['TEC Current [%]', 'S16', 100.0/32767], - 3: ['Laser Frequency Error [MHz]', 'S16', 10], - 4: ['Laser Temperature [C]', 'S16', 1.0/256], - 5: ['eSNR Media Input [dB]', 'U16', 1.0/256], - 6: ['eSNR Host Input [dB]', 'U16', 1.0/256], - 7: ['PAM4 Level Transition Parameter Media Input [dB]', 'U16', 1.0/256], - 8: ['PAM4 Level Transition Parameter Host Input [dB]', 'U16', 1.0/256], - 9: ['Pre-FEC BER Minimum Media Input', 'F16', 1], - 10: ['Pre-FEC BER Minimum Host Input', 'F16', 1], - 11: ['Pre-FEC BER Maximum Media Input', 'F16', 1], - 12: ['Pre-FEC BER Maximum Host Input', 'F16', 1], - 13: ['Pre-FEC BER Average Media Input', 'F16', 1], - 14: ['Pre-FEC BER Average Host Input', 'F16', 1], - 15: ['Pre-FEC BER Current Value Media Input', 'F16', 1], - 16: ['Pre-FEC BER Current Value Host Input', 'F16', 1], - 17: ['Errored Frames Minimum Media Input', 'F16', 1], - 18: ['Errored Frames Minimum Host Input', 'F16', 1], - 19: ['Errored Frames Maximum Media Input', 'F16', 1], - 20: ['Errored Frames Maximum Host Input', 'F16', 1], - 21: ['Errored Frames Average Media Input', 'F16', 1], - 22: ['Errored Frames Average Host Input', 'F16', 1], - 23: ['Errored Frames Current Value Media Input', 'F16', 1], - 24: ['Errored Frames Current Value Host Input', 'F16', 1], - 128: ['Modulator Bias X/I [%]', 'U16', 100.0/65535], - 129: ['Modulator Bias X/Q [%]', 'U16', 100.0/65535], - 130: ['Modulator Bias Y/I [%]', 'U16', 100.0/65535], - 131: ['Modulator Bias Y/Q [%]', 'U16', 100.0/65535], - 132: ['Modulator Bias X_Phase [%]', 'U16', 100.0/65535], - 133: ['Modulator Bias Y_Phase [%]', 'U16', 100.0/65535], - 134: ['CD high granularity, short link [ps/nm]', 'S16', 1], - 135: ['CD low granularity, long link [ps/nm]', 'S16', 20], - 136: ['DGD [ps]', 'U16', 0.01], - 137: ['SOPMD [ps^2]', 'U16', 0.01], - 138: ['PDL [dB]', 'U16', 0.1], - 139: ['OSNR [dB]', 'U16', 0.1], - 140: ['eSNR [dB]', 'U16', 0.1], - 141: ['CFO [MHz]', 'S16', 1], - 142: ['EVM_modem [%]', 'U16', 100.0/65535], - 143: ['Tx Power [dBm]', 'S16', 0.01], - 144: ['Rx Total Power [dBm]', 'S16', 0.01], - 145: ['Rx Signal Power [dBm]', 'S16', 0.01], - 146: ['SOP ROC [krad/s]', 'U16', 1], - 147: ['MER [dB]', 'U16', 0.1] + # VDM_ID: [VDM_NAME, DATA_TYPE, SCALE, OBSERVABLE_TYPE] + # OBSERVABLE_TYPE: 'B' = Basic (instantaneous), 'S' = Statistic (min/max/avg) + 1: ['Laser Age [%]', 'U16', 1, 'B'], + 2: ['TEC Current [%]', 'S16', 100.0/32767, 'B'], + 3: ['Laser Frequency Error [MHz]', 'S16', 10, 'B'], + 4: ['Laser Temperature [C]', 'S16', 1.0/256, 'B'], + 5: ['eSNR Media Input [dB]', 'U16', 1.0/256, 'B'], + 6: ['eSNR Host Input [dB]', 'U16', 1.0/256, 'B'], + 7: ['PAM4 Level Transition Parameter Media Input [dB]', 'U16', 1.0/256, 'B'], + 8: ['PAM4 Level Transition Parameter Host Input [dB]', 'U16', 1.0/256, 'B'], + 9: ['Pre-FEC BER Minimum Media Input', 'F16', 1, 'S'], + 10: ['Pre-FEC BER Minimum Host Input', 'F16', 1, 'S'], + 11: ['Pre-FEC BER Maximum Media Input', 'F16', 1, 'S'], + 12: ['Pre-FEC BER Maximum Host Input', 'F16', 1, 'S'], + 13: ['Pre-FEC BER Average Media Input', 'F16', 1, 'S'], + 14: ['Pre-FEC BER Average Host Input', 'F16', 1, 'S'], + 15: ['Pre-FEC BER Current Value Media Input', 'F16', 1, 'B'], + 16: ['Pre-FEC BER Current Value Host Input', 'F16', 1, 'B'], + 17: ['Errored Frames Minimum Media Input', 'F16', 1, 'S'], + 18: ['Errored Frames Minimum Host Input', 'F16', 1, 'S'], + 19: ['Errored Frames Maximum Media Input', 'F16', 1, 'S'], + 20: ['Errored Frames Maximum Host Input', 'F16', 1, 'S'], + 21: ['Errored Frames Average Media Input', 'F16', 1, 'S'], + 22: ['Errored Frames Average Host Input', 'F16', 1, 'S'], + 23: ['Errored Frames Current Value Media Input', 'F16', 1, 'B'], + 24: ['Errored Frames Current Value Host Input', 'F16', 1, 'B'], + 128: ['Modulator Bias X/I [%]', 'U16', 100.0/65535, 'B'], + 129: ['Modulator Bias X/Q [%]', 'U16', 100.0/65535, 'B'], + 130: ['Modulator Bias Y/I [%]', 'U16', 100.0/65535, 'B'], + 131: ['Modulator Bias Y/Q [%]', 'U16', 100.0/65535, 'B'], + 132: ['Modulator Bias X_Phase [%]', 'U16', 100.0/65535, 'B'], + 133: ['Modulator Bias Y_Phase [%]', 'U16', 100.0/65535, 'B'], + 134: ['CD high granularity, short link [ps/nm]', 'S16', 1, 'B'], + 135: ['CD low granularity, long link [ps/nm]', 'S16', 20, 'B'], + 136: ['DGD [ps]', 'U16', 0.01, 'B'], + 137: ['SOPMD [ps^2]', 'U16', 0.01, 'B'], + 138: ['PDL [dB]', 'U16', 0.1, 'B'], + 139: ['OSNR [dB]', 'U16', 0.1, 'B'], + 140: ['eSNR [dB]', 'U16', 0.1, 'B'], + 141: ['CFO [MHz]', 'S16', 1, 'B'], + 142: ['EVM_modem [%]', 'U16', 100.0/65535, 'B'], + 143: ['Tx Power [dBm]', 'S16', 0.01, 'B'], + 144: ['Rx Total Power [dBm]', 'S16', 0.01, 'B'], + 145: ['Rx Signal Power [dBm]', 'S16', 0.01, 'B'], + 146: ['SOP ROC [krad/s]', 'U16', 1, 'B'], + 147: ['MER [dB]', 'U16', 0.1, 'B'] } CDB_FAIL_STATUS = { diff --git a/sonic_platform_base/sonic_xcvr/sfp_optoe_base.py b/sonic_platform_base/sonic_xcvr/sfp_optoe_base.py index 6e1ca2c5d..71f4c7635 100644 --- a/sonic_platform_base/sonic_xcvr/sfp_optoe_base.py +++ b/sonic_platform_base/sonic_xcvr/sfp_optoe_base.py @@ -63,14 +63,35 @@ def is_transceiver_vdm_supported(self): api = self.get_xcvr_api() return api.is_transceiver_vdm_supported() if api is not None else None + def is_vdm_statistic_supported(self): + """ + Returns whether the optic advertises any VDM statistic observable types + """ + api = self.get_xcvr_api() + return api.is_vdm_statistic_supported() if api is not None else None + def get_transceiver_vdm_real_value(self): """ - Retrieves VDM real (sample) values for this xcvr (applicable for CMIS and C-CMIS) + Retrieves all VDM real (sample) values for this xcvr (applicable for CMIS and C-CMIS) Specifically, it retrieves sample data from pages 24h to 27h """ api = self.get_xcvr_api() return api.get_transceiver_vdm_real_value() if api is not None else None + def get_transceiver_vdm_real_value_basic(self): + """ + Retrieves basic (instantaneous) VDM real values for this xcvr + """ + api = self.get_xcvr_api() + return api.get_transceiver_vdm_real_value_basic() if api is not None else None + + def get_transceiver_vdm_real_value_statistic(self): + """ + Retrieves statistic (min/max/avg) VDM real values for this xcvr + """ + api = self.get_xcvr_api() + return api.get_transceiver_vdm_real_value_statistic() if api is not None else None + def get_transceiver_vdm_thresholds(self): api = self.get_xcvr_api() return api.get_transceiver_vdm_thresholds() if api is not None else None diff --git a/tests/sonic_xcvr/test_cmis.py b/tests/sonic_xcvr/test_cmis.py index 18170cfd3..771e69827 100755 --- a/tests/sonic_xcvr/test_cmis.py +++ b/tests/sonic_xcvr/test_cmis.py @@ -2450,6 +2450,92 @@ def test_get_transceiver_vdm_real_value(self, vdm_raw_dict, expected_result): result = self.api.get_transceiver_vdm_real_value() assert result == expected_result + @pytest.mark.parametrize( + "vdm_raw_dict, expected_basic", + [ + # Test case 1: Only basic observables (IDs 1-8, 15-16, 23-24) + ( + { + 'Laser Temperature [C]': { + 1: [10.0, None, None, None, None, None, None, None, None], + 2: [20.0, None, None, None, None, None, None, None, None]}, + 'Pre-FEC BER Current Value Media Input': {1: [0.001, None, None, None, None, None, None, None, None]}, + 'eSNR Media Input [dB]': {1: [22.5, None, None, None, None, None, None, None, None]}, + 'Pre-FEC BER Average Media Input': {1: [0.002, 0.0125, 0, 0.01, 0, False, False, False, False]} # Statistic - should be filtered out + }, + { + 'laser_temperature_media1': 10.0, + 'laser_temperature_media2': 20.0, + 'prefec_ber_curr_media_input1': 0.001, + 'esnr_media_input1': 22.5, + # Note: Pre-FEC BER Average should NOT be included (it's statistic) + } + ), + ] + ) + def test_get_transceiver_vdm_real_value_basic(self, vdm_raw_dict, expected_basic): + self.api.vdm = MagicMock() + self.api.get_vdm = MagicMock(return_value=vdm_raw_dict) + + result = self.api.get_transceiver_vdm_real_value_basic() + # Check that all expected basic keys are present + for key, value in expected_basic.items(): + assert key in result + assert result[key] == value + # Check that no statistic keys (like prefec_ber_avg, prefec_ber_min, prefec_ber_max) are present + assert 'prefec_ber_avg_media_input1' not in result + assert 'prefec_ber_min_media_input1' not in result + assert 'prefec_ber_max_media_input1' not in result + + @pytest.mark.parametrize( + "vdm_raw_dict, expected_statistic", + [ + # Test case 1: Only statistic observables (IDs 9-14, 17-22) + ( + { + 'Laser Temperature [C]': {1: [10.0, None, None, None, None, None, None, None, None]}, # Basic - should be filtered out + 'Pre-FEC BER Average Media Input': {1: [0.002, 0.015, 0.001, 0.01, 0.005, False, False, False, False]}, + 'Pre-FEC BER Minimum Media Input': {1: [0.001, 0.012, 0, 0.008, 0.003, False, False, False, False]}, + 'Pre-FEC BER Maximum Media Input': {1: [0.003, 0.018, 0.002, 0.012, 0.007, False, False, False, False]}, + }, + { + 'prefec_ber_avg_media_input1': 0.002, + 'prefec_ber_min_media_input1': 0.001, + 'prefec_ber_max_media_input1': 0.003, + # Note: Laser Temperature should NOT be included (it's basic) + } + ), + ] + ) + def test_get_transceiver_vdm_real_value_statistic(self, vdm_raw_dict, expected_statistic): + self.api.vdm = MagicMock() + self.api.get_vdm = MagicMock(return_value=vdm_raw_dict) + + result = self.api.get_transceiver_vdm_real_value_statistic() + # Check that all expected statistic keys are present + for key, value in expected_statistic.items(): + assert key in result + assert result[key] == value + # Check that no basic keys (like laser_temperature) are present + assert 'laser_temperature_media1' not in result + assert 'esnr_media_input1' not in result + + @pytest.mark.parametrize("vdm_return, expected", [ + (True, True), + (False, False), + ]) + def test_is_vdm_statistic_supported(self, vdm_return, expected): + self.api.vdm = MagicMock() + self.api.vdm.is_vdm_statistic_supported = MagicMock(return_value=vdm_return) + + result = self.api.is_vdm_statistic_supported() + assert result == expected + + def test_is_vdm_statistic_supported_vdm_none(self): + self.api.vdm = None + result = self.api.is_vdm_statistic_supported() + assert result == False + def generate_vdm_thrsholds_expected_dict(base_dict): default_dict = dict() for _, db_prefix_key_map in CMIS_VDM_KEY_TO_DB_PREFIX_KEY_MAP.items(): diff --git a/tests/sonic_xcvr/test_cmisVDM.py b/tests/sonic_xcvr/test_cmisVDM.py index f57bf67fe..890d03c8a 100644 --- a/tests/sonic_xcvr/test_cmisVDM.py +++ b/tests/sonic_xcvr/test_cmisVDM.py @@ -112,6 +112,92 @@ def test_get_vdm_page_none_vdm_descriptor(self, input_param, mock_response, expe result = self.api.get_vdm_page(*input_param) assert result == expected + def test_get_vdm_page_observable_type_basic_only(self): + """Test get_vdm_page with VDM_OBSERVABLE_BASIC filters out statistic observables""" + # Descriptor: typeIDs at odd positions: 9(S), 11(S), 13(S), 15(B), 10(S), 10(S), 0, 0 + # Only typeID 15 is basic ('B') + descriptor = ( + 16, 9, 16, 11, 16, 13, 16, 15, 32, 10, 33, 10, 0, 0, 0, 0, + 80,128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 160,143,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ) + mock_responses = [ + descriptor, + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + ] + self.api.xcvr_eeprom.read_raw = MagicMock(side_effect=mock_responses) + result = self.api.get_vdm_page(0x20, [0]*128, observable_type=CmisVdmApi.VDM_OBSERVABLE_BASIC) + # Only basic observable (ID 15: Pre-FEC BER Current Value Media Input) should be present + assert 'Pre-FEC BER Current Value Media Input' in result + # Statistic observables should be filtered out + assert 'Pre-FEC BER Minimum Media Input' not in result + assert 'Pre-FEC BER Maximum Media Input' not in result + assert 'Pre-FEC BER Average Media Input' not in result + assert 'Pre-FEC BER Minimum Host Input' not in result + + def test_get_vdm_page_observable_type_statistic_only(self): + """Test get_vdm_page with VDM_OBSERVABLE_STATISTIC filters out basic observables""" + descriptor = ( + 16, 9, 16, 11, 16, 13, 16, 15, 32, 10, 33, 10, 0, 0, 0, 0, + 80,128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 160,143,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ) + mock_responses = [ + descriptor, + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + bytearray(b'\x00\x00'), bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00'), + ] + self.api.xcvr_eeprom.read_raw = MagicMock(side_effect=mock_responses) + result = self.api.get_vdm_page(0x20, [0]*128, observable_type=CmisVdmApi.VDM_OBSERVABLE_STATISTIC) + # Statistic observables should be present + assert 'Pre-FEC BER Minimum Media Input' in result + assert 'Pre-FEC BER Maximum Media Input' in result + assert 'Pre-FEC BER Average Media Input' in result + assert 'Pre-FEC BER Minimum Host Input' in result + # Basic observable (ID 15) should be filtered out + assert 'Pre-FEC BER Current Value Media Input' not in result + + @pytest.mark.parametrize("vdm_supported, groups_raw, descriptor, expected", [ + # Case 1: VDM not supported + (0, None, None, False), + # Case 2: VDM supported but groups_raw is None + (1, None, None, False), + # Case 3: VDM supported, descriptor has only basic types (ID 15=B) + (1, 0, bytearray([16, 15] + [0]*126), False), + # Case 4: VDM supported, descriptor has a statistic type (ID 9=S) + (1, 0, bytearray([16, 9] + [0]*126), True), + # Case 5: VDM supported, descriptor is None/empty + (1, 0, None, False), + ]) + def test_is_vdm_statistic_supported(self, vdm_supported, groups_raw, descriptor, expected): + self.api.xcvr_eeprom.read = MagicMock(side_effect=[vdm_supported, groups_raw]) + self.api.xcvr_eeprom.read_raw = MagicMock(return_value=descriptor) + result = self.api.is_vdm_statistic_supported() + assert result == expected + @pytest.mark.parametrize("mock_response, expected", [ ( [ # mock_response diff --git a/tests/sonic_xcvr/test_sfp_optoe_base.py b/tests/sonic_xcvr/test_sfp_optoe_base.py index 949ba5ad5..1c2ef614f 100644 --- a/tests/sonic_xcvr/test_sfp_optoe_base.py +++ b/tests/sonic_xcvr/test_sfp_optoe_base.py @@ -25,11 +25,23 @@ class TestSfpOptoeBase(object): def test_is_transceiver_vdm_supported_non_cmis(self): self.sfp_optoe_api.get_xcvr_api = MagicMock(return_value=self.sff8472_api) - try: + with pytest.raises(NotImplementedError): self.sfp_optoe_api.is_transceiver_vdm_supported() - except NotImplementedError: - exception_raised = True - assert exception_raised + + def test_is_vdm_statistic_supported_non_cmis(self): + self.sfp_optoe_api.get_xcvr_api = MagicMock(return_value=self.sff8472_api) + with pytest.raises(NotImplementedError): + self.sfp_optoe_api.is_vdm_statistic_supported() + + def test_get_transceiver_vdm_real_value_basic_non_cmis(self): + self.sfp_optoe_api.get_xcvr_api = MagicMock(return_value=self.sff8472_api) + with pytest.raises(NotImplementedError): + self.sfp_optoe_api.get_transceiver_vdm_real_value_basic() + + def test_get_transceiver_vdm_real_value_statistic_non_cmis(self): + self.sfp_optoe_api.get_xcvr_api = MagicMock(return_value=self.sff8472_api) + with pytest.raises(NotImplementedError): + self.sfp_optoe_api.get_transceiver_vdm_real_value_statistic() @pytest.mark.parametrize("mock_response1, mock_response2, expected", [ (0, cmis_api, 0), @@ -107,6 +119,48 @@ def test_get_vdm_unfreeze_status(self, mock_response1, mock_response2, expected) result = self.sfp_optoe_api.get_vdm_unfreeze_status() assert result == expected + @pytest.mark.parametrize("mock_response1, mock_response2, expected", [ + (True, cmis_api, True), + (False, cmis_api, False), + (None, None, None), + ]) + def test_is_vdm_statistic_supported(self, mock_response1, mock_response2, expected): + self.sfp_optoe_api.get_xcvr_api = MagicMock() + self.sfp_optoe_api.get_xcvr_api.return_value = mock_response2 + self.cmis_api.is_vdm_statistic_supported = MagicMock() + self.cmis_api.is_vdm_statistic_supported.return_value = mock_response1 + + result = self.sfp_optoe_api.is_vdm_statistic_supported() + assert result == expected + + @pytest.mark.parametrize("mock_response1, mock_response2, expected", [ + ({'key1': 1.0}, cmis_api, {'key1': 1.0}), + (None, cmis_api, None), + (None, None, None), + ]) + def test_get_transceiver_vdm_real_value_basic(self, mock_response1, mock_response2, expected): + self.sfp_optoe_api.get_xcvr_api = MagicMock() + self.sfp_optoe_api.get_xcvr_api.return_value = mock_response2 + self.cmis_api.get_transceiver_vdm_real_value_basic = MagicMock() + self.cmis_api.get_transceiver_vdm_real_value_basic.return_value = mock_response1 + + result = self.sfp_optoe_api.get_transceiver_vdm_real_value_basic() + assert result == expected + + @pytest.mark.parametrize("mock_response1, mock_response2, expected", [ + ({'key1': 1.0}, cmis_api, {'key1': 1.0}), + (None, cmis_api, None), + (None, None, None), + ]) + def test_get_transceiver_vdm_real_value_statistic(self, mock_response1, mock_response2, expected): + self.sfp_optoe_api.get_xcvr_api = MagicMock() + self.sfp_optoe_api.get_xcvr_api.return_value = mock_response2 + self.cmis_api.get_transceiver_vdm_real_value_statistic = MagicMock() + self.cmis_api.get_transceiver_vdm_real_value_statistic.return_value = mock_response1 + + result = self.sfp_optoe_api.get_transceiver_vdm_real_value_statistic() + assert result == expected + @patch("builtins.open", new_callable=mock_open) @patch.object(SfpOptoeBase, 'get_eeprom_path') def test_set_optoe_write_timeout_success(self, mock_get_eeprom_path, mock_open): @@ -143,11 +197,8 @@ def test_set_optoe_write_timeout_oserror(self, mock_get_eeprom_path, mock_open): def test_set_power(self): mode = 1 - try: + with pytest.raises(NotImplementedError): self.sfp_optoe_api.set_power(mode) - except NotImplementedError: - exception_raised = True - assert exception_raised def test_default_page(self): with patch("builtins.open", mock_open(read_data=b'\x01')) as mocked_file: