1 /**
2  * Fins client
3  *
4  *	Copyright: © 2016-2026 Orfeo Da Vià.
5  *	License: Boost Software License - Version 1.0 - August 17th, 2003
6  *	Authors: Orfeo Da Vià
7  */
8 module dfins.fins;
9 
10 import dfins.channel;
11 
12 /**
13  * Fins protocol exception
14  */
15 class FinsException : Exception {
16    /**
17     * Constructor which takes two error codes.
18     *
19     * Params:
20     *  mainCode = Main error code
21     *  subCode = Sub error code
22     *  file = Fine name
23     *  line = Line number
24     */
25    this(ubyte mainCode, ubyte subCode, string file = null, size_t line = 0) @trusted {
26       _mainCode = mainCode;
27       _subCode = subCode;
28       import std.string : format;
29 
30       super("Fins error %s".format(mainErrToString(_mainCode)));
31    }
32 
33    private ubyte _mainCode;
34    /**
35     * Main error code
36     */
37    ubyte mainCode() {
38       return _mainCode;
39    }
40 
41    private ubyte _subCode;
42    /**
43     * Sub error code
44     */
45    ubyte subCode() {
46       return _subCode;
47    }
48 }
49 
50 /**
51  * Memory area code. See page 15 of $(I FINS Commands reference manual)
52  */
53 enum MemoryArea : ubyte {
54    CIO_BIT = 0x30,
55    W_BIT = 0x31,
56    H_BIT = 0x32,
57    A_BIT = 0x33,
58    D_BIT = 0x02,
59    /**
60     * CIO Channel IO area, word
61     */
62    IO = 0xB0,
63    /**
64     * WR Work area, word
65     */
66    WR = 0xB1,
67    /*
68     * HR Holding area, word
69     */
70    HR = 0xB2,
71    /**
72     * AR Auxiliary Relay area, word
73     */
74    AR = 0xB3,
75    /**
76     * DM Data Memory area, word
77     */
78    DM = 0x82,
79    /**
80     * CNT, Counter area, word
81     */
82    CT = 0x89
83 }
84 
85 ///
86 enum FINS_HEADER_LEN = 12;
87 
88 ///
89 struct Header {
90    /**
91     * Information Control Field, set to 0x80
92     */
93    ubyte icf = 0x80;
94    //ubyte rsv;//reserved, set to 0x00
95    //ubyte gct = 0x02;//gateway count, set to 0x02
96    /**
97     * Destination network address, 0x0 local , 0x01 if there are not network intermediaries
98     */
99    ubyte dna;
100    /**
101     * Destination node number.
102     *
103     * If set to default this is the subnet byte of the ip of the plc
104     * Examples:
105     * --------------------
106     * ex. 192.168.0.1 -> 0x01
107     * --------------------
108     */
109    ubyte da1;
110    /**
111     * Destination unit number
112     *
113     * The unit number, see the hardware config of plc, generally 0x00
114     */
115    ubyte da2;
116    /**
117     * Source network
118     *
119     * generally 0x01
120     */
121    ubyte sna;
122    /**
123     * Source node number.
124     *
125     * Like the destination node number, you could set a fixed number into plc config
126     */
127    ubyte sa1 = 0x02;
128    /**
129     * Source unit number.
130     *
131     * Like the destination unit number.
132     */
133    ubyte sa2;
134    /**
135     * Counter for the resend.
136     *
137     * Generally 0x00
138     */
139    ubyte sid;
140    /**
141     * Main request code (high byte).
142     */
143    ubyte mainRqsCode;
144    /**
145     * Sub request code.
146     */
147    ubyte subRqsCode;
148 }
149 
150 /**
151  * Convenience function for creating an `Header` with destination node number (`da1`) and source node number (`sa1`).
152  *
153  * `da1` is the subnet byte of the ip of the plc
154  *
155  * Params:
156  *  dstNodeNumber = Destination node number
157  *  srcNodeNumber = Source node number
158  */
159 Header header(ubyte dstNodeNumber, ubyte srcNodeNumber = 0x02) {
160    Header h;
161    h.da1 = dstNodeNumber;
162    h.sa1 = srcNodeNumber;
163    return h;
164 }
165 
166 unittest {
167    immutable(Header) hdr = header(0x11);
168    assert(hdr.icf == 0x80);
169    assert(hdr.dna == 0x0);
170    assert(hdr.da1 == 0x11);
171    assert(hdr.sa1 == 0x02);
172    immutable(Header) hdr2 = header(0x10, 0x42);
173    assert(hdr2.da1 == 0x10);
174    assert(hdr2.sa1 == 0x42);
175 }
176 
177 /**
178  * Get subnet (`da1`) from ip address.
179  *
180  * Examples:
181  * --------------------
182  * enum IP = "192.168.1.42";
183  * IChannel chan = createUdpChannel(IP, 2000);
184  * Header h = header(IP.getSubnet); // subnet == da1 == 42
185  * FinsClient f = new FinsClient(chan, h);
186  * --------------------
187  */
188 ubyte getSubnet(string ip) @safe {
189    import std.regex : regex, matchFirst;
190    import std.conv : to;
191    import std.exception : enforce;
192 
193    auto ipReg = regex(
194          r"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
195    auto m = matchFirst(ip, ipReg);
196    enforce(!m.empty && m.length > 4, "Invalid ip");
197    return m[4].to!ubyte;
198 }
199 
200 unittest {
201    import std.exception : assertThrown;
202 
203    assert("192.168.221.64".getSubnet == 64);
204    assert("192.168.221.1".getSubnet == 1);
205    assert("192.168.22.2".getSubnet == 2);
206 
207    assertThrown("192.168.221".getSubnet);
208    assertThrown("400.168.221.1".getSubnet);
209    assertThrown("".getSubnet);
210    assertThrown("asas".getSubnet);
211 }
212 
213 /**
214  * Convert an `Header` to array of bytes.
215  */
216 ubyte[] toBytes(Header data) {
217    enum RSV = 0x0;
218    enum GCT = 0x02;
219    ubyte[] b;
220    b ~= data.icf;
221    b ~= RSV;
222    b ~= GCT;
223    b ~= data.dna;
224    b ~= data.da1;
225    b ~= data.da2;
226    b ~= data.sna;
227    b ~= data.sa1;
228    b ~= data.sa2;
229    b ~= data.sid;
230    b ~= data.mainRqsCode;
231    b ~= data.subRqsCode;
232    return b;
233 }
234 
235 unittest {
236    Header data;
237    data.dna = 0;
238    data.da1 = 0x16;
239    data.da2 = 0;
240    data.sna = 0;
241    data.sa1 = 0x02;
242    data.sa2 = 0;
243    data.mainRqsCode = 0x01;
244    data.subRqsCode = 0x01;
245 
246    ubyte[] exp = [0x80, 0x00, 0x02, 0x00, 0x16, 0x0, 0x00, 0x02, 0x0, 0x0, 0x01, 0x01];
247    auto b = data.toBytes;
248    assert(b.length == FINS_HEADER_LEN);
249 
250    import std.conv : to;
251 
252    for (int i = 0; i < FINS_HEADER_LEN; ++i) {
253       assert(b[i] == exp[i], i.to!string());
254    }
255 }
256 
257 /**
258  * Converts an array of bytes to `Header`
259  */
260 Header toHeader(ubyte[] blob)
261 in {
262    assert(blob.length >= FINS_HEADER_LEN, "Blob too short (less than 12)");
263 }
264 do {
265    Header h;
266    h.icf = blob[0];
267    h.dna = blob[3];
268    h.da1 = blob[4];
269    h.da2 = blob[5];
270    h.sna = blob[6];
271    h.sa1 = blob[7];
272    h.sa2 = blob[8];
273    h.sid = blob[9];
274    h.mainRqsCode = blob[10];
275    h.subRqsCode = blob[11];
276    return h;
277 }
278 
279 unittest {
280    ubyte[] blob = [0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02];
281    immutable(Header) h = blob.toHeader;
282    assert(h.icf == 0xC0);
283    assert(h.dna == 0x0);
284    assert(h.da1 == 0x2);
285    assert(h.da2 == 0x0);
286    assert(h.sna == 0x0);
287    assert(h.sa1 == 0x16);
288    assert(h.sa2 == 0x0);
289    assert(h.sid == 0x0);
290    assert(h.mainRqsCode == 0x01);
291    assert(h.subRqsCode == 0x02);
292 }
293 
294 ///
295 struct ResponseData {
296    /**
297     * Response header
298     */
299    Header header;
300    /**
301     * Main response code
302     */
303    ubyte mainRspCode;
304    /**
305     * Sub response code
306     */
307    ubyte subRspCode;
308    /**
309     * Payload
310     */
311    ubyte[] text;
312 }
313 
314 /**
315  * Converts an array of bytes to `ResponseData` structure
316  */
317 ResponseData toResponse(ubyte[] data)
318 in {
319    assert(data.length > FINS_HEADER_LEN + 1, "Invalid data length");
320 }
321 do {
322    ResponseData resp;
323    resp.header = data.toHeader;
324    resp.mainRspCode = data[12];
325    resp.subRspCode = data[13];
326    resp.text = data[14 .. $];
327    return resp;
328 }
329 
330 unittest {
331    // dfmt off
332    ubyte[] blob = [
333       0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02,
334       0x42, 0x43,  // rsp code
335       0x64, 0x65, 0x66, 0x67, 0x68, 0x69 // data
336    ];
337    // dfmt on
338    ResponseData r = blob.toResponse;
339    assert(r.header.icf == 0xC0);
340    assert(r.header.dna == 0x0);
341    assert(r.header.da1 == 0x2);
342    assert(r.header.da2 == 0x0);
343    assert(r.header.sna == 0x0);
344    assert(r.header.sa1 == 0x16);
345    assert(r.header.sa2 == 0x0);
346    assert(r.header.sid == 0x0);
347    assert(r.header.mainRqsCode == 0x01);
348    assert(r.header.subRqsCode == 0x02);
349 
350    assert(r.mainRspCode == 0x42);
351    assert(r.subRspCode == 0x43);
352    assert(r.text == [0x64, 0x65, 0x66, 0x67, 0x68, 0x69]);
353 
354    //import std.exception : assertThrown;
355    //ubyte[] invalid = [0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02];
356    //assertThrown( invalid.toResponse());
357    ubyte[] nodata = [0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02, 0x42, 0x43];
358    assert(nodata.toResponse.text == []);
359 }
360 
361 /**
362  * Returs an array with start address and size.
363  */
364 private ubyte[] getAddrBlock(ushort start, ushort size) {
365    ubyte[] cmdBlock;
366 
367    //beginning address
368    cmdBlock ~= cast(ubyte)(start >> 8);
369    cmdBlock ~= cast(ubyte)start;
370    cmdBlock ~= 0x00;
371    cmdBlock ~= cast(ubyte)(size >> 8);
372    cmdBlock ~= cast(ubyte)size;
373    return cmdBlock;
374 }
375 
376 /**
377  * Client for Fins protocol
378  */
379 class FinsClient {
380    private IChannel channel;
381    private Header header;
382    /**
383     * Constructor which takes a channel and header.
384     */
385    this(IChannel channel, Header header) {
386       assert(channel !is null);
387       this.channel = channel;
388       this.header = header;
389    }
390 
391    /**
392     * Write an Omron PLC area: the area must be defined as CJ like area
393     *
394     * Params:
395     *  area = The area type
396     *  start = The start offset for the write process.
397     *  buffer = The byte array buffer which will be write in the PLC.
398     */
399    void writeArea(MemoryArea area, ushort start, ubyte[] buffer)
400    in {
401       assert((buffer.length & 1) == 0, "Odd buffer length");
402    }
403    do {
404       import std.conv : to;
405       enum BYTES_PER_WORD = 2;
406 
407       ubyte[] text;
408       //memory area code
409       text ~= cast(ubyte)area;
410       //IMPORTANT: The size is expressed in WORD (2 byte)
411       immutable(ushort) size = (buffer.length / BYTES_PER_WORD).to!ushort;
412       text ~= getAddrBlock(start, size);
413       text ~= buffer;
414       sendFinsCommand(0x01, 0x02, text);
415    }
416 
417    /**
418     * Read an Omron PLC area
419     *
420     * Params:
421     *  area = The area type
422     *  start = The start offset for the read process.
423     *  size = The size of the area to read. IMPORTANT: The size is expressed in WORD (2 byte)
424     *
425     * Returns:
426     * The byte array buffer in which will be store the PLC readed area.
427     */
428    ubyte[] readArea(MemoryArea area, ushort start, ushort size) {
429       ubyte[] cmdBlock;
430 
431       //memory area code
432       cmdBlock ~= cast(ubyte)area;
433       cmdBlock ~= getAddrBlock(start, size);
434       return sendFinsCommand(0x01, 0x01, cmdBlock);
435    }
436 
437    private ubyte[] sendFinsCommand(ubyte mainCode, ubyte subCode, ubyte[] comText) {
438       header.mainRqsCode = mainCode;
439       header.subRqsCode = subCode;
440 
441       ubyte[] sendFrame = header.toBytes() ~ comText;
442       ubyte[] receiveFrame = channel.send(sendFrame);
443 
444       ResponseData response = receiveFrame.toResponse();
445       if (response.mainRspCode != 0) {
446          throw new FinsException(response.mainRspCode, response.subRspCode);
447       }
448       return response.text;
449    }
450 }
451 
452 /**
453  * Converts main error code into string
454  */
455 string mainErrToString(ubyte mainErr) {
456    switch (mainErr) {
457       case 0x01:
458          return "Local node error";
459       case 0x02:
460          return "Destination node error";
461       case 0x03:
462          return "Communications controller error";
463       case 0x04:
464          return "Not executable";
465       case 0x05:
466          return "Routing error";
467       case 0x10:
468          return "Command format error";
469       case 0x11:
470          return "Parameter error";
471       case 0x20:
472          return "Read not possible";
473       case 0x21:
474          return "Write not possible";
475       case 0x22:
476          return "Not executable in current mode";
477       case 0x23:
478          return "No Unit";
479       case 0x24:
480          return "Start/stop not possible";
481       case 0x25:
482          return "Unit error";
483       case 0x26:
484          return "Command error";
485       case 0x30:
486          return "Access right error";
487       case 0x40:
488          return "Abort";
489       default:
490          return "Unknown error";
491    }
492 }
493 
494 /**
495  * Convenience functions that create an `FinsClient` object.
496  *
497  * Examples:
498  * --------------------
499  * FinsClient f = createFinsClient("192.168.1.1", 2000, 9600);
500  * --------------------
501  *
502  * Params:
503  *  ip = IP address
504  *  timeout = Send and receive timeout in ms
505  *  port = Port number (default 9600)
506  *  srcNodeNumber = Source node number
507  */
508 FinsClient createFinsClient(string ip, long timeout, ushort port = 9600, ubyte srcNodeNumber = 0x02)
509 in {
510    assert(ip.length);
511    assert(timeout >= 0);
512 }
513 do {
514    import dfins.channel : IChannel, createUdpChannel;
515 
516    IChannel chan = createUdpChannel(ip, timeout, port);
517    Header h = header(ip.getSubnet, srcNodeNumber);
518    return new FinsClient(chan, h);
519 }