Files
Mapo-IOB-WIN/IOB-WIN-PSER/IOB-WIN-PSER.py
T
Samuele Locatelli 5524850eac update IOB-PSER:
- fix condizione invio immediata "secondo dato" x calcolo delta errato
- fix log eccezioni
2024-06-13 12:57:53 +02:00

706 lines
25 KiB
Python

# -*- coding: utf-8 -*-
# IOB-WIN-PSER v. 2.8.2
# - porting versione interpretata su Python2 + XP a Python3
# - aggiunta classi Redis
# - compilato con pyInterpreter
# - single instance timer
# - invio multiplo x send eventi AccodaEVti
# - attenzione .. non c'e' la possibilita di definire bit blinking, inversione ecc.
# - gestione wait se troppi errori in msglen...
# - 2.8 correzione gestione loghandler x logrotate e scrittura doppia (log + console separate)
# - 2.8.2 fix gestione eccezioni e log + fix gestione timeout short (SAMPLETIME e non delta x calcolo scadenza)
#---------------------------------------------------------------
import configparser
import json
import logging
import logging.handlers
import os, sys
import queue
import redis
import serial
import sys
import threading
import time
import urllib
from datetime import datetime
from array import *
from logging.handlers import RotatingFileHandler
from urllib.request import urlopen
#--------------------------------------------------------------
# Setup COSTANTI e variabili globali
#--------------------------------------------------------------
# Conf Generale
PROGRAM_NAME= 'IOB-WIN-PythonSERial v.2.8.2'
# IOB_NAME='IOB'
# IOB_CONF='IOB.cfg'
IOB_NAME='L001'
IOB_CONF='L001.cfg'
# gestione folders
BASE_PATH = 'c:\\Steamware'
BASE_LOG_PATH = 'c:\\Steamware\\logs'
# gestione REDIS
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_DB = 0
# Logging
MAY_LOG_FILE = 30
LOGFILE = 'logfile.txt'
LOGLEVEL = 1
MAX_LOG_SIZE = 1024 * 10
NUM_LOG_RETAIN = 5
# parametri per comunicazione Seriale
MSGLEN = 5
TIMEOUTSERIALE = 10
MAXRETRY = 3
# numero campioni filtraggio segnale ballerino
MAX_COUNTER_BLINK = 10
# DA FILE CONF
idxMacchina = 'L003'
WAIT_RECONN = 1
SAMPLETIME = 0.1
TIMEOUTSHORT = (SAMPLETIME*20)
TIMEOUTLONG = (SAMPLETIME*600)
SENDURLTIME = 0.08
NMAXSEND = 5 # numero massimo di invii per singolo ciclo di svuotamento
#--------------------------------------------------------------
#--------------------------------------------------------------
# Setup VARIABILI globali
#--------------------------------------------------------------
# appoggio
to_enable = False
to_short = TIMEOUTSHORT
to_long = TIMEOUTLONG
errormsglen = 0
# timer x conteggio tempi esecuzione
last_exe = time.time()
# contatore: serve x match tra AccodaEV ed invia x possibile controllo a posteriori... ogni volta che accodo incremento di 1, va da 0 a 9999
cont = '0'
# variabile stato online/offline della macchina
onLine = '1'
# variabile stato seinding/waiting x la parte invio URL
sending = '0'
# variabile stato timer thread busy
timer_busy = False
# Setup array per ingressi filtrati
i_counters = array ('i',[0,0,0,0,0,0,0,0])
B_blinking = array ('B',[0,0,0,0,0,0,0,0])
B_previous = array ('B',[0,0,0,0,0,0,0,0])
B_input = array ('B',[0,0,0,0,0,0,0,0])
B_output = array ('B',[0,0,0,0,0,0,0,0])
B_inverting = array ('B',[0,0,0,0,0,0,0,0])
B_filter = array ('B',[0,0,0,0,0,0,0,0])
B_filter_prev = array ('B',[0,0,0,0,0,0,0,0])
B_temp = array ('B',[0,0,0,0,0,0,0,0])
i_filter_counters = array ('i',[0,0,0,0,0,0,0,0])
# Gestione coda (condivisa) x registrazione eventi ed invio URL
Coda = queue.Queue(0)
#--------------------------------------------------------------
#--------------------------------------------------------------
# Classe helper x pubblicazione stato vero IOB-MAN
#--------------------------------------------------------------
class IobWinStatus:
def toJSON(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
#stream = 'cse' # Class Variable
def __init__(self,codIob,iobType):
self.CodIob = codIob
self.IobType = iobType
self.counterIOB = 0
self.counterMAC = 0
self.freeNotes = ''
self.lastDataIn = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.lastDataOut = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.lastUpdate = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.online = False
self.queueAlLen = 0
self.queueEvLen = 0
self.queueFlLen = 0
self.queueMsLen = 0
self.queueRawTransfLen = 0
self.queueUlLen = 0
self.semIn = 'ND'
self.semOut = 'ND'
self.currParams = {}
self.setupParams = {}
#--------------------------------------------------------------
#---------------------------------------------------------------
# Funzione helper x timer thread
#---------------------------------------------------------------
def do_every (interval, worker_func, iterations = 0):
if iterations != 1:
threading.Timer (
interval,
do_every, [interval, worker_func, 0 if iterations == 0 else iterations-1]
).start ();
worker_func ();
#---------------------------------------------------------------
#--------------------------------------------------------------
# Seriale: chiusura porta + uscita da programma
#--------------------------------------------------------------
def ChiudiSerialeEsci():
global ser
if ser.isOpen():
ser.close()
#--------------------------------------------------------------
#--------------------------------------------------------------
# Seriale: Gestione Comunicazione lettura
# - lettura buffer seriale e pulizia caratteri non stampabili
# - ritorna '00' se non c'è un messaggio buono o il messaggio pulito (due bytes hex)
# - il messaggio ha il formato xx<ACK>i00 00<CR><LF>xxx CON IL COMANDO $i
# - il messaggio ha il formato xx<ACK>I00<CR><LF>xxx CON IL COMANDO $I
#--------------------------------------------------------------
def ReadSeriale():
# global to_serial
# global to_retry
# global errormsglen
global ser
global redIobMan
current = '00'
# 0x3F se volessimo tagliare ultimi 2 bit...
# 0xFF se se leggiamo tutti gli 8 bit...
sigMask = 0x3F
try:
# se è connesso...
if (ser.is_open):
sRaw = ser.read(12).decode()
if len(sRaw)> 4:
lAns = sRaw.split('\r')
rawVal = lAns[1]
adesso = datetime.now()
redIobMan.lastDataIn = adesso.strftime("%Y-%m-%d %H:%M:%S")
current = rawVal[0:2]
# current = "{0:0>2X}".format(int (current, 16) & 0X3F)
current = "{0:0>2X}".format(int (current, 16) & sigMask)
# richiesta dati ad IOB
RequestData()
# n_trials = 0
# errormsglen = 0
# b_ok = True # esco dal loop cd
else:
logPro.error('Serial closed, no data')
time.sleep(WAIT_RECONN)
RiAvviaSeriale()
time.sleep(WAIT_RECONN)
RequestData()
except Exception as exc:
logPro.error (f' err. 18 - Serial Port error... --EXIT -- ---> {str(exc)}')
RiAvviaSeriale()
# ChiudiSerialeEsci()
return current
#--------------------------------------------------------------
#--------------------------------------------------------------
# Richiesta dati ad IOB : scrittura su seriale
#--------------------------------------------------------------
def RequestData ():
global ser
global redIobMan
cmdCall = '$i\r\n'
serCall = cmdCall.encode()
try :
#$I per vers 1, $i per vers 2 scheda, prima con 6 seconda con 9 char risposta
ser.reset_input_buffer()
ser.write(serCall)
adesso = datetime.now()
redIobMan.lastDataOut = adesso.strftime("%Y-%m-%d %H:%M:%S")
time.sleep(SAMPLETIME)
except Exception as exc:
logPro.error (f'SERIAL: err. 19 - Write / flush \n\n{str(exc)}')
#--------------------------------------------------------------
#--------------------------------------------------------------
# Funzione di accodamento evento (valore corrente) con try-except
#--------------------------------------------------------------
def AccodaEV():
try:
dtEve = datetime.utcnow().strftime('%Y%m%d%H%M%S%f')[:-3]
Coda.put(dtEve + '#' + value + '#' + cont)
except queue.Full:
logPro.error(f'Queue full | {dtEve} #{value}#{cont}')
except Exception as exc:
logPro.error(f'NETWORK:Errore http-no com rete-timeout | {url}\n\n{str(exc)}')
#--------------------------------------------------------------
#--------------------------------------------------------------
# Svuotamento coda eventi x invio dati al server
#--------------------------------------------------------------
def SvuotaCodaEV():
global onLine
global sending
global timer_busy
global NMAXSEND
#print('start timer')
if (timer_busy == False):
timer_busy = True
#print('start timer OK')
try:
if not Coda.empty():
#print('coda da svuotare!')
response = urlopen(URLALIVE)
answ = response.read().decode()
if answ == 'OK':
#print('OK alive')
response2 = urlopen(URLENABLED + idxMacchina)
answ2 = response2.read().decode()
if answ2 == 'OK':
# aggiorno stato ad online
if onLine == '0':
logPro.info('IOB ONLINE!')
#print('IOB ONLINE')
onLine = '1' # imposto comunque online
else:
if onLine == '1':
logPro.error('IOB offline')
#print('IOB offline')
onLine = '0'
else:
if onLine == '1':
logPro.error('Server offline')
#print('Server offline')
onLine = '0'
# ora verifico SE si possa inviare (ovvero sia online server e NON ci siano altri send attivi...)
if onLine == '1':
if sending == '0':
#segnalo che sono in sending!
sending = '1'
# SAM 2016.12.23: modifica x invio FINO A nMaxSend ELEMENTI ad ogni ciclo di svuotamento
i = NMAXSEND
while i >= 0:
if not Coda.empty():
# formatto dataOra corrente
dtCurr = datetime.utcnow().strftime('%Y%m%d%H%M%S%f')[:-3]
#prendo primo elemento dalla coda
resp = Coda.get()
# recupero valori da elemento coda!
dtEve = resp.split('#')[0]
value = resp.split('#')[1]
cnt = resp.split('#')[2]
url = URLBASE + idxMacchina + URLADV1 + value
url = url + '&dtCurr=' + dtCurr + '&dtEve=' + dtEve + '&cnt=' + cnt
# CHIAMO URL
response3 = urlopen (url)
answ3 = response3.read().decode()
print(url)
# log valore inviato!
logSnd.info(value + ' ['+ cnt +']' + ' R:' + answ3)
#print('Valore smaltito dalla coda')
# tolgo 1 al contatore
i -= 1
# completato invio, riporto sending a zero!
sending = '0'
else:
logPro.info('WAIT active send to complete')
else:
pass
else:
pass
except Exception as exc:
if onLine == '1':
logPro.error(f'Server Non raggiungibile | Eccezione in invio dati:{str(exc)}')
#print('Non raggiungibile')'
else:
logPro.err(f'Eccezione in invio dati:{str(exc)}')
onLine = '0'
# in ogni caso
timer_busy = False
#print('end timer ok')
#print('end timer')'
#---------------------------------------------------------------
#---------------------------------------------------------------
# Gestione contatore round robin
#---------------------------------------------------------------
def CounterDoIncr():
try:
global cont
ctr = int(cont)
ctr +=1
ctr = ctr % 10000 # round robin 10000 eventi x track
cont = str(ctr)
except Exception as exc:
logPro.error('errore incremento contatore')
logPro.error(str(exc))
#---------------------------------------------------------------
#---------------------------------------------------------------
# Gestione avvio porta seriale
#---------------------------------------------------------------
def AvviaSeriale():
global ser
try:
ser = serial.Serial(
port = comm_port,
baudrate = 9600,
parity = serial.PARITY_NONE,
stopbits = serial.STOPBITS_ONE,
bytesize = serial.EIGHTBITS,
timeout = 1
)
except serial.serialutil.SerialException as exc :
sys.stdout.write (f'\n{PROGRAM_NAME} | err 11 - opening serial\n\n{str(exc)}\n\n')
RiAvviaSeriale()
# ChiudiSerialeEsci()
logPro.info(f'\n\n{PROGRAM_NAME} - init ok \n\n')
#---------------------------------------------------------------
#---------------------------------------------------------------
# Ri-avvia porta seriale
#---------------------------------------------------------------
def RiAvviaSeriale():
global ser
try:
if ser.isOpen():
ser.close()
time.sleep(WAIT_RECONN)
ser = serial.Serial(
port = comm_port,
baudrate = 9600,
parity = serial.PARITY_NONE,
stopbits = serial.STOPBITS_ONE,
bytesize = serial.EIGHTBITS,
timeout = 1
)
except serial.serialutil.SerialException as exc :
sys.stdout.write (f'\n{PROGRAM_NAME} | err 12 - reopening serial\n\n{str(exc)}\n\n')
time.sleep(WAIT_RECONN)
RiAvviaSeriale()
#---------------------------------------------------------------
#---------------------------------------------------------------
# funzione WatchDog verso REDIS
#---------------------------------------------------------------
def SendWatchDog():
# invio su redis stato
adesso = datetime.now()
redIobMan.lastUpdate = adesso.strftime("%Y-%m-%d %H:%M:%S")
# verificare sia DAVVERO online verso server...
redIobMan.online = (onLine == '1')
rawVal = redIobMan.toJSON()
redSrv0.set(f'IOB-WIN-PSER:IOB:{IOB_NAME}', rawVal)
logPro.debug('Invio a REDIS WatchDog')
#---------------------------------------------------------------
#----------------------------------------------------------------------
# Setup
#----------------------------------------------------------------------
try:
print('----------------------------')
print('Setup')
print('----------------------------')
# inizio con parse dei parametri... se li ho sono in esecuzione interattiva, imposto BASE_PATH di conseguenza
if len(sys.argv) > 1:
IOB_NAME = sys.argv[1]
# calcolo PATH esecuzione x versione COMPILATA
exe = sys.executable
BASE_PATH = os.path.dirname(exe) + '\\'
else:
# calcolo PATH esecuzione x versione INTERPRETATA
BASE_PATH = os.path.dirname(__file__) + '\\'
print(f'Iob name: {IOB_NAME}')
# fix config
IOB_CONF = f'{IOB_NAME}.cfg'
print(f'BasePath: {BASE_PATH}')
BASE_LOG_PATH = BASE_PATH + 'logs\\'
print(f'LogPath: {BASE_LOG_PATH}')
config = configparser.ConfigParser()
confFilePath = os.path.join(BASE_PATH, 'CONF', IOB_CONF)
config.read(confFilePath)
# lettura parametri di base
LOGFILE = config.get('log','LOGFILE')
LOGLEVEL = config.getint('log','LOGLEVEL')
MAX_LOG_SIZE = config.getint('log','MAX_LOG_SIZE')
NUM_LOG_RETAIN = config.getint('log','NUM_LOG_RETAIN')
REDIS_HOST = config.get('redis','REDIS_HOST')
REDIS_PORT = config.getint('redis','REDIS_PORT')
REDIS_DB = config.getint('redis','REDIS_DB')
# fix cartella log...
BASE_LOG_PATH = os.path.join(BASE_PATH, 'logs', IOB_NAME)
logPath = os.path.join(BASE_LOG_PATH, LOGFILE)
logDirOk=os.path.exists(logPath)
# controllo cartella log o la creo
if(not logDirOk):
os.makedirs(os.path.dirname(logPath), exist_ok=True)
print(f'Config file: {confFilePath}')
# setup redis...
redSrv0 = redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB)
print(f'REDIS_HOST: {REDIS_HOST}')
print(f'REDIS_PORT: {REDIS_PORT}')
print(f'REDIS_DB: {REDIS_DB}')
# setup oggetto comunicazione redis...
redIobMan = IobWinStatus(IOB_NAME,'IOB-WIN-PSER')
# continuo setup altri parametri specifici IOB
SAMPLETIME = config.getfloat ('time','SAMPLETIME')
TIMEOUTSHORT = config.getfloat ('time','TIMEOUTSHORT')
TIMEOUTLONG = config.getfloat ('time','TIMEOUTLONG')
SENDURLTIME = config.getfloat ('time','SENDURLTIME')
WAIT_RECONN = config.getint ('time','WAIT_RECONN')
NMAXSEND = config.getint ('time','NMAXSEND')
idxMacchina = config.get ('id','idxMacchina')
comm_port = config.get ('comm','port')
URLBASE = config.get ('web','URLBASE')
URLENABLED = config.get('web', 'URLENABLED')
URLALIVE = config.get ('web', 'URLALIVE')
URLADV1 = config.get ('web','URLADV1')
# setup parametri speciali (blink)
B_blinking[0] = config.getint ('blink','bit0')
B_blinking[1] = config.getint ('blink','bit1')
B_blinking[2] = config.getint ('blink','bit2')
B_blinking[3] = config.getint ('blink','bit3')
B_blinking[4] = config.getint ('blink','bit4')
B_blinking[5] = config.getint ('blink','bit5')
B_blinking[6] = config.getint ('blink','bit6')
B_blinking[7] = config.getint ('blink','bit7')
MAX_COUNTER_BLINK = config.getint ('blink','MAX_COUNTER_BLINK')
# setup gestione inversione segnali ingresso
B_inverting[0] = config.getint ('invert','bit0')
B_inverting[1] = config.getint ('invert','bit1')
B_inverting[2] = config.getint ('invert','bit2')
B_inverting[3] = config.getint ('invert','bit3')
B_inverting[4] = config.getint ('invert','bit4')
B_inverting[5] = config.getint ('invert','bit5')
B_inverting[6] = config.getint ('invert','bit6')
B_inverting[7] = config.getint ('invert','bit7')
# setup gestione filtraggio segnali brevi (spike)
B_filter[0] = config.getint ('filter','bit0')
B_filter[1] = config.getint ('filter','bit1')
B_filter[2] = config.getint ('filter','bit2')
B_filter[3] = config.getint ('filter','bit3')
B_filter[4] = config.getint ('filter','bit4')
B_filter[5] = config.getint ('filter','bit5')
B_filter[6] = config.getint ('filter','bit6')
B_filter[7] = config.getint ('filter','bit7')
MAX_COUNTER_FILTER = config.getint ('filter','MAX_COUNTER_FILTER')
except Exception as exc :
print (f'{PROGRAM_NAME} - Exception - Error 4 in config file \n\n{str(exc)}\n-- EXIT --')
#sys.exit(1)
#--------------------------------------------
#--------------------------------------------
# Oggetto Logger
#--------------------------------------------
try:
logPath = os.path.join(BASE_LOG_PATH, LOGFILE)
cLogLevel = logging.DEBUG
if LOGLEVEL > 10:
cLogLevel = logging.WARN
elif LOGLEVEL > 5:
cLogLevel = logging.INFO
logging.basicConfig(level=cLogLevel)
# add a rotating handler
handler = RotatingFileHandler(logPath,
maxBytes=MAX_LOG_SIZE,
backupCount=NUM_LOG_RETAIN
)
# Define a custom formatter
formatter = logging.Formatter('%(asctime)s %(name)-8s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
# aggiungo logger specifici (program,queue,send...)
logQue = logging.getLogger('queue')
logSnd = logging.getLogger('sendUrl')
logPro = logging.getLogger('program')
logQue.addHandler(handler)
logSnd.addHandler(handler)
logPro.addHandler(handler)
print('Setup completed')
print('----------------------------')
except Exception as exc:
# manda mail o simili - FARE!!!
print (f'LOG: Impossibile creare file log con nome {LOGFILE}')
#--------------------------------------------
#------------------------------------------------------------------------------------------------------------------------------
# MAIN
#------------------------------------------------------------------------------------------------------------------------------
print(f'\n\n {PROGRAM_NAME} \n\n')
logPro.info("Start " + PROGRAM_NAME)
logPro.info(' Iob name = %s' % (IOB_NAME))
logPro.info(' BasePath = %s' % (BASE_PATH))
logPro.info(' LogPath = %s' % (BASE_LOG_PATH))
logPro.info(' comm_port = %s' % (comm_port))
logPro.info(' idxMacchina = %s' % (idxMacchina))
logPro.info(' SAMPLETIME = %4.2f' % (SAMPLETIME))
logPro.info(' TIMEOUTSHORT = %4.2f' % (TIMEOUTSHORT))
logPro.info(' TIMEOUTLONG = %4.2f' % (TIMEOUTLONG))
logPro.info(' SENDURLTIME = %4.2f' % (SENDURLTIME))
logPro.info(' URLBASE = %s' % (URLBASE))
logPro.info(' URLADV1 = %s' % (URLADV1))
logPro.info(' LOGFILE = %s' % (LOGFILE))
logPro.info(' LOGLEVEL = %s' % (LOGLEVEL))
to_short = TIMEOUTSHORT
to_long = TIMEOUTLONG
#--------------------------------------------------------------
# apertura seriale come prima cosa
AvviaSeriale()
#--------------------------------------------------------------
#--------------------------------------------------------------
# Qui c'è avvio thread secondari: "svuotaCoda" e check REDIS x invio watchdog
# svuota coda è molto frequente, tipicamente 0.15 sec
do_every (SENDURLTIME, SvuotaCodaEV)
# watchdog redis lo è molto meno, tipicamente ogni 6 sec
do_every (SENDURLTIME * 40, SendWatchDog )
#--------------------------------------------------------------
#---------------------------------------------------------------
# inizio con richiesta dati ad IOB
old = ''
try:
RequestData()
except Exception as exc:
logPro.error('err 20 - try RequestData -- EXIT --')
logPro.error(str(exc))
RiAvviaSeriale()
# ChiudiSerialeEsci()
#print('Avvio ciclo"
logPro.info('Avvio loop principale')
# ciclo forever and ever
while True :
try:
time.sleep (SAMPLETIME)
except Exception as e:
logPro.error('Err 21 -First_SLEEP: errore attesa sampletime')
logPro.error(str(e))
# calcolo tempo trascorso
adesso = time.time()
delta = adesso - last_exe
last_exe = adesso
value = ''
# lettura dati da IOB
try:
value = ReadSeriale()
except Exception as e:
logPro.error('err 22 - main loop read serial -- EXIT --')
logPro.error(str(e))
ChiudiSerialeEsci()
if (value != '') :
if value != old :
#loggo e invio dati
try:
logQue.info(value + ' ['+ cont +']')
AccodaEV()
CounterDoIncr()
except Exception as e:
logPro.error('err 23 - URLBROWSER: errore registrazione valore e AccodaEV')
logPro.error(str(e))
pass
#enable e reset timer
to_enable = True
to_short = TIMEOUTSHORT
to_long = TIMEOUTLONG
old = value
# gestione timeout breve
if (to_enable) :
# to_short = to_short - delta
to_short = to_short - SAMPLETIME
if to_short <= 0:
#loggo e invio dati
try:
logQue.info('>' + value + ' ['+ cont +']')
AccodaEV()
CounterDoIncr()
except Exception as exc:
logPro.error('err 24 - URLBROWSER: errore registrazione valore e AccodaEV TO_short')
logPro.error(str(exc))
pass
to_short = TIMEOUTSHORT
to_enable = False # dopo un colpo il timer breve viene disabilitato
to_long = TIMEOUTLONG
# gestione timeout lungo
# to_long = to_long - delta
to_long = to_long - SAMPLETIME
if to_long <= 0:
#loggo e invio dati
try:
logQue.info('>>' + value + ' ['+ cont +']')
AccodaEV()
CounterDoIncr()
except Exception as exc:
logPro.error('err 25 - URLBROWSER: errore registrazione valore e AccodaEV TO_long')
logPro.error(str(exc))
pass
to_long = TIMEOUTLONG
#------------------------------------------------------------------------------------------------------------------------------