'''
  _   _ ____  __  __ 
 | \ | |  _ \|  \/  |
 |  \| | |_) | |\/| |
 | |\  |  __/| |  | |
 |_| \_|_|   |_|  |_|

Script to get NPM data via Modbus

Improved version with data stream lenght check

/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py

Modbus RTU
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]

Pour récupérer
 les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
 les 5 cannaux
 la température et l'humidité à l'intérieur du capteur
Donnée actualisée toutes les 10 secondes

Request
\x01\x03\x00\x38\x00\x55\...\...

\x01        Slave Address (slave device address)
\x03        Function code (read multiple holding registers)
\x00\x38    Starting Address (The request starts reading from holding register address x38 or 56)
\x00\x55    Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
\...\...    Cyclic Redundancy Check (checksum )

MAJ 2026 --> renvoie des 0 si pas de réponse du NPM

'''
import serial
import requests
import json
import sys
import crcmod
import sqlite3
import time

# Dry-run mode: print JSON output without writing to database
dry_run = "--dry-run" in sys.argv

# Connect to the SQLite database
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor()

def load_config(config_file):
    try:
        with open(config_file, 'r') as file:
            config_data = json.load(file)
        return config_data
    except Exception as e:
        print(f"Error loading config file: {e}")
        return {}

# Load the configuration data
npm_solo_port = "/dev/ttyAMA5"  #port du NPM solo

#GET RTC TIME from SQlite
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone()  # Get the first (and only) row
rtc_time_str = row[1]  # '2025-02-07 12:30:45'

# Initialize default error values
pm1_10s = 0
pm25_10s = 0
pm10_10s = 0
channel_1 = 0
channel_2 = 0
channel_3 = 0
channel_4 = 0
channel_5 = 0
relative_humidity = 0
temperature = 0
npm_status = 0xFF  # Default: all errors (will be overwritten if read succeeds)

try:
    # Initialize serial port
    ser = serial.Serial(
        port=npm_solo_port,
        baudrate=115200,
        parity=serial.PARITY_EVEN,
        stopbits=serial.STOPBITS_ONE,
        bytesize=serial.EIGHTBITS,
        timeout=2
    )

    # Define Modbus CRC-16 function
    crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')

    # Request frame without CRC
    data = b'\x01\x03\x00\x38\x00\x55'

    # Calculate and append CRC
    crc = crc16(data)
    crc_low = crc & 0xFF
    crc_high = (crc >> 8) & 0xFF
    request = data + bytes([crc_low, crc_high])

    # Clear serial buffer before sending
    ser.flushInput()

    # Send request
    ser.write(request)
    time.sleep(0.2)  # Wait for sensor to respond

    # Read response
    response_length = 2 + 1 + (2 * 85) + 2  # Address + Function + Data + CRC
    byte_data = ser.read(response_length)

    # Validate response length
    if len(byte_data) < response_length:
        if not dry_run:
            print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
        raise Exception("Incomplete response")

    # Verify CRC
    received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
    calculated_crc = crc16(byte_data[:-2])

    if received_crc != calculated_crc:
        if not dry_run:
            print("[ERROR] CRC check failed! Corrupted data received.")
        raise Exception("CRC check failed")

    # Convert response to hex for debugging
    formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
    #print("Response:", formatted)

    # Extract and print PM values
    def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
        REGISTER_START = 56
        offset = (register - REGISTER_START) * 2 + 3

        if single_register:
            value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
        else:
            lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')  
            msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')  
            value = (msw << 16) | lsw

        value = value / scale

        if round_to == 0:
            return int(value)
        elif round_to is not None:
            return round(value, round_to)
        else:
            return value

    pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
    pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
    pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)

    #print("10 sec concentration:")
    #print(f"PM1: {pm1_10s}")
    #print(f"PM2.5: {pm25_10s}")
    #print(f"PM10: {pm10_10s}")

    # Extract values for 5 channels
    channel_1 = extract_value(byte_data, 128,  round_to=0)  # 0.2 - 0.5μm
    channel_2 = extract_value(byte_data, 130,  round_to=0)  # 0.5 - 1.0μm
    channel_3 = extract_value(byte_data, 132,  round_to=0)  # 1.0 - 2.5μm
    channel_4 = extract_value(byte_data, 134,  round_to=0)  # 2.5 - 5.0μm
    channel_5 = extract_value(byte_data, 136,  round_to=0)  # 5.0 - 10.0μm

    #print(f"Channel 1 (0.2->0.5): {channel_1}")
    #print(f"Channel 2 (0.5->1.0): {channel_2}")
    #print(f"Channel 3 (1.0->2.5): {channel_3}")
    #print(f"Channel 4 (2.5->5.0): {channel_4}")
    #print(f"Channel 5 (5.0->10.): {channel_5}")

            
    # Retrieve relative humidity from register 106 (0x6A)
    relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
    # Retrieve temperature from register 106 (0x6A)
    temperature = extract_value(byte_data, 107, 100, single_register=True)

    #print(f"Internal Relative Humidity: {relative_humidity} %")
    #print(f"Internal temperature: {temperature} °C")

    # Read NPM status register (register 19 = 0x13, 1 register)
    # Modbus request: slave=0x01, func=0x03, addr=0x0013, qty=0x0001
    status_request = b'\x01\x03\x00\x13\x00\x01'
    status_crc = crc16(status_request)
    status_request += bytes([status_crc & 0xFF, (status_crc >> 8) & 0xFF])

    ser.flushInput()
    ser.write(status_request)
    time.sleep(0.2)

    # Response: addr(1) + func(1) + byte_count(1) + data(2) + crc(2) = 7 bytes
    status_response = ser.read(7)
    if len(status_response) == 7:
        status_recv_crc = int.from_bytes(status_response[-2:], byteorder='little')
        status_calc_crc = crc16(status_response[:-2])
        if status_recv_crc == status_calc_crc:
            npm_status = int.from_bytes(status_response[3:5], byteorder='big') & 0xFF
            if not dry_run:
                print(f"NPM status: 0x{npm_status:02X} ({npm_status})")
        else:
            if not dry_run:
                print("[WARNING] NPM status CRC check failed, keeping default")
    else:
        if not dry_run:
            print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")

    ser.close()

except Exception as e:
    if not dry_run:
        print(f"[ERROR] Sensor communication failed: {e}")
    # Variables already set to -1 at the beginning

finally:
    if dry_run:
        # Print JSON output without writing to database
        result = {
            "PM1": pm1_10s,
            "PM25": pm25_10s,
            "PM10": pm10_10s,
            "temperature": temperature,
            "humidity": relative_humidity,
            "npm_status": npm_status,
            "npm_status_hex": f"0x{npm_status:02X}"
        }
        print(json.dumps(result))
    else:
        # Always save data to database, even if all values are 0
        cursor.execute('''
        INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
        , (rtc_time_str, channel_1, channel_2, channel_3, channel_4, channel_5))

        cursor.execute('''
        INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm, npm_status) VALUES (?,?,?,?,?,?,?)'''
        , (rtc_time_str, pm1_10s, pm25_10s, pm10_10s, temperature, relative_humidity, npm_status))

        # Commit and close the connection
        conn.commit()

    conn.close()