342 lines
13 KiB
Python
342 lines
13 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# readParallela v. 2.6.2 12 Ingressi
|
|
# - single instance timer
|
|
# - invio multiplo x send eventi accodati
|
|
# - gestione segnali BLINKING
|
|
# - gestione INVERSIONE segnali cv 10-VII-2018
|
|
# - gestione FILTRAGGIO segnali brevi cv 23-VII-2018
|
|
# - (2.3) gestione 12 bit cv 14-I-2020
|
|
# - (2.4) fix ingressi e conf apertura parallela + gestione vari bit filtraggio x nuovi ingressi + update conf con 12 parametri bit SEL 15-I-2020
|
|
# - (2.4.8) versione adatta a raspberry PI vecchia generazione (GPIO corto, 8bit)
|
|
# - (2.5) Fix (hope) ciclo "wait send to complete", gestione timeout (rety infinito se IO riparte in modo anomalo)
|
|
# - (2.5.1) Fix numero versione 18.05.2023
|
|
# - (2.5.2) Fix gestione eccezioni con report dettagliato
|
|
# - (2.5.3) Fix gestione stringhe e print x python 3.11 in debian 12 / raspberry OS 2025
|
|
# - (2.6.0) Aggiunto gestione Redis x code salvate ogni minuto e ricaricate all'avvio 2025.04.17
|
|
# - (2.6.1) Cleanup generale vecchia queue post test vari
|
|
# - (2.6.2) Fix in global di to_retry in send_coda per evitare problemi
|
|
|
|
|
|
import time
|
|
import sys
|
|
import os
|
|
import logging
|
|
import logging.handlers
|
|
import threading
|
|
import configparser
|
|
from datetime import datetime
|
|
from array import array
|
|
|
|
import redis
|
|
import requests
|
|
|
|
# Note: RPi.GPIO is imported inside the class or at runtime to prevent errors on non-Pi systems
|
|
try:
|
|
import RPi.GPIO as GPIO
|
|
except ImportError:
|
|
GPIO = None
|
|
|
|
class ReadParallelaIOB:
|
|
def __init__(self, config_path='IOB.cfg'):
|
|
self.PROGRAM_NAME = "ReadPar IOB-pi v.3.0.0 (Refactored)"
|
|
self.MAXRETRY = 10
|
|
self.MAX_COUNTER_BLINK = 10
|
|
|
|
# Configuration and State
|
|
self.config = configparser
|
|
self.load_config(config_path)
|
|
|
|
# Hardware Pins (12 inputs)
|
|
self.input_pins = [11, 12, 13, 15, 16, 18, 22, 7, 29, 31, 32, 36]
|
|
|
|
# Internal State Arrays
|
|
self.i_counters = array('i', [0] * 12)
|
|
self.B_blinking = array('B', [0] * 12)
|
|
self.B_previous = array('B', [0] * 12)
|
|
self.B_input = array('B', [0] * 12)
|
|
self.B_output = array('B', [0] * 12)
|
|
self.B_inverting = array('B', [0] * 12)
|
|
self.B_filter = array('B', [0] * 12)
|
|
self.B_filter_prev = array('B', [0] * 12)
|
|
self.B_temp = array('B', [0] * 12)
|
|
self.i_filter_counters = array('i', [0] * 12)
|
|
|
|
# Control Variables
|
|
self.cont = 0
|
|
self.onLine = '1'
|
|
self.sending = False
|
|
self.timer_busy = False
|
|
self.to_enable = False
|
|
self.to_short = self.TIMEOUTSHORT
|
|
self.to_long = self.TIMEOUTLONG
|
|
self.to_retry = self.MAXRETRY
|
|
|
|
# Redis
|
|
self.CodaR = redis.Redis(host='localhost', port=6379, db=0, password='24068Seriate')
|
|
self.queue_name = 'IOB'
|
|
|
|
# Logging
|
|
self.setup_logging()
|
|
|
|
def load_config(self, path):
|
|
config = configparser.RawConfigParser()
|
|
if not os.path.exists(path):
|
|
print(f"Error: Config file {path} not found.")
|
|
sys.exit(1)
|
|
config.read(path)
|
|
|
|
self.idxMacchina = config.get('id', 'idxMacchina')
|
|
self.SAMPLETIME = config.getfloat('time', 'SAMPLETIME')
|
|
self.TIMEOUTSHORT = config.getfloat('time', 'TIMEOUTSHORT')
|
|
self.TIMEOUTLONG = config.getfloat('time', 'TIMEOUTLONG')
|
|
self.SENDURLTIME = config.getfloat('time', 'SENDURLTIME')
|
|
self.NMAXSEND = config.getint('time', 'NMAXSEND')
|
|
|
|
self.URLBASE = config.get('web', 'URLBASE')
|
|
self.URLENABLED = config.get('web', 'URLENABLED')
|
|
self.URLALIVE = config.get('web', 'URLALIVE')
|
|
self.URLADV1 = config.get('web', 'URLADV1')
|
|
|
|
self.LOGFILE = config.get('log', 'LOGFILE')
|
|
self.LOGLEVEL = config.get('log', 'LOGLEVEL')
|
|
|
|
# Load bit settings using loops instead of manual lines
|
|
for i in range(12):
|
|
self.B_blinking[i] = config.getint('blink', f'bit{i}')
|
|
self.MAX_COUNTER_BLINK = config.getint('blink', 'MAX_COUNTER_BLINK')
|
|
|
|
for i in range(12):
|
|
self.B_inverting[i] = config.getint('invert', f'bit{i}')
|
|
self.B_filter[i] = config.getint('filter', f'bit{i}')
|
|
self.MAX_COUNTER_FILTER = config.getint('filter', 'MAX_COUNTER_FILTER')
|
|
|
|
def setup_logging(self):
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format='%(asctime)s %(name)-8s %(levelname)-8s %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S',
|
|
filename=self.LOGFILE,
|
|
filemode='a'
|
|
)
|
|
self.logQue = logging.getLogger('queue')
|
|
self.logSnd = logging.getLogger('sendUrl')
|
|
self.logPro = logging.getLogger('program')
|
|
|
|
def setup_gpio(self):
|
|
if GPIO is None:
|
|
self.logPro.error("GPIO library not found. Are you on a Raspberry Pi?")
|
|
sys.exit(1)
|
|
try:
|
|
GPIO.setmode(GPIO.BOARD)
|
|
GPIO.setwarnings(False)
|
|
for pin in self.input_pins:
|
|
GPIO.setup(pin, GPIO.IN)
|
|
self.logPro.info("GPIO initialized successfully.")
|
|
except Exception as e:
|
|
self.logPro.error(f"GPIO Setup Error: {e}")
|
|
sys.exit(1)
|
|
|
|
def rqEnqueue(self, item):
|
|
self.CodaR.rpush(self.queue_name, item)
|
|
|
|
def rqDequeue(self):
|
|
item = self.CodaR.lpop(self.queue_name)
|
|
return item.decode('utf-8') if item else None
|
|
|
|
def rqLen(self):
|
|
return self.CodaR.llen(self.queue_name)
|
|
|
|
def readParallelaFiltrata(self):
|
|
try:
|
|
# 1. Efficient GPIO Read
|
|
for i, pin in enumerate(self.input_pins):
|
|
# Read and invert logic immediately if required
|
|
raw_val = 0 if GPIO.input(pin) else 1
|
|
if self.B_inverting[i] == 1:
|
|
raw_val = 1 - raw_val
|
|
self.B_input[i] = raw_val
|
|
|
|
# 2. Processing Loop (Filtering & Blinking)
|
|
for i in range(12):
|
|
# Debounce / Filter logic
|
|
if self.B_filter[i] == 1:
|
|
if self.B_input[i] == 1 and self.B_filter_prev[i] == 0:
|
|
if self.i_filter_counters[i] == 0:
|
|
self.i_filter_counters[i] = self.MAX_COUNTER_FILTER
|
|
self.B_temp[i] = 0
|
|
else:
|
|
self.i_filter_counters[i] = 0
|
|
self.B_temp[i] = 1
|
|
elif self.B_input[i] == 1 and self.B_filter_prev[i] == 1:
|
|
if self.i_filter_counters[i] > 0:
|
|
self.i_filter_counters[i] -= 1
|
|
self.B_temp[i] = 0
|
|
else:
|
|
self.B_temp[i] = 1
|
|
elif self.B_input[i] == 0 and self.B_filter_prev[i] == 1:
|
|
if self.i_filter_counters[i] == 0:
|
|
self.i_filter_counters[i] = self.MAX_COUNTER_FILTER
|
|
self.B_temp[i] = 1
|
|
else:
|
|
self.i_filter_counters[i] = 0
|
|
self.B_temp[i] = 0
|
|
elif self.B_input[i] == 0 and self.B_filter_prev[i] == 0:
|
|
if self.i_filter_counters[i] > 0:
|
|
self.i_filter_counters[i] -= 1
|
|
self.B_temp[i] = 1
|
|
else:
|
|
self.B_temp[i] = 0
|
|
|
|
self.B_filter_prev[i] = self.B_input[i]
|
|
self.B_input[i] = self.B_temp[i]
|
|
|
|
# Blinking Logic
|
|
if self.B_blinking[i] == 0:
|
|
self.B_output[i] = self.B_input[i]
|
|
else:
|
|
if self.B_previous[i] != self.B_input[i]:
|
|
self.B_previous[i] = self.B_input[i]
|
|
if self.B_input[i] == 1:
|
|
self.B_output[i] = 1
|
|
self.i_counters[i] = self.MAX_COUNTER_BLINK
|
|
else:
|
|
if self.B_input[i] == 0 and self.i_counters[i] > 0:
|
|
self.i_counters[i] -= 1
|
|
if self.i_counters[i] == 0:
|
|
self.B_output[i] = 0
|
|
|
|
# 3. Optimized Bitwise Reconstruction
|
|
new_value = 0
|
|
for i in range(12):
|
|
if self.B_output[i]:
|
|
new_value |= (1 << i)
|
|
|
|
return hex(new_value)[2:].upper()
|
|
|
|
except Exception as e:
|
|
self.logPro.error(f"Error in readParallelaFiltrata: {e}")
|
|
return ''
|
|
|
|
def accoda(self, value):
|
|
try:
|
|
dt_eve = datetime.utcnow().strftime('%Y%m%d%H%M%S%f')[:-3]
|
|
self.rqEnqueue(f"{dt_eve}#{value}#{self.cont}")
|
|
except Exception as e:
|
|
self.logPro.error(f"QUEUE ERROR: {e}")
|
|
|
|
def svuotaCoda(self):
|
|
if self.timer_busy:
|
|
return
|
|
|
|
self.timer_busy = True
|
|
try:
|
|
if self.rqLen() > 0:
|
|
# Check connectivity using requests
|
|
try:
|
|
res_alive = requests.get(self.URLALIVE, timeout=5)
|
|
if res_alive.text == 'OK':
|
|
res_enabled = requests.get(self.URLENABLED + self.idxMacchina, timeout=5)
|
|
if res_enabled.text == 'OK':
|
|
if self.onLine == '0':
|
|
self.logPro.info("IOB ONLINE!")
|
|
self.onLine = '1'
|
|
else:
|
|
self.onLine = '0'
|
|
else:
|
|
self.onLine = '0'
|
|
except Exception as e:
|
|
self.logPro.error(f"Server Connection Error: {e}")
|
|
self.onLine = '0'
|
|
|
|
if self.onLine == '1' and not self.sending:
|
|
self.sending = True
|
|
for _ in range(self.NMAXSEND):
|
|
if self.rqLen() == 0:
|
|
break
|
|
|
|
resp = self.rqDequeue()
|
|
if not resp: break
|
|
|
|
parts = resp.split("#")
|
|
dt_eve, val, cnt = parts[0], parts[1], parts[2]
|
|
dt_curr = datetime.utcnow().strftime('%Y%m%d%H%M%S%f')[:-3]
|
|
|
|
url = f"{self.URLBASE}{self.idxMacchina}{self.URLADV1}{val}&dtCurr={dt_curr}&dtEve={dt_eve}&cnt={cnt}"
|
|
|
|
try:
|
|
r = requests.get(url, timeout=5)
|
|
self.logSnd.info(f"{val} [{cnt}] R:{r.text}")
|
|
except Exception as e:
|
|
self.logSnd.error(f"Send Error: {e}")
|
|
|
|
self.sending = False
|
|
elif self.sending:
|
|
if self.to_retry > 0:
|
|
self.to_retry -= 1
|
|
self.logPro.info("WAIT active send to complete")
|
|
else:
|
|
self.sending = False
|
|
self.to_retry = self.MAXRETRY
|
|
self.logPro.info("END WAIT, reset to_retry")
|
|
except Exception as e:
|
|
self.logPro.error(f"svuotaCoda Error: {e}")
|
|
finally:
|
|
self.timer_busy = False
|
|
|
|
def run(self):
|
|
self.setup_gpio()
|
|
|
|
# Start background thread for queue emptying
|
|
def timer_worker():
|
|
while True:
|
|
self.svuotaCoda()
|
|
time.sleep(self.SENDURLTIME)
|
|
|
|
threading.Thread(target=timer_worker, daemon=True).start()
|
|
|
|
old_value = ''
|
|
self.logPro.info("Starting main loop")
|
|
|
|
while True:
|
|
try:
|
|
time.sleep(self.SAMPLETIME)
|
|
value = self.readParallelaFiltrata()
|
|
|
|
if value != '':
|
|
if value != old_value:
|
|
self.logQue.info(f"{value} [{self.cont}]")
|
|
self.accoda(value)
|
|
self.cont = (self.cont + 1) % 10000
|
|
|
|
self.to_enable = True
|
|
self.to_short = self.TIMEOUTSHORT
|
|
self.to_long = self.TIMEOUTLONG
|
|
old_value = value
|
|
|
|
# Handle Timeouts
|
|
if self.to_enable:
|
|
self.to_short -= self.SAMPLETIME
|
|
if self.to_short <= 0:
|
|
self.logQue.info(f">{value} [{self.cont}]")
|
|
self.accoda(value)
|
|
self.to_short = self.TIMEOUTSHORT
|
|
self.to_enable = False
|
|
self.to_long = self.TIMEOUTLONG
|
|
|
|
self.to_long -= self.SAMPLETIME
|
|
if self.to_long <= 0:
|
|
self.logQue.info(f">>{value} [{self.cont}]")
|
|
self.accoda(value)
|
|
self.to_long = self.TIMEOUTLONG
|
|
|
|
except KeyboardInterrupt:
|
|
break
|
|
except Exception as e:
|
|
self.logPro.error(f"Main loop error: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
app = ReadParallelaIOB()
|
|
app.run()
|