#!/usr/bin/python3
# print useful diagnostics info about any attached LIGO PCIe Timing Cards (LPTC).
# Uses sysfs information generated by gpstime kernel module, which must be installed.

import argparse
import os
from collections import namedtuple


def read_lptc_val(fname):
    with open(os.path.join("/sys/kernel/gpstime/lptc", fname), "rt") as f:
        return f.readline().strip()


Flag = namedtuple("Flag", ['name', 'inverted'], defaults=[None, False])


def process_bitfield_flags(name, value, flags):
    """
    Print out which flags are set and which are not in 'value', a bitfield where individual
    bits are flags.

    if a key in flags bitwise-anded with value is true, the the value pointed to by that key is a set flag.

    :param name: string used to customize output.
    :param value: bitfield to be scanned for flags
    :param flags: A dictionary with a bitmask ask as key, and flag name as a string value, or Flag named tuple
    :return: None
    """
    flags_set = []
    flags_not_set = []
    for mask, _flag in flags.items():
        if isinstance(_flag, str):
            flag = Flag(_flag, False)
        else:
            flag = _flag
        if bool(value & mask) != bool(flag.inverted):
            flags_set.append(flag.name)
        else:
            flags_not_set.append(flag.name)

    flags_txt = ",".join(flags_set)
    print(f"{name} flags set = {flags_txt}")
    flags_txt = ",".join(flags_not_set)
    print(f"{name} flags not set = {flags_txt}")


def process_lptc_status(status):
    """Decodes Status and interrupt control register defined in
    DCC doc 2000406"""
    flags = {
        0x80000000: 'OK_LOCKED',
        0x40000000: 'ROOT_NODE',
        0x20000000: 'FANOUT_PORTS',
        0x10000000: 'UPLINK_UP',
        0x8000000:  'LOSS_OF_SIGNAL',
        0x4000000:  'OCXO_LOCKED',
        0x2000000:  'GPS_LOCKED',
        0x1000000:  'VCXO_VOLT_OUT_OF_RANGE',
        0x800000:   'UTC_MODE',
        0x400000:   'LEAP_SEC_DECODE',
        0x200000:   'LEAP_DEC_PENDING',
        0x100000:   'LEAP_INC_PENDING',
    }

    print(f"raw LPTC status = 0x{status:{'x'}}")

    process_bitfield_flags("lptc status", status, flags)

    leap_seconds = (status & 0xff00) >> 8
    print(f"leap seconds = {leap_seconds}")

    active_msi_ints = []
    for msi_int in range(3, -1, -1):
        mask = 1 << msi_int
        if status & mask:
            active_msi_ints.append(msi_int)
    if len(active_msi_ints) > 0:
        msi_ints_txt = ",".join([str(i) for i in active_msi_ints])
    else:
        msi_ints_txt = "none"
    print(f"active MSI interrupts = {msi_ints_txt}")


def process_backplane_status(status):
    print(f"raw bp status = 0x{status:{'x'}}")
    flags = {
        0x200:  "BP_PRESENT",
        0x100:  "X5_INPUT",
        0x80:   "X3_INPUT",
        0x40:   "X1_INPUT",
        0x20:   "TEMP_ALARM",
        0x2:    "CLOCKS_RUNNNG",
        0x1:    "CLOCKS_ACTIVE",
    }

    process_bitfield_flags("bp status", status, flags)

    bp_rev = (status & 0x18) >> 3
    print(f"bp rev = {bp_rev}")


def process_backplane_config(config):
    print(f"raw bp config = 0x{config:{'x'}}")
    flags = {
        0x10:   "START_NEXT_IDLE",
        0x8:    "START_NEXT_SECOND",
        0x4:    "GLOBAL_ENABLE",
        0x2:    "WD_RESET_ON_READ",
        0x1:    "DISABLE_DUOTONE_OUTPUT",
    }
    process_bitfield_flags("bp config", config, flags)


def process_board_version(rawid):
    if (rawid >> 4) != 0x2000329:
        print(f"raw board id = 0x{rawid:{'x'}} not recognized")
    else:
        bid = rawid & 0xf
        print(f"board id = {bid}")


