てぃってぃの楽しい副業日記!

ワイと嫁(てぃってぃ)夫婦の、副業(ブログ運営)、プログラミング、育児、ゲーム等を綴ったブログです☆

【C#】三菱PLCとのMCプロトコルによる通信アプリを作る(メモリ読み書き)

C#の実装メモ。

TCPにて、三菱製PLC(内蔵Ethernetポート)とMCプロトコルにて通信を行い、GUI上でPLCのメモリをスムーズに読み書きできるアプリケーションを作製しました。ほぼ製品レベルのクオリティですが、ソースコード全文をほとんどそのまま公開します。参考にしてもらえれば、様々な通信系アプリに応用できると思います。(私は慣れているので1日程度で作成できましたが、慣れていないと、マネして作るだけでも1週間はかかると思います)

「キーワード」の章をご参照頂き、そこに読みたい情報があれば参考にしていただけると幸いです。主に画面処理(delegate、invokeなどのコールバック系/各種ボタンのイベントハンドラ)、通信処理(TCPクライアント)あたりで、色々参考になるかと思います。


開発環境/動作環境

開発環境(WindowsPC)

  • Windows(7以降)
  • VisualStudio2017
  • C#
  • .Net4.7

動作環境

(1) PCは、.Net4.7インストール済みPC(Windows7/Windows10)で動作します。
(2) 三菱PLC側は、「Q**CPU」系統で、CPUユニット内蔵のEthernet内蔵のCPUのみ通信可能です(通信コマンド「QnA互換3Cフレーム」を使用するため)。ただし、ちょこっと改造して、「QnA互換3Eフレーム」に対応すれば、Ethernetユニットとも通信可能に出来ます。(暇があればやるかも。)

完成画面(動作イメージ)

下記画面にて、三菱PLC(CPUユニット内蔵Ethernetポート)のIPアドレス、ポート番号を設定して、三菱PLCとリアルタイムに通信を行います。通信は以下2種類の処理を行います。

(1) 指定したメモリアドレスの値を読み出し、画面に表示します。
(2) 画面で編集した値をPLCに書き込みます。

C#三菱PLCとの通信テスター(動作画面)
三菱PLC通信テスター 動作画面

キーワード

ハードウェア関連

  • 三菱PLC
  • Ethernet

画面処理関連

  • iniファイル読込/書込
  • イベントハンドラ(invoke / delegate)
  • dataGridView(データグリッドビュー)

通信処理関連

  • C# / TCPクライアント
  • MCプロトコル
  • QnA互換3Cフレーム
  • メモリ読み書き

実装内容(ソースコード)

1. 画面クラス(Form1.cs)

1-1. 画面レイアウト

クラスの名前、デフォルトだった…(笑)。画面のレイアウトと各コントロール名は以下の様につけています。「GuiMelsecData」は「dataGirdView(データグリッドビュー)」というコントロールを使用しています。

三菱PLC通信テスター
メイン画面とコントロール名

1-2. ソースコード

メイン画面の処理部分のソースコードです。デザイナ―部分は割愛しますが、画面レイアウトと同じコントロール名にすれば、以下のソースがそのまま使えるかと思います。画面処理部分のソースを丸々載せておきます。

using System;
using System.Windows.Forms;

namespace MelsecMonitor
{
    public partial class Form1 : Form
    {
        /* 定数 */
        private readonly int COL_SIZE = 4;

        /* メンバー */
        private bool m_isRunning = false;
        private bool m_isEditing = false;
        private MelsecMgr m_melsecMgr;

        public Form1()
        {
            InitializeComponent();

            // iniファイル読込
            ReadConfig();

            // 通信アドレス読込
            GuiReadArea.SelectedItem = Config.memArea;
            GuiReadAddress.Text = Config.memStartAddr.ToString();
            GuiReadWordNum.Text = Config.memWordNum.ToString();

            // 通信ハンドル
            m_melsecMgr = new MelsecMgr();
            m_melsecMgr.m_onConnect += new MelsecMgr.ConnectEventHandler(OnDisconnect);
            m_melsecMgr.m_onRecv += new MelsecMgr.RecvEventHandler(OnRecv);
        }

