Namespace TCP.Frame
    ''' <summary>
    ''' Classe de base pour les trames Modbus
    ''' </summary>
    ''' <remarks></remarks>
    Friend MustInherit Class ClsFrameModbusBase
#Region "Classes d'exception"
        ''' <summary>
        ''' Classe signalant une erreur de longueur de trame
        ''' </summary>
        ''' <remarks></remarks>
        Public NotInheritable Class LenErrorException
            Inherits IncorrectFrameException
            ''' <summary>
            ''' Constructeur de la classe
            ''' </summary>
            ''' <param name="ptByte">Trame erronée</param>
            ''' <remarks></remarks>
            Public Sub New(ByVal ptByte As Byte())
                MyBase.New("Longueur de la trame incorrect", ptByte)
            End Sub
        End Class
        ''' <summary>
        ''' Signal que le format de la trame est incorrect
        ''' </summary>
        ''' <remarks></remarks>
        Public Class IncorrectFrameException
            Inherits Exception
            ''' <summary>
            ''' Constructeur
            ''' </summary>
            ''' <param name="pReason">Raison</param>
            ''' <param name="pFrame">Trame</param>
            ''' <remarks></remarks>
            Public Sub New(ByVal pReason As String, ByVal pFrame As Byte())
                MyBase.New(pReason + " : " + Array2String(pFrame))
            End Sub
        End Class
#End Region

#Region "Enumération de type"
        ''' <summary>
        ''' Type de trame
        ''' </summary>
        ''' <remarks></remarks>
        Public Enum EFrameType
            ''' <summary>
            ''' Inconnu
            ''' </summary>
            ''' <remarks></remarks>
            Unknow = 0
            ''' <summary>
            ''' Requête
            ''' </summary>
            ''' <remarks></remarks>
            Request
            ''' <summary>
            ''' Réponse
            ''' </summary>
            ''' <remarks></remarks>
            Response
            ''' <summary>
            ''' Erreur
            ''' </summary>
            ''' <remarks></remarks>
            [Error]
        End Enum
#End Region

        'Undone : à adapter pour TCP
        ''' <summary>
        ''' Retourne la trame correspondante aux données reçues (Nothing si inconnu ou incorrect)
        ''' </summary>
        ''' <param name="pFrame">Trame reçue</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Shared Function GetClassFromFrame(ByVal pFrame As Byte()) As ClsFrameModbusBase
            If pFrame.Length < 8 OrElse (pFrame(2) <> 0 Or pFrame(3) <> 0) Then
                Return Nothing
            End If
            Select Case pFrame(7)
                Case &H3
                    If pFrame.Length = (8 + 6 - 2) Then
                        Return New ClsFrameModbus03Req(pFrame, pFrame)
                    Else
                        Return New ClsFrameModbus03Rep(pFrame, pFrame)
                    End If
                Case &H83
                    Return New ClsFrameModbus03Err(pFrame, pFrame)
                Case &H4
                    If pFrame.Length = (8 + 6 - 2) Then
                        Return New ClsFrameModbus04Req(pFrame, pFrame)
                    Else
                        Return New ClsFrameModbus04Rep(pFrame, pFrame)
                    End If
                Case &H84
                    Return New ClsFrameModbus04Err(pFrame, pFrame)
                Case &H10
                    If pFrame.Length = (8 + 6 - 2) Then
                        Return New ClsFrameModbus16Rep(pFrame, pFrame)
                    Else
                        Return New ClsFrameModbus16Req(pFrame, pFrame)
                    End If
                Case &H90
                    Return New ClsFrameModbus16Err(pFrame, pFrame)
                Case Else
                    Return Nothing
            End Select
#If DEBUG Then
            'Si l'on arrive ici, c'est qu'il faut virer un commentaire
            Stop
#End If
            Return Nothing
        End Function

        ''' <summary>
        ''' Retourne la trame correspondante aux données reçues (Nothing si inconnu ou incorrect)
        ''' </summary>
        ''' <param name="pFrame">Trame reçue (ASCII)</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Shared Function GetClassFromFrame(ByVal pFrame As String) As ClsFrameModbusBase
            Dim ltBytes As New List(Of Byte)
            Dim lCtrl As String = "0123456789ABCDEF"
            Dim lStr As String = ""

            pFrame = pFrame.ToUpper

            For i As Integer = 0 To pFrame.Length - 1
                If lCtrl.Contains(pFrame.Substring(i, 1)) Then
                    lStr += pFrame.Substring(i, 1)
                End If
                If lStr.Length >= 2 Then
                    ltBytes.Add(Byte.Parse(lStr.Substring(0, 2), Globalization.NumberStyles.HexNumber))
                    If lStr.Length = 2 Then
                        lStr = ""
                    Else
                        lStr = lStr.Substring(2)
                    End If
                End If
            Next

            Return GetClassFromFrame(ltBytes.ToArray)
        End Function


        ''' <summary>
        ''' ID de l'esclave
        ''' </summary>
        ''' <remarks></remarks>
        Private mSlaveId As Byte
        ''' <summary>
        ''' Numéro de fonction
        ''' </summary>
        ''' <remarks></remarks>
        Private mFunction As Byte
        ''' <summary>
        ''' Mémorise l'identifiant de connexion
        ''' </summary>
        ''' <remarks></remarks>
        Private mTransId As UShort

        ''' <summary>
        ''' Constructeur de la classe
        ''' </summary>
        ''' <param name="pSlaveId">Numéro d'esclave</param>
        ''' <param name="pFunction">Numéro de fonction</param>
        ''' <remarks></remarks>
        Protected Sub New(ByVal pSlaveId As Byte, ByVal pFunction As Byte)
            mSlaveId = pSlaveId
            mFunction = pFunction
            mTransId = 0
        End Sub
        ''' <summary>
        ''' Constructeur de la classe
        ''' </summary>
        ''' <param name="pTransId">numéro de la transaction</param>
        ''' <param name="pSlaveId">Numéro d'esclave</param>
        ''' <param name="pFunction">Numéro de fonction</param>
        ''' <remarks></remarks>
        Protected Sub New(ByVal pTransId As UShort, ByVal pSlaveId As Byte, ByVal pFunction As Byte)
            mSlaveId = pSlaveId
            mFunction = pFunction
            mTransId = pTransId
        End Sub
        ''' <summary>
        ''' Récupère les données de la trame
        ''' </summary>
        ''' <param name="ptData">Trame à récupérer</param>
        ''' <remarks></remarks>
        Protected Sub New(ByRef ptData As Byte())
            If ptData.Length < 8 OrElse CInt(ptData(5)) + 6 <> ptData.Length Then
                Throw New LenErrorException(ptData)
            End If
            mTransId = (CUShort(ptData(0)) << 8) + CUShort(ptData(1))
            mSlaveId = ptData(6)
            mFunction = ptData(7)
            ptData = GetArrayRange(ptData, 8, ptData.Length - 8)
        End Sub

        ''' <summary>
        ''' Retourne ou définit le numéro d'esclave
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property SlaveId() As Byte
            Get
                Return mSlaveId
            End Get
            Set(ByVal value As Byte)
                mSlaveId = value
            End Set
        End Property
        ''' <summary>
        ''' Retourne le numéro de fonction
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property [Function]() As Byte
            Get
                Return mFunction
            End Get
        End Property
        ''' <summary>
        ''' Retourne ou définit l'Id de la transaction
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property TransactionId() As UShort
            Get
                Return mTransId
            End Get
            Set(ByVal value As UShort)
                mTransId = value
            End Set
        End Property

        ''' <summary>
        ''' Retourne la trame
        ''' </summary>
        ''' <param name="pData">Données à encapluser</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected Overridable Function GetFrame(ByVal pData As Byte()) As Byte()
            Dim ltByte As New List(Of Byte)
            With ltByte
                .Add(CByte(mTransId >> 8))
                .Add(CByte(mTransId And &HFF))
                .Add(0)
                .Add(0) 'undone : doute concernant la taille
                .Add(0)
                .Add(CByte(pData.Length + 2))
                .Add(mSlaveId)
                .Add(mFunction)
                .AddRange(pData)
            End With
            Return ltByte.ToArray
        End Function
        ''' <summary>
        ''' Retourne la trame Modbus
        ''' </summary>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected MustOverride Function GetFrame() As Byte()
        ''' <summary>
        ''' Retourne le type de trame
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public MustOverride ReadOnly Property FrameType() As EFrameType
        ''' <summary>
        ''' Retourne La trame au format ASCII
        ''' </summary>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Function GetFrameAscii() As String
            Dim ltByte As Byte() = GetFrame()
            Dim lStr As New Text.StringBuilder

            For i As Integer = 0 To ltByte.GetUpperBound(0)
                lStr.Append(ltByte(i).ToString("X2"))
            Next

            lStr.Append(vbCr)
            lStr.Append(vbLf)

            Return lStr.ToString
        End Function

#Region "CRC et opération tableaux"
        ''' <summary>
        ''' Calcule le CRC16 d'un tableau d'octet
        ''' </summary>
        ''' <param name="ptByte">Tableau à calculer</param>
        ''' <param name="pExistCRC">Signal si les 2 derniers octets  sont à ignorer (contenant déjà un CRC)</param>
        Private Function CalcCRC16(ByVal ptByte As Byte(), Optional ByVal pExistCRC As Boolean = False) As UShort
            Dim j As Integer
            Dim Crc As Integer
            Dim n As Integer
            Dim Retenue As Boolean

            Crc = &HFFFF
            For j = 0 To ptByte.GetUpperBound(0) - IIF(pExistCRC, 2, 0)
                Crc = Crc Xor CInt(ptByte(j))
                n = 0
                Do
                    Retenue = False
                    If (Crc Mod &H2) <> 0 Then
                        Retenue = True
                    End If
                    Crc = Crc \ 2
                    If Retenue Then
                        Crc = Crc Xor &HA001
                    End If
                    n = n + 1
                Loop While (n <= 7)
            Next j

            Return CUShort(Crc)
        End Function

        ''' <summary>
        ''' Convertit le tableau de 2 octets en UShort
        ''' </summary>
        ''' <param name="ptByte">Tableau de 2 octet</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected Function ByteArrayToUShortCRC(ByVal ptByte() As Byte) As UShort
            Dim i As Integer = ptByte.GetUpperBound(0)
            If i < 1 Then
                Return 0
            Else
                Return CUShort(CUShort(ptByte(i)) * 256) + ptByte(i - 1)
            End If
        End Function

        ''' <summary>
        ''' Convertit  un UShort en tableau de 2 octets
        ''' </summary>
        ''' <param name="pCRC">UShort à convertir</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected Function UShortToByteArrayCRC(ByVal pCRC As UShort) As Byte()
            Return New Byte() {CByte(pCRC And &HFF), CByte((pCRC And &HFF00) \ &H100)}
        End Function

        ''' <summary>
        ''' Retourne une partie d'un tableau
        ''' </summary>
        ''' <param name="ptByte">Tableau à traiter</param>
        ''' <param name="pOffset">Offset de départ</param>
        ''' <param name="pLength">Longueur</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected Function GetArrayRange(ByVal ptByte As Byte(), ByVal pOffset As Integer, ByVal pLength As Integer) As Byte()
            Dim ltByte As New List(Of Byte)
            ltByte.AddRange(ptByte)
            Return ltByte.GetRange(pOffset, pLength).ToArray
        End Function
#End Region
    End Class
End Namespace
