##
## This file is part of the libsigrokdecode project.
##
## Copyright (C) 2013 Bert Vermeulen <bert@biot.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <http://www.gnu.org/licenses/>.
##

# XFP protocol decoder

import sigrokdecode as srd
import os

MODULE_ID = {
    0x01: 'GBIC',
    0x02: 'Integrated module/connector',
    0x03: 'SFP',
    0x04: '300-pin XBI',
    0x05: 'XENPAK',
    0x06: 'XFP',
    0x07: 'XFF',
    0x08: 'XFP-E',
    0x09: 'XPAK',
    0x0a: 'X2',
}

ALARM_THRESHOLDS = {
    0:  "Temp high alarm",
    2:  "Temp low alarm",
    4:  "Temp high warning",
    6:  "Temp low warning",
    16: "Bias high alarm",
    18: "Bias low alarm",
    20: "Bias high warning",
    22: "Bias low warning",
    24: "TX power high alarm",
    26: "TX power low alarm",
    28: "TX power high warning",
    30: "TX power low warning",
    32: "RX power high alarm",
    34: "RX power low alarm",
    36: "RX power high warning",
    38: "RX power low warning",
    40: "AUX 1 high alarm",
    42: "AUX 1 low alarm",
    44: "AUX 1 high warning",
    46: "AUX 1 low warning",
    48: "AUX 2 high alarm",
    50: "AUX 2 low alarm",
    52: "AUX 2 high warning",
    54: "AUX 2 low warning",
}

AD_READOUTS = {
    0:  "Module temperature",
    4:  "TX bias current",
    6:  "Measured TX output power",
    8:  "Measured RX input power",
    10: "AUX 1 measurement",
    12: "AUX 2 measurement",
}

GCS_BITS = [
    "TX disable",
    "Soft TX disable",
    "MOD_NR",
    "P_Down",
    "Soft P_Down",
    "Interrupt",
    "RX_LOS",
    "Data_Not_Ready",
    "TX_NR",
    "TX_Fault",
    "TX_CDR not locked",
    "RX_NR",
    "RX_CDR not locked",
]

CONNECTOR = {
    0x01:   "SC",
    0x02:   "Fibre Channel style 1 copper",
    0x03:   "Fibre Channel style 2 copper",
    0x04:   "BNC/TNC",
    0x05:   "Fibre Channel coax",
    0x06:   "FiberJack",
    0x07:   "LC",
    0x08:   "MT-RJ",
    0x09:   "MU",
    0x0a:   "SG",
    0x0b:   "Optical pigtail",
    0x20:   "HSSDC II",
    0x21:   "Copper pigtail",
}

TRANSCEIVER = [
    # 10GB Ethernet
    ["10GBASE-SR", "10GBASE-LR", "10GBASE-ER", "10GBASE-LRM", "10GBASE-SW",
        "10GBASE-LW",   "10GBASE-EW"],
    # 10GB Fibre Channel
    ["1200-MX-SN-I", "1200-SM-LL-L", "Extended Reach 1550 nm",
        "Intermediate reach 1300 nm FP"],
    # 10GB Copper
    [],
    # 10GB low speed
    ["1000BASE-SX / 1xFC MMF", "1000BASE-LX / 1xFC SMF", "2xFC MMF",
        "2xFC SMF", "OC48-SR", "OC48-IR", "OC48-LR"],
    # 10GB SONET/SDH interconnect
    ["I-64.1r", "I-64.1", "I-64.2r", "I-64.2", "I-64.3", "I-64.5"],
    # 10GB SONET/SDH short haul
    ["S-64.1", "S-64.2a", "S-64.2b", "S-64.3a", "S-64.3b", "S-64.5a", "S-64.5b"],
    # 10GB SONET/SDH long haul
    ["L-64.1", "L-64.2a", "L-64.2b", "L-64.2c", "L-64.3", "G.959.1 P1L1-2D2"],
    # 10GB SONET/SDH very long haul
    ["V-64.2a", "V-64.2b", "V-64.3"],
]

SERIAL_ENCODING = [
    "64B/66B",
    "8B/10B",
    "SONET scrambled",
    "NRZ",
    "RZ",
]