        /// <summary>
        /// iniファイル読込
        /// </summary>
        private void ReadConfig()
        {
            // iniファイル読込
            Config.ReadConfig();

            // 接続先IPアドレス
            string serverIpAddress = Config.serverIpAddress;
            string[] ip = serverIpAddress.Split('.');
            GuiServerIp1.Text = ip[0];
            GuiServerIp2.Text = ip[1];
            GuiServerIp3.Text = ip[2];
            GuiServerIp4.Text = ip[3];

            // 接続先ポート番号
            int serverPort = Config.serverPort;
            GuiServerPort.Text = serverPort.ToString();

            // 接続先ユニット種別
            int serverUnitType = Config.serverUnitType;
            GuiServerUnitType.SelectedIndex = serverUnitType;

            // 通信周期
            int commCycle = Config.commCycle;
            GuiCommCycle.Text = commCycle.ToString();

            // 16進表記
            GuiHexCheck.Checked = Config.hexShow;
        }

        /// <summary>
        /// iniファイル書込
        /// </summary>
        private void WriteConfig()
        {
            try
            {
                // 画面から設定値を取得
                string serverIpAddress = $"{GuiServerIp1.Text}.{GuiServerIp2.Text}.{GuiServerIp3.Text}.{GuiServerIp4.Text}";
                int serverPort         = Int32.Parse(GuiServerPort.Text);
                int serverUnitType     = GuiServerUnitType.SelectedIndex;

                // iniファイル書込
                Config.WriteConfig(serverIpAddress, serverPort, serverUnitType);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        /// <summary>
        /// 「設定値読込」ボタン クリックイベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiIniReadButton_Click(object sender, EventArgs e)
        {
            ReadConfig();
        }

        /// <summary>
        /// 「設定値書込」ボタン クリックイベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiIniWriteButton_Click(object sender, EventArgs e)
        {
            WriteConfig();
        }

        /// <summary>
        /// 「通信開始」ボタン クリックイベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiStartButton_Click(object sender, EventArgs e)
        {
            // 停止中
            if (!m_isRunning)
            {
                // コントロールの無効化
                GuiMelsecData.Enabled          = true;
                GuiIniReadButton.Enabled    = false;
                GuiIniWriteButton.Enabled   = false;
                GuiServerIp1.Enabled        = false;
                GuiServerIp2.Enabled        = false;
                GuiServerIp3.Enabled        = false;
                GuiServerIp4.Enabled        = false;
                GuiServerPort.Enabled       = false;
                GuiServerUnitType.Enabled   = false;

                // ユニット設定
                MelsecMgr.unitType unitType = MelsecMgr.unitType.NONE;
                if (GuiServerUnitType.Text.StartsWith("1 :"))
                {
                    unitType = MelsecMgr.unitType.E71;
                }
                string deviceCode = GuiReadArea.Text;
                int startAddr     = Int32.Parse(GuiReadAddress.Text);
                short readWordNum = Int16.Parse(GuiReadWordNum.Text);
                int commCycle     = Int32.Parse(GuiCommCycle.Text);

                if (m_melsecMgr.SetUnitInfo(unitType, deviceCode, startAddr, readWordNum, commCycle))
                {
                    // 通信開始
                    string serverIpAddress = $"{GuiServerIp1.Text}.{GuiServerIp2.Text}.{GuiServerIp3.Text}.{GuiServerIp4.Text}";
                    int serverPort = Int32.Parse(GuiServerPort.Text);
                    if (m_melsecMgr.StartMelsec(this, serverIpAddress, serverPort))
                    {
                        GuiStartButton.Text = "通信停止";
                        GuiReadCheck.Enabled = true;
                        GuiReadCheck.Checked = true;
                        m_isRunning = true;
                    }
                }
            }
            else
            {
                // 通信停止処理
                m_melsecMgr.StopMelsec();

                // 通信停止が完了するまでボタン操作不可にする
                GuiStartButton.Enabled = false;
            }
        }

        /// <summary>
        /// 通信スレッドからの切断通知
        /// </summary>
        private void OnDisconnect()
        {
            // コントロールの有効化
            GuiMelsecData.Enabled          = false;
            GuiIniReadButton.Enabled    = true;
            GuiIniWriteButton.Enabled   = true;
            GuiServerIp1.Enabled        = true;
            GuiServerIp2.Enabled        = true;
            GuiServerIp3.Enabled        = true;
            GuiServerIp4.Enabled        = true;
            GuiServerPort.Enabled       = true;
            GuiServerUnitType.Enabled   = true;

            // 通信開始許可
            GuiStartButton.Text    = "通信開始";
            m_isRunning            = false;
            GuiReadCheck.Checked   = false;
            GuiReadCheck.Enabled   = false;
            GuiStartButton.Enabled = true;
        }
        
        /// <summary>
        /// 通信スレッドからの受信通知
        /// </summary>
        private void OnRecv(string strTeleLog)
        {
            int offset = 0;
            int column = 0;
            int row = 0;

            // 4文字ずつ区切る
            while (offset < strTeleLog.Length)
            {
                string data = strTeleLog.Substring(offset, 4);

                // 10進表記の場合は変換
                if (!GuiHexCheck.Checked)
                {
                    Int16 tmpBin = Int16.Parse(data, System.Globalization.NumberStyles.HexNumber);
                    data = tmpBin.ToString();
                }

                // データグリッドに反映
                GuiMelsecData[column, row].Value = data;

                // 更新位置
                column++;
                if (column >= COL_SIZE)
                {
                    column = 0;
                    row++;
                }

                offset += 4;
            }
        }

        /// <summary>
        /// 「自動更新」チェックボックス(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiReadCheck_CheckedChanged(object sender, EventArgs e)
        {
            if (GuiReadCheck.Checked)
            {
                int wordNum = 0;
                int startAddr = 0;
                int columnNum = COL_SIZE;
                int rowNum = 0;

                // データグリッド初期化
                GuiMelsecData.Rows.Clear();
                GuiMelsecData.Columns.Clear();

                // 読出ワード数の判定
                if (!Int32.TryParse(GuiReadWordNum.Text, out wordNum))
                {
                    GuiReadCheck.Checked = false;
                    return;
                }
                // 読出開始アドレスの判定
                if (!Int32.TryParse(GuiReadAddress.Text, out startAddr))
                {
                    GuiReadCheck.Checked = false;
                    return;
                }

                // コントロール操作禁止
                GuiReadArea.Enabled    = false;
                GuiReadWordNum.Enabled = false;
                GuiReadAddress.Enabled = false;
                GuiCommCycle.Enabled   = false;

                // データグリッド列の設定
                if (wordNum < COL_SIZE)
                {
                    columnNum = wordNum;
                }
                for (int i = 0; i < columnNum; i++)
                {
                    GuiMelsecData.Columns.Add($"data{i}", $"+{i.ToString()}");
                    GuiMelsecData.Columns[i].SortMode = DataGridViewColumnSortMode.NotSortable;
                    GuiMelsecData.Columns[i].Width = 45;
                    GuiMelsecData.Columns[i].DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
                }
                GuiMelsecData.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;

                // データグリッド行の設定
                rowNum = (wordNum - 1) / COL_SIZE + 1;
                for (int i = 0; i < rowNum; i++)
                {
                    int Offset = startAddr + i * COL_SIZE;
                    GuiMelsecData.Rows.Add();
                    GuiMelsecData.Rows[i].HeaderCell.Value = GuiReadArea.Text + Offset.ToString();
                    GuiMelsecData.RowHeadersWidth = 80;
                }
                GuiMelsecData.RowHeadersWidth = 80;
                GuiMelsecData.RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;

                // 初期データ表示(欠測)
                for (int i = 0; i < columnNum; i++)
                {
                    for (int j = 0; j < rowNum; j++)
                    {
                        GuiMelsecData[i, j].Value = "****";
                    }
                }

                // 三菱PLC通信クラス データ読み出しON
                m_melsecMgr.m_IsReadData = true;
            }
            else
            {
                // コントロール操作許可
                GuiReadArea.Enabled    = true;
                GuiReadWordNum.Enabled = true;
                GuiReadAddress.Enabled = true;
                GuiCommCycle.Enabled   = true;
            }
        }

        /// <summary>
        /// 「エリア」(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiReadArea_SelectedIndexChanged(object sender, EventArgs e)
        {
            Config.WriteReadArea(GuiReadArea.SelectedItem.ToString());
        }

        /// <summary>
        /// 「先頭アドレス」(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiReadAddress_TextChanged(object sender, EventArgs e)
        {
            Config.WriteReadStartAddr(Int32.Parse(GuiReadAddress.Text));
        }

        /// <summary>
        /// 「ワード数」(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiReadWordNum_TextChanged(object sender, EventArgs e)
        {
            Config.WriteReadWordNum(Int32.Parse(GuiReadWordNum.Text));
        }

        /// <summary>
        /// 「通信周期」(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiCommCycle_TextChanged(object sender, EventArgs e)
        {
            Config.WriteCommCycle(Int32.Parse(GuiCommCycle.Text));
        }

        /// <summary>
        /// 「16進表記」(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GuiHexCheck_CheckedChanged(object sender, EventArgs e)
        {
            Config.WriteHexShow(GuiHexCheck.Checked);
        }

        /// <summary>
        /// データグリッド値変更開始(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MelsecData_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
        {
            // TextBoxの編集のみ
            if (e.Control is DataGridViewTextBoxEditingControl)
            {
                // ユーザー手入力開始
                m_isEditing = true;

                // TextBoxのコントロールを取得
                DataGridViewTextBoxEditingControl textBox = (DataGridViewTextBoxEditingControl)e.Control;

                // イベントハンドラを削除
                textBox.KeyPress -= new KeyPressEventHandler(MelsecData_KeyPress);

                // 該当列
                DataGridView dgv = (DataGridView)sender;
                if (((DataGridView)sender).CurrentCell.OwningColumn.Name.StartsWith("data"))
                {
                    // イベントハンドラを追加
                    textBox.KeyPress += new KeyPressEventHandler(MelsecData_KeyPress);
                }
            }
        }

        /// <summary>
        /// データグリッドキー入力(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private void MelsecData_KeyPress(object sender, KeyPressEventArgs e)
        {
            // 通信停止時は処理しない
            if (!m_isRunning)
            {
                return;
            }

            // ユーザー手入力以外処理しない
            if (!m_isEditing)
            {
                return;
            }

            // 10進表記の時は数値以外NG
            if (!GuiHexCheck.Checked)
            {
                if (e.KeyChar != '\b')
                {
                    if (e.KeyChar < 0x30 || 0x39 < e.KeyChar)
                    {
                        e.Handled = true;
                        return;
                    }
                }
            }

            // 16進表記の時は数値、a~f以外NG
            if (GuiHexCheck.Checked)
            {
                if (e.KeyChar != '\b')
                {
                    if ((e.KeyChar < 0x30 || 0x39 < e.KeyChar) && (e.KeyChar < 'a' || 'f' < e.KeyChar) && (e.KeyChar < 'A' || 'F' < e.KeyChar))
                    {
                        e.Handled = true;
                        return;
                    }
                }
            }

        }

        /// <summary>
        /// データグリッド値変更(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MelsecData_CellValueChanged(object sender, DataGridViewCellEventArgs e)
        {
            // 通信停止時は処理しない
            if (!m_isRunning)
            {
                return;
            }

            // ユーザー手入力以外処理しない
            if (!m_isEditing)
            {
                return;
            }

            // 変更位置を取得
            int column = e.ColumnIndex;
            int row = e.RowIndex;

            // 変更後のアドレスを取得
            int startAddr = Int32.Parse(GuiReadAddress.Text) + row * COL_SIZE + column;

            // 変更後のデータを取得
            string value = GuiMelsecData[column, row].Value.ToString();
            System.Globalization.NumberStyles numStyle = System.Globalization.NumberStyles.Number;
            if (GuiHexCheck.Checked)
            {
                numStyle = System.Globalization.NumberStyles.HexNumber;
            }

            // シーケンサーに送信
            short writeData = Int16.Parse(value, numStyle);
            m_melsecMgr.ForceSend(startAddr, writeData);

            // ユーザー手入力終了
            m_isEditing = false;
        }

        /// <summary>
        /// 「×」(イベントハンドラ)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // 通信中の場合はハンドルする
            if (m_isRunning)
            {
                MessageBox.Show("通信停止してから終了して下さい", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                e.Cancel = true;
            }
        }
    }
}


深く解説はしませんが、ポイントは以下の通りです。

(1) 起動時、コンストラクタ(Form1())にて、三菱PLC通信クラス「m_melsecMgr」に、「通信切断検知」用のコールバック(m_melsecMgr.m_onConnect)、「受信検知」用のコールバック(m_melsecMgr.m_onRecv)を登録します。

(2) 「通信開始」ボタン(GuiStartButton)押下で、イベントハンドラ「GuiStartButton_Click()」を駆動します(画面デザイナにて、「通信開始」ボタンをダブルクリックすれば、イベントハンドラの登録が自動的に出来ます)。関数内で、「m_melsecMgr.StartMelsec()」を呼び、通信を開始します。

(3) 受信検知時は、(1)でm_onRecvに登録したコールバック関数「OnRecv()」が駆動します。OnRecv()内で、受信データをデータグリッドビュー(GuiMelsecData)に反映します。

(4) ユーザーが画面からデータグリッドビュー(GuiMelsecData)を編集時、編集開始時にイベントハンドラ「MelsecData_EditingControlShowing()」、編集終了時にイベントハンドラ「MelsecData_CellValueChanged()」が駆動します。編集終了と共に、三菱PLC通信クラスの送信キューに、編集したセルの値を書き換える「メモリ書込」電文を追加します。通信クラスは、キューに追加された電文を送信する仕組みにしています。

2. 設定値クラス(Config.cs)

2-1. ソースコード

iniファイルとのアクセスを行うクラスです。画面の「設定値読込」時にファイルから読み出し、「設定値書込」時にファイルに書き込みます。また、「通信監視」(画面下部)側の各パラメータは、変更時に即座にiniファイルに書き込むようにしています。
iniファイルの読み書きを参考にしたい方は読んでみて下さい。通信部分を読みたい方は特に気にせず飛ばしてOKです。大したことはやっていませんので。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

namespace MelsecMonitor
{
    public static class Config
    {
        /* iniファイル設定 */
        private static readonly string iniFilePath          = @".\MelsecMonitor.ini";
        private static readonly string iniSecAp             = "ap_config";
        private static readonly string iniKeyServerAddr     = "server_address";
        private static readonly string iniKeyServerPort     = "server_port";
        private static readonly string iniKeyServerUnitType = "server_unit_type";
        private static readonly string iniSecMem            = "mem_config";
        private static readonly string iniKeyMemArea        = "area";
        private static readonly string iniKeyMemStartAddr   = "start_address";
        private static readonly string iniKeyMemWordNum     = "word_num";
        private static readonly string iniKeyCommCycle      = "comm_cycle";
        private static readonly string iniKeyHexShow        = "hex_show";

