【vsomeip】ラズパイ同士でSOMEIP通信をさせる
AUTOSARの悪口を見ていたら、vsomeipを使ってSOMEIP通信することができると書いてあったので試してみました(かなり生々の内容で体裁が非常に汚いのはご勘弁を)。
https://www.reddit.com/r/embedded/comments/leq366/comment/gmiq6d0/
SOMEIP通信とは
車載ソフトの規格団体であるAUTOSARによって策定された通信プロトコル。SOA(Service Orientied Archtecture)の思想のもとに策定されている。
Vectorの説明資料が一番わかり易いです:
https://www.vector.com/jp/ja/know-how/vj-beginners/someip/
ここからは自分の感想になりますが、SOAというだけあり、システムを「Service」の集合体として扱うのが特徴です。
SOA
例えば、ECU1とECU2の通信を考えます。
-
ECU1にはカメラと温度センサがありこれらを制御するアプリ(カメラアプリ、温度センサアプリ)がある
-
ECU2には複数のアプリがあり、カメラと温度の情報を使用する
とします。
このときカメラアプリはカメラ画像を提供するためのIFの集合体を、温度アプリは温度を提供するためのIFの集合体を提供します。これらはそれぞれServiceと呼ばれます。Serviceを提供するアプリは他のECUにOfferしてServiceをどこで(EndPoint情報)提供しているかを知らせます。
一方でECU2のアプリは、必要なServiceをFindします。Offerが届いたらEndPointに対して通信を確立します。
ここでSOAの特徴として挙げられるのは、
-
アプリは、Service単位でIFを持ち、複数のServiceを提供することができる
-
反対に、アプリは必要なServiceを発見しIFにアクセスすることができる
-
Serviceを提供するアプリは、誰が自分の情報を取得しているかに興味をもたない(セキュリティ的に制限することもできる)
-
疎結合なのでServiceの追加・削除が容易
SOMEIP通信には大きく以下の通信方式があります:
-
Method Request & Response
-
クライアントからのRequestに対してサーバがResponseを送信する
-
例)クライアントから画像提供要求・サーバから応答
-
Method Fire & Forget
-
クライアントから一方的に通信を投げつける
-
例)クラインアントの状態を定期的にサーバに伝える
-
Event
-
サーバから一方的に通信を投げつける
-
例)定期的に温度情報を送信する
クライアントはEventを受信するには、Subscribeというパケットを送信必要があります。購読という言葉通りで、自分はこのEventを購読します!と宣言します。サーバは妥当性を検討してSubscribeに対しACKまたはNACKを返します。
SOMEIPにおいてServiceやIFを識別するために各種IDが使用されます
-
ServiceID
-
Serviceを一位に識別するID(カメラサービス・温度サービス)
-
InstanceID
-
ServiceのInstance(実体)を識別するためのID
-
ServiceにはMajor VersionとMinor Versionがあり、Major Versionを変更した際はInstanceを分けることが推奨されている
-
Method ID
-
提供するMethodを識別するためのID
-
EventGroup・EventID
-
提供するEventGroup・Eventを識別するためのID
SOMEIP通信のシーケンス
ここに詳しく書かれていますが、、、ARP解決等はされている前提とします。
ざっくりした順序はSOMEIP-SD(Service Discovety)→SOMEIP通信となります。SOMEIP通信はL4(トランスポート層)にUDP及びTCPを使用することができますが、今回はTCPを使う場合を考えます(Service DiscoveryはUDPで実施されます)
-
【クライアント・サーバ】IGMPプロトコルによってマルチキャストIPにJoinする
-
【クライアント】Find Serviceを送信する(ServiceID, InstanceID)
-
【サーバ】Offer Seviceを送信する(ServiceID, InstanceID, EndPoint情報を含む)
-
【クライアント・サーバ】TCPの3ウェイハンドシェイクを実施
-
【クライアント】Subscribeを送信
-
【サーバ】Subscribe ACK/NACKを送信
-
【サーバ・クライアント】SOMEIP通信を実施
vsomeipによるラズパイ同士のSOMEIP通信
COVESAは神
コネクテッドビークルシステムズアライアンス(COVESA)は、以前はGENIVIアライアンスとして知られており、コネクテッドビークルに搭載されているオペレーティングシステムとミドルウェア、および関連するクラウドサービスを統合するためのリファレンスアプローチを開発する非営利の自動車業界アライアンスです。
環境
使用した機材
-
Raspberry Pi 4 Model B/4GB(ラズパイ4B) 2台
-
Raspberry Pi OSをインストール
-
Dockerを使えば2台用意しなくていいらしい
-
M2 Macbook Air
失敗した例
ラズパイでSOMEIP通信と書いたけど、色々(本質的でないことで)試行錯誤しました。ここではうまく行かなかったことを記載します。
ラズパイ4BとUTM上のUbuntuでのSOMEIP通信**
ラズパイ4Bは1台しか持っていなかったので、最初はラズパイとMacbook上のVMでSOMEIP通信させようとしていました(今思うとUbuntuではなくMacで動かせばよかったかも)。フィアウォールを突破できず失敗
ラズパイ4BとラズパイZero-WのSOMEIP通信**
ラズパイZero-Wではスペック的にvsomeipのビルド環境が作れませんでした。クロスコンパイルも試したのですがうまく行かず断念
ラズパイ2台でvsomeip通信させる手順
手元にSSHできるラズパイがあるものとします。手順はほぼここに書いてあります。
必須のパッケージをインストールする
$ sudo apt install -y build-essential cmake git libboost-all-dev
gitからvsomeipのソースをクローンする
$ git clone https://github.com/COVESA/vsomeip.git
$ cd vsomeip
ここからは上記リンク「準備/前提条件」からの手順通りの操作になるので省略。
例にはサンプルコードも含まれており、同じ端末の別ターミナルでサーバとクライアントを立ち上げると通信ができます。
注意点としてここでの通信はプロセス間通信であること。パケットのキャプチャをすることはできません(なので厳密にSOMEIP通信というのは微妙かも。AUTOSARのara::comがするようなSOMEIP通信に準拠したIPC通信となりそう)。
通信をお外に出すためにはさらに操作が必要になります。
SOMEIP通信(UDP)を使用
お外との通信では使用するネットワークインターフェースやIPを定義したJSONを置く必要があります。ここではSOMEIP通信にUnreliable=UDP通信を使用することにしています。
サーバとなるラズパイに以下のservice.jsonを置きます:
{
"unicast" : "192.168.11.29",
"network" : "wlan0",
"logging" :
{
"level" : "debug",
"console" : "true",
"file" : { "enable" : "false", "path" : "/tmp/vsomeip.log" },
"dlt" : "false"
},
"applications" :
[
{
"name" : "World",
"id" : "0x1212"
}
],
"services" :
[
{
"service" : "0x100a",
"instance" : "0x0001",
"unreliable": "30509",
"event-groups": [
{ "event-group": "0x8001",
"events": ["0x8001"]}
]
},
{
"service" : "0x100a",
"instance" : "0x0001",
"reliable": "30508"
}
],
"routing" : "World",
"service-discovery" :
{
"enable" : "true",
"multicast" : "224.244.224.245",
"port" : "30490",
"protocol" : "udp",
"initial_delay_min" : "0",
"initial_delay_max" : "10",
"repetitions_base_delay" : "200",
"repetitions_max" : "3",
"ttl" : "3",
"cyclic_offer_delay" : "2000",
"request_response_delay" : "1500"
}
}
クライアントとなるラズパイにはclient.jsonを置きます
{
"unicast" : "192.168.11.33",
"network" : "wlan0",
"logging" :
{
"level" : "debug",
"console" : "true",
"file" : { "enable" : "false", "path" : "/var/log/vsomeip.log" },
"dlt" : "false"
},
"applications" :
[
{
"name" : "Hello",
"id" : "0x1313"
}
],
"routing" : "Hello",
"service-discovery" :
{
"enable" : "true",
"multicast" : "224.244.224.245",
"port" : "30490",
"protocol" : "udp",
"initial_delay_min" : "10",
"initial_delay_max" : "100",
"repetitions_base_delay" : "200",
"repetitions_max" : "3",
"ttl" : "3",
"cyclic_offer_delay" : "2000",
"request_response_delay" : "1500"
}
}
~/.basrcに以下を仕込んで設定を恒久化させます(サーバ側)
export LD_LIBRARY_PATH=$HOME/work/vsomeip/build:$LD_LIBRARY_PATH
export VSOMEIP_CONFIGURATION=$HOME/work/vsomeip/test_example/service.json
export VSOMEIP_APPLICATION_NAME=World
※ source ~/.bashrcを忘れずに
次が詰まりポイントだと思うのですが、ラズパイのルーティングテーブルにマルチキャストIPを追加します。これを忘れるとマルチキャストIP宛のパケットが出ません。
$ sudo ip route add 224.244.224.0/24 dev eth0
サーバ側のソースコードは以下のようにしました。サーバ側は3秒ごとにEventを送信させています。また、MethodのResponseに対しRequestを返すようにしています。
#include
#include
#include
#include
#include
#include
#include
#define SAMPLE_SERVICE_ID 0x100a
#define SAMPLE_INSTANCE_ID 0x0001
#define SAMPLE_METHOD_ID 0x0005
#define SAMPLE_METHOD_ID_FF 0x0006
#define SAMPLE_EVENTGROUP_ID 0x8001
#define SAMPLE_EVENT_ID 0x8001
using namespace vsomeip_v3::logger;
std::shared_ptr app;
std::shared_ptr payload;
std::atomic running{true};
// Method 受信時
void on_message(const std::shared_ptr &_request) {
std::cout req_payload = _request->get_payload();
if (req_payload) {
const vsomeip::byte_t* data = req_payload->get_data();
std::size_t length = req_payload->get_length();
std::string json_str(reinterpret_cast(data), length);
std::cout its_response =
vsomeip::runtime::get()->create_response(_request);
std::shared_ptr resp_payload =
vsomeip::runtime::get()->create_payload();
std::string text = "This is the server.";
std::vector its_payload_data(text.begin(), text.end());
resp_payload->set_data(its_payload_data);
its_response->set_payload(resp_payload);
app->send(its_response);
}
// Method Fire & Foreget 受信時に呼び出し
void on_message_ff(const std::shared_ptr &_request) {
std::cout its_payload = _request->get_payload();
if (its_payload) {
const vsomeip::byte_t* data = its_payload->get_data();
std::size_t length = its_payload->get_length();
// string に変換
std::string json_str(reinterpret_cast(data), length);
std::cout (c) };
payload = vsomeip::runtime::get()->create_payload();
payload->set_data(its_data, sizeof(its_data));
app->notify(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENT_ID, payload);
std::cout its_groups;
its_groups.insert(SAMPLE_EVENTGROUP_ID);
app = vsomeip::runtime::get()->create_application("World");
if (!app->init()) {
message(level_e::LL_INFO) register_message_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_METHOD_ID, on_message);
app->register_message_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_METHOD_ID_FF, on_message_ff);
app->offer_event(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENT_ID, its_groups);
app->offer_service(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID);
std::cout start();
return 0;
}
クライアント側のソースコードです。key a押下でMethodのRequestを、b押下でMethod のFire & Forgetが飛びます。
#include
#include
#include
#include
#include
#include
#include
// 各ID
#define SAMPLE_SERVICE_ID 0x100a
#define SAMPLE_INSTANCE_ID 0x0001
#define SAMPLE_METHOD_ID 0x0005
#define SAMPLE_METHOD_ID_FF 0x0006
#define SAMPLE_EVENTGROUP_ID 0x8001
#define SAMPLE_EVENT_ID 0x8001
std::shared_ptr app;
std::mutex mutex;
std::condition_variable condition;
std::atomic running{true}; // Ctrl+Cで終了
// Method Request送信
void send_request() {
std::shared_ptr request = vsomeip::runtime::get()->create_request();
request->set_service(SAMPLE_SERVICE_ID);
request->set_instance(SAMPLE_INSTANCE_ID);
request->set_method(SAMPLE_METHOD_ID);
// ペイロード作成
std::string text = "Who are you?";
std::shared_ptr its_payload = vsomeip::runtime::get()->create_payload();
std::vector its_payload_data(text.begin(), text.end());
its_payload->set_data(its_payload_data);
request->set_payload(its_payload);
std::cout send(request);
}
// Fire & Forget 送信
void send_fire_and_forget() {
std::shared_ptr fireforget = vsomeip::runtime::get()->create_request();
fireforget->set_service(SAMPLE_SERVICE_ID);
fireforget->set_instance(SAMPLE_INSTANCE_ID);
fireforget->set_method(SAMPLE_METHOD_ID_FF);
fireforget->set_message_type(vsomeip::message_type_e::MT_REQUEST_NO_RETURN);
std::shared_ptr its_payload = vsomeip::runtime::get()->create_payload();
std::string text = "Hello!";
std::vector its_payload_data(text.begin(), text.end());
its_payload->set_data(its_payload_data);
fireforget->set_payload(its_payload);
std::cout send(fireforget);
}
// Method Response受信時
void on_message(const std::shared_ptr &_response) {
std::shared_ptr its_payload = _response->get_payload();
if (its_payload) {
const vsomeip::byte_t* data = its_payload->get_data();
std::size_t length = its_payload->get_length();
// string に変換
std::string json_str(reinterpret_cast(data), length);
std::cout &_response) {
std::cout its_groups;
its_groups.insert(SAMPLE_EVENTGROUP_ID);
// Subscribe開始
app->request_event(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENT_ID, its_groups);
app->subscribe(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENTGROUP_ID);
std::cout register_message_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_METHOD_ID, on_message);
app->register_message_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENT_ID, on_message_event);
}
// キーボード入力監視スレッド
// key aでリクエスト・bでFire&Forget送信
void input_thread() {
char key;
while (running) {
std::cin.get(key);
if (key == 'a') {
send_request();
} else if (key == 'b') {
send_fire_and_forget();
}
}
}
int main() {
app = vsomeip::runtime::get()->create_application("Hello");
app->init();
app->register_availability_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, on_availability);
app->request_service(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID);
// キーボード入力スレッド開始
std::thread input_worker(input_thread);
input_worker.detach();
app->start(); // vsomeip メインループ開始
running = false;
}
SOMEIP通信のシーケンス(パケット)の説明
実際にtcpdumpで取得したパケットをWireSharkで見ていきましょう!
- IGMPv3プロトコルでマルチキャストIPにJoinします
377 14.661597 192.168.11.29 224.0.0.22 IGMPv3 54 Membership Report / Join group 224.244.224.245 for any sources
- ClientはFindパケットでServiceを探していることを通知します(クライントは先にOfferを受信するとFindを送信しません)
271 9.187558 192.168.11.33 224.244.224.245 SOME/IP-SD 86 SOME/IP Service Discovery Protocol [Find] ?
- SOMEIP-SDプロトコルのEntriesArrrayには探しているServiceのIDとInstanceIDが含まれます
Find Service Entry (Service ID 0x100a, Instance ID 0x0001, Version ANY.ANY, Options: None)
- ServerはOfferパケットでService提供を通知します
354 14.508403 192.168.11.29 224.244.224.245 SOME/IP-SD 98 SOME/IP Service Discovery Protocol [Offer]
- SOMEIP-SDプロトコルのEntriesArrayには提供しているServiceのIDとInstanceIDが含まれ、
Offer Service Entry (Service ID 0x100a, Instance ID 0x0001, Version 0.0, Options: 0-0)
- SOMEIP-SDプロトコルのOptinsArratにはEndPoint情報が含まれます
0: IPv4 Endpoint Option (192.168.11.29:30509 (UDP))
- ClinetはSubscribeを送信します
357 14.518243 192.168.11.33 192.168.11.29 SOME/IP-SD 98 SOME/IP Service Discovery Protocol [Subscribe]
- ServerはSubscribeACKを送信します
358 14.518566 192.168.11.29 192.168.11.33 SOME/IP-SD 86 SOME/IP Service Discovery Protocol [SubscribeAck]
- ServerからのEvent送信が始まります
408 17.509306 192.168.11.29 192.168.11.33 SOME/IP 59 SOME/IP Protocol (Service ID: 0x100a, Method ID: 0x8001, Length: 9)
- ClientからのMethod Requestに対し、サーバはResponseを返します
492 22.949609 192.168.11.33 192.168.11.29 SOME/IP 70 SOME/IP Protocol (Service ID: 0x100a, Method ID: 0x0005, Length: 20)
493 22.954979 192.168.11.29 192.168.11.33 SOME/IP 77 SOME/IP Protocol (Service ID: 0x100a, Method ID: 0x0005, Length: 27)
- クライアント側のコンソールの表示はこんな感じ
CLIENT: Received Event Message!!
a
[CLIENT] Sending Request (method call)
CLIENT: Method Response Payload = This is the server.
CLIENT: Received Method Response from Server
CLIENT: Received Event Message!!
b
[CLIENT] Sending Fire&Forget
この後TCP通信をさせた様子を追記予定。また、C++の学習にもなったのでコードの解説も書きたい
