Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

lunedì 25 settembre 2023

Ricomincio da DEB
come creare un Debian Package - pt.1

Gaetano: Chello ch'è stato è stato, basta! Ricomincio da tre!
Lello: Da zero.
Gaetano: Eh?
Lello: Da zero! Ricominci da zero!
GaetanoNossignore, ricomincio da... cioè, tre cose me so' riuscite ind'a vita, pecchè aggià perdere pure cheste?! Aggià ricominciare da zero? Da tre!

Dopo il tormentone del CAN bus (un articolo in ben 4 parti!) ho deciso di ricominciare da zero, anzi... Ricomincio da tre, come il grande Massimo Troisi nella sua opera prima capolavoro. Ricominciare in che senso? Nel senso che, almeno per questa volta, invece di parlare della mia (nostra) amata programmazione in C parleremo di un argomento collegato e un po' trascurato, che corrisponde alla domanda: "Ma dopo aver sviluppato una applicazione Linux come la distribuisco?". Ecco, ci sono vari metodi, ma oggi ho voglia di parlare del più elementare e facile da usare, e cioè il "pacchetto di distribuzione" e, in particolare, mi soffermerò sul tipo più usato, il Debian Package (DEB Package o Pacchetto Debian per gli amici) che si usa nelle distribuzioni Linux più diffuse (Ubuntu, Mint, Debian, etc.).

...ma perché devo ricominciare da zero? Io ricomincio da DEB!...

Come detto sopra, ci sono vari metodi per distribuire una applicazione Linux e, prima di parlare del Debian Package, bisogna fare alcune precisazioni: indubbiamente, il metodo più rigoroso di distribuzione è basato su autotool: si distribuisce tutto l'ambiente di sviluppo (sorgenti, makefile e file di configurazione), e quindi si compila e installa usando il classico comando "./configure && make install". Questo metodo è rigoroso perché si auto-adatta (grazie ad autotool) alla macchina destinazione su cui può essere in uso una qualsiasi versione di Linux, tanto l'applicazione viene compilata e linkata localmente con le risorse a disposizione.

Ma usare autotool è un metodo molto specialistico (credo sia evidente... è roba da programmatori ah ah ah) mentre gli utenti "normali"  vogliono installare e usare in quattro e quattr’otto, (e se gli utenti hanno un passato Windows non ne parliamo neanche...). E quindi anche nel mondo Linux si usano degli installer che si chiamano "pacchetti di distribuzione", e che sono facilissimi da usare ma sono un po' meno flessibili di autotool: non si adattano automaticamente alla macchina (contengono l' applicazione precompilata) e quindi in alcuni casi potrebbero non essere installabili al primo colpo: ad esempio è possibile che la applicazione da installare usi una libreria di una versione differente di quella già presente nella macchina, e questo genera la segnalazione di un errore. In generale, però, è sempre abbastanza facile risolvere eventuali problemi, se l'installer è ben fatto.

Ah, dimenticavo: ovviamente un Debian Package ha un doppio uso: installare e disinstallare (e anche questa seconda attività è importante, no?). E allora, come si crea un pacchetto di questo tipo? Vediamo prima di tutto come è fatto: il Debian Package è un file di tipo archivio compresso che, internamente, ha la seguente struttura tipica:

├── DEBIAN
│ ├── control
│ ├── preinst
│ ├── postinst
│ ├── prerm
│ └── postrm
├── lib
│ └── systemd
│ └── system
│ └── myservice.service
└── usr
└── bin
└── myapp