        /* 設定値 */
        public static string serverIpAddress;
        public static int    serverPort;
        public static int    serverUnitType;
        public static string memArea;
        public static int    memStartAddr;
        public static int    memWordNum;
        public static int    commCycle;
        public static bool   hexShow;

        /* DLL関数インポート */
        [DllImport("KERNEL32.DLL")]
        private static extern uint GetPrivateProfileString(
            string lpAppName,
            string lpKeyName,
            string lpDefault,
            StringBuilder lpReturnedString,
            uint nSize,
            string lpFileName
            );
        [DllImport("kernel32.dll")]
        private static extern int WritePrivateProfileString(
            string lpAppName,
            string lpKeyName,
            string lpString,
            string lpFileName
            );

        /// <summary>
        /// iniファイル読込ラッパー
        /// </summary>
        /// <param name="lpSection"></param>
        /// <param name="lpKeyName"></param>
        /// <param name="lpDefault"></param>
        /// <param name="lpFileName"></param>
        /// <returns></returns>
        private static string GetIniString(string lpSection, string lpKeyName, string lpDefault, string lpFileName)
        {
            System.Text.StringBuilder strBuilder = new System.Text.StringBuilder(255);
            uint sLen = GetPrivateProfileString(lpSection, lpKeyName, lpDefault, strBuilder, 255, lpFileName);
            return strBuilder.ToString().Trim();
        }

