1 /** 2 * Fins client 3 * 4 * Copyright: © 2016-2026 Orfeo Da Vià. 5 * License: Boost Software License - Version 1.0 - August 17th, 2003 6 * Authors: Orfeo Da Vià 7 */ 8 module dfins.fins; 9 10 import dfins.channel; 11 12 /** 13 * Fins protocol exception 14 */ 15 class FinsException : Exception { 16 /** 17 * Constructor which takes two error codes. 18 * 19 * Params: 20 * mainCode = Main error code 21 * subCode = Sub error code 22 * file = Fine name 23 * line = Line number 24 */ 25 this(ubyte mainCode, ubyte subCode, string file = null, size_t line = 0) @trusted { 26 _mainCode = mainCode; 27 _subCode = subCode; 28 import std.string : format; 29 30 super("Fins error %s".format(mainErrToString(_mainCode))); 31 } 32 33 private ubyte _mainCode; 34 /** 35 * Main error code 36 */ 37 ubyte mainCode() { 38 return _mainCode; 39 } 40 41 private ubyte _subCode; 42 /** 43 * Sub error code 44 */ 45 ubyte subCode() { 46 return _subCode; 47 } 48 } 49 50 /** 51 * Memory area code. See page 15 of $(I FINS Commands reference manual) 52 */ 53 enum MemoryArea : ubyte { 54 CIO_BIT = 0x30, 55 W_BIT = 0x31, 56 H_BIT = 0x32, 57 A_BIT = 0x33, 58 D_BIT = 0x02, 59 /** 60 * CIO Channel IO area, word 61 */ 62 IO = 0xB0, 63 /** 64 * WR Work area, word 65 */ 66 WR = 0xB1, 67 /* 68 * HR Holding area, word 69 */ 70 HR = 0xB2, 71 /** 72 * AR Auxiliary Relay area, word 73 */ 74 AR = 0xB3, 75 /** 76 * DM Data Memory area, word 77 */ 78 DM = 0x82, 79 /** 80 * CNT, Counter area, word 81 */ 82 CT = 0x89 83 } 84 85 /// 86 enum FINS_HEADER_LEN = 12; 87 88 /// 89 struct Header { 90 /** 91 * Information Control Field, set to 0x80 92 */ 93 ubyte icf = 0x80; 94 //ubyte rsv;//reserved, set to 0x00 95 //ubyte gct = 0x02;//gateway count, set to 0x02 96 /** 97 * Destination network address, 0x0 local , 0x01 if there are not network intermediaries 98 */ 99 ubyte dna; 100 /** 101 * Destination node number. 102 * 103 * If set to default this is the subnet byte of the ip of the plc 104 * Examples: 105 * -------------------- 106 * ex. 192.168.0.1 -> 0x01 107 * -------------------- 108 */ 109 ubyte da1; 110 /** 111 * Destination unit number 112 * 113 * The unit number, see the hardware config of plc, generally 0x00 114 */ 115 ubyte da2; 116 /** 117 * Source network 118 * 119 * generally 0x01 120 */ 121 ubyte sna; 122 /** 123 * Source node number. 124 * 125 * Like the destination node number, you could set a fixed number into plc config 126 */ 127 ubyte sa1 = 0x02; 128 /** 129 * Source unit number. 130 * 131 * Like the destination unit number. 132 */ 133 ubyte sa2; 134 /** 135 * Counter for the resend. 136 * 137 * Generally 0x00 138 */ 139 ubyte sid; 140 /** 141 * Main request code (high byte). 142 */ 143 ubyte mainRqsCode; 144 /** 145 * Sub request code. 146 */ 147 ubyte subRqsCode; 148 } 149 150 /** 151 * Convenience function for creating an `Header` with destination node number (`da1`) and source node number (`sa1`). 152 * 153 * `da1` is the subnet byte of the ip of the plc 154 * 155 * Params: 156 * dstNodeNumber = Destination node number 157 * srcNodeNumber = Source node number 158 */ 159 Header header(ubyte dstNodeNumber, ubyte srcNodeNumber = 0x02) { 160 Header h; 161 h.da1 = dstNodeNumber; 162 h.sa1 = srcNodeNumber; 163 return h; 164 } 165 166 unittest { 167 immutable(Header) hdr = header(0x11); 168 assert(hdr.icf == 0x80); 169 assert(hdr.dna == 0x0); 170 assert(hdr.da1 == 0x11); 171 assert(hdr.sa1 == 0x02); 172 immutable(Header) hdr2 = header(0x10, 0x42); 173 assert(hdr2.da1 == 0x10); 174 assert(hdr2.sa1 == 0x42); 175 } 176 177 /** 178 * Get subnet (`da1`) from ip address. 179 * 180 * Examples: 181 * -------------------- 182 * enum IP = "192.168.1.42"; 183 * IChannel chan = createUdpChannel(IP, 2000); 184 * Header h = header(IP.getSubnet); // subnet == da1 == 42 185 * FinsClient f = new FinsClient(chan, h); 186 * -------------------- 187 */ 188 ubyte getSubnet(string ip) @safe { 189 import std.regex : regex, matchFirst; 190 import std.conv : to; 191 import std.exception : enforce; 192 193 auto ipReg = regex( 194 r"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"); 195 auto m = matchFirst(ip, ipReg); 196 enforce(!m.empty && m.length > 4, "Invalid ip"); 197 return m[4].to!ubyte; 198 } 199 200 unittest { 201 import std.exception : assertThrown; 202 203 assert("192.168.221.64".getSubnet == 64); 204 assert("192.168.221.1".getSubnet == 1); 205 assert("192.168.22.2".getSubnet == 2); 206 207 assertThrown("192.168.221".getSubnet); 208 assertThrown("400.168.221.1".getSubnet); 209 assertThrown("".getSubnet); 210 assertThrown("asas".getSubnet); 211 } 212 213 /** 214 * Convert an `Header` to array of bytes. 215 */ 216 ubyte[] toBytes(Header data) { 217 enum RSV = 0x0; 218 enum GCT = 0x02; 219 ubyte[] b; 220 b ~= data.icf; 221 b ~= RSV; 222 b ~= GCT; 223 b ~= data.dna; 224 b ~= data.da1; 225 b ~= data.da2; 226 b ~= data.sna; 227 b ~= data.sa1; 228 b ~= data.sa2; 229 b ~= data.sid; 230 b ~= data.mainRqsCode; 231 b ~= data.subRqsCode; 232 return b; 233 } 234 235 unittest { 236 Header data; 237 data.dna = 0; 238 data.da1 = 0x16; 239 data.da2 = 0; 240 data.sna = 0; 241 data.sa1 = 0x02; 242 data.sa2 = 0; 243 data.mainRqsCode = 0x01; 244 data.subRqsCode = 0x01; 245 246 ubyte[] exp = [0x80, 0x00, 0x02, 0x00, 0x16, 0x0, 0x00, 0x02, 0x0, 0x0, 0x01, 0x01]; 247 auto b = data.toBytes; 248 assert(b.length == FINS_HEADER_LEN); 249 250 import std.conv : to; 251 252 for (int i = 0; i < FINS_HEADER_LEN; ++i) { 253 assert(b[i] == exp[i], i.to!string()); 254 } 255 } 256 257 /** 258 * Converts an array of bytes to `Header` 259 */ 260 Header toHeader(ubyte[] blob) 261 in { 262 assert(blob.length >= FINS_HEADER_LEN, "Blob too short (less than 12)"); 263 } 264 do { 265 Header h; 266 h.icf = blob[0]; 267 h.dna = blob[3]; 268 h.da1 = blob[4]; 269 h.da2 = blob[5]; 270 h.sna = blob[6]; 271 h.sa1 = blob[7]; 272 h.sa2 = blob[8]; 273 h.sid = blob[9]; 274 h.mainRqsCode = blob[10]; 275 h.subRqsCode = blob[11]; 276 return h; 277 } 278 279 unittest { 280 ubyte[] blob = [0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02]; 281 immutable(Header) h = blob.toHeader; 282 assert(h.icf == 0xC0); 283 assert(h.dna == 0x0); 284 assert(h.da1 == 0x2); 285 assert(h.da2 == 0x0); 286 assert(h.sna == 0x0); 287 assert(h.sa1 == 0x16); 288 assert(h.sa2 == 0x0); 289 assert(h.sid == 0x0); 290 assert(h.mainRqsCode == 0x01); 291 assert(h.subRqsCode == 0x02); 292 } 293 294 /// 295 struct ResponseData { 296 /** 297 * Response header 298 */ 299 Header header; 300 /** 301 * Main response code 302 */ 303 ubyte mainRspCode; 304 /** 305 * Sub response code 306 */ 307 ubyte subRspCode; 308 /** 309 * Payload 310 */ 311 ubyte[] text; 312 } 313 314 /** 315 * Converts an array of bytes to `ResponseData` structure 316 */ 317 ResponseData toResponse(ubyte[] data) 318 in { 319 assert(data.length > FINS_HEADER_LEN + 1, "Invalid data length"); 320 } 321 do { 322 ResponseData resp; 323 resp.header = data.toHeader; 324 resp.mainRspCode = data[12]; 325 resp.subRspCode = data[13]; 326 resp.text = data[14 .. $]; 327 return resp; 328 } 329 330 unittest { 331 // dfmt off 332 ubyte[] blob = [ 333 0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02, 334 0x42, 0x43, // rsp code 335 0x64, 0x65, 0x66, 0x67, 0x68, 0x69 // data 336 ]; 337 // dfmt on 338 ResponseData r = blob.toResponse; 339 assert(r.header.icf == 0xC0); 340 assert(r.header.dna == 0x0); 341 assert(r.header.da1 == 0x2); 342 assert(r.header.da2 == 0x0); 343 assert(r.header.sna == 0x0); 344 assert(r.header.sa1 == 0x16); 345 assert(r.header.sa2 == 0x0); 346 assert(r.header.sid == 0x0); 347 assert(r.header.mainRqsCode == 0x01); 348 assert(r.header.subRqsCode == 0x02); 349 350 assert(r.mainRspCode == 0x42); 351 assert(r.subRspCode == 0x43); 352 assert(r.text == [0x64, 0x65, 0x66, 0x67, 0x68, 0x69]); 353 354 //import std.exception : assertThrown; 355 //ubyte[] invalid = [0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02]; 356 //assertThrown( invalid.toResponse()); 357 ubyte[] nodata = [0xc0, 0x0, 0x02, 0x0, 0x02, 0x0, 0x0, 0x16, 0x0, 0x0, 0x01, 0x02, 0x42, 0x43]; 358 assert(nodata.toResponse.text == []); 359 } 360 361 /** 362 * Returs an array with start address and size. 363 */ 364 private ubyte[] getAddrBlock(ushort start, ushort size) { 365 ubyte[] cmdBlock; 366 367 //beginning address 368 cmdBlock ~= cast(ubyte)(start >> 8); 369 cmdBlock ~= cast(ubyte)start; 370 cmdBlock ~= 0x00; 371 cmdBlock ~= cast(ubyte)(size >> 8); 372 cmdBlock ~= cast(ubyte)size; 373 return cmdBlock; 374 } 375 376 /** 377 * Client for Fins protocol 378 */ 379 class FinsClient { 380 private IChannel channel; 381 private Header header; 382 /** 383 * Constructor which takes a channel and header. 384 */ 385 this(IChannel channel, Header header) { 386 assert(channel !is null); 387 this.channel = channel; 388 this.header = header; 389 } 390 391 /** 392 * Write an Omron PLC area: the area must be defined as CJ like area 393 * 394 * Params: 395 * area = The area type 396 * start = The start offset for the write process. 397 * buffer = The byte array buffer which will be write in the PLC. 398 */ 399 void writeArea(MemoryArea area, ushort start, ubyte[] buffer) 400 in { 401 assert((buffer.length & 1) == 0, "Odd buffer length"); 402 } 403 do { 404 import std.conv : to; 405 enum BYTES_PER_WORD = 2; 406 407 ubyte[] text; 408 //memory area code 409 text ~= cast(ubyte)area; 410 //IMPORTANT: The size is expressed in WORD (2 byte) 411 immutable(ushort) size = (buffer.length / BYTES_PER_WORD).to!ushort; 412 text ~= getAddrBlock(start, size); 413 text ~= buffer; 414 sendFinsCommand(0x01, 0x02, text); 415 } 416 417 /** 418 * Read an Omron PLC area 419 * 420 * Params: 421 * area = The area type 422 * start = The start offset for the read process. 423 * size = The size of the area to read. IMPORTANT: The size is expressed in WORD (2 byte) 424 * 425 * Returns: 426 * The byte array buffer in which will be store the PLC readed area. 427 */ 428 ubyte[] readArea(MemoryArea area, ushort start, ushort size) { 429 ubyte[] cmdBlock; 430 431 //memory area code 432 cmdBlock ~= cast(ubyte)area; 433 cmdBlock ~= getAddrBlock(start, size); 434 return sendFinsCommand(0x01, 0x01, cmdBlock); 435 } 436 437 private ubyte[] sendFinsCommand(ubyte mainCode, ubyte subCode, ubyte[] comText) { 438 header.mainRqsCode = mainCode; 439 header.subRqsCode = subCode; 440 441 ubyte[] sendFrame = header.toBytes() ~ comText; 442 ubyte[] receiveFrame = channel.send(sendFrame); 443 444 ResponseData response = receiveFrame.toResponse(); 445 if (response.mainRspCode != 0) { 446 throw new FinsException(response.mainRspCode, response.subRspCode); 447 } 448 return response.text; 449 } 450 } 451 452 /** 453 * Converts main error code into string 454 */ 455 string mainErrToString(ubyte mainErr) { 456 switch (mainErr) { 457 case 0x01: 458 return "Local node error"; 459 case 0x02: 460 return "Destination node error"; 461 case 0x03: 462 return "Communications controller error"; 463 case 0x04: 464 return "Not executable"; 465 case 0x05: 466 return "Routing error"; 467 case 0x10: 468 return "Command format error"; 469 case 0x11: 470 return "Parameter error"; 471 case 0x20: 472 return "Read not possible"; 473 case 0x21: 474 return "Write not possible"; 475 case 0x22: 476 return "Not executable in current mode"; 477 case 0x23: 478 return "No Unit"; 479 case 0x24: 480 return "Start/stop not possible"; 481 case 0x25: 482 return "Unit error"; 483 case 0x26: 484 return "Command error"; 485 case 0x30: 486 return "Access right error"; 487 case 0x40: 488 return "Abort"; 489 default: 490 return "Unknown error"; 491 } 492 } 493 494 /** 495 * Convenience functions that create an `FinsClient` object. 496 * 497 * Examples: 498 * -------------------- 499 * FinsClient f = createFinsClient("192.168.1.1", 2000, 9600); 500 * -------------------- 501 * 502 * Params: 503 * ip = IP address 504 * timeout = Send and receive timeout in ms 505 * port = Port number (default 9600) 506 * srcNodeNumber = Source node number 507 */ 508 FinsClient createFinsClient(string ip, long timeout, ushort port = 9600, ubyte srcNodeNumber = 0x02) 509 in { 510 assert(ip.length); 511 assert(timeout >= 0); 512 } 513 do { 514 import dfins.channel : IChannel, createUdpChannel; 515 516 IChannel chan = createUdpChannel(ip, timeout, port); 517 Header h = header(ip.getSubnet, srcNodeNumber); 518 return new FinsClient(chan, h); 519 }