diff --git a/README.md b/README.md index 21b9fc4..eeb1d44 100755 --- a/README.md +++ b/README.md @@ -7,14 +7,13 @@ Version Pro du ModuleAir avec CM4, SaraR4 et ecran Matrix LED p2 64x64. sudo apt update sudo apt install git gh apache2 php php-sqlite3 python3 python3-pip jq g++ autossh i2c-tools python3-smbus -y sudo pip3 install pyserial requests sensirion-shdlc-sfa3x RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil --break-system-packages -sudo gh auth login -git config --global user.email "paulvuarambon@gmail.com" -git config --global user.name "PaulVua" -sudo gh repo clone aircarto/moduleair_pro_4g /var/www/moduleair_pro_4g +sudo git clone http://gitea.aircarto.fr/PaulVua/moduleair_pro_4g.git /var/www/moduleair_pro_4g sudo mkdir /var/www/moduleair_pro_4g/logs sudo touch /var/www/moduleair_pro_4g/logs/app.log /var/www/moduleair_pro_4g/logs/loop.log /var/www/moduleair_pro_4g/matrix/input_NPM.txt /var/www/moduleair_pro_4g/matrix/input_MHZ16.txt /var/www/moduleair_pro_4g/wifi_list.csv sudo cp /var/www/moduleair_pro_4g/config.json.dist /var/www/moduleair_pro_4g/config.json sudo chmod -R 777 /var/www/moduleair_pro_4g/ +git config core.fileMode false + ``` ## Apache diff --git a/SARA/reboot/start.py b/SARA/reboot/start.py index 7b37c10..db8f135 100644 --- a/SARA/reboot/start.py +++ b/SARA/reboot/start.py @@ -17,52 +17,96 @@ import time import sys import json import re +import sqlite3 -#get data from config -def load_config(config_file): +# database connection +conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db") +cursor = conn.cursor() + +#get config data from SQLite table +def load_config_sqlite(): + """ + Load configuration data from SQLite config table + + Returns: + dict: Configuration data with proper type conversion + """ try: - with open(config_file, 'r') as file: - config_data = json.load(file) + + # Query the config table + cursor.execute("SELECT key, value, type FROM config_table") + rows = cursor.fetchall() + + # Create config dictionary + config_data = {} + for key, value, type_name in rows: + # Convert value based on its type + if type_name == 'bool': + config_data[key] = value == '1' or value == 'true' + elif type_name == 'int': + config_data[key] = int(value) + elif type_name == 'float': + config_data[key] = float(value) + else: + config_data[key] = value + return config_data + except Exception as e: - print(f"Error loading config file: {e}") + print(f"Error loading config from SQLite: {e}") return {} -#Fonction pour mettre à jour le JSON de configuration -def update_json_key(file_path, key, value): +def update_sqlite_config(key, value): """ - Updates a specific key in a JSON file with a new value. + Updates a specific key in the SQLite config_table with a new value. - :param file_path: Path to the JSON file. - :param key: The key to update in the JSON file. + :param key: The key to update in the config_table. :param value: The new value to assign to the key. """ try: - # Load the existing data - with open(file_path, "r") as file: - data = json.load(file) + + # Check if the key exists and get its type + cursor.execute("SELECT type FROM config_table WHERE key = ?", (key,)) + result = cursor.fetchone() - # Check if the key exists in the JSON file - if key in data: - data[key] = value # Update the key with the new value - else: - print(f"Key '{key}' not found in the JSON file.") + if result is None: + print(f"Key '{key}' not found in the config_table.") + conn.close() return - # Write the updated data back to the file - with open(file_path, "w") as file: - json.dump(data, file, indent=2) # Use indent for pretty printing + # Get the type of the value from the database + value_type = result[0] - print(f"💾 updating '{key}' to '{value}'.") + # Convert the value to the appropriate string representation based on its type + if value_type == 'bool': + # Convert Python boolean or string 'true'/'false' to '1'/'0' + if isinstance(value, bool): + str_value = '1' if value else '0' + else: + str_value = '1' if str(value).lower() in ('true', '1', 'yes', 'y') else '0' + elif value_type == 'int': + str_value = str(int(value)) + elif value_type == 'float': + str_value = str(float(value)) + else: + str_value = str(value) + + # Update the value in the database + cursor.execute("UPDATE config_table SET value = ? WHERE key = ?", (str_value, key)) + + # Commit the changes and close the connection + conn.commit() + + print(f"💾 Updated '{key}' to '{value}' in database.") except Exception as e: - print(f"Error updating the JSON file: {e}") + print(f"Error updating the SQLite database: {e}") -# Define the config file path -config_file = '/var/www/moduleair_pro_4g/config.json' -# Load the configuration data -config = load_config(config_file) +#Load config +config = load_config_sqlite() +#config baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4 device_id = config.get('deviceID', '').upper() #device ID en maj +sara_r5_DPD_setup = False ser_sara = serial.Serial( port='/dev/ttyAMA2', @@ -117,21 +161,46 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout return response.decode('utf-8', errors='replace') # Return the full response if no target line is found + + try: print('

