Search code examples
vb.netmodbus

Extracting UInt16 from serial port (ModibusRTU) response


I (new to programming) am attempting to use VB.net (scripts) on VB2019 to query a vintage PID controller. This is connected to my desktop via PIC card RS485 Modbus RTU. While it is a Modbus connection, the controller is not using standard function code like F03, to read its registers, rather an array 81 81 52 00 00 53 (check below image)..

example of vb.net

I modified a code i found on YouTube for me to be able to somewhat send and receive the data; I received 98 42 01 80 00 10 A0; as per manual(wE49B) the result is supposed to be 42 98 08 01 00 A0 10; of which I'm most interested in (98 42) which contains the PV value (DEC: 17048);

Imports System.IO.Ports
Imports System.Threading

Public Class ReadHoldingRegistersForm

    'Declare variables & constants
    Private serialPort As SerialPort = Nothing

    Private Sub ReadHoldingRegistersForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Try
            serialPort = New SerialPort("COM4", 9600, Parity.None, 8, StopBits.Two)
            serialPort.Open() 'Open COM4
        Catch ex As Exception
            MessageBox.Show(Me, ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End Try
    End Sub

    Private Sub ReadHoldingRegistersForm_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        Try
            serialPort.Close() ' Close COM4.
        Catch ex As Exception
            MessageBox.Show(Me, ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End Try
    End Sub

    Private Sub btnReadHoldingRegisters_Click(sender As Object, e As EventArgs) Handles btnReadHoldingRegisters.Click
        Try
            Dim MeterAddress As Byte = 129
            Dim MeterAddress1 As Byte = 129
            Dim startAddress As Byte = 82
            Dim ReadOpMaker As Byte = 0
            Dim ReadData As Byte = 0
            Dim ReadData1 As Byte = 0
            Dim ReadPMaeker As Byte = 83
            Dim ReadPcode As Byte = ReadOpMaker

            Dim frame As Byte() = Me.ReadHolingRegisters(MeterAddress, MeterAddress1, startAddress, ReadOpMaker, ReadData, ReadData1, ReadPMaeker, ReadPcode)
            txtSendMsg.Text = Me.DisplayValue(frame) ' Diplays frame: send.
            serialPort.Write(frame, 0, frame.Length) ' Send frame to modbus slave.

            Thread.Sleep(100) ' Delay 100ms.

            If serialPort.BytesToRead > 5 Then
                Dim buffRecei As Byte() = New Byte(serialPort.BytesToRead) {}
                serialPort.Read(buffRecei, 0, buffRecei.Length) ' Read data from modbus slave.
                txtReceiMsg.Text = Me.DisplayValue(buffRecei) ' Display frame: received.

                Dim data As Byte() = New Byte(buffRecei.Length - 5) {}
                Array.Copy(buffRecei, 3, data, 0, data.Length)

                ' Convert byte array to word array
                Dim result As UInt16() = DataType.Word.ToArray(data) '

                'Display Result

                For Each item As UInt16 In result
                    txtResult.Text += String.Format("{0:0} ", item)
                Next
            End If

        Catch ex As Exception
            MessageBox.Show(Me, ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End Try
    End Sub

    ''' <summary>
    ''' Read holding registers
    ''' </summary>
    ''' <param name="MeterAddress">Slave Address</param>
    ''' <param name="MeterAddress1">Function</param>
    ''' <param name="startAddress">Starting Address</param>

    ''' <returns>Byte()</returns>
    Private Function ReadHolingRegisters(MeterAddress As Byte, MeterAddress1 As Byte, startAddress As Byte, ReadOpMaker As Byte, ReadData As Byte, ReadData1 As Byte, ReadPMaeker As Byte, ReadPcode As Byte) As Byte()
        Dim frame As Byte() = New Byte(7) {} ' Total 8 Bytes
        frame(0) = MeterAddress ' Slave Address
        frame(1) = MeterAddress1 'Function
        frame(2) = startAddress  'Starting Address Hi.
        frame(3) = ReadOpMaker  'Starting Address Lo.
        frame(4) = ReadData  ' Quantity of Registers Hi.
        frame(5) = ReadData1  ' Quantity of Registers Lo.
        frame(6) = ReadPMaeker ' Error Check Lo
        frame(7) = ReadPcode ' Error Check Hi
        Return frame '
    End Function

The manual states: Part of manual showing structure of receive data

and: Part of manual showing structure of sent data

Currently the value am getting 32768??? Any way i wish to be guided on how I can via code collect the PV(98 42)(DEC:17048) and display it on text bar. Below is part of code that i believe i need guide to achieve my goal.


            If serialPort.BytesToRead > 5 Then
                Dim buffRecei As Byte() = New Byte(serialPort.BytesToRead) {}
                serialPort.Read(buffRecei, 0, buffRecei.Length) ' Read data from modbus slave.
                txtReceiMsg.Text = Me.DisplayValue(buffRecei) ' Display frame: received.

                Dim data As Byte() = New Byte(buffRecei.Length - 5) {}
                Array.Copy(buffRecei, 3, data, 0, data.Length)

                ' Convert byte array to word array
                Dim result As UInt16() = DataType.Word.ToArray(data) '

                'Display Result

                For Each item As UInt16 In result
                    txtResult.Text += String.Format("{0:0} ", item)
                Next
            End If

the 'DataType' comes from this:

Imports System.Collections.Generic
Imports System.Text

Namespace DataType
    Public Class Word
#Region "Chuyển đổi mảng bytes thành kiểu Word."

        ''' <summary>
        ''' Phương thức chuyển đổi mảng bytes thành kiểu Word.
        ''' </summary>
        ''' <param name="bytes">Mảng byte cần chuyển đổi</param>
        ''' <returns>Trả về giá trị kiểu Word</returns>
        Public Shared Function FromByteArray(bytes As Byte()) As UInt16
            ' bytes[0] -> HighByte
            ' bytes[1] -> LowByte
            Return FromBytes(bytes(1), bytes(0))
        End Function

        ''' <summary>
        ''' Phương thức chuyển đổi mảng bytes thành kiểu Word.
        ''' </summary>
        ''' <param name="LoVal">Giá trị byte thấp</param>
        ''' <param name="HiVal">Giá trị byte cao</param>
        ''' <returns>Trả về giá trị kiểu Word</returns>
        Public Shared Function FromBytes(LoVal As Byte, HiVal As Byte) As UInt16
            Return CType(HiVal * 256 + LoVal, UInt16)
        End Function

#End Region

#Region "Chuyển đổi kiểu Word thành mảng bytes."

        ''' <summary>
        ''' Phương thức chuyển đổi kiểu Word thành mảng bytes.
        ''' </summary>
        ''' <param name="value">Giá trị kiểu Word</param>
        ''' <returns>Trả về giá trị mảng kiểu byte</returns>
        Public Shared Function ToByteArray(value As UInt16) As Byte()
            Dim array1 As Byte() = BitConverter.GetBytes(value)
            Array.Reverse(array1)
            Return array1
        End Function

        ''' <summary>
        ''' Phương thức chuyển đổi mảng kiểu Word thành mảng bytes.
        ''' </summary>
        ''' <param name="value">Mảng kiểu Word</param>
        ''' <returns>Trả về mảng kiểu byte</returns>
        Public Shared Function ToByteArray(value As UInt16()) As Byte()
            Dim arr As New ByteArray()
            For Each val As UInt16 In value
                arr.Add(ToByteArray(val))
            Next
            Return arr.array
        End Function

        ''' <summary>
        ''' Phương thức chuyển đổi kiểu mảng bytes thành mảng word.
        ''' </summary>
        ''' <param name="bytes">Giá trị mảng bytes</param>
        ''' <returns>Trả về giá trị mảng kiểu Word</returns>
        Public Shared Function ToArray(bytes As Byte()) As UInt16()
            Dim values As UInt16() = New UInt16(bytes.Length \ 2 - 1) {}
            Dim counter As Integer = 0
            For cnt As Integer = 0 To values.Length - 1 Step 2
                values(cnt) = FromByteArray(New Byte() {bytes(cnt), bytes(cnt + 1)})
            Next
            Return values
        End Function


#End Region

    End Class
End Namespace

Solution

  • Lets simplify your code considerably:

    Dim buffRecei() as Byte= { &H98, &H42, &H01, &H80, &H00, &H10, &Ha0, &H00, &H00 }
    
    Dim data As Byte() = New Byte(buffRecei.Length - 5) {}
    Console.WriteLine(BitConverter.ToString(buffRecei).Replace("-"," "))
    Array.Copy(buffRecei, 3, data, 0, data.Length)
    Console.WriteLine(BitConverter.ToString(data).Replace("-"," "))
    
    Dim result As UInt16() = ToArray(data)
    Dim txtResult as string
    For Each item As UInt16 In result
             txtResult += String.Format("{0} ", item.ToString())
    Next
    Console.WriteLine(txtResult)
    

    This outputs:

    98 42 01 80 00 10 A0 00 00
    80 00 10 A0 00
    32768 0 
    

    Hopefully this shows what is happening. You are throwing away the first 3 bytes with the Array.Copy and then converting 80 00 to an int16 (0x8000 = 32768 decimal).

    To get the result you want you can just do something like:

    Dim data() as Byte= { &H98, &H42, &H01, &H80, &H00, &H10, &Ha0, &H00, &H00 }
    Console.WriteLine(BitConverter.ToString(data).Replace("-"," "))
    
    Dim result As UInt16() = ToArray(data)
    Dim txtResult as string
    For Each item As UInt16 In result
             txtResult += String.Format("{0} ", item.ToString())
    Next
    Console.WriteLine(txtResult)
    
    ' Alternative
    dim val as UInt16 = BitConverter.ToUInt16(data, 0)
    Console.WriteLine(val)
    

    Which will output:

    98 42 01 80 00 10 A0 00 00
    38978 0 384 0 
    17048
    

    So why does your code output 38978, whereas the alternative I provided in the comments outputs 17048? The answer is endianness:

    • 0x9842 = 38978 decimal (big endian)
    • 0x4298 = 17048 decimal (little endian)

    You might assume that the first approach (Big Endian) is logical but different chips organise memory in different ways (and often send data over the serial link in the same order as held in memory). The code you are using makes an assumption that does not hold up (whereas BitConverter uses your systems endianess so the above code might have a different result on a different system).

    The manual you showed (note that this is NOT Modbus; the device may support Modbus but it's also supporting something else!) clearly states that the first byte is Low; so you would need to edit the function as follows:

    ' Old: values(cnt) = FromByteArray(New Byte() {bytes(cnt), bytes(cnt + 1)})
    values(cnt) = FromByteArray(New Byte() {bytes(cnt+1), bytes(cnt)})
    

    Here is my code in an online playground (so you can play around with it).