        /// <summary>
        /// iniファイル読込
        /// </summary>
        public static void ReadConfig()
        {
            string strTmp;

            try
            {
                // 接続先IPアドレス
                serverIpAddress = GetIniString(iniSecAp, iniKeyServerAddr, "127.0.0.1", iniFilePath);

                // 接続先ポート番号
                strTmp = GetIniString(iniSecAp, iniKeyServerPort, string.Empty, iniFilePath);
                serverPort = strTmp != string.Empty ? Int32.Parse(strTmp) : 1024;

                // 接続先ユニット種別
                strTmp = GetIniString(iniSecAp, iniKeyServerUnitType, string.Empty, iniFilePath);
                serverUnitType = strTmp != string.Empty ? Int32.Parse(strTmp) : 0;

                // 通信エリア
                memArea = GetIniString(iniSecMem, iniKeyMemArea, "D", iniFilePath);

                // 通信開始アドレス
                strTmp = GetIniString(iniSecMem, iniKeyMemStartAddr, string.Empty, iniFilePath);
                memStartAddr = strTmp != string.Empty ? Int32.Parse(strTmp) : 0;

                // 通信ワード数
                strTmp = GetIniString(iniSecMem, iniKeyMemWordNum, string.Empty, iniFilePath);
                memWordNum = strTmp != string.Empty ? Int32.Parse(strTmp) : 4;

                // 通信周期[ms]
                strTmp = GetIniString(iniSecMem, iniKeyCommCycle, string.Empty, iniFilePath);
                commCycle = strTmp != string.Empty ? Int32.Parse(strTmp) : 500;

                // 16進数表記
                strTmp = GetIniString(iniSecMem, iniKeyHexShow, string.Empty, iniFilePath);
                hexShow = strTmp != string.Empty ? (strTmp == "True" ? true : false) : false;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        /// <summary>
        /// iniファイル書込
        /// </summary>
        public static void WriteConfig(string serverAddressCurr, int serverPortCurr, int serverUnitTypeCurr)
        {
            WritePrivateProfileString(iniSecAp, iniKeyServerAddr, serverAddressCurr, iniFilePath);
            WritePrivateProfileString(iniSecAp, iniKeyServerPort, serverPortCurr.ToString(), iniFilePath);
            WritePrivateProfileString(iniSecAp, iniKeyServerUnitType, serverUnitTypeCurr.ToString(), iniFilePath);
        }

        public static void WriteReadArea(string readAreaCurr)
        {
            WritePrivateProfileString(iniSecMem, iniKeyMemArea, readAreaCurr, iniFilePath);
        }
        public static void WriteReadStartAddr(int readStartAddrCurr)
        {
            WritePrivateProfileString(iniSecMem, iniKeyMemStartAddr, readStartAddrCurr.ToString(), iniFilePath);
        }
        public static void WriteReadWordNum(int readWordNumCurr)
        {
            WritePrivateProfileString(iniSecMem, iniKeyMemWordNum, readWordNumCurr.ToString(), iniFilePath);
        }
        public static void WriteCommCycle(int commCycleCurr)
        {
            WritePrivateProfileString(iniSecMem, iniKeyCommCycle, commCycleCurr.ToString(), iniFilePath);
        }
        public static void WriteHexShow(bool hexShowCurr)
        {
            WritePrivateProfileString(iniSecMem, iniKeyHexShow, hexShowCurr.ToString(), iniFilePath);
        }
    }
}


3. TCPクライアントクラス(TcpClient.cs)

3-1. ソースコード

TCPクライアント部分は、先の記事『C#で「TCPクライアントを実装する」』で紹介したソースコードをゴッソリそのまま使っています(namespaceだけ変えました)ので、ここでの紹介は割愛します。以下の記事をご参照し、そのままご使用下さい。

www.kt2525family.com


4. 三菱PLC通信クラス(MelsecMgr.cs)

4-1. ソースコード

三菱PLCとの通信を行う、本アプリケーションの一番重要部分ですね。メイン画面の「通信開始」ボタンから本クラスの「StartMelsec()」が呼ばれ、通信開始します。通信の実体自体は「TcpClient」クラスにて実装しています。

using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;

namespace MelsecMonitor
{
    class MelsecMgr
    {
        /* 定数値 */
        private readonly int HDR_SIZE  = 11;