Start reboot python script

') #check modem status + #Attention: + # SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B + # SArA R5 response: SARA-R500S-01B-00 print("⚙️Check SARA Status") command = f'ATI\r' ser_sara.write(command.encode('utf-8')) response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"]) print(response_SARA_ATI) - match = re.search(r"Model:\s*(.+)", response_SARA_ATI) - model = match.group(1).strip() if match else "Unknown" # Strip unwanted characters - print(f" Model: {model}") - update_json_key(config_file, "modem_version", model) + + # Check for SARA model with more robust regex + model = "Unknown" + if "SARA-R410M" in response_SARA_ATI: + model = "SARA-R410M" + print("📱 Detected SARA R4 modem") + elif "SARA-R500" in response_SARA_ATI: + model = "SARA-R500" + print("📱 Detected SARA R5 modem") + sara_r5_DPD_setup = True + else: + # Fallback to regex match if direct string match fails + match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI) + if match: + model = match.group(1).strip() + else: + model = "Unknown" + print("⚠️ Could not identify modem model") + + print(f"🔍 Model: {model}") + update_sqlite_config("modem_version", model) time.sleep(1) + ''' + AIRCARTO + ''' # 1. Set AIRCARTO URL print('➡️Set aircarto URL') @@ -143,6 +212,10 @@ try: print(response_SARA_1) time.sleep(1) + ''' + uSpot + ''' + #2. Set uSpot URL print('➡️Set uSpot URL') uSpot_profile_id = 1 @@ -179,9 +252,9 @@ try: else: print("❌ Failed to extract coordinates.") - #update config.json - update_json_key(config_file, "latitude_raw", float(latitude)) - update_json_key(config_file, "longitude_raw", float(longitude)) + #update sqlite table + update_sqlite_config("latitude_raw", float(latitude)) + update_sqlite_config("longitude_raw", float(longitude)) time.sleep(1) diff --git a/SARA/sara.py b/SARA/sara.py index e6e0996..cfc5030 100755 --- a/SARA/sara.py +++ b/SARA/sara.py @@ -28,11 +28,9 @@ port='/dev/'+parameter[0] # ex: ttyAMA2 command = parameter[1] # ex: AT+CCID? timeout = float(parameter[2]) # ex:2 -baudrate = 9600 - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_connectNetwork.py b/SARA/sara_connectNetwork.py index 73b79d2..ea94fa1 100755 --- a/SARA/sara_connectNetwork.py +++ b/SARA/sara_connectNetwork.py @@ -24,11 +24,9 @@ port='/dev/'+parameter[0] # ex: ttyAMA2 networkID = parameter[1] # ex: 20801 timeout = float(parameter[2]) # ex:2 -baudrate = 9600 - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_eraseMessage.py b/SARA/sara_eraseMessage.py index 262740f..a53f015 100755 --- a/SARA/sara_eraseMessage.py +++ b/SARA/sara_eraseMessage.py @@ -17,11 +17,9 @@ parameter = sys.argv[1:] # Exclude the script name port='/dev/'+parameter[0] # ex: ttyAMA2 message = parameter[1] # ex: Hello -baudrate = 9600 - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_readMessage.py b/SARA/sara_readMessage.py index 421e250..847fb08 100755 --- a/SARA/sara_readMessage.py +++ b/SARA/sara_readMessage.py @@ -16,11 +16,9 @@ parameter = sys.argv[1:] # Exclude the script name port='/dev/'+parameter[0] # ex: ttyAMA2 message = parameter[1] # ex: Hello -baudrate = 9600 - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_sendMessage.py b/SARA/sara_sendMessage.py index 46864c7..7018fad 100755 --- a/SARA/sara_sendMessage.py +++ b/SARA/sara_sendMessage.py @@ -18,11 +18,9 @@ port='/dev/'+parameter[0] # ex: ttyAMA2 endpoint = parameter[1] # ex: /pro_4G/notif_message.php profile_id = parameter[2] -baudrate = 9600 - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_setAPN.py b/SARA/sara_setAPN.py index 7104c18..e7d6df3 100755 --- a/SARA/sara_setAPN.py +++ b/SARA/sara_setAPN.py @@ -22,12 +22,9 @@ apn_address = parameter[1] # ex: data.mono timeout = float(parameter[2]) # ex:2 -baudrate = 9600 - - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_setURL.py b/SARA/sara_setURL.py index 68f1868..7798878 100755 --- a/SARA/sara_setURL.py +++ b/SARA/sara_setURL.py @@ -27,12 +27,9 @@ port='/dev/'+parameter[0] # ex: ttyAMA2 url = parameter[1] # ex: data.mobileair.fr profile_id = parameter[2] #ex: 0 - -baudrate = 9600 - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_setURL_uSpot_noSSL.py b/SARA/sara_setURL_uSpot_noSSL.py index b0b8af9..8d71295 100755 --- a/SARA/sara_setURL_uSpot_noSSL.py +++ b/SARA/sara_setURL_uSpot_noSSL.py @@ -46,11 +46,9 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout return response.decode('utf-8', errors='replace') -baudrate = 9600 - ser_sara = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/SARA/sara_writeMessage.py b/SARA/sara_writeMessage.py index 4e0d6f9..2fe4a91 100755 --- a/SARA/sara_writeMessage.py +++ b/SARA/sara_writeMessage.py @@ -17,12 +17,9 @@ parameter = sys.argv[1:] # Exclude the script name port='/dev/'+parameter[0] # ex: ttyAMA2 message = parameter[1] # ex: Hello - -baudrate = 9600 - ser = serial.Serial( port=port, #USB0 or ttyS0 - baudrate=baudrate, #115200 ou 9600 + baudrate=115200, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, diff --git a/loop/SARA_send_data_v2.py b/loop/SARA_send_data_v2.py index 93732cf..06515bc 100755 --- a/loop/SARA_send_data_v2.py +++ b/loop/SARA_send_data_v2.py @@ -156,60 +156,88 @@ def blink_led(pin, blink_count, delay=1): GPIO.cleanup(pin) # Clean up the specific pin to reset its state print(f"GPIO {pin} cleaned up") -#get data from config -def load_config(config_file): +#get config data from SQLite table +def load_config_sqlite(): + """ + Load configuration data from SQLite config table + + Returns: + dict: Configuration data with proper type conversion + """ try: - with open(config_file, 'r') as file: - config_data = json.load(file) + + # Query the config table + cursor.execute("SELECT key, value, type FROM config_table") + rows = cursor.fetchall() + + # Create config dictionary + config_data = {} + for key, value, type_name in rows: + # Convert value based on its type + if type_name == 'bool': + config_data[key] = value == '1' or value == 'true' + elif type_name == 'int': + config_data[key] = int(value) + elif type_name == 'float': + config_data[key] = float(value) + else: + config_data[key] = value + return config_data + except Exception as e: - print(f"Error loading config file: {e}") + print(f"Error loading config from SQLite: {e}") return {} -#Fonction pour mettre à jour le JSON de configuration -def update_json_key(file_path, key, value): +def load_config_scripts_sqlite(): """ - Updates a specific key in a JSON file with a new value. - - :param file_path: Path to the JSON file. - :param key: The key to update in the JSON file. - :param value: The new value to assign to the key. + Load script configuration data from SQLite config_scripts_table + + Returns: + dict: Script paths as keys and enabled status as boolean values """ try: - # Load the existing data - with open(file_path, "r") as file: - data = json.load(file) + # Query the config_scripts_table + cursor.execute("SELECT script_path, enabled FROM config_scripts_table") + rows = cursor.fetchall() - # Check if the key exists in the JSON file - if key in data: - data[key] = value # Update the key with the new value - else: - print(f"Key '{key}' not found in the JSON file.") - return + # Create config dictionary with script paths as keys and enabled status as boolean values + scripts_config = {} + for script_path, enabled in rows: + # Convert integer enabled value (0/1) to boolean + scripts_config[script_path] = bool(enabled) - # Write the updated data back to the file - with open(file_path, "w") as file: - json.dump(data, file, indent=2) # Use indent for pretty printing + return scripts_config - print(f"updating '{key}' to '{value}'.") except Exception as e: - print(f"Error updating the JSON file: {e}") + print(f"Error loading scripts config from SQLite: {e}") + return {} # Define the config file path config_file = '/var/www/moduleair_pro_4g/config.json' -# Load the configuration data -config = load_config(config_file) -baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4 -device_id = config.get('deviceID', '').upper() #device ID en maj -bme_280_config = config.get('BME280/get_data_v2.py', False) #présence du BME280 -envea_cairsens= config.get('envea/read_value_v2.py', False) -send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.moduleair.fr) -send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot () -selected_networkID = config.get('SARA_R4_neworkID', '') -npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM +#Load config +config = load_config_sqlite() +#config +device_id = config.get('deviceID', 'unknown') +device_id = device_id.upper() +modem_config_mode = config.get('modem_config_mode', False) +device_latitude_raw = config.get('latitude_raw', 0) +device_longitude_raw = config.get('longitude_raw', 0) +modem_version=config.get('modem_version', "") +Sara_baudrate = config.get('SaraR4_baudrate', 115200) +npm_5channel = config.get('npm_5channel', False) #5 canaux du NPM +selected_networkID = int(config.get('SARA_R4_neworkID', 0)) +send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot () -modem_config_mode = config.get('modem_config_mode', False) #modem 4G en mode configuration +reset_uSpot_url = False + +#config_scripts +config_scripts = load_config_scripts_sqlite() +bme_280_config = config_scripts.get('BME280/get_data_v2.py', False) +envea_cairsens= config_scripts.get('envea/read_value_v2.py', False) +co2_mhz19= config_scripts.get('MH-Z19/write_data.py', False) +sensirion_sfa30= config_scripts.get('sensirion/SFA30_read.py', False) #update device id in the payload json payload_json["moduleairid"] = device_id @@ -221,7 +249,7 @@ if modem_config_mode: ser_sara = serial.Serial( port='/dev/ttyAMA2', - baudrate=baudrate, #115200 ou 9600 + baudrate=Sara_baudrate, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, @@ -272,6 +300,182 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout return response.decode('utf-8', errors='replace') # Return the full response if no target line is found +def extract_error_code(response): + """ + Extract just the error code from AT+UHTTPER response + """ + for line in response.split('\n'): + if '+UHTTPER' in line: + try: + # Split the line and get the third value (error code) + parts = line.split(':')[1].strip().split(',') + if len(parts) >= 3: + error_code = int(parts[2]) + return error_code + except: + pass + + # Return None if we couldn't find the error code + return None + +def send_error_notification(device_id, error_type, additional_info=None): + """ + Send an error notification to the server when issues with the SARA module occur. + Will silently fail if there's no internet connection. + + Parameters: + ----------- + device_id : str + The unique identifier of the device + error_type : str + Type of error encountered (e.g., 'serial_error', 'cme_error', 'http_error', 'timeout') + additional_info : str, optional + Any additional information about the error for logging purposes + + Returns: + -------- + bool + True if notification was sent successfully, False otherwise + """ + + # Create the alert URL with all relevant parameters + base_url = 'http://data.nebuleair.fr/pro_4G/alert.php' + alert_url = f'{base_url}?capteur_id={device_id}&error_type={error_type}' + + # Add additional info if provided + if additional_info: + # Make sure to URL encode the additional info + from urllib.parse import quote + alert_url += f'&details={quote(str(additional_info))}' + + # Try to send the notification, catch ALL exceptions + try: + response = requests.post(alert_url, timeout=3) + if response.status_code == 200: + print(f"✅ Alert notification sent successfully") + return True + else: + print(f"⚠️ Alert notification failed: Status code {response.status_code}") + except Exception as e: + print(f"⚠️ Alert notification couldn't be sent: {e}") + + return False + +def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id): + """ + Performs a complete modem restart sequence: + 1. Reboots the modem using the appropriate command for its version + 2. Waits for the modem to restart + 3. Resets the HTTP profile + 4. For SARA-R5, resets the PDP connection + + Args: + modem_version (str): The modem version, e.g., 'SARA-R500' or 'SARA-R410' + aircarto_profile_id (int): The HTTP profile ID to reset + + Returns: + bool: True if the complete sequence was successful, False otherwise + """ + print('🔄 Complete SARA reboot and reinitialize sequence 🔄') + + # Step 1: Reboot the modem - Integrated modem_software_reboot logic + print('🔄 Software SARA reboot! 🔄') + + # Use different commands based on modem version + if 'R5' in modem_version: # For SARA-R5 series + command = 'AT+CFUN=16\r' # Normal restart for R5 + else: # For SARA-R4 series + command = 'AT+CFUN=15\r' # Factory reset for R4 + + ser_sara.write(command.encode('utf-8')) + response = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"], debug=True) + + print('

') + print(response) + print("

", end="") + + # Check if reboot command was acknowledged + reboot_success = response is not None and "OK" in response + if not reboot_success: + print("⚠️ Modem reboot command failed") + return False + + # Step 2: Wait for the modem to restart (adjust time as needed) + print("Waiting for modem to restart...") + time.sleep(15) # 15 seconds should be enough for most modems to restart + + # Step 3: Check if modem is responsive after reboot + print("Checking if modem is responsive...") + ser_sara.write(b'AT\r') + response_check = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True) + if response_check is None or "OK" not in response_check: + print("⚠️ Modem not responding after reboot") + return False + + print("✅ Modem restarted successfully") + + # Step 4: Reset the HTTP Profile + print('🔧 Resetting the HTTP Profile') + command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r' + ser_sara.write(command.encode('utf-8')) + responseResetHTTP = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, + wait_for_lines=["OK", "+CME ERROR"], debug=True) + print('

') + print(responseResetHTTP) + print("

", end="") + + http_reset_success = responseResetHTTP is not None and "OK" in responseResetHTTP + if not http_reset_success: + print("⚠️ HTTP profile reset failed") + # Continue anyway, don't return False here + + # Step 5: For SARA-R5, reset the PDP connection + pdp_reset_success = True + if modem_version == "SARA-R500": + print("⚠️ Need to reset PDP connection for SARA-R500") + + # Activate PDP context 1 + print('➡️ Activate PDP context 1') + command = f'AT+CGACT=1,1\r' + ser_sara.write(command.encode('utf-8')) + response_pdp1 = read_complete_response(ser_sara, wait_for_lines=["OK"]) + print(response_pdp1, end="") + pdp_reset_success = pdp_reset_success and (response_pdp1 is not None and "OK" in response_pdp1) + time.sleep(1) + + # Set the PDP type + print('➡️ Set the PDP type to IPv4 referring to the output of the +CGDCONT read command') + command = f'AT+UPSD=0,0,0\r' + ser_sara.write(command.encode('utf-8')) + response_pdp2 = read_complete_response(ser_sara, wait_for_lines=["OK"]) + print(response_pdp2, end="") + pdp_reset_success = pdp_reset_success and (response_pdp2 is not None and "OK" in response_pdp2) + time.sleep(1) + + # Profile #0 is mapped on CID=1 + print('➡️ Profile #0 is mapped on CID=1.') + command = f'AT+UPSD=0,100,1\r' + ser_sara.write(command.encode('utf-8')) + response_pdp3 = read_complete_response(ser_sara, wait_for_lines=["OK"]) + print(response_pdp3, end="") + pdp_reset_success = pdp_reset_success and (response_pdp3 is not None and "OK" in response_pdp3) + time.sleep(1) + + # Activate the PSD profile + print('➡️ Activate the PSD profile #0: the IPv4 address is already assigned by the network.') + command = f'AT+UPSDA=0,3\r' + ser_sara.write(command.encode('utf-8')) + response_pdp4 = read_complete_response(ser_sara, wait_for_lines=["OK", "+UUPSDA"]) + print(response_pdp4, end="") + pdp_reset_success = pdp_reset_success and (response_pdp4 is not None and ("OK" in response_pdp4 or "+UUPSDA" in response_pdp4)) + time.sleep(1) + + if not pdp_reset_success: + print("⚠️ PDP connection reset had some issues") + + # Return overall success + return http_reset_success and pdp_reset_success + try: ''' _ ___ ___ ____ @@ -288,16 +492,35 @@ try: 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' - # Convert to a datetime object - dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S') - # Convert to InfluxDB RFC3339 format with UTC 'Z' suffix - influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ') - print(influx_timestamp) + print(rtc_time_str) + + if rtc_time_str == 'not connected': + print("⛔ Atttention RTC module not connected⛔") + rtc_status = "disconnected" + influx_timestamp="rtc_disconnected" + else : + # Convert to a datetime object + dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S') + # Check if timestamp is reset (year 2000) + if dt_object.year == 2000: + print("⛔ Attention: RTC has been reset to default date ⛔") + rtc_status = "reset" + else: + print("✅ RTC timestamp is valid") + rtc_status = "valid" + + # Always convert to InfluxDB format + # Convert to InfluxDB RFC3339 format with UTC 'Z' suffix + influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ') + rtc_status = "valid" + print(influx_timestamp) #NEXTPM print("➡️Getting NPM values (last 6 measures)") #cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1") - cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6") + #cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6") + cursor.execute("SELECT rowid, * FROM data_NPM ORDER BY rowid DESC LIMIT 6") + rows = cursor.fetchall() # Exclude the timestamp column (assuming first column is timestamp) data_values = [row[1:] for row in rows] # Exclude timestamp @@ -396,12 +619,42 @@ try: print("Verify SARA R4 connection") # Getting the LTE Signal - print("-> Getting LTE signal <-") + print("➡️Getting LTE signal") ser_sara.write(b'AT+CSQ\r') - response2 = read_complete_response(ser_sara, wait_for_lines=["OK"]) + response2 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR", "+CME ERROR"]) print('

