プログラムを書こう!

実務や自作アプリ開発で習得した役に立つソフトウェア技術情報を発信するブログ

C++/CLIでシリアル通信を行う。

この記事は2018年08月19日に投稿しました。

f:id:paveway:20190914064630j:plain

目次

  1. はじめに
  2. シリアル通信処理
  3. おわりに

実践C++/CLI 極めるための基礎と実用テクニック

実践C++/CLI 極めるための基礎と実用テクニック

1. はじめに

こんにちは、iOSのエディタアプリPWEditorの開発者の二俣です。

今回は業務で使用しているC++/CLIの話です。
業務でシリアル通信の処理を実装しました。
それをもとにシリアル通信に最低限必要な処理を、サンプルで実装しなおしました。
今回は実装しなおしたコードをすべて公開しました。
そのため処理内容の詳細は、各クラスのコードを参照してください。

目次へ

2. シリアル通信処理

各クラスの概要です。

  • SerialUtilクラス : シリアル通信を実際に行うクラスです。
    シングルトンで実装しています。
  • MsgQueueクラス : シリアル通信クラスでデータを受信した際、MainFormクラスにデータ受信を通知します。
    シングルトンで実装しています。
  • MainFormクラス : シリアル通信で、文字列を送信したり、受信した文字列を表示したりする画面です。

SerialUtil.h

#pragma once

#include "MsgQueue.h"

namespace SampleSerial {
    using namespace System;
    using namespace System::Threading;
    using namespace System::IO::Ports;
    using namespace System::Text;

    ref class SerialUtil
    {
    private:
        /// 自クラスのインスタンス
        static SerialUtil^ instance;
        
        /// シリアル処理を行うオブジェクト
        SerialPort^ serial;
        /// メッセージキュー
        MsgQueue^ msgQueue;
        /// 受信スレッド
        Thread^ receiveThread;
        /// 受信スレッド停止フラグ
        bool stopReceiveThread;

    private:
        SerialUtil(String^ portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake, int timeout);

    public:
        static SerialUtil^ GetInstance(String^ portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake, int timeout);
        
        bool SendData(String^ data, String^% errMsg);
        void Close();

    private:
        void InitSerial(String^ portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake, int timeout);
        void StartReceiveThread();
        void ReceiveThread();
    };
}

SerialUtil.cpp

#include "SerialUtil.h"

#define ENCODING       "Shift_JIS"  // 文字コードはShift_JISの前提です。
#define RCV_BUFF_LEN   (10)       // 受信バッファ長です。1文字ずつ受信するためそんなに大きくしていません。

namespace SampleSerial {

    /// <summary>
    /// コンストラクタ
    /// </summary>
    SerialUtil::SerialUtil(String^ portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake, int timeout)
    {
        // シリアル通信の初期化を行います。
        InitSerial(portName, baudRate, parity, dataBits, stopBits, handshake, timeout);
        
        msgQueue = MsgQueue::GetInstance(100);
        receiveThread = nullptr;
        stopReceiveThread = false;
        
        // 受信スレッドを開始します。
        StartReceiveThread();
    }

    /// <summray>
    /// インスタンスを生成します。
    /// </summary>
    SerialUtil^ SerialUtil::GetInstance(String^ portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake, int timeout)
    {
        if (instance == nullptr)
        {
            instance = gcnew SerialUtil(portName, baudRate, parity, dataBits, stopBits, handshake, timeout);
        }
        return instance;
    }

    /// <summary>
    /// データを送信します。
    /// </summray>
    bool SerialUtil::SendData(String^ data, String^% errMsg)
    {
        bool result = true;
        try
        {
            array<Byte>^ buffer = Encoding::GetEncoding(ENCODING)->GetBytes(data);
            int lenght = buffer->Length;
            serial->Write(buffer, 0, lenght);
        }
        catch (Exception^ e)
        {
            errMsg = e->Message;
            result = false;
        }
        return result;
    }

    /// <summary>
    /// 終了時の処理を行います。
    /// </summary>
    void SerialUtil::Close()
    {
        // 受信スレッドを停止します。
        // Join()する前に受信スレッドがクリアされることがあるため
        // nullチェックを行ってからJoin()します。
        stopReceiveThread = true;
        if (receiveThread != nullptr)
        {
            receiveThread->Join();
        }

        // シリアルをクローズします。
        serial->Close();
    }

    /// <summary>
    /// シリアル通信の初期化処理を行います。
    /// </summary>
    void SerialUtil::InitSerial(String^ portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake, int timeout)
    {
        serial = gcnew SerialPort(portName, baudRate, parity, dataBits, stopBits);
        serial->Handshake = handshake;
        serial->ReadTimeout = timeout;
        serial->Open();
    }