ossia: ci sono tre directory principali (DEBIAN, lib e usr) che hanno il seguente uso:

  • DEBIAN: contiene i file di controllo e di pre/post installazione/disinstallazione:
    • control: è il file di controllo master, quello che guida l'installazione, infatti è l'unico file obbligatorio.
    • preinst: è uno shell script preparatorio eseguito automaticamente prima dell'installazione vera e propria. È opzionale.
    • postinst: è uno shell script di finalizzazione eseguito automaticamente dopo l'installazione. È opzionale.
    • prerm: è uno shell script preparatorio eseguito automaticamente prima della disinstallazione vera e propria. È opzionale.
    • postrm: è uno shell script di finalizzazione eseguito automaticamente dopo la disinstallazione. È opzionale.
  • lib: contiene l'albero di directory che replica l'albero dei servizi systemd della macchina destino: in questo lib/systemd/system si copieranno i servizi (file con estensione .service) necessari alla nostra applicazione (questa directory lib è opzionale, serve solo se il Package installa anche dei servizi systemd).
  • usr: contiene l'albero di directory che replica l'albero delle applicazioni della macchina destino: in questo usr/bin si copieranno gli eseguibili che compongono la nostra applicazione (nell'esempio sopra è una sola applicazione che si chiama "myapp").

E come è fatto un file control? È un file di testo che contiene alcune linee che descrivono il Package, e le linee che, come minimo, devono essere presenti sono:

  • Package: il nome dell'applicazione da installare.
  • Version: la versione dell'applicazione da installare.
  • Maintainer – il nome e l'indirizzo email del responsabile del Package.
  • Description – una descrizione corta dell'applicazione. Di solito questa è l'ultima linea del control: sotto questa linea si può aggiungere una descrizione più lunga che deve, però, cominciare con uno spazio.

si possono aggiungere molte altre linee con funzioni particolari, in particolare due linee che non dovrebbero mai mancare sono queste (N.B.: in realtà la lista dei campi required/recommended/optional varia un po' tra un manuale e l'altro... diciamo che usando i quattro campi qui sopra più i due qui sotto non si dovrebbero avere problemi):

  • Architecture – la architettura dove può correre la applicazione (i.e.: all, oppure amd64 oppure i386, oppure sparc... e molte altre. all si usa per le installazioni compatibili con qualsiasi macchina).
  • Depends: descrive le librerie (con le rispettive versioni) indispensabili all'installazione.

La linea Depends è un poco particolare: ci sono vari metodi per cercare quali sono le librerie indispensabili alla nostra applicazione, e uno dei metodi più interessanti usa un meccanismo dello stesso ambiente di generazione del Package, però con alcune stranezze (la prima volta che l'ho usato mi è costato un po' farlo funzionare). Ma uno dei miei compiti è svelare i trucchi, no? E vediamoli! Bisogna creare un mini-albero secondario che somiglia a quello principale mostrato sopra, un mini-albero come questo:

├── debian
│ └── control
└── usr
└── bin
└── myapp

sempre supponendo che la applicazione si chiami "myapp". Notare che debian è in minuscolo e che il file control  deve contenere solo questa linea:

source: myapp

dopodiché,  posizionandosi nella root-directory del mini-albero , si può eseguire il comando dpkg-shlibdeps ottenendo un risultato di questo tipo:

aldo@Linux $ dpkg-shlibdeps -O usr/local/bin/myapp
dpkg-shlibdeps: Avviso: binaries to analyze should already be installed in their package's directory
shlibs:Depends=libc6 (>= 2.34)
aldo@Linux $

Il risultato è, quindi, la lista delle librerie indispensabili (nell'esempio sopra c'è solo: libc6 dalla ver.2.34 in su) che potremo aggiungere in DEBIAN/control nella linea Depends. Dopodiché il mini-albero che abbiamo usato possiamo cancellarlo (ha già esaurito il suo compito) e possiamo tornare a usare l'albero principale.

Dopo questa introduzione teorica è l'ora di passare a un caso pratico, no? Vedendo un caso reale molti dubbi vengono cancellati automaticamente (beh, almeno a me succede spesso). Visto che ce l'abbiamo fresco in mente e abbiamo già il codice disponibile negli ultimi articoli pubblicati, cercheremo di creare un Debian Package che installa nel sistema canrecv, che è un CAN Server (e scusate se ritiro fuori un argomento appena visto!).

Ah, fino a qui ho dato per scontato che tutti sappiano cos'è un servizio systemd (e magari ci tornerò in futuro con un articolo) comunque in questo caso è sufficiente sapere che è la maniera più "moderna" di assegnare compiti a Linux, compiti del tipo: "al boot avvia questo", "se si interrompe riavvialo", ecc. Prima si usavano altri metodi (non so se a tutti è familiare il vecchio e sempre valido "init" derivato da "SysVinit"), ma da qualche anno a questa parte si usa quasi sempre systemd per cui lo useremo anche noi.

E torniamo al punto: l'obiettivo è, quindi, che il nostro Debian Package installi queste cose nel sistema:

  • un eseguibile cansetup.sh
  • un eseguibile canrecv
  • un servizio systemd  cansetup.service che produce l'esecuzione del CAN setup al boot  del sistema
  • un servizio systemd  canrecv.service che produce l'avvio del Server al boot  del sistema

Il codice di canrecv (canrecv.c) l'ho già pubblicato e lo possiamo trovare qui: è un Server molto minimale (riceve un messaggio ed esce) però per questo semplice esempio va più che bene (compito a casa: modificare canrecv.c per ottenere un CAN Server che riceva messaggi in loop). Anche lo shell script  cansetup.sh lo abbiamo visto in quest'altro articolo, ed è indispensabile al funzionamento del Server, visto che prepara il dispositivo virtual CAN del sistema. Ci mancano solo i due servizi: questo è cansetup.service:

[Unit]
Description=setup del vcan0 device
After=network.target

[Service]
ExecStart=/usr/local/bin/cansetup.sh

[Install]
WantedBy=default.target
mentre questo è canrecv.service:
[Unit]
Description=avvia il processo canrecv
After=cansetup.service

[Service]
ExecStart=/usr/local/bin/canrecv

[Install]
WantedBy=default.target

Come potete vedere sono abbastanza semplici (oserei dire: auto-esplicativi) e, comunque, spiegare come funzionano non è l'argomento di questo articolo (ripeto la semi-promessa: magari ci tornerò in futuro). L'importante è lo scopo di questi servizi, e quello credo che sia abbastanza chiaro. Quindi non ci resta che:

  • costruire l'albero di installazione (facile!)
  • copiarci dentro eseguibili e servizi (facilissimo!)
  • scrivere il file control (si può fare...)
  • scrivere gli eventuali script di pre/post installazione (spoiler: in questo semplice esempio ci serve solo lo script postinst)

E qui potrei concludere l'articolo aggiungendo le parti appena elencate... ma qui viene fuori la mia (nostra) anima di programmatore: perché fare a mano le operazioni descritte quando si possono automatizzare? Non posso concludere un articolo senza aggiungere una minima parte di programmazione, no?  E allora, nella seconda parte che arriverà prossimamente (giuro), vi presenterò un utile shell script che ho scritto per creare e ricreare (a piacere) il nostro Debian Package: è facilmente adattabile ad ogni uso futuro ed è una buona base per qualsiasi tipo di applicazione che abbisogna di un installer. Quindi per il momento vi saluto e, come sempre, vi consiglio di non trattenere il respiro nell'attesa!

Ciao, e al prossimo post! 

mercoledì 23 agosto 2023

Guardians of the CAN bus
come usare il CAN bus in C - pt.4

Stakar Ogord: Uniformi della OrgoCorp. Dovrete indossarle per muovervi nell'Orgosfera senza attirare l'attenzione.
Drax: Questo non è il mio colore.
Stakar Ogord: Cosa hai detto?
Drax: Stona con i miei occhi.
Ed eccoci alla quarta (e ultima!) parte di Guardians of the CAN bus, un ciclo di articoli ispirati al bellissimo Guardiani della Galassia Vol.3 (ripassatevi la prima, la seconda e la terza parte, please…). Nel dialogo qui sopra c'è, di nuovo, tutta la vena comica del film, che viaggia come al solito in parallelo alla parte seria. Mi ripeterò: è po’ come il CAN bus, che è così ben studiato ed efficace che rende divertente scrivere Software che lo usa, salvo quando devi risolvere un problema molto complicato e allora entri nella fase seria e drammatica... In questa quarta parte scopriremo gli ultimi segreti che ci mancano per diventare dei veri e propri Guardians of the CAN bus!

...ma anche il CAN bus stona coi tuoi occhi?...

Come ho fatto notare nella chiusura dell'ultimo articolo oramai ci manca solo il CAN FD per ultimare il nostro viaggio nei segreti del CAN bus. E allora veniamo subito al dunque: cosa è il CAN FD? Il CAN FD (CAN Flexible Data Rate) è un'espansione del CAN bus "classico" (Classical CAN) ed è definito dalla norma ISO 11898-1:2015. Il CAN FD facilita non solo l'invio di volumi di dati più grandi, visto che il limite di 8 byte del CAN classico è stato aumentato a 64 byte. Inoltre, offre la possibilità di una velocità di trasmissione più elevata nella fase dei dati, raggiungendo agevolmente i 5 Mbit/s. Anche del CAN FD esiste una versione extended, quindi, alla fin fine, abbiamo quattro tipi di frame, quelli mostrati in questa figura riassuntiva:

can bus versions

Si può commentare che, con questa versione FD, il CAN bus si snatura un poco, passando da semplice e affidabilissimo "bus di campo" a un rango superiore, quasi quello dei protocolli di rete sofisticati (tipo Ethernet) che permettono di trasferire notevoli moli di dati: comunque, anche se "snaturato", è assolutamente pronto all'uso e quando è il caso si può/deve usare, perché offre delle grandi prestazioni.

Ok, con ben quattro tipi disponibili sembrerebbe abbastanza complicato scrivere Software CAN "compatibile" (o quasi) passando da un tipo all'altro senza fare troppi cambi... Ma per questo c'è il nostro amico SocketCAN! Come abbiamo già visto nell'articolo precedente, passare da CAN classic a CAN extended è decisamente semplice, e sono felice di comunicarvi che, grazie alla flessibilità di SocketCAN, anche il CAN FD si usa in maniera molto semplificata. Vediamo come: non so se ricordate, ma nel primo articolo del ciclo avevo mostrato come è descritto (nell'header can.h) un frame CAN di SocketCAN, ve lo ripropongo:

/**
* struct can_frame - basic CAN frame structure
* @can_id: the CAN ID of the frame and CAN_*_FLAG flags, see above.
* @can_dlc: the data length field of the CAN frame
* @data: the CAN frame payload.
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* data length code: 0 .. 8 */
__u8 data[8] __attribute__((aligned(8)));
};

Ecco, questa sopra è una versione abbastanza datata (Linux 2.6) che non trattava ancora CAN FD. In Linux 3.7 troviamo la prima versione "moderna" che include il CAN FD:

/**
* struct can_frame - basic CAN frame structure
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
* @can_dlc: frame payload length in byte (0 .. 8) aka data length code
* N.B. the DLC field from ISO 11898-1 Chapter 8.4.2.3 has a 1:1
* mapping of the 'data length code' to the real payload length
* @data: CAN frame payload (up to 8 byte)
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* frame payload length in byte (0 .. CAN_MAX_DLEN) */
__u8 data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};

/**
* struct canfd_frame - CAN flexible data rate frame structure
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
* @len: frame payload length in byte (0 .. CANFD_MAX_DLEN)
* @flags: additional flags for CAN FD
* @__res0: reserved / padding
* @__res1: reserved / padding
* @data: CAN FD frame payload (up to CANFD_MAX_DLEN byte)
*/
struct canfd_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 len; /* frame payload length in byte */
__u8 flags; /* additional flags for CAN FD */
__u8 __res0; /* reserved / padding */
__u8 __res1; /* reserved / padding */
__u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8)));
};