') print(response2) - print("

") + print("

", end="") + + #Here it's possible that the SARA do not repond at all or send a error message + #-> TO DO : harware reboot + #-> send notification + #-> end loop, no need to continue + + #1. No answer at all form SARA + if response2 is None or response2 == "": + print("No answer from SARA module") + print('🛑STOP LOOP🛑') + print("
") + + #Send notification (WIFI) + send_error_notification(device_id, "serial_error") + + #end loop + sys.exit() + + #2. si on a une erreur + elif "+CME ERROR" in response2: + print(f"SARA module returned error: {response2}") + print("The CSQ command is not supported by this module or in its current state") + print("⚠️ATTENTION: SARA is connected over serial but CSQ command not supported") + print('🛑STOP LOOP🛑') + #end loop + sys.exit() + + else : + print("✅SARA is connected over serial") + match = re.search(r'\+CSQ:\s*(\d+),', response2) if match: signal_quality = int(match.group(1)) @@ -418,7 +671,7 @@ try: responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20) print('

') print(responseReconnect) - print("

") + print("

", end="") print('🛑STOP LOOP🛑') print("
") @@ -439,14 +692,14 @@ try: print("Open JSON:") command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r' ser_sara.write(command.encode('utf-8')) - response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=False) + response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=True) print(response_SARA_1) time.sleep(1) #2. Write to shell print("Write data to memory:") ser_sara.write(csv_string.encode()) - response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) + response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True) print(response_SARA_2) #3. Send to endpoint (with device ID) @@ -454,11 +707,11 @@ try: command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&datetime={influx_timestamp}","server_response.txt","sensordata_csv.json",4\r' ser_sara.write(command.encode('utf-8')) - response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR"], debug=True) + response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR", "ERROR"], debug=True) print('

