Serial Over IPによるSD330コントローラーのLAN化2017年05月05日 19:08

はじめにお読みください。→当サイトのリンクと免責事項

現在SDD330コントローラはUSBにてパソコンにつないでいます。
SD330を遠隔で操作しようとするとSD330をコントロールしているPCにリモートアクセスして。そのPCのデスクトップより制御するようになります。
コントロールPCをリモートデスクトップから操作します。

これでも良いのですがコントロールPCを単なるシリアル通信のトンネルにして遠隔のPCから直接操作できるような仕組みを考えました。
コントロールPCにUSBインターフェースでSD330コントローラは繋がっています。
そこでコントロールPCでTCP/IPに変換し送信、遠隔のPCでTCP/IPからシリアル通信に変換しあたかもSD330コントローラが直接つながっているようにします。
コントロールPCのUSBインターフェースはUSB仮想COMポートとしていますが、遠隔のPCではLAN仮想COMポートになります。いわゆるSerial Over IPまたはSerial Over LANと言われるアーキティクチャーです。
しかしこの方法には欠点があります。遅延の問題です。SD330コントローラからのデータは一旦コントロールPCのバッファに入ります。次にデータは送られて遠隔PCのバッファに入ります。SD330コントローラからは921.6kbpsで送出されます。PC間はLANですのでそれなりに早いです。遠隔PCの仮想COMポートはSD330コントローラに合わせ921.6kbpsです。
Windows の.NETのプログラムの作り方にも拠りますがどれくらいのレスポンスになるのでしょうか。

この遅延を嫌うならSD330コントローラの出口をUSBでなくシリアル出力にしてそこにメディアサーバー(Serial-TCP/IP)を付けます。そのままLANになります。
921.6kbpsをサポートしている製品で手ごろな価格なのがLantronix製デバイスサーバです。同社のXport-XEは3.3Vのシリアル通信ですので300~920Kbpsをサポートしています。多くのRS232Cタイプはラインドライバーの関係でほとんどが最高115k bpsです。これらの機器はMCUで処理しますのでWindows より格段に速くなります。また1:多が可能なので1台のPCから複数のシリアル機器を制御できます。

とりあえずVB.NETで簡単に作ってみます。
今回のソフトは、コントロールPCはサーバーとして先に起動し遠隔PC(クライアント)からの接続要求を待ちます。接続が確立したらTCP/IPの受信ループに入ります。TCP/IPが受信するとそのままシリアルにデータを送信します。
シリアルの方は受信イベントが発生の都度SD330コントローラから受信しそのままTCP/IPに送信します。

遠隔PCの方はクライアントなのでプログラム起動と同時に接続要求を出します。接続が確立したらTCP/IPの受信ループに入ります。TCP/IPが受信するとそのままシリアルにデータを送信します。
シリアルの方は受信イベントが発生の都度SD330コントローラから受信しそのままTCP/IPに送信します。
つまり全く同じ構成です。これは1:1の通信を想定しているからです。

コントロールPCは接続されているSerial機器と送受信しますが、遠隔PCの方はこのソフトでリモートPCからのデータをCOM4につなぎます。COM4は専有されますので他のソフトから読み書きできません。そこで2つのCOMポートが繋がっているようなドライバーを用意します。今回はcom0comというドライバーを使いました。
このcom0comドライバは 2つの仮想のCOMポートをクロスケーブルで接続したように見せかけ、
片方の仮想COMポートからもう一方の仮想COMポートへ通信する事ができます。
COM4に送られたデータはCOM3にでます。COM3からのデータはCOM4に出てきます。そこでアナライザなどのコントロールソフトはCOM3を使用すればCOM4を経由してTCP/IP経由でリモートPCに行きSerial機器を制御できます。com0comについてはこちらを参考にしてください。


実際の動作での結果ですがRigExpert社のAA-54で試験したところコントロールPCからデータを受信し描画するまで100データで11秒でした。本プログラムを介し遠隔PCから同条件で行ったところ13秒かかりました。
AA-54は38.4kbpsで元々遅い感じですので全く気にならない遅延でした。
SD330コントローラの場合HF全域を測定しその8000データを受信し描画するまでの時間を計ったところどちらも6秒で遅延はありませんでした。Serialは921.6kBPSです。
バッファ制御など考えずに簡単に作った割には結構実用になると思います。

