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 }