質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Windows Forms

Windows Forms(WinForms)はMicrosoft .NET フレームワークに含まれる視覚的なアプリケーションのプログラミングインターフェイス(API)です。WinFormsは管理されているコードの既存のWindowsのAPIをラップすることで元のMicrosoft Windowsのインターフェイスのエレメントにアクセスすることができます。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

Q&A

解決済

4回答

10688閲覧

シリアル通信で順序的な手順で確実にデータ受信できる方法

Dally

総合スコア2

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Windows Forms

Windows Forms(WinForms)はMicrosoft .NET フレームワークに含まれる視覚的なアプリケーションのプログラミングインターフェイス(API)です。WinFormsは管理されているコードの既存のWindowsのAPIをラップすることで元のMicrosoft Windowsのインターフェイスのエレメントにアクセスすることができます。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

0グッド

0クリップ

投稿2021/09/24 07:37

前提・実現したいこと

WindowsFormアプリケーションでシリアル通信プログラムを作っています。
以下に示すような手順で同期的な(順序的な)手続きに従う、
データの送受信をしたいのですが実現できていません。
イベントハンドラの使用には拘らないです。
どのようにすれば実現できるかご教示ください。

【実現したい手順】
フォームボタンをクリックすると以下の手順を繰り返す。
繰り返し間隔は1秒周期を考えています。
(全データのバイト数は70バイト程度なので1秒程度の周期であれば
問題なく送受信可能と考えています。19200baudなので)

 手順1.スレーブ機器にコマンド1を送信する(機器の状態を設定するため)
手順2.コマンド1に対する応答を受信する(通信仕様で自動的に応答が戻ります)
↓ 受信したデータから機器の状態を遷移させる判断を行い、
自動的に手順3のコマンド送信を行う。
手順3.スレーブ機器にコマンド2を送信する(機器の状態を読み込む)
手順4.コマンド2に対する応答を受信する(通信仕様で自動的に応答が戻ります)
1~4を繰り返す。

発生している問題・エラーメッセージ

問題1:所望する”順序的に確実に受信を検出し、データ受信する”ことができていません。
問題2:Timerで周期的な処理をさせようとすると、バッファオーバーランが発生します。

1巡目の手順2で受信したデータは所望するバイト数で正しくデータを受信できていますが、
1巡目の手順4で受信したデータは所望するバイト数ではなく、
1巡目の手順2で受信したバイト数+手順4で受信したバイト数となるときもあれば、
1巡目の手順2で受信したバイト数+2巡目の手順2で受信したバイト数となるとき
などなど・・・・

MSDNなどの情報など検索した限り、実現手段は千差万別ですが、
私は「試したこと」に書いてますが、DataRecieveイベントを確実に制御できる
レベルではないです。
そこで、イベントハンドラに頼らない下記のような事例紹介がありますが、
このような手段で実現したいことが可能なのか?
★印をつけた部分が特に理解できていません。
また、bufferの配列数がどのようにデバッグ、確認を行えばよいかも含め。