    /// <summray>
    /// 受信スレッドを開始します。
    /// </summray>
    void SerialUtil::StartReceiveThread()
    {
        ThreadStart^ start = gcnew ThreadStart(this, &SerialUtil::ReceiveThread);
        receiveThread = gcnew Thread(start);
        receiveThread->Start();
    }

    /// <summary>
    /// 受信スレッド処理
    /// </summary>
    void SerialUtil::ReceiveThread()
    {
        // タイムアウト例外が発生してループするため、
        // whileの条件式で受信スレッド停止フラグをチェックします。
        array<Byte>^ buffer = gcnew array<Byte>(RCV_BUFF_LEN);
        while (!stopReceiveThread)
        {
            try
            {
                int bufLen = buffer->Length;
                for (int i = 0; i < bufLen; i++)
                {
                    buffer[i] = 0;
                }

                // シリアルからデータを受信した場合
                int len = serial->Read(buffer, 0, 1);
                if (len > 0)
                {
                    // 受信したデータを文字列に変更し、メッセージキューに設定します。
                    String^ data = Encoding::GetEncoding(ENCODING)->GetString(buffer, 0, 1);
                    msgQueue->SetMsg(data);
                }
            }
            catch (Exception^)
            {
                // 必要に応じてエラー処理を行ってください。
            }
        }
    }
}

MsgQueue.h

#pragma once

namespace SampleSerial {
    using namespace System;
    using namespace System::Collections;
    using namespace System::Threading;

    ref class MsgQueue
    {
    private:
        /// 自身のインスタンス
        static MsgQueue^ instance;
        
        /// キューオブジェクト
        Queue^ queue;

        /// イベントオブジェクト
        ManualResetEvent^ event;
        
    private:
        MsgQueue(int capacity);

    public:
        static MsgQueue^ GetInstance(int capacity);
        
        void SetMsg(String^ msg);
        String^ GetMsg();
    };
}

MsgQueue.cpp

#include "MsgQueue.h"

namespace SampleSerial {
    
    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="capcity">キューイング数</param>
    MsgQueue::MsgQueue(int capacity)
    {
        queue = gcnew Queue(capacity);
        queue->Clear();
        event = gcnew ManualResetEvent(false);
    }

    /// <summary>
    /// インスタンスを返却します。
    /// </summary>
    /// <param name="capacity">キューイング数</param>
    MsgQueue^ MsgQueue::GetInstance(int capacity)
    {
        if (instance == nullptr)
        {
            instance = gcnew MsgQueue(capacity);
        }
        return instance;
    }

    /// <summary>
    /// メッセージをセットします。
    /// <summary>
    /// <param name="msg">メッセージ</param>
    void MsgQueue::SetMsg(String^ msg)
    {
        // キューオブジェクトをロックします。
        Monitor::Enter(queue);

        // メッセージをキューイングします。
        queue->Enqueue(msg);

        // イベントを発行します。
        event->Set();

        // キューオブジェクトのロックを解除します。
        Monitor::Exit(queue);
    }

    /// <summary>
    /// メッセージを取得します。
    /// </summary>
    /// <returns>
    /// メッセージ
    /// </returns>
    String^ MsgQueue::GetMsg()
    {
        String^ msg;

        // イベント待ち
        // SetMsgメソッドが呼び出されると、イベント待ちが解除されます。
        event->WaitOne();

        // キューオブジェクトをロックします。
        Monitor::Enter(queue);

        // キューイングされている場合
        if (queue->Count > 0)
        {
            // メッセージを取得します。
            msg = (String^)queue->Dequeue();

            // キューイングされたメッセージがなくなった場合
            if (queue->Count == 0)
            {
                // 非シグナル状態にします。
                event->Reset();
            }
        }

        // キューイングされたメッセージがない場合
        else
        {
            // 非シグナル状態にします。
            event->Reset();
        }

        // キューオブジェクトのロックを解除します。
        Monitor::Exit(queue);

        return msg;
    }
}

MainForm.h

#pragma once

#include "SerialUtil.h"
#include "MsgQueue.h"

namespace SampleSerial {

    using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;

