Codetics · Rust et OCPP

OCPP en Rust : sept bibliothèques open source

Une borne de recharge et son serveur se parlent dans une langue commune, OCPP. Nous avons écrit trois versions de cette langue en Rust, un langage de programmation conçu pour arrêter toute une famille de plantages avant que le logiciel tourne, et nous sommes allés jusqu'au serveur complet. Si vous êtes développeur, tout se télécharge et s'essaie sur votre machine. Si vous cherchez à qui confier un projet de recharge, vous pouvez lire notre travail avant même de nous parler.

Code public, licence Apache-2.0, version 0.1.0. Tutoriels exécutés le 5 novembre 2026 sur les versions publiées.

  • 7bibliothèques publiées sur crates.io
  • 3versions d'OCPP : 1.6, 2.0.1 et 2.1
  • Apache-2.0une licence open source, code public sur GitHub
  • 3profils de sécurité OCPP, de 1 à 3, pris en charge par le serveur
Pourquoi ce travail existe

Savoir parler aux bornes, cela se prouve en l'écrivant

OCPP, c'est ce qu'une borne et son serveur utilisent pour se dire qu'une voiture est branchée, qu'une charge démarre, qu'elle vient de s'arrêter. Sur le papier, c'est une spécification. Dans le code, chaque version arrive avec ses messages et ses règles. La 1.6 traîne encore des codes d'erreur à l'orthographe historique qu'il faut savoir reconnaître, et la 2.1 ajoute le transfert d'énergie dans les deux sens et l'échange de batteries.

Kevin Vavelin a écrit ces sept bibliothèques pour Codetics, message par message, sans autre bibliothèque OCPP et sans génération de code, jusqu'à la couche WebSocket qui transporte les échanges. Cela prend du temps, et c'est justement la partie que nous aimons : à la fin, nous savons pourquoi chaque ligne est là.

Si vous n'êtes pas développeur, retenez une chose. Quand nous vous parlons de recharge, vous n'avez pas à nous croire sur parole. Le code est public, ses limites sont écrites, et une personne de confiance peut tout relire avant que vous décidiez.

Les sept crates

Sept briques, à prendre ensemble ou une par une

Une crate, c'est le nom d'une bibliothèque en Rust, un paquet de code prêt à réutiliser. Toutes sont en version 0.1.0 sur crates.io, publiées le 21 octobre 2026, et compilent à partir de Rust 1.88. Prenez le serveur si vous voulez un système qui tourne, ou seulement la brique qui vous manque.

  • codetics-ocpp-service 0.1.0

    Le serveur de gestion complet

    Un CSMS, c'est-à-dire le serveur auquel les bornes se connectent, pour OCPP 1.6J, 2.0.1 et 2.1. Avec TLS et les profils de sécurité 1 à 3, la recharge intelligente, les réservations, les tarifs et le calcul des coûts, une API HTTP et le stockage dans PostgreSQL.

  • codetics-ocpp16 0.1.0

    OCPP 1.6

    Chaque message de la version 1.6 sous forme de types Rust simples, avec validation et aiguillage typé. Aucune autre bibliothèque OCPP, aucune génération de code.

  • codetics-ocpp201 0.1.0

    OCPP 2.0.1

    La même approche pour la version 2.0.1, édition 4 : chaque message en types Rust simples, validé et aiguillé par type.

  • codetics-ocpp21 0.1.0

    OCPP 2.1

    La version 2.1, édition 2, avec ce qu'elle apporte : les messages SEND, le transfert d'énergie dans les deux sens, l'échange de batteries et les flux d'événements périodiques.

  • codetics-ocpp-core 0.1.0

    Le socle commun

    Ce qui ne dépend d'aucune version d'OCPP-J : le découpage des messages RPC, les codes d'erreur avec les orthographes historiques de la 1.6, les dates RFC 3339, la validation, la négociation du sous-protocole et les identifiants de message.

  • codetics-ocpp-ws 0.1.0

    La couche WebSocket

    Notre propre pile WebSocket, RFC 6455 et RFC 7692 (permessage-deflate, obligatoire pour un CSMS en 2.0.1 et 2.1), pensée pour de nombreuses connexions de longue durée. Un petit client l'accompagne pour les simulateurs et les tests.

  • codetics-evse-id 0.1.0

    Les identifiants de mobilité électrique

    Les identifiants EVSE, pool et station au format IDRR / eMI3, avec le caractère de contrôle de l'annexe 1 de l'IDRR, qui détecte toute erreur sur un seul caractère et toute inversion de deux caractères voisins.

