1 /**
2  * Fins client
3  */
4 module dfins.fins;
5 
6 import dfins.channel;
7 
8 /**
9  * Fins protocol exception
10  */
11 class FinsException : Exception {
12    this(ubyte mainCode, ubyte subCode, string file = null, size_t line = 0) @trusted {
13       _mainCode = mainCode;
14       _subCode = subCode;
15       import std..string : format;
16 
17       super("Fins error %s".format(mainErrToString(_mainCode)));
18    }
19 
20    private ubyte _mainCode;
21    /**
22     * Main error code
23     */
24    ubyte mainCode() {
25       return _mainCode;
26    }
27 
28    private ubyte _subCode;
29    /**
30     * Sub error code
31     */
32    ubyte subCode() {
33       return _subCode;
34    }
35 }
36 
37 /**
38  * Memory area code. See pg.15 of $(I FINS Commands reference manual)
39  */
40 enum MemoryArea : ubyte {
41    CIO_BIT = 0x30,
42    W_BIT = 0x31,
43    H_BIT = 0x32,
44    A_BIT = 0x33,
45    D_BIT = 0x02,
46    /**
47     * CIO Channel IO area, word
48     */
49    IO = 0xB0,
50    /**
51     * WR Work area, word
52     */
53    WR = 0xB1,
54    /*
55     * HR Holding area, word
56     */
57    HR = 0xB2,
58    /**
59     * AR Auxiliary Relay area, word
60     */
61    AR = 0xB3,
62    /**
63     * DM Data Memory area, word
64     */
65    DM = 0x82,
66    /**
67     * CNT, Counter area, word
68     */
69    CT = 0x89
70 }
71 
72 ///
73 enum FINS_HEADER_LEN = 12;
74 
75 ///
76 struct Header {
77    /**
78     * Information Control Field, set to 0x80
79     */
80    ubyte icf = 0x80;
81    //ubyte rsv;//reserved, set to 0x00
82    //ubyte gct = 0x02;//gateway count, set to 0x02
83    /**
84     * Destination network address, 0x0 local , 0x01 if there are not network intermediaries
85     */
86    ubyte dna;
87    /**
88     * Destination node number, if set to default this is the subnet byte of the ip of the plc (ex. 192.168.0.1 -> 0x01)
89     */
90    ubyte da1;
91    /**
92     * Destination unit number, the unit number, see the hw config of plc, generally 0x00
93     */
94    ubyte da2;
95    /**
96     * Source network, generally 0x01
97     */
98    ubyte sna;
99    /**
100     * Source node number, like the destination node number, you could set a fixed number into plc config
101     */
102    ubyte sa1 = 0x02;
103    /**
104     * Source unit number, like the destination unit number
105     */
106    ubyte sa2;
107    /**
108     * Counter for the resend, generally 0x00
109     */
110    ubyte sid;
111    /**
112     * Main command code (high byte)
113     */
114    ubyte mainRqsCode;
115    /**
116     * Sub request code
117     */
118    ubyte subRqsCode;
119 }
120 
121 /**
122  * Convenience function for creating an `Header` with subnet data (`da1`)
123  */
124 Header header(ubyte subnet) {
125    Header h;
126    h.da1 = subnet;
127    return h;
128 }
129 
130 unittest {
131    Header hdr = header(0x11);
132    assert(hdr.icf == 0x80);
133    assert(hdr.dna == 0x0);
134    assert(hdr.da1 == 0x11);
135 }
136 
137 /**
138  * Convert an `Header` to array of bytes.
139  */
140 ubyte[] toBytes(Header data) {
141    enum RSV = 0x0;
142    enum GCT = 0x02;
143    ubyte[] b;
144    b ~= data.icf;
145    b ~= RSV;
146    b ~= GCT;
147    b ~= data.dna;
148    b ~= data.da1;
149    b ~= data.da2;
150    b ~= data.sna;
151    b ~= data.sa1;
152    b ~= data.sa2;
153    b ~= data.sid;
154    b ~= data.mainRqsCode;
155    b ~= data.subRqsCode;
156    return b;
157 }
158 
159 unittest {
160    Header data;
161    data.dna = 0;
162    data.da1 = 0x16;
163    data.da2 = 0;
164    data.sna = 0;
165    data.sa1 = 0x02;
166    data.sa2 = 0;
167    data.mainRqsCode = 0x01;
168    data.subRqsCode = 0x01;
169 
170    ubyte[] exp = [0x80, 0x00, 0x02, 0x00, 0x16, 0x0, 0x00, 0x02, 0x0, 0x0, 0x01, 0x01];
171    auto b = data.toBytes;
172    assert(b.length == FINS_HEADER_LEN);
173 
174    import std.conv;
175 
176    for (int i = 0; i < FINS_HEADER_LEN; ++i) {
177       assert(b[i] == exp[i], i.to!string());
178    }
179 }
180 
181 /**
182  * Converts an array of bytes to `Header`
183  */
184 Header toHeader(ubyte[] blob)
185 in {
186    assert(blob.length >= FINS_HEADER_LEN, "Blob too short (less than 12)");
187 }
188 do {
189    Header h;
190    h.icf = blob[0];
191    h.dna = blob[3];
192    h.da1 = blob[4];
193    h.da2 = blob[5];
194    h.sna = blob[6];
195    h.sa1 = blob[7];
196    h.sa2 = blob[8];
197    h.sid = blob[9];
198    h.mainRqsCode = blob[10];
199    h.subRqsCode = blob[11];
200    return h;
201 }
202 
203 unittest {
204    ubyte[] blob = [0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02];
205    Header h = blob.toHeader;
206    assert(h.icf == 0xC0);
207    assert(h.dna == 0x0);
208    assert(h.da1 == 0x2);
209    assert(h.da2 == 0x0);
210    assert(h.sna == 0x0);
211    assert(h.sa1 == 0x16);
212    assert(h.sa2 == 0x0);
213    assert(h.sid == 0x0);
214    assert(h.mainRqsCode == 0x01);
215    assert(h.subRqsCode == 0x02);
216 }
217 
218 ///
219 struct ResponseData {
220    Header header;
221    ubyte mainRspCode;
222    ubyte subRspCode;
223    ubyte[] text;
224 }
225 
226 /**
227  * Converts an array of bytes to `ResponseData` structure
228  */
229 ResponseData toResponse(ubyte[] data) {
230    ResponseData resp;
231    resp.header = data.toHeader;
232    resp.mainRspCode = data[12];
233    resp.subRspCode = data[13];
234    for (int i = 0; i < data.length - 14; i++) {
235       resp.text ~= data[14 + i];
236    }
237    return resp;
238 }
239 
240 /**
241  * Returs an array with start address and size.
242  */
243 private ubyte[] getAddrBlock(ushort start, ushort size) {
244    ubyte[] cmdBlock;
245 
246    //beginning address
247    cmdBlock ~= cast(ubyte)(start >> 8);
248    cmdBlock ~= cast(ubyte)start;
249    cmdBlock ~= 0x00;
250    cmdBlock ~= cast(ubyte)(size >> 8);
251    cmdBlock ~= cast(ubyte)size;
252    return cmdBlock;
253 }
254 
255 /**
256  * Client for Fins protocol
257  */
258 class FinsClient {
259    private IChannel channel;
260    private Header header;
261    this(IChannel channel, Header header) {
262       assert(channel !is null);
263       this.channel = channel;
264       this.header = header;
265    }
266 
267    /**
268     * Write an Omron PLC area: the area must be defined as CJ like area
269     *
270     * Params:
271     *  area = The area type
272     *  start = The start offset for the write process.
273     *  buffer = The byte array buffer which will be write in the PLC.
274     */
275    void writeArea(MemoryArea area, ushort start, ubyte[] buffer)
276    in {
277       assert((buffer.length & 1) == 0, "Odd buffer length");
278    }
279    do {
280       import std.conv : to;
281       import dfins.util : BYTES_PER_WORD;
282 
283       ubyte[] text;
284       //memory area code
285       text ~= cast(ubyte)area;
286       //IMPORTANT: The size is expressed in WORD (2 byte)
287       ushort size = (buffer.length / BYTES_PER_WORD).to!ushort;
288       text ~= getAddrBlock(start, size);
289       text ~= buffer;
290       sendFinsCommand(0x01, 0x02, text);
291    }
292 
293    /**
294     * Read an Omron PLC area: the area must be defined as CJ like area
295     *
296     * Params:
297     *  area = The area type
298     *  start = The start offset for the read process.
299     *  size = The size of the area to read. IMPORTANT: The size is expressed in WORD (2 byte)
300     *
301     *
302     * Returns:
303     * The byte array buffer in which will be store the PLC readed area.
304     *
305     */
306    ubyte[] readArea(MemoryArea area, ushort start, ushort size) {
307       import std.stdio;
308 
309       ubyte[] cmdBlock;
310 
311       //memory area code
312       cmdBlock ~= cast(ubyte)area;
313       cmdBlock ~= getAddrBlock(start, size);
314 
315       return sendFinsCommand(0x01, 0x01, cmdBlock);
316    }
317 
318    private ubyte[] sendFinsCommand(ubyte mainCode, ubyte subCode, ubyte[] comText) {
319       header.mainRqsCode = mainCode;
320       header.subRqsCode = subCode;
321 
322       ubyte[] sendFrame = header.toBytes() ~ comText;
323       ubyte[] receiveFrame = channel.send(sendFrame);
324 
325       ResponseData response = receiveFrame.toResponse();
326       if (response.mainRspCode != 0) {
327          throw new FinsException(response.mainRspCode, response.subRspCode);
328       }
329       return response.text;
330    }
331 }
332 
333 /**
334  * Converts main error code into string
335  */
336 string mainErrToString(ubyte mainErr) {
337    switch (mainErr) {
338       case 0x01:
339          return "Local node error";
340       case 0x02:
341          return "Destination node error";
342       case 0x03:
343          return "Communications controller error";
344       case 0x04:
345          return "Not executable";
346       case 0x05:
347          return "Routing error";
348       case 0x10:
349          return "Command format error";
350       case 0x11:
351          return "Parameter error";
352       case 0x20:
353          return "Read not possible";
354       case 0x21:
355          return "Write not possible";
356       case 0x22:
357          return "Not executable in current mode";
358       case 0x23:
359          return "No Unit";
360       case 0x24:
361          return "Start/stop not possible";
362       case 0x25:
363          return "Unit error";
364       case 0x26:
365          return "Command error";
366       case 0x30:
367          return "Access right error";
368       case 0x40:
369          return "Abort";
370       default:
371          return "Unknown error";
372    }
373 }