    /// <summary>
    /// MainForm の概要
    /// </summary>
    public ref class MainForm : public System::Windows::Forms::Form
    {
    public:
        MainForm(void)
        {
            InitializeComponent();
            //
            //TODO: ここにコンストラクター コードを追加します
            //
        }

    protected:
        /// <summary>
        /// 使用中のリソースをすべてクリーンアップします。
        /// </summary>
        ~MainForm()
        {
            if (components)
            {
                delete components;
            }
        }
    private: System::Windows::Forms::TextBox^  TxtBoxSendData;
    protected:
    private: System::Windows::Forms::Button^  BtnSend;
    private: System::Windows::Forms::TextBox^  TxtBoxRcvData;
    protected:
    private: System::Windows::Forms::Button^  BtnClear;

    private:
        /// <summary>
        /// 必要なデザイナー変数です。
        /// </summary>
        System::ComponentModel::Container ^components;

#pragma region Windows Form Designer generated code
        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        void InitializeComponent(void)
        {
            this->TxtBoxSendData = (gcnew System::Windows::Forms::TextBox());
            this->BtnSend = (gcnew System::Windows::Forms::Button());
            this->TxtBoxRcvData = (gcnew System::Windows::Forms::TextBox());
            this->BtnClear = (gcnew System::Windows::Forms::Button());
            this->SuspendLayout();
            // 
            // TxtBoxSendData
            // 
            this->TxtBoxSendData->Location = System::Drawing::Point(8, 8);
            this->TxtBoxSendData->Name = L"TxtBoxSendData";
            this->TxtBoxSendData->Size = System::Drawing::Size(272, 19);
            this->TxtBoxSendData->TabIndex = 0;
            // 
            // BtnSend
            // 
            this->BtnSend->Location = System::Drawing::Point(288, 8);
            this->BtnSend->Name = L"BtnSend";
            this->BtnSend->Size = System::Drawing::Size(75, 23);
            this->BtnSend->TabIndex = 1;
            this->BtnSend->Text = L"送信";
            this->BtnSend->UseVisualStyleBackColor = true;
            this->BtnSend->Click += gcnew System::EventHandler(this, &MainForm::BtnSend_Click);
            // 
            // TxtBoxRcvData
            // 
            this->TxtBoxRcvData->Location = System::Drawing::Point(8, 40);
            this->TxtBoxRcvData->Multiline = true;
            this->TxtBoxRcvData->Name = L"TxtBoxRcvData";
            this->TxtBoxRcvData->Size = System::Drawing::Size(352, 240);
            this->TxtBoxRcvData->TabIndex = 2;
            // 
            // BtnClear
            // 
            this->BtnClear->Location = System::Drawing::Point(288, 288);
            this->BtnClear->Name = L"BtnClear";
            this->BtnClear->Size = System::Drawing::Size(75, 23);
            this->BtnClear->TabIndex = 3;
            this->BtnClear->Text = L"クリア";
            this->BtnClear->UseVisualStyleBackColor = true;
            this->BtnClear->Click += gcnew System::EventHandler(this, &MainForm::BtnClear_Click);
            // 
            // MainForm
            // 
            this->AutoScaleDimensions = System::Drawing::SizeF(6, 12);
            this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font;
            this->ClientSize = System::Drawing::Size(369, 314);
            this->Controls->Add(this->BtnClear);
            this->Controls->Add(this->TxtBoxRcvData);
            this->Controls->Add(this->BtnSend);
            this->Controls->Add(this->TxtBoxSendData);
            this->FormBorderStyle = System::Windows::Forms::FormBorderStyle::FixedSingle;
            this->MaximizeBox = false;
            this->Name = L"MainForm";
            this->Text = L"シリアル通信サンプル";
            this->FormClosing += gcnew System::Windows::Forms::FormClosingEventHandler(this, &MainForm::MainForm_FormClosing);
            this->Load += gcnew System::EventHandler(this, &MainForm::MainForm_Load);
            this->ResumeLayout(false);
            this->PerformLayout();
        }
#pragma endregion
    private:
        delegate void SetMsgDelegate(String^ msg);

        SerialUtil^ serialUtil;
        MsgQueue^ msgQueue;
        Thread^ receiveThread;
        bool stopReceiveThread;

    private:
        System::Void MainForm_Load(System::Object^  sender, System::EventArgs^  e);
        System::Void MainForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e);
        System::Void BtnSend_Click(System::Object^  sender, System::EventArgs^  e);
        System::Void BtnClear_Click(System::Object^  sender, System::EventArgs^  e);

    private:
        void StartReceiveThread();
        void ReceiveThread();
        void SetMsg(String^ msg);
        void StopReceiveThread();
    };
}

MainForm.cpp

#include "MainForm.h"

namespace SampleSerial {

    /// <summary>
    /// フォームが表示された時に呼び出されます。
    /// </summary>
    System::Void MainForm::MainForm_Load(System::Object^  sender, System::EventArgs^  e) {
        // シリアルユーティリティオブジェクトを作成します。
        // シリアル通信で必要なパラメータは、ここでは固定値とします。
        serialUtil = SerialUtil::GetInstance("COM2", 9600, Parity::None, 8, StopBits::One, Handshake::None, 1000);
        // メッセージキューオブジェクトを作成します。
        msgQueue = MsgQueue::GetInstance(10);
        
        // 受信スレッド停止フラグをクリアします。
        stopReceiveThread = false;

        // 受信スレッドを開始します。
        StartReceiveThread();
    }

