flowchart TD C1([Client]) --> S[Server] C2([Client]) --> S C3([Client]) --> S S -.-> C1 S -.-> C2 S -.-> C3
Coding Session n.1
Università di Trento, Dipartimento di Ingegneria Industriale
A che punto ci siamo lasciati
bind()
)connect()
)flowchart TD C1([Client]) --> S[Server] C2([Client]) --> S C3([Client]) --> S S -.-> C1 S -.-> C2 S -.-> C3
bind()
oppure connect()
)connect()
oppure bind()
)flowchart TD P[Publisher] P --> C1([Subscriber]) P --> C2([Subscriber]) P --> C3([Subscriber])
Fortunatamente, in ZeroMQ è indifferente chi fa il bind()
Ciò consente di risolvere il problema degli indirizzi dei publisher introducendo un broker
bind()
: è l’unico indirizzo che serve conoscereconnect()
)connect()
)flowchart LR P1([Publisher]) --> B[Broker] P2([Publisher]) --> B P3([Publisher]) --> B P4([Publisher]) --> B B --> S1([Subscriber]) B --> S2([Subscriber]) B --> S3([Subscriber]) B --> S4([Subscriber])
flowchart LR A([Source agent]) --> Br[Broker] B([Source agent]) --> Br C([Source agent]) --> Br Br --> D([Filter agent]) D --> Br Br --> E([Filter agent]) E --> Br Br --> F([Filter agent]) F --> Br Br --> S([Sink agent]) S ==BSON==> Db[(Database)] style S fill:#f9f
sequenceDiagram actor Source participant Broker actor Filter actor Sink par Initialization Source ->> Broker: Settings please? Broker -->> Source: Settings! and Filter -->> Broker: Settings, please? Broker -->> Filter: Settings! end loop Anynchronous Source -->> Broker: Data Broker -->> Filter: Data activate Filter Filter -->> Broker: Data deactivate Filter end Broker -->> Sink: Data
Qualcuno ha detto:
Ma perché non facciamo un’unica applicazione grafica che si occupa di fare tutto, in modo da riciclare il più possibile quanto fatto in passato?
Rispetto ad un’applicazione monolitica multithread la architettura ad agenti multiprocesso ha questi vantaggi:
Miroscic::Agent
Al momento c’è un unico agente di tipo sink che si occupa della registrazione di tutto il traffico che passa per il broker su database MongoDB
MongoDB è un database non-relazionale (NoSQL) che organizza i dati gerarchicamente
flowchart RL D1[JSON Document] --> C1[Collection] D2[JSON Document] --> C1 D3[JSON Document] --> C1 D4[JSON Document] --> C2[Collection] D5[JSON Document] --> C2[Collection] C1 --> DB[Database] C2 --> DB[Database] DB --> S[Server]
Ogni documento, o record, è un oggetto JSON in struttura libera
Uno stesso server può avere uno o più database; ogni database contiene una o più collection
In Miroscic, ogni collection contiene tutti i messaggi con lo stesso topic
Novità sul codice dall’ultima riunione
broker
Tutti gli agenti ascoltano sempre su un topic dedicato, control
, sul quale circolano comandi di controllo remoto
Questo topic consente, al momento, di comandare l’arresto o il riavvio di tutti gli agenti attivi
Al momento ci sono due modi per inviare questi comandi:
scripts/command
, che internamente fa una chiamata all’agente bridge
GUI/MiroscicMetadata
Per supportare questa funzione la classe Agent
dispone di due nuovi metodi:
Agent::enable_remote_control()
: va chiamato subito prima di Agent::connect()
; se l’agente è un puro publisher, è sufficiente questoAgent::remote_control()
: va chiamato all’interno del loop principale — solo per gli agenti che sono anche subscriberLa GUI in QT6 ha al momento queste funzioni:
Per semplificare lo sviluppo di nuovi agenti ho introdotto un’architettura a plugin
Un plugin è una libreria dinamica che viene caricata runtime dall’eseguibile principale
Può essere sviluppato e compilato in maniera completamente separata, ma deve fornire un’interfaccia ABI standard, cioè deve esporre dei simboli (C o C++) prefissati, a cui l’eseguibile principale si collega quando carica il plugin
Generalmente la complessità di garantire questa interfaccia scoraggia questo approccio, ma la libreria pugg lo rende in realità molto semplice
Penalità: solo un trascurabile ritardo al primo caricamento della libreria (al lancio)
Potenzialità: i plugin potrebbero essere distribuiti direttamente dal broker assieme alle impostazioni, consentendone l’aggiornamento remoto!
Caricare runtime una libreria richiede due funzioni C: dlopen()
e dlsym()
dlopen()
apre il file corrispondente e prepara le operazioni successivedlsym()
importa ciascun simbolo (funzione) che si desidera utilizzare; va usato una volta per ogni funzioneA causa del name mangling C++ è possibile caricare solo funzioni extern(C)
. Queste ultime però possono ritornare puntatori void *
di cui è possibile fare il cast a classi C++ note all’eseguibile
La libreria pugg sfrutta questo meccanismo: definisce alcune classi base (destinazioni del cast) che devono essere note all’eseguibile. I plugin possono implementare classi derivate che però hanno sempre la stessa interfaccia (metodi base)
È possibile definire tre tipi di classi base:
In maniera simmetrica, nel namespace Miroscic
ci sono altrettanti agenti generici di tipo source, filter e sink
Ogni agente generico carica solo plugin di tipo corrispondente. In che formato si scambiano i dati di I/O tra eseguibile e plugin?
Si usano istanze della classe nlohman/json
, la stessa usata per l’I/O a livello di network Miroscic
Filter
template <typename Tin = vector<double>, typename Tout = vector<double>>
class Filter {
public:
static const int version = 1;
static const std::string server_name() { return "FilterServer"; }
Filter() : _error("No error"), dummy(false) {}
virtual ~Filter() {}
virtual std::string kind() = 0;
virtual return_type load_data(Tin &data) = 0; // Tin può essere nlohmann::json
virtual return_type process(Tout *out) = 0; // Tout può essere nlohmann::json
virtual void set_params(void *params){}; // params può essere nlohmann::json (cast)
std::string error() { return _error; }
bool dummy;
private:
std::string _error;
};
Lo sviluppatore di plugin deve:
Nota: l’eseguibile principale non conosce le clasi derivate, quindi può interagire con il plugin SOLO mediante i metodi dichiarati nelle classi base.
Queste operazioni sono predisposte nel progetto github.com/miroscic/plugin_cpp
Il CMakeLists.txt
fornito con il progetto consente di compilare, per ogni plugin, anche una versione direttamente eseguibile, utile per il debugging e lo sviluppo
Per sviluppare un plugin si parte con un fork del progetto github.com/miroscic/plugin_cpp
Lo sviluppo può essere condotto in maniera assolutamente isolata dal resto di Miroscic, utilizzando la versione eseguibile del plugin per il testing.
Il nuovo progetto può utilizzare qualsiasi libreria necessaria. Se le librerie sono statiche, il plugin risultante sarà autosufficiente; se sono dinamiche, esse dovranno essere installate anche sulla macchina di destinazione
In Miroscic, gli agenti source e filter richiedono il file plugin come argomento e usano il nome del plugin come chiave per identificare topic e sezione del file INI.
Le variabili statiche Filter.version
e Source.version
vanno incrementate ogni volta che si cambia l’interfaccia. In questo modo plugin non compatibili segnalano l’errore al caricamento
Se necessario, sarà possibile modificare il broker in modo che quando un agente all’avvio richiede il file di impostazioni riceva in risposta, se è il caso, anche il file plugin.
Il broker dovrà mantenere un folder con tutti i plugin necessari, per le varie architetture
In questo modo, l’aggiornamento degli agenti può essere fatto direttamente e solo sul broker, comandando poi un riavvio di tutti gli agenti mediante GUI
logger
e salvati sul database MongoDB2024-02-14T09:52:31.453+00:00
Operiamo un binning dei documenti sulla base dei tempi, raccogliendo tutti i documenti generati con timestamp in intervalli di 1/25 s (40 ms)
Per semplificare, ogni documento viene marcato, oltre che con il timestamp, con un campo timecode
Il timecode è il numero di secondi trascorsi dalla mezzanotte con una risoluzione di 40 ms. Tutti i documenti con lo stesso timecode appartengono allo stesso frame e possono essere quindi accorpati
Il meccanismo di aggregation
di MongoDB consente di creare una nuova collezione dinamica read-only (in realtime!) che contiene i documenti aggregati utilizzando timecode
come chiave di aggregazione
Nella repository di Miroscic, la cartella analysis
contiene un notebook Python db_access.ipynb
che mostra come aggregare i documenti della piattaforma di forza con quelli dell’RFID
Il programma della sessione è il seguente:
Per lo sviluppo del plugin partiremo da un esempio di human pose estimation che utilizza il toolkit OpenVINO per fare inferenza mediante un modello OpenPOSE
source
paolo.bosetti@unitn.it