サーバー側のルーチンです。デザイン画面はForm1にLogboxという名前のListBoxが一つあるだけです。
またSerialPortコントロールを張り付けています。(SerialPort1)
Port番号とCOM番号はコード中に組み込んでいます。
Formは必要ないのですがデバックを兼ねています。
このルーチンはSD330用で1:1通信で尚且つ並列動作は無い、いわゆる半2重通信が前提です。
SerialもTCPもBinaryでRead、Writeしています。データの最大長は8kByteです。
サーバーはクライアントからの接続待ちでブロッキングしていて接続が完了すれば受信待ちになります。クライアントから切断要求がくれば接続待ちに戻ります。終了はありません。フォーム画面の✖で閉じない限り無限ループです。内部にてエラーが発生するとクライアントにシャットダウンを送信してFormを閉じて終了します。
言語はVB.NET (MS Visual Studio Express 2013 for Windows DESKTOP)です。

-------------------------------------------------------------------------------------------------------------------
Option Strict On
Imports System.IO
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading

'-------------------------------------------------------------------------------------------------------------------
Class Form1
    Inherits System.Windows.Forms.Form
    Dim myStream As NetworkStream                 ' myStream は 受信のほか送信でも使うためグローバル
    Dim myS_Socket As Socket                            ' シャットダウンを送信するためグローバル

'-------------------------------------------------------------------------------------------------------------------
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Dim myThreads As Thread
        myThreads = New Thread(AddressOf serverSTART)    '接続待ち受信スレッド
        myThreads.IsBackground = True                   ' バックグランドスレッド MAINスレッド終了と共に終了
        myThreads.Start()                                           ' スレッド スタート
        ' シリアルポートのオープン
        SerialPort1.PortName = "COM3"
        ' シリアルポートの通信速度指定
        SerialPort1.BaudRate = 921600
        ' シリアルポートのパリティ指定
        SerialPort1.Parity = IO.Ports.Parity.None
        ' シリアルポートのビット数指定
        SerialPort1.DataBits = 8
        ' シリアルポートのストップビット指定
        SerialPort1.StopBits = IO.Ports.StopBits.One
        SerialPort1.ReadBufferSize = 10000
        ' シリアルポートのオープン
        SerialPort1.Open()
        SerialPort1.ReadTimeout = 3000
        LOGOUTPUT("COM3は開かれました")
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub Form1_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
        SerialPort1.Close()
        LOGOUTPUT("COM3は閉じられました")
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub serverSTART()             '接続&受信待ちスレッド
        Dim mySocket As Socket
        '        Dim myS_Socket As Socket       ' シャットダウンを送信するためグローバル
        Dim myAddress As IPAddress
        Dim myPort As Integer
        Dim myEndPoint As EndPoint
        '        Dim myStream As NetworkStream        ' myStream は 受信のほか送信でも使うためグローバル
        Dim resBytes As Byte() = New Byte(10000) {}
        Dim resSize As Integer = 0
        Dim i_send As Integer
        Try
            mySocket = New Socket(AddressFamily.InterNetwork, _
                                    SocketType.Stream, ProtocolType.Tcp)
            LOGOUTPUT("ソケット作成")
            myPort = 45678
            myAddress = IPAddress.Any
            myEndPoint = New IPEndPoint(myAddress, myPort)
            mySocket.Bind(myEndPoint)
            LOGOUTPUT("バインド")
            mySocket.Listen(10)
            LOGOUTPUT("リッスン開始")
            LOGOUTPUT("")
            Do                                          '外側のDOループは接続待ちループ
                myS_Socket = mySocket.Accept()                'ブロッキング モードで Accept 接続の試行 待ち
                LOGOUTPUT("クライアント接続")
                myStream = New NetworkStream(myS_Socket)
                Try
                    Do                                  '内側のDOループは受信待ちループ
                        '受信
                        resSize = myStream.Read(resBytes, 0, resBytes.Length)  'ブロッキング モードで  Read 待ち
                        If resSize = 0 Then  'クライアントが切断
                            LOGOUTPUT("クライアントからシャットダウン要求")
                            Exit Do
                        End If
                        '************************ COM OUTPT **********************
                        '                                               TCPからCOMにデータを送信
                        i_send = 0
                        Do Until i_send = resSize
                            SerialPort1.Write(resBytes, i_send, 1)
                            i_send = i_send + 1
                        Loop
                        If resSize < 11 Then             '10BYTE以下なら電文をLOGに、それ以上はLENGSを
                            Dim i_c As Integer = 0
                            Do Until i_c = resSize
                                LOGOUTPUT("COM------->送信>> " & resBytes(i_c) & "=" & Chr(resBytes(i_c)))
                                i_c = i_c + 1
                            Loop
                        Else
                            LOGOUTPUT("COM------->送信LNG>> " & resSize.ToString)
                        End If
                        '******************************************************************
                        If resSize < 11 Then            '10BYTE以下なら電文をLOGに、それ以上はLENGSを
                            Dim i_c As Integer = 0
                            Do Until i_c = resSize
                                LOGOUTPUT("受信>> " & resBytes(i_c) & "=" & Chr(resBytes(i_c)))
                                i_c = i_c + 1
                            Loop
                        Else
                            LOGOUTPUT("受信LNG>> " & resSize.ToString)
                        End If
                    Loop
                    '終了処理
                    myS_Socket.Shutdown(SocketShutdown.Send)
                    LOGOUTPUT("クライアントへシャットダウン送信")
                    myS_Socket.Close()
                    LOGOUTPUT("Socket_Close 受信終了 次の接続を待つ")
                Catch ex As Exception
                    LOGOUTPUT(ErrorCodeofSocket(ex).ToString & ex.Message)
                    myS_Socket.Shutdown(SocketShutdown.Send)
                    LOGOUTPUT("クライアントへシャットダウン送信")
                    If Not IsNothing(myS_Socket) Then
                        myS_Socket.Close()
                    End If
                    LOGOUTPUT("Socket_Error 受信停止 次の接続を待つ")
                 End Try
            Loop
        Catch ex As Exception
            LOGOUTPUT("* 接続失敗再起動" & ex.Message)
            myS_Socket.Shutdown(SocketShutdown.Send)
            LOGOUTPUT("クライアントへシャットダウン送信")
            If Not IsNothing(myS_Socket) Then
                myS_Socket.Close()
                LOGOUTPUT("Socket.Close")
            End If
            If Not IsNothing(mySocket) Then
                mySocket.Close()
                LOGOUTPUT("Socket.Close")
            End If
            Form_Close()
        End Try
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub Form_Close()
        Dim Cancel_Event_Args As New System.ComponentModel.CancelEventArgs()
        Application.Exit(Cancel_Event_Args)              'アプリケーション終了
        If Cancel_Event_Args.Cancel Then                  'アプリケーションの終了がキャンセルされたか調べる
            LOGOUTPUT("アプリケーション終了がキャンセルされました")
        End If
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Function ErrorCodeofSocket(ByVal ex As Exception) As Integer  'ソケットエラーコード解析
        If IsNothing(ex) Then
            Return -1
        ElseIf TypeOf ex Is SocketException Then
            Return CType(ex, SocketException).ErrorCode
        Else
            Return ErrorCodeofSocket(ex.InnerException)
        End If
    End Function
 'Windows ソケットのエラー コード、値、および意味はこちら
