Skip to content

Commit 3e87651

Browse files
authored
Merge pull request #105 from grid-x/feat/modbus-device-identification
feat(modbus): implement Read Device Identification (0x2B/0x0E)
2 parents 4b99c91 + 9f38c52 commit 3e87651

File tree

5 files changed

+186
-25
lines changed

5 files changed

+186
-25
lines changed

api.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,13 @@ type Client interface {
4949
//ReadFIFOQueue reads the contents of a First-In-First-Out (FIFO) queue
5050
// of register in a remote device and returns FIFO value register.
5151
ReadFIFOQueue(ctx context.Context, address uint16) (results []byte, err error)
52+
53+
// Byte access
54+
55+
// ReadDeviceIdentification reads the device identification objects using Modbus Function Code 0x2B (MEI type 0x0E).
56+
// It supports Basic, Regular, and Extended identification requests, including handling multi-part responses via
57+
// the "More Follows" field and iteratively fetching objects until all are received.
58+
ReadDeviceIdentification(ctx context.Context, readDeviceIDCode ReadDeviceIDCode) (results [][]byte, err error)
59+
// ReadDeviceIdentification behaves like ReadDeviceIdentification but with an Object ID offset
60+
ReadDeviceIdentificationWithObjectIDOffset(ctx context.Context, readDeviceIDCode ReadDeviceIDCode, objectIDOffset int) (results [][]byte, err error)
5261
}

client.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,111 @@ func (mb *client) ReadFIFOQueue(ctx context.Context, address uint16) (results []
505505
return
506506
}
507507

508+
// Request:
509+
//
510+
// Function code : 1 byte (0x2B)
511+
// MEI Type : 1 byte (0x0E)
512+
// Read Device ID Code : 1 byte (01 for basic, 02 for regular, 03 for extended, 04 for specific)
513+
// Object ID : 1 byte (0x00 to 0xFF)
514+
//
515+
// Response:
516+
//
517+
// Function code : 1 byte (0x2B)
518+
// MEI Type : 1 byte (0x0E)
519+
// Read Device ID Code : 1 byte (01 for basic, 02 for regular, 03 for extended, 04 for specific)
520+
// Conformity level : 1 byte (0x01 / 0x02 / 0x03 / 0x81 / 0x82 / 0x83)
521+
// More Follows : 1 byte (0x00 for no, 0xFF for yes)
522+
// Next Object ID : 1 byte
523+
// Number of Objects : 1 byte
524+
// List of (length = Number of Objects):
525+
// Object ID : 1 byte
526+
// Object length : 1 byte
527+
// Object value : Object length (see above)
528+
func (mb *client) ReadDeviceIdentification(ctx context.Context, readDeviceIDCode ReadDeviceIDCode) (results [][]byte, err error) {
529+
return mb.ReadDeviceIdentificationWithObjectIDOffset(ctx, readDeviceIDCode, 0)
530+
}
531+
532+
func (mb *client) ReadDeviceIdentificationWithObjectIDOffset(ctx context.Context, readDeviceIDCode ReadDeviceIDCode, objectIDOffset int) (results [][]byte, err error) {
533+
var objectID byte
534+
switch readDeviceIDCode {
535+
case ReadDeviceIDCodeBasic:
536+
objectID = 0x00
537+
case ReadDeviceIDCodeRegular:
538+
objectID = 0x03
539+
case ReadDeviceIDCodeExtended:
540+
objectID = 0x80
541+
default:
542+
return nil, fmt.Errorf("unsupported readDeviceIDCode %d", readDeviceIDCode)
543+
}
544+
545+
objectID += byte(objectIDOffset)
546+
547+
return mb.readDeviceIdentificationWithObjectID(ctx, readDeviceIDCode, objectID)
548+
}
549+
550+
func (mb *client) readDeviceIdentificationWithObjectID(ctx context.Context, readDeviceIDCode ReadDeviceIDCode, objectID byte) (results [][]byte, err error) {
551+
const meiType = meiTypeReadDeviceIdentification
552+
553+
request := ProtocolDataUnit{
554+
FunctionCode: FuncCodeReadDeviceIdentification,
555+
Data: []byte{byte(meiType), byte(readDeviceIDCode), objectID},
556+
}
557+
558+
response, err := mb.send(ctx, &request)
559+
if err != nil {
560+
return nil, err
561+
}
562+
563+
if got, want := len(response.Data), 6; got < want {
564+
return nil, fmt.Errorf("missing required headers, got %d, want %d", got, want)
565+
}
566+
567+
moreFollows := response.Data[3]
568+
nextObjectID := response.Data[4]
569+
numObjects := int(response.Data[5])
570+
571+
offset := 5
572+
for i := 0; i < numObjects; i++ {
573+
// Object ID is not required
574+
offset++
575+
576+
// Read object length
577+
offset++
578+
if len(response.Data)-1 < offset {
579+
return nil, fmt.Errorf("missing object length for object #%d", i)
580+
}
581+
objectLength := response.Data[offset]
582+
583+
// Read object value
584+
offset++
585+
end := offset + int(objectLength)
586+
if len(response.Data) < end {
587+
return nil, fmt.Errorf("data too short to read object #%d at index %d", i, end)
588+
}
589+
objectValue := response.Data[offset:end]
590+
591+
// Set new offset for next iteration
592+
offset = end - 1
593+
594+
results = append(results, objectValue)
595+
}
596+
597+
if moreFollows != 0xFF {
598+
return results, nil
599+
}
600+
601+
if nextObjectID == 0x00 {
602+
return results, nil
603+
}
604+
605+
nextResults, err := mb.readDeviceIdentificationWithObjectID(ctx, readDeviceIDCode, nextObjectID)
606+
if err != nil {
607+
return nil, err
608+
}
609+
610+
return append(results, nextResults...), nil
611+
}
612+
508613
// Helpers
509614

