-
Notifications
You must be signed in to change notification settings - Fork 14
/
ioi.go
337 lines (299 loc) · 9.21 KB
/
ioi.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
package gologix
import (
"encoding/binary"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
)
type tagPartDescriptor struct {
FullPath string
BasePath string
Array_Order []int
BitNumber int
BitAccess bool
}
var bit_access_regex, _ = regexp.Compile(`\.\d+$`)
var array_access_regex, _ = regexp.Compile(`\[([\d]|[,]|[\s])*\]$`)
func (tag *tagPartDescriptor) Parse(tagpath string) error {
var err error
tag.FullPath = tagpath
tag.BasePath = tagpath
// Check if tag is accessing a bit in the data
bitpos := bit_access_regex.FindStringIndex(tagpath)
if bitpos == nil {
tag.BitAccess = false
} else {
tag.BitAccess = true
bit_access_text := tagpath[bitpos[0]+1 : bitpos[1]]
tag.BasePath = strings.ReplaceAll(tag.BasePath, bit_access_text, "")
tag.BitNumber, err = strconv.Atoi(bit_access_text)
if err != nil {
return fmt.Errorf("could't parse %v to a bit portion of tag. %w", bit_access_text, err)
}
}
// check if tag is accessing an array
arrpos := array_access_regex.FindStringIndex(tagpath)
if arrpos == nil {
tag.Array_Order = nil
} else {
arr_access_text := tagpath[arrpos[0]+1 : arrpos[1]-1]
tag.BasePath = strings.ReplaceAll(tag.BasePath, arr_access_text, "")
if strings.Contains(arr_access_text, ",") {
parts := strings.Split(arr_access_text, ",")
tag.Array_Order = make([]int, len(parts))
for i, part := range parts {
tag.Array_Order[i], err = strconv.Atoi(part)
if err != nil {
return fmt.Errorf("could't parse %v to an array position. %w", arr_access_text, err)
}
}
} else {
tag.Array_Order = make([]int, 1)
tag.Array_Order[0], err = strconv.Atoi(arr_access_text)
if err != nil {
return fmt.Errorf("could't parse %v to an array position. %w", arr_access_text, err)
}
}
}
return nil
}
// parse the tag name into its base tag (remove array index or bit) and get the array index if it exists
func parse_tag_name(tagpath string) (tag tagPartDescriptor, err error) {
err = tag.Parse(tagpath)
return
}
// Internal Object Identifier. Used to specify a tag name in the controller
// the Buffer has the CIP route for a tag path.
type tagIOI struct {
Path string
Type CIPType
BitAccess bool
BitPosition int
Buffer []byte
}
func (ioi *tagIOI) Write(p []byte) (n int, err error) {
ioi.Buffer = append(ioi.Buffer, p...)
return len(p), nil
}
func (ioi *tagIOI) Bytes() []byte {
return ioi.Buffer
}
func (ioi *tagIOI) Len() int {
return len(ioi.Buffer)
}
// this is the default buffer size for tag IOI generation.
const defaultIOIBufferSize = 256
// The IOI is the tag name structure that CIP requires. It's parsed out into tag length, tag name pairs with additional
// data on the backside to indicate what index is requested if needed.
func (client *Client) newIOI(tagpath string, datatype CIPType) (ioi *tagIOI, err error) {
client.ioi_cache_lock.Lock()
defer client.ioi_cache_lock.Unlock()
if client.ioi_cache == nil {
client.ioi_cache = make(map[string]*tagIOI)
}
ioi = new(tagIOI)
// CIP doesn't care about case. But we'll make it lowercase to match
// the encodings shown in 1756-PM020H-EN-P
tagpath = strings.ToLower(tagpath)
// on firmwares greater than 20, we can do some optimizations.
if client.firmware() > 20 {
tag_info, ok := client.KnownTags[tagpath]
if ok {
// we'll assume the user knows what they're doing if they're dumping data into a struct.
/*
if tag_info.Info.Type != datatype && (datatype != CIPTypeUnknown && datatype != CIPTypeStruct) {
err = fmt.Errorf("data type mismatch for IOI. %v was specified, but I have reason to believe that it's really %v", datatype, tag_info.Info.Type)
return
}
*/
if tag_info.Info.Type != 0 && datatype != CIPTypeStruct && datatype != CIPTypeUnknown && datatype != CIPTypeSTRING {
ioi.Buffer = tag_info.Bytes()
return
}
}
}
extant, exists := client.ioi_cache[tagpath]
if exists {
ioi = extant
return
}
tag_array := strings.Split(tagpath, ".")
ioi.Path = tagpath
ioi.Type = datatype
// we'll build this byte structure up as we go.
ioi.Buffer = make([]byte, 0, defaultIOIBufferSize)
for _, tag_part := range tag_array {
if strings.HasSuffix(tag_part, "]") {
// part of an array
start_index := strings.Index(tag_part, "[")
ioi_part, err := marshalIOIPart(tag_part[0:start_index])
if err != nil {
return nil, err
}
_, err = ioi.Write(ioi_part)
if err != nil {
return ioi, fmt.Errorf("problem writing ioi part %w", err)
}
t, err := parse_tag_name(tag_part)
if err != nil {
client.Logger.Warn("problem parsing path", "error", err)
}
for _, order_size := range t.Array_Order {
if order_size < 256 {
// byte, byte
index_part := []byte{byte(cipElement_8bit), byte(order_size)}
err = binary.Write(ioi, binary.LittleEndian, index_part)
if err != nil {
return nil, fmt.Errorf("problem reading index part. %w", err)
}
} else if order_size < 65536 {
// uint16, uint16
index_part := []uint16{uint16(cipElement_16bit), uint16(order_size)}
err = binary.Write(ioi, binary.LittleEndian, index_part)
if err != nil {
return nil, fmt.Errorf("problem reading index part. %w", err)
}
} else {
// uint16, uint32
index_part0 := []uint16{uint16(cipElement_32bit)}
err = binary.Write(ioi, binary.LittleEndian, index_part0)
if err != nil {
return nil, err
}
index_part1 := []uint32{uint32(order_size)}
err = binary.Write(ioi, binary.LittleEndian, index_part1)
if err != nil {
return nil, err
}
}
}
} else {
// not part of an array
bit_access, err := strconv.Atoi(tag_part)
if err == nil && bit_access <= 31 {
// This is a bit access.
// we won't do anything for now and will just parse the
// bit out of the word when that time comes.
ioi.BitAccess = true
ioi.BitPosition = bit_access
continue
}
ioi_part, err := marshalIOIPart(tag_part)
if err != nil {
return nil, err
}
_, err = ioi.Write(ioi_part)
if err != nil {
return nil, err
}
}
}
client.ioi_cache[tagpath] = ioi
return
}
func marshalIOIPart(tagpath string) ([]byte, error) {
t, err := parse_tag_name(tagpath)
if err != nil {
return nil, fmt.Errorf("could not parse tag path: %w", err)
}
tag_size := len(t.BasePath)
need_extend := false
if tag_size%2 == 1 {
need_extend = true
//tag_size += 1
}
tag_name_header := [2]byte{byte(segmentTypeExtendedSymbolic), byte(tag_size)}
tag_name_msg := append(tag_name_header[:], []byte(t.BasePath)...)
// has to be an even number of bytes.
if need_extend {
tag_name_msg = append(tag_name_msg, []byte{0x00}...)
}
return tag_name_msg, nil
}
// these next functions are for reversing the bytes back to a tag string
func getAsciiTagPart(item *CIPItem) (string, error) {
var tag_len byte
err := item.DeSerialize(&tag_len)
if err != nil {
return "", fmt.Errorf("problem getting tag len. %w", err)
}
b := make([]byte, tag_len)
err = item.DeSerialize(&b)
if err != nil {
return "", fmt.Errorf("problem reading tag path. %w", err)
}
if tag_len%2 == 1 {
var pad byte
err = item.DeSerialize(&pad)
if err != nil {
return "", fmt.Errorf("problem reading pad byte. %w", err)
}
}
tag_str := string(b)
return tag_str, nil
}
func getTagFromPath(item *CIPItem) (string, error) {
tag_str := ""
morepath:
for {
// we haven't read all the tag path info.
var tag_path_type byte
err := item.DeSerialize(&tag_path_type)
if err != nil {
if errors.Is(err, io.EOF) {
return tag_str, nil
}
return "", fmt.Errorf("couldn't get path part type: %w", err)
}
switch tag_path_type {
case 0x28:
// one byte index
var array_index byte
err = item.DeSerialize(&array_index)
if err != nil {
return "", fmt.Errorf("couldn't get array index: %w", err)
}
if tag_str[len(tag_str)-1] == ']' {
tag_str = fmt.Sprintf("%s,%d]", tag_str[:len(tag_str)-1], array_index)
} else {
tag_str = fmt.Sprintf("%s[%d]", tag_str, array_index)
}
case 0x29:
// two byte index
var pad byte
err = item.DeSerialize(&pad)
if err != nil {
return "", fmt.Errorf("couldn't get padding: %w", err)
}
var array_index uint16
err = item.DeSerialize(&array_index)
if err != nil {
return "", fmt.Errorf("couldn't get array index: %w", err)
}
if tag_str[len(tag_str)-1] == ']' {
tag_str = fmt.Sprintf("%s,%d]", tag_str[:len(tag_str)-1], array_index)
} else {
tag_str = fmt.Sprintf("%s[%d]", tag_str, array_index)
}
case 0x91:
// ascii portion of tag path
s, err := getAsciiTagPart(item)
if err != nil {
return "", fmt.Errorf("problem in ascii tag part: %w", err)
}
if tag_str == "" {
tag_str = s
} else {
tag_str = fmt.Sprintf("%s.%s", tag_str, s)
}
default:
// this byte does not indicate the tag path is continuing. go back by one in the item's data buffer to "unread" it.
item.Pos--
break morepath
}
}
return tag_str, nil
}