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

Script to get NPM data via Modbus
need parameter: port
/usr/bin/python3 /var/www/moduleair_pro_4g/NPM/get_data_modbus_v2.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 )

'''

import serial
import requests
import json
import sys
import crcmod
import sqlite3

# 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
config_file = '/var/www/nebuleair_pro_4g/config.json'
config = load_config(config_file)
npm_solo_port = config.get('NPM_solo_port', '')  #port du NPM solo

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 CRC
crc = crc16(data)
crc_low = crc & 0xFF
crc_high = (crc >> 8) & 0xFF

# Append CRC to the frame
request = data + bytes([crc_low, crc_high])
#print(f"Request frame: {request.hex()}")

ser.write(request)

#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'

while True:
    try:
        byte_data = ser.readline()
        formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
        print(formatted)

        # Register base (56 = 0x38)
        REGISTER_START = 56

        # Function to extract 32-bit values from Modbus response
        def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
            """Extracts a value from Modbus response.
            
            - `register`: Modbus register to read.
            - `scale`: Value is divided by this (e.g., `1000` for PM values).
            - `single_register`: If `True`, only reads 16 bits (one register).
            """
            offset = (register - REGISTER_START) * 2 + 3  # Calculate byte offset

            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  # 32-bit value

            value = value / scale  # Apply scaling

            if round_to == 0:
                return int(value)  # Convert to integer to remove .0
            elif round_to is not None:
                return round(value, round_to)  # Apply normal rounding
            else:
                return value  # No rounding if round_to is None

        # 10-sec PM Concentration (PM1, PM2.5, PM10)
        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}")

        # 1-min PM Concentration
        pm1_1min = extract_value(byte_data, 68, 1000, round_to=1)
        pm25_1min = extract_value(byte_data, 70, 1000, round_to=1)
        pm10_1min = extract_value(byte_data, 72, 1000, round_to=1)

        #print("1 min concentration:")
        #print(f"PM1: {pm1_1min}")
        #print(f"PM2.5: {pm25_1min}")
        #print(f"PM10: {pm10_1min}")

        # 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)
        #print(f"Internal Relative Humidity: {relative_humidity} %")
        # Retrieve temperature from register 106 (0x6A)
        temperature = extract_value(byte_data, 107, 100, single_register=True)
        #print(f"Internal temperature: {temperature} °C")

        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) VALUES (?,?,?,?,?,?)'''
        , (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity  ))


        # Commit and close the connection
        conn.commit()


        break
    except KeyboardInterrupt:
        print("User interrupt encountered. Exiting...")
        time.sleep(3)
        exit()
    except:
        # for all other kinds of error, but not specifying which one
        print("Unknown error...")
        time.sleep(3)
        exit()

conn.close()
