Developer Documentation

Documentazione tecnica delle classi Python, con docstrings, annotazioni di tipo e logica di business completa.

Diagrammi UML

Visualizza la struttura delle classi e le loro relazioni.

Diagramma UML del Progetto FEBRARY

1. Modelli Base (Abstract)

Le classi astratte definiscono l'interfaccia comune per Libri e Utenti.

Classe Libro (libro.py)

from abc import ABC, abstractmethod
from datetime import date

class Libro(ABC):
    """
    Classe astratta che rappresenta un generico libro nella biblioteca.
    Definisce gli attributi comuni (titolo, autore, isbn) e impone
    l'implementazione del calcolo della scadenza alle sottoclassi.
    """
    def __init__(self, titolo, autore, isbn, anno_pubblicazione):
        self.titolo = titolo
        self.autore = autore
        self.isbn = isbn
        self.anno_pubblicazione = anno_pubblicazione

    @abstractmethod
    def calcola_scadenza(self, data_inizio: date) -> date:
        """
        Metodo astratto per calcolare la data di scadenza del prestito.
        La durata varia in base al tipo di libro (Fisico vs EBook).
        """
        pass

    def descrizione(self) -> str:
        """Restituisce una stringa formattata con i dettagli principali."""
        return f"{self.titolo} - {self.autore} ({self.anno_pubblicazione})"

Classe Utente (utente.py)

from abc import ABC, abstractmethod

class Utente(ABC):
    """
    Classe astratta per gli utenti della biblioteca.
    Gestisce i dati anagrafici e la lista dei prestiti attivi.
    """
    def __init__(self, nome, cognome, numero_tessera, email):
        self.nome = nome
        self.cognome = cognome
        self.numero_tessera = numero_tessera
        self.email = email
        self.lista_prestiti_attivi = []

    @abstractmethod
    def calcola_limite_prestiti(self) -> int:
        """Restituisce il numero massimo di libri che l'utente può avere in prestito."""
        pass

    @abstractmethod
    def calcola_multa(self, giorni_ritardo: int) -> float:
        """Calcola l'importo della multa in base ai giorni di ritardo."""
        pass

    def aggiungi_prestito(self, prestito):
        """Aggiunge un oggetto Prestito alla lista dell'utente."""
        self.lista_prestiti_attivi.append(prestito)

    def rimuovi_prestito(self, prestito):
        """Rimuove un prestito dalla lista (solitamente alla restituzione)."""
        if prestito in self.lista_prestiti_attivi:
            self.lista_prestiti_attivi.remove(prestito)

2. Implementazioni Concrete

Le sottoclassi che estendono i modelli base con logiche specifiche.

LibroFisico (libro_fisico.py)

from datetime import date, timedelta
from libro import Libro

class LibroFisico(Libro):
    """
    Rappresenta un libro cartaceo.
    Caratteristiche: Ha una posizione fisica e richiede manutenzione.
    Scadenza prestiti: 30 giorni.
    """
    def __init__(self, titolo, autore, isbn, anno_pubblicazione, 
                 numero_pagine, stato_conservazione, posizione_scaffale):
        super().__init__(titolo, autore, isbn, anno_pubblicazione)
        self.numero_pagine = numero_pagine
        self.stato_conservazione = stato_conservazione
        self.posizione_scaffale = posizione_scaffale

    def calcola_scadenza(self, data_inizio: date) -> date:
        """Aggiunge 30 giorni alla data di inizio."""
        return data_inizio + timedelta(days=30)

    def effettua_manutenzione(self):
        """Aggiorna lo stato di conservazione del libro."""
        self.stato_conservazione = "Ottimo (Restaurato)"
        print(f"Manutenzione effettuata su: {self.titolo}")

EBook (ebook.py)

from datetime import date, timedelta
from libro import Libro

class EBook(Libro):
    """
    Rappresenta un libro digitale.
    Caratteristiche: Ha formato file (PDF/EPUB) e dimensione.
    Scadenza prestiti: 14 giorni.
    """
    def __init__(self, titolo, autore, isbn, anno_pubblicazione, 
                 formato, dimensione_file):
        super().__init__(titolo, autore, isbn, anno_pubblicazione)
        self.formato = formato
        self.dimensione_file = dimensione_file

    def calcola_scadenza(self, data_inizio: date) -> date:
        """Aggiunge 14 giorni alla data di inizio."""
        return data_inizio + timedelta(days=14)

    def scarica(self):
        print(f"Scaricamento in corso di '{self.titolo}' ({self.dimensione_file})... Completato.")

StudenteUniversitario (studente_universitario.py)

from utente import Utente

class StudenteUniversitario(Utente):
    """
    Utente Studente.
    Limite prestiti: 5 libri.
    Multa giornaliera: 0.50€.
    """
    def __init__(self, nome, cognome, numero_tessera, email, numero_matricola, facolta):
        super().__init__(nome, cognome, numero_tessera, email)
        self.numero_matricola = numero_matricola
        self.facolta = facolta

    def calcola_limite_prestiti(self) -> int:
        return 5

    def calcola_multa(self, giorni_ritardo: int) -> float:
        return giorni_ritardo * 0.50