    /// <summary>
    /// フォームが閉じられる時に呼び出されます。
    /// </summary>
    System::Void MainForm::MainForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e) {
        // 受信スレッドを停止します。
        StopReceiveThread();
        
        // シリアル通信の終了処理を行います。
        serialUtil->Close();
    }

    /// <summary>
    /// 送信ボタンが押下された時に呼び出されます。
    /// </summary>
    System::Void MainForm::BtnSend_Click(System::Object^  sender, System::EventArgs^  e) {
        // 送信用テキストボックスに入力された文字列をシリアルで送信します。
        String^ sendData = TxtBoxSendData->Text;
        if (String::IsNullOrEmpty(sendData))
        {
            return;
        }

        String^ errMsg;
        if (!serialUtil->SendData(sendData, errMsg))
        {
            MessageBox::Show(errMsg, "エラー", MessageBoxButtons::OK, MessageBoxIcon::Error);
        }
    }

    /// <summary>
    /// クリアボタンが押下された時に呼び出されます。
    /// </summary>
    System::Void MainForm::BtnClear_Click(System::Object^  sender, System::EventArgs^  e) {
        // 受信メッセージを表示するテキストボックスをクリアします。
        TxtBoxRcvData->Text = "";
    }

    /// <summary>
    /// 受信スレッドを開始します。
    /// </summary>
    void MainForm::StartReceiveThread()
    {
        ThreadStart^ start = gcnew ThreadStart(this, &MainForm::ReceiveThread);
        receiveThread = gcnew Thread(start);
        receiveThread->Start();
    }

    /// <summary>
    /// 受信スレッド処理
    /// </summary>
    void MainForm::ReceiveThread()
    {
        // stopReceiveThreadでループを抜けるため、無限ループします。
        while (true)
        {
            // メッセージ受信待ち
            String^ msg = msgQueue->GetMsg();

            // 受信スレッド停止フラグが設定された場合
            if (stopReceiveThread)
            {
                // ループを抜けます、受信スレッドを終了します。
                break;
            }

            // 受信した文字列をテキストボックスに追加表示します。
            SetMsg(msg);
        }
    }

    /// <summary>
    /// 受信したメッセージをテキストボックスに追加します。
    /// 受信スレッドから呼び出されるため、Invokeメソッドによりデリゲート経由で処理します。
    /// </summary>
    void MainForm::SetMsg(String^ msg)
    {
        if (this->InvokeRequired)
        {
            SetMsgDelegate^ method = gcnew SetMsgDelegate(this, &MainForm::SetMsg);
            array<Object^>^ args = { msg };
            this->Invoke(method, args);
        }
        else
        {
            TxtBoxRcvData->Text += msg;
        }
    }

    /// <summary>
    /// 受信スレッドを停止します。
    /// </summary>
    void MainForm::StopReceiveThread()
    {
        // 受信スレッド停止フラグを設定します。
        stopReceiveThread = true;
        
        // 受信スレッドのメッセージ待ちを解除するため、空文字列をメッセージキューに設定します。
        msgQueue->SetMsg("");
        
        // 受信スレッドが終了するのを待ちます。
        receiveThread->Join();
    }
}

main.cpp

#include "MainForm.h"

using namespace SampleSerial;
[STAThreadAttribute]
int main()
{
    // メインフォームを作成し、表示します。
    Form^ form = gcnew MainForm();
    form->ShowDialog();
    return 0;
}

目次へ

3. おわりに

実際に業務でシリアル通信処理部分を実装したのは同僚でした。
そのため勉強がてらシリアル通信処理部分のみ切り出して、サンプルを実装しなおしました。
実装しなおすことにより、理解が深まりました。
コードを読むだけでも勉強になりますが、やはり実装するのが一番理解できます。

未経験のITエンジニア転職なら【TECH::EXPERT】

紹介している一部の記事のコードはGitlabで公開しています。
興味のある方は覗いてみてください。

目次へ


私が勤務しているニューラルでは、主に組み込み系ソフトの開発を行っております。
弊社製品のハイブリッドOS Bi-OSは高い技術力を評価されており、特に制御系や通信系を得意としています。
私自身はiOSモバイルアプリウィンドウズアプリを得意としております。
ソフトウェア開発に関して相談などございましたら、お気軽にご連絡ください。

また一緒に働きたい技術者の方も随時募集中です。
興味がありましたらご連絡ください。

EMAIL : info-nr@newral.co.jp / m-futamata@newral.co.jp
TEL : 042-523-3663
FAX : 042-540-1688

目次へ