#!/usr/bin/env python3
import hid
import time
import struct
from datetime import datetime
import math

class MicrochipHIDSolar:
    def __init__(self, vendor_id=0x04d8, product_id=0x003f):
        self.vendor_id = vendor_id
        self.product_id = product_id
        self.device = None
        self.packet_size = 65
        
        # System parameters
        self.system_info = {
            'rshunt': 0.0,
            'gain_filtered': 1.0,
            'gain_unfiltered': 1.0,
            'offset_volts': 0.0
        }
        
        # Constants
        self.ONE_ADU_VOLTS = 4.096 / 65535.0
        self.SUN_DIAMETER_ARCSEC = 1900.0
        self.WAVELENGTH_M = 550e-9  # 550 nm
        
    def connect(self):
        """Connect to device"""
        try:
            self.device = hid.device()
            self.device.open(self.vendor_id, self.product_id)
            self.device.set_nonblocking(1)
            
            manufacturer = self.device.get_manufacturer_string()
            product = self.device.get_product_string()
            print(f"✓ Connected to: {manufacturer} - {product}\n")
            return True
        except Exception as e:
            print(f"✗ Connection error: {e}")
            return False
    
    def disconnect(self):
        """Disconnect"""
        if self.device:
            self.device.close()
            print("\n✓ Disconnected")
    
    def send_command(self, command, data=None):
        """Send command"""
        try:
            packet = [0x00, command] + (data if data else []) + [0x00] * (self.packet_size - 2 - len(data if data else []))
            bytes_written = self.device.write(packet)
            return bytes_written > 0
        except Exception as e:
            print(f"✗ Send error: {e}")
            return False
    
    def read_response(self, timeout_ms=500):
        """Read response"""
        try:
            start_time = time.time()
            timeout_sec = timeout_ms / 1000.0
            
            while (time.time() - start_time) < timeout_sec:
                response = self.device.read(self.packet_size)
                if response:
                    return list(response)
                time.sleep(0.01)
            return None
        except Exception as e:
            print(f"✗ Read error: {e}")
            return None
    
    def decode_system_info(self, buffer_out):
        """
        Decode system info (command 0x80)
        
        USB HID Mapping:
        buffer_out[0]  = Report ID (0x00)
        buffer_out[1]  = MajorVersion
        buffer_out[2]  = MinorVersion
        buffer_out[3]  = ChannelToRead
        buffer_out[4]  = Led_Enable
        buffer_out[5..8]  = Serial (Big Endian)
        buffer_out[9..12] = RShunt (Float Big Endian)
        buffer_out[13..16] = GainFiltered (Float Big Endian)
        buffer_out[17..20] = GainUnfiltered (Float Big Endian)
        buffer_out[21..24] = OffsetVolts (Float Big Endian)
        """
        if not buffer_out or len(buffer_out) < 25:
            return None
        
        try:
            # Versions (bytes 1-2)
            major_version = buffer_out[1]
            minor_version = buffer_out[2]
            
            # Channel to read (byte 3)
            channel_to_read = buffer_out[3]
            
            # LED Enable (byte 4)
            led_enable = buffer_out[4] > 0
            
            # Serial (bytes 5-8) - Big Endian
            serial = struct.unpack('>I', bytes([
                buffer_out[5],  # as4 (MSB)
                buffer_out[6],  # as3
                buffer_out[7],  # as2
                buffer_out[8]   # as1 (LSB)
            ]))[0]
            
            # RShunt (bytes 9-12) - Float Big Endian
            rshunt = struct.unpack('>f', bytes([
                buffer_out[9],   # as4 (MSB)
                buffer_out[10],  # as3
                buffer_out[11],  # as2
                buffer_out[12]   # as1 (LSB)
            ]))[0]
            
            # GainFiltered (bytes 13-16)
            gain_filtered = struct.unpack('>f', bytes([
                buffer_out[13],
                buffer_out[14],
                buffer_out[15],
                buffer_out[16]
            ]))[0]
            
            # GainUnfiltered (bytes 17-20)
            gain_unfiltered = struct.unpack('>f', bytes([
                buffer_out[17],
                buffer_out[18],
                buffer_out[19],
                buffer_out[20]
            ]))[0]
            
            # OffsetVolts (bytes 21-24)
            offset_volts = struct.unpack('>f', bytes([
                buffer_out[21],
                buffer_out[22],
                buffer_out[23],
                buffer_out[24]
            ]))[0]
            
            info = {
                'major_version': major_version,
                'minor_version': minor_version,
                'channel_to_read': channel_to_read,
                'led_enable': led_enable,
                'serial': serial,
                'rshunt': rshunt,
                'gain_filtered': gain_filtered,
                'gain_unfiltered': gain_unfiltered,
                'offset_volts': offset_volts
            }
            
            # Update system parameters
            self.system_info['rshunt'] = rshunt
            self.system_info['gain_filtered'] = gain_filtered
            self.system_info['gain_unfiltered'] = gain_unfiltered
            self.system_info['offset_volts'] = offset_volts
            
            return info
            
        except Exception as e:
            print(f"✗ Decode system info error: {e}")
            import traceback
            traceback.print_exc()
            return None
    
    def decode_measurement_data(self, buffer_out):
        """
        Decode measurement data
        
        Mapping corresponding to Delphi DecodeFrom/DecodeFromW:
        
        DecodeFrom(BufferOUT, 1)  -> reads BufferOUT[2..5]  -> buffer_out[1..4]   MeanADU_NF
        DecodeFrom(BufferOUT, 5)  -> reads BufferOUT[6..9]  -> buffer_out[5..8]   EC_ADU_NF
        DecodeFrom(BufferOUT, 9)  -> reads BufferOUT[10..13] -> buffer_out[9..12]  MeanADU_F
        DecodeFrom(BufferOUT, 13) -> reads BufferOUT[14..17] -> buffer_out[13..16] PowerScintADU
        DecodeFromW(BufferOUT, 17) -> reads BufferOUT[18..21] -> buffer_out[17..20] NbSamples
        DecodeFrom(BufferOUT, 25) -> reads BufferOUT[26..29] -> buffer_out[25..28] TempInsideBox
        """
        if not buffer_out or len(buffer_out) < 29:
            print("✗ Frame too short for measurements")
            return None
        
        try:
            # MeanADU_NF (bytes 1-4)
            mean_adu_nf = struct.unpack('>f', bytes([
                buffer_out[1],
                buffer_out[2],
                buffer_out[3],
                buffer_out[4]
            ]))[0]
            
            # EC_ADU_NF (bytes 5-8)
            ec_adu_nf = struct.unpack('>f', bytes([
                buffer_out[5],
                buffer_out[6],
                buffer_out[7],
                buffer_out[8]
            ]))[0]
            
            # MeanADU_F (bytes 9-12)
            mean_adu_f = struct.unpack('>f', bytes([
                buffer_out[9],
                buffer_out[10],
                buffer_out[11],
                buffer_out[12]
            ]))[0]
            
            # PowerScintADU (bytes 13-16)
            power_scint_adu = struct.unpack('>f', bytes([
                buffer_out[13],
                buffer_out[14],
                buffer_out[15],
                buffer_out[16]
            ]))[0]
            
            # NbSamples (bytes 17-20) - LongWord
            nb_samples = struct.unpack('>I', bytes([
                buffer_out[17],
                buffer_out[18],
                buffer_out[19],
                buffer_out[20]
            ]))[0]
            
            # TempInsideBox (bytes 25-28)
            temp_inside_box = struct.unpack('>f', bytes([
                buffer_out[25],
                buffer_out[26],
                buffer_out[27],
                buffer_out[28]
            ]))[0]
            
            # Convert to volts
            mean_volts_nf = self.ONE_ADU_VOLTS * mean_adu_nf
            ec_volts_nf = self.ONE_ADU_VOLTS * ec_adu_nf
            power_scint_volts = self.ONE_ADU_VOLTS * power_scint_adu
            mean_scint_volts = self.ONE_ADU_VOLTS * mean_adu_f
            
            # Useful signal
            gain_nf = self.system_info['gain_unfiltered']
            offset_v = self.system_info['offset_volts']
            
            if gain_nf != 0:
                signal_utile_v = (mean_volts_nf / gain_nf) - offset_v
            else:
                signal_utile_v = 0.0
            
            # Solar current
            rshunt = self.system_info['rshunt']
            if rshunt != 0:
                current_solar_a = signal_utile_v / rshunt
            else:
                current_solar_a = 0.0
            
            # Seeing (atmospheric turbulence)
            gain_f = self.system_info['gain_filtered']
            if gain_f != 0 and signal_utile_v != 0:
                seeing_arcsec = (self.SUN_DIAMETER_ARCSEC / gain_f) * (power_scint_volts / signal_utile_v)
            else:
                seeing_arcsec = 0.0
            
            # Seeing validation
            valid_seeing = (0.1 < seeing_arcsec < 10.0)
            
            # Fried parameter R0
            r0_constant = (self.WAVELENGTH_M * 1000.0 / math.pi) * 3600.0 * 180.0
            if seeing_arcsec > 0 and valid_seeing:
                r0 = r0_constant / seeing_arcsec
            else:
                r0 = 0.0
            
            return {
                # Raw ADU data
                'mean_adu_nf': mean_adu_nf,
                'ec_adu_nf': ec_adu_nf,
                'mean_adu_f': mean_adu_f,
                'power_scint_adu': power_scint_adu,
                
                # Voltage data
                'mean_volts_nf': mean_volts_nf,
                'ec_volts_nf': ec_volts_nf,
                'power_scint_volts': power_scint_volts,
                'mean_scint_volts': mean_scint_volts,
                'signal_utile_v': signal_utile_v,
                
                # Physical measurements
                'nb_samples': nb_samples,
                'current_solar_a': current_solar_a,
                'seeing_arcsec': seeing_arcsec,
                'r0': r0,
                'valid_seeing': valid_seeing,
                'temp_inside_box': temp_inside_box,
                
                # Timestamps
                'current_date': datetime.now(),
                'utc_date': datetime.utcnow()
            }
            
        except Exception as e:
            print(f"✗ Decode measurement error: {e}")
            import traceback
            traceback.print_exc()
            return None
    
    def get_system_info(self):
        """Read system information (command 0x80)"""
        if not self.send_command(0x80):
            return None
        
        time.sleep(0.1)
        response = self.read_response()
        
        if response:
            return self.decode_system_info(response)
        return None
    
    def get_measurement_data(self, command=0x81):
        """Read measurement data"""
        if not self.send_command(command):
            return None
        
        time.sleep(0.1)
        response = self.read_response()
        
        if response:
            return self.decode_measurement_data(response)
        return None
    
    def display_system_info(self, info):
        """Display system information"""
        if not info:
            return
        
        print("\n" + "=" * 80)
        print("SYSTEM INFORMATION")
        print("=" * 80)
        print(f"Version          : {info['major_version']}.{info['minor_version']}")
        print(f"Serial number    : {info['serial']} (0x{info['serial']:08X})")
        print(f"Channel to read  : {info['channel_to_read']}")
        print(f"LED enabled      : {'YES' if info['led_enable'] else 'NO'}")
        print(f"\nCALIBRATION:")
        print(f"RShunt           : {info['rshunt']:.6f} Ω")
        print(f"Gain filtered    : {info['gain_filtered']:.6f}")
        print(f"Gain unfiltered  : {info['gain_unfiltered']:.6f}")
        print(f"Voltage offset   : {info['offset_volts']:.6f} V")
        print("=" * 80 + "\n")
    
    def display_measurement(self, data):
        """Display measurements"""
        if not data:
            return
        
        print("\n" + "=" * 80)
        print("SOLAR SCINTILLOMETER MEASUREMENTS")
        print("=" * 80)
        
        print(f"Local time  : {data['current_date'].strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}")
        print(f"UTC time    : {data['utc_date'].strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}")
        print(f"Samples     : {data['nb_samples']}")
        
        print(f"\n--- RAW DATA (ADU) ---")
        print(f"Mean (unfiltered)       : {data['mean_adu_nf']:10.2f} ADU")
        print(f"Std dev (unfiltered)    : {data['ec_adu_nf']:10.2f} ADU")
        print(f"Mean (filtered)         : {data['mean_adu_f']:10.2f} ADU")
        print(f"Scintillation power     : {data['power_scint_adu']:10.2f} ADU")
        
        print(f"\n--- VOLTAGES ---")
        print(f"Mean (unfiltered)    : {data['mean_volts_nf']:10.6f} V")
        print(f"Standard deviation   : {data['ec_volts_nf']:10.6f} V")
        print(f"Scintillation RMS    : {data['power_scint_volts']:10.6f} V")
        print(f"Useful signal        : {data['signal_utile_v']:10.6f} V")
        
        print(f"\n--- PHYSICAL MEASUREMENTS ---")
        print(f"Solar current         : {data['current_solar_a']*1000:10.3f} mA")
        print(f"Atmospheric seeing    : {data['seeing_arcsec']:10.3f} arcsec  {'✓' if data['valid_seeing'] else '✗'}")
        print(f"Fried parameter R₀    : {data['r0']:10.2f} mm")
        print(f"Box temperature       : {data['temp_inside_box']:10.2f} °C")
        
        if data['valid_seeing']:
            if data['seeing_arcsec'] < 1.0:
                quality = "EXCELLENT"
            elif data['seeing_arcsec'] < 2.0:
                quality = "GOOD"
            elif data['seeing_arcsec'] < 3.0:
                quality = "AVERAGE"
            else:
                quality = "POOR"
            print(f"\nAtmospheric quality   : {quality}")
        
        print("=" * 80 + "\n")