        /* 通信ハンドル */
        private TCPClient m_melsecComm = null;
        private Thread m_melsecThread  = null;
        private bool m_isStopCommand   = false;
        private Control m_windowHandle = null;

        /* 送信キュー */
        private List<byte[]> m_sendQue = new List<byte[]>();

        /* 三菱PLC ユニットデータ */
        public enum unitType { NONE, CPU, C24, E71 };
        private enum commType { READ, WRITE };
        private unitType m_unitType;
        private byte m_deviceCode;
        private int m_startAddr;
        private short m_readWordNum;
        private int m_commCycle;

        /* 制御フラグ */
        public bool m_IsReadData = false;

        /* コールバック */
        public delegate void ConnectEventHandler();
        public event ConnectEventHandler m_onConnect = null;
        public delegate void RecvEventHandler(string teleLog);
        public event RecvEventHandler m_onRecv = null;

        /// <summary>
        /// 三菱PLC読出ユニット設定
        /// </summary>
        /// <param name="_unitType"></param>
        /// <param name="deviceCode"></param>
        public bool SetUnitInfo(unitType _unitType, string deviceCode, int startAddr, short readWordNum, int commCycle)
        {
            bool result = true;

            // 通信ユニット
            m_unitType = _unitType;

            // デバイスコード
            switch (deviceCode)
            {
                case "B":
                    m_deviceCode = 0xA0;
                    break;
                case "D":
                    m_deviceCode = 0xA8;
                    break;
                case "W":
                    m_deviceCode = 0xB4;
                    break;
                case "ZR":
                    m_deviceCode = 0xB0;
                    break;
                default:
                    m_deviceCode = 0x00;
                    result = false;
                    break;
            }

            // 開始エリア
            m_startAddr = startAddr;

            // 読出ワード数
            m_readWordNum = readWordNum;

            // 読出周期
            m_commCycle = commCycle;

            return result;
        }

