Fix commento
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# readParallela v. 3.1.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
|
||||
# - (3.1.0) Ottimizzazione gestione letture GPIO (sempre code-assisted)
|
||||
|
||||
|
||||
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.1.0 (2026)"
|
||||
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
|
||||
# Using a local reference for speed in loops
|
||||
gpio_input = GPIO.input
|
||||
pins = self.input_pins
|
||||
inverting = self.B_inverting
|
||||
input_arr = self.B_input
|
||||
|
||||
for i in range(12):
|
||||
# Read and invert logic immediately if required
|
||||
raw_val = 0 if gpio_input(pins[i]) else 1
|
||||
if inverting[i]:
|
||||
raw_val = 1 - raw_val
|
||||
input_arr[i] = raw_val
|
||||
|
||||
# 2. Processing Loop (Filtering & Blinking)
|
||||
# Pre-caching references to reduce attribute lookups in tight loops
|
||||
filter_arr = self.B_filter
|
||||
filter_prev = self.B_filter_prev
|
||||
filter_counters = self.i_filter_counters
|
||||
max_filter = self.MAX_COUNTER_FILTER
|
||||
|
||||
blinking = self.B_blinking
|
||||
previous = self.B_previous
|
||||
output_arr = self.B_output
|
||||
blink_counters = self.i_counters
|
||||
|
||||
for i in range(12):
|
||||
# Debounce / Filter logic
|
||||
if filter_arr[i]:
|
||||
curr_in = input_arr[i]
|
||||
prev_in = filter_prev[i]
|
||||
|
||||
if curr_in != prev_in:
|
||||
# State change detected
|
||||
if curr_in == 1:
|
||||
filter_counters[i] = max_filter if filter_counters[i] == 0 else 0
|
||||
self.B_temp[i] = 0 if filter_counters[i] == max_filter else 1
|
||||
else:
|
||||
filter_counters[i] = max_filter if filter_counters[i] == 0 else 0
|
||||
self.B_temp[i] = 1 if filter_counters[i] == max_filter else 0
|
||||
|
||||
filter_prev[i] = curr_in
|
||||
input_arr[i] = self.B_temp[i]
|
||||
else:
|
||||
# No state change, maintain or decrement counter
|
||||
if filter_counters[i] > 0:
|
||||
filter_counters[i] -= 1
|
||||
self.B_temp[i] = 0 if curr_in == 1 else 1
|
||||
else:
|
||||
self.B_temp[i] = 1 if curr_in == 1 else 0
|
||||
|
||||
# Update the actual input array with the filtered value
|
||||
input_arr[i] = self.B_temp[i]
|
||||
|
||||
# Blinking Logic
|
||||
if blinking[i] == 0:
|
||||
output_arr[i] = input_arr[i]
|
||||
else:
|
||||
if previous[i] != input_arr[i]:
|
||||
previous[i] = input_arr[i]
|
||||
if input_arr[i] == 1:
|
||||
output_arr[i] = 1
|
||||
blink_counters[i] = self.MAX_COUNTER_BLINK
|
||||
else:
|
||||
if input_arr[i] == 0 and blink_counters[i] > 0:
|
||||
blink_counters[i] -= 1
|
||||
if blink_counters[i] == 0:
|
||||
output_arr[i] = 0
|
||||
|
||||
# 3. Optimized Bitwise Reconstruction
|
||||
new_value = 0
|
||||
for i in range(12):
|
||||
if output_arr[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("-------------------------------")
|
||||
self.logPro.info(self.PROGRAM_NAME)
|
||||
self.logPro.info("-------------------------------")
|
||||
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()
|
||||
Reference in New Issue
Block a user