Techに戻る

BootNotificationからHeartbeatまでの実装ポイント

2025-08-02
OCPP 2.0.1 ハンドシェイクのC++17実装手順をコード付きで解説 (5〜10分)

OCPP 2.0.1 で CS (充電器) が CSMS と通信を開始する際、必ず発生するハンドシェイクが BootNotification → Heartbeat です。 本記事では C++17 (Boost.Asio/Beast) での実装例を通じて、以下を解説します。

  1. 接続確立 (WebSocket Upgrade)
  2. BootNotification 送受信と検証
  3. Heartbeat のスケジューリング
  4. タイムアウト & 再接続戦略

1. シーケンス概要

2. 接続確立

connect() 関数で TLS 接続 + WebSocket Upgrade を行います。

cpp
void Client::connect(const tcp::resolver::results_type& endpoints) {
    // SSL handshake
    beast::get_lowest_layer(ws_).connect(endpoints);
    ws_.next_layer().handshake(ssl::stream_base::client);

    // WebSocket handshake
    ws_.handshake(host_, "/ocpp/v201");
}

3. BootNotification 実装

3.1 リクエスト生成

cpp
json make_boot_request() {
    return json::array({
        2, // CALL
        uuid::to_string(uuid::random_generator()()),
        "BootNotification",
        {
            {"chargingStation", {{"model", model_}, {"vendorName", vendor_}}},
            {"reason", "PowerUp"},
            {"reasonTimestamp", now_iso()}
        }
    });
}

3.2 応答ハンドラ

cpp
void on_boot_conf(const json& payload) {
    if (payload["status"] != "Accepted") {
        logger.error("BootNotification rejected");
        return retry_later();
    }
    interval_ = payload["interval"].get<int>();
    schedule_heartbeat();
}

4. Heartbeat スケジューリング

boost::asio::steady_timer を利用して一定間隔で Heartbeat を送信します。

cpp
void schedule_heartbeat() {
    timer_.expires_after(std::chrono::seconds(interval_));
    timer_.async_wait([this](beast::error_code ec) {
        if (ec) return; // cancelled
        send_heartbeat();
        schedule_heartbeat();
    });
}

void send_heartbeat() {
    json req = {
        2, uuid::to_string(uuid::random_generator()()), "Heartbeat", json::object() }
    ws_.async_write(net::buffer(req.dump()),
        beast::bind_front_handler(&Client::on_write, shared_from_this()));
}

応答検証

cpp
void on_heartbeat_conf(const json& payload) {
    auto srv_time = payload["currentTime"].get<std::string>();
    sync_clock(srv_time); // NTP 不要の簡易同期
}

5. タイムアウト & 再接続

  • BootNotification 未応答: boost::asio::deadline_timer で 10 秒を越えたら TCP を切断し、再接続を試行。
  • Heartbeat 連続失敗: 3 回連続失敗で再接続。
  • WebSocket close フレーム受信: 直ちに再接続、BootNotification から再開。
cpp
void Client::fail(beast::error_code ec, std::string_view what) {
    logger.warn("{}: {}", what, ec.message());
    reconnect();
}

6. ログとメトリクス

KPI収集方法
Boot 成功率BootNotification.conf の Accepted カウント / 試行回数
Heartbeat RTT送信時刻と応答時刻の差分を spdlog で追跡
再接続回数fail ハンドラの発火数

Prometheus Exporter を埋め込み、boot_success_totalheartbeat_rtt_seconds を監視することで、フィールドテスト時の接続品質を可視化できます。

7. まとめ

  • BootNotification の interval を厳守し、CSMS の期待する通信量に合わせる
  • Heartbeat タイマーは async_wait → send → 再スケジュール の再帰パターンがシンプル
  • タイムアウトや WebSocket 切断を検知したら BootNotification からリトライ することで状態機械を簡単に保つ

次回は スマート充電 (SetChargingProfile) の実装 を取り上げる予定です。