Le tutoriel

De rien du tout à une borne simulée qui parle à votre serveur

Cinq étapes sur votre machine, sans aucun matériel, puisque la borne est un petit programme. À la fin, votre serveur répond Accepted à une borne qu'il vient de reconnaître, et vous avez vu passer chaque échange.

Ce qu'il vous faut
  • Rust 1.88 ou plus récent
  • PostgreSQL, avec les extensions pg_trgm et unaccent
  • OpenSSL, sauf si vous installez avec la fonction vendored-openssl
  • curl, pour parler au serveur depuis le terminal
  1. 01

    Installer le serveur

    cargo install télécharge le code source depuis crates.io et le compile sur votre machine, ce qui prend quelques minutes. Le serveur a besoin d'OpenSSL. S'il n'est pas là, la fonction vendored-openssl le compile avec le reste.

    Terminal
    cargo install codetics-ocpp-service
    
    # No OpenSSL on this machine? Build it from source instead:
    cargo install codetics-ocpp-service --features vendored-openssl
  2. 02

    Créer la base de données

    Le serveur garde tout dans PostgreSQL. createdb crée une base vide, et DATABASE_URL dit au serveur où la trouver. Vous n'avez aucune table à créer : le premier démarrage s'en charge, et c'est là qu'il a besoin des extensions pg_trgm et unaccent.

    Terminal
    createdb ocpp
    export DATABASE_URL=postgres://localhost/ocpp
  3. 03

    Démarrer le serveur

    Le serveur écoute maintenant à deux endroits : le port 9000 pour les bornes, et 127.0.0.1:8080 pour l'API HTTP, celle que vos propres outils appellent. Laissez-le tourner et ouvrez un second terminal. Si /health répond status ok et database true, tout est branché.

    Terminal
    codetics-ocpp-service
    
    # In another terminal:
    curl http://127.0.0.1:8080/health
    Résultat attendu
    {"connections":0,"database":true,"node":"my-machine","status":"ok"}
  4. 04

    Déclarer la station

    Une station inconnue est refusée avec une erreur HTTP 404, c'est pourquoi on la déclare d'abord. DEMO-01 est l'identité que la borne simulée présentera en se connectant.

    Terminal
    curl -X POST http://127.0.0.1:8080/api/v1/stations \
      -H 'content-type: application/json' \
      -d '{"ocpp_identity":"DEMO-01","name":"My first charger"}'
  5. 05

    Connecter une borne simulée

    Ce petit programme joue le rôle de la borne. Mettez-le dans src/main.rs et lancez cargo run. Il se connecte au port 9000 sur /ocpp/DEMO-01, propose le sous-protocole ocpp1.6, puis envoie un BootNotification, le message par lequel une borne se présente. La première réponse peut surprendre, car c'est un TriggerMessage(BootNotification) : cette station n'a encore jamais démarré, alors le serveur lui demande de se présenter. Une vraie borne y répondrait, notre simulateur se contente de l'afficher. La ligne suivante répond à votre message avec Accepted, et votre serveur vient d'accepter sa première station.

    Terminal
    cargo new station && cd station
    cargo add codetics-ocpp-ws
    cargo add tokio -F rt-multi-thread,macros
    src/main.rs
    use codetics_ocpp_ws::Message;
    use codetics_ocpp_ws::client::{ClientOptions, connect};
    
    #[tokio::main]
    async fn main() {
        let options = ClientOptions {
            addr: "127.0.0.1:9000",
            path: "/ocpp/DEMO-01",
            subprotocols: &["ocpp1.6"],
            deflate: false,
            basic_auth: None,
        };
        let mut station = connect(&options).await.expect("connection refused");
        println!("connected, protocol: {:?}", station.subprotocol);
    
        let boot = r#"[2,"1","BootNotification",{"chargePointVendor":"Demo","chargePointModel":"Simulator"}]"#;
        station.writer.send_text(boot).await.unwrap();
    
        while let Ok(Some(message)) = station.reader.recv().await {
            if let Message::Text(reply) = message {
                println!("server replied: {reply}");
                if reply.contains(r#""1""#) {
                    break;
                }
            }
        }
    }
    Résultat attendu
    connected, protocol: Some("ocpp1.6")
    server replied: [2,"64da928f-...","TriggerMessage",{"requestedMessage":"BootNotification"}]
    server replied: [3,"1",{"currentTime":"2026-11-05T16:51:20.306Z","interval":300,"status":"Accepted"}]