def process_software_version(sid, rev):

    releases = {
        (1, 0x1f0): "Aug 2021",
        (2, 0x2f4): "2023 Feb 9",
    }
    if (sid >> 4) != 0x2000337:
        print(f"raw software id = 0x{sid:{'x'}} not recognized")
    else:
        sid = sid & 0xf
        ver = (sid, rev)
        if ver in releases:
            release = releases[ver]
        else:
            release = "version not recognized"
        print(f"software id = {sid} rev = 0x{rev:{'x'}}, release = {release}")


# Onboard features
def process_board_conf(conf):
    exp = conf & 0xf  # exponent for exponential method
    div = (conf & 0xff0) >> 4
    if exp > 0:
        in_clock_hz = 2**(exp + 10)
    else:
        in_clock_hz = 2**26 / (div+1)
    print(f"external synchronization frequency = {in_clock_hz} Hz")


def process_board_and_powersupply_status(status):
    flags = {
        0x100: 'SWTCH_REG_INT',
        0x4: 'SWTCH_SPLY_TEMP_INT',
        0x8: 'SWTCH_SPLY_IN_V_INT',
        0x10: 'SWTCH_SPLY_1_INT',
        0x20: 'SWTCH_SPLY_2_INT',
        0x40: 'SWTCH_SPLY_3_INT',
        0x80: 'SWTCH_SPLY_4_INT',
        0x2: 'GBIT_XCVR_PWR_GOOD',
        0x1: 'SWTCH_SPLY_PWR_GOOD',
    }

    for i in range(16):
        flag = 0x10000 << i
        flags[flag] = Flag(f"DIP_{i+1}", inverted=True)

    process_bitfield_flags("board and power supply", status, flags)


def process_xadc_status(status):
    flags = {
        0x20: 'XADC_ENBLD',
        0x10: 'VCCAUX_ALARM',
        0x8:  'VCCINT_ALARM',
        0x4:  'USER_TEMP_ALARM',
        0x2:  'OVER_TEMP_ALARM',
        0x1:  'ALARM',
    }
    process_bitfield_flags("XADC", status, flags)


def process_temperature(temp):
    temp_c = (temp/65536.0) * 503.975 - 273.15
    print(f"Temp = {temp_c:.1f} deg C")


def calc_volts(raw, word_offset, gain, offset):
    """
    Calculate a voltage from a raw register, an offset as number of 16 bit words, and a gain.
    Gives a voltage based on a 16 bit value recovered from chopping of offset*16 bits, scaling by
    2**16 adc size, and dividing by gain.
    :param raw: A register
    :param word_offset: number of 16 bit words to throw away.
    :param gain: analog gain applied to signal before conversion.
    :param offset: added to value
    :return: volts as floating point
    """
    counts = (raw >> (word_offset * 16)) & 0xffff
    return (counts / (2**16) / gain) + offset


def print_volts(name, volts, nominal):
    print(f"{name} = {volts:.1f}V ({float(nominal):.1f}V nominal)")


def print_amps(name, amps):
    print(f"{name} = {amps:.3f}A")


# represents a single 16 bit voltage reading in
# name = display name for voltage
# register_offset = added to base address for the reading to get register.
#  A voltage from 'external power supply 8' has an register_offset of 7
# word_offset: number of 16 bit words to offset reading, with lowest being 1.
# gain: analog gain.
# nominal: approximately what voltage should be
VoltageReading = namedtuple("VoltageReading",
                            ['name', 'register_offset', 'word_offset',
                             'gain', 'offset', 'nominal'],
                            defaults=[0, 0],
                            )
CurrentReading = namedtuple("CurrentReading", ['name', 'register_offset',
                                               'word_offset', 'gain', 'offset'],
                            defaults=[-0.05, ])


def read_lptc_array(fname):
    with open(os.path.join("/sys/kernel/gpstime/lptc", fname), "rt") as f:
        line = f.readline().strip()
    return [int(x, 16) for x in line.split(",")]


def calc_and_print_volts(vr, vals):
    """

    :param vr: a VoltageReading tuple
    :param vals: an array of integers to grab the value from.
    :return: None
    """
    # same calc used for volts and amps
    volts = calc_volts(vals[vr.register_offset], vr.word_offset, vr.gain, vr.offset)

    if isinstance(vr, VoltageReading):
        print_volts(vr.name, volts, vr.nominal)
    elif isinstance(vr, CurrentReading):
        print_amps(vr.name, volts)
    else:
        raise Exception("Unknown A2D reading type")


