#Golang-Raspberry | Chapter 10 : Modbus TCP (client dan server)
Hai warga nasi uduk, yang makannya sambil duduk dan mengantuk.
Pad chapter ini penulis bahas modbus tcp client dan server, sebelumnya kita udah banyak bahas terkait modbus rtu, penulis pikir untuk gak ada salahnya bahas yang modbus tcp, biar nambah referensi. Pada chapter kali ini penulis, main di lokal aja. Rekan-rekan bisa coba di laptop maupun di raspberry atau SBC yang lain.
stepnya:
- buka terminal
- git clone https://github.com/AndrianTriPutra/modbus_client-server.git modbus
Part 1. TCP Server
- cd modbus
- buka dir server dengan vs code
- perhatikan tree nya
- bukan file config.ini, sesuaikan config ini dengan kebutuhan rekan-rekan
- arahkan ke file main.go
- pertama kita ke init, di sana kita panggil func untuk baca config.ini
func init() { config.Setup("config.ini")}
- di main, kita gunakan beberapa fitur wait group, channel, signal notify, dan runtime gosched..
- perhatikan routine 1 berikut
go func() { defer wg.Done() server.ModbusServer() runtime.Gosched()}()
- routine di atas untuk modbus tcp server
- perhatikan routine 2 berikut
go func() { defer wg.Done() ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() counter.Counter(*ticker, stopchan) runtime.Gosched()}()
- routine di atas berfungsi untuk untuk mengolah data yang akan dikonsumsi server
func Counter
- arahkan ke dir counter/counter.go
- declare struct ModbuData
- register 1–4 untuk function code “holding register”
- regist 5–6 ntuk function code “input register”
- kemudian init awal register 1–6
- kemudian counter 1,2,4 dan reset jika lebih dari threshold
ModbusData.Addr_1++ModbusData.Addr_2++ModbusData.Addr_4++if ModbusData.Addr_1 >= 65535 { ModbusData.Addr_1 = 100} else if ModbusData.Addr_2 >= 65535 { ModbusData.Addr_2 = 500} else if ModbusData.Addr_4 >= 1000 { ModbusData.Addr_4 = -1000}
- counter untuk register 5,6 dan reset jika lebih dari threshold
ModbusData.Addr_5++ModbusData.Addr_6++if ModbusData.Addr_5 > 65535 { ModbusData.Addr_5 = 10000} else if ModbusData.Addr_6 > 10000 { ModbusData.Addr_6 = -10000}
- insert register 7,8 dengan timestamp
//current unix timeModbusData.UnixTs_s = uint32(time.Now().Unix() & 0xffffffff)//the 16 most significant bits of the current unix timeModbusData.Addr_7 = uint16((ModbusData.UnixTs_s >> 16) & 0xffff)//the 16 least significant bits of the current unix timeModbusData.Addr_8 = uint16(ModbusData.UnixTs_s & 0xffff)
- insert register 9, 10 dengan phi
ModbusData.Floting = math.Float32bits(3.1415)//return 3.1415, encoded as a 32-bit floating point number in inputModbusData.Addr_9 = uint16((ModbusData.Floting >> 16) & 0xffff)// returh the 16 least significant bits of the numberModbusData.Addr_10 = uint16(ModbusData.Floting & 0xffff)
- function ini akan di eksekusi per detik
func ModbusServer
- arahkan ke server/server.go
- define struct exampleHandler
- buat object handler
eh = &exampleHandler{}
- buat objek server
server, err := modbus.NewServer(&modbus.ServerConfiguration{ // listen on url URL: config.ServerSetting.Url, // close idle connections after xs of inactivity Timeout: config.ServerSetting.Timeout, // accept max concurrent connections max. MaxClients: uint(config.ServerSetting.MaxClients),}, eh)if err != nil { log.Printf("failed to create server: %v\n", err) os.Exit(1)}
- start connection
err = server.Start()if err != nil { log.Printf("failed to start server: %v\n", err) os.Exit(1)} else { log.Println("starting server")}
- handler yang penulis gunakan saat ini hanya holding register dan input register
- arahkan ke server/HandleHoldingRegisters.go
- recheck slave id
if req.UnitId != uint8(config.ServerSetting.SlaveID) { err = modbus.ErrIllegalFunction return}
- di case 1, hanya read. Data ModbusData.Addr_1 di append ke res
- di case 2, read and write dan data di buffer di ModbusData.Addr_2
- di case 3, read and write dan data di buffer di ModbusData.Addr_3. di case ini data yg di accept hanya 1/0. di sini penulis asumsikan register ini untuk engine, 1 untuk ON dan 0 untuk OFF
- di case 4, read and write dan data di buffer di ModbusData.Addr_4. di case ini tipe data yang digunakan int16
- arahkan ke server/HandleInputRegisters.go
- recheck slave id
- case 5, case 6 , hanya read. Data ModbusData.Addr_5 di append ke res begitupun Data ModbusData.Addr_6
- case 7, hanya read. Data most timestamp ModbusData.Addr_7 di append ke res
- case 8, hanya read. Data least timestamp ModbusData.Addr_8 di append ke res
- case 9, hanya read. Data most phi ModbusData.Addr_9 di append ke res
- case 10, hanya read. Data least phi ModbusData.Addr_10 di append ke res
- switch ke terminal
- go run .
Part 2. TCP Client
- buka dir client dengan vs code
- perhatikan tree nya
- di sini penulis menggunakan 2 library, yaitu goburrow dan simmonvetter. walaupun simmonvetter berdependency dengan goburrow kita pake aja.
- kelebihan simmonvetter yaitu data yang di return bukan lagi byte tapi sudah bisa uint16/uint32 atau float32, jadi akan memudahkan
- register yang akan penulis query yaitu 1–4 untuk holding register dan 5–10 untuk input register, jika lebih dari itu makan ilegal register.
- untuk func disini penulis pisahkan menjadi goburrow read dan write, dan simmon read dan write
- jadi untuk eksekusinya bisa dengan :
- “go run . 1” untuk goburrow read
- “go run . 2” untuk goburrow write
- “go run . 3” untuk simmon read
- “go run . 4” untuk simmon write
goburrow
- pertama penulis bahas yang read terlebih dahulu
- arahkan ke goburrow.go
- perhatikan ini function goburrowRead, untuk bahas metode read
hex, data, err := goburrow.Read("localhost:5502", 1, timeout)
- function read diatas akan me return value hex dan data bertipe data slice string
- slice itu saya ubah ke string dulu agar gampang diprint dan mudah dibacanya
- kemudian data register 7 dan 8 yang di buffer di array index 6 kita balikan kembali ke format time
intTime, _ := strconv.ParseInt(resultData[6], 10, 64)//ts := time.Unix(intTime, 0).UTC()ts := time.Unix(intTime, 0)timestamp := ts.Format(time.RFC3339)
- penulis bahas agak dikit
- arahkan ke goburrow/read.go
- perhatikan init handler ini
handler := modbus.NewTCPClientHandler(host)handler.Timeout = timeouthandler.SlaveId = slave_id
- sejauh ini yang penulis lihat, yang membedakan rtu dan tcp hanya di handler ini.
- pada register 1–4 function code nya holding register dan register 4 di parsing ke int16
- untuk register 5–10 function code nya input register
- pada register 7–8, yang membedakan data return diparsing ke uint32
RIu32s := util.BytesToUint32s(util.BIG_ENDIAN, util.HIGH_WORD_FIRST, databyte)
- karena uint32 jadi data 7–8 dimarge
- pada register 9–10, data byte yang direturn diparsing ke float32
RIf32s := util.BytesToFloat32s(util.BIG_ENDIAN, util.HIGH_WORD_FIRST, databyte)
- switch ke terminal “go run . 1”
- kemudian penulis switch ke metode write
- arahkan ke goburrow.go ke function goburrowWrite
- perhatikan function ini
goburrow.Write("localhost:5502", 1, timeout)
- function di atas dipanggil dari goburrow/write.go
- perhatikan berikut
var dw = [3]int{700, 0, 50}var output []bytevar outputFinal []bytefor i := 0; i < 3; i++ { output = intToHex(int64(dw[i])) output = append(output[:0], output[6:]...) outputFinal = append(outputFinal, output...)}
- penulis siapkan variabel array int yang berisi 3 value “700,0,50”
- kemudian data tersebut di parsing ke hex satu per satu dan kemudian di simpan di output final
- kemudian penulis write ke register 2–4
_, err = client.WriteMultipleRegisters(2, 3, outputFinal)
- di sini menggunakan multiple register karena ada 3 register yang diwrite
- jika yang mau di write hanya 1 register, misal register 2
var dw = [3]int{700}output = intToHex(int64(dw[0]))output = append(output[:0], output[6:]...)outputFinal = append(outputFinal, output...)
- kemudian write
_, err = client.WriteMultipleRegisters(2, 1, outputFinal)
- bisa juga menggunakan write single register
_, err = client.WriteSingleRegister(2, 700)
- switch ke terminal, “go run . 2”
- penulis kira sudah cukup yang goburrow, sekarang switch ke simmonvetter
simmonvetter
- arahkan ke file simmon.go function simmonRead untuk bahas read
- perhatikan ini
hex, data, err := simmon.Read("tcp://localhost:5502", 1, timeout)
- function read diatas akan me return value hex dan data bertipe data slice string
- index 6 diparsing kembali ke format waktu
- kemudian arahkan ke simmon/read.go
- perhatikan ini cara buka client baru
client, err := modbus.NewClient(&modbus.ClientConfiguration{ URL: host, Timeout: timeout,})
- set slave_id/unit_id
client.SetUnitId(slave_id)
- untuk register 1–4 cara query nya seperti di bawah, dengan return nya uint16
RHu16s, err := client.ReadRegisters(start, length, modbus.HOLDING_REGISTER)
- untuk register 5–6 cara query nya seperti di bawah, dengan return nya uint16
RIu16s, err := client.ReadRegisters(start, length, modbus.INPUT_REGISTER)
- untuk register 7–8 cara query nya seperti di bawah, dengan return nya uint32
RIu32, err := client.ReadUint32(start, modbus.INPUT_REGISTER)
- untuk register 9–10 cara query nya seperti di bawah, dengan return nya float32
RIf32s, err := client.ReadFloat32(start, modbus.INPUT_REGISTER)
- switch ke terminal, “go run . 3”
- ok, sekarang penulis switch ke write
- arahkan ke simmon.go func simmonWrite
- di sini memanggil write dari dir simmon
simmon.Write("tcp://localhost:5502", 1, timeout)
- penulis bahas sedikit, arahkan ke simmon/write.go
- perhatikan ini
var val []uint16val = append(val, 800) //reg 2val = append(val, 1) //reg 3val = append(val, 70) //reg 4err = client.WriteRegisters(2, val)
- disini penulis siapkan variabel val slice uint16, yang diisi beberapa value
- kemudian value tadi diwrite ke register 2–4
- karena menggunakan “s” berarti ke banyak register
- jika hendak ke satu register bisa seperti berikut
err = client.WriteRegister(2, 800)
- switch ke terminal, “go run . 4”
- saya kira cukup untuk bahas yang versi simmon
Selain modbus rtu, modbus tcp ini cukup banyak digunakan. Misalnya pada PM schneider 5500, tornatech engine maupun electric.
OK, cukup sekian dulu untuk chapter ini, sorry kalo panjang penjelasannya. Semoga bermanfaat untuk kita sebagai warga nasi uduk.