') print(response_SARA_3) - print("

") + print("

", end="") # si on recoit la réponse UHTTPCR if "+UUHTTPCR" in response_SARA_3: @@ -485,9 +738,7 @@ try: print('ATTENTION: CME ERROR') print("error:", lines[-1]) print("*****") - #update status - update_json_key(config_file, "SARA_R4_network_status", "disconnected") - + # Gestion de l'erreur spécifique if "No connection to phone" in lines[-1]: print("No connection to the phone. Retrying or reset may be required.") @@ -519,11 +770,10 @@ try: http_response = lines[-1] # "+UUHTTPCR: 0,4,0" parts = http_response.split(',') - # 2.1 code 0 (HTTP failed) + # 2.1 code 0 (HTTP failed) ⛔⛔⛔ if len(parts) == 3 and parts[-1] == '0': # The third value indicates success print("*****") print('ATTENTION: HTTP operation failed') - update_json_key(config_file, "SARA_R4_network_status", "disconnected") print("*****") print("Blink red LED") # Run LED blinking in a separate thread @@ -537,44 +787,120 @@ try: response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) print('

') print(response_SARA_9) - print("

") + print("

", end="") - ''' - +UHTTPER: profile_id,error_class,error_code + # Extract just the error code + error_code = extract_error_code(response_SARA_9) + if error_code is not None: + # Display interpretation based on error code + if error_code == 0: + print('

