:Namespace MODBUS
⍝ Driver for serial Modbus communication in RTU Mode
⍝
⍝ The following command are supported
⍝ FORCE_COIL_ON    = Force ON a Single Coil
⍝ FORCE_COIL_OFF   = Force OFF a Single Coil
⍝ READ_REG_FLOAT   = Read One FLOAT Register
⍝ READ_BYTE        = Read Multiple Registers as Bytes
⍝ PRESET_REG_FLOAT = Write a Float Value to a Register
⍝ PRESET_REG_INT   = Write an Integer Value to a Register
⍝
⍝ See the comments of the individual methods for more information


    (⎕IO ⎕ML ⎕WX)←1 3 3   ⍝ System variables

    I2H2←{'0123456789ABCDEF'[⎕IO+16 16⊤⍵]}              ⍝ Convert one Integer (0-255) to 2 Bytes Hexadecimal
    I2H4←{,⍉'0123456789ABCDEF'[⎕IO+16 16⊤255 255⊤⍵]}    ⍝ Convert one Integer (0-65535) to 4 Bytes Hexadecimal
    H22B←{16⊥¨16|(-⎕IO)+(⊂'0123456789ABCDEF0123456789abcdef')⍳¨((⍴⍵)⍴2 1)⊂⍵}   ⍝ Convert a pair of HEX characters to Bytes (0-255)


    ∇ r←sendObj FORCE_COIL_ON(modbusAddr coilNumber)
      :Access Public
    ⍝ To Force ON a Single Coil
    ⍝
    ⍝ sendObj    = SerialPort or TCP object
    ⍝ modbusAddr = Address of MODBUS device
    ⍝ coilNumber = Coil Number
    ⍝
    ⍝ r[1] = 1 for Success, 0 for Failure
    ⍝ r[2] = Value if Success or Literal Error if failure
     
      r←I2H2 modbusAddr       ⍝ Address of MODBUS device
      r,←'05'                 ⍝ Force Single Coil Command (5 decimal)
      r,←I2H4 coilNumber-1    ⍝ Coil Number (coil #1 is 0 in the program)
      r,←'FF00'               ⍝ Coil ON hex command
      r,←CRC_H r              ⍝ Checksum Calculation
      r←H22B r                ⍝ To Bytes
      r←sendObj.SendReceive r ⍝ Communication
     
    ⍝ To Exit on Communication Error
      :If 0=1⊃r ⋄ r←0(2⊃r) ⋄ →0 ⋄ :Else ⋄ r←2⊃r ⋄ :End
     
    ⍝ To Check if Byte Count is Correct
      :If 7=⍴,r ⋄ r←0 'Byte Count Incorrect Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if the CRC of the Answer is Correct
      :If ~(CRC_D ¯2↓r)≡¯2↑r
          r←0 'CRC Error on Answer' ⋄ →0
      :End
     
    ⍝ To Check if the Meter Address is Present
      :If modbusAddr≠r[1] ⋄ r←0 'Modbus Address Not Present Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Command Number is Present
      :If 131=r[2] ⋄ r←0 'Command Number Error' ⋄ →0 ⋄ :End
      :If 5≠r[2] ⋄ r←0 'Command Number Not Present' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Coil ON is in the answer
      :If 255 0≢r[5 6] ⋄ r←0 'Error in answer' ⋄ →0 ⋄ :End
     
    ⍝ Success
      r←1
    ∇


    ∇ r←sendObj FORCE_COIL_OFF(modbusAddr coilNumber)
      :Access Public
    ⍝ To Force OFF a Single Coil
    ⍝
    ⍝ sendObj    = SerialPort or TCP object
    ⍝ modbusAddr = Address of MODBUS device
    ⍝ coilNumber = Coil Number
    ⍝
    ⍝ r[1] = 1 for Success, 0 for Failure
    ⍝ r[2] = Value if Success or Literal Error if failure
     
      r←I2H2 modbusAddr       ⍝ Address of MODBUS device
      r,←'05'                 ⍝ Force Single Coil Command (5 decimal)
      r,←I2H4 coilNumber-1    ⍝ Coil Number (coil #1 is 0 in the program)
      r,←'0000'               ⍝ Coil OFF hex command
      r,←CRC_H r              ⍝ Checksum Calculation
      r←H22B r                ⍝ To Bytes
      r←sendObj.SendReceive r ⍝ Communication
     
    ⍝ To Exit on Communication Error
      :If 0=1⊃r ⋄ r←0(2⊃r) ⋄ →0 ⋄ :Else ⋄ r←2⊃r ⋄ :End
     
    ⍝ To Check if Byte Count is Correct
      :If 7=⍴,r ⋄ r←0 'Byte Count Incorrect Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if the CRC of the Answer is Correct
      :If ~(CRC_D ¯2↓r)≡¯2↑r
          r←0 'CRC Error on Answer' ⋄ →0
      :End
     
    ⍝ To Check if the Meter Address is Present
      :If modbusAddr≠r[1] ⋄ r←0 'Modbus Address Not Present Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Command Number is Present
      :If 131=r[2] ⋄ r←0 'Command Number Error' ⋄ →0 ⋄ :End
      :If 5≠r[2] ⋄ r←0 'Command Number Not Present' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Coil OFF is in the answer
      :If 0 0≢r[5 6] ⋄ r←0 'Error in answer' ⋄ →0 ⋄ :End
     
    ⍝ Success
      r←1
    ∇


    ∇ r←sendObj READ_BYTE(modbusAddr regAddr regQty)
      :Access Public
    ⍝ Read Register(s) as Bytes
    ⍝
    ⍝ sendObj    = SerialPort or TCP object
    ⍝ modbusAddr = Address of MODBUS device
    ⍝ regAddr    = Starting register
    ⍝ regQty     = Number of registers to read (2 bytes per registers)
    ⍝
    ⍝ r[1] = 1 for Success, 0 for Failure
    ⍝ r[2] = Bytes in register(s) if Success or Literal Error if failure
     
      r←I2H2 modbusAddr       ⍝ Address of MODBUS device
      r,←'03'                 ⍝ Read Holding Register Command (3 decimal)
      r,←I2H4 regAddr-40001   ⍝ Starting Address (address 0 is 40001)
      r,←I2H4 regQty          ⍝ Number of Registers To Read
      r,←CRC_H r              ⍝ Checksum Calculation
      r←H22B r                ⍝ Hex To Bytes
      r←sendObj.SendReceive r ⍝ Communication
     
    ⍝ To Exit on Communication Error
      :If 0=1⊃r ⋄ r←0(2⊃r) ⋄ →0 ⋄ :Else ⋄ r←2⊃r ⋄ :End
     
     ⍝ To Check if Byte Count is Correct
      :If 3>⍴,r
      :OrIf r[3]≠¯5+⍴,r
          r←0 'Byte Count Incorrect Error' ⋄ →0
      :End
     
    ⍝ To Check if the CRC of the Answer is Correct
      :If ~(CRC_D ¯2↓r)≡¯2↑r
          r←0 'CRC Error on Answer' ⋄ →0
      :End
     
    ⍝ To Check if the Modbus Address is Present
      :If modbusAddr≠r[1] ⋄ r←0 'Modbus Address Not Present Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Command Number is Present
      :If 131=r[2] ⋄ r←0 'Command Number Error' ⋄ →0 ⋄ :End
      :If 3≠r[2] ⋄ r←0 'Command Number Not Present' ⋄ →0 ⋄ :End
     
    ⍝ Success
      r←1(3↓¯2↓r)
    ∇


    ∇ r←sendObj READ_REG_FLOAT(modbusAddr regAddr)
      :Access Public
    ⍝ Read One FLOAT Register of a MODBUS device
    ⍝
    ⍝ sendObj    = SerialPort or TCP object
    ⍝ modbusAddr = Address of MODBUS device
    ⍝ regAddr    = Address of Register
    ⍝
    ⍝ r[1] = 1 for Success, 0 for Failure
    ⍝ r[2] = Value if Success or Literal Error if failure
     
      r←I2H2 modbusAddr       ⍝ Address of MODBUS device
      r,←'03'                 ⍝ Read Holding Register Command (3 decimal)
      r,←I2H4 regAddr-40001   ⍝ Starting Address
      r,←'0002'               ⍝ 2 Registers To Read
      r,←CRC_H r              ⍝ Checksum Calculation
      r←H22B r                ⍝ To Bytes
      r←sendObj.SendReceive r ⍝ Communication
     
    ⍝ To Exit on Communication Error
      :If 0=1⊃r ⋄ →0 ⋄ :Else ⋄ r←2⊃r ⋄ :End
     
    ⍝ To Check if Byte Count is Correct
      :If 3>⍴,r
      :OrIf r[3]≠¯5+⍴,r
          r←0 'Byte Count Incorrect Error' ⋄ →0
      :End
     
    ⍝ To Check if the CRC of the Answer is Correct
      :If ~(CRC_D ¯2↓r)≡¯2↑r
          r←0 'CRC Error on Answer' ⋄ →0
      :End
     
    ⍝ To Check if the Address of MODBUS device is Present
      :If modbusAddr≠r[1] ⋄ r←0 'Serial Address of MODBUS device Not Present Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Command Number is Present
      :If 131=r[2] ⋄ r←0 'Command Number Error' ⋄ →0 ⋄ :End
      :If 3≠r[2] ⋄ r←0 'Command Number Not Present' ⋄ →0 ⋄ :End
     
    ⍝ Success
      r←3↓¯2↓r
      r←1,(FLOAT_TO_DEC r[⍳4])
    ∇


    ∇ r←sendObj PRESET_REG_FLOAT(modbusAddr regAddr value)
      :Access Public
    ⍝ Write a Float Value to a single Register
    ⍝
    ⍝ sendObj    = SerialPort or TCP object
    ⍝ modbusAddr = Address of MODBUS device
    ⍝ regAddr    = Address of Register
    ⍝ value      = Value
    ⍝
    ⍝ r[1] = 1 for Success, 0 for Failure
    ⍝ r[2] = Literal Error if Failure
     
      r←I2H2 modbusAddr       ⍝ Serial Address of MODBUS device
      r,←'10'                 ⍝ Preset Register Command (16 decimal)
      r,←I2H4 regAddr-40001   ⍝ Starting Address
      r,←'0002'               ⍝ 2 Registers To Write
      r,←'02'                 ⍝ Byte Count
      r,←DEC_TO_FLOAT value   ⍝ Decimal to Hex
      r,←CRC_H r              ⍝ Checksum Calculation
      r←H22B r                ⍝ Hex To Bytes
      r←sendObj.SendReceive r ⍝ Communication
     
    ⍝ To Exit on Communication Error
      :If 0=1⊃r ⋄ →0 ⋄ :Else ⋄ r←2⊃r ⋄ :End
     
    ⍝ To Check if the CRC of the Answer is Correct
      :If ~(CRC_D ¯2↓r)≡¯2↑r
          r←0 'CRC Error on Answer' ⋄ →0
      :End
     
    ⍝ To Check if the Address of MODBUS device is Present
      :If modbusAddr≠r[1] ⋄ r←0 'Serial Address of MODBUS device Not Present Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Command Number is Present
      :If 131=r[2] ⋄ r←0 'Command Number Error' ⋄ →0 ⋄ :End
      :If 16≠r[2] ⋄ r←0 'Command Number Not Present' ⋄ →0 ⋄ :End
     
    ⍝ Success
      r←1
    ∇


    ∇ r←sendObj PRESET_REG_INT(modbusAddr regAddr value)
      :Access Public
    ⍝ Write an Integer Value to a single Register
    ⍝
    ⍝ sendObj    = SerialPort or TCP object
    ⍝ modbusAddr = Address of MODBUS device
    ⍝ regAddr    = Address of Register
    ⍝ value      = Value (0 - 65535)
    ⍝
    ⍝ r[1] = 1 for Success, 0 for Failure
    ⍝ r[2] = Literal Error if Failure
     
      r←I2H2 modbusAddr       ⍝ Serial Address of MODBUS device
      r,←'10'                 ⍝ Preset Register Command
      r,←I2H4 regAddr-40001   ⍝ Starting Address
      r,←'0001'               ⍝ 1 Registers To Write
      r,←'02'                 ⍝ Byte Count
      r,←I2H4 value           ⍝ Integer to Hex
      r,←CRC_H r              ⍝ Checksum Calculation
      r←H22B r                ⍝ Hex To Bytes
      r←sendObj.SendReceive r ⍝ Communication
     
    ⍝ To Exit on Communication Error
      :If 0=1⊃r ⋄ →0 ⋄ :Else ⋄ r←2⊃r ⋄ :End
     
    ⍝ To Check if the CRC of the Answer is Correct
      :If ~(CRC_D ¯2↓r)≡¯2↑r
          r←0 'CRC Error on Answer' ⋄ →0
      :End
     
    ⍝ To Check if the Address of MODBUS device is Present
      :If modbusAddr≠r[1] ⋄ r←0 'Address of MODBUS device Not Present Error' ⋄ →0 ⋄ :End
     
    ⍝ To Check if Command Number is Present
      :If 131=r[2] ⋄ r←0 'Command Number Error' ⋄ →0 ⋄ :End
      :If 16≠r[2] ⋄ r←0 'Command Number Not Present' ⋄ →0 ⋄ :End
     
    ⍝ No error
      r←1
    ∇


    ∇ r←CRC_D v;i;ii;lsb
    ⍝ Calculate the CRC of a Decimal String
      r←16⍴1
      :For i :In ⍳⍴v
          r[⎕IO+7+⍳8]←(¯8↑r)≠,⍉(8⍴2)⊤v[i] ⍝ XOR est ≠
     
          :For ii :In ⍳8
              lsb←¯1↑r ⋄ r←0,15↑r
              :If 1=lsb
                  r←r≠1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1
              :End
          :End
      :End
      r←⌽2⊥⍉2 8⍴r
    ∇


    ∇ r←CRC_H v;hex;i;ii;lsb
    ⍝ Calculate the CRC of a Hexadecimal String
      hex←'0123456789ABCDEF' ⋄ v←(-⎕IO)+hex⍳,v~' ' ⋄ r←16⍴1
      :For i :In ¯1+2×⍳0.5×⍴v
          r[⎕IO+7+⍳8]←(¯8↑r)≠,⍉2 2 2 2⊤v[i,i+1] ⍝ XOR est ≠
     
          :For ii :In ⍳8
              lsb←¯1↑r ⋄ r←0,15↑r
              :If 1=lsb
                  r←r≠1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1
              :End
          :End
      :End
      r←2⌽hex[⎕IO+2⊥⍉4 4⍴r]
    ∇


    ∇ r←FLOAT_TO_DEC v;exponent;sign;significand
    ⍝ Convert a Float Quadlet (32 bits) to Decimal Number
      v←,(⍉(8⍴2)⊤v)[3 4 1 2;]
      sign←,1↑[1]⍉v
      exponent←2⊥8↑[1]1↓[1]⍉v
      significand←2⊥¯23↑[1]⍉v
      r←(¯1*sign)×(1+significand÷2*23)×2*(exponent-127)
      r←(0≠+/[1]⍉v)×r
    ∇


    ∇ r←DEC_TO_FLOAT v;exponent;sign;significand
    ⍝ Convert a Decimal Number to a Float Quadlet
    ⍝ THE 2*¯127 IS TO TAKE CARE OF ZERO (ERROR IF ⍟0)
      sign←¯1=×v
      exponent←⍉(8⍴2)⊤127+⌊2⍟|v⍝+2*¯127
      significand←⍉(23⍴2)⊤⌊(2*23)ׯ1+2*1|2⍟|v⍝+2*¯127
      r←sign,exponent,significand
      r←4⌽'0123456789ABCDEF'[⎕IO+2⊥⍉8 4⍴r]
    ∇

:EndNamespace