Insegnante (insegnante.py)

from utente import Utente

class Insegnante(Utente):
    """
    Utente Insegnante.
    Limite prestiti: 10 libri.
    Multa giornaliera: 0.25€ (Agevolata).
    """
    def __init__(self, nome, cognome, numero_tessera, email, dipartimento, ore_insegnamento):
        super().__init__(nome, cognome, numero_tessera, email)
        self.dipartimento = dipartimento
        self.ore_insegnamento = ore_insegnamento

    def calcola_limite_prestiti(self) -> int:
        return 10

    def calcola_multa(self, giorni_ritardo: int) -> float:
        return giorni_ritardo * 0.25

3. Core Business Logic (Biblioteca & Prestiti)

Le classi che gestiscono il flusso dei dati, le transazioni di prestito e il calcolo delle sanzioni.

Biblioteca (biblioteca.py)

La classe "Controller" principale che orchestra utenti e libri.

from datetime import date
from prestito import Prestito

class Biblioteca:
    """
    Classe Facade che gestisce l'intero sistema bibliotecario.
    Mantiene liste di libri, utenti, prestiti attivi e multe.
    """
    def __init__(self, nome):
        self.nome = nome
        self.lista_libri = []
        self.lista_utenti = []
        self.lista_prestiti = []
        self.lista_multe = []

    def aggiungi_libro(self, libro):
        """Aggiunge un nuovo libro al catalogo."""
        self.lista_libri.append(libro)
        print(f"Libro aggiunto: {libro.titolo}")

    def registra_utente(self, utente):
        """Registra un nuovo utente nel sistema."""
        self.lista_utenti.append(utente)
        print(f"Utente registrato: {utente.nome} {utente.cognome}")

    def controlla_disponibilita(self, libro) -> bool:
        """
        Verifica se un libro è disponibile.
        Return: False se il libro è attualmente in un prestito non restituito.
        """
        for prestito in self.lista_prestiti:
            if prestito.libro == libro and prestito.data_restituzione is None:
                return False
        return True

    def presta_libro(self, utente, libro, data_inizio):
        """
        Effettua un prestito se:
        1. Il libro è disponibile.
        2. L'utente non ha raggiunto il suo limite di prestiti.
        """
        if not self.controlla_disponibilita(libro):
            print(f"Errore: Il libro '{libro.titolo}' non è disponibile.")
            return None
            
        if len(utente.lista_prestiti_attivi) < utente.calcola_limite_prestiti():
            scadenza = libro.calcola_scadenza(data_inizio)
            nuovo_prestito = Prestito(libro, utente, data_inizio, scadenza)
            
            self.lista_prestiti.append(nuovo_prestito)
            utente.aggiungi_prestito(nuovo_prestito)
            print(f"Prestito registrato: '{libro.titolo}' a {utente.nome}. Scadenza: {scadenza}")
            return nuovo_prestito
        else:
            print(f"Errore: {utente.nome} ha raggiunto il limite di prestiti.")
            return None

    def restituisci_libro(self, prestito, data_restituzione):
        """
        Gestisce la restituzione. 
        Se la data_restituzione è successiva alla scadenza, genera una multa.
        """
        if prestito in self.lista_prestiti:
            prestito.data_restituzione = data_restituzione
            prestito.utente.rimuovi_prestito(prestito)
            
            multa = prestito.genera_multa(data_restituzione)
            if multa:
                self.lista_multe.append(multa)
                print(f"Libro restituito in RITARDO. Generata multa di {multa.importo}€.")
                return multa
            else:
                print(f"Libro '{prestito.libro.titolo}' restituito in orario.")
                return None
        else:
            print("Errore: Prestito non trovato.")

    def calcola_multa_utente(self, utente):
        """Somma tutte le multe non pagate di uno specifico utente."""
        totale = sum(m.importo for m in self.lista_multe if m.utente == utente and not m.pagata_flag)
        print(f"Totale multe da pagare per {utente.nome}: {totale}€")
        return totale

Prestito (prestito.py)

Classe di associazione che collega un Libro a un Utente per un periodo di tempo.

from datetime import date
from multa import Multa

class Prestito:
    """
    Rappresenta la transazione di prestito di un libro a un utente.
    Memorizza data inizio, data scadenza e data restituzione effettiva.
    """
    def __init__(self, libro, utente, data_inizio, data_scadenza):
        self.libro = libro
        self.utente = utente
        self.data_inizio = data_inizio
        self.data_scadenza = data_scadenza
        self.data_restituzione = None

    def calcola_giorni_ritardo(self, data_restituzione: date) -> int:
        """Restituisce la differenza in giorni tra restituzione e scadenza (se positiva)."""
        if data_restituzione > self.data_scadenza:
            delta = data_restituzione - self.data_scadenza
            return delta.days
        return 0

    def genera_multa(self, data_restituzione: date):
        """
        Crea un oggetto Multa se c'è un ritardo.
        Delega il calcolo dell'importo all'utente (strategia polimorfica).
        """
        giorni = self.calcola_giorni_ritardo(data_restituzione)
        if giorni > 0:
            importo = self.utente.calcola_multa(giorni)
            return Multa(self.utente, importo, data_restituzione)
        return None