        /// <summary>
        /// 開始処理
        /// </summary>
        public bool StartMelsec(Control windowHandle, string serverIpAddress, int serverPort)
        {
            // ハンドル保持
            m_windowHandle = windowHandle;

            // 通信設定
            m_melsecComm = new TCPClient();
            if (!m_melsecComm.SetConnectInfo(serverIpAddress, serverPort, 3000))
            {
                return false;
            }

            // 三菱PLC 通信スレッド起動
            m_isStopCommand = false;
            m_melsecThread = new Thread(MelsecThread);
            m_melsecThread.Start();

            // 正常起動
            return true;
        }

        /// <summary>
        /// 終了処理
        /// </summary>
        public void StopMelsec()
        {
            m_isStopCommand = true;
        }

        /// <summary>
        /// 通信スレッド
        /// </summary>
        public void MelsecThread()
        {
            while (!m_isStopCommand)
            {
                // 未接続時は接続する
                if (!m_melsecComm.m_connected)
                {
                    m_melsecComm.Connect();
                }

                // 接続している場合
                if (m_melsecComm.m_connected)
                {
                    // 送信電文(メモリ書込)が存在する場合
                    while (m_sendQue.Count > 0)
                    {
                        byte[] writeCommData = m_sendQue[0];

                        // 送信電文(メモリ書込) 送信
                        m_melsecComm.Send(writeCommData);

                        // 応答待ち
                        int recvExpectSize = HDR_SIZE;
                        byte[] recvCommData = new byte[recvExpectSize];
                        int recvSize = m_melsecComm.Receive(recvCommData);

                        // 応答解析は不要
                        // (次周期の読出にて更新する)

                        // 送信電文
                        m_sendQue.RemoveAt(0);
                    }

                    // 読出ONの場合
                    if (m_IsReadData)
                    {
                        // 送信電文(メモリ読出) 作成
                        byte[] readCommData = MakeCommData(commType.READ, m_deviceCode, m_startAddr, m_readWordNum);

                        // 送信電文(メモリ読出) 送信
                        m_melsecComm.Send(readCommData);

                        // 応答待ち
                        int recvExpectSize = HDR_SIZE + m_readWordNum * 2;
                        byte[] recvCommData = new byte[recvExpectSize];
                        int recvSize = m_melsecComm.Receive(recvCommData);

                        // 応答解析
                        ParseRecvData(recvCommData, recvSize);
                    }
                }

                // 読出周期だけスリープ
                Thread.Sleep(m_commCycle);
            }

            // 終了処理
            if (m_melsecComm.m_connected)
            {
                m_melsecComm.Disconnect();
            }

            // 終了通知
            if (m_windowHandle != null && m_onConnect != null)
            {
                m_windowHandle.Invoke(m_onConnect);
            }
        }

