# V1.0 Maintain Ah on restart # @reboot sleep 20 && sudo python /var/www/html/BatteryMon_3.py & # 24.6.23 Voltage measurement for Controller 2 # IMPORT DEFINITIONS #************************************************************************************** import platform # for platform.system() check from pathlib import Path import sys import os # for CSV file existence check import time import serial import datetime # for time management import numpy as np # !!!not sure if needed import matplotlib # Styles matplotlib.use('Agg') #matplotlib.use('TkAgg') # !!! backend should speed up import pylab import matplotlib.pyplot as plt #graphs library #import random as rnd plt.style.use('ggplot') #plt.style.use(['dark_background', 'presentation']) import matplotlib.dates as mdates #formatting the graph from drawnow import * import atexit from csv import writer # for CSV file control import csv import logging import traceback import signal # FILE DEFINITIONS #************************************************************************************** if platform.system() == "Windows": base_path = Path("C:\Apache24\htdocs\Data") # Vaihda halutuksi else: base_path = Path("/var/www/html/Data") Controlfile = base_path / 'Control.csv' # CSV file for control data StatusFile = base_path / 'Status.csv' # CSV file for status data DataFile = base_path / 'Tiedosto.csv' # CSV file for log data GraphFile = base_path / 'GraphFile.csv' # CSV file for graph data Graph = base_path / 'Graph6' # CSV file for graph data # HW RELATED DEFINITIONS #************************************************************************************** global VoltageFactor VoltageFactor = 0.0179 #value for older HW #VoltageFactor = 0.0189 original value #VoltageFactor = 0.0195 global IdcFactor # DC virran mittaus #IdcFactor = 0.715/20 # = 0,03575 Controller V2 IdcFactor = 0.749/50 # = 0,01498 Controller V1 # ARDUINO INTERFACE #************************************************************************************** # Detect the operating system and define the serial USB port accordingly def get_serial_port(): if platform.system() == "Windows": return "COM4" # Example for Windows elif platform.system() == "Linux": return "/dev/ttyACM0" # Example for Raspberry Pi else: raise EnvironmentError("Unsupported platform") port = get_serial_port() # Define and open serial port, baud must be same as in Arduino #port = "/dev/ttyUSB0" #vanhalla Arduino Unolla #port = "COM4" # Arduino Nano with PC #port = "/dev/ttyUSB0" # Arduino Nano #port = "/dev/ttyACM0" # Arduino Every baud = 9600 # GRAPHS DEFINITIONS #************************************************************************************** def OwnGraph1(): plt.figure(1) fig = plt.figure(1) fig.set_figwidth(5) #Picture sizing fig.set_figheight(8) #SoC plt.subplot(311) plt.ylim(0, AhValue) plt.title("Varaustila %1.1f Ah" %Energy2, fontweight='bold', fontsize=12) plt.plot(x, values3, 'g') plt.grid(linewidth=1) plt.axhspan(60, 300, facecolor="green", alpha=0.1) plt.axhspan(30, 60, facecolor="yellow", alpha=0.1) plt.axhspan(0, 30, facecolor="red", alpha=0.1) #plt.suptitle("xxxxx", fontsize=12) #Current plt.subplot(312) plt.title("Virta %1.1f A, (+lataus, -purku)" %Current, fontweight='bold', fontsize=12) if ((max(values2) < 10) & (min(values2) > -10)): plt.ylim(-10, 10) plt.plot(x, values2, 'r') #Voltage plt.subplot(313) plt.title("Jannite %1.1f V" %Voltage, fontweight='bold', fontsize=12) plt.ylim(10, 16) plt.plot(x, values, 'b') plt.grid(linewidth=1) plt.axhspan(14.8, 16, facecolor="red", alpha=0.1) plt.axhspan(11.5, 14.8, facecolor="green", alpha=0.1) plt.axhspan(10, 11.5, facecolor="red", alpha=0.1) fig.tight_layout() plt.gcf().autofmt_xdate() #plt.show() # FUNCTIONS #************************************************************************************** def setup_logging(): """Setup logging to capture script termination reasons""" log_file = '/var/www/html/BatteryMon_errors.log' logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(log_file), logging.StreamHandler() # Also log to console ] ) logging.info("BatteryMon script started") def log_termination_reason(reason, exc_info=None): """Log the reason for script termination""" logging.error(f"Script terminating: {reason}") if exc_info: logging.error(f"Exception details: {traceback.format_exc()}") def signal_handler(signum, frame): """Handle system signals that might terminate the script""" signal_names = { signal.SIGTERM: "SIGTERM (termination request)", signal.SIGINT: "SIGINT (Ctrl+C)", signal.SIGKILL: "SIGKILL (force kill)", signal.SIGHUP: "SIGHUP (hangup)" } reason = signal_names.get(signum, f"Unknown signal {signum}") log_termination_reason(f"Received signal: {reason}") updateGraphFile() # Save data before exit sys.exit(0) def cleanup_on_exit(): """Cleanup function to run on normal exit""" log_termination_reason("Normal script exit") updateGraphFile() # Save data before exit def readArduino (Energy): global EnergyIn global EnergyOut global Energy2 # This is new energy value, Energy is the old value given via function call global Time0 # global Voltage global VoltageFactor global Current try: data = serialArduino.readline()[:-2] #the last bit gets rid of the new-line chars b = data koe = b.split(b" ")[0] if koe == b"Graph1": c = b.split(b" ")[1] e = b.split(b" ")[2] Voltage1 = int(c) #Proto1 Voltage = round(Voltage1*0.0189,2) #12V corresponds 2,99V in ADC and as 635 binary with Vref 4.5V #Voltage = round(Voltage1*0.0198,2) Voltage = round(Voltage1*VoltageFactor,2) Current1 = int(e) Current = round(IOffset+ICal*Current1*IdcFactor,3) #50 (proto1) or 20 (proto2) is the multiplier in Arduino # remove 1 bit noise if (Current1*Current1) == 1: Current = 0 Time = (datetime.datetime.now()-Time0).total_seconds() #print (Time, Current) Energy2 = round(Energy+Current*Time/3600,2) #Ensure that energy is not negative or above battery capacity if Energy2 < 0.05: Energy2 = 0.1 if Energy2 > (AhValue - 2): Energy2 = (AhValue - 2) Time0 = datetime.datetime.now() EnergyChange = round(Current*Time/3600,3) if EnergyChange > 0: EnergyIn = round(EnergyIn+EnergyChange,3) else: EnergyOut = round(EnergyOut-EnergyChange,3) # HOX low voltage power cut off removed 15.11.2022 #if Voltage <= 11.5: #sendToArduino("Load", "1") #State = "LowVolt" # print ("Idendified Arduino value:",koe,Voltage,Current,Energy2) timestamp = datetime.datetime.now().strftime('%Y:%d:%m-%H:%M:%S') updateStatusfile(Voltage,Current,Energy2,EnergyIn,EnergyOut, timestamp) updateGraph2(Voltage,Current,Energy2) updateLogfile(Voltage,Current,Energy2) return Energy2 except serial.SerialException as e: logging.error(f"Serial communication error: {e}") log_termination_reason(f"Serial error: {e}", exc_info=True) raise except ValueError as e: logging.error(f"Data parsing error: {e}, raw data: {data}") return Energy # Return previous energy value on parse error except Exception as e: logging.error(f"Unexpected error in readArduino: {e}") log_termination_reason(f"Unexpected error in readArduino: {e}", exc_info=True) raise def updateGraph2 (Voltage,Current,Energy): global values, values2, values3, x2 #startTime = time.time() try: if Voltage <= 10240000: if Voltage >= 0: # Step values values.append(Voltage) # Add new value in list, uses values.append method values.pop(0) # Remove first value from the list values2.append(Current) # Add new value in list, uses values.append method values2.pop(0) # Remove first value from the list values3.append(Energy) values3.pop(0) # Step time x2 = datetime.datetime.now() x.append(x2) x.pop(0) drawnow(OwnGraph1) pylab.savefig("/var/www/html/Graafi6.png", bbox_inches="tight") return Voltage, Current, Energy; else: print("Invalid! negative number") else: print("Invalid! too large") except ValueError: print (Voltage, Current, Energy, x2) print("Invalid! cannot cast") def sendToArduino( Label, Data ): #Add message header/terminator as defined in Arduino SW Task = "<" + Label + " " + Data + ">" print ("Sent command:",Task) serialArduino.write(Task.encode()) return; def initControlData (): global ControlData0 with open(Controlfile, 'r') as f: reader = csv.reader(f) ControlData0 = list(reader) lines = len(ControlData0) print ("Control data lines from file ",lines) for ind in range(0,lines): #print (ind,1) ControlData0[ind][1] = "xx" #Read data from CSV file cell, compare to previous and send delta to Arduino def readControlfile (): global ControlData0 global Energy2 global allowedMin # This reads the cells in csv file into an array with open(Controlfile, 'r') as f: reader = csv.reader(f) ControlData = list(reader) #compare firs full arrays, if delta compare line by line if ControlData0 != ControlData: C = len(ControlData0) #print ("Conrol data length",C) for ind in range(0, C): if ControlData0[ind] != ControlData[ind]: Label = ControlData[ind][0] Data = ControlData[ind][1] if Label == "Energy": Eluku = float(Data) if Eluku >= 2: Energy2 = Eluku #print ("komento", Label, Data) if Label == "AhValue": global AhValue AhValue = float(Data) # CURRENT MEASUREMENT CALIBRATION if Label == "IOffset": global IOffset IOffset = float(Data) if Label == "ICal": global ICal ICal = float(Data) # CHARGER AUTOSTART PARAMETERS if Label == "VStart": # voltage to start charger global VStart VStart = float(Data) if Label == "AhStart": # Ah to start charger global AhStart AhStart = float(Data) if Label == "VStop": # voltage to stop charger global VStop VStop = float(Data) if Label == "AhStop": # Ah to stop charger global AhStop AhStop = float(Data) if Label == "InvTrigger": global InvTrigger InvTrigger = float(Data) elif Label == "Run": if Data == "Stop": updateGraphFile() print("Stopped by Run stop command") sys.exit() elif Label == "VMin": t = float(Data) if t != allowedMin: #print('new t: {}'.format(t)) allowedMin = t #print("Print to Arduino due Control data change") sendToArduino(Label, Data) ControlData0 = ControlData return "NA" return "NA" def createLogfile (): fname = LogFile if os.path.isfile(fname): print("file does exist at this time") else: print("no such file exists at this time") #with open("Tiedosto.csv", "w", newline="") as f: with open(LogFile, "w") as f: writer = csv.writer(f) writer.writerow(['Volt','Curr','Ah']) writer.writerow([0,0,0,0]) writer.writerow([0,0,0,0]) def updateStatusfile (v,c,e,eIn,eOut, timestamp): """Update status file with atomic write to prevent corruption during concurrent access""" temp_file = StatusFile + ".tmp" try: # Write to temporary file first with open(temp_file, "w") as f: writer = csv.writer(f) writer.writerow(["Voltage/V",v]) writer.writerow(["Current/A",c]) writer.writerow(["Energy/Ah",e]) writer.writerow(["EnergyIn",eIn]) writer.writerow(["EnergyOut",eOut]) writer.writerow(["Timestamp",timestamp]) # Atomic rename to final filename import os if os.name == 'nt': # Windows if os.path.exists(StatusFile): os.remove(StatusFile) os.rename(temp_file, StatusFile) else: # Unix/Linux os.rename(temp_file, StatusFile) except Exception as e: logging.error(f"Error updating status file: {e}") # Clean up temp file if it exists try: if os.path.exists(temp_file): os.remove(temp_file) except: pass #This reads the last line -1 and picks 3rd column as oled energy value def readStatusfile (): global EnergyIn global EnergyOut global Energy2 EnergyOld = 0 try: with open(StatusFile, 'r') as f: reader = csv.reader(f) ControlData = list(reader) C = len(ControlData) for ind in range(0, C): if ControlData[ind][0] == "Energy/Ah": ener = ControlData[ind][1] EnergyOld = float(ener) Energy2 = float(ener) elif ControlData[ind][0] == "EnergyIn": ener = ControlData[ind][1] EnergyIn = float(ener) elif ControlData[ind][0] == "EnergyOut": ener = ControlData[ind][1] EnergyOut = float(ener) except FileNotFoundError: logging.warning("Status file not found, using default values") EnergyOld = 0 Energy2 = 0 EnergyIn = 0 EnergyOut = 0 except Exception as e: logging.error(f"Error reading status file: {e}") EnergyOld = 0 Energy2 = 0 EnergyIn = 0 EnergyOut = 0 return EnergyOld def updateLogfile (v,c,ah): with open(LogFile, "a") as f: writer = csv.writer(f) x2 = datetime.datetime.now().strftime('%d:%m-%H:%M:%S') writer.writerow([v,c,ah,x2]) def AutoStart (Energy): global Energy2 global Voltage global Current global State if State == "Idle": if Energy2 < AhStart or Voltage <= VStart : print("Start condition ",Energy2," ", AhStart," ",Voltage," ",VStart) startCharge() if State == "Charge": if Energy2 > AhStop or Voltage > VStop: print("Stop condition",Energy2," ", AhStart," ",Voltage," ",VStart) stopCharge() return (1) def startCharge (): global State global shortTimerOn State = "Charge" readControlfile() updateControlData0("Charge",1) updateControlData0("GraphT",5) updateControlfile() return def stopCharge (): global State State = "Idle" readControlfile() updateControlData0("Charge",0) updateControlData0("GraphT",5) updateControlfile() return def updateControlData0 (Labelx,Datax): global ControlData0 C = len(ControlData0) for ind in range(0, C): Label = ControlData0[ind][0] Data = ControlData0[ind][1] if Label == Labelx: ControlData0[ind][1] = Datax print ("New control data:", Labelx, Datax) #Hox 12.11.2022 #This function writes ControlData0 back into file.This can be used to change controls from Python! def updateControlfile (): global ControlData0 # File opened with w for adding data with open(Controlfile, "w") as f: writer = csv.writer(f) lines = len(ControlData0) for ind in range(0,lines): writer.writerow([ControlData0[ind][0],ControlData0[ind][1]]) print ("Controlfile updated") def updateGraphFile (): global values, values2, values3, x with open(GraphFile, "w") as f: writer = csv.writer(f) for ind in range(0,XLength): writer.writerow([x[ind],values[ind],values2[ind],values3[ind]]) def readGraphFile (): # File opened with w for adding data global values, values2, values3, x with open(GraphFile, "r") as f: reader = csv.reader(f) GraphData = list(reader) # Check that oldest time stamp is less than 12 hours and data is then relevant Time1 = datetime.datetime.strptime(GraphData[1][0], '%Y-%m-%d %H:%M:%S.%f') Timeperiod = ((datetime.datetime.now() - Time1)/3600).seconds if Timeperiod > 24: return for ind in range(0,XLength): time = GraphData[ind][0] x[ind] = datetime.datetime.strptime(time, '%Y-%m-%d %H:%M:%S.%f') values[ind] = float(GraphData[ind][1]) values2[ind] = float(GraphData[ind][2]) values3[ind] = float(GraphData[ind][3]) print ("Max value", min(values3)) # MAIN #************************************************************************************** if __name__ == '__main__': try: # Setup logging first setup_logging() # Register signal handlers signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) if hasattr(signal, 'SIGHUP'): # SIGHUP not available on Windows signal.signal(signal.SIGHUP, signal_handler) # Register cleanup function atexit.register(cleanup_on_exit) logging.info("Initializing BatteryMon script...") # INTERFACES #************************************************************************************** # Define and open serial port, baud must be same as in Arduino #port = "/dev/ttyUSB0" #vanhalla Arduino Unolla #port = "COM10" # Arduino Nano port = "/dev/ttyUSB0" # Arduino Nano #port = "/dev/ttyACM0" # Arduino Every baud = 9600 try: serialArduino = serial.Serial(port, baud, timeout=1) logging.info(f"Serial connection established on {port}") except serial.SerialException as e: log_termination_reason(f"Failed to open serial port {port}: {e}") sys.exit(1) time.sleep(1) #Controlfile = '/var/www/html/Control.csv' #Controlfile2 = '/var/www/html/Control2.csv' #StatusFile = '/var/www/html/Status.csv' #LogFile = '/var/www/html/Tiedosto.csv' #GraphFile = '/var/www/html/GraphFile.csv' # TABLE AND ARRAYS DEFINITIONS #************************************************************************************** # Define table to be used for graph values = [] values2 = [] values3 = [] x = [] plt.ion() XLength=600 #pre-load dummy data into table for i in range(0,XLength): # original value was 26 values.append(0) # Add new value in list, uses values.append method values2.append(0) values3.append(0) x = [datetime.datetime.now() + datetime.timedelta(seconds=i-XLength) for i in range(0,XLength)] #updateGraphFile() readGraphFile() OldCommand="" createLogfile() #clear serial buffer to remove junk and noise serialArduino.flushInput() serialArduino.flushOutput() initControlData() #initialise array containing Command data from PHP # VARIABLE DEFINITIONS #************************************************************************************** Current = 0.0 Voltage = 12 Energy2 = 0.0 Energy = 0.0 EnergyIn = 0.0 EnergyOut = 0.0 Time0 = datetime.datetime.now() # Time when new set of samples from Arduino start AhValue = 100 allowedMin = 10 IOffset = 0 ICal = 0 State = "Idle" shortTimerOn = 0 sec5Timer = 60 graphTimer = 600 readStatusfile() readControlfile() updateControlData0("GraphT",5) updateControlfile() print ("TGraph A = ",ControlData0[0][1]) startTime = time.time() graphTime = time.time() logging.info("Starting main monitoring loop...") while True: try: readControlfile() #startTime = time.time() readArduino(Energy2) #reads Arduino and updates Graphs #print("Loop took {0}".format(time.clock()-startTime)) if float(ControlData0[17][1]) == 1: # Autostart activated AutoStart(200) # Counter to remove 5 sec interval setting after sec5Timer if shortTimerOn == 0: if float(ControlData0[0][1]) == 5: shortTimerOn = 1 startTime = time.time() if shortTimerOn == 1: timeSec = (time.time() - startTime) if (timeSec > sec5Timer): shortTimerOn = 0 updateControlData0("GraphT",120) updateControlfile() sendToArduino("GraphT","120") timeSec = (time.time() - graphTime) if (timeSec > graphTimer): updateGraphFile() print ("Graph data stored") graphTime = time.time() except KeyboardInterrupt: log_termination_reason("User interrupted with Ctrl+C") break except serial.SerialException as e: log_termination_reason(f"Serial communication lost: {e}") break except FileNotFoundError as e: log_termination_reason(f"Required file not found: {e}") break except PermissionError as e: log_termination_reason(f"Permission denied: {e}") break except Exception as e: logging.error(f"Unexpected error in main loop: {e}") logging.error(f"Traceback: {traceback.format_exc()}") # Don't break on unexpected errors, just log and continue time.sleep(1) # Brief pause before retrying endTime = time.time() logging.info(f"Loop took {endTime-startTime}") print("Loop took {0}".format(endTime-startTime)) except Exception as e: log_termination_reason(f"Fatal error during initialization: {e}", exc_info=True) sys.exit(1)