// Capture client for scripts/ab-capture/capture.sh. // // One libplctag tag, one operation, then Dispose + plc_tag_shutdown so the Forward Close // reaches the wire. The tag is built with exactly the properties the drivers set // (LibplctagTagRuntime / LibplctagLegacyTagRuntime / LibplctagTagEnumerator): Gateway, Path, // PlcType, Protocol=ab_eip, Name, Timeout, and ElementCount when > 1. Nothing else. // // usage: // ab-capture-client version // ab-capture-client read [count] // ab-capture-client write // // a libplctag.PlcType name: ControlLogix, Micro800, Slc500, MicroLogix, Plc5, LogixPccc // dint | int | real | string | bool | raw (read only: hex dump of the buffer) // dintseq (write only): element i (0-based) = ((i+1) << 24) | (i+1), for elements; // is ignored but required. // env UNCONNECTED=1 sets UseConnectedMessaging=false (ignored by libplctag 2.6.0 for Micro800). using libplctag; using libplctag.NativeImport; if (args.Length == 0) { Console.Error.WriteLine("usage: see the header of Program.cs"); return 2; } if (args[0] == "seed-bool") { // ab_server rejects libplctag's BOOL write with CIP 0x03: libplctag sends two data bytes for a // one-byte BOOL and ab_server's "too much data" check refuses it. This seeds a BOOL for a // later libplctag READ capture with one raw UCMM Write Tag carrying exactly one data byte. // usage: seed-bool <0|1> (route is port 1 / slot 0) return await SeedBoolAsync(args[1], int.Parse(args[2]), args[3], args[4] == "1" ? (byte)1 : (byte)0); } if (args[0] == "version") { Console.WriteLine($"{plctag.plc_tag_get_int_attribute(0, "version_major", -1)}." + $"{plctag.plc_tag_get_int_attribute(0, "version_minor", -1)}." + $"{plctag.plc_tag_get_int_attribute(0, "version_patch", -1)}"); return 0; } var op = args[0]; var gateway = args[1]; var path = args[2] == "-" ? null : args[2]; var plc = Enum.Parse(args[3], ignoreCase: true); var name = args[4]; var type = args[5]; var count = args.Length > 6 ? int.Parse(args[6]) : 1; var tag = new Tag { Gateway = gateway, Path = path, PlcType = plc, Protocol = Protocol.ab_eip, Name = name, Timeout = TimeSpan.FromSeconds(5), }; if (count > 1) tag.ElementCount = count; if (Environment.GetEnvironmentVariable("UNCONNECTED") == "1") tag.UseConnectedMessaging = false; var exit = 0; try { await tag.InitializeAsync(); if (op == "write") { var value = args[7]; switch (type) { case "dint": tag.SetInt32(0, int.Parse(value)); break; case "int": tag.SetInt16(0, short.Parse(value)); break; case "real": tag.SetFloat32(0, float.Parse(value)); break; case "string": tag.SetString(0, value); break; // As LibplctagTagRuntime does: a whole-byte write (SetBit on a Logix BOOL is ErrorBadParam). case "bool": tag.SetInt8(0, value is "1" or "true" ? (sbyte)1 : (sbyte)0); break; case "dintseq": for (var i = 0; i < count; i++) tag.SetInt32(i * 4, ((i + 1) << 24) | (i + 1)); break; default: throw new ArgumentException($"unknown write type {type}"); } await tag.WriteAsync(); Console.WriteLine("write ok"); } else { await tag.ReadAsync(); Console.WriteLine($"read ok size={tag.GetSize()}"); Console.WriteLine(type switch { "dint" => tag.GetInt32(0).ToString(), "int" => tag.GetInt16(0).ToString(), "real" => tag.GetFloat32(0).ToString(), "string" => tag.GetString(0), "bool" => tag.GetBit(0).ToString(), _ => Convert.ToHexString(tag.GetBuffer()), }); } } catch (Exception ex) { Console.WriteLine($"EXC {ex.GetType().Name}: {ex.Message}"); exit = 1; } finally { tag.Dispose(); } plctag.plc_tag_shutdown(); await Task.Delay(500); // let the Forward Close flush before the process exits return exit; static async Task SeedBoolAsync(string host, int port, string tagName, byte value) { using var tcp = new System.Net.Sockets.TcpClient(); await tcp.ConnectAsync(host, port); var s = tcp.GetStream(); // RegisterSession var reg = new byte[28]; reg[0] = 0x65; reg[2] = 4; reg[24] = 1; await s.WriteAsync(reg); var regReply = await ReadFrameAsync(s); var session = BitConverter.ToUInt32(regReply, 4); // Write Tag (0x4D): ANSI symbol segment, type BOOL 0xC1, count 1, ONE data byte. var name = System.Text.Encoding.ASCII.GetBytes(tagName); var sym = new List { 0x91, (byte)name.Length }; sym.AddRange(name); if (sym.Count % 2 == 1) sym.Add(0); var write = new List { 0x4D, (byte)(sym.Count / 2) }; write.AddRange(sym); write.AddRange([0xC1, 0x00, 0x01, 0x00, value]); // Unconnected Send (0x52) to the Connection Manager, routed to port 1 / slot 0. var ucmm = new List { 0x52, 0x02, 0x20, 0x06, 0x24, 0x01, 0x0A, 0x05 }; ucmm.AddRange(BitConverter.GetBytes((ushort)write.Count)); ucmm.AddRange(write); if (write.Count % 2 == 1) ucmm.Add(0); ucmm.AddRange([0x01, 0x00, 0x01, 0x00]); // SendRRData: interface handle, timeout, 2 items (null address, unconnected data). var data = new List { 0, 0, 0, 0, 0x0A, 0, 2, 0, 0, 0, 0, 0, 0xB2, 0 }; data.AddRange(BitConverter.GetBytes((ushort)ucmm.Count)); data.AddRange(ucmm); var frame = new byte[24 + data.Count]; frame[0] = 0x6F; BitConverter.GetBytes((ushort)data.Count).CopyTo(frame, 2); BitConverter.GetBytes(session).CopyTo(frame, 4); data.CopyTo(frame, 24); await s.WriteAsync(frame); var reply = await ReadFrameAsync(s); var cip = reply.AsSpan(24 + 16).ToArray(); // after handle, timeout, count, null item, data item header Console.WriteLine($"seed-bool reply CIP: {Convert.ToHexString(cip)}"); return cip.Length >= 3 && cip[0] == 0xCD && cip[2] == 0 ? 0 : 1; } static async Task ReadFrameAsync(System.Net.Sockets.NetworkStream s) { var header = new byte[24]; await s.ReadExactlyAsync(header); var body = new byte[BitConverter.ToUInt16(header, 2)]; await s.ReadExactlyAsync(body); return [.. header, .. body]; }