Techに戻る

C++17でOCPPゲートウェイを構築する

Boost.Asio + WebSocket + JSON でシンプルな OCPP 2.0.1 ゲートウェイを 5〜10 分で理解する

本記事では、OCPP 対応充電器と CSMS の間に配置するゲートウェイを C++17 で開発する際の最小構成例を紹介します。小規模 POC から商用拡張の足がかりになることを目指します。

1. 全体アーキテクチャ

  1. WebSocket Listener: 複数の充電器からの接続を非同期で受け付ける
  2. Router: 受信したメッセージ種別 (messageTypeId) に応じてハンドラーへ振り分け
  3. Handlers: ドメインロジック(検証・変換・DB 書込など)
  4. CSMS Client: CSMS との上り通信。TLS・署名処理を担当

本実装では Boost.Asio / Beast を基盤に据え、JSON 処理に nlohmann/json を利用します。

2. 主要ライブラリ選定

機能ライブラリ理由
非同期 I/OBoost.AsioC++17 標準的、成熟度高い
WebSocketBoost.BeastAsio と親和性、HTTP Upgrade 対応
JSONnlohmann/jsonヘッダオンリー、直感的 API
TLSOpenSSL 1.1+実運用で必須、Beast から直接利用可
ロギングspdlog高速、シングルヘッダオプション有

3. セッションとルーティングの実装例

session.hpp 抜粋:

cpp
class Session : public std::enable_shared_from_this<Session> {
    websocket::stream<tcp::socket> ws_;
    flat_buffer buffer_;
public:
    template <typename... Args>
    Session(Args&&... args) : ws_(std::forward<Args>(args)...) {}

    void start() { do_accept(); }

private:
    void do_accept() {
        ws_.async_accept(
            beast::bind_front_handler(&Session::on_accept, shared_from_this()));
    }
    void on_accept(beast::error_code ec) {
        if (ec) return fail(ec, "accept");
        do_read();
    }
    void do_read() {
        ws_.async_read(buffer_,
            beast::bind_front_handler(&Session::on_read, shared_from_this()));
    }
    void on_read(beast::error_code ec, std::size_t) {
        if (ec == websocket::error::closed) return;
        if (ec) return fail(ec, "read");

        auto msg = beast::buffers_to_string(buffer_.data());
        route_message(msg);
        buffer_.consume(buffer_.size());
        do_read();
    }
};

ルータ例 (router.cpp):

cpp
void Router::route_message(const std::string& raw) {
    auto json = nlohmann::json::parse(raw);
    int frameType = json[0].get<int>();
    if (frameType != 2) return; // CALL のみ処理

    std::string action = json[2];
    if (action == "BootNotification") {
        handlers_.boot(raw);
    } else if (action == "TransactionEvent") {
        handlers_.tx(raw);
    } else {
        logger.warn("Unhandled action {}", action);
    }
}

4. BootNotificationハンドラーの雛形

cpp
void BootHandler::operator()(const std::string& raw) {
    auto j = nlohmann::json::parse(raw)[3]; // payload
    std::string model = j["chargingStation"]["model"];
    std::string vendor = j["chargingStation"]["vendorName"];
    db_.upsert_station(vendor, model);

    // 応答生成
    nlohmann::json res = {
        3, j["msgId"], {
            {"status", "Accepted"},
            {"interval", 300},
            {"currentTime", now_iso()}
        }
    };
    session_.write(res.dump());
}

5. CMake 設定のポイント

CMakeLists.txt スニペット:

cmake
find_package(Boost 1.78 COMPONENTS system thread REQUIRED)
find_package(OpenSSL 1.1 REQUIRED)
add_executable(ocpp-gateway
    main.cpp session.hpp router.cpp handlers.cpp)

target_link_libraries(ocpp-gateway
    Boost::system Boost::thread OpenSSL::SSL OpenSSL::Crypto)
set_property(TARGET ocpp-gateway PROPERTY CXX_STANDARD 17)

6. テスト & シミュレーション

  • Virtual Charge Point: https://github.com/solidstudiosh/ocpp-virtual-charge-point を利用し、BootNotification → Heartbeat → TransactionEvent の一連を送信。
  • ローカルで ./ocpp-gateway を起動し、Wireshark で WebSocket フレームを確認。
  • 単体テスト: GoogleTest で Router のルーティングテーブルを検査。

7. 今後の拡張ポイント

  1. セキュリティ Whitepaper 準拠:JWS 署名検証・鍵ローテーション API の実装
  2. スマート充電SetChargingProfile ハンドラーとスケジューラ追加
  3. メトリクス:Prometheus Exporter でレイテンシとメッセージ数を可視化
  4. マルチスレッド化boost::asio::thread_pool でコア数に合わせてセッションを分散

各ポイントの補足

セキュリティ Whitepaper 準拠

OCPP 2.0.1 Security Whitepaper に沿って JWS 署名検証や鍵ローテーション API を実装し、改ざん検出と通信の完全性を担保します。

スマート充電

SetChargingProfile / ClearChargingProfile を解釈し、ピークカットや最適料金に合わせて出力電流を動的制御するスケジューラを追加します。

メトリクス

Boost.Beast で /metrics エンドポイントを公開し、Prometheus 形式でレイテンシやメッセージレートを可視化します。

マルチスレッド化

boost::asio::thread_pool を導入し、I/O とドメインロジックを分離して CPU コア数に合わせてスケールさせます。

参考リポジトリ

サンプル実装を GitHub で公開しています。ビルド手順や詳細ドキュメントもあるので、ぜひご覧ください。

github.com/yuu999/ocpp-gw

これで 「シングルバイナリで動く最小ゲートウェイ」 の骨格が完成します。次の記事では、OCPP 2.0.1 のセキュリティ対策を具体的に解説します。