def process_voltage_readings(vrs, vals):
    """
    Process a list of VoltageReading tuples against an array of integer raw register values, 
    :param vrs: An array of VoltageReading
    :param vals: An array of ints
    :return:     
    """
    for vr in vrs:
        calc_and_print_volts(vr, vals)


def read_and_process_internal_power():
    print("Internal Power Supply")
    vrs = [
        VoltageReading("VCCINT",
                       register_offset=0,
                       word_offset=1,
                       gain=1/3,
                       nominal=1),
        VoltageReading("VCCINT",
                       register_offset=1,
                       word_offset=0,
                       gain=1 / 3,
                       nominal=1.8),
        VoltageReading("VCCINT",
                       register_offset=1,
                       word_offset=1,
                       gain=1 / 3,
                       nominal=1),
    ]

    vals = read_lptc_array('internal_pwr')

    process_voltage_readings(vrs, vals)


def read_and_process_external_power():
    print("External Power Supply")
    vrs = [
        CurrentReading("3.3V",
                       register_offset=0,
                       word_offset=0,
                       gain=1,
                       ),
        CurrentReading("VCCINT",
                       register_offset=0,
                       word_offset=1,
                       gain=1,
                       ),

        CurrentReading("VCCAUX",
                       register_offset=1,
                       word_offset=0,
                       gain=1,
                       ),
        CurrentReading("2.5V",
                       register_offset=1,
                       word_offset=1,
                       gain=1/3,
                       ),

        VoltageReading("VREG",
                       register_offset=2,
                       word_offset=0,
                       gain=1 / 6,
                       nominal=5.1),
        VoltageReading("VDD",
                       register_offset=2,
                       word_offset=1,
                       gain=1 / 4,
                       nominal=2.5),

        VoltageReading("AVCC",
                       register_offset=3,
                       word_offset=0,
                       gain=2 / 3,
                       nominal=1.0),
        VoltageReading("AVTT",
                       register_offset=3,
                       word_offset=1,
                       gain=2 / 3,
                       nominal=1.2),

        VoltageReading("P5",
                       register_offset=4,
                       word_offset=0,
                       gain=1 / 6,
                       nominal=5),
        VoltageReading("N5",
                       register_offset=4,
                       word_offset=1,
                       gain=1 / 6,
                       offset=-6.25,
                       nominal=-5),

        VoltageReading("VCC",
                       register_offset=5,
                       word_offset=0,
                       gain=1 / 5,
                       nominal=3.3),
        VoltageReading("N12",
                       register_offset=5,
                       word_offset=1,
                       gain=1 / 15,
                       offset=-17.5,
                       nominal=-12),

        VoltageReading("VADC",
                       register_offset=6,
                       word_offset=0,
                       gain=1 / 2,
                       nominal=1.8),
        VoltageReading("P10",
                       register_offset=6,
                       word_offset=1,
                       gain=1 / 11,
                       nominal=10),

        VoltageReading("V12",
                       register_offset=7,
                       word_offset=0,
                       gain=1 / 15,
                       nominal=12),
        CurrentReading("V12",
                       register_offset=7,
                       word_offset=1,
                       gain=1,),
    ]

    vals = read_lptc_array('external_pwr')

    process_voltage_readings(vrs, vals)


def process_slot_status(status):
    print(f"raw slot status = 0x{status:{'x'}}")
    flags = {
        0x400000:   'BIT_2',
        0x100000:   'BIT_1',
        0x80000:    'DUOTONE_LAST_DAC',
        0x40000:    'DUOTONE_PENULT_ADC',
        0x20000:    'DUOTONE_LAST_ADC',
        0x2:        'CLOCK_RUNNING',
        0x1:        'CLOCK_ACTIVE',
    }
    process_bitfield_flags("status", status, flags)