XMIT_TECH = [
    "850 nm VCSEL",
    "1310 nm VCSEL",
    "1550 nm VCSEL",
    "1310 nm FP",
    "1310 nm DFB",
    "1550 nm DFB",
    "1310 nm EML"
    "1550 nm EML"
    "copper",
]

CDR = [
    "9.95Gb/s",
    "10.3Gb/s",
    "10.5Gb/s",
    "10.7Gb/s",
    "11.1Gb/s",
    "(unknown)",
    "lineside loopback mode",
    "XFI loopback mode",
]

DEVICE_TECH = [
    ["no wavelength control", "sctive wavelength control"],
    ["uncooled transmitter device", "cooled transmitter"],
    ["PIN detector", "APD detector"],
    ["transmitter not tunable", "transmitter tunable"],
]

ENHANCED_OPTS = [
    "VPS",
    "soft TX_DISABLE",
    "soft P_Down",
    "VPS LV regulator mode",
    "VPS bypassed regulator mode",
    "active FEC control",
    "wavelength tunability",
    "CMU",
]

AUX_TYPES = [
    "not implemented",
    "APD bias voltage",
    "(unknown)",
    "TEC current",
    "laser temperature",
    "laser wavelength",
    "5V supply voltage",
    "3.3V supply voltage",
    "1.8V supply voltage",
    "-5.2V supply voltage",
    "5V supply current",
    "(unknown)",
    "(unknown)",
    "3.3V supply current",
    "1.8V supply current",
    "-5.2V supply current",
]