Poi, in Linux 5.11, il can_frame è diventato così (mentre il canfd_frame è rimasto invariato):

/**
* struct can_frame - Classical CAN frame structure (aka CAN 2.0B)
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
* @len: CAN frame payload length in byte (0 .. 8)
* @can_dlc: deprecated name for CAN frame payload length in byte (0 .. 8)
* @__pad: padding
* @__res0: reserved / padding
* @len8_dlc: optional DLC value (9 .. 15) at 8 byte payload length
* len8_dlc contains values from 9 .. 15 when the payload length is
* 8 bytes but the DLC value (see ISO 11898-1) is greater then 8.
* CAN_CTRLMODE_CC_LEN8_DLC flag has to be enabled in CAN driver.
* @data: CAN frame payload (up to 8 byte)
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
union {
/* CAN frame payload length in byte (0 .. CAN_MAX_DLEN)
* was previously named can_dlc so we need to carry that
* name for legacy support
*/
__u8 len;
__u8 can_dlc; /* deprecated */
};
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 len8_dlc; /* optional DLC for 8 byte payload length (9 .. 15) */
__u8 data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};

Come si nota il campo can_dlc è stato deprecato (ma non cancellato) e il nome è stato allineato a quello usato nel CAN FD, proprio per permettere la scrittura semplificata di Software per entrambi i tipi senza troppi cambi!. Insomma, alla fine della fiera, ora è possibile riscrivere gli esempi del primo articolo (cansend.c e canrecv.c) per usare "Classic" o FD definendo una semplice variabile di compilazione (che ho chiamato CAN_FD). Cominciamo con canfdsend.c, vai col codice!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

// definizione del tipo CanFrame
#ifdef CAN_FD
typedef struct canfd_frame CanFrame; // CAN FD
#else
typedef struct can_frame CanFrame; // CAN classic
#endif

