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 }