def main():
    solar = MicrochipHIDSolar()
    
    if not solar.connect():
        return
    
    try:
        # Read system parameters
        print("→ Reading system parameters...")
        sys_info = solar.get_system_info()
        
        if not sys_info:
            print("✗ Unable to read system parameters")
            return
        
        solar.display_system_info(sys_info)
        
        # Measurement loop
        print("Starting measurements (Ctrl+C to stop)...\n")
        counter = 0
        
        while True:
            print(f"[Measurement #{counter}]")
            
            # ADAPT THE COMMAND CODE ACCORDING TO YOUR FIRMWARE (0x81, 0x82, etc.)
            data = solar.get_measurement_data(command=0x81)
            
            if data:
                solar.display_measurement(data)
                
                # Summary
                print(f"→ Seeing={data['seeing_arcsec']:.2f}\" | "
                      f"I={data['current_solar_a']*1000:.1f}mA | "
                      f"T={data['temp_inside_box']:.1f}°C | "
                      f"R₀={data['r0']:.0f}mm")
            else:
                print("← No data")
            
            print()
            counter += 1
            time.sleep(2)
            
    except KeyboardInterrupt:
        print("\n✓ Stop requested")
    except Exception as e:
        print(f"\n✗ Error: {e}")
        import traceback
        traceback.print_exc()
    finally:
        solar.disconnect()


if __name__ == "__main__":
    main()