Ce tutoriel sert à un essai local, sur une connexion non chiffrée. Pour un usage réel, mettez en place TLS (OCPP_TLS_LISTEN, certificats, profils de sécurité) et API_TOKEN, et donnez un mot de passe à chaque borne.

Une bibliothèque à la fois

Six bibliothèques, six petits exemples à lancer

Chaque exemple est un src/main.rs complet, compilé et lancé sur les versions publiées. Créez un projet avec cargo new, ajoutez la dépendance avec la commande indiquée, collez le code, puis lancez cargo run.

  • codetics-ocpp16

    Lire un message de borne en 1.6

    StationCall::decode prend le nom de l'action et son contenu JSON, et vous rend un type Rust. L'exemple lit un BootNotification, puis tente un Reset. Ce message va du serveur vers la borne, jamais l'inverse, alors le décodage le refuse avec WrongDirection. L'erreur de sens s'arrête là, avant d'atteindre votre code métier.

    Ajouter à votre projet
    cargo add codetics-ocpp16
    src/main.rs
    use codetics_ocpp16::{DecodeError, StationCall};
    
    fn main() {
        let payload = r#"{"chargePointVendor":"VendorX","chargePointModel":"X1"}"#;
        let call = StationCall::decode("BootNotification", payload).unwrap();
        let StationCall::BootNotification(boot) = call else { unreachable!() };
        println!("vendor: {}", boot.charge_point_vendor);
    
        // Reset is sent by the central system, never by a charger, so it is refused.
        let wrong = StationCall::decode("Reset", r#"{"type":"Hard"}"#);
        assert!(matches!(wrong, Err(DecodeError::WrongDirection(_))));
    }
  • codetics-ocpp201

    Le même message, une autre forme en 2.0.1

    Toujours un BootNotification, mais le fabricant se trouve maintenant dans un objet chargingStation, avec une raison de démarrage en plus. Comparez avec l'exemple précédent : même action, autre structure, et c'est pour cela que chaque version a sa propre bibliothèque.

    Ajouter à votre projet
    cargo add codetics-ocpp201
    src/main.rs
    use codetics_ocpp201::StationCall;
    
    fn main() {
        let payload = r#"{"reason":"PowerUp","chargingStation":{"model":"SingleSocketCharger","vendorName":"VendorX"}}"#;
        let call = StationCall::decode("BootNotification", payload).unwrap();
        let StationCall::BootNotification(boot) = call else { unreachable!() };
        println!("vendor: {}", boot.charging_station.vendor_name);
    }
  • codetics-ocpp21

    Les nouveautés de la 2.1

    Action connaît le sens de chaque message : BatterySwap va de la station vers le CSMS. is_send repère les messages SEND, ceux qui n'attendent pas de réponse, comme NotifyPeriodicEventStream, un flux périodique de mesures que l'exemple décode ensuite avec StationSend.

    Ajouter à votre projet
    cargo add codetics-ocpp21
    src/main.rs
    use codetics_ocpp21::{Action, Direction, StationSend};
    
    fn main() {
        assert_eq!(Action::BatterySwap.direction(), Direction::StationToCsms);
        assert!(Action::NotifyPeriodicEventStream.is_send());
    
        let payload = r#"{"id":1,"pending":0,"basetime":"2025-01-01T00:00:00Z","data":[{"t":0,"v":"230.4"}]}"#;
        let StationSend::NotifyPeriodicEventStream(stream) =
            StationSend::decode("NotifyPeriodicEventStream", payload).unwrap();
        println!("points received: {}", stream.data.len());
    }
  • codetics-ocpp-core

    Se mettre d'accord, puis lire une trame

    Le client annonce les versions qu'il parle, par ordre de préférence, et le serveur retient la première qu'il accepte. Ici, il n'accepte que la 1.6, donc c'est elle. Frame::parse lit ensuite une trame [2, identifiant, action, contenu], la forme de tout appel OCPP-J, et encode_result prépare la réponse avec le même identifiant.

    Ajouter à votre projet
    cargo add codetics-ocpp-core serde_json
    src/main.rs
    use codetics_ocpp_core::rpc::{self, Frame};
    use codetics_ocpp_core::version::negotiate;
    use codetics_ocpp_core::OcppVersion;
    
    fn main() {
        // The client lists versions by preference, the server picks the first it accepts.
        let (version, token) = negotiate(["ocpp2.1, ocpp1.6"], &[OcppVersion::V16]).unwrap();
        println!("{version:?} over {token}");
    
        if let Frame::Call { id, action, .. } = Frame::parse(r#"[2,"19223201","Heartbeat",{}]"#).unwrap() {
            let body = serde_json::json!({ "currentTime": "2025-01-01T00:00:00Z" });
            println!("{action}: {}", rpc::encode_result(&id, &body).unwrap());
        }
    }
  • codetics-ocpp-ws

    Une session WebSocket qui renvoie ce qu'elle reçoit

    server reçoit un flux que votre serveur HTTP (hyper, axum...) a déjà fait passer en WebSocket. La boucle renvoie les textes, répond aux pings et ferme proprement. Pour que l'exemple tourne tel quel, un tuyau en mémoire remplace la vraie connexion : on ferme l'autre bout, la session se termine et le programme affiche session closed.

    Ajouter à votre projet
    cargo add codetics-ocpp-ws
    cargo add tokio -F rt-multi-thread,macros
    src/main.rs
    use codetics_ocpp_ws::{Message, WsConfig, close_code};
    use tokio::io::{AsyncRead, AsyncWrite};
    
    // Give it the stream your HTTP server has already upgraded (hyper, axum...).
    async fn session<S: AsyncRead + AsyncWrite>(upgraded: S) {
        let (mut reader, mut writer) = codetics_ocpp_ws::server(upgraded, WsConfig::default(), None);
        while let Ok(Some(message)) = reader.recv().await {
            match message {
                Message::Text(text) => writer.send_text(&text).await.unwrap(),
                Message::Ping(data) => writer.send_pong(&data).await.unwrap(),
                Message::Close(_) => {
                    let _ = writer.send_close(close_code::NORMAL, "").await;
                    break;
                }
                _ => {}
            }
        }
    }
    
    #[tokio::main]
    async fn main() {
        // Two ends of an in-memory pipe stand in for a real connection, so this runs as is.
        let (server_end, client_end) = tokio::io::duplex(4096);
        drop(client_end);
        session(server_end).await;
        println!("session closed");
    }
  • codetics-evse-id

    Construire un identifiant de point de charge

    EvseId::station crée l'identifiant d'une station, evse_of celui d'un point de charge qui lui appartient, et kind confirme le type obtenu. with_operator ajoute le pays et l'opérateur. Tout l'intérêt du format tient dans son caractère de contrôle : une faute de frappe sur un caractère, ou deux caractères voisins inversés, se détecte.

    Ajouter à votre projet
    cargo add codetics-evse-id
    src/main.rs
    use codetics_evse_id::{EvseId, IdType};
    
    fn main() {
        let station = EvseId::station(1);
        let evse = EvseId::evse_of(&station, 2).unwrap();
        assert_eq!(evse.kind(), IdType::Evse);
    
        println!("{}", station.as_str());
        println!("{}", evse.with_operator("FR", "ABC"));
    }
Les limites

Où s'arrête la version 0.1.0

Mieux vaut le savoir avant de construire dessus.

  • Plusieurs instances du serveur

    Le routage des commandes entre nœuds n'est pas encore écrit. Avec plusieurs instances, une commande destinée à une station connectée ailleurs retourne une erreur 409.

  • La gestion des certificats

    Il n'y a pas encore d'intégration avec une autorité de certification ni avec OCSP, donc SignCertificate est rejeté.

  • Les messages de sécurité de la 1.6

    Les messages du livre blanc de sécurité d'OCPP 1.6, comme SecurityEventNotification et SignedUpdateFirmware, ne sont pas encore implémentés.

  • Une première version

    Tout est en version 0.1.0, et nos tutoriels utilisent une borne simulée. Avant un usage réel, faites vos propres essais avec vos bornes.

  • Un projet indépendant

    Ces bibliothèques ne sont ni affiliées à l'Open Charge Alliance, ni approuvées par elle, et nous ne revendiquons aucune certification.

Et EV Supervision Lite ?

EV Supervision Lite, c'est un autre sujet

Les deux viennent de Codetics et les deux parlent aux bornes. Ce sont pourtant deux travaux distincts, et mieux vaut ne pas les confondre, surtout au moment d'acheter.

  • Deux logiciels séparés

    EV Supervision Lite tourne sur Kotlin, parle OCPP 1.6 JSON et n'utilise pas ces bibliothèques.

  • Deux licences différentes

    Ces bibliothèques sont open source, sous Apache-2.0. Le code d'EV Supervision Lite est public, sous la licence d'attribution Codetics 1.0, qui n'est pas une licence open source.

  • Hors de l'offre à 3 000 € HT

    Cette offre couvre la configuration et le déploiement d'EV Supervision Lite. Ces bibliothèques n'en font pas partie.

  • Avec Codetics, sur devis

    Elles sont libres, vous n'avez pas besoin de nous pour vous en servir. Si vous voulez les intégrer à votre système ou construire dessus avec nous, c'est un travail sur devis.

Open source sous Apache-2.0, avec le crédit conservé

Vous pouvez utiliser, modifier et redistribuer ces bibliothèques, y compris dans un produit commercial, en respectant la licence Apache-2.0. Si vous les redistribuez, gardez le fichier NOTICE, qui crédite Kevin Vavelin et Codetics.

Voir le dépôt sur GitHub

Questions fréquentes sur les bibliothèques OCPP en Rust de Codetics

Oui. Codetics publie sur crates.io codetics-ocpp16, codetics-ocpp201 et codetics-ocpp21 : chaque message de la version sous forme de types Rust simples, avec validation et aiguillage typé, sans autre bibliothèque OCPP ni génération de code. Elles sont en version 0.1.0, sous licence Apache-2.0, et compilent à partir de Rust 1.88.

Installez codetics-ocpp-service avec cargo install, qui le compile en quelques minutes, créez une base PostgreSQL, indiquez-la dans DATABASE_URL et lancez le serveur. Déclarez ensuite une station par l'API HTTP, car une station inconnue est refusée avec une erreur 404, puis connectez un simulateur sur le port 9000. Le serveur répond Accepted à son BootNotification.

Oui, elles sont sous licence Apache-2.0 et leur code est public sur GitHub. Vous pouvez les utiliser, les modifier et les redistribuer, à condition notamment de garder le fichier NOTICE qui crédite Kevin Vavelin et Codetics. EV Supervision Lite, un autre produit Codetics, suit une licence d'attribution qui n'est pas une licence open source.

Non. Le projet est indépendant, il n'est ni affilié à l'Open Charge Alliance ni approuvé par elle, et Codetics ne revendique aucune certification. C'est une version 0.1.0 avec des manques écrits : pas de routage des commandes entre instances, pas de gestion des certificats, pas de messages du livre blanc de sécurité de la 1.6. Faites vos propres essais avec vos bornes avant un usage réel.

Aucun lien technique. EV Supervision Lite tourne sur Kotlin, parle OCPP 1.6 JSON et n'utilise pas ces bibliothèques. L'offre à 3 000 € HT couvre la configuration et le déploiement d'EV Supervision Lite, et ces bibliothèques n'en font pas partie. Les intégrer à votre système ou construire dessus avec Codetics est un travail sur devis.
Démarrer

Un problème qui demande de l'ingénierie ?

Parlons-en. Décrivez la situation avec vos mots, même si vous ne savez pas encore comment la résoudre. Votre nom, votre e-mail et quelques lignes suffisent.

  1. 01Vous décrivez le problème
  2. 02Nous en parlons
  3. 03Vous recevez un périmètre et un prix écrits
Parlez-nous de votre défi
Évaluation technique

Le problème est encore flou ?

390 €

Prix fixe, payé en ligne · TVA non applicable, art. 293 B du CGI.

L'évaluation technique vous dit ce qu'il cache et ce que coûterait la suite.

Voir l'évaluation technique