No error detected

') + elif error_code == 4: + print('

Error 4: Invalid server Hostname

') + elif error_code == 11: + print('

Error 11: Server connection error

') + elif error_code == 22: + print('

⚠️Error 22: PSD or CSD connection not established (SARA-R5 need to reset PDP conection)⚠️

') + elif error_code == 73: + print('

Error 73: Secure socket connect error

') + else: + print(f'

Unknown error code: {error_code}

') + else: + print('

Could not extract error code from response

') + - error_class - 0 OK, no error - 3 HTTP Protocol error class - 10 Wrong HTTP API USAGE + #Software Reboot + software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id) + if software_reboot_success: + print("Modem successfully rebooted and reinitialized") + else: + print("There were issues with the modem reboot/reinitialize process") - error_code (for error_class 3) - 0 No error - 11 Server connection error - 73 Secure socket connect error - ''' - - #Pas forcément un moyen de résoudre le soucis - #print("resetting the URL (domain name):") - #command = f'AT+UHTTP={aircarto_profile_id},1,"{url_moduleair}"\r' - #ser_sara.write(command.encode('utf-8')) - #response_SARA_31 = read_complete_response(ser_sara) - #print(response_SARA_31) - - # 2.2 code 1 (HHTP succeded) + # 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅) else: # Si la commande HTTP a réussi - print('HTTP operation successful.') - update_json_key(config_file, "SARA_R4_network_status", "connected") + print('✅✅HTTP operation successful.') print("Blink blue LED") led_thread = Thread(target=blink_led, args=(23, 5, 0.5)) led_thread.start() + #4. Read reply from server print("Reply from server:") ser_sara.write(b'AT+URDFILE="server_response.txt"\r') response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) print('