Multa (multa.py)

class Multa:
    """
    Rappresenta una sanzione pecuniaria per un utente.
    Traccia se la multa è stata saldata o meno.
    """
    def __init__(self, utente, importo, data_creazione):
        self.utente = utente
        self.importo = importo
        self.data_creazione = data_creazione
        self.pagata_flag = False

    def pagata(self):
        """Segna la multa come pagata."""
        self.pagata_flag = True
        print(f"Multa di {self.importo}€ saldata dall'utente {self.utente.nome}.")

Database (MySQL)

Il database remoto è gestito tramite mysql-connector-python. Contiene tabelle per libri, utenti, prestiti e multe.

Struttura Tabelle Principali:

CREATE TABLE libri (
    id INT AUTO_INCREMENT PRIMARY KEY,
    titolo VARCHAR(200) NOT NULL,
    autore VARCHAR(200) NOT NULL,
    isbn VARCHAR(20) UNIQUE NOT NULL,
    anno_pubblicazione INT,
    disponibile BOOLEAN DEFAULT TRUE,
    tipo_libro ENUM('Fisico', 'EBook') NOT NULL,
    -- ... altri campi specifici per tipo_libro (es. posizione_scaffale, link_download, ...)
);

CREATE TABLE utenti (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(100) NOT NULL,
    cognome VARCHAR(100) NOT NULL,
    numero_tessera VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100),
    tipo_utente ENUM('Studente', 'Insegnante') NOT NULL,
    -- ... altri campi specifici per tipo_utente (es. facolta, dipartimento, ...)
    data_registrazione TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE prestiti (
    id INT AUTO_INCREMENT PRIMARY KEY,
    id_libro INT NOT NULL,
    id_utente INT NOT NULL,
    data_inizio TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    data_fine_prevista TIMESTAMP NOT NULL,
    data_fine_effettiva TIMESTAMP NULL,
    FOREIGN KEY (id_libro) REFERENCES libri(id),
    FOREIGN KEY (id_utente) REFERENCES utenti(id)
);
Icona Database Scarica SQL per il Database

Integrazione Hardware (Arduino + NFC)

Il sistema utilizza un Arduino Nano collegato a un modulo NFC RC522. L'Arduino legge l'UID del tag NFC e lo invia al computer tramite la porta seriale. L'applicazione Python usa il modulo pyserial e una classe dedicata SerialListener per ricevere questi dati in background.

Classe SerialListener (serial_listener.py)

import serial, threading, time
from queue import Queue
from typing import Optional


class SerialListener:
    def __init__(self, uid_queue: Queue, port: str = 'COM3', baudrate: int = 9600):
        self.uid_queue = uid_queue
        self.port = port
        self.baudrate = baudrate
        self.ser: Optional[serial.Serial] = None
        self._running = threading.Event()
        self.thread = threading.Thread(target=self.run, daemon=True)

    def start(self):
        try:
            self.ser = serial.Serial(self.port, self.baudrate, timeout=1)
            self._running.set()
            self.thread.start()
            print(f"SerialListener avviato su {self.port}")
        except serial.SerialException as e:
            print(f"Errore nell'apertura della porta seriale: {e}")

    def stop(self):
        self._running.clear()
        if self.ser:
            self.ser.close()
        self.thread.join()
        print("SerialListener fermato.")

    def run(self):
        while self._running.is_set():
            if self.ser and self.ser.in_waiting:
                try:
                    raw_line = self.ser.readline().decode('utf-8').strip()
                    if raw_line:
                        self.uid_queue.put(raw_line) # Inserisce l'UID in una coda
                except UnicodeDecodeError:
                    pass # Ignora linee corrotte
            time.sleep(0.1) # Piccola pausa per non sovraccaricare il thread

Schema e Sorgente Arduino

Di seguito è riportato lo schema di collegamento hardware tra Arduino Nano e il modulo RC522, come descritto nel codice.

Schema di Collegamento (SPI)

Schema di collegamento Arduino Nano e RC522

⚠️Assicurati di collegare il VCC del modulo RC522 al pin 3.3V dell'Arduino Nano, non al 5V, per evitare di danneggiare il modulo.⚠️

Codice Sorgente Arduino

Clicca sul pulsante qui sotto per scaricare il file .ino completo da caricare sul tuo Arduino Nano.

Scarica il codice (.ino)

Setup per Sviluppatori

Per configurare l'ambiente di sviluppo locale:

  1. Assicurati di avere Python 3.11+ installato.
  2. Installa le dipendenze: pip install customtkinter mysql-connector-python pyserial pillow opencv-python.
  3. Configura il file main.py con i dati di accesso corretti al database MySQL remoto (host, user, password, database).
  4. Collega l'Arduino Nano con il modulo NFC e identifica la porta seriale corretta (es. COM3 su Windows, /dev/ttyUSB0 su Linux).
  5. Esegui python main.py.