事例 Example implementation using synchronous API and dynamic timeout properties update: static byte[] SendMessage(byte[] message, TimeSpan timeout) { // Use stopwatch to update SerialPort.ReadTimeout and SerialPort.WriteTimeout // as we go. var stopwatch = Stopwatch.StartNew(); // Organize critical section for logical operations using some standard .NET tool. lock (_syncRoot) { var originalWriteTimeout = _serialPort.WriteTimeout; var originalReadTimeout = _serialPort.ReadTimeout; try { // Start logical request. _serialPort.WriteTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0); _serialPort.Write(message, 0, message.Length); // Expected response length. Look for the constant value from // the device communication protocol specification or extract // from the response header (first response bytes) if there is // any specified in the protocol. int count = ...; byte[] buffer = new byte[count]; int offset = 0; // Loop until we recieve a full response. while (count > 0) { _serialPort.ReadTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0); var readCount = _serialPort.Read(buffer, offset, count); offset += readCount; count -= readCount; } return buffer; } finally { // Restore SerialPort state. _serialPort.ReadTimeout = originalReadTimeout; _serialPort.WriteTimeout = originalWriteTimeout; } } } And example usage: byte[] request = ...; TimeSpan timeout = ...; var sendTask = Task.Run(() => SendMessage(request, timeout)); try   //★ これ以降の意図が特に判らない。 { await await Task.WhenAny(sendTask, Task.Delay(timeout));    } catch (TaskCanceledException) { throw new TimeoutException(); } byte[] response = await sendTask;

該当のソースコード

C#

1 private void btnRemote_Click(object sender, EventArgs e) 2 { 3 short[] tmpData = new short[] { }; 4      //レジスタ数 5 ushort numOfPoint = 3; 6      //スレーブIDの取得 7 slaveID = GetSlaveID(); 8      //以下の処理は送受信が同期的に行われているか?を確認するための 9      //ボタン表示切替処理 10 switch (btnRemote.Text) 11 { 12 case "リモート接続": 13 try 14 { 15             // 手順1の設定送信(FunctionCode6) 16 short[] data = new short[] { Convert.ToInt16(1) }; 17 byte[] frame = mb.WriteSingleRegMsg(slaveID, Convert.ToUInt16(100), (byte)FunctionCode.FC6, data); 18 sp.Write(frame, 0, frame.Length); 19            //手順2 スレーブが正しく設定メッセージを受け取ったか? 20            //    の確認を応答メッセージとして受信したい。 21            //    〇一巡目は正しいバイト数で受信できている。 22 23            // ---- 判断処理 始まり 24            //    手順1で送信したメッセージを正しく受信できた 25            //    ことを確認できれば手順3で当該のレジスタ情報を読込む 26            // ---- 判断処理 終わり 27 28 // 手順3の設定確認(FunctionCode3) 29 numOfPoint = 1; 30 frame = mb.ReadHoldingRegiMsg(slaveID, Convert.ToUInt16(100), (byte)FunctionCode.FC3, (ushort)numOfPoint); 31 sp.Write(frame, 0, frame.Length); 32            //手順4 スレーブが正しく設定メッセージを受け取ったか? 33            //    の確認を応答メッセージとして受信したい。 34            //    × 一巡目で受信できなかったり、できていたり・・・ 35 36            // ---- 判断処理 始まり 37            //    手順4で受信した応答メッセージに含まれるデータを用いて 38            //    判断処理を行う。 39            // ---- 判断処理 終わり 40 41 if (mb.Check_FunctionCode_3(ref tmpData,slaveDataByteArry)) 42 { 43 if ((tmpData[0] == 1)&& (tmpData[2] == 0)) 44 { 45 } 46 break; 47 48 case "リモート解除": 49 try 50 { 51 // 同じような手順に基づく送受信データ手続き。 52           } 53 break; 54 } 55 56 } 57 public void serialPort_DataReceived(object sender,System.IO.Ports.SerialDataReceivedEventArgs e) 58 { 59 if (sp.IsOpen == false) 60 { 61 return; 62 } 63 try 64 { 65 //! 受信データを読み込む. 66 //string data = sp.ReadExisting(); 67 byte[] data = new byte[sp.BytesToRead]; 68 sp.Read(data, 0, data.GetLength(0)); 69 70 short[] tmpShort = dc.ByteToShort_Array(data); 71 72 for (int index = 0; index < data.GetLength(0); index++) 73 intermediate_list.Add(data[index]); 74 75 //this.Invoke(new MethodInvoker(() => ReadCode += data)); 76        // ↑試み1:ReadCodeに受信した配列のデータを追加する。 77 //data.ToList().ForEach(b => slaveDataByteArry.Enqueue(b)); 78        // ↑試み2:リストに受信データを追加する。 79 80 } 81 catch(Exception ex) 82 { 83 MessageBox.Show(ex.Message); 84 } 85 }

試したこと

受信イベントがどのようなタイミングで発生しているのか?
たとえ、不規則でも順序は守られているか?を確認するため、ハンドラ内部で
ブレークポイントは設けず、以下のようにして受信データを確認。
その結果、
・イベント自身は発生しているが、同期的に(順序的に)発生していることはない。
● この結果から、受信データを割り込みを使わず、順序的に確実に取り込む方法が 
必要と判断したが、
・ 受信が終わったことはどのように判断できるのか?判らない。
結論:イベントハンドラの利用はあきらめる。

試み1:mainスレッドにコントロールを戻し、受信データが同期的に取得できているか?
試み2:publicなリストを作り、そのリストのデータが同期的に取得できているか?

補足情報(FW/ツールのバージョンなど)

.NET framework4.5,Visual Studio 2017 enterprize

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

ベストアンサー

直接的な回答ではないかもしれませんが:

まず、シリアル通信に対しての向き合い方が逆なのだと思います。C#や.NET Frameworkが用意しているメソッド含む様々なやり方を採用すれば自動的にデータを順序立てて確実に受信できる訳ではありません。そう捉えてしまっているから、検索して得られる様々な記事の方法に惑わされます。また、自力で試して読み解けないコードであれば、正しく使うことはまずできません。エラーを含む様々な受信ケースに対応できなくなり、途方に暮れるばかりです。

そうではなく、プロトコル仕様を踏まえた上で何が最初のデータ、受信開始のトリガーとなるのか、何が最後のデータ、受信終了のトリガーとなるのかを見極めて、自分のプログラムに合ったやり方を採用します。それは受信専用のスレッドに全て任せて、逐次、Reaadメソッドを実行しても良いかもしれないし、SerialPortクラスで用意されているDataReceivedイベントを捉えてそのイベント内で受信処理をしても良いかもしれない。

別回答者であるradianさんの回答欄のコメントに「Modbus RTU」をプロトコルとして使っているとありましたが、如何にスタート、エンドが無通信時間をもってそれらを示しているとしても、マスターである質問者さんのプログラムからのリクエストに対してスレーブが有限個の応答データをレスポンスとして返してくるのですから、その応答データ中に応答データ自体の長さを決められる何らかのデータ項目があるのではないでしょうか。受信側では少しずつでも受信データを貯めつつ、サイズを確定できるほどにデータを受信したら、そのサイズ分受信して確定するまでで読み出しを続けます。レスポンスとして完成しなかったら、それは恐らく受信中の回線断であるとか、タイムアウトであるとかのエラーである可能性があるので、それなりに対処します。その為にはタイムアウト監視が必要になります。

言葉の捉え方ですが、スタートが3文字分、エンドが5文字分の無通信時間でそれぞれを示すとありますが、言い方を変えると「データフレームの受信が始まるまで少なくとも3文字分の無通信時間が設けられる。」「5文字分以上の無通信時間が発生したら、とにかくそればデータフレームの送信が終わったことを示す。」とも取れます。そう考えるとデータフレームの受信にほぼ専念できるような気がしますがどうでしょうか。


Modbus RTUのプロトコルを考慮して追記しました:

こちらは株式会社エム・システム技研(M-System)さんがWEBで公開されているModbusのプロトコルに関する概要の説明書ですが、
Modbus プロトコル概説書 - M-System

一部引用させていただくと、「2.2.(2) RTUモード時のフレーム」において以下の文章があります。

RTU モードの場合、少なくとも 3.5 文字分のサイレントインターバル(無通信時間)で始まり、3.5
文字分のサイレントインターバルで終ります。これを(表 2)では T1-T2-T3-T4 で表します

データ項目については少なくともAddressとFunctionがあるのですから、後続のData部が可変長であっても、先行してマスターから送信する際のクエリ内容を考えればどんなAddressとFunctionを受信すべきか、更には期待するデータ部の個数もマスター自身が知っているはずです。0個以上の可変になるならCRCを考慮に含めるか、その後の無通信時間をもって終了と判定できると考えられます。

投稿2021/09/25 00:58

編集2021/09/25 16:18
dodox86

総合スコア9256

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Dally

2021/09/29 03:22

dodox86さん初め皆様 連絡有難うございます。 頂きました意見、考え方に従い、要求を満たすプログラムの制御構造とデータの流れをまず再検討します。プログラミングスキルはNmodbusのコードも眺めおり、メソッドなど作り方にいくつかの定型があるようにも感じていますので、これを参考にスキルアップを図ります。 最後に、Replyが遅くなったこと申し訳ないです。
guest

0

なんか難しく考えすぎていますね
順序的な手順とかなんとか考える必要はないです
タイマとかイベントとかを使う必要はありません

別スレッドでも立てて、そこで常に受信処理してればいいです。
受信データをバッファに貯めていって、あなたのいうEndが来たら、CRCをチェックしてOKなら、そのデータを受信パケットとして処理に回す、というだけのはなしです。

投稿2021/09/24 10:14

y_waiwai

総合スコア88042

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Dally

2021/09/24 11:15

y_waiwaiさん 連絡有難うございます。 プロトコル仕様で”StratとEndはそれぞれ3.5文字分の無通信時間”と定義されています。 この無通信時間の計測はどのように行えるのでしょうか? 計測手段を教えて頂けませんでしょうか? 周期的な処理は最終的に必要と考えてます。 理由は、ソフトウェア的に?WatchDogに相当するカウンタを相手先レジスタに書き込む 必要があるからです。 これがなければ多少は楽なのですが。
y_waiwai

2021/09/24 11:45

ならタイマで無通信時間を測ればいい。 けど、その仕様はちょっとまずいです WindowsやLinuxなどのリアルタイムOSではないOSでそのような時間計測はできません。 ハードウエアで無通信時間を計測しないとどうしようもないですね その通信仕様をどうにかするのを考えないといけませんねー
dodox86

2021/09/24 12:02

そうですね。Windowsアプリで数ミリ秒単位の正確な計測は”不可能”なので、StartとEndで生じる無通信時間はいっしょくたにして、何かしら受信している間はデータフレームとして扱い、最後にデータを受信してからタイムアウト監視して一定時間経ったらそれは受信終了、と判断するくらいしかない気がします。反対に、質問者さんのアプリから送信するときにもそういった正確なタイミング制御が必要な場合、越えられない壁になります。
Dally

2021/09/24 14:04

y-waiwaiさん、dodox86さん 連絡ありがとうございます。 仕様:通信仕様をASCIIモードに変更可能か?を調整してみます。 タイミング:周期的に必須な通信データはマスター側、スレーブ側のWDカウントです。       タイミングの作り方はThreadTimerなどあると思うので今後も色々試します。 バッファリング:無通信時間に続くデータフレームの接頭バイトはスレーブアドレスです。         このバイトデータをトリガに70バイト分をバッファにため込むことは         可能です。 頂いた意見と受信中の排他制御をすることも考えるとバッファリングが完了するまでLockステータスを 入れることも含め検討します。 ありがとうございました。
dodox86

2021/09/24 15:09

> 仕様:通信仕様をASCIIモードに変更可能か?を調整してみます ASCIIでもバイナリでも、もはや関係無いです。
y_waiwai

2021/09/24 22:01

ENDを無通信時間にする、というのが無茶です 通信仕様に手を加えることができるなら、ENDに特定値、あるいはバイト列としましょう。 その上で周期的にするならすればいいです、そうじゃないと破綻します。
guest

0

受信が終わったことはどのように判断できるのか?判らない。

まず、接続対象の機器のデータシート、仕様書なりを見てデータの仕様を確認した方がよいのでは?(文字列?バイナリデータ?)
文字列であれば、通常は終端文字列が決まっていると思うので、SerialPort.NewLine プロパティ に終端文字列を設定すれば、単純にReadLine、WriteLineでデータをやり取り出来る筈です。

投稿2021/09/24 08:42

退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Dally

2021/09/24 09:20

radianさん。 連絡ありがとうございます。 プロトコルがMODBUS-RTUです。フレーム構成は Start+データフレーム+Endなのですが、それぞれ3.5文字分の無通信時間となります。 データフレームはバイナリです。 フレーム毎に7文字分の無通信時間が存在することになります。 データフレーム部の先頭はスレーブID、終端はCRCです。 CRCは受信側で再構成し、終端文字列として設定しましたが すべてのデータ受信後に設定できるのでうまく処理ができませんでした。 ASCIIモードであればいいのですが、客先仕様は変更できないので苦労しています。
guest

0

別に同期的な受信でいいなら、

  1. コマンドを送信
  2. Serial.Read()で応答をバッファに入れることを必要分まで繰り返す
  3. バッファに入った受信データに対して好きなことをする
  4. バッファを消す
  5. 次のコマンドも同様の手順で

投稿2021/09/24 07:55

編集2021/09/24 07:56
ozwk

総合スコア13553

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Dally

2021/09/24 08:34

ozwkさん。 連絡有難うございます。 イベントハンドラにするのではなく、serialPort_DataReceivedを単なるメソッドとして都度配列にためる。ということですね? 次に、勉強のために伺いたいのですが、事例で示したコードの利用例の部分にある try~catchのところはどのような意図があるのでしょうか? AWAITを2回も使用しているのはなぜですか? 手間を取らせますが教えて頂けませんか?
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問