'-------------------------------------------------------------------------------------------------------------------
    Private Delegate Sub D_Logupdate(ByVal str As String) '別スレッドからLOG書き込み

'-------------------------------------------------------------------------------------------------------------------
    Private Sub LOGOUTPUT(ByVal strr As String)
        If Me.InvokeRequired Then                                   '別スレッドか判定
            Me.Invoke(New D_Logupdate(AddressOf Logupdate), strr)   '別スレッドならデリゲート
        Else
            Logupdate(strr)
        End If
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub Logupdate(ByVal str As String)          ' LOG書き出しルーチン
        SyncLock logBox
            logBox.Items.Add(str)
            logBox.TopIndex = logBox.Items.Count - 1
            logBox.Refresh()
        End SyncLock
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub SerialPort1_DataReceived(sender As Object, e As Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
        '        LOGOUTPUT("EVENT_DataReceived")
        System.Console.Beep(440, 300)                   'COMデータ受信しTCPに送信
        Dim leng As Integer = 0
        Dim dat(10000) As Byte
        Try
            leng = SerialPort1.Read(dat, 0, dat.GetLength(0))

            TCP_DATA_SEND(dat, leng)            'クライアントにデータを送信する
            If leng < 11 Then                       '10BYTE以下なら電文をLOGに、それ以上はLENGS
                Dim i_c As Integer = 0
                Do Until i_c = leng
                    LOGOUTPUT("COM<-------受信>> " & dat(i_c) & "=" & Chr(dat(i_c)))
                    i_c = i_c + 1
                Loop
            Else
                LOGOUTPUT("COM<-------受信LNG>> " & leng.ToString)
            End If
        Catch ex As TimeoutException
            LOGOUTPUT("COM3ERR* " & ex.Message)
            SerialPort1.Close()
            myS_Socket.Shutdown(SocketShutdown.Send)
            LOGOUTPUT("クライアントへシャットダウン送信")
            Form_Close()
        End Try
        '************************************************************************
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub TCP_DATA_SEND(buffer() As Byte, size As Integer)        'クライアントにデータを送信する
        Try
            myStream.Write(buffer, 0, size)
            If size < 11 Then                       '10BYTE以下なら電文をLOGに、それ以上はLENGSを
                Dim i_c As Integer = 0
                Do Until i_c = size
                    LOGOUTPUT("送信>> " & buffer(i_c) & "=" & Chr(buffer(i_c)))
                    i_c = i_c + 1
                Loop
            Else
                LOGOUTPUT("送信LNG>> " & size.ToString)
            End If
        Catch ex As Exception
            LOGOUTPUT(ErrorCodeofSocket(ex).ToString & ex.Message)
            LOGOUTPUT("TCP送信エラー ")
            myS_Socket.Shutdown(SocketShutdown.Send)
            LOGOUTPUT("クライアントへシャットダウン送信")
            Form_Close()                        '
        End Try
    End Sub
End Class

-------------------------------------------------------------------------------------------------------------------

続いてクライアント側のルーティンです。こちらもデザイン画面はForm1にLogboxという名前のListBoxが一つあるだけです。またSerialPortコントロールを張り付けています。(SerialPort1)
IP_ADDRESSとPort番号、COM番号はコード中に組み込んでいます。
クライアントが起動するとサーバーに接続要求をします。接続後は交信します。終了はFormの✖によってサーバーに切断要求を送り、返事のシャットダウンが来るとプログラムを終了します。サーバーの電文中にシャットダウンがあればFormを閉じ終了します。

通常は再度プログラムを起動すれば接続から始まります。

-------------------------------------------------------------------------------------------------------------------
Option Strict On
Imports System.IO
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading

'-------------------------------------------------------------------------------------------------------------------
Class Form1
    Inherits System.Windows.Forms.Form
    Dim mySocket As Socket                  'セッションエンドの中でも使用のためグローバル
    Dim myStream As NetworkStream           'データの送信のためグローバル

'-------------------------------------------------------------------------------------------------------------------
    Private Sub Form1_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
        SerialPort1.Close()
        TCP_CLOSE()
        SeesionEND()
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' シリアルポートのオープン
        SerialPort1.PortName = "COM4"
        ' シリアルポートの通信速度指定
        SerialPort1.BaudRate = 921600
        ' シリアルポートのパリティ指定
        SerialPort1.Parity = IO.Ports.Parity.None
        ' シリアルポートのビット数指定
        SerialPort1.DataBits = 8
        ' シリアルポートのストップビット指定
        SerialPort1.StopBits = IO.Ports.StopBits.One
        SerialPort1.ReadBufferSize = 10000
        ' シリアルポートのオープン
        SerialPort1.Open()
        SerialPort1.ReadTimeout = 3000
        LOGOUTPUT("COM4は開かれCOM3と繋がりました")
        TCP_OPEN()                                  ' サーバーと接続 受信スレッド起動
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub TCP_OPEN()
        Dim myThread As Thread
        Dim myAddress As IPAddress
        Dim myPort As Integer
        Dim myEndPoint As EndPoint
        Try
            mySocket = New Socket(AddressFamily.InterNetwork, _
                                    SocketType.Stream, ProtocolType.Tcp)
            LOGOUTPUT("ソケット作成")
            myAddress = IPAddress.Parse("192.168.1.249")
            myPort = 45678
            myEndPoint = New IPEndPoint(myAddress, myPort)
            mySocket.Connect(myEndPoint)
            LOGOUTPUT("接続")
            myStream = New NetworkStream(mySocket)
            '別スレッド
            myThread = New Thread(AddressOf ClientSTART)
            myThread.IsBackground = True                ' バックグランドスレッド MAINスレッドの終了と共に終了
            myThread.Start()                                        ' スレッドスタート
            LOGOUTPUT("受信スレッド起動")
        Catch ex As Exception
            If Not IsNothing(mySocket) Then
                mySocket.Close()
            End If
            LOGOUTPUT("* " & ex.Message)
            LOGOUTPUT("* 再起動してください")
            SeesionEND()
        End Try
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub TCP_CLOSE()
        mySocket.Shutdown(SocketShutdown.Send)            '送信の終了
        LOGOUTPUT("サーバーへシャットダウン送信")
        mySocket.Close()
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Sub ClientSTART()                     '受信
        Dim resBytes As Byte() = New Byte(10000) {}
        Dim resSize As Integer = 0
        Try
            Do
                resSize = myStream.Read(resBytes, 0, resBytes.Length)          'ブロッキング 'データ受信
                If resSize = 0 Then         'サーバー切断
                    LOGOUTPUT("サーバーからシャットダウン要求")
                    Exit Do
                End If
                '***************************************** COM DATA SEND
                '                                               TCPからCOMにデータを送信
      SerialPort1.Write(resBytes, 0, resSize)   
                '**************************************************************
                If resSize < 11 Then                                    '10BYTE以下なら電文をLOGに、それ以上はLENGSを
                    Dim i_c As Integer = 0
                    Do Until i_c = resSize
                        LOGOUTPUT("受信>> " & resBytes(i_c))
                        i_c = i_c + 1
                    Loop
                Else
                    LOGOUTPUT("受信LNG>> " & resSize.ToString)
                End If
            Loop
            LOGOUTPUT("受信スレッド終了")
            SeesionEND()
        Catch ex As Exception
            LOGOUTPUT(ErrorCodeofSocket(ex).ToString & ex.Message)
            SeesionEND()
        End Try
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Function ErrorCodeofSocket(ByVal ex As Exception) As Integer  'ソケットエラーコード解析
        If IsNothing(ex) Then
            Return -1
        ElseIf TypeOf ex Is SocketException Then
            Return CType(ex, SocketException).ErrorCode
        Else
            Return ErrorCodeofSocket(ex.InnerException)
        End If
    End Function
  'Windows ソケットのエラー コード、値、および意味はこちら
'-------------------------------------------------------------------------------------------------------------------
    Sub SeesionEND()                                 'セッション終了
        If Not IsNothing(mySocket) Then
            mySocket.Close()
            LOGOUTPUT("ソケットCLOSE")
        End If
        LOGOUTPUT("")
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Delegate Sub D_Logupdate(ByVal str As String)  '別スレッドからLOG書き込み

'-------------------------------------------------------------------------------------------------------------------
    Sub LOGOUTPUT(ByVal strr As String)
        If Me.InvokeRequired Then                                   '別スレッドか判定
            Me.Invoke(New D_Logupdate(AddressOf Logupdate), strr)   '別スレッドならデリゲート
        Else
            Logupdate(strr)
        End If
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub Logupdate(ByVal str As String)          ' LOG書き出しルーチン
        SyncLock logBox
            logBox.Items.Add(str)
            logBox.TopIndex = logBox.Items.Count - 1
            logBox.Refresh()
        End SyncLock
    End Sub

'-------------------------------------------------------------------------------------------------------------------
    Private Sub SerialPort1_DataReceived(sender As Object, e As Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
        System.Console.Beep(440, 300)                   'COMデータ受信しTCPに送信
        Dim leng As Integer = 0
        Dim dat(10000) As Byte
        Try
            leng = SerialPort1.Read(dat, 0, dat.GetLength(0))
        Catch ex As TimeoutException
            LOGOUTPUT("COM4ERR* " & ex.Message)
            SeesionEND()
        End Try
        '*********************************************************TCP DATA SEND
        Try
            'クライアントにデータを送信する
            myStream.Write(dat, 0, leng)
            If leng < 11 Then                       '10BYTE以下なら電文をLOGに、それ以上はLENGSを
                Dim i_c As Integer = 0
                Do Until i_c = leng
                    LOGOUTPUT("送信>> " & dat(i_c))
                    i_c = i_c + 1
                Loop
            Else
                LOGOUTPUT("送信LNG>> " & leng.ToString)
            End If
        Catch ex As Exception
            'エラー処理
            LOGOUTPUT("* " & ex.Message)
            SeesionEND()
        End Try
    End Sub
End Class
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

サーバールーチンですが当初TCPで受信しCOMにデータを送る時、クライアントと同じようにしていました。

    resSize = myStream.Read(resBytes, 0, resBytes.Length)          'データ受信
    SerialPort1.Write(resBytes, 0, resSize)                                      'データ送信

シリアルが低速の場合は何の問題もありませんでしたが921.6KPSにしたら通信できない場合がでてきました。試行錯誤の末、次のように変更して事なきを得ました。サーバーのCOMへの通信はコマンドの送出で最長4BYTEです。1BYTEづつ送ってもレスポンスには影響ありません。

          resSize = myStream.Read(resBytes, 0, resBytes.Length)   'データ受信
          i_send = 0
                        Do Until i_send = resSize
                            SerialPort1.Write(resBytes, i_send, 1)       '1BYTEづつ送信
                            i_send = i_send + 1
                        Loop

原因は分かりません。相手(Serial機器)のあることですので誰もがこのようにしてうまくいくか分かりません。

コメント

トラックバック