使用golang gomodbus 開發,控制巧力電表

下面這是我們要控制的設備,它本身包含RS485的通訊界面,並使用Modbus RTU作為通訊協定,

電表外觀
電表modbus的規格

我們可以用電腦或一個開發版,例如Raspberry pi作為溝通的主機。以下程式說明如何連上設備、並對設備進行讀取,然後將設備的slaveID從01改成02,注意,此設備要求進行寫入動作時,需要先寄送一個驗證碼,並於驗證成功後才能進行寫入,因此在此範例中,我們會用到3(讀取寄存器)、16(寫入多個寄存器)、22(Mask寫入操作、這邊用來作為驗證用)這三個功能碼。

輸入的Modbus RTU 命令 Hex形式 分別如下 (關於Modbus RTU 命令請參考這裡)

讀取日期時間: 01 03 00 12 00 04 e4 0c

發送驗證: 01 16 08 08 00 02 04 1d 2c 3b 4a 20 b4

修改表號(從01變成02): 01 10 00 0e 00 01 02 00 02 26 bf

golang的程式碼如下:

package main

import (
	"fmt"
	"time"

	modbus "github.com/thinkgos/gomodbus/v2"
)

func main(){
    //根據規格,先設定連線
    p := modbus.NewRTUClientProvider()
    p.Address = "COM3"
    p.BaudRate = 9600
    p.DataBits = 8
    p.Parity = "N"
    p.StopBits = 1
    p.Timeout = 1 * modbus.SerialDefaultTimeout

    //產生client並連線
    client := modbus.NewClient(p)
    client.LogMode(true)
    err := client.Connect()
    if err != nil {
	fmt.Println("connect failed, ", err)
	return
    }
    defer client.Close()

    //進行讀取日期時間:    01 03 00 12 00 04 e4 0c
    fmt.Println("讀取日期時間 start")
    result, err := client.ReadHoldingRegisters(1, 18, 4)
    if err != nil {
	fmt.Println(err.Error())
    } else {
        fmt.Printf("讀取日期時間結果 %#v\r\n", result)

    }
    fmt.Println("確認日期時間 finished")

    //進行發送驗證:  01 16 08 08 00 02 04 1d 2c 3b 4a 20 b4
    fmt.Println("寄送金鑰start")
    aduRequest := []byte{1, 22, 8, 8, 0, 2, 4, 29, 44, 59, 74, 32, 180} 
    
    aduResponse, err := client.SendRawFrame(aduRequest)
    if err != nil {
	fmt.Println(err.Error()) // 會出現timeout,但實際上成功了
    } else {
	fmt.Println(aduResponse)
    }
    fmt.Println("金鑰驗證 finished")

    //進行修改表號(從01變成02): 01 10 00 0e 00 01 02 00 02 26 bf
    fmt.Println("修改表號start")
    err = client.WriteMultipleRegisters(1, 14, 1, []uint16{2})
    if err != nil {
	fmt.Println(err.Error())
    }
    fmt.Println("WriteMultipleResigers finished")

}

說明:

  • 對於功能碼03、16的部分,可以直接使用gomodbus的函數,分別是ReadHoldingRegisters及WriteMultipleRegisters,根據原始命令轉乘函數的輸入參數即可達成(如上述程式碼,須注意函數的輸入參數多為byte 或是uint16的格式,我們需要將原本的hex digit的命令轉成byte或是uint16,例如0e轉成14、b4轉成180 etc.)。
  • 關於發送驗證的部分,使用的功能碼為22( hex digit: 16),我們查詢功能碼可得知為Mask Write Register(關於此功能碼較為複雜,包含了又AND Mask及 OR Mask對寄存器進行操作寫入,這邊不降息說明,若有興趣可以google一下囉),gomodbus的函數為 client.MaskWriteRegister(),但使用後發現會timeout,研究後發現原來此款巧力電表中的驗證方式與原本的Mask Write Register一般的定義稍有不同。
  • 一般的Mask Write Register的cmd為 01 16 08 08 1d 2c 3b 4a CRC CRC,但我們這邊電表的驗證命令為01 16 08 08 00 02 04 1d 2c 3b 4a CRC CRC,可看出多了中間三碼,導致原本的gomodbus MaskWriteRegister函數無法達成我們的需求,無法傳送出正確的命令。
  • 因此,我們使用gomodbus的另一個函數 SendRawFrame(),來直接傳送我們的Modbus RTU cmd給設備,這邊要注意的是需要我們自己處理CRC的計算(也可以直接copy gomodbus中的 CRC.go內的函數)
  • 另外,由於設備回傳的訊息格式也跟一般的不一樣,因此SendRawFrame後會出現timeout,這是因為gomodbus的SendRawFrame要去讀取回復的訊息,但因為回復訊息的長度不一樣,導致一直等不到要讀的內容,以我們的使用情境來講,此時實際上已經完成驗證,不需要去處理回覆訊息長度的問題,可直接進行下一步。
  • 若不希望有上述timeout的產生,就必須回過頭來修改gomodbus的source code了,若有需求或有興趣的朋友再留言給我,我可以再詳細說明~~
  • 提醒一下,表號從01改成02後,再使用上述的程式時記得修改呼叫的命令的slaveID(第一碼),以及因為命令改過後的CRC,不要像筆者一樣第二次測試時不會動覺得很奇怪。