I2L 2025 Groupe8

De wiki-se.plil.fr
Aller à la navigation Aller à la recherche

Proposition de système (étudiants)

Notre projet consiste à mettre en place un système d’authentification pour une application utilisant un badge électronique NFC. L’objectif est de permettre à un utilisateur de se connecter rapidement et de manière sécurisée sans saisir de mot de passe. L’utilisateur présente son badge au lecteur, si l’identification est valide, l’application se débloque et une LED verte ou un message indique le succès, si l’identification est invalide, l’accès est refusé et une LED rouge ou un message indique l’échec. Chaque tentative peut être enregistrée pour un suivi des accès.

Contre-proposition (intervenant)

Proposez un mode autonome et un mode connecté via USB. Le lecteur NFC PN532 semble pouvoir fonctionner en 3.3V (avec la batterie) ou en 5V (connexion USB).

En mode autonome, vous pouvez par exemple afficher des informations sur la carte ou le badge identifié. En mode connecté vous pouvez effectivement débloquer une application. L'application communiquera avec la carte via la classe USB "vendeur" (spécifique). Il suffit d'une interruption IN pour savoir si une carte a été identifiée.

Une bibliothèque C pour gérer un lecteur PN532 semble disponible [1].

Une bibliothèque pour gérer le contrôleur graphique SDD1306 est aussi disponible [2].

Proposition définitive (étudiants)

Mise en place d’un système d’authentification pour une application reposant sur l’utilisation d’un badge électronique NFC.

Après le scan du badge, l’utilisateur accède à un menu permettant :

  • d’authentifier et de déverrouiller l’application,
  • d’écrire ou de mettre à jour les informations stockées sur le badge.
  • de lire les données présentes sur le badge (nom, prénom, mot de passe, etc.),

L’application propose deux modes de fonctionnement :

  • un mode autonome,
  • un mode connecté via USB.

Une interface dédiée est également disponible pour les besoins de test et de validation du système.

Répartition du travail (étudiants)

Concernant la répartition des tâches, l’un a travaillé sur le mode autonome tandis que l’autre s’est concentré sur le mode connecté via USB. Toutefois, nous avons constamment collaboré et nous nous sommes mutuellement aidés tout au long du projet.

Carte

Schéma initial (étudiants)

Schéma de la carte

Carte routée (intervenant)

Vous utiliserez la carte avec l'écran OLED. Vous avez deux exemplaires de la carte suivant que vous souhaitez utiliser une connexion I2C ou série avec le lecteur NFC.

Composants (intervenant)

Carte réalisée (intervenant)

Photo de la carte

La carte est entiérement soudée. Eventuellement vous pouvez demander l'ajout d'un buzzer.

Travaux (étudiants)

Carte au 13/10/2025

Voici ce qu'on a pu tester :

- Clignotement des LEDs avec des boutons respectifs

- Mise en place de l'horloge (1000ms)

Carte au 16/10/2025

Voici ce qu'on a pu tester :

- Test de l'écran OLED : problème avec l'écran

- Test du NFC I2C en cours

- Test du NFC SPI en cours

Carte au 13/11/2025

- Écran OLED