class Decoder(srd.Decoder):
    api_version = 1
    id = 'xfp'
    name = 'XFP'
    longname = '10 Gigabit Small Form Factor Pluggable Module (XFP)'
    desc = 'Data structure describing display device capabilities.'
    license = 'gplv3+'
    inputs = ['i2c']
    outputs = ['xfp']
    probes = []
    optional_probes = []
    options = {}
    annotations = [
        ['XFP field names and values', 'XFP structure field names and values'],
        ['XFP fields', 'XFP structure fields'],
    ]

    def __init__(self, **kwargs):
        # Received data items, used as an index into samplenum/data
        self.cnt = -1
        # Start/end sample numbers per data item
        self.sn = []
        # Multi-byte structure buffer
        self.buf = []
        # Filled in by address 0x7f in low memory
        self.cur_highmem_page = 0
        # Filled in by extended ID value in table 2
        self.have_clei = False
        # Handlers for each field in the structure, keyed by the end
        # index of that field. Each handler is fed all unhandled bytes
        # up until that point, so mark unused space with the dummy
        # handler self.ignore().
        self.MAP_LOWER_MEMORY = {
            0:  self.module_id,
            1:  self.signal_cc,
            57: self.alarm_warnings,
            59: self.vps,
            69: self.ignore,
            71: self.ber,
            75: self.wavelength_cr,
            79: self.fec_cr,
            95: self.int_ctrl,
            109: self.ad_readout,
            111: self.gcs,
            117: self.ignore,
            118: self.ignore,
            122: self.ignore,
            126: self.ignore,
            127: self.page_select,
        }
        self.MAP_HIGH_TABLE_1 = {
            128: self.module_id,
            129: self.ext_module_id,
            130: self.connector,
            138: self.transceiver,
            139: self.serial_encoding,
            140: self.br_min,
            141: self.br_max,
            142: self.link_length_smf,
            143: self.link_length_e50,
            144: self.link_length_50um,
            145: self.link_length_625um,
            146: self.link_length_copper,
            147: self.device_tech,
            163: self.vendor,
            164: self.cdr,
            167: self.vendor_oui,
            183: self.vendor_pn,
            185: self.vendor_rev,
            187: self.wavelength,
            189: self.wavelength_tolerance,
            190: self.max_case_temp,
            191: self.ignore,
            195: self.power_supply,
            211: self.vendor_sn,
            219: self.manuf_date,
            220: self.diag_mon,
            221: self.enhanced_opts,
            222: self.aux_mon,
            223: self.ignore,
            255: self.maybe_ascii,
        }

    def start(self, metadata):
        self.out_ann = self.add(srd.OUTPUT_ANN, 'xfp')

    def decode(self, ss, es, data):
        cmd, data = data

        # We only care about actual data bytes that are read (for now).
        if cmd != 'DATA READ':
            return

        self.cnt += 1
        self.sn.append([ss, es])

        self.buf.append(data)
        if self.cnt < 0x80:
            if self.cnt in self.MAP_LOWER_MEMORY:
                self.MAP_LOWER_MEMORY[self.cnt](self.buf)
                self.buf.clear()
        elif self.cnt < 0x0100 and self.cur_highmem_page == 0x01:
            # Serial ID memory map
            if self.cnt in self.MAP_HIGH_TABLE_1:
                self.MAP_HIGH_TABLE_1[self.cnt](self.buf)
                self.buf.clear()

    # Annotation helper
    def annotate(self, key, value, start_cnt=None, end_cnt=None):
        if start_cnt is None:
            start_cnt = self.cnt - len(self.buf) + 1
        if end_cnt is None:
            end_cnt = self.cnt
        self.put(self.sn[start_cnt][0], self.sn[end_cnt][1],
                self.out_ann, [0, [key + ": " + value]])
        self.put(self.sn[start_cnt][0], self.sn[end_cnt][1],
                 self.out_ann, [1, [value]])

    # Placeholder handler, needed to advance the buffer past unused or
    # reserved space in the structures.
    def ignore(self, data):
        pass

    # Show as ASCII if possible
    def maybe_ascii(self, data):
        for i in range(len(data)):
            if data[i] >= 0x20 and data[i] < 0x7f:
                cnt = self.cnt - len(data) + 1
                self.annotate("Vendor ID", chr(data[i]), cnt, cnt)

    # Convert 16-bit two's complement values, with each increment
    # representing 1/256C, to degrees Celcius.
    def to_temp(self, value):
        if value & 0x8000:
            value = -((value ^ 0xffff) + 1)
        temp = value / 256.0
        return "%.1f C" % temp

    # TX bias current in uA. Each increment represents 0.2uA
    def to_current(self, value):
        current = value / 500000.0
        return "%.1f mA" % current

    # Power in mW, with each increment representing 0.1uW
    def to_power(self, value):
        power = value / 10000.0
        return "%.2f mW" % power

    # Wavelength in increments of 0.05nm
    def to_wavelength(self, value):
        wl = value / 20
        return "%d nm" % wl

    # Wavelength in increments of 0.005nm
    def to_wavelength_tolerance(self, value):
        wl = value / 200.0
        return "%.1f nm" % wl

    def module_id(self, data):
        self.annotate("Module identifier", MODULE_ID.get(data[0], "Unknown"))

    def signal_cc(self, data):
        # No good data available.
        if (data[0] != 0x00):
            self.annotate("Signal Conditioner Control", "%.2x" % data[0])

    def alarm_warnings(self, data):
        cnt_idx = self.cnt - len(data)
        idx = 0
        while idx < 56:
            if idx == 8:
                # Skip over reserved A/D flag thresholds
                idx += 8
            value = (data[idx] << 8) | data[idx + 1]
            if value != 0:
                name = ALARM_THRESHOLDS.get(idx, "...")
                if idx in (0, 2, 4, 6):
                    self.annotate(name, self.to_temp(value),
                            cnt_idx + idx, cnt_idx + idx + 1)
                elif idx in (16, 18, 20, 22):
                    self.annotate(name, self.to_current(value),
                            cnt_idx + idx, cnt_idx + idx + 1)
                elif idx in (24, 26, 28, 30, 32, 34, 36, 38):
                    self.annotate(name, self.to_power(value),
                            cnt_idx + idx, cnt_idx + idx + 1)
                else:
                    self.annotate(name, "%d" % name, value, cnt_idx + idx,
                            cnt_idx + idx + 1)
            idx += 2

    def vps(self, data):
        # No good data available.
        if (data != [0, 0]):
            self.annotate("VPS", "%.2x%.2x" % (data[0], data[1]))

    def ber(self, data):
        # No good data available.
        if (data != [0, 0]):
            self.annotate("BER", str(data))

    def wavelength_cr(self, data):
        # No good data available.
        if (data != [0, 0, 0, 0]):
            self.annotate("WCR", str(data))

    def fec_cr(self, data):
        if (data != [0, 0, 0, 0]):
            self.annotate("FEC", str(data))

    def int_ctrl(self, data):
        # No good data available. Also boring.
        out = []
        for d in data:
            out.append("%.2x" % d)
        self.annotate("Interrupt bits", ' '.join(out))

    def ad_readout(self, data):
        cnt_idx = self.cnt - len(data) + 1
        idx = 0
        while idx < 14:
            if idx == 2:
                # Skip over reserved field
                idx += 2
            value = (data[idx] << 8) | data[idx + 1]
            name = AD_READOUTS.get(idx, "...")
            if value != 0:
                if idx == 0:
                    self.annotate(name, self.to_temp(value),
                            cnt_idx + idx, cnt_idx + idx + 1)
                elif idx == 4:
                    self.annotate(name, self.to_current(value),
                            cnt_idx + idx, cnt_idx + idx + 1)
                elif idx in (6, 8):
                    self.annotate(name, self.to_power(value),
                            cnt_idx + idx, cnt_idx + idx + 1)
                else:
                    self.annotate(name, str(value), cnt_idx + idx,
                            cnt_idx + idx + 1)
            idx += 2

    def gcs(self, data):
        allbits = (data[0] << 8) | data[1]
        out = []
        for b in range(13):
            if allbits & 0x8000:
                out.append(GCS_BITS[b])
            allbits <<= 1
        self.annotate("General Control/Status", ', '.join(out))

    def page_select(self, data):
        self.cur_highmem_page = data[0]

    def ext_module_id(self, data):
        out = ["Power level %d module" % ((data[0] >> 6) + 1)]
        if data[0] & 0x20 == 0:
            out.append("CDR")
        if data[0] & 0x10 == 0:
            out.append("TX ref clock input required")
        if data[0] & 0x08 == 0:
            self.have_clei = True
        self.annotate("Extended id", ', '.join(out))

    def connector(self, data):
        if data[0] in CONNECTOR:
            self.annotate("Connector", CONNECTOR[data[0]])

    def transceiver(self, data):
        out = []
        for t in range(8):
            if data[t] == 0:
                continue
            value = data[t]
            for b in range(8):
                if value & 0x80:
                    if len(TRANSCEIVER[t]) < b + 1:
                        out.append("(unknown)")
                    else:
                        out.append(TRANSCEIVER[t][b])
                value <<= 1
        self.annotate("Transceiver compliance", ', '.join(out))

    def serial_encoding(self, data):
        out = []
        value = data[0]
        for b in range(8):
            if value & 0x80:
                if len(SERIAL_ENCODING) < b + 1:
                    out.append("(unknown)")
                else:
                    out.append(SERIAL_ENCODING[b])
                value <<= 1
        self.annotate("Serial encoding support", ', '.join(out))

    def br_min(self, data):
        # Increments represent 100Mb/s
        rate = data[0] / 10.0
        self.annotate("Minimum bit rate", "%.3f GB/s" % rate)

    def br_max(self, data):
        # Increments represent 100Mb/s
        rate = data[0] / 10.0
        self.annotate("Maximum bit rate", "%.3f GB/s" % rate)

    def link_length_smf(self, data):
        if data[0] == 0:
            length = "(standard)"
        elif data[0] == 255:
            length = "> 254 km"
        else:
            length = "%d km" % data[0]
        self.annotate("Link length (SMF)", length)

    def link_length_e50(self, data):
        if data[0] == 0:
            length = "(standard)"
        elif data[0] == 255:
            length = "> 508 m"
        else:
            length = "%d m" % (data[0] * 2)
        self.annotate("Link length (extended, 50μm MMF)", length)

    def link_length_50um(self, data):
        if data[0] == 0:
            length = "(standard)"
        elif data[0] == 255:
            length = "> 254 m"
        else:
            length = "%d m" % data[0]
        self.annotate("Link length (50μm MMF)", length)

    def link_length_625um(self, data):
        if data[0] == 0:
            length = "(standard)"
        elif data[0] == 255:
            length = "> 254 m"
        else:
            length = "%d m" % (data[0])
        self.annotate("Link length (62.5μm MMF)", length)

    def link_length_copper(self, data):
        if data[0] == 0:
            length = "(unknown)"
        elif data[0] == 255:
            length = "> 254 m"
        else:
            length = "%d m" % (data[0] * 2)
        self.annotate("Link length (copper)", length)

    def device_tech(self, data):
        out = []
        xmit = data[0] >> 4
        if xmit <= len(XMIT_TECH) - 1:
            out.append("%s transmitter" % XMIT_TECH[xmit])
        dev = data[0] & 0x0f
        for b in range(4):
            out.append(DEVICE_TECH[b][(dev >> (3 - b)) & 0x01])
        self.annotate("Device technology", ', '.join(out))

    def vendor(self, data):
        name = bytes(data).strip().decode('ascii').strip('\x00')
        if name:
            self.annotate("Vendor", name)

    def cdr(self, data):
        out = []
        value = data[0]
        for b in range(8):
            if value & 0x80:
                out.append(CDR[b])
            value <<= 1
        self.annotate("CDR support", ', '.join(out))

    def vendor_oui(self, data):
        if data != [0, 0, 0]:
            self.annotate("Vendor OUI", "%.2X-%.2X-%.2X" % tuple(data))

    def vendor_pn(self, data):
        name = bytes(data).strip().decode('ascii').strip('\x00')
        if name:
            self.annotate("Vendor part number", name)

    def vendor_rev(self, data):
        name = bytes(data).strip().decode('ascii').strip('\x00')
        if name:
            self.annotate("Vendor revision", name)

    def wavelength(self, data):
        value = (data[0] << 8) | data[1]
        self.annotate("Wavelength", self.to_wavelength(value))

    def wavelength_tolerance(self, data):
        value = (data[0] << 8) | data[1]
        self.annotate("Wavelength tolerance", self.to_wavelength_tolerance(value))

    def max_case_temp(self, data):
        self.annotate("Maximum case temperature", "%d C" % data[0])

    def power_supply(self, data):
        out = []
        self.annotate("Max power dissipation",
                "%.3f W" % (data[0] * 0.02), self.cnt - 3, self.cnt - 3)
        self.annotate("Max power dissipation (powered down)",
                "%.3f W" % (data[1] * 0.01), self.cnt - 2, self.cnt - 2)
        value = (data[2] >> 4) * 0.050
        self.annotate("Max current required (5V supply)",
                "%.3f A" % value, self.cnt - 1, self.cnt - 1)
        value = (data[2] & 0x0f) * 0.100
        self.annotate("Max current required (3.3V supply)",
                "%.3f A" % value, self.cnt - 1, self.cnt - 1)
        value = (data[3] >> 4) * 0.100
        self.annotate("Max current required (1.8V supply)",
                "%.3f A" % value, self.cnt, self.cnt)
        value = (data[3] & 0x0f) * 0.050
        self.annotate("Max current required (-5.2V supply)",
                "%.3f A" % value, self.cnt, self.cnt)

    def vendor_sn(self, data):
        name = bytes(data).strip().decode('ascii').strip('\x00')
        if name:
            self.annotate("Vendor serial number", name)

    def manuf_date(self, data):
        y = int(bytes(data[0:2])) + 2000
        m = int(bytes(data[2:4]))
        d = int(bytes(data[4:6]))
        mnf = "%.4d-%.2d-%.2d" % (y, m, d)
        lot = bytes(data[6:]).strip().decode('ascii').strip('\x00')
        if lot:
            mnf += " lot " + lot
        self.annotate("Manufacturing date", mnf)

    def diag_mon(self, data):
        out = []
        if data[0] & 0x10:
            out.append("BER support")
        else:
            out.append("no BER support")
        if data[0] & 0x08:
            out.append("average power measurement")
        else:
            out.append("OMA power measurement")
        self.annotate("Diagnostic monitoring", ', '.join(out))

    def enhanced_opts(self, data):
        out = []
        value = data[0]
        for b in range(8):
            if value & 0x80:
                out.append(ENHANCED_OPTS[b])
            value <<= 1
        self.annotate("Enhanced option support", ', '.join(out))

    def aux_mon(self, data):
        aux = AUX_TYPES[data[0] >> 4]
        self.annotate("AUX1 monitoring", aux)
        aux = AUX_TYPES[data[0] & 0x0f]
        self.annotate("AUX2 monitoring", aux)