1 /**
2  * Omron HostLink protocols
3  */
4 module dolina.hostlink;
5 
6 version (unittest) {
7    import unit_threaded;
8 }
9 
10 import std.array;
11 import std.conv;
12 import std.exception;
13 import std.stdio;
14 import std.string;
15 
16 import dolina.channel;
17 
18 /**
19  * Defines the basic HostLink protocol interface.
20  */
21 interface IHostLink {
22    /**
23     * Reads the contents of specified DM $(D ushort), starting from the specified
24     * address.
25     *
26     * Params:
27     * address = Beginning DM address
28     * length = Number of DM to read
29     */
30    ushort[] readDM(const(int) address, const(int) length);
31 
32    /**
33     * Writes data to the DM area, starting from the specified address.
34     *
35     * Params:
36     * address = Beginning DM address
37     * data = DM ($(D ushort)) to write
38     *
39     * Examples:
40     * --------------------
41     * auto buffer = appender!(const(ushort)[]);
42     * buffer.write!float(12.68);
43     * buffer.write!ushort(5);
44     * plc.writeDM(address, buffer.data);
45     * --------------------
46     */
47    void writeDM(const(int) address, const(ushort)[] data);
48 
49    /**
50     * PLC Unit number.
51     */
52    @property int unit();
53    @property void unit(int u);
54 
55    /**
56     * Data memory area size (DM)
57     */
58    @property int dataMemorySize();
59 }
60 
61 /**
62  * Provides the implementation for HostLink protocol
63  */
64 class HostLink : IHostLink {
65    private IHostLinkChannel channel;
66 
67    this(IHostLinkChannel channel) {
68       assert(channel !is null);
69       this.channel = channel;
70    }
71 
72    @property int dataMemorySize() {
73       enum DM_SIZE = 6656;
74       return DM_SIZE;
75    }
76 
77    private int _unit;
78    @property int unit() {
79       return _unit;
80    }
81 
82    @property void unit(int u) {
83       assert(u >= 0 && u < 32);
84       this._unit = u;
85    }
86 
87    private enum MAX_FRAME_SIZE = 29;
88    ushort[] readDM(const(int) address, const(int) length)
89    in {
90       assert(address >= 0, "negative address");
91       assert(length >= 0, "negative lenght");
92    }
93    do {
94       if (length > MAX_FRAME_SIZE) {
95          return readDM(address, MAX_FRAME_SIZE) ~ readDM(address + MAX_FRAME_SIZE, length - MAX_FRAME_SIZE);
96       } else if (length <= 0) {
97          return null;
98       } else {
99          string w = getRDString(_unit, address, length);
100          channel.write(w);
101 
102          string r = channel.read();
103          string reply = crop(r);
104 
105          immutable(int) err = getErrorCodeFromReply(reply);
106          if (err > 0) {
107             throw new OmronException(err);
108          } else {
109             return toDM(reply[6 .. $ - 2]);
110          }
111       }
112    }
113 
114    void writeDM(const(int) address, const(ushort)[] data)
115    in {
116       assert(address >= 0, "negative address");
117    }
118    do {
119       if (data.length > MAX_FRAME_SIZE) {
120          writeDM(address, data[0 .. MAX_FRAME_SIZE]);
121          writeDM(address + MAX_FRAME_SIZE, data[MAX_FRAME_SIZE .. $]);
122       } else {
123          channel.write(getWDString(_unit, address, data));
124          string reply = crop(channel.read());
125 
126          immutable(int) err = getErrorCodeFromReply(reply);
127          if (err > 0) {
128             throw new OmronException(err);
129          }
130       }
131    }
132 }
133 
134 /**
135  * The NullHostLing will not read or write.
136  */
137 class NullHostLink : IHostLink {
138    ushort[] readDM(const(int) address, const(int) length) {
139       return new ushort[length];
140    }
141 
142    void writeDM(const(int) address, const(ushort)[] data) {
143    }
144 
145    private int _unit;
146    @property int unit() {
147       return _unit;
148    }
149 
150    @property void unit(int u) {
151       _unit = u;
152    }
153 
154    @property int dataMemorySize() {
155       return 100;
156    }
157 }
158 
159 /*
160  * Get a string command to read data memory.
161  *
162  *  String shold be (see chap. 4.9 Omron manual 1E66)
163  * ---
164  * @|unit|RD|start|length|fcs|*CR
165  * ---
166  *
167  * Params:
168  * unit = Node number
169  *
170  * address = Beginning word address
171  * length = Number of words
172  *
173  * Returns: RD command string
174  */
175 private pure string getRDString(const(int) unit, const(int) address, const(int) length) {
176    string send = "@" ~ format("%.2d", unit) ~ "RD" ~ format("%.4d", address) ~ format("%.4d", length);
177 
178    send ~= fcs(send) ~ "*\r";
179    return send;
180 }
181 
182 unittest {
183    getRDString(0, 23, 5).shouldEqual("@00RD0023000552*\r");
184    getRDString(0, 100, 2).shouldEqual("@00RD0100000255*\r");
185    getRDString(0, 258, 2).shouldEqual("@00RD025800025B*\r");
186 }
187 
188 /*
189  * Get a string command to write data memory.
190  *
191  * String shold be (see chap. 4.22 Omron manual 1E66)
192  * ---
193  * @|unit|WD|address|d0|d1...|fcs|*CR|
194  * ---
195  *
196  * Params:
197  * unit = Node number
198  * address = Beginning word address
199  * length = Number of words
200  * data = Words to write
201  *
202  * Returns: WD command string
203  */
204 private string getWDString(const(int) unit, const(int) address, const(ushort)[] data) {
205    string send = "@" ~ format("%.2d", unit) ~ "WD" ~ format("%.4d", address);
206    foreach (i; 0 .. data.length) {
207       send ~= format("%.4x", data[i]);
208    }
209    send ~= fcs(send) ~ "*\r";
210    return send;
211 }
212 
213 unittest {
214    getWDString(2, 302, [100, 6500]).shouldEqual("@02WD03020064196458*\r");
215 }
216 
217 /*
218  * Converts a string returned by DM read into an array of `ushort`.
219  *
220  * The string consists of groups of four characters, each representing a digit in hex.
221  * Each group of four characters is the value of a DM
222  *
223  * Eg. the string representation of `[15, 16, 17]` is  `000F 0010 0011` (spaces are added for readability)
224  *
225  * Params:  input = string to convert
226  * Returns:  Array of converted values
227  */
228 private ushort[] toDM(string input) {
229    // std.array.appender:
230    // Convenience function that returns an Appender!A object initialized with
231    // array.
232 
233    auto data = appender!(ushort[])();
234    while (input.length > 0) {
235       immutable(ushort) x = parseFront(input);
236       data.put(x);
237       input = input.length >= 4 ? input[4 .. $] : [];
238    }
239    return data.data;
240 }
241 
242 unittest {
243    ushort[][string] testCase = [
244       "000100020003" : [0x1, 0x2, 0x3], "00FF8000FFFF" : [0xFF, 0x8000, 0xFFFF], "5" : [0x5], "12345" : [0x1234, 0x5],
245    ];
246 
247    foreach (key, value; testCase) {
248       toDM(key).shouldEqual(value);
249    }
250    toDM("").length.shouldEqual(0);
251 }
252 
253 /*
254  * Converts a DM string representation into a numeric value $(D_CODE ushort).
255  *
256  * A DM is represented by four characters: if the input string contains more, then
257  * `parseFront` converts the first four, if it contains less then all available characters are converted
258  *
259  * Params:  input = string to convert
260  * Returns:  Converted value
261  */
262 private pure ushort parseFront(string input) {
263    assert(input.length > 0);
264 
265    string front;
266    if (input.length >= 4) {
267       front = input[0 .. 4];
268    } else {
269       front = input[0 .. $];
270    }
271    // std.conv
272    // 16 indica che la stringa e' hex
273    return parse!ushort(front, 16);
274 }
275 
276 unittest {
277    parseFront("000F").shouldEqual(15);
278    parseFront("000F00FF").shouldEqual(15);
279    parseFront("000Fasd").shouldEqual(15);
280    parseFront("F").shouldEqual(15);
281    parseFront("0F").shouldEqual(15);
282    parseFront("0010").shouldEqual(16);
283    parseFront("1").shouldEqual(1);
284 }
285 
286 /*
287  * Computes FCS of message.
288  *
289  * The FCS is an 8-bit data represented by two ASCII characters (00 to FF).
290  *
291  * It is a result of Exclusive OR sequentially performed on each character in
292  * the message.
293  *
294  * Params:  msg = Message
295  *
296  * Returns: Mesage FCS
297  */
298 private pure string fcs(string msg) {
299    if (msg.length == 0) {
300       return "00";
301    } else {
302       /*
303          std.string.representation:
304          Returns the representation of a string, which has the same
305          type as the string except the character type is replaced by ubyte,
306          ushort, or uint depending on the character width
307        */
308       immutable(ubyte[]) b = representation(msg);
309       int fcs;
310       foreach (i; 0 .. b.length) {
311          fcs ^= b[i];
312       }
313       return std..string.format("%.2X", fcs);
314    }
315 }
316 
317 unittest {
318    fcs("@02WD00").shouldEqual("51");
319    fcs("12").shouldEqual("03");
320    fcs("123").shouldEqual("30");
321    fcs("0").shouldEqual("30");
322    fcs("abc").shouldEqual("60");
323    fcs("@00RD00300001").shouldEqual("54");
324    fcs("@00RD00300001").shouldEqual("54");
325    fcs("@00RD02580002").shouldEqual("5B");
326    fcs("").shouldEqual("00");
327    fcs(null).shouldEqual("00");
328 }
329 
330 /*
331  * Get message data.
332  *
333  * The message data are between the character '@' at the start and '*' at the end.
334  *  ---
335  *  @|uu|RD|ee|--data --|fc|*|CR
336  *  ---
337  *  Params:  message = Input message
338  *
339  *  Returns:
340  * The message data if the message is valid, otherwise an empty string
341  */
342 private string crop(string message) {
343    import std.regex : match, regex;
344 
345    string data;
346    if (message.length > 0) {
347       string pattern = r"@(?P<core>[^\*]+)\*.*";
348       auto m = match(message, regex(pattern));
349       if (m) {
350          data = m.captures["core"];
351       }
352    }
353    return data;
354 }
355 
356 unittest {
357    crop("@abc*").shouldEqual("abc");
358    crop("@abc").shouldEqual("");
359    crop("").shouldEqual("");
360    crop("xy").shouldEqual("");
361    crop("@01RD00").shouldEqual("");
362    crop("@01RD00*").shouldEqual("01RD00");
363    crop("@01RD00*\n").shouldEqual("01RD00");
364    crop("@01RD00*\n@03").shouldEqual("01RD00");
365    crop("@00RD000000000056*\r").shouldEqual("00RD000000000056");
366 }
367 
368 /*
369  * Returns the error code inside the response.
370  *
371  * In response, there are eight frame characters around error code:
372  *
373  * ---
374  * uu|RD|ee|...dati..|FC|
375  * ---
376  * with:
377  * $(UL
378  * $(LI uu: unit no)
379  * $(LI RD: read dm)
380  * $(LI ee: error code)
381  * )
382  *
383  * Params: reply = PLC reply
384  *
385  * Returns: error code or 0 if there are no errors.
386  */
387 private int getErrorCodeFromReply(string reply) {
388    int err = 1001;
389    writeln("errcode imp: ", reply);
390 
391    if (reply.length > 5) {
392       string code = reply[4 .. 6];
393       writeln("errcode: ", code);
394       err = parse!int(code, 16);
395    }
396    return err;
397 }
398 
399 unittest {
400    getErrorCodeFromReply("02RD0015").shouldEqual(0);
401    getErrorCodeFromReply("01RD0").shouldEqual(1001);
402    getErrorCodeFromReply("01RD0715").shouldEqual(7);
403    getErrorCodeFromReply("01RD0A15").shouldEqual(10);
404    getErrorCodeFromReply("01RD0B312").shouldEqual(11);
405    getErrorCodeFromReply("").shouldEqual(1001);
406    getErrorCodeFromReply("01RD").shouldEqual(1001);
407    getErrorCodeFromReply("00RD1354").shouldEqual(0x13);
408 }
409 
410 class OmronException : Exception {
411    this(int errCode) {
412       _errCode = errCode;
413       super(getErrorMessage(errCode));
414    }
415 
416    private int _errCode;
417    @property int errCode() {
418       return _errCode;
419    }
420 }
421 
422 private pure string getErrorMessage(const(int) errorNr) {
423    switch (errorNr) {
424       case 0x00:
425          return "Success";
426       case 0x01:
427          return "Execution was not possible the PLC is in RUN mode";
428       case 0x13:
429          return "Check sum error";
430       case 0x14:
431          return "Command format error";
432       case 0x15:
433          return "An incorrect data area designation was made for READ or WRITE";
434       case 0x18:
435          return "Frame length error";
436       case 0x22:
437          return "The specified memory unit does not exists";
438       case 0x23:
439          return "The specified memory unit is write protected";
440       case 0xA3:
441          return "Aborted due to checksum error in transmit data";
442       case 0xA5:
443          return "Aborted due to entry number data error in transmit data";
444       case 0xA6:
445          return "Aborted due to frame length error in transmit data";
446       case 0x3E8:
447          return "dolina: Frame lenght error in received data"; //10000
448       case 0x3E9:
449          return "dolina: Invalid format of error code"; // 1001
450       default:
451          return "Unkown error code " ~ to!string(errorNr);
452    }
453 }
454 
455 unittest {
456    import std.string : startsWith;
457 
458    getErrorMessage(0x44).startsWith("Unkown").shouldBeTrue;
459    getErrorMessage(0x13).shouldEqual("Check sum error");
460 }