またまた久しぶりの更新になってしまいました。今回のテーマは三菱PLCとイーサネット通信。将来的にはRaspberry Piなどの非WindowsのからPLCへアクセスしたいと考えています。
まずは通信の確認。Perlで通信確認をしているブログがあるのでこれを参考にしました。Raspberry Piで動作させるならPerlでも十分なのですが、マイコンでも動作させることを考えて、C++で作成しました。
このサイトを参考ししています。
Perlで三菱シーケンサーとSocket通信出来上がったプログラムはGistに載せました。
melsec_test.cppやっていることは、UDP接続でD300~D302のデータメモリを受信をリクエストして、返答を受信するという内容です。基本的に参考にしたサイトの内容をC++に置き換えているだけですが、通信方式はUDPにしたところが違います。
環境ですが、MacbookにUSB接続優先LANでPLCと接続しています。UDP通信にはAsio C++ Libraryを使っています。Asioはhppファイルのみで構成されているので、ファイルをダウンロードして、適当なフォルダに移して、コンパイル時にインクルードするだけで使用できます。コンパイルはこのような感じに。
clang++ -std=c++11 -stdlib=libc++ -g -O0 -W -I../../source/asio-1.12.2/include melsec_test.cpp -o melsec_test
プログラムの内容について説明していくと
Boostに依存したくない場合はインクルードする前に、ASIO_STANDALONEマクロを実行する必要があるとのこと。
#define ASIO_STANDALONE
#include <iostream>
#include <asio.hpp>
テストなのでMCプロトコルの受信コマンドを文字列で定義します。コマンドの詳細については「Perlで三菱シーケンサーとSocket通信」を確認してください。
// 送信コマンド
std::string cmd = "500000FFFF03000C001000010400002C0100A80300";
std::cout << "send: " << cmd << std::endl
array配列を作って、文字列をバイナリデータに変換しています。perlだとpack関数があるようですが、ここでは自分で作りました。(もしC++に便利なライブラリがあるのなら教えていただきたいです。)
//16進数アスキーを数値に変換
char hex2char(char h)
{
if (h >= '0' && h <= '9')
return h - '0';
else if (h >= 'A' && h <= 'F')
return h - 'A' + 10;
else if (h >= 'a' && h <= 'f')
return h - 'a' + 10;
return 0;
}
文字を一文字ずつ数値に変換して、char一つについて2つの数値が入るように詰めて配列に入れています。16進数の文字列を0〜15に変換する関数も作りました。
// 送信コマンドをバイナリデータへ変換
std::array<unsigned char, 128> send_buf;
int cnt;
for (size_t i = 0; i < cmd.size(); i += 2)
{
cnt = i / 2;
send_buf[cnt] = (hex2char(cmd[i]) << 4) + hex2char(cmd[i + 1]);
;
}
int send_buf_len = cnt + 1;
ASIOの送信部分です。非同期I/Oオブジェクトを作って、送信先のアドレスとポートをエンドポイントに設定し、ソケットを開き、データをバッファに入れて送信。という流れです。ASIOについてはあまり理解していないので引き続き勉強が必要です。
// UDP送信
std::string const host = "192.168.0.10";
short const port = 1025;
asio::io_context context;
asio::ip::udp::endpoint endpoint(
asio::ip::address::from_string(host), port);
asio::ip::udp::socket socket(context);
socket.open(asio::ip::udp::v4());
s
ちなみにTCP送信のときは次のようにすればできます。(PLC側にTCPポートを1026に登録)
// TCP送信
std::string const host = "192.168.0.10";
short const port = 1026;
asio::io_context context;
asio::ip::tcp::endpoint endpoint(
asio::ip::address::from_string(host), port);
asio::ip::tcp::socket socket(context);
asio::connect(socket,&endpoint);
socket.write_some(asio::buffer(send_buf, send_buf_len));
次にUDPからの受信。送信とは逆に配列を作って、受信バッファから配列に入れます。
// UDP受信
std::array<unsigned char, 128> recv_buf;
asio::ip::udp::endpoint sender_endpoint;
size_t recv_buf_len = socket.receive_from(
asio::buffer(recv_buf), sender_endpoint);
最後に受信したバイナリデータを文字列に変えていきます。Perlではunpack関数があるのですが、ここでも自作しました。配列を一つずつ見ていきcharの上位と下位をそれぞれ文字列に変換します。
// 受信バイナリデータを文字列に変換
std::string result = "";
for (size_t i = 0; i < recv_buf_len; i++)
{
result += char2hex((recv_buf[i] >> 4) & 0xf);
result += char2hex(recv_buf[i] & 0xf);
}
数値を文字にする関数は下記のとおりです。
//0から15までの数値をアスキーに変換
char char2hex(char h)
{
if (h >= 0 && h <= 9)
return h + '0';
else if (h >= 10 && h <= 15)
return h - 10 + 'A';
return 0;
}
実行結果は下記の通り。Perlと同じ結果になりました。
send: 500000FFFF03000C001000010400002C0100A80300
rev : D00000FFFF03000800000001000A006400
次回以降、コマンドの構造体を作り、ライブラリで使えるようにしていきたいです。