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