510615
// send sends request and checks possible exception in the response.

cmd/modbus-cli/main.go

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,21 @@ func main() {
5050
flag.BoolVar(&opt.rtu.rs485.rxDuringTx, "rs485-rxDuringTx", false, "Allow bidirectional rx during tx")
5151

5252
var (
53-
register = flag.Int("register", -1, "")
54-
fnCode = flag.Int("fn-code", 0x03, "fn")
55-
quantity = flag.Int("quantity", 2, "register quantity, length in bytes")
56-
ignoreCRCError = flag.Bool("ignore-crc", false, "ignore crc")
57-
eType = flag.String("type-exec", "uint16", "")
58-
pType = flag.String("type-parse", "raw", "type to parse the register result. Use 'raw' if you want to see the raw bits and bytes. Use 'all' if you want to decode the result to different commonly used formats.")
59-
writeValue = flag.Float64("write-value", math.MaxFloat64, "")
60-
readParseOrder = flag.String("read-parse-order", "", "order to parse the register that was read out. Valid values: [AB, BA, ABCD, DCBA, BADC, CDAB]. Can only be used for 16bit (1 register) and 32bit (2 registers). If used, it will overwrite the big-endian or little-endian parameter.")
61-
writeParseOrder = flag.String("write-exec-order", "", "order to execute the register(s) that should be written to. Valid values: [AB, BA, ABCD, DCBA, BADC, CDAB]. Can only be used for 16bit (1 register) and 32bit (2 registers). If used, it will overwrite the big-endian or little-endian parameter.")
62-
parseBigEndian = flag.Bool("order-parse-bigendian", true, "t: big, f: little")
63-
execBigEndian = flag.Bool("order-exec-bigendian", true, "t: big, f: little")
64-
filename = flag.String("filename", "", "")
65-
logframe = flag.Bool("log-frame", false, "prints received and send modbus frame to stdout")
53+
register = flag.Int("register", -1, "")
54+
fnCode = flag.Int("fn-code", 0x03, "fn")
55+
quantity = flag.Int("quantity", 2, "register quantity, length in bytes")
56+
ignoreCRCError = flag.Bool("ignore-crc", false, "ignore crc")
57+
eType = flag.String("type-exec", "uint16", "")
58+
pType = flag.String("type-parse", "raw", "type to parse the register result. Use 'raw' if you want to see the raw bits and bytes. Use 'all' if you want to decode the result to different commonly used formats.")
59+
writeValue = flag.Float64("write-value", math.MaxFloat64, "")
60+
readParseOrder = flag.String("read-parse-order", "", "order to parse the register that was read out. Valid values: [AB, BA, ABCD, DCBA, BADC, CDAB]. Can only be used for 16bit (1 register) and 32bit (2 registers). If used, it will overwrite the big-endian or little-endian parameter.")
61+
writeParseOrder = flag.String("write-exec-order", "", "order to execute the register(s) that should be written to. Valid values: [AB, BA, ABCD, DCBA, BADC, CDAB]. Can only be used for 16bit (1 register) and 32bit (2 registers). If used, it will overwrite the big-endian or little-endian parameter.")
62+
parseBigEndian = flag.Bool("order-parse-bigendian", true, "t: big, f: little")
63+
execBigEndian = flag.Bool("order-exec-bigendian", true, "t: big, f: little")
64+
filename = flag.String("filename", "", "")
65+
logframe = flag.Bool("log-frame", false, "prints received and send modbus frame to stdout")
66+
readDeviceIDCode = flag.Int("device-id-code", 0x01, "Read Device ID Code (01 for basic, 02 for regular, 03 for extended, 04 for specific)")
67+
objectIDOffset = flag.Int("object-id-offset", 0, "Starting Object ID offset for device identification")
6668
)
6769

6870
flag.Parse()
@@ -73,10 +75,12 @@ func main() {
7375
}
7476

7577
logger := slog.Default()
76-
if *register > math.MaxUint16 || *register < 0 {
77-
intRegister := *register
78-
logger.Error("invalid register value: " + strconv.Itoa(intRegister))
79-
os.Exit(-1)
78+
if *fnCode != modbus.FuncCodeReadDeviceIdentification {
79+
if *register > math.MaxUint16 || *register < 0 {
80+
intRegister := *register
81+
logger.Error("invalid register value: " + strconv.Itoa(intRegister))
82+
os.Exit(-1)
83+
}
8084
}
8185

8286
startReg := uint16(*register)
@@ -113,7 +117,7 @@ func main() {
113117

114118
client := modbus.NewClient(handler)
115119

116-
result, err := exec(ctx, client, eo, *writeParseOrder, *register, *fnCode, *writeValue, *eType, *quantity)
120+
result, err := exec(ctx, client, eo, *writeParseOrder, *register, *fnCode, *writeValue, *eType, *quantity, *readDeviceIDCode, *objectIDOffset)
117121
if err != nil && strings.Contains(err.Error(), "crc") && *ignoreCRCError {
118122
logger.Info("ignoring crc error: %+v\n", err)
119123
} else if err != nil {
@@ -122,13 +126,21 @@ func main() {
122126
}
123127

124128
var res string
125-
switch *pType {
126-
case "raw":
127-
res, err = resultToRawString(result, int(startReg))
128-
case "all":
129-
res, err = resultToAllString(result)
129+
switch *fnCode {
130+
case modbus.FuncCodeReadDeviceIdentification:
131+
results := bytes.Split(result, []byte{'\n'})
132+
for i, result := range results {
133+
res += fmt.Sprintf("ObjectID[%d]: %s\n", i, string(result))
134+
}
130135
default:
131-
res, err = resultToString(result, po, *readParseOrder, *pType)
136+
switch *pType {
137+
case "raw":
138+
res, err = resultToRawString(result, int(startReg))
139+
case "all":
140+
res, err = resultToAllString(result)
141+
default:
142+
res, err = resultToString(result, po, *readParseOrder, *pType)
143+
}
132144
}
133145

134146
if err != nil {
@@ -158,6 +170,8 @@ func exec(
158170
wval float64,
159171
etype string,
160172
quantity int,
173+
readDeviceIDCode int,
174+
objectIDOffset int,
161175
) ([]byte, error) {
162176
var err error
163177
var result []byte
@@ -192,6 +206,12 @@ func exec(
192206
result, err = client.ReadInputRegisters(ctx, uint16(register), uint16(quantity))
193207
case 0x03:
194208
result, err = client.ReadHoldingRegisters(ctx, uint16(register), uint16(quantity))
209+
case modbus.FuncCodeReadDeviceIdentification:
210+
objects, err := client.ReadDeviceIdentificationWithObjectIDOffset(ctx, modbus.ReadDeviceIDCode(readDeviceIDCode), objectIDOffset)
211+
if err != nil {
212+
return nil, err
213+
}
214+
result = bytes.Join(objects, []byte("\n"))
195215
default:
196216
err = fmt.Errorf("function code %d is unsupported", fnCode)
197217
}

modbus.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ const (
3636
FuncCodeMaskWriteRegister = 22
3737
// FuncCodeReadFIFOQueue 16-bit wise access
3838
FuncCodeReadFIFOQueue = 24
39+
// FuncCodeReadDeviceIdentification for byte wise access
40+
FuncCodeReadDeviceIdentification = 43
41+
)
42+
43+
// meiType specifies a MEI Type as defined in https://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf#page=44
44+
type meiType byte
45+
46+
const (
47+
// meiTypeReadDeviceIdentification is used together with FuncCodeReadDeviceIdentification
48+
meiTypeReadDeviceIdentification meiType = 14
49+
)
50+
51+
// ReadDeviceIDCode specifies a Read Device ID Code as defined in https://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf#page=45
52+
type ReadDeviceIDCode byte
53+
54+
const (
55+
// ReadDeviceIDCodeBasic queries for VendorName, ProductCode, and MajorMinorRevision.
56+
ReadDeviceIDCodeBasic ReadDeviceIDCode = iota + 1
57+
58+
// ReadDeviceIDCodeRegular queries for VendorURL, ProductName, ModelName, and UserApplicationName.
59+
ReadDeviceIDCodeRegular
60+
61+
// ReadDeviceIDCodeExtended queries for regular and private (custom) objects.
62+
ReadDeviceIDCodeExtended
63+
64+
// ReadDeviceIDCodeSpecific // Currently unsupported
3965
)
4066

4167
const (

rtuclient.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,8 @@ func calculateResponseLength(adu []byte) int {
321321
length += 4
322322
case FuncCodeMaskWriteRegister:
323323
length += 6
324-
case FuncCodeReadFIFOQueue:
324+
case FuncCodeReadFIFOQueue,
325+
FuncCodeReadDeviceIdentification:
325326
// undetermined
326327
default:
327328
}

0 commit comments

Comments
 (0)