Files
Mapo-IOB/IOB-PI/readParallela.py
T
Samuele Locatelli 328a5fe540 aggiunta commenti
2026-05-12 16:30:43 +02:00

380 lines
15 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# readParallela v. 3.0.0 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
# - (3.0.0) Prima versione code-assisted: riformulazione e ottimizzazione globale programma
import time
import sys
import os
import logging
import logging.handlers
import threading
import configparser
from datetime import datetime, timezone
from array import array
import redis
import requests
import urllib3
# Disable urllib3 debug logging to keep application logs clean
urllib3.disable_warnings()
logging.getLogger("urllib3").setLevel(logging.WARNING)
# 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
# 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)
# Load configuration after arrays are initialized
self.load_config(config_path)
# 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):
"""
Loads configuration parameters from the specified .cfg file.
Sets up timing, URL, logging, and bit-specific settings (blinking, inversion, filtering).
"""
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):
"""
Configures the logging system.
"""
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):
"""
Initializes the GPIO pins for input.
"""
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):
"""
Adds an item to the Redis queue.
"""
self.CodaR.rpush(self.queue_name, item)
def rqDequeue(self):
"""
Removes and returns an item from the Redis queue.
"""
item = self.CodaR.lpop(self.queue_name)
return item.decode('utf-8') if item else None
def rqLen(self):
"""
Returns the current length of the Redis queue.
"""
return self.CodaR.llen(self.queue_name)
def readParallelaFiltrata(self):
"""
Performs the core logic: reads GPIO, applies inversion, filtering,
and blinking, then reconstructs the value as a hex string.
"""
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.now(timezone.utc).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.now(timezone.utc).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):
"""
Starts the main execution loop:
1. Initializes GPIO.
2. Spawns a background daemon thread to empty the Redis queue.
3. Enters a loop to sample GPIO inputs, apply filters/blinking, and queue changes.
"""
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:
self.logPro.info("Keyboard interrupt received. Exiting...")
break
except Exception as e:
self.logPro.error(f"Main loop error: {e}")
if __name__ == "__main__":
app = ReadParallelaIOB()
app.run()