def process_slot_config(config):
    print(f"raw slot config = 0x{config:{'x'}}")
    flags = {
        0x400000:   'BIT_2_SET',
        0x200000:   'BIT_2_BIN',
        0x100000:   'BIT_1_SET',
        0x80000:    'BIT_1_BIN',
        0x40000:    'DUOTONE_PENULT_ADC',
        0x20000:    'DUOTONE_LAST_ADC',
        0x10000:    'LVDS_CLOCK_LINES',
        0x1000:     'IDLE_CLOCK_PULLUP',
        0x800:      'CLOCK_NEXT_IDLE',
        0x400:      'CLOCK_NEXT_SEC',
        0x200:      'CLOCK_INV',
        0x100:      'CLOCK_ENABLE',
    }
    process_bitfield_flags("config", config, flags)

    freq = config & 0xff
    if freq > 127:
        freq = 256 - freq
    freq = 2**freq
    print(f"frequency = {freq} Hz")


def process_slot_phase(phase):
    phase_deg = phase * 360.0 / 2**32
    print(f"phase = {phase_deg} deg")

def process_duotone_shift(shift):
    print(f"duotone shift = {shift:x}")

def process_duotone_config(can_configure, amplitude, frequency_code):
    freqs = [
        "960, 961",
        "1920, 1921",
        "3840, 3841",
        "15424, 15423",
    ]
    if can_configure:
        print("Can configure duotone")
        print(f"duotone amplitude = {amplitude:x}")
        if frequency_code >= 0 and frequency_code < len(freqs):
            print(f"duotone frequencies (Hz) = {freqs[frequency_code]}")
        else:
            print(f"Unkown frequency code {frequency_code}")
    else:
        print("Cannot configure duotone")

def print_slot_status(slot):
    print(f"\nInfo for slot {slot}")
    try:
        process_slot_status(int(read_lptc_val(f"slot_{slot}/status"), 16))
        process_slot_config(int(read_lptc_val(f"slot_{slot}/config"), 16))
        process_slot_phase(int(read_lptc_val(f"slot_{slot}/phase"), 16))
    except IOError as e:
        print(f"Could not read sys filesystem for slot {slot}:{e}\nCheck 'gpstime' kernel module")


def print_lptc_status():
    try:
        process_lptc_status(int(read_lptc_val("status"), 16))
        process_backplane_status(int(read_lptc_val("backplane_status"), 16))
        process_backplane_config(int(read_lptc_val("backplane_config"), 16))

        process_duotone_config(
            int(read_lptc_val("duotone_can_config"), 16),
            int(read_lptc_val("duotone_amplitude"), 16),
            int(read_lptc_val("duotone_frequency"), 16),
        )

        process_duotone_shift(int(read_lptc_val("duotone_shift"), 16))
        process_board_version(int(read_lptc_val("board_id"), 16))
        process_software_version(int(read_lptc_val("software_id"), 16), int(read_lptc_val("software_rev"), 16))
    except IOError as e:
        print(f"Could not read sys filesystem for LIGO PCIe Timing Card:{e}\nCheck 'gpstime' kernel module.")


def print_onboard_status():
    try:
        process_board_conf(int(read_lptc_val("brd_synch_factors"), 16))
        process_board_and_powersupply_status(int(read_lptc_val("board_and_powersupply_status"), 16))
        process_xadc_status(int(read_lptc_val("xadc_status"), 16))
        process_temperature(int(read_lptc_val("temp"), 16))
        read_and_process_internal_power()
        read_and_process_external_power()
    except IOError as e:
        print(f"Could not read sys filesystem for LIGO PCIe Timing Card:{e}\nCheck 'gpstime' kernel module.")


def main():
    parser = argparse.ArgumentParser(description="Show info about an attached LIGO PCIe Timing Card")
    parser.add_argument('--info', '-i', help="show basic info, also shown when no args given", action='store_true')
    parser.add_argument('--onboard', '-b', help="show on-board info", action='store_true')
    parser.add_argument('--slots', '-s', help="show status and configuration for each slot", action='store_true')
    parser.add_argument('--all', '-a', help="show on-board info", action='store_true')
    args = parser.parse_args()
    if (not args.slots and not args.onboard) or args.all or args.info:
        print_lptc_status()
    if args.onboard or args.all:
        print_onboard_status()
    if args.slots or args.all:
        for slot in range(1, 11):
            print_slot_status(slot)


if __name__ == "__main__":
    main()
