├── MdbusClient └── Main.go ├── GoModbusTCP ├── InputCoil.go ├── InputRegister.go ├── OutputCoil.go ├── HoldingRegister.go └── FactoryAndHandler.go └── README.md /MdbusClient/Main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "GoMdbus/GoModbusTCP" 7 | "flag" 8 | ) 9 | 10 | func main() { 11 | fmt.Println("Modbus Client Started") 12 | regType := flag.Uint("t", 4 ,"Register type") 13 | raddr := flag.Uint("r", 1 ,"Starting address") 14 | uaddr := flag.Uint("u", 2 ,"Unit address") 15 | operation := flag.String("o","W", "Read or Write operation") 16 | value := flag.Uint("v", 3 ,"Value to Write for write requests") 17 | dest := flag.String("dst","127.0.0.1:502", "Destination address") 18 | flag.Parse() 19 | requestHandler := GoModbusTCP.ModbusRequest{ 20 | *regType, 21 | *raddr, 22 | *uaddr, 23 | *operation, 24 | *value, 25 | *dest, 26 | } 27 | result, err := requestHandler.Handlerequest() 28 | checkError(err) 29 | if(result != nil) { 30 | fmt.Println("Operation results: ", result) 31 | } 32 | } 33 | 34 | 35 | 36 | func checkError(err error) { 37 | if (err != nil) { 38 | fmt.Println("Error occured", err) 39 | os.Exit(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /GoModbusTCP/InputCoil.go: -------------------------------------------------------------------------------- 1 | ///////////////////////////// 2 | //Author: Mina Andrawos////// 3 | //////////////////////////// 4 | 5 | package GoModbusTCP 6 | import ( 7 | "fmt" 8 | "encoding/binary" 9 | "errors" 10 | ) 11 | 12 | //Input Coil 13 | type InputCoil struct{ 14 | Seq uint16 15 | } 16 | 17 | func(R *InputCoil)ConstructWriteMessage(unitAddr byte, regAddr uint16, value uint16)(b []byte, err error) { 18 | return nil, errors.New("Input coils do not allow writes..") 19 | } 20 | 21 | func (R *InputCoil)ConstructReadMessage(unitAddr byte, regaddr uint16, length uint16) (b []byte, err error){ 22 | addrSlice := make([]byte,2) 23 | lenSlice := make([]byte, 2) 24 | binary.BigEndian.PutUint16(addrSlice, regaddr) 25 | binary.BigEndian.PutUint16(lenSlice, length) 26 | ReadMsg := []byte{0x00,0x00,0x00,0x00,0x00,0x06, unitAddr, 0x02 ,addrSlice[0], addrSlice[1],lenSlice[0], lenSlice[1]} 27 | fmt.Println(ReadMsg) 28 | return ReadMsg, nil 29 | } 30 | 31 | func (R *InputCoil) Readmessage(data []byte)(interface {}, error){ 32 | return nil,nil 33 | } 34 | -------------------------------------------------------------------------------- /GoModbusTCP/InputRegister.go: -------------------------------------------------------------------------------- 1 | ///////////////////////////// 2 | //Author: Mina Andrawos////// 3 | //////////////////////////// 4 | 5 | package GoModbusTCP 6 | 7 | import ( 8 | "fmt" 9 | "encoding/binary" 10 | "errors" 11 | ) 12 | 13 | //Input Register 14 | type IRMessage struct{ 15 | Seq uint16 16 | } 17 | 18 | func(R *IRMessage)ConstructWriteMessage(unitAddr byte, regAddr uint16, value uint16)(b []byte, err error) { 19 | return nil, errors.New("Input registers do not allow writes..") 20 | } 21 | 22 | func (R *IRMessage)ConstructReadMessage(unitAddr byte, regAddr uint16, length uint16) (b []byte, err error){ 23 | addrSlice := make([]byte,2) 24 | lenSlice := make([]byte, 2) 25 | binary.BigEndian.PutUint16(addrSlice, regAddr) 26 | binary.BigEndian.PutUint16(lenSlice, length) 27 | ReadMsg := []byte{0x00,0x00,0x00,0x00,0x00,0x06, unitAddr, 0x03 ,addrSlice[0], addrSlice[1],lenSlice[0], lenSlice[1]} 28 | fmt.Println(ReadMsg) 29 | return ReadMsg, nil 30 | } 31 | 32 | func (R *IRMessage) Readmessage(data []byte)(interface {}, error){ 33 | return nil,nil 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GoModbus 2 | ======== 3 | 4 | A TCP Modbus client in Go 5 | 6 | This is an expermintal TCP Modbus client written in Go for testing and protyping purposes. The code is in an alpha state, still needs some work and is provided as-is. Please keep my name in the files as the author if reused. 7 | 8 | What is Modbus? 9 | Modbus is a popular industrial device protocol used to communicate with numerous devices worldwide. A good primer on the protocol can be found at: http://www.simplymodbus.ca/FAQ.htm#Modbus 10 | 11 | Code structure: 12 | 1. ModbusClient folder is where the main package is, the entry point for the project 13 | 2. GoModbusTCP folder is where the Modbus TCP implementation resides. There is a file for each Modbus device type and a file that acts as a factory of device structs based on the user requests. This could evolve to become an API in the future 14 | 15 | Notes: 16 | 1. CRC not supported 17 | 2. If enough people find that interesting, I would like to evolve this to be a Go Modbus API 18 | 2. Code can be tested with Modbus simulators 19 | 3. I may write a tutorial in the future covering the design details when I get a chance. 20 | 4. Intrested for more material? check my website at www.minaandrawos.com 21 | -------------------------------------------------------------------------------- /GoModbusTCP/OutputCoil.go: -------------------------------------------------------------------------------- 1 | ///////////////////////////// 2 | //Author: Mina Andrawos////// 3 | //////////////////////////// 4 | 5 | package GoModbusTCP 6 | 7 | import ( 8 | "fmt" 9 | "encoding/binary" 10 | ) 11 | 12 | //Output Coil 13 | type OutputCoil struct{ 14 | Seq uint16 15 | } 16 | 17 | //TODO support multiple writes 18 | //Single coil only 19 | func(R *OutputCoil)ConstructWriteMessage(unitAddr byte, regAddr uint16, value uint16)(b []byte, err error) { 20 | addrSlice := make([]byte,2) 21 | valSlice := make([]byte, 2) 22 | binary.BigEndian.PutUint16(addrSlice, regAddr) 23 | binary.BigEndian.PutUint16(valSlice, value) 24 | writeMsg := []byte{0x00,0x00,0x00,0x00,0x00,0x06, unitAddr, 0x05 ,addrSlice[0], addrSlice[1],valSlice[0], valSlice[1]} 25 | fmt.Println(writeMsg) 26 | return writeMsg, nil 27 | } 28 | 29 | func (R *OutputCoil)ConstructReadMessage(unitAddr byte, regAddr uint16, length uint16) (b []byte, err error){ 30 | addrSlice := make([]byte,2) 31 | lenSlice := make([]byte, 2) 32 | binary.BigEndian.PutUint16(addrSlice, regAddr) 33 | binary.BigEndian.PutUint16(lenSlice, length) 34 | ReadMsg := []byte{0x00,0x00,0x00,0x00,0x00,0x06, unitAddr, 0x01 ,addrSlice[0], addrSlice[1],lenSlice[0], lenSlice[1]} 35 | fmt.Println(ReadMsg) 36 | return ReadMsg, nil 37 | } 38 | 39 | func (R *OutputCoil) Readmessage(data []byte)(interface {}, error){ 40 | return nil,nil 41 | } 42 | -------------------------------------------------------------------------------- /GoModbusTCP/HoldingRegister.go: -------------------------------------------------------------------------------- 1 | ///////////////////////////// 2 | //Author: Mina Andrawos////// 3 | //////////////////////////// 4 | 5 | package GoModbusTCP 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "encoding/binary" 11 | "bytes" 12 | ) 13 | 14 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 15 | //Holding Register 16 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | type HRMessage struct{ 19 | Seq uint16 20 | } 21 | 22 | //Write a single value 23 | //TODO support multiple writes 24 | func(R *HRMessage)ConstructWriteMessage(unitAddr byte, regAddr uint16 , value uint16)(b []byte, err error){ 25 | addrSlice := make([]byte,2) 26 | valSlice := make([]byte, 2) 27 | binary.BigEndian.PutUint16(addrSlice, regAddr) 28 | binary.BigEndian.PutUint16(valSlice, value) 29 | WriteMsg := []byte{0x00,0x00,0x00,0x00,0x00,0x06, unitAddr, 0x06,addrSlice[0], addrSlice[1],valSlice[0], valSlice[1]} 30 | return WriteMsg, nil 31 | } 32 | 33 | func (R *HRMessage)ConstructReadMessage(unitAddr byte, regAddr uint16, length uint16) (b []byte, err error){ 34 | addrSlice := make([]byte,2) 35 | lenSlice := make([]byte, 2) 36 | binary.BigEndian.PutUint16(addrSlice, regAddr) 37 | binary.BigEndian.PutUint16(lenSlice, length) 38 | ReadMsg := []byte{0x00,0x01,0x00,0x00,0x00,0x06, unitAddr, 0x03 ,addrSlice[0], addrSlice[1],lenSlice[0], lenSlice[1]} 39 | return ReadMsg, nil 40 | } 41 | 42 | func (R *HRMessage) Readmessage(data []byte)(interface {}, error){ 43 | fmt.Println("Processing response message... ") 44 | //if the length of the byte array is less than 6, then the message is invalid 45 | if len(data) < 6{ 46 | return nil, errors.New("Invalid response message size") 47 | } 48 | //Could panic 49 | length := binary.BigEndian.Uint16([]byte{data[4],data[5]}) 50 | fmt.Println("Message size: " , length) 51 | fmt.Println("Slave ID: ", data[6]) 52 | if data[7] == 0x03{ 53 | fmt.Println("Read response confirmed, proceeding with the read operation... ") 54 | } else { 55 | return nil, errors.New("Invalid function type in reponse message ") 56 | } 57 | nValues := int(data[8]/2) 58 | fmt.Println("Number of values available: ", nValues) 59 | values := make([]uint16, nValues) 60 | valuesbuf := bytes.NewReader(data[9:]) 61 | err := binary.Read(valuesbuf, binary.BigEndian, &values) 62 | return values,err 63 | } 64 | -------------------------------------------------------------------------------- /GoModbusTCP/FactoryAndHandler.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////// 2 | //factory for device structs// 3 | //Author: Mina Andrawos////// 4 | //////////////////////////// 5 | 6 | package GoModbusTCP 7 | 8 | import ( 9 | "os" 10 | "errors" 11 | "fmt" 12 | "net" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | DISCRETE_OUT_COIL = 0 18 | DISCRETE_IN_COIL = 1 19 | ANALOG_IN_REGISTER = 3 20 | ANALOG_HOLDING_REGISTER = 4 21 | ) 22 | 23 | type RegisterFactory interface { 24 | ConstructWriteMessage(byte,uint16, uint16) ([]byte,error) 25 | ConstructReadMessage(byte, uint16, uint16)([]byte,error) 26 | Readmessage(data []byte)(interface {}, error) 27 | } 28 | 29 | func CreateRegister(Type uint) (RegisterFactory, error){ 30 | switch Type{ 31 | case DISCRETE_OUT_COIL: 32 | return new(OutputCoil),nil 33 | case DISCRETE_IN_COIL: 34 | return new(InputCoil),nil 35 | case ANALOG_IN_REGISTER: 36 | return new(IRMessage),nil 37 | case ANALOG_HOLDING_REGISTER: 38 | return new(HRMessage),nil 39 | } 40 | return nil,errors.New("ERROR: Invalid modbus register type..") 41 | } 42 | 43 | //Everything must be upper case 44 | type ModbusRequest struct{ 45 | RegType uint 46 | Raddr uint 47 | Uaddr uint 48 | Operation string 49 | Value uint // value for write operations or length for read operations 50 | Dest string 51 | } 52 | 53 | func (mr * ModbusRequest)Handlerequest()(result interface{}, err error){ 54 | Register,err := CreateRegister(mr.RegType) 55 | checkError(err) 56 | mr.Operation = strings.ToUpper(mr.Operation) 57 | if mr.Operation == "W" { 58 | result,err = performWrites(Register, mr) 59 | } else if mr.Operation == "R"{ 60 | result, err = performReads(Register, mr) 61 | } else { 62 | //fmt.Println("Operation not defined!! It needs to be either R for read operations or W for write operations") 63 | return nil, errors.New("Operation not defined!! It needs to be either R for read operations or W for write operations..") 64 | } 65 | return result, err 66 | } 67 | 68 | func performReads(register RegisterFactory, mr *ModbusRequest )(interface{}, error){ 69 | readMsg,err := register.ConstructReadMessage(byte(mr.Uaddr), uint16(mr.Raddr), uint16(mr.Value)) 70 | checkError(err) 71 | rcvdData := handleMessage(mr.Dest, readMsg) 72 | return register.Readmessage(rcvdData) 73 | } 74 | 75 | func performWrites(register RegisterFactory, mr *ModbusRequest )(interface{}, error){ 76 | writeMsg,err := register.ConstructWriteMessage(byte(mr.Uaddr), uint16(mr.Raddr), uint16(mr.Value)) 77 | if(err!=nil){ 78 | return nil, err 79 | } 80 | rcvdData := handleMessage(mr.Dest, writeMsg) 81 | 82 | //If the received message is an echo of the sent write message then the write was successful 83 | return (rcvdData[0]!=0x86 && rcvdData[0]!=0x90), nil 84 | } 85 | 86 | func handleMessage(dst string , data []byte) (response []byte){ 87 | //sanity check 88 | if data == nil{ 89 | return 90 | } 91 | conn, err := net.Dial("tcp", dst) 92 | checkError(err) 93 | defer conn.Close() 94 | fmt.Println("Send data Message \n", data) 95 | _, err = conn.Write(data) 96 | checkError(err) 97 | fmt.Println("Received data Message") 98 | response = make([]byte,512) 99 | n,err := conn.Read(response) 100 | fmt.Println(response[1:n]) 101 | checkError(err) 102 | return 103 | } 104 | 105 | func checkError(err error){ 106 | if(err!=nil){ 107 | fmt.Println("Error occured", err) 108 | os.Exit(1) 109 | } 110 | } 111 | --------------------------------------------------------------------------------