On a créé une librairie SSD1306 (inspirée d'une déjà existante) pour faire fonctionner notre écran avec le microcontrôleur.

- Mode autonome

On a réussi à communiquer le microcontrôleur avec le NFC via I2C, pour cela, on a créé une librairie PN532 (inspirée d'une déjà existante).

- Mode connecté USB

On a réussi à transformer le microcontrôleur en périphérique USB en utilisant LUFA. Alors, on a pu échanger des données de notre microcontrôleur vers le PC et a pu débloqué le site facilement.

On a réussi à écrire sur la carte (badge) pour stocker des informations.

- Remarque :

Pour le badge (bleu), on a constaté qu'il faudrait avoir une certaine distance pour que ça fonctionne, et le blanc (petit rectangle) il faut coller directement sur le lecteur NFC.

Rendu Final

Description du Projet

L'objectif de ce projet est de réaliser un système d'authentification physique pour une application Web via la technologie NFC. Le système repose sur une architecture matérielle composée d'un microcontrôleur ATmega32u4 et d'un contrôleur NFC PN532. La partie logicielle comprend une application Web de démonstration et une API Python (faisant office de passerelle) pour assurer la communication série (USB) entre le matériel et l'interface Web.

Le système propose deux modes de fonctionnement :

  1. Mode autonome : Ce mode permet d'utiliser le lecteur NFC de manière indépendante. Grâce à un menu interactif sur l'écran OLED, l'utilisateur peut lire et visualiser les informations des badges (UID, nom, prénom) directement sur le boîtier, sans nécessiter de connexion à un ordinateur.
  2. Mode connecté (USB) : Une fois le module connecté à l'ordinateur, ce mode active la communication avec l'API Python. Il permet trois actions principales :
    • Lecture étendue : Transmission des données du badge scanné vers l'ordinateur.
    • Authentification Web : Déverrouillage de l'application Web par simple scan d'un badge enregistré.
    • Écriture : Enregistrement de nouveaux utilisateurs en écrivant les données (nom, prénom, mot de passe) sur une carte vierge depuis l'interface.

Matériel utilisé :

Carte complete intégrant le lecteur NFC
Badge NFC
Badge NFC 2.png

PS: Pour le badge (bleu), on a constaté qu'il faudrait avoir une certaine distance pour que ça fonctionne, et le blanc (petit rectangle) il faut coller directement sur le lecteur NFC.

La réalisation de ce projet nécessitait l'interaction avec deux périphériques principaux : le lecteur NFC et l'écran OLED SSD1306. La plupart des solutions open-source existantes (comme ssd1306 ou pn532-lib) étant conçues spécifiquement pour l'environnement Arduino, elles n'étaient pas directement compatibles avec notre architecture logicielle sur l'ATmega32u4.

Nous avons donc développé nos propres bibliothèques en nous inspirant de l’existant :

  1. Communication I2C : Nous avons d'abord écrit une bibliothèque bas niveau pour gérer le protocole I2C sur le microcontrôleur. C'est la fondation utilisée par nos autres pilotes.
  2. Gestion de l'écran OLED : En nous basant sur la bibliothèque de lexus2k, nous avons réécrit un pilote optimisé et compatible avec notre couche I2C.
  3. Gestion du lecteur NFC : De même, pour le PN532, nous avons adapté les bibliothèques Arduino existantes pour créer une version autonome utilisant notre pilote I2C.

Extraits significatifs de code (étudiants)

1. Badge NFC

Les données d’un badge NFC sont stockées dans des blocs mémoire bien définis, chacun étant identifié par une adresse numérique.

Le badge est donc découpé en plusieurs blocs, et chaque bloc peut contenir une information spécifique.

Dans notre cas, nous avons choisi d’organiser les données de la manière suivante : le nom est stocké dans le bloc 1, le prénom dans le bloc 2, et le mot de passe dans le bloc 5, comme indiqué ci-dessous.

// Configuration Blocs NFC
#define BLOCK_ADDR_NAME    1
#define BLOCK_ADDR_PRENOM  2
#define BLOCK_ADDR_PASS    5

Cette fonction ci dessous écrit des données dans un bloc spécifique d’un badge NFC après avoir authentifié l’accès. Elle complète le bloc à 16 octets avec des zéros si nécessaire et retourne true si l’écriture réussit, sinon false.

bool helper_write_block(PN532 *pn532, uint8_t block_addr, uint8_t *uid, int32_t uid_len, 
                               uint8_t *data, const char *label) {
    // Préparation du bloc de 16 octets 
    // Remplissage 0 si data plus court
    uint8_t block_data[BLOCK_SIZE];
    memset(block_data, 0, BLOCK_SIZE);
    
    size_t len = strlen((char*)data);
    if (len > BLOCK_SIZE) len = BLOCK_SIZE;
    memcpy(block_data, data, len);

    if (PN532_MifareClassicAuthenticateBlock(pn532, uid, uid_len, block_addr, MIFARE_CMD_AUTH_A, KEY_DEFAULT) != PN532_ERROR_NONE)
        return false;

    return (PN532_MifareClassicWriteBlock(pn532, block_data, block_addr) == PN532_ERROR_NONE);
}

Voici la méthode permettant de lire un bloc et d'afficher son contenu à l'écran :

// Lit un bloc. Si dest_buffer != NULL, copie les données brutes.
bool helper_read_block(PN532 *pn532, uint8_t block_addr, uint8_t *uid, int32_t uid_len, 
                              uint8_t *dest_buffer, const char *label, uint8_t ui_line) {
    uint8_t raw_data[BLOCK_SIZE];
    char display_str[32];
    char safe_content[BLOCK_SIZE + 1];

    // Authentification + Lecture
    if (PN532_MifareClassicAuthenticateBlock(pn532, uid, uid_len, block_addr, MIFARE_CMD_AUTH_A, KEY_DEFAULT) == PN532_ERROR_NONE &&
        PN532_MifareClassicReadBlock(pn532, raw_data, block_addr) == PN532_ERROR_NONE) 
    {
        // Copie vers destination
        if (dest_buffer != NULL)
            memcpy(dest_buffer, raw_data, BLOCK_SIZE);

        // Nettoyage pour affichage
        memcpy(safe_content, raw_data, BLOCK_SIZE);
        safe_content[BLOCK_SIZE] = '\0';
        for (int i = 0; i < BLOCK_SIZE; i++)
            if (!isprint((unsigned char)safe_content[i])) safe_content[i] = ' ';

        snprintf(display_str, sizeof(display_str), "%s: %s", label, safe_content);
        ssd1306_print_utf8_center(display_str, ui_line);
        return true;
    } 
    else {
        snprintf(display_str, sizeof(display_str), "%s: Erreur", label);
        ssd1306_print_utf8_center(display_str, ui_line);
        return false;
    }
}


2. Mode autonome

Pour le mode autonome sans PC, on a créée cette fonction ci-dessous. Elle attend qu’une carte NFC soit présentée, lit ses données principales (nom, prénom), les affiche sur l’écran OLED.

void action_infos(void) {
    uint8_t uid[MIFARE_UID_MAX_LENGTH];
    
    ssd1306_clear();
    ui_wait_card_screen("pour infos...");
    PORTD |= (1 << LED6);

    int32_t uid_len = detect_card(&pn532, uid);
    if (uid_len <= 0) {
        ui_show_msg("Pas de carte", 3);
        _delay_ms(DELAY_MSG_SHORT);
        goto cleanup;
    }

    ssd1306_clear();

    ui_format_and_print_uid(uid, uid_len);

    helper_read_block(&pn532,BLOCK_ADDR_NAME,   uid, uid_len, NULL, "Nom",    3);
    helper_read_block(&pn532,BLOCK_ADDR_PRENOM, uid, uid_len, NULL, "Prenom", 5);

    _delay_ms(SELECTION_DELAY);

cleanup:
    PORTD &= ~(1 << LED6);
    ssd1306_clear();
}


3. Mode connecté USB

On propose aussi un mode connecté USB sur PC pour pouvoir gérer les informations présentes sur le badge.

Cette fonction permet de lire les informations présentes sur le badge NFC et de l'envoyer au PC pour être afficher à l'interface utilisateur :

void action_read(void) {
    uint8_t uid[MIFARE_UID_MAX_LENGTH];
    uint8_t usb_buffer[USB_BUF_SIZE] = {0};
    
    ssd1306_clear();
    ui_wait_card_screen("pour lire...");
    PORTD |= (1 << LED5);

    int32_t uid_len = detect_card(&pn532, uid);
    if (uid_len <= 0) {
        ui_show_msg("Pas de carte", 3);
        _delay_ms(DELAY_MSG_LONG);
        goto cleanup;
    }

    ssd1306_clear();

    usb_buffer[0] = 'R'; 
    int copy_len = (uid_len > 7) ? 7 : uid_len; 
    memcpy(&usb_buffer[OFFSET_UID], uid, copy_len);

    ui_format_and_print_uid(uid, uid_len);
    
    helper_read_block(&pn532, BLOCK_ADDR_NAME,   uid, uid_len, &usb_buffer[OFFSET_NAME],   "Nom",    3);
    helper_read_block(&pn532,BLOCK_ADDR_PRENOM, uid, uid_len, &usb_buffer[OFFSET_PRENOM], "Prenom", 5);
    helper_read_block(&pn532,BLOCK_ADDR_PASS,   uid, uid_len, &usb_buffer[OFFSET_PASS],   "Pass",   7);

    usb_send_response(usb_buffer, USB_BUF_SIZE);
    
    _delay_ms(SELECTION_DELAY * 2);

cleanup:
    PORTD &= ~(1 << LED5);
    ssd1306_clear();
}

Voici la fonction pour envoyer les informations au PC :

void usb_send_response(uint8_t *buffer, uint16_t size) {
    Endpoint_SelectEndpoint(MYIN_EPADDR);
    if (Endpoint_IsINReady()) {
        Endpoint_Write_Stream_LE(buffer, size, NULL);
        Endpoint_ClearIN();
    }
}

Cette méthode ci-dessous permet d'écrire sur le badge NFC via le PC :

void action_write(void) {
    uint8_t uid[MIFARE_UID_MAX_LENGTH];
    uint8_t usb_buffer[USB_BUF_SIZE];
    
    ssd1306_clear();
    ssd1306_print_utf8_center("En attente", 2);
    ssd1306_print_utf8_center("donnees USB...", 4);
    PORTD |= (1 << LED4);

    bool received = false;
    for (int i = 0; i < TIMEOUT_USB_WAIT; i++) {
        USB_USBTask();
        Endpoint_SelectEndpoint(MYOUT_EPADDR);
        if (Endpoint_IsOUTReceived()) {
            if (Endpoint_BytesInEndpoint() >= 52) { // Header(4) + Data(48)
                Endpoint_Read_Stream_LE(usb_buffer, USB_BUF_SIZE, NULL);
                Endpoint_ClearOUT();
                received = true;
                break;
            }
        }
        _delay_ms(50);
    }

    if (!received) {
        ui_show_msg("Timeout USB", 3);
        _delay_ms(DELAY_MSG_SHORT);
        goto cleanup;
    }

    if (usb_buffer[0] != 'W') {
        ui_show_msg("Cmd Invalide", 3);
        _delay_ms(DELAY_MSG_SHORT);
        goto cleanup;
    }

    uint8_t len_n = usb_buffer[1];
    uint8_t len_p = usb_buffer[2];
    uint8_t len_pw = usb_buffer[3];

    // Buffers à écrire max 16 octets + null 
    uint8_t data_name[17] = {0};
    uint8_t data_prenom[17] = {0};
    uint8_t data_pass[17] = {0};

    memcpy(data_name,   &usb_buffer[4],      (len_n > 16) ? 16 : len_n);
    memcpy(data_prenom, &usb_buffer[4 + 16], (len_p > 16) ? 16 : len_p);
    memcpy(data_pass,   &usb_buffer[4 + 32], (len_pw > 16) ? 16 : len_pw);

    ssd1306_clear();
    char tmp_disp[32];
    snprintf(tmp_disp, 32, "Nom: %s", data_name);
    ssd1306_print_utf8_center(tmp_disp, 1);
    snprintf(tmp_disp, 32, "Pre: %s", data_prenom);
    ssd1306_print_utf8_center(tmp_disp, 3);
    snprintf(tmp_disp, 32, "Pas: %s", data_pass);
    ssd1306_print_utf8_center(tmp_disp, 5);
    ssd1306_print_utf8_center("Approchez carte", 7);
    
    _delay_ms(DELAY_MSG_SHORT);

    int32_t uid_len = detect_card(&pn532, uid);
    if (uid_len <= 0) {
        ui_show_msg("Pas de carte", 3);
        _delay_ms(DELAY_MSG_SHORT);
        goto cleanup;
    }

    ui_show_msg("Ecriture...", 3);
    
    bool success = true;
    if (success) success = helper_write_block(&pn532, BLOCK_ADDR_NAME,   uid, uid_len, data_name,   "Nom");
    _delay_ms(20);
    if (success) success = helper_write_block(&pn532,BLOCK_ADDR_PRENOM, uid, uid_len, data_prenom, "Prenom");
    _delay_ms(20);
    if (success) success = helper_write_block(&pn532,BLOCK_ADDR_PASS,   uid, uid_len, data_pass,   "Pass");

    uint8_t response[64] = {0};
    response[0] = 'W';
    response[1] = success ? 0x01 : 0x00;
    usb_send_response(response, 64);

    ui_show_msg(success ? "Ecriture OK!" : "Echec Ecriture", 3);
    _delay_ms(DELAY_MSG_LONG);

cleanup:
    PORTD &= ~(1 << LED4);
    ssd1306_clear();
}


4. Fonctionnement global

Le programme fonctionne comme un automate fini, où chaque état correspond à une option du menu (écriture, lecture, informations).

Les boutons Haut/Bas font transiter l’état courant vers un autre état, et le bouton Select déclenche l’action associée à l’état actif.

Après chaque action, l’automate revient dans l’état du menu sélectionné et attend la prochaine interaction.


Voici le code correspondant :

int main(void) {
    MenuOption current_selection = MENU_WRITE;

    init_system();
    menu_display(current_selection);

    while (1) {
        USB_USBTask();

        if (button_up_pressed()) {
            current_selection = (current_selection == 0) ? MENU_COUNT - 1 : current_selection - 1;
            menu_display(current_selection);
        }

        if (button_down_pressed()) {
            current_selection = (current_selection == MENU_COUNT - 1) ? 0 : current_selection + 1;
            menu_display(current_selection);
        }

        if (button_select_pressed()) {
            switch (current_selection) {
                case MENU_WRITE: action_write(); break;
                case MENU_READ:  action_read();  break;
                case MENU_INFOS: action_infos(); break;
            }
            menu_display(current_selection);
        }
    }
    return 0;
}

4. Librairies

Voici les principales fonctions utilisées dans les bibliothèques suivantes :

  • i2c-lib-atmega32u4
    void i2c_init(void) {
        TWBR = (uint8_t)TWBR_val;
    }
    
    void i2c_start_write(uint8_t address) {
        TWCR = (1<<TWSTA)|(1<<TWEN)|(1<<TWINT);
        while (!(TWCR & (1<<TWINT)));
        TWDR = (address << 1); // écriture
        TWCR = (1<<TWEN)|(1<<TWINT);
        while (!(TWCR & (1<<TWINT)));
    }
    void i2c_start_read(uint8_t address) {
        TWCR = (1<<TWSTA)|(1<<TWEN)|(1<<TWINT);
        while (!(TWCR & (1<<TWINT)));
        TWDR = (address << 1) | 1; // bit R/W = 1 pour lecture
        TWCR = (1<<TWEN)|(1<<TWINT);
        while (!(TWCR & (1<<TWINT)));
    }
    
    void i2c_stop(void) {
        TWCR = (1<<TWSTO)|(1<<TWEN)|(1<<TWINT);
        _delay_us(10);
    }
    
    void i2c_write(uint8_t data) {
        TWDR = data;
        TWCR = (1<<TWEN)|(1<<TWINT);
        while (!(TWCR & (1<<TWINT)));
    }
    uint8_t i2c_read(uint8_t ack) {
        if (ack)
            TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA);
        else
            TWCR = (1<<TWEN)|(1<<TWINT);
        while (!(TWCR & (1<<TWINT)));
        return TWDR;
    }
    
  • pn532-lib-atmega32u4
    int PN532_I2C_ReadData(uint8_t* data, uint16_t count) {
        i2c_start_read(PN532_I2C_ADDRESS);
        uint8_t status = i2c_read(1); // read status + ACK
        if (status != PN532_I2C_READY) {
            i2c_stop();
            return PN532_STATUS_ERROR;
        }
    
        for (uint16_t i = 0; i < count; i++) {
            data[i] = i2c_read(i < (count - 1));
        }
        i2c_stop();
        return PN532_STATUS_OK;
    }
    
    int PN532_I2C_WriteData(uint8_t* data, uint16_t count) {
        i2c_start_write(PN532_I2C_ADDRESS);
        for (uint16_t i = 0; i < count; i++) {
            i2c_write(data[i]);
        }
        i2c_stop();
        return PN532_STATUS_OK;
    }
    
    bool PN532_I2C_WaitReady(uint32_t timeout_ms) {
        uint8_t status;
        uint32_t waited = 0;
    
        while (waited < timeout_ms) {
            i2c_start_read(PN532_I2C_ADDRESS);
            status = i2c_read(0);
            i2c_stop();
            if (status == PN532_I2C_READY) {
                return true;
            }
            _delay_ms(5);
            waited += 5;
        }
        return false;
    }
    
    int PN532_I2C_Wakeup(void) {
        uint8_t wakeup[] = {0x55, 0x55, 0x00, 0x00, 0x00};
        i2c_start_write(PN532_I2C_ADDRESS);
        for (uint8_t i = 0; i < sizeof(wakeup); i++) {
            i2c_write(wakeup[i]);
        }
        i2c_stop();
        _delay_ms(100);
        return PN532_STATUS_OK;
    }
    
    int PN532_Reset(void) {
        _delay_ms(400);
        return PN532_STATUS_OK;
    }
    
    void PN532_I2C_Init(PN532* pn532) {
        i2c_init();
    
        pn532->reset = PN532_Reset;
        pn532->read_data = PN532_I2C_ReadData;
        pn532->write_data = PN532_I2C_WriteData;
        pn532->wait_ready = PN532_I2C_WaitReady;
        pn532->wakeup = PN532_I2C_Wakeup;
       // pn532->log = PN532_Log;
    
        pn532->reset();
        pn532->wakeup();
    }
    
  • pn532-lib-atmega32u4
#define GLYPH_WIDTH 5
#define GLYPH_SPACING 1
#define GLYPH_PX (GLYPH_WIDTH + GLYPH_SPACING)

// Draw a 5x7 glyph on the screen
static void draw_glyph5x7(const uint8_t glyph[GLYPH_WIDTH]) {
    for (uint8_t i = 0; i < GLYPH_WIDTH; ++i) {
        ssd1306_send_data_byte(glyph[i]);
    }
    ssd1306_send_data_byte(0x00); // 1px spacing
}

// Decode next glyph from a limited UTF-8 set (ASCII included)
// Returns number of bytes consumed and updates *out
static uint8_t next_glyph(const char *s, const uint8_t **out) {
    unsigned char c = (unsigned char)s[0];
    if (c == 0x00) {
        *out = NULL;
        return 0;
    }

    // Handle accented UTF-8 letters
    if (c == 0xC3 && s[1]) {
        unsigned char t = (unsigned char)s[1];
        switch (t) {
            case 0xA9: *out = font_accent[0]; return 2; // é
            case 0x88: *out = font_accent[1]; return 2; // è
            case 0x80: *out = font_accent[2]; return 2; // à
            case 0x94: *out = font_accent[3]; return 2; // ô
            case 0x99: *out = font_accent[4]; return 2; // ù
            case 0xA7: *out = font_accent[5]; return 2; // ç
            default:
                *out = font5x7['?' - FONT5X7_FIRST];
                return 2;
        }
    }

    // Treat all other characters (including ASCII)
    if (c < FONT5X7_FIRST || c > FONT5X7_LAST) c = '?';
    *out = font5x7[c - FONT5X7_FIRST];
    return 1;
}

// Measure the width in pixels of a string (up to '\n')
uint16_t measure_string_px(const char *s) {
    uint16_t count = 0;
    while (*s && *s != '\n') {
        const uint8_t *glyph = NULL;
        uint8_t consumed = next_glyph(s, &glyph);
        if (!consumed) break;
        count++;
        s += consumed;
    }
    return count * GLYPH_PX;
}

static void print_string_core(const char *s, uint8_t start_page, uint8_t start_col,
                              uint16_t max_width_px, bool center, uint16_t scroll_px) {
    if (!s || start_page >= SSD1306_PAGES) return;

    uint16_t base_col = start_col < SSD1306_WIDTH ? start_col : 0;
    uint16_t line_right = max_width_px ? base_col + max_width_px : SSD1306_WIDTH;
    if (line_right > SSD1306_WIDTH) line_right = SSD1306_WIDTH;

    uint8_t page = start_page;
    uint16_t col = base_col;

    while (*s && page < SSD1306_PAGES) {
        // Handle new line
        if (*s == '\n') {
            page++;
            if (page >= SSD1306_PAGES) break;
            col = base_col;
            ssd1306_set_cursor(page, (uint8_t)col);
            s++;
            continue;
        }

        const uint8_t *glyph = NULL;
        uint8_t consumed = next_glyph(s, &glyph);
        if (!consumed) break;

        // Scroll adjustment
        if (scroll_px >= GLYPH_PX) {
            scroll_px -= GLYPH_PX;
            s += consumed;
            continue;
        }

        // Wrap to next line if glyph exceeds line width
        if ((col + GLYPH_PX) > line_right) {
            page++;
            if (page >= SSD1306_PAGES) break;
            col = base_col;
            ssd1306_set_cursor(page, (uint8_t)col);
        }

        // Center adjustment (ignored if scrolling)
        if (center && scroll_px == 0 && col == base_col) {
            uint16_t remaining = 0;
            const char *line_start = s;
            while (*line_start && *line_start != '\n') {
                const uint8_t *tmp_glyph = NULL;
                uint8_t tmp_consumed = next_glyph(line_start, &tmp_glyph);
                if (!tmp_consumed) break;
                remaining += GLYPH_PX;
                line_start += tmp_consumed;
            }
            uint16_t draw_w = (remaining > (line_right - base_col)) ? (line_right - base_col) : remaining;
            col += ((line_right - base_col) - draw_w) / 2u;
            ssd1306_set_cursor(page, (uint8_t)col);
        }

        draw_glyph5x7(glyph);
        col += GLYPH_PX;
        s += consumed;
    }
}



// Public functions

// Print UTF-8 string with optional max width and optional scroll offset
void ssd1306_print_utf8_with_max_width_scroll(const char *s, uint8_t start_page, uint8_t start_col,
                                              uint8_t max_width_px, uint16_t scroll_px) {
    print_string_core(s, start_page, start_col, max_width_px, false, scroll_px);
}

// Print UTF-8 string across the full width
void ssd1306_print_utf8(const char *s, uint8_t start_page, uint8_t start_col) {
    print_string_core(s, start_page, start_col, 0, false, 0);
}

// Print UTF-8 string centered on the page
void ssd1306_print_utf8_center(const char *s, uint8_t page) {
    print_string_core(s, page, 0, 0, true, 0);
}
// Print UTF-8 string with optional max width
void ssd1306_print_utf8_with_max_width(const char *s, uint8_t start_page, uint8_t start_col, uint8_t max_width_px) {
    print_string_core(s, start_page, start_col, max_width_px, false, 0);
}


// Print a single character at a specific page/column
void ssd1306_putc(char c, uint8_t page, uint8_t col) {
    const uint8_t *glyph = NULL;
    char s[2] = {c, 0};
    next_glyph(s, &glyph);
    ssd1306_set_cursor(page, col);
    draw_glyph5x7(glyph);
}

// Scroll a long UTF-8 string horizontally on a single line
// scroll_offset: number of pixels to shift to the left
void ssd1306_print_utf8_scroll(const char *s, uint8_t page, uint8_t start_col,
                               uint8_t max_width_px, uint16_t scroll_offset) {
    if (!s || page >= SSD1306_PAGES) return;

    uint16_t base_col = start_col < SSD1306_WIDTH ? start_col : 0;
    uint16_t line_right = base_col + max_width_px;
    if (line_right > SSD1306_WIDTH) line_right = SSD1306_WIDTH;

    // Clear only this line/page
    ssd1306_set_cursor(page, 0);
    for (uint16_t i = 0; i < SSD1306_WIDTH; i++) {
        ssd1306_send_data_byte(0x00);
    }

    uint16_t col = base_col;
    const char *p = s;
    uint16_t skipped_px = 0;

    ssd1306_set_cursor(page, (uint8_t)col);

    while (*p && col < line_right) {
        const uint8_t *glyph = NULL;
        uint8_t consumed = next_glyph(p, &glyph);
        if (!consumed) break;

        // Skip pixels until scroll_offset is reached
        if (skipped_px + GLYPH_PX <= scroll_offset) {
            skipped_px += GLYPH_PX;
            p += consumed;
            continue;
        }

        // Draw visible glyph
        draw_glyph5x7(glyph);
        col += GLYPH_PX;
        p += consumed;
    }
}

Démonstrations


Affichage des informations sur le badge et authentification


Écriture sur le badge et affichage des infos

Rendus (étudiants)

Projet KiCAD : Fichier:I2L-2025-Carte-G8-final.zip

Programmes :