        /// <summary>
        /// 送信電文キュー追加(メモリ書込)
        /// </summary>
        public void ForceSend(int startAddr, short writeWord)
        {
            if (m_melsecComm.m_connected)
            {
                // 送信電文(メモリ書込) 作成
                byte[] writeCommData = MakeCommData(commType.WRITE, m_deviceCode, startAddr, 1, writeWord);

                // 送信キューに追加
                m_sendQue.Add(writeCommData);
            }
        }
        
        /// <summary>
        /// 送信電文作成
        /// </summary>
        /// <returns></returns>
        private byte[] MakeCommData(commType commType, byte deviceCode, int startAdd, short wordNum, short writeWord = 0x00)
        {
            int sendSize  = commType == commType.READ ? HDR_SIZE + 10 : HDR_SIZE + 10 + wordNum * 2;
            short reqSize = (short)(sendSize - 9);
            byte[] sendData = new byte[sendSize];

            // サブヘッダ部
            sendData[0] = 0x50;
            sendData[1] = 0x00;

            // Qヘッダ部
            sendData[2] = 0x00;                                // ネットワーク№
            sendData[3] = 0xFF;                                // PC番号
            sendData[4] = 0xFF;                                // 要求先ユニット I/O番号
            sendData[5] = 0x03;                                // 〃 (上位)
            sendData[6] = 0x00;                                // 要求先ユニット局番号
            sendData[7] = (byte)((reqSize & 0x00ff) >> 0);     // 要求データ長
            sendData[8] = (byte)((reqSize & 0xff00) >> 8);     // 〃 (上位)
            sendData[9] = 0x10;                                // CPU監視タイマ
            sendData[10] = 0x00;                               // 〃 (上位)

            // 種別により分岐
            switch (commType)
            {
                // メモリ読出
                case commType.READ:
                    // データ部 QnA互換3Eフレーム (CPU内蔵Ethernetポート向け)
                    // ※ 0406(複数ブロック一括読出)は出来ないことに注意
                    sendData[11] = 0x01;    // コマンド
                    sendData[12] = 0x04;    // 〃 (上位)
                    sendData[13] = 0x00;    // サブコマンド
                    sendData[14] = 0x00;    // 〃 (上位)
                    sendData[15] = (byte)((startAdd & 0x0000ff) >> 0);    // 先頭デバイス
                    sendData[16] = (byte)((startAdd & 0x00ff00) >> 8);    // 〃
                    sendData[17] = (byte)((startAdd & 0xff0000) >> 16);   // 〃 (上位)
                    sendData[18] = deviceCode;                            // デバイスコード
                    sendData[19] = (byte)((wordNum & 0x00ff) >> 0);       // デバイス点数
                    sendData[20] = (byte)((wordNum & 0xff00) >> 8);       // 〃 (上位)
                    break;

                // メモリ書込
                case commType.WRITE:
                    // データ部 QnA互換3Eフレーム (CPU内蔵Ethernetポート向け)
                    // ※ 0406(複数ブロック一括読出)は出来ないことに注意
                    sendData[11] = 0x01;    // コマンド
                    sendData[12] = 0x14;    // 〃 (上位)
                    sendData[13] = 0x00;    // サブコマンド
                    sendData[14] = 0x00;    // 〃 (上位)
                    sendData[15] = (byte)((startAdd & 0x0000ff) >> 0);    // 先頭デバイス
                    sendData[16] = (byte)((startAdd & 0x00ff00) >> 8);    // 〃
                    sendData[17] = (byte)((startAdd & 0xff0000) >> 16);   // 〃 (上位)
                    sendData[18] = deviceCode;                            // デバイスコード
                    sendData[19] = (byte)((wordNum & 0x00ff) >> 0);       // デバイス点数
                    sendData[20] = (byte)((wordNum & 0xff00) >> 8);       // 〃 (上位)
                    sendData[21] = (byte)((writeWord & 0x00ff) >> 0);     // 書込データ
                    sendData[22] = (byte)((writeWord & 0xff00) >> 8);     // 書込データ
                    break;

                default:
                    break;
            }

            return sendData;
        }