') print(response_SARA_4) - print('

') + print("

", end="") + + #Parse the server datetime + # Extract just the date from the response + date_string = None + date_start = response_SARA_4.find("Date: ") + if date_start != -1: + date_end = response_SARA_4.find("\n", date_start) + date_string = response_SARA_4[date_start + 6:date_end].strip() + print(f'
Server date: {date_string}
', end="") + + # Optionally convert to datetime object + try: + from datetime import datetime + server_datetime = datetime.strptime( + date_string, + "%a, %d %b %Y %H:%M:%S %Z" + ) + #print(f'

Parsed datetime: {server_datetime}

') + except Exception as e: + print(f'

Error parsing date: {e}

') + + # Get RTC time from SQLite + cursor.execute("SELECT * FROM timestamp_table LIMIT 1") + row = cursor.fetchone() + rtc_time_str = row[1] # '2025-02-07 12:30:45' or '2000-01-01 00:55:21' or 'not connected' + print(f'
RTC time: {rtc_time_str}
', end="") + + # Compare times if both are available + if server_datetime and rtc_time_str != 'not connected': + try: + # Convert RTC time string to datetime + rtc_datetime = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S') + + # Calculate time difference in seconds + time_diff = abs((server_datetime - rtc_datetime).total_seconds()) + + print(f'
Time difference: {time_diff:.2f} seconds
', end="") + + # Check if difference is more than 60 seconds + # and update the RTC clock + if time_diff > 60: + print(f'
⚠️ RTC time differs from server time by {time_diff:.2f} seconds!
', end="") + # Format server time for RTC update + server_time_formatted = server_datetime.strftime('%Y-%m-%d %H:%M:%S') + + #update RTC module do not wait for answer, non blocking + #/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39' + # Launch RTC update script as non-blocking subprocess + import subprocess + update_command = [ + "/usr/bin/python3", + "/var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py", + server_time_formatted + ] + + # Execute the command without waiting for result + subprocess.Popen(update_command, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + print(f'
➡️ Updating RTC with server time: {server_time_formatted}
', end="") + + else: + print(f'
✅ RTC time is synchronized with server time (within 60 seconds)
') + + except Exception as e: + print(f'

Error comparing times: {e}

') + + + #Si non ne recoit pas de réponse UHTTPCR #on a peut etre une ERROR de type "+CME ERROR: No connection to phone" @@ -606,7 +932,7 @@ try: responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True) print('

') print(responseReconnect) - print("

") + print("

", end="") # Handle "Operation not allowed" error if error_message == "Operation not allowed": print('❓Try Resetting the HTTP Profile❓') @@ -615,20 +941,32 @@ try: responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True) print('

') print(responseResetHTTP_profile) - print("

") + print("

", end="") + if "ERROR" in line: + print("⛔Attention ERROR!⛔") + #Send notification (WIFI) + send_error_notification(device_id, "sara_error") + #Software Reboot + software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id) + if software_reboot_success: + print("Modem successfully rebooted and reinitialized") + else: + print("There were issues with the modem reboot/reinitialize process") #5. empty json print("Empty SARA memory:") ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r') response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) + print('

') print(response_SARA_5) + print("

", end="") + + if "+CME ERROR" in response_SARA_5: + print("⛔ Attention CME ERROR ⛔") - - - # Calculate and print the elapsed time elapsed_time = time.time() - start_time_script print(f"Elapsed time: {elapsed_time:.2f} seconds")