// canfdsend - funzione main()
int main(int argc, char *argv[])
{
// creo il socket
int sockfd;
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) == -1) {
// errore socket()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// set degli attributi di i/o
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", "vcan0");
if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) {
// errore ioctl()
printf("%s: errore ioctl() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

#ifdef CAN_FD
// set del modo CAN_FD
int enable_canfd = 1;
if ( setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable_canfd, sizeof(enable_canfd)) == -1) {

// errore setsockopt()
printf("%s: errore setsockopt() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}
#endif

// set attributi dell'indirizzo
struct sockaddr_can addr;
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

// assegna l'indirizzo al socket
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// errore bind()
printf("%s: errore bind() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// compongo il frame
CanFrame frame;
memset(&frame, 0, sizeof(CanFrame));
frame.can_id = 0x100U;
frame.len = 8;
snprintf(frame.data, sizeof(frame.data), "0123456");

// invio il frame
if (write(sockfd, &frame, sizeof(CanFrame)) != sizeof(CanFrame) == -1) {
// errore write()
printf("%s: errore write() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

close(sockfd);
return EXIT_SUCCESS;
}

ed ora passiamo a canfdrecv.c!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

// definizione del tipo CanFrame
#ifdef CAN_FD
typedef struct canfd_frame CanFrame; // CAN FD
#else
typedef struct can_frame CanFrame; // CAN classic
#endif

// canfdrecv - funzione main()
int main(int argc, char *argv[])
{
// creo il socket
int sockfd;
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) == -1) {
// errore socket()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// set degli attributi di i/o
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", "vcan0");
if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) {
// errore ioctl()
printf("%s: errore ioctl() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

#ifdef CAN_FD
// set del modo CAN_FD
int enable_canfd = 1;
if ( setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable_canfd, sizeof(enable_canfd)) == -1) {

// errore setsockopt()
printf("%s: errore setsockopt() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}
#endif

// set attributi dell'indirizzo
struct sockaddr_can addr;
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

// assegna l'indirizzo al socket
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// errore bind()
printf("%s: errore bind() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// ricevo il frame
CanFrame frame;
if (read(sockfd, &frame, sizeof(CanFrame)) == -1) {
// errore read()
printf("%s: errore read() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// mostro il frame ricevuto
printf("id:0x%x dlc:%d data: ", frame.can_id, frame.len);
for (int i = 0; i < frame.len; i++)
printf("%c ", frame.data[i]);

printf("\n");

close(sockfd);
return EXIT_SUCCESS;
}

Visto come è facile? Nel codice ci sono gli appositi "#ifdef CAN_FD" che decidono che parte di codice usare. Quindi per ottenere la versione "Classic" si può compilare con:

aldo@Linux $ gcc canfdsend.c -o canfdsend
aldo@Linux $ gcc canfdrecv.c -o canfdrecv

Mentre, per ottenere la versione FD si deve compilare con:

aldo@Linux $ gcc canfdsend.c -o canfdsend -DCAN_FD
aldo@Linux $ gcc canfdrecv.c -o canfdrecv -DCAN_FD

E i cambi nel codice sono veramente limitatissimi, grazie alla flessibilità dell'interfaccia SocketCAN: basta definire un nuovo tipo (che ho chiamato CanFrame) da usare al posto della struct can_frame (o della struct canfd_frame), così i cambi nel codice sono minimi, anche grazie al fatto che il campo can_dlc ora si chiama len in entrambe le strutture, e quindi, in pratica, l'unico vero cambio nel codice è questo:

#ifdef CAN_FD
// set del modo CAN_FD
int enable_canfd = 1;
if ( setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable_canfd, sizeof(enable_canfd)) == -1) {

// errore setsockopt()
printf("%s: errore setsockopt() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}
#endif

che forza l'uso, nel socket aperto precedentemente, dei frame di tipo FD. Insomma, come avevo promesso è veramente semplice scrivere codice "compatibile" con entrambi i tipi "Classic" e FD. E il CAN FD extended? Vabbé, ve lo lascio come utile esercizio, basti sapere che è sufficiente modificare il codice per FD con le minime modifiche che avevo mostrato nello scorso articolo al riguardo del CAN extended.

Ok, penso che con questo possa bastare. Il nostro viaggio nei misteri del CAN bus e del suo uso attraverso SocketCAN è terminato, ora possiamo qualificarci come Guardians of the CAN bus! Giuro che non ci sarà (per il momento...) una quinta parte, quindi nel prossimo articolo parleremo d'altro. E non trattenete il fiato nell'attesa, mi raccomando!

Ciao, e al prossimo post!

sabato 15 luglio 2023

Guardians of the CAN bus
come usare il CAN bus in C - pt.3

Mantis: È sbagliato manipolare i sentimenti degli amici.
Drax: E quella volta che mi hai fatto innamorare del mio calzino?
Mantis: Beh, è stato divertente.

Ed eccoci alla terza parte di Guardians of the CAN bus, un ciclo di articoli ispirati al bellissimo Guardiani della Galassia Vol.3 (ripassatevi la prima e la seconda parte, please...). Nel dialogo qui sopra c'è tutta la vena comica del film, che viaggia sempre in parallelo alla vena seria e drammatica. Un po' come il CAN bus, che è così ben studiato ed efficace che rende divertente scrivere Software che lo usa, salvo quando devi risolvere un problema molto complicato e allora entri nella fase seria e drammatica... ma siamo qui per questo! In questa terza parte scopriremo gli ultimi segreti che ci mancano per diventare dei veri e propri Guardians of The CAN bus!

...peró il CAN bus è ancora più divertente!...

Nelle parti una e due abbiamo affrontato un bel po' di argomenti: la struttura del CAN bus e dei frame, il protocollo SocketCAN, esempi elementari di send e receive, il can_frame di SocketCAN, le can-utils, il setup di un CAN device sotto Linux, il sistema di filtraggio... E cosa ci manca allora? Direi che per completare l'opera bisogna descrivere: il trattamento degli errori, il CAN extended e, dulcis in fundo, il CAN FD. Uh, è molta roba e il tempo stringe: cominciamo!

E cominceremo dal trattamento degli errori: nel caso che qualcosa vada male nel protocollo è prevista la generazione di messaggi di errore che, opportunamente decodificati, conducono alla identificazione del problema: ossia, la mia funzione di ricezione riceverà de messaggi auto-generati dal controller che sta nel dispositivo fisico con cui comunico, e questi messaggi mi diranno cosa c'è che non va. È un sistema di auto-diagnostica molto efficace, quindi. Vediamo allora una versione di canrecv.c che realizza questo punto: vai col codice di canrecverr.c!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <linux/can/error.h>

// canrecverr - funzione main()
int main(int argc, char *argv[])
{
// creo il socket
int sockfd;
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) == -1) {
// errore socket()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// set degli attributi di i/o
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", "vcan0");
if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) {
// errore ioctl()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// set attributi dell'indirizzo
struct sockaddr_can addr;
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

// assegna l'indirizzo al socket
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// errore bind()
printf("%s: errore bind() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// set errori da testare (in questo caso 3 ma se ne possono aggiungere a piacere)
can_err_mask_t err_mask = (CAN_ERR_TX_TIMEOUT | CAN_ERR_LOSTARB | CAN_ERR_PROT);
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_ERR_FILTER, &err_mask, sizeof(err_mask));

// ricevo il frame
struct can_frame frame;
if (read(sockfd, &frame, sizeof(struct can_frame)) == -1) {
// errore read()
printf("%s: errore read() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// analizzo se è un error frame
if ((frame.can_id & CAN_ERR_FLAG) != 0) {
printf("%s: messaggio di errore\n", argv[0]);
close(sockfd);
return EXIT_FAILURE;
}

// mostro il frame ricevuto
printf("id:0x%x dlc:%d data: ", frame.can_id, frame.can_dlc);
for (int i = 0; i < frame.can_dlc; i++)
printf("%c ", frame.data[i]);

printf("\n");

close(sockfd);
return EXIT_SUCCESS;
}

Visto? Usando la onnipresente system-call  setsockopt(2) si attiva la gestione degli errori usando il flag CAN_RAW_ERR_FILTER e passandole la maschera err_mask  degli errori che vogliamo vedere: io ne ho messi tre, ma se ne possono mettere altri a piacere, o addirittura aggiungerli tutti assegnando a err_mask  la maschera speciale CAN_ERR_MASK. Per attivare questa gestione l'unico altro cambio da effettuare nel codice è il test della presenza del flag CAN_ERR_FLAG nel can_id del messaggio ricevuto: se il flag è preesente non è un messaggio normale ma è un messaggio di errore da analizzare per estrarre l'informazione contenuta. L'analisi dell'errore si fa cercando nel can_id quali sono i bit attivi, seguendo questo esempio:

// errore: "TX timeout (by netdevice driver)"
if (frame->can_id & CAN_ERR_TX_TIMEOUT)
printf("TX timeout (by netdevice driver)");

// errore: "arbitration lost in bit: (data[0])"
if (frame->can_id & CAN_ERR_LOSTARB)
printf("arbitration lost in bit: 0x%02x", frame->data[0]);

// errore: "protocol violations: type (data[2]) location (data[3])"
if (frame->can_id & CAN_ERR_PROT)
printf("protocol violations: type 0x%02x location 0x%02x", frame->data[2], frame->data[3]);

L'esempio evidentemente non è completo, visto che gli errori trattabili sono veramente molti, ma per completarlo basta aiutarsi con le molte #define contenute in linux/can/error.h, è una attività relativemente semplice. Notare che la gestione degli errori è compatibile con quella dei filtri: nessuno impedisce di usarle (con oculatezza) allo stesso tempo.

Chiuso il capitolo errori possiamo passare al CAN extended, identificato dal protocollo CAN2.0(B) (mentre il CAN standard è CAN2.0(A)). Come si può intuire dal nome è una versione (quasi identica) del bus che usa un can_id esteso, e il risultato lo possiamo vedere nella seguente figura, dove (a) el il tipo normale e (b) è l'extended:

Frame--Standard-Extended

Con l'extended, praticamente, possiamo trasferire gli stessi dati, ma abbiamo una capacità di indirizzamento superiore. Dal punto di vista che ci interessa, quello del codice, questo cambio implica poche differenze visto che il "lavoro sporco" lo effettua il protocollo SocketCAN che, come gia visto nella primo articolo del ciclo, usa un can_id di 32 bit, quindi è compatibile con il CAN extended. La differenza fondamentale è nell'uso delle maschere definite nell'header can.h, di cui vi passo un estratto (utile anche per il punto precedente, quello degli errori):

/* special address description flags for the CAN_ID */
#define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */
#define CAN_RTR_FLAG 0x40000000U /* remote transmission request */
#define CAN_ERR_FLAG 0x20000000U /* error message frame */

/* valid bits in CAN ID for frame formats */
#define CAN_SFF_MASK 0x000007FFU /* standard frame format (SFF) */
#define CAN_EFF_MASK 0x1FFFFFFFU /* extended frame format (EFF) */
#define CAN_ERR_MASK 0x1FFFFFFFU /* omit EFF, RTR, ERR flags */

quindi, ad esempio, nel codice per la gestione degli errori visto sopra (canrecverr.c) si dovrebbe cambiare una piccola parte del codice per usare CAN extended (vi mostro solo il frammento modificato):

// set di 2 filtri (ma possono essere di più) e attivazione con setsockopt(2)
struct can_filter rfilter[2];
rfilter[0].can_id = 0x100;
rfilter[0].can_id |= CAN_EFF_FLAG;
rfilter[0].can_mask = (CAN_EFF_FLAG | CAN_RTR_FLAG | CAN_EFF_MASK);
rfilter[1].can_id = 0x200;
rfilter[1].can_id |= CAN_EFF_FLAG;
rfilter[1].can_mask = (CAN_EFF_FLAG | CAN_RTR_FLAG | CAN_EFF_MASK);
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

E, analogamente, quando inviamo o riceviamo un messaggio in modo CAN extended dobbiamo fare questo:

// preparo la spedizione di un messaggio
...
send_frame.can_id = 0x100;
send_frame.can_id |= CAN_EFF_FLAG; // per usare CAN extended
...

// esamino un messaggio ricevuto
...
printf("recvd: 0x%03X\n", frame->can_id & CAN_EFF_MASK);
...

Tutto qua!

E adesso dovremmo concludere in bellezza con il CAN FD. Dovremmo, ma l'atmosfera pre-vacanziera mi sta travolgendo, e poi non voglio buttare troppa carne al fuoco a Luglio inoltrato (scrivo questo pensiero in tempo reale: mentre scrivevo quello che avete appena letto sopra non avevo ancora questa intenzione. Giuro!)... quasi quasi lo rimando al prossimo articolo. Si, lo rimando... termineremo con una quarta parte in Agosto, tutta sul CAN FD, sperando che qualcuno la legga in spiaggia, ah ah ah.

Ciao, e al prossimo post!