        /// <summary>
        /// 受信電文解析
        /// </summary>
        /// <param name="recvData"></param>
        /// <param name="recvSize"></param>
        private void ParseRecvData(byte[] recvData, int recvSize)
        {
            // ヘッダ部のデータが足りない場合はエラー
            if (recvSize < HDR_SIZE)
            {
                return;
            }

            // 応答データ長
            short recvLen = (short)(recvData[7] | (recvData[8] << 8));

            // 終了コード
            short endCode = (short)(recvData[9] | (recvData[10] << 8));

            // 正常終了以外はエラー
            if (endCode != 0x0000)
            {
                return;
            }

            // データ部を取得
            string strTeleLog = m_melsecComm.MakeTeleLog(recvData, recvSize);
            string strDataLog = strTeleLog.Substring(HDR_SIZE * 2);

            // エンディアン逆転
            int offset = 0;
            string fixedTeleLog = "";
            while (offset < strDataLog.Length)
            {
                fixedTeleLog += strDataLog.Substring(offset + 2, 2);
                fixedTeleLog += strDataLog.Substring(offset, 2);
                offset += 4;
            }

            // 受信通知
            if (m_windowHandle != null && m_onRecv != null)
            {
                m_windowHandle.Invoke(m_onRecv, new object[1] { fixedTeleLog });
            }
        }
    }
}


といっても、結構ソースはシンプルです。ポイントは以下です。

(1) メンバに「ConnectEventHandler」と「RecvEventHandler」をdelegateで宣言します。いわば型なので、名前は自由に付けられます。delegateで宣言したこの型を、event型でメンバに宣言しておきます。メイン側で「切断検知」と「受信検知」時にコールバックしたい関数を、このevent型で登録したメンバに追加(+=)することで、三菱PLC通信クラス側からコールバックを呼ぶことができます。

(2) 通信開始関数「StartMelsec()」で、通信スレッド「MelsecThread」を駆動します。

(3) 通信スレッド「MelsecThread」にて、通信処理を行います。メモリ書込電文は、送信キューの「m_sendQue」にデータが入っていれば、それを送ります。その後、メモリ読出電文を「MakeCommData()」にて作成し、メモリ読出を行います。「m_melsecComm.Receive()」にて受信が行えるので、受信結果を解析し、メイン画面側にコールバック「m_windowHandle.Invoke()」にて通知します。

(4) 通信電文作成関数「MakeCommData()」は、三菱PLCとの通信プロトコル「QnA互換 3Cフレーム」にて実装しています。「3Eフレーム」等に対応したい場合でも、この関数を数行いじるのと、受信処理を数行いじるだけで対応できるかと思います。


免責事項

おそらく産業系ネットワークプログラマに需要があるかなと思い、公開してみました。何か間違いなどあれば、コメント欄にお願いします。
また、本ソースを商用利用することはかまいませんが、本ソースの不具合による損害・損失等の補償は致しかねるので、ご了承下さい(かたっ)。

ご購読、ありがとうございました。