├── .gitignore ├── README.md ├── canned ├── fault_descriptions.go ├── faults.go └── forces.go ├── cip_definitions.go ├── client.go ├── connect.go ├── device_type.go ├── device_type_test.go ├── disconnect.go ├── docs ├── EtherNetIP Adapter Protocol API 12 EN.pdf ├── connectionTimeouts.csv ├── resources.md └── server.md ├── errors.go ├── examples ├── ArrayRead │ └── main.go ├── Canned │ └── main.go ├── ConnectionDropTest │ └── main.go ├── GenericCIPMessage │ └── main.go ├── GetAttribute │ └── main.go ├── GetControllerTime │ └── main.go ├── Heartbeat │ └── main.go ├── HttpServer │ ├── config.go │ ├── config.json │ └── main.go ├── ListIdentityAndServices │ └── main.go ├── ListPrograms │ └── main.go ├── LogixMsg_HttpServer │ └── main.go ├── MapRead │ └── main.go ├── Micro820_List │ └── main.go ├── Micro820_Read │ └── main.go ├── MultiRead │ └── main.go ├── MultiWrite │ └── main.go ├── ProductionService │ └── main.go ├── ReadAllTags │ └── main.go ├── ReadPowerflexParam │ └── main.go ├── ReadTagsFromL5X │ └── main.go ├── ReadUnknownTypes │ └── main.go ├── Server_Class1 │ ├── AddModule.png │ ├── CIPModule.png │ ├── CipModuleConfig.png │ ├── EthernetBridge.png │ ├── EthernetBridgeSetup.png │ ├── IO_Tree.png │ └── main.go ├── Server_Class1_V2 │ ├── AddModule.png │ ├── CIPModule.png │ ├── CipModuleConfig.png │ ├── EthernetBridge.png │ ├── EthernetBridgeSetup.png │ ├── IO_Tree.png │ └── main.go ├── Server_Class3 │ ├── Read1.png │ ├── Read2.png │ ├── Write1.png │ ├── Write2.png │ └── main.go ├── Server_ProducedTag │ └── main.go ├── SimpleRead │ └── main.go ├── UDTReadWrite │ └── main.go └── Write │ └── main.go ├── getattr.go ├── go.mod ├── go.sum ├── ioi.go ├── ioi_test.go ├── items.go ├── l5x ├── RSLogix5000_V35.xsd ├── load_tags.go ├── schema.go └── types.go ├── lgxtypes ├── control.go ├── counter.go ├── readme.md ├── string.go └── timer.go ├── license.MD ├── listallprograms.go ├── listalltags.go ├── listidentity.go ├── listidentity_test.go ├── listmembers.go ├── listservices.go ├── listservices_test.go ├── listsubtags.go ├── logger.go ├── messages.go ├── pack.go ├── pack_test.go ├── partialread.go ├── path.go ├── path_test.go ├── read.go ├── sendreceive.go ├── server.go ├── server_connected.go ├── server_connections.go ├── server_io.go ├── server_iochannel.go ├── server_router.go ├── server_unconnected.go ├── services.go ├── tests ├── decode_l5x_test.go ├── generic_cip_message_test.go ├── getattr_test.go ├── gologix_tests_Program.L5X ├── list_identity_test.go ├── list_services_test.go ├── listalltags_test.go ├── listmembers_test.go ├── listsubtags_test.go ├── multiple_conection_test.go ├── read_list_test.go ├── read_multi_test.go ├── read_test.go ├── readcontrol_test.go ├── readcounter_test.go ├── readtimer_test.go ├── realhardware_test.go ├── reconnection_test.go ├── test_config template.json ├── test_setup.go ├── write_multi_test.go └── write_test.go ├── types.go ├── udt_write.go ├── udt_write_test.go ├── vendors.go ├── vendors_test.go └── write.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | .vscode/* 3 | tests/test_config.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gologix 2 | 3 | gologix is a communication driver written in native Go that lets you easily read/write values from tags in Rockwell Automation ControlLogix and CompactLogix PLCs over Ethernet/IP using Go. PLCs that use CIP over Ethernet/IP are supported (ControlLogix, CompactLogix, Micro820). Models like PLC5, SLC, and MicroLogix that use PCCC instead of CIP are *not* supported. 4 | 5 | It is modeled after pylogix with changes to make it usable in Go. 6 | 7 | ### Your First Client Program: 8 | 9 | There are a few examples in the `examples` folder. Here is an abridged version of the `/examples/SimpleRead` example. See the actual example for a more thorough description of what is going on. 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "log" 17 | "github.com/danomagnum/gologix" 18 | ) 19 | 20 | func main() { 21 | client := gologix.NewClient("192.168.2.241") 22 | err := client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client: %v", err) 25 | return 26 | } 27 | defer client.Disconnect() 28 | 29 | var tag1 int16 30 | err = client.Read("testint", &tag1) 31 | if err != nil { 32 | log.Printf("Error reading testint: %v", err) 33 | return 34 | } 35 | log.Printf("tag1 has value %d", tag1) 36 | } 37 | ``` 38 | 39 | ### Your First Server Program: 40 | 41 | There are a few examples in the `examples` folder. Here is an abridged version of the `/examples/Server_Class3` example. See the actual example(s) for a more thorough description of what is going on. Basically, it listens to incoming MSG instructions doing CIP Data Table Writes and CIP Data Table Reads and maps the data to/from an internal Go map. You can then access the data through that map as long as you acquire the lock on it. 42 | 43 | ```go 44 | package main 45 | 46 | import ( 47 | "log" 48 | "os" 49 | "time" 50 | "github.com/danomagnum/gologix" 51 | ) 52 | 53 | func main() { 54 | r := gologix.PathRouter{} 55 | 56 | p1 := gologix.MapTagProvider{} 57 | path1, err := gologix.ParsePath("1,0") 58 | if err != nil { 59 | log.Printf("Problem parsing path: %v", err) 60 | os.Exit(1) 61 | } 62 | r.AddHandler(path1.Bytes(), &p1) 63 | 64 | s := gologix.NewServer(&r) 65 | go s.Serve() 66 | 67 | t := time.NewTicker(time.Second * 5) 68 | for { 69 | <-t.C 70 | p1.Mutex.Lock() 71 | log.Printf("Data 1: %v", p1.Data) 72 | p1.Mutex.Unlock() 73 | } 74 | } 75 | ``` 76 | 77 | ### Canned Functions 78 | 79 | There is a `canned` package that can be used for common features such as reading controller fault codes or the status of forces. Look at the `/examples/Canned` directory to see how to use these. You can also use the code in `/canned/` as good examples of how to do particular things. Pull requests for extensions to the `canned` package are welcome (as are all pull requests). 80 | 81 | ### Other Features 82 | 83 | - Can behave as a class 1 or class 3 server, allowing push messages from a PLC (class 3 via MSG instruction) or implicit messaging (class 1). See the *server examples*. 84 | - You can read/write multiple tags at once by defining a struct with each field tagged with `gologix:"tagname"`. See `MultiRead` in the examples directory. 85 | - To read multiple items from an array, pass a slice to the `Read` method. 86 | - To read more than one arbitrary tag at once, use the `ReadList` method. The first parameter is a slice of tag names, and the second parameter is a slice of each tag's type. 87 | - You can read UDTs if you define an equivalent struct to blit the data into. Arrays of UDTs also work (see limitation below about UDTs with packed bools). 88 | 89 | There is also a `Server` type that lets you receive MSG instructions from the controller. See "Server" in the examples folder. It currently handles reads and writes of atomic data types (SINT, INT, DINT, REAL). You could use this to create a "push" mechanism instead of having to poll the controller for data changes. 90 | 91 | ### Limitations 92 | 93 | - You cannot write multiple items from an array at once yet, but you can do them piecewise if needed. 94 | - You can write to BOOL tags but NOT to bits of integers yet (e.g., "MyBool" is OK, but "MyDint.3" is NOT). You can read from either just fine. A "write with mask" implementation may be needed for this. 95 | - No UDTs or arrays in the server yet. 96 | 97 | ## License 98 | 99 | This project is licensed under the MIT license. 100 | 101 | ## Acknowledgements 102 | 103 | - pylogix 104 | - go-ethernet-ip 105 | -------------------------------------------------------------------------------- /canned/faults.go: -------------------------------------------------------------------------------- 1 | package canned 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | type FaultType uint32 10 | 11 | type FaultEvent struct { 12 | Timestamp uint64 13 | FaultClass uint16 14 | FaultCode uint16 15 | Detail [30]byte 16 | } 17 | 18 | func (f FaultEvent) Description() FaultDescription { 19 | t, ok := FaultDescriptions[int(f.FaultClass)] 20 | if !ok { 21 | return FaultDescription{ 22 | Type: int(f.FaultClass), 23 | Code: int(f.FaultCode), 24 | Display: fmt.Sprintf("Unknown fault T%02d:C%02d", f.FaultClass, f.FaultCode), 25 | Description: string(f.Detail[:]), 26 | CorrectiveAction: "Unknown", 27 | TypeName: "Unknown", 28 | } 29 | } 30 | 31 | desc, ok := t[int(f.FaultCode)] 32 | if !ok { 33 | return FaultDescription{ 34 | Type: int(f.FaultClass), 35 | Code: int(f.FaultCode), 36 | Display: fmt.Sprintf("Unknown fault T%02d:C%02d", f.FaultClass, f.FaultCode), 37 | Description: string(f.Detail[:]), 38 | CorrectiveAction: "Unknown", 39 | TypeName: "Unknown", 40 | } 41 | } 42 | return desc 43 | 44 | } 45 | func (f FaultEvent) String() string { 46 | if f.FaultClass == 0 && f.FaultCode == 0 { 47 | return "No Fault" 48 | } 49 | return f.Description().Display 50 | } 51 | 52 | type FaultSummary struct { 53 | MajorType FaultType 54 | MinorType FaultType 55 | MajorCount uint16 56 | MinorCount uint16 57 | Events [3]FaultEvent // Last 3 major faults. 58 | } 59 | 60 | const ( 61 | FaultType_None FaultType = 1 << iota // bit 1 62 | FaultType_Powerup // bit 2 63 | FaultType_IO // bit 3 64 | FaultType_Program // bit 4 65 | _ // 5 66 | FaultType_Watchdog // bit 6 67 | FaultType_NVMemory // bit 7 68 | FaultType_ModeChange // bit 8 69 | FaultType_SerialPort // bit 9 70 | FaultType_EnergyStorage // bit 10 71 | FaultType_Motion // bit 11 72 | FaultType_Redundancy // bit 12 73 | FaultType_RTC // bit 13 74 | FaultType_NonRecoverable // bit 14 75 | _ // 15 76 | FaultType_Communication // bit 16 77 | FaultType_Diagnostics // bit 17 78 | FaultType_CIPMotion // bit 18 79 | FaultType_Ethernet // bit 19 80 | FaultType_License // bit 20 81 | FaultType_Alarm // bit 21 82 | FaultType_OPCUA // bit 22 83 | ) 84 | 85 | func (f FaultType) PowerupFault() bool { 86 | return f&FaultType_Powerup != 0 87 | } 88 | 89 | func (f FaultType) IOFault() bool { 90 | return f&FaultType_IO != 0 91 | } 92 | 93 | func (f FaultType) ProgramFault() bool { 94 | return f&FaultType_Program != 0 95 | } 96 | 97 | func (f FaultType) WatchdogFault() bool { 98 | return f&FaultType_Watchdog != 0 99 | } 100 | 101 | func (f FaultType) NVMemoryFault() bool { 102 | return f&FaultType_NVMemory != 0 103 | } 104 | 105 | func (f FaultType) ModeChangeFault() bool { 106 | return f&FaultType_ModeChange != 0 107 | } 108 | 109 | func (f FaultType) SerialPortFault() bool { 110 | return f&FaultType_SerialPort != 0 111 | } 112 | 113 | func (f FaultType) EnergyStorageFault() bool { 114 | return f&FaultType_EnergyStorage != 0 115 | } 116 | 117 | func (f FaultType) MotionFault() bool { 118 | return f&FaultType_Motion != 0 119 | } 120 | 121 | func (f FaultType) RedundancyFault() bool { 122 | return f&FaultType_Redundancy != 0 123 | } 124 | 125 | func (f FaultType) RTCFault() bool { 126 | return f&FaultType_RTC != 0 127 | } 128 | 129 | func (f FaultType) NonRecoverableFault() bool { 130 | return f&FaultType_NonRecoverable != 0 131 | } 132 | 133 | func (f FaultType) CommunicationFault() bool { 134 | return f&FaultType_Communication != 0 135 | } 136 | 137 | func (f FaultType) DiagnosticsFault() bool { 138 | return f&FaultType_Diagnostics != 0 139 | } 140 | 141 | func (f FaultType) CIPMotionFault() bool { 142 | return f&FaultType_CIPMotion != 0 143 | } 144 | 145 | func (f FaultType) EthernetFault() bool { 146 | return f&FaultType_Ethernet != 0 147 | } 148 | 149 | func (f FaultType) LicenseFault() bool { 150 | return f&FaultType_License != 0 151 | } 152 | 153 | func (f FaultType) AlarmFault() bool { 154 | return f&FaultType_Alarm != 0 155 | } 156 | 157 | func (f FaultType) OPCUAFault() bool { 158 | return f&FaultType_OPCUA != 0 159 | } 160 | 161 | func GetFaults(client *gologix.Client) (FaultSummary, error) { 162 | path, _ := gologix.Serialize(gologix.CIPClass(0x73), gologix.CIPInstance(0x1)) 163 | item, err := client.GenericCIPMessage(gologix.CIPService_GetAttributeAll, path.Bytes(), nil) 164 | 165 | if err != nil { 166 | return FaultSummary{}, err 167 | } 168 | 169 | if item == nil { 170 | return FaultSummary{}, fmt.Errorf("item is nil") 171 | } 172 | 173 | response := FaultSummary{} 174 | 175 | err = item.DeSerialize(&response) 176 | if err != nil { 177 | return FaultSummary{}, fmt.Errorf("could not deserialize response: %v", err) 178 | } 179 | return response, nil 180 | } 181 | -------------------------------------------------------------------------------- /canned/forces.go: -------------------------------------------------------------------------------- 1 | package canned 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | type ForceStatus uint16 10 | 11 | func (f ForceStatus) Enabled() bool { 12 | return f&1 != 0 13 | } 14 | func (f ForceStatus) Exist() bool { 15 | return f&1 != 0 16 | } 17 | 18 | func GetForces(client *gologix.Client) (ForceStatus, error) { 19 | item, err := client.GetAttrList(gologix.CipObject_IOClass, 0, 9) 20 | if err != nil { 21 | return 0, err 22 | } 23 | 24 | if item == nil { 25 | return 0, fmt.Errorf("item is nil") 26 | } 27 | 28 | response := struct { 29 | ID uint16 30 | Status uint16 31 | Forces ForceStatus 32 | }{} 33 | 34 | err = item.DeSerialize(&response) 35 | if err != nil { 36 | return 0, fmt.Errorf("could not deserialize response: %v", err) 37 | } 38 | 39 | return response.Forces, nil 40 | 41 | } 42 | -------------------------------------------------------------------------------- /device_type.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | type DeviceType uint16 4 | 5 | func (v DeviceType) Name() string { 6 | if name, ok := cipDeviceTypeNames[v]; ok { 7 | return name 8 | } 9 | 10 | return "Unknown" 11 | } 12 | 13 | // List of Device Types from the CIP vendor ID list. 14 | // Obtained from the CIP Wireshark dissector on 2025-03-18. 15 | // Available at https://fossies.org/linux/wireshark/epan/dissectors/packet-cip.c 16 | var cipDeviceTypeNames = map[DeviceType]string{ 17 | 0x00: "Generic Device (deprecated)", 18 | 0x02: "AC Drive", 19 | 0x03: "Motor Overload", 20 | 0x04: "Limit Switch", 21 | 0x05: "Inductive Proximity Switch", 22 | 0x06: "Photoelectric Sensor", 23 | 0x07: "General Purpose Discrete I/O", 24 | 0x09: "Resolver", 25 | 0x0C: "Communications Adapter", 26 | 0x0E: "Programmable Logic Controller", 27 | 0x10: "Position Controller", 28 | 0x13: "DC Drive", 29 | 0x15: "Contactor", 30 | 0x16: "Motor Starter", 31 | 0x17: "Soft Start", 32 | 0x18: "Human-Machine Interface", 33 | 0x1A: "Mass Flow Controller", 34 | 0x1B: "Pneumatic Valve", 35 | 0x1C: "Vacuum Pressure Gauge", 36 | 0x1D: "Process Control Value", 37 | 0x1E: "Residual Gas Analyzer", 38 | 0x1F: "DC Power Generator", 39 | 0x20: "RF Power Generator", 40 | 0x21: "Turbomolecular Vacuum Pump", 41 | 0x22: "Encoder", 42 | 0x23: "Safety Discrete I/O Device", 43 | 0x24: "Fluid Flow Controller", 44 | 0x25: "CIP Motion Drive", 45 | 0x26: "CompoNet Repeater", 46 | 0x27: "Mass Flow Controller, Enhanced", 47 | 0x28: "CIP Modbus Device", 48 | 0x29: "CIP Modbus Translator", 49 | 0x2A: "Safety Analog I/O Device", 50 | 0x2B: "Generic Device (keyable)", 51 | 0x2C: "Managed Ethernet Switch", 52 | 0x2D: "CIP Motion Safety Drive Device", 53 | 0x2E: "Safety Drive Device", 54 | 0x2F: "CIP Motion Encoder", 55 | 0x30: "CIP Motion Converter", 56 | 0x31: "CIP Motion I/O", 57 | 0x32: "ControlNet Physical Layer Component", 58 | 0x33: "Circuit Breaker", 59 | 0x34: "HART Device", 60 | 0x35: "CIP-HART Translator", 61 | 0xC8: "Embedded Component", 62 | } 63 | -------------------------------------------------------------------------------- /device_type_test.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestKnownDeviceTypeName(t *testing.T) { 8 | deviceType := DeviceType(0x02) 9 | name := deviceType.Name() 10 | if name != "AC Drive" { 11 | t.Errorf("expected 'AC Drive', got '%s'", name) 12 | } 13 | } 14 | 15 | func TestUnknownDeviceTypeName(t *testing.T) { 16 | deviceType := DeviceType(0x99) 17 | name := deviceType.Name() 18 | if name != "Unknown" { 19 | t.Errorf("expected 'Unknown', got '%s'", name) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /disconnect.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "time" 7 | ) 8 | 9 | // You will want to defer this after a successful Connect() to make sure you free up the controller resources 10 | // to disconnect we send two items - a null item and an unconnected data item for the unregister service 11 | func (client *Client) Disconnect() error { 12 | if client.connecting { 13 | client.Logger.Debug("waiting for client to finish connecting before disconnecting") 14 | for client.connecting { 15 | time.Sleep(time.Millisecond * 10) 16 | } 17 | } 18 | if !client.connected || client.disconnecting { 19 | return nil 20 | } 21 | client.disconnecting = true 22 | defer func() { client.disconnecting = false }() 23 | client.connected = false 24 | var err error 25 | client.Logger.Info("starting disconnection") 26 | 27 | if client.keepAliveRunning { 28 | close(client.cancel_keepalive) 29 | } 30 | 31 | items := make([]CIPItem, 2) 32 | items[0] = CIPItem{} // null item 33 | items[1] = CIPItem{Header: cipItemHeader{ID: cipItem_UnconnectedData}} 34 | 35 | path, err := Serialize( 36 | client.Controller.Path, 37 | CipObject_MessageRouter, 38 | CIPInstance(1), 39 | ) 40 | if err != nil { 41 | client.Logger.Error("Error serializing path", slog.Any("err", err)) 42 | return fmt.Errorf("error serializing path: %w", err) 43 | } 44 | 45 | msg := msgCipUnRegister{ 46 | Service: CIPService_ForwardClose, 47 | CipPathSize: 0x02, 48 | ClassType: cipClass_8bit, 49 | Class: 0x06, 50 | InstanceType: cipInstance_8bit, 51 | Instance: 0x01, 52 | Priority: 0x0A, 53 | TimeoutTicks: 0x0E, 54 | ConnectionSerialNumber: client.ConnectionSerialNumber, 55 | VendorID: client.VendorId, 56 | OriginatorSerialNumber: client.SerialNumber, 57 | PathSize: byte(path.Len() / 2), 58 | Reserved: 0x00, 59 | } 60 | 61 | err = items[1].Serialize(msg) 62 | if err != nil { 63 | return fmt.Errorf("error serializing disconnect msg: %w", err) 64 | } 65 | err = items[1].Serialize(path) 66 | if err != nil { 67 | return fmt.Errorf("error serializing disconnect path: %w", err) 68 | } 69 | 70 | itemData, err := serializeItems(items) 71 | if err != nil { 72 | client.Logger.Error( 73 | "unable to serialize itemData. Forcing connection closed", 74 | slog.Any("err", err), 75 | ) 76 | } else { 77 | header, data, err := client.send_recv_data(cipCommandSendRRData, itemData) 78 | if err != nil { 79 | client.Logger.Error( 80 | "error sending disconnect request", 81 | slog.Any("err", err), 82 | ) 83 | } else { 84 | _, err = client.parseResponse(&header, data) 85 | if err != nil { 86 | client.Logger.Error( 87 | "error parsing disconnect response", 88 | slog.Any("err", err), 89 | ) 90 | } 91 | } 92 | } 93 | 94 | err = client.conn.Close() 95 | if err != nil { 96 | client.Logger.Error("error closing connection", slog.Any("err", err)) 97 | } 98 | 99 | client.Logger.Info("successfully disconnected from controller") 100 | return nil 101 | } 102 | 103 | // Cancels keepalive if KeepAliveAutoStart is false. Use force to cancel keepalive regardless. 104 | // If forced, the keepalive will not resume unless the client is reconnected or KeepAlive is triggered 105 | func (client *Client) KeepAliveCancel(force bool) error { 106 | if client.KeepAliveAutoStart && !force { 107 | return fmt.Errorf("unable to cancel keepalive due to AutoKeepAlive == true") 108 | } 109 | close(client.cancel_keepalive) 110 | return nil 111 | } 112 | 113 | type msgCipUnRegister struct { 114 | Service CIPService 115 | CipPathSize byte 116 | ClassType cipClassSize 117 | Class byte 118 | InstanceType cipInstanceSize 119 | Instance byte 120 | Priority byte 121 | TimeoutTicks byte 122 | ConnectionSerialNumber uint16 123 | VendorID uint16 124 | OriginatorSerialNumber uint32 125 | PathSize uint8 126 | Reserved byte // Always 0x00 127 | } 128 | -------------------------------------------------------------------------------- /docs/EtherNetIP Adapter Protocol API 12 EN.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/docs/EtherNetIP Adapter Protocol API 12 EN.pdf -------------------------------------------------------------------------------- /docs/connectionTimeouts.csv: -------------------------------------------------------------------------------- 1 | ,L27,,,,,Micro 820, 2 | ,rpi (ms),timeout (s),,,,rpi (ms),timeout (s) 3 | ,1000,32,,,,1000, 4 | ,1500,48,,,,1500,48 5 | ,1750,56,,,,1750, 6 | ,2500,80,,,,2500,80 7 | ,7500,240,,,,7500, 8 | ,9000,288,,,,9000,120 9 | ,,,,,,, 10 | ,,,,,,, 11 | ,,,,,,, 12 | ,These times are tests ran with different forward open RPIs to see how long before the PLC closed the TCP connection.,,,,,, 13 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | 2 | # Manufacturer Documentation and Resources: 3 | 4 | - https://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf 5 | - https://www.odva.org/wp-content/uploads/2020/06/PUB00123R1_Common-Industrial_Protocol_and_Family_of_CIP_Networks.pdf 6 | - https://scadahacker.com/library/Documents/ICS_Protocols/Rockwell%20-%20Communicating%20with%20RA%20Products%20Using%20EtherNetIP%20Explicit%20Messaging.pdf 7 | - http://iatips.com/digiwiki/quick_eip_demo.pdf 8 | - https://github.com/EIPStackGroup/OpENer/ 9 | - https://github.com/loki-os/go-ethernet-ip 10 | - https://www.can-cia.org/fileadmin/resources/documents/proceedings/2005_schiffer.pdf 11 | - https://github.com/ruscito/pycomm 12 | - http://www.plctalk.net/qanda/showthread.php?t=133853 13 | - https://www.odva.org/wp-content/uploads/2020/05/PUB00070_Recommended-Functionality-for-EIP-Devices-v10.pdf 14 | - https://literature.rockwellautomation.com/idc/groups/literature/documents/qs/2080-qs002_-en-e.pdf 15 | - https://www.rockwellautomation.com/content/dam/rockwell-automation/sites/downloads/pdf/TypeEncode_CIPRW.pdf 16 | - https://assets.omron.eu/downloads/manual/en/v2/w506_nx_nj-series_cpu_unit_built-in_ethernet_ip_port_users_manual_en.pdf 17 | - https://github.com/JeremyMedders/LogixLibraries 18 | - https://github.com/mikeav-soft/LogixTool/tree/master 19 | - https://www.automation-pros.com/enip1/UserManual.pdf 20 | - https://rockwellautomation.custhelp.com/ci/okcsFattach/get/114390_5 21 | - https://www.rockwellautomation.com/content/dam/rockwell-automation/sites/downloads/pdf/developerguide.pdf -------------------------------------------------------------------------------- /docs/server.md: -------------------------------------------------------------------------------- 1 | If you use a connected message from a logix processor it will send the same message over and over again with the same sequence number. It sends them at the RPI of the msg command. 2 | 3 | You can find the RPI in the msg tag of the tag browser as the ".ConnectionRate" tag. 4 | 5 | 6 | cipService_GetAttributeAll 7 | 8 | ignition handshake: 9 | 82 2 32 6 36 1 3 250 6 0 1 2 32 1 36 1 1 0 1 0 10 | 11 | kepware handshake: 12 | 82 2 32 6 36 1 7 233 6 0 1 2 32 1 36 1 1 0 1 0 13 | service path size path Timeout msg req size msg req path 14 | 0x20 0x06 15 | 0x24 0x01 16 | connection manager 17 | instance 1 -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // Represents a CIP error. 9 | type CIPError struct { 10 | Code byte 11 | Extended uint16 12 | } 13 | 14 | func (err *CIPError) Error() string { 15 | ec := fmt.Sprintf("error %X%X: ", err.Code, err.Extended) 16 | switch err.Code { 17 | case 0x00: 18 | return ec + " no error? This shouldn't happen :/" 19 | case 0x01: 20 | return ec + " connection failure" 21 | case 0x02: 22 | return ec + " resource unavailable" 23 | case 0x03: 24 | return ec + " bad parameter, size > 12 or size greater than size of element." 25 | case 0x04: 26 | return ec + " a syntax error was detected decoding the request path" 27 | case 0x05: 28 | return ec + " request path destination unknown: probably instance number is not present" 29 | case 0x06: 30 | return ec + " insufficient packet space: not enough room in the response buffer for all the data" 31 | case 0x07: 32 | return ec + " connection lost" 33 | case 0x08: 34 | return ec + " service is not supported for the object/instance" 35 | case 0x09: 36 | return ec + " could not write attribute data - possibly in valid or wrong type" 37 | case 0x0A: 38 | return ec + " attribute list error, generally attribute not supported. the status of the unsupported attribute is 0x14" 39 | case 0x10: 40 | switch err.Extended { 41 | case 0x2101: 42 | return ec + " device state conflict: keyswitch position: the requestor is changing force information in HARD RUN mode" 43 | case 0x2802: 44 | return ec + " device state conflict: safety status: the controller is in a state in which safety memory cannot be modified" 45 | } 46 | case 0x13: 47 | return ec + " insufficient Request Data: Data too short for expected param" 48 | case 0x16: 49 | return ec + " object does not exist" 50 | case 0x1A: 51 | return ec + " routing failure: request too large" 52 | case 0x1B: 53 | return ec + " routing failure: response too large" 54 | case 0x1C: 55 | return ec + " attribute list shortage: the list of attribute numbers was too few for the number of attributes parameter" 56 | case 0x26: 57 | return ec + " the request path size received was shorter or longer than expected." 58 | case 0xFF: 59 | switch err.Extended { 60 | case 0x2104: 61 | return ec + "General Error: Offset is beyond end of the requested tag." 62 | case 0x2105: 63 | return ec + "General Error: Number of Elements or Byte Offset is beyond the end of the requested tag." 64 | case 0x2107: 65 | return ec + "General Error: Tag type used n request does not match the target tag's data type." 66 | } 67 | } 68 | 69 | return ec + "Unknown Error" 70 | } 71 | 72 | func newMultiError(err error) multiError { 73 | if err != nil { 74 | return multiError{[]error{err}} 75 | } 76 | return multiError{[]error{}} 77 | } 78 | 79 | // combine multiple errors together. 80 | type multiError struct { 81 | errs []error 82 | } 83 | 84 | func (e *multiError) Add(err error) { 85 | e.errs = append(e.errs, err) 86 | } 87 | 88 | func (e multiError) Error() string { 89 | err_str := "" 90 | for i := range e.errs { 91 | err_str = fmt.Sprintf("%s: %s", err_str, e.errs[i]) 92 | } 93 | return err_str 94 | } 95 | 96 | func (e multiError) Unwrap() error { 97 | if len(e.errs) > 2 { 98 | e.errs = e.errs[1:] 99 | return e 100 | } 101 | if len(e.errs) == 2 { 102 | return e.errs[1] 103 | } 104 | if len(e.errs) == 1 { 105 | return e.errs[0] 106 | } 107 | return nil 108 | } 109 | 110 | func (e multiError) Is(target error) bool { 111 | return errors.Is(e.errs[0], target) 112 | } 113 | -------------------------------------------------------------------------------- /examples/ArrayRead/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for reading elements from a DINT array named "TestDintArr" in the controller. 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | // client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 28 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 29 | // if that happens (about a minute) 30 | defer client.Disconnect() 31 | 32 | // define a variable with a type that matches the tag you want to read. 33 | // In this case we're creating an int32 slice of length 5 to hold 5 elements from the DINT array. 34 | tag1 := make([]int32, 5) 35 | 36 | // call the read function. 37 | // This reads 5 elements from TestDintArr starting at index 2. 38 | // note that tag names are case insensitive. 39 | // also note that for atomic types and structs you need to use a pointer. 40 | // for slices you don't use a pointer. 41 | err = client.Read("TestDintArr[2]", tag1) 42 | if err != nil { 43 | log.Printf("error reading TestDintArr. %v", err) 44 | } 45 | // do whatever you want with the value 46 | log.Printf("tag1 has value %d", tag1) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /examples/Canned/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | "github.com/danomagnum/gologix/canned" 8 | ) 9 | 10 | func main() { 11 | // This is an example of using functions in the canned package to do common tasks. 12 | var err error 13 | 14 | // setup the client. If you need a different path you'll have to set that. 15 | client := gologix.NewClient("192.168.2.241") 16 | 17 | // for example, to have a controller on slot 1 instead of 0 you could do this 18 | // client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 19 | // or this 20 | // client.Path, err = gologix.ParsePath("1,1") 21 | 22 | // connect using parameters in the client struct 23 | err = client.Connect() 24 | if err != nil { 25 | log.Printf("Error opening client. %v", err) 26 | return 27 | } 28 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 29 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 30 | // if that happens (about a minute) 31 | defer client.Disconnect() 32 | 33 | forceStatus, err := canned.GetForces(client) 34 | if err != nil { 35 | log.Printf("error reading forces. %v", err) 36 | return 37 | } 38 | if forceStatus.Exist() { 39 | log.Printf("Forces exist in the controller") 40 | } else { 41 | log.Printf("Forces do not exist in the controller") 42 | } 43 | 44 | if forceStatus.Enabled() { 45 | log.Printf("Forces are enabled in the controller") 46 | } else { 47 | log.Printf("Forces are not enabled in the controller") 48 | } 49 | 50 | faultCode, err := canned.GetFaults(client) 51 | if err != nil { 52 | log.Printf("error reading fault code. %v", err) 53 | return 54 | } 55 | if faultCode.MajorType != 0 || faultCode.MinorType != 0 { 56 | log.Printf("Fault code: %d:%d", faultCode.MajorType, faultCode.MinorType) 57 | log.Printf("Fault Description: %s", faultCode.Events[0]) 58 | } else { 59 | log.Printf("No fault code") 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /examples/ConnectionDropTest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/danomagnum/gologix" 10 | ) 11 | 12 | // Demo program for testing PLC connection reliability by reading 13 | // a tag at increasingly longer intervals until the connection drops. 14 | func main() { 15 | var err error 16 | 17 | // Setup the client with the PLC IP address 18 | client := gologix.NewClient("192.168.2.244") 19 | client.Controller.Path = &bytes.Buffer{} 20 | 21 | // For example, to have a controller on slot 1 instead of 0 you could do this 22 | // client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 23 | // or this 24 | // client.Path, err = gologix.ParsePath("1,1") 25 | 26 | // Connect using parameters in the client struct 27 | err = client.Connect() 28 | if err != nil { 29 | log.Printf("Error opening client. %v", err) 30 | os.Exit(1) 31 | } 32 | // Setup a deferred disconnect. If you don't disconnect properly, you might have 33 | // trouble reconnecting because you won't have sent the close forward open. 34 | // You'll have to wait for the CIP connection to time out if that happens (about a minute) 35 | defer client.Disconnect() 36 | 37 | // Define a variable with a type that matches the tag you want to read. 38 | // In this case, we're using int32 for the 'inputs[0]' tag 39 | var tag1 int32 40 | 41 | // Initial read of the tag to verify connection 42 | err = client.Read("inputs[0]", &tag1) 43 | if err != nil { 44 | log.Printf("error reading inputs[0]. %v", err) 45 | os.Exit(1) 46 | } 47 | 48 | // Start with a delay of 287 seconds and increase by 1 second each iteration 49 | t := time.Second * 287 50 | for { 51 | time.Sleep(t) 52 | err = client.Read("inputs[0]", &tag1) 53 | if err != nil { 54 | log.Printf("error reading inputs[0] after %v seconds: %v", t, err) 55 | break 56 | } 57 | // Log the value and time interval 58 | log.Printf("after %v seconds tag1 has value %d", t, tag1) 59 | t += time.Second * 1 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/GenericCIPMessage/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for doing a generic CIP message. 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 28 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 29 | // if that happens (about a minute) 30 | defer client.Disconnect() 31 | 32 | // for generic messages we need to create the cip path ourselves. The serialize function can be used to do this. 33 | path, err := gologix.Serialize(gologix.CipObject_RunMode, gologix.CIPInstance(1)) 34 | if err != nil { 35 | log.Printf("could not serialize path: %v", err) 36 | return 37 | } 38 | 39 | // This generic message would probably stop the controller, but you'd have to figure out how to elevate 40 | // the privileges associated with your connection first. As it stands, you will probably get an 0x0F status code 41 | // and it won't do anything. 42 | resp, err := client.GenericCIPMessage(gologix.CIPService_Stop, path.Bytes(), []byte{}) 43 | if err != nil { 44 | log.Printf("problem stopping PLC: %v", err) 45 | return 46 | } 47 | _ = resp 48 | 49 | } 50 | -------------------------------------------------------------------------------- /examples/GetAttribute/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for reading attributes from a logix PLC 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 28 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 29 | // if that happens (about a minute) 30 | defer client.Disconnect() 31 | 32 | // the attributes of the Identity object are as follows: 33 | // 34 | // 1 - vendor ID 35 | // 2 - Device Type 36 | // 3 - Product Code 37 | // 4 - Revision 38 | // 5 - Status 39 | // 6 - Serial Number 40 | // 7 - Product Name 41 | item, err := client.GetAttrSingle(gologix.CipObject_Identity, 1, 1) 42 | if err != nil { 43 | log.Fatalf("problem reading attribute 1: %v", err) 44 | } 45 | 46 | // The GetAttrSingle returns an entire CIP Object (some attributes may have complex data responses) 47 | // so you have to parse them yourself. For simple data types you can just call the appropriate 48 | // function on the item to get the value directly. 49 | 50 | // Instance 1 of the identity object is a 16 bit vendor ID 51 | vendor, err := item.Int16() 52 | if err != nil { 53 | // We could have an error here if there wasn't enough data returned for example. 54 | log.Fatalf("problem getting vendor ID from the response item: %v", err) 55 | } 56 | log.Printf("Vendor ID: %X", vendor) 57 | 58 | item, err = client.GetAttrList(gologix.CipObject_Identity, 1, 1, 2, 3, 4) 59 | if err != nil { 60 | log.Fatalf("problem reading list: %v", err) 61 | } 62 | 63 | // The GetAttrList returns an entire CIP Object as described in the function docs. 64 | // Since we have to know before hand what the types are for the attributes we are 65 | // reading, we can create a struct to parse the response all at once. 66 | 67 | type AttrResults struct { 68 | Attr1_ID uint16 69 | Attr1_Status uint16 70 | Attr1_Value uint16 71 | 72 | Attr2_ID uint16 73 | Attr2_Status uint16 74 | Attr2_Value uint16 75 | 76 | Attr3_ID uint16 77 | Attr3_Status uint16 78 | Attr3_Value uint32 79 | 80 | Attr4_ID uint16 81 | Attr4_Status uint16 82 | Attr4_Value uint32 83 | } 84 | 85 | var ar AttrResults 86 | 87 | err = item.DeSerialize(&ar) 88 | if err != nil { 89 | log.Fatalf("Problem parsing attribute results: %v", err) 90 | } 91 | log.Printf("Attr results: %+v", ar) 92 | 93 | } 94 | -------------------------------------------------------------------------------- /examples/GetControllerTime/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/danomagnum/gologix" 9 | ) 10 | 11 | // this program will read the current controller time out of the PLC using a custom generic CIP message. 12 | // this could also be done with the GetAttrList() function (see tests/getattr_test.go) and I would recommend that 13 | // method for this specific purpose, but this example should apply to other devices like drives and other services 14 | // on PLCs as desired. 15 | func main() { 16 | client := gologix.NewClient("192.168.2.241") 17 | err := client.Connect() 18 | if err != nil { 19 | fmt.Print(err) 20 | return 21 | } 22 | defer func() { 23 | err := client.Disconnect() 24 | if err != nil { 25 | fmt.Printf("problem disconnecting. %v", err) 26 | } 27 | }() 28 | 29 | // for generic messages we need to create the cip path ourselves. The serialize function can be used to do this. 30 | path, err := gologix.Serialize(gologix.CipObject_TIME, gologix.CIPInstance(1)) 31 | if err != nil { 32 | log.Printf("could not serialize path: %v", err) 33 | return 34 | } 35 | 36 | r, err := client.GenericCIPMessage(gologix.CIPService_GetAttributeList, path.Bytes(), []byte{0x01, 0x00, 0x0B, 0x00}) 37 | if err != nil { 38 | fmt.Printf("bad result: %v", err) 39 | return 40 | } 41 | type response_str struct { 42 | Attr_Count int16 43 | Attr_ID uint16 44 | Status uint16 45 | Usecs int64 // the microseconds since the unix epoch. 46 | } 47 | 48 | rs := response_str{} 49 | err = r.DeSerialize(&rs) 50 | if err != nil { 51 | fmt.Printf("could not deserialize response structure: %v", err) 52 | return 53 | } 54 | 55 | log.Printf("result: us: %v / %v", rs.Usecs, time.UnixMicro(int64(rs.Usecs))) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /examples/Heartbeat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/danomagnum/gologix" 8 | ) 9 | 10 | // Demo program for monitoring a heartbeat tag named "TestHeartBeat" in the controller. 11 | // This example demonstrates how to detect when a value stops changing, which can be used 12 | // to determine if a remote process is still running. 13 | func main() { 14 | var err error 15 | 16 | // setup the client. If you need a different path you'll have to set that. 17 | client := gologix.NewClient("192.168.2.241") 18 | 19 | // for example, to have a controller on slot 1 instead of 0 you could do this 20 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 21 | // or this 22 | // client.Path, err = gologix.ParsePath("1,1") 23 | 24 | // connect using parameters in the client struct 25 | err = client.Connect() 26 | if err != nil { 27 | log.Printf("Error opening client. %v", err) 28 | return 29 | } 30 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 31 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 32 | // if that happens (about a minute) 33 | defer client.Disconnect() 34 | 35 | // the client should be threadsafe so kicking off goroutines doing reads/writes in the background like this should be no issue. 36 | hbChan := Heartbeat[bool](client, "TestHeartBeat", time.Second, time.Second*10) 37 | 38 | for { 39 | hbStatusChange := <-hbChan 40 | 41 | if hbStatusChange { 42 | log.Printf("heartbeat now OK") 43 | } else { 44 | log.Printf("heartbeat now bad") 45 | } 46 | 47 | } 48 | 49 | } 50 | 51 | // monitor tag for changes in a background goroutine. 52 | // poll at an interval of pollrate 53 | // sends a single true on the output channel if the value starts to change 54 | // sends a single false on the output channel if the value stops changing for timeout 55 | func Heartbeat[T gologix.GoLogixTypes](client *gologix.Client, tag string, pollrate, timeout time.Duration) <-chan bool { 56 | 57 | hbstatus := make(chan bool) 58 | 59 | go func() { 60 | // heart beat value variables 61 | var lastHB T 62 | var newHB T 63 | 64 | // hb last change reference 65 | lastHB_Time := time.Now() 66 | 67 | // hb status 68 | ok := false 69 | 70 | ticker := time.NewTicker(pollrate) 71 | for range ticker.C { 72 | err := client.Read(tag, &newHB) 73 | if err != nil { 74 | // heartbeat read failed. send an edge triggered message about that. 75 | if ok { 76 | hbstatus <- false 77 | ok = false 78 | } 79 | continue 80 | } 81 | // if the value changes, update the timestamp and set to ok flag to true. 82 | if newHB != lastHB { 83 | lastHB_Time = time.Now() 84 | if !ok { 85 | hbstatus <- true 86 | } 87 | ok = true 88 | } 89 | 90 | // see if we've timed out. Send an edge triggered message out. 91 | if time.Since(lastHB_Time) > timeout { 92 | if ok { 93 | hbstatus <- false 94 | ok = false 95 | } 96 | } 97 | lastHB = newHB 98 | } 99 | }() 100 | 101 | return hbstatus 102 | 103 | } 104 | -------------------------------------------------------------------------------- /examples/HttpServer/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type PLCConfig struct { 4 | Name string 5 | Address string 6 | Path string 7 | } 8 | 9 | type ServerConfig struct { 10 | Address string 11 | Port int 12 | TLS_Cert string 13 | TLS_Key string 14 | } 15 | 16 | type AppConfig struct { 17 | Server ServerConfig 18 | PLCs []PLCConfig 19 | } 20 | 21 | var Config AppConfig 22 | -------------------------------------------------------------------------------- /examples/HttpServer/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": 3 | { 4 | "address":"localhost", 5 | "port":8080, 6 | "tls_cert":"", 7 | "tls_key":"" 8 | 9 | }, 10 | "plcs": 11 | [ 12 | { 13 | "name":"L27", 14 | "address":"192.168.2.241", 15 | "path":"1,0" 16 | }, 17 | { 18 | "name":"M820", 19 | "address":"192.168.2.244", 20 | "path":"" 21 | } 22 | ] 23 | 24 | } -------------------------------------------------------------------------------- /examples/HttpServer/main.go: -------------------------------------------------------------------------------- 1 | // this example is for a web server that allows GET requests to read tags and POST requests to write them. 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "os" 11 | "strings" 12 | 13 | "github.com/danomagnum/gologix" 14 | ) 15 | 16 | var Connections = make(map[string]*gologix.Client) 17 | 18 | func main() { 19 | 20 | configfile, err := os.Open("config.json") 21 | if err != nil { 22 | log.Panicf("couldn't open config.json. %v", err) 23 | } 24 | jd := json.NewDecoder(configfile) 25 | err = jd.Decode(&Config) 26 | if err != nil { 27 | log.Panicf("Problem reading config.json: %v", err) 28 | } 29 | log.Printf("Config: %+v", Config) 30 | 31 | log.Printf("=== Connecting to PLCs. ===") 32 | for _, plcconf := range Config.PLCs { 33 | path, err := gologix.ParsePath(plcconf.Path) 34 | if err != nil { 35 | log.Printf("problem with plc connection %s. Can't parse path. %v", plcconf.Name, err) 36 | continue 37 | } 38 | controller := gologix.Controller{ 39 | IpAddress: plcconf.Address, 40 | Path: path, 41 | } 42 | conn := gologix.Client{ 43 | Controller: controller, 44 | } 45 | 46 | err = conn.Connect() 47 | if err != nil { 48 | log.Printf("problem with plc connection %s. Can't connect. %v", plcconf.Name, err) 49 | continue 50 | } 51 | defer conn.Disconnect() 52 | Connections[plcconf.Name] = &conn 53 | } 54 | log.Printf("=== Starting Webserver. ===") 55 | 56 | // set up a web request handler and start the server 57 | mux := http.NewServeMux() 58 | mux.HandleFunc("/", httpreq) 59 | connection_addr := fmt.Sprintf("%s:%d", Config.Server.Address, Config.Server.Port) 60 | 61 | if Config.Server.TLS_Cert != "" { 62 | err = http.ListenAndServeTLS(connection_addr, Config.Server.TLS_Cert, Config.Server.TLS_Key, mux) 63 | if err != nil { 64 | log.Panicf("problem starting https server. %v", err) 65 | } 66 | 67 | } else { 68 | // no TLS cert specified - just use a plain HTTP server. 69 | err = http.ListenAndServe(connection_addr, mux) 70 | log.Panicf("problem starting http server. %v", err) 71 | } 72 | 73 | } 74 | 75 | func httpreq(w http.ResponseWriter, r *http.Request) { 76 | log.Printf("Request Path: %v", r.URL.Path) 77 | switch r.Method { 78 | case "GET": 79 | // reads 80 | httpread(w, r) 81 | case "POST": 82 | // writes 83 | httpwrite(w, r) 84 | default: 85 | // not supported 86 | } 87 | } 88 | 89 | func httpread(w http.ResponseWriter, r *http.Request) { 90 | conn, path, err := parsePLC(r.URL.Path) 91 | if err != nil { 92 | // problem getting connection. 93 | w.Write([]byte(fmt.Sprintf("Problem connecting. %v", err))) 94 | return 95 | } 96 | value, err := conn.Read_single(path, gologix.CIPType(0), 1) 97 | if err != nil { 98 | w.Write([]byte(fmt.Sprintf("Problem reading. %v", err))) 99 | return 100 | } 101 | w.Write([]byte(fmt.Sprintf("Value: %v", value))) 102 | 103 | } 104 | 105 | func httpwrite(w http.ResponseWriter, r *http.Request) { 106 | // not implemented yet. 107 | } 108 | 109 | func parsePLC(reqPath string) (*gologix.Client, string, error) { 110 | if reqPath[0] == '/' { 111 | reqPath = reqPath[1:] 112 | } 113 | parts := strings.Split(reqPath, "/") 114 | if len(parts) == 0 { 115 | return nil, "", fmt.Errorf("could not get PLC from %v", reqPath) 116 | } 117 | 118 | client, ok := Connections[parts[0]] 119 | if !ok { 120 | return nil, "", fmt.Errorf("unknown PLC '%v'", parts[0]) 121 | } 122 | switch len(parts) { 123 | case 2: 124 | return client, parts[1], nil 125 | case 1: 126 | return client, "", nil 127 | default: 128 | return nil, "", errors.New("bad path") 129 | 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /examples/ListIdentityAndServices/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/danomagnum/gologix" 9 | ) 10 | 11 | // Demo program showing how to use ListIdentity and ListServices 12 | // to discover information about EtherNet/IP devices on the network. 13 | func main() { 14 | var err error 15 | 16 | // Setup the client with the PLC IP address 17 | // Replace with your actual device IP 18 | client := gologix.NewClient("192.168.2.244") 19 | 20 | // Connect using parameters in the client struct 21 | err = client.Connect() 22 | if err != nil { 23 | log.Printf("Error opening client. %v", err) 24 | os.Exit(1) 25 | } 26 | // Setup a deferred disconnect for proper cleanup 27 | defer client.Disconnect() 28 | 29 | // List Identity - Get information about the device 30 | fmt.Println("Listing Identity Information:") 31 | identity, err := client.ListIdentity() 32 | if err != nil { 33 | log.Printf("Error listing identity: %v", err) 34 | os.Exit(1) 35 | } 36 | 37 | // Display the identity information 38 | fmt.Printf("Identity Response: %+v\n", identity) 39 | fmt.Println() 40 | 41 | // List Services - Get information about available services 42 | fmt.Println("Listing Available Services:") 43 | services, err := client.ListServices() 44 | if err != nil { 45 | log.Printf("Error listing services: %v", err) 46 | os.Exit(1) 47 | } 48 | 49 | // Display the services information 50 | fmt.Printf("Services Response: %+v\n", services) 51 | fmt.Println() 52 | 53 | // You can further process and display specific fields from the 54 | // identity and services responses based on their actual structure 55 | } 56 | -------------------------------------------------------------------------------- /examples/ListPrograms/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/danomagnum/gologix" 8 | ) 9 | 10 | // Demo program for listing programs and tags from a PLC. 11 | func main() { 12 | var err error 13 | 14 | // Setup the client to connect to a PLC with the specified IP address 15 | client := gologix.NewClient("192.168.2.241") 16 | 17 | // For example, to have a controller on slot 1 instead of 0 you could do this: 18 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 19 | // or this: 20 | // client.Path, err = gologix.ParsePath("1,1") 21 | client.KeepAliveAutoStart = false 22 | 23 | // Connect to the PLC using parameters in the client struct 24 | err = client.Connect() 25 | if err != nil { 26 | log.Printf("Error opening client. %v", err) 27 | return 28 | } 29 | // Setup a deferred disconnect. If you don't disconnect properly, you might have trouble reconnecting 30 | // as the CIP connection will need to time out (about a minute) 31 | defer client.Disconnect() 32 | 33 | // List all programs on the PLC 34 | err = client.ListAllPrograms() 35 | if err != nil { 36 | log.Printf("Error getting program list. %v", err) 37 | return 38 | } 39 | 40 | // For a specific program, list its subtags with a depth of 1 41 | for _, p := range client.KnownPrograms { 42 | if p.Name == "gologix_tests" { 43 | client.ListSubTags(p, 1) 44 | } 45 | } 46 | 47 | // Display the number of tags found 48 | fmt.Printf("Found %d tags.\n", len(client.KnownTags)) 49 | } 50 | -------------------------------------------------------------------------------- /examples/LogixMsg_HttpServer/main.go: -------------------------------------------------------------------------------- 1 | // this example is for a web server that accepts "push" message from a controller. 2 | // the controller writes using a MSG instruction to this server and that value and tag will show up when doing a http GET 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "log" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/danomagnum/gologix" 12 | ) 13 | 14 | func main() { 15 | 16 | r := gologix.PathRouter{} 17 | 18 | // one memory based tag provider at slot 0 on the virtual "backplane" 19 | // a message to 2,xxx.xxx.xxx.xxx,1,0 (compact logix) will get this provider (where xxx.xxx.xxx.xxx is the ip address 20 | // of the computer running this server) The message path before the IP address in the msg instruction will be different 21 | // based on the actual controller you're using, but the part after the IP address is what this matches 22 | p1 := gologix.MapTagProvider{} 23 | path1, err := gologix.ParsePath("1,0") 24 | if err != nil { 25 | log.Printf("problem parsing path. %v", err) 26 | os.Exit(1) 27 | } 28 | r.Handle(path1.Bytes(), &p1) 29 | 30 | // create the ethernet/ip class 3 message server 31 | s := gologix.NewServer(&r) 32 | go s.Serve() 33 | 34 | // this is the function that will handle the web requests 35 | // we'll get a lock on the tag provider, marshal the data into json, and then return that 36 | // you could get fancier and allow retrieving specific tag keys from the data map. 37 | // you could also create a function that supports posting to specific keys that could be read back 38 | // with "CIP Data Table Read" msgs in the controller. You'd need to be careful with types though - the json 39 | // library likes to use float64 for values. 40 | send_json := func(w http.ResponseWriter, req *http.Request) { 41 | w.Header().Set("Content-Type", "application/json") 42 | p1.Mutex.Lock() 43 | defer p1.Mutex.Unlock() 44 | enc := json.NewEncoder(w) 45 | err := enc.Encode(p1.Data) 46 | if err != nil { 47 | w.WriteHeader(http.StatusInternalServerError) 48 | return 49 | } 50 | } 51 | 52 | // set up a web request handler and start the server 53 | mux := http.NewServeMux() 54 | mux.HandleFunc("/", send_json) 55 | http.ListenAndServe("localhost:8080", mux) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /examples/MapRead/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for readng multiple tags at once where the tags are in a go map. 10 | // the keys in the map are the tag names, and the values need to be the correct type 11 | // for the tag. The ReadMap function will update the values in the map to the current values 12 | // in the controller. 13 | func main() { 14 | var err error 15 | 16 | // setup the client. If you need a different path you'll have to set that. 17 | client := gologix.NewClient("192.168.2.241") 18 | 19 | // for example, to have a controller on slot 1 instead of 0 you could do this 20 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 21 | // or this 22 | // client.Path, err = gologix.ParsePath("1,1") 23 | 24 | // connect using parameters in the client struct 25 | err = client.Connect() 26 | if err != nil { 27 | log.Printf("Error opening client. %v", err) 28 | return 29 | } 30 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 31 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 32 | // if that happens (about a minute) 33 | defer client.Disconnect() 34 | 35 | // define a struct where fields have the tag to read from the controller specified 36 | // note that tag names are case insensitive. 37 | m := make(map[string]any) 38 | m["TestInt"] = int16(0) 39 | m["TestDint"] = int32(0) 40 | m["TestDintArr[2]"] = make([]int32, 5) 41 | 42 | // call the read multi function with the structure passed in as a pointer. 43 | err = client.ReadMulti(m) 44 | if err != nil { 45 | log.Printf("error reading testint. %v", err) 46 | } 47 | // do whatever you want with the values 48 | log.Printf("multiread map has values %+v", m) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /examples/Micro820_List/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | 7 | "github.com/danomagnum/gologix" 8 | ) 9 | 10 | // Demo program for readng an INT tag named "TestInt" in the controller. 11 | func main() { 12 | var err error 13 | 14 | // setup the client. If you need a different path you'll have to set that. 15 | client := gologix.NewClient("192.168.2.244") 16 | // micro8xx use no path. So an empty buffer will give us that. 17 | client.Controller.Path = &bytes.Buffer{} 18 | 19 | // connect using parameters in the client struct 20 | err = client.Connect() 21 | if err != nil { 22 | log.Printf("Error opening client. %v", err) 23 | return 24 | } 25 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 26 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 27 | // if that happens (about a minute) 28 | defer client.Disconnect() 29 | 30 | err = client.ListAllTags(0) 31 | if err != nil { 32 | log.Printf("Error reading tags: %v", err) 33 | return 34 | } 35 | log.Printf("All Tags:") 36 | for i := range client.KnownTags { 37 | tag := client.KnownTags[i] 38 | log.Printf("%v: / %v[%v]", tag.Name, tag.Info.Type, tag.Info.Dimension1) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /examples/Micro820_Read/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | 7 | "github.com/danomagnum/gologix" 8 | ) 9 | 10 | // Demo program for reading various tag types from a Micro820 controller. 11 | // This example shows how to read: 12 | // - An array of integers from "inputs" tag 13 | // - A boolean value from "MyVar1" tag 14 | // - An integer value from "MyVar2" tag 15 | // - And demonstrates that multi-reads are not supported on Micro8x0 series 16 | func main() { 17 | var err error 18 | 19 | // setup the client. If you need a different path you'll have to set that. 20 | client := gologix.NewClient("192.168.2.244") 21 | // micro8xx use no path. So an empty buffer will give us that. 22 | client.Controller.Path = &bytes.Buffer{} 23 | 24 | // connect using parameters in the client struct 25 | err = client.Connect() 26 | if err != nil { 27 | log.Printf("Error opening client. %v", err) 28 | return 29 | } 30 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 31 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 32 | // if that happens (about a minute) 33 | defer client.Disconnect() 34 | 35 | // define a variable with a type that matches the tag you want to read. In this case it is an INT so 36 | // int16 or uint16 will work. 37 | input_dat := make([]int32, 8) 38 | 39 | // call the read function. 40 | // note that tag names are case insensitive. 41 | // also note that for atomic types and structs you need to use a pointer. 42 | // for slices you don't use a pointer. 43 | // 44 | // As far as I can tell you can't read program scope tags 45 | err = client.Read("inputs", input_dat) 46 | if err != nil { 47 | log.Printf("error reading 'input' tag. %v\n", err) 48 | } 49 | // do whatever you want with the value 50 | log.Printf("input_dat has value %d\n", input_dat) 51 | 52 | var mybool bool 53 | err = client.Read("MyVar1", &mybool) 54 | if err != nil { 55 | log.Printf("error reading 'MyVar1' tag. %v\n", err) 56 | } 57 | log.Printf("MyVar1 = %v", mybool) 58 | 59 | var mydint int32 60 | err = client.Read("MyVar2", &mydint) 61 | if err != nil { 62 | log.Printf("error reading 'MyVar2' tag. %v\n", err) 63 | } 64 | log.Printf("MyVar2 = %v", mydint) 65 | 66 | // Note that this will NOT work. Micro 8x0 does not support multi-reads. 67 | readall := struct { 68 | MyVar1 bool `gologix:"MyVar1"` 69 | MyVar2 int32 `gologix:"MyVar2"` 70 | InputDat []int32 `gologix:"inputs"` 71 | }{} 72 | readall.InputDat = make([]int32, 8) 73 | 74 | err = client.ReadMulti(&readall) 75 | if err != nil { 76 | // this error will always happen with a micro 8x0 77 | log.Printf("error reading multi. %v\n", err) 78 | } 79 | log.Printf("Multi = %+v", readall) 80 | 81 | } 82 | -------------------------------------------------------------------------------- /examples/MultiRead/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for readng an INT tag named "TestInt" in the controller. 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 28 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 29 | // if that happens (about a minute) 30 | defer client.Disconnect() 31 | 32 | // define a struct where fields have the tag to read from the controller specified 33 | // note that tag names are case insensitive. 34 | type multiread struct { 35 | TestInt int16 `gologix:"TestInt"` 36 | TestDint int32 `gologix:"TestDint"` 37 | TestArr []int32 `gologix:"TestDintArr[2]"` 38 | } 39 | var mr multiread 40 | mr.TestArr = make([]int32, 5) 41 | 42 | // call the read multi function with the structure passed in as a pointer. 43 | err = client.ReadMulti(&mr) 44 | if err != nil { 45 | log.Printf("error reading testint. %v", err) 46 | } 47 | // do whatever you want with the values 48 | log.Printf("multiread struct has values %+v", mr) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /examples/MultiWrite/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for writing multiple tags at once. 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a disconnect. If you don't disconnect you might have trouble reconnecting 28 | defer client.Disconnect() 29 | 30 | // Set up a map[string]any of tag:value pairs. 31 | 32 | write_map := make(map[string]any) 33 | write_map["program:gologix_tests.MultiWriteInt"] = int16(123) 34 | write_map["program:gologix_tests.MultiWriteReal"] = float32(456.7) 35 | write_map["program:gologix_tests.MultiWriteDint"] = int32(891011) 36 | write_map["program:gologix_tests.MultiWriteString"] = "Test String!" 37 | write_map["program:gologix_tests.MultiWriteBool"] = true 38 | 39 | err = client.WriteMap(write_map) 40 | if err != nil { 41 | log.Printf("error writing to multiple tags at once: %v", err) 42 | } 43 | 44 | log.Printf("No Errors!") 45 | // no error = write OK. 46 | 47 | } 48 | -------------------------------------------------------------------------------- /examples/ProductionService/main.go: -------------------------------------------------------------------------------- 1 | // This example shows a good way to continuously poll a PLC and get the data back into your main program without having to worry about 2 | // reconnect logic, etc... 3 | // 4 | // Obviously this won't work for every scenario but it is a pretty solid foundation for simple things. 5 | // 6 | // The basic gist is that we create a channel and kick a goroutine off to pump data into that channel at a specified rate 7 | // as the PLC is polled. Then the main program can just receive on that channel to get fresh PLC data. 8 | package main 9 | 10 | import ( 11 | "log" 12 | "time" 13 | 14 | "github.com/danomagnum/gologix" 15 | ) 16 | 17 | type PLCPollData struct { 18 | Testtag1 int32 `gologix:"testtag1"` 19 | Testtag2 float32 `gologix:"testtag2"` 20 | Testdint int32 `gologix:"testdint"` 21 | Testint int16 `gologix:"testint"` 22 | } 23 | 24 | func main() { 25 | // Here i'm using a struct and a multi-read, but you could use whatever makes sense for your application. 26 | c := make(chan PLCPollData) 27 | 28 | watchdog := time.NewTicker(time.Minute) 29 | 30 | go StartPLCComms(c) 31 | 32 | for { 33 | select { 34 | case newdata := <-c: 35 | // we got a new message so we'll reset the watchdog. 36 | watchdog.Reset(time.Minute) 37 | // now do whatever we want with the new data. 38 | log.Printf("multiread map has values %+v", newdata) 39 | case <-watchdog.C: 40 | // uh-oh, we didn't get new data from the PLC in longer than we were expecting. 41 | log.Printf("Didn't receive a message in too long!!!") 42 | 43 | } 44 | } 45 | } 46 | 47 | // this handles connecting and reconnecting to the PLC and then getting data forever. 48 | // it runs as a goroutine in the background 49 | // PollPLC could be inlined or made an inline anonymous function if desired. 50 | func StartPLCComms(c chan PLCPollData) { 51 | for { 52 | PollPLC(c) 53 | time.Sleep(time.Second * 10) 54 | log.Print("Retrying connection in 10 seconds") 55 | } 56 | 57 | } 58 | 59 | // This is a separate function from prod_handler so i can defer() the Disconnect. This makes it cleaner since now 60 | // I don't have to worry about where we return from - we'll still clean up the connection resources properly. 61 | func PollPLC(c chan PLCPollData) { 62 | 63 | // connect 64 | // Connect to the PLC. You can hard code the address as shown or make it a parameter or something. 65 | client := gologix.NewClient("localhost") 66 | err := client.Connect() 67 | if err != nil { 68 | log.Printf("Error opening client: %v", err) 69 | return 70 | } 71 | defer client.Disconnect() 72 | 73 | // Set up the poll-rate for the PLC data read. You can hard code this as shown or make it a parameter or something. 74 | pollrate := time.NewTicker(time.Second * 10) 75 | 76 | // loop forever until there is a problem. 77 | for { 78 | // set up the data as needed. 79 | m := PLCPollData{} 80 | 81 | // read the data 82 | err = client.ReadMulti(&m) 83 | if err != nil { 84 | log.Printf("error getting polled data: %v", err) 85 | return 86 | } 87 | 88 | // send it back to the main program. One change that is sometimes helpful is to only send the data on the channel 89 | // if it has changed since the last poll. (You'd have to do the watchdog in the main thread differently). 90 | c <- m 91 | 92 | // wait to do the read again until the next poll time 93 | <-pollrate.C 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /examples/ReadAllTags/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for listing all tags in a controller and reading their values. 10 | // This example shows how to: 11 | // - Connect to an Allen Bradley PLC 12 | // - List all available tags in the controller 13 | // - Read specific tags by name 14 | // - Read all tags in a loop 15 | func main() { 16 | var err error 17 | 18 | // setup the client with the PLC's IP address 19 | client := gologix.NewClient("192.168.2.241") 20 | 21 | // for example, to have a controller on slot 1 instead of 0 you could do this 22 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 23 | // or this 24 | // client.Path, err = gologix.ParsePath("1,1") 25 | client.KeepAliveAutoStart = false 26 | 27 | // connect using parameters in the client struct 28 | err = client.Connect() 29 | if err != nil { 30 | log.Printf("Error opening client. %v", err) 31 | return 32 | } 33 | // setup a deferred disconnect. 34 | defer client.Disconnect() 35 | 36 | // update the client's list of tags. 37 | err = client.ListAllTags(0) 38 | if err != nil { 39 | log.Printf("Error getting tag list. %v", err) 40 | return 41 | } 42 | 43 | // Reading specific tags as examples 44 | var y int32 45 | err = client.Read("Program:gologix_tests.ReadDint", &y) 46 | if err != nil { 47 | log.Printf("Error reading tag. %v", err) 48 | return 49 | } 50 | var x float32 51 | err = client.Read("Program:gologix_tests.ReadReal", &x) 52 | if err != nil { 53 | log.Printf("Error reading tag. %v", err) 54 | return 55 | } 56 | 57 | log.Printf("Found %d tags.", len(client.KnownTags)) 58 | // loop through the tag list and read all tags 59 | for tagname := range client.KnownTags { 60 | tag := client.KnownTags[tagname] 61 | log.Printf("%s: %v", tag.Name, tag.Info.Type) 62 | 63 | // Handle array tags by accessing first element 64 | qty := uint16(1) 65 | if tag.Info.Dimension1 != 0 { 66 | tagname = tagname + "[0]" 67 | x := tag.Info.Atomic() 68 | qty = uint16(tag.Info.Dimension1) 69 | _ = x 70 | } 71 | if tag.UDT == nil && !tag.Info.Atomic() { 72 | //log.Print("Not Atomic or UDT") 73 | continue 74 | } 75 | if tag.UDT != nil { 76 | log.Printf("%s size = %d", tag.Name, tag.UDT.Size()) 77 | } 78 | 79 | // Read and display the value of each tag 80 | val, err := client.Read_single(tagname, tag.Info.Type, qty) 81 | if err != nil { 82 | log.Printf("Error! Problem reading tag %s. %v", tagname, err) 83 | continue 84 | } 85 | log.Printf(" = %v", val) 86 | } 87 | 88 | log.Printf("Found %d tags.", len(client.KnownTags)) 89 | } 90 | -------------------------------------------------------------------------------- /examples/ReadPowerflexParam/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/danomagnum/gologix" 8 | ) 9 | 10 | func main() { 11 | // PowerFlex IP Address - replace with your drive's IP address 12 | ipAddress := "192.168.2.246" 13 | 14 | // Create a new client with the PowerFlex IP address 15 | // The PowerFlex drive typically uses port 44818 (default EIP port) 16 | c := gologix.NewClient(ipAddress) 17 | err := c.Connect() 18 | if err != nil { 19 | log.Fatalf("Failed to connect: %v", err) 20 | } 21 | 22 | defer c.Disconnect() 23 | 24 | ParamNo := 44 // param 44 = maximum hertz * 100 25 | 26 | path, err := gologix.Serialize( 27 | gologix.CipObject_Parameter, 28 | gologix.CIPInstance(ParamNo), 29 | gologix.CIPAttribute(1), 30 | ) 31 | if err != nil { 32 | log.Printf("could not serialize path: %v", err) 33 | return 34 | } 35 | 36 | result, err := c.GenericCIPMessage(gologix.CIPService_GetAttributeSingle, path.Bytes(), []byte{}) 37 | 38 | if err != nil { 39 | log.Fatalf("Failed to read parameter: %v", err) 40 | } 41 | 42 | // Parse the response 43 | val, err := result.Int16() 44 | if err != nil { 45 | log.Fatalf("Failed to parse response: %v", err) 46 | } 47 | maxhz := float64(val) / 100 48 | fmt.Printf("Parameter %d: %3.2f\n", ParamNo, maxhz) 49 | 50 | // you can also read multiple parameters at once using the GetAttributeList service 51 | path, err = gologix.Serialize(gologix.CipObject_DPIParams, gologix.CIPInstance(0)) 52 | if err != nil { 53 | log.Printf("could not serialize path: %v", err) 54 | return 55 | } 56 | 57 | // a scattered read is used to read multiple parameters at once and takes a list of parameter IDs as 32 bit ints 58 | // Actually, the param IDs are 16 bit ints followed by a 16 bit WRITE value. But since we're reading we can ignore the WRITE value and 59 | // use int32 to pad that area with zeroes. If you swap to a ScatteredWrite you'd have to change this accordingly. 60 | params, err := gologix.Serialize([]int32{ 61 | 1, // output freq 62 | 2, // command freq 63 | 3, // output current 64 | 4, // output voltage 65 | 7, // fault code 66 | 27, // drive temp 67 | }) 68 | 69 | if err != nil { 70 | log.Printf("could not serialize params: %v", err) 71 | return 72 | } 73 | 74 | r, err := c.GenericCIPMessage(gologix.CIPService_ScatteredRead, path.Bytes(), params.Bytes()) 75 | 76 | if err != nil { 77 | log.Fatalf("Failed to read parameters: %v", err) 78 | } 79 | 80 | // the powerflex 525 uses 16 bit integers for all of its parameters 81 | // so we can use a struct to parse the response. 82 | // the response will contain the parameter ID followed by the value 83 | // for each parameter requested. We don't need the parameter ID so we use _ to skip it. 84 | var resultData = struct { 85 | _ int16 86 | OutputFreq int16 87 | _ int16 88 | CommandFreq int16 89 | _ int16 90 | OutputCurrent int16 91 | _ int16 92 | OutputVoltage int16 93 | _ int16 94 | FaultCode int16 95 | _ int16 96 | DriveTemp int16 97 | }{} 98 | 99 | err = r.DeSerialize(&resultData) 100 | if err != nil { 101 | log.Fatalf("Failed to parse response: %v", err) 102 | } 103 | 104 | log.Printf("Drive Status: %+v", resultData) 105 | 106 | } 107 | -------------------------------------------------------------------------------- /examples/ReadTagsFromL5X/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/danomagnum/gologix/l5x" 10 | ) 11 | 12 | // This is an example of how to read tags from an L5X file. 13 | // 14 | // the tags will be loaded into a map[string]any where the key is the tag name and the value is the tag value. 15 | // any program tags will be in a nested map[string]any on a key of "program:". 16 | // structures will be in nested map[string]any's. 17 | // arrays will be in slices. 18 | func main() { 19 | var l5xData l5x.RSLogix5000Content 20 | 21 | log.SetOutput(os.Stdout) 22 | 23 | f, err := os.Open("gologix_tests_Program.L5X") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | b, err := io.ReadAll(f) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | err = xml.Unmarshal(b, &l5xData) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | tags, err := l5x.LoadTags(l5xData) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | for k, v := range tags { 42 | log.Printf("%s: %v\n", k, v) 43 | } 44 | 45 | tagComments, err := l5x.LoadTagComments(l5xData) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | for k, v := range tagComments { 51 | log.Printf("%s: %v\n", k, v) 52 | } 53 | 54 | rungComments, err := l5x.LoadRungComments(l5xData) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | for k, v := range rungComments { 59 | log.Printf("%s: %v\n", k, v) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /examples/ReadUnknownTypes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for reading various tags from the controller when you don't know the data type 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | // client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 28 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 29 | // if that happens (about a minute) 30 | defer client.Disconnect() 31 | 32 | // define a variable of type any if you don't know the type of the tag. 33 | var tag1 any 34 | // call the read function. 35 | // note that tag names are case insensitive. 36 | // also note that for you need to use a pointer. 37 | err = client.Read("testint", &tag1) 38 | if err != nil { 39 | log.Printf("error reading testint. %v", err) 40 | } 41 | // do whatever you want with the value. You'll probably have to type assert it to figure out what it is. 42 | switch x := tag1.(type) { 43 | case int16: 44 | log.Printf("tag1 has value %d", x) 45 | default: 46 | log.Printf("tag1 has type %T", x) 47 | } 48 | 49 | // If you try to read a UDT as an unknown type, you'll get a packed byte slice of the data back. 50 | // To read a UDT and get usable data you need to define a struct that matches the UDT definition in the controller. 51 | // See the simple read example for how to do that. 52 | var dat any 53 | err = client.Read("Program:Gologix_Tests.ReadUDT", &dat) 54 | if err != nil { 55 | log.Printf("error reading udt. %v", err) 56 | } 57 | 58 | log.Printf("dat has value %v", dat) 59 | 60 | // to read multiple unknown types at once you can use the ReadMap function 61 | 62 | // define a map of string to any. The keys are the tag names and the values are nil since we don't know the types. 63 | // if you assigned a value to the map, it will be used as the type for the read. 64 | mr := make(map[string]any) 65 | mr["TestInt"] = nil 66 | mr["Program:Gologix_Tests.ReadUDT"] = nil 67 | 68 | err = client.ReadMap(mr) 69 | if err != nil { 70 | log.Printf("error reading map. %v", err) 71 | } 72 | // now you can use the values in the map. The types are determined by the tag type on the PLC. 73 | log.Printf("TestInt has type %T and value %v", mr["TestInt"], mr["TestInt"]) 74 | log.Printf("ReadUDT has type %T and value %v", mr["Program:Gologix_Tests.ReadUDT"], mr["Program:Gologix_Tests.ReadUDT"]) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /examples/Server_Class1/AddModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1/AddModule.png -------------------------------------------------------------------------------- /examples/Server_Class1/CIPModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1/CIPModule.png -------------------------------------------------------------------------------- /examples/Server_Class1/CipModuleConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1/CipModuleConfig.png -------------------------------------------------------------------------------- /examples/Server_Class1/EthernetBridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1/EthernetBridge.png -------------------------------------------------------------------------------- /examples/Server_Class1/EthernetBridgeSetup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1/EthernetBridgeSetup.png -------------------------------------------------------------------------------- /examples/Server_Class1/IO_Tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1/IO_Tree.png -------------------------------------------------------------------------------- /examples/Server_Class1/main.go: -------------------------------------------------------------------------------- 1 | // This example shows how to set up a class 1 IO connection. To support multiple connections you should use the "Ethernet Bridge" module 2 | // in the IO tree. Then you should add one "CIP Mdoule" per virtual IO rack position. 3 | // 4 | // I think multiple readers should work. Multiple writers would also appear to work but they would step on each other. 5 | // 6 | // You should create your own class that fulfills the TagProvider interface with the IORead and IOWrite methods completed where you handle the 7 | // serializing and deserializing of data properly. 8 | // 9 | // I think you should be able to have class 3 tag providers AND class 1 tag providers at the same time for the same path, BUT you'll have to 10 | // combine their logic into a single class since the router will resolve all messages to the same place. For this reason it might be easiest 11 | // to keep class 3 tag providers and class 1 tag providers segregated to different "slots" on the "backplane" 12 | // 13 | // Note that you won't be able to have multiple servers on a single computer. They bind to the EIP ports on TCP and UDP so you'll need 14 | // to multiplex multiple connections through one program. 15 | // 16 | // In the current inarnation of this server it doesn't matter what assembly instance IDs you select in the controller, although you could create your own 17 | // TagProvider that changed behavior based on that. 18 | package main 19 | 20 | import ( 21 | "log" 22 | "os" 23 | "time" 24 | 25 | "github.com/danomagnum/gologix" 26 | ) 27 | 28 | // these types will be the input and output data section for the io connection. 29 | // the input/output nomenclature is from the PLC's point of view - Input goes to the PLC and output 30 | // comes to us. 31 | // 32 | // the size (in bytes) of these structures has to match the size you set up in the IO tree for the IO connection. 33 | // presumably you can also use other formats than bytes for the data type, but the sizes still have to match. 34 | type InStr struct { 35 | Data [9]byte 36 | Count byte 37 | } 38 | type OutStr struct { 39 | Data [10]byte 40 | } 41 | 42 | func main() { 43 | 44 | //////////////////////////////////////////////////// 45 | // First we set up the tag providers. 46 | // 47 | // Each one will have a path and an object that fulfills the gologix.TagProvider interface 48 | // We set those up and then pass them to the Router object. 49 | // here we're using the build in io tag provider which just has 10 bytes of inputs and 10 bytes of outputs 50 | // 51 | //////////////////////////////////////////////////// 52 | 53 | r := gologix.PathRouter{} 54 | 55 | // define the Input and Output instances. (Input and output here is from the plc's perspective) 56 | inInstance := InStr{} 57 | 58 | // an IO handler in slot 2 59 | //p3 := gologix.IOProvider[InStr, OutStr]{} 60 | p3 := gologix.IOChannelProvider[InStr, OutStr]{} 61 | path3, err := gologix.ParsePath("1,2") 62 | if err != nil { 63 | log.Printf("problem parsing path. %v", err) 64 | os.Exit(1) 65 | } 66 | r.Handle(path3.Bytes(), &p3) 67 | 68 | s := gologix.NewServer(&r) 69 | go s.Serve() 70 | 71 | t := time.NewTicker(time.Second) 72 | data_chan := p3.GetOutputDataChannel() 73 | 74 | for { 75 | select { 76 | case <-t.C: 77 | // time to update the input data 78 | inInstance.Count++ 79 | p3.SetInputData(inInstance) 80 | case outdat := <-data_chan: 81 | log.Printf("PLC Output: %v", outdat) 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /examples/Server_Class1_V2/AddModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1_V2/AddModule.png -------------------------------------------------------------------------------- /examples/Server_Class1_V2/CIPModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1_V2/CIPModule.png -------------------------------------------------------------------------------- /examples/Server_Class1_V2/CipModuleConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1_V2/CipModuleConfig.png -------------------------------------------------------------------------------- /examples/Server_Class1_V2/EthernetBridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1_V2/EthernetBridge.png -------------------------------------------------------------------------------- /examples/Server_Class1_V2/EthernetBridgeSetup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1_V2/EthernetBridgeSetup.png -------------------------------------------------------------------------------- /examples/Server_Class1_V2/IO_Tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class1_V2/IO_Tree.png -------------------------------------------------------------------------------- /examples/Server_Class1_V2/main.go: -------------------------------------------------------------------------------- 1 | // This example shows how to set up a class 1 IO connection. To support multiple connections you should use the "Ethernet Bridge" module 2 | // in the IO tree. Then you should add one "CIP Mdoule" per virtual IO rack position. 3 | // 4 | // I think multiple readers should work. Multiple writers would also appear to work but they would step on each other. 5 | // 6 | // You should create your own class that fulfills the TagProvider interface with the IORead and IOWrite methods completed where you handle the 7 | // serializing and deserializing of data properly. 8 | // 9 | // I think you should be able to have class 3 tag providers AND class 1 tag providers at the same time for the same path, BUT you'll have to 10 | // combine their logic into a single class since the router will resolve all messages to the same place. For this reason it might be easiest 11 | // to keep class 3 tag providers and class 1 tag providers segregated to different "slots" on the "backplane" 12 | // 13 | // Note that you won't be able to have multiple servers on a single computer. They bind to the EIP ports on TCP and UDP so you'll need 14 | // to multiplex multiple connections through one program. 15 | // 16 | // In the current inarnation of this server it doesn't matter what assembly instance IDs you select in the controller, although you could create your own 17 | // TagProvider that changed behavior based on that. 18 | package main 19 | 20 | import ( 21 | "log" 22 | "os" 23 | "time" 24 | 25 | "github.com/danomagnum/gologix" 26 | ) 27 | 28 | // these types will be the input and output data section for the io connection. 29 | // the input/output nomenclature is from the PLC's point of view - Input goes to the PLC and output 30 | // comes to us. 31 | // 32 | // the size (in bytes) of these structures has to match the size you set up in the IO tree for the IO connection. 33 | // presumably you can also use other formats than bytes for the data type, but the sizes still have to match. 34 | type InStr struct { 35 | Data [9]byte 36 | Count byte 37 | } 38 | type OutStr struct { 39 | Data [10]byte 40 | } 41 | 42 | func main() { 43 | 44 | //////////////////////////////////////////////////// 45 | // First we set up the tag providers. 46 | // 47 | // Each one will have a path and an object that fulfills the gologix.TagProvider interface 48 | // We set those up and then pass them to the Router object. 49 | // here we're using the build in io tag provider which just has 10 bytes of inputs and 10 bytes of outputs 50 | // 51 | //////////////////////////////////////////////////// 52 | 53 | r := gologix.PathRouter{} 54 | 55 | // define the Input and Output instances. (Input and output here is from the plc's perspective) 56 | inInstance := InStr{} 57 | outInstance := OutStr{} 58 | 59 | // an IO handler in slot 2 60 | //p3 := gologix.IOProvider[InStr, OutStr]{} 61 | p3 := gologix.IOProvider[InStr, OutStr]{ 62 | In: &inInstance, 63 | Out: &outInstance, 64 | } 65 | path3, err := gologix.ParsePath("1,0") 66 | if err != nil { 67 | log.Printf("problem parsing path. %v", err) 68 | os.Exit(1) 69 | } 70 | 71 | path_bytes := path3.Bytes() 72 | 73 | // if you want to use a "generic ethernet module" instead of a "generic ethernet bridge" you can use this path 74 | // I'm not sure if this is always the case or if it's just the way I have it set up. Either way if you set up the gologix server 75 | // in your hardware tree and get an error of the form "no tag provider for path [52 4]" but with different numbers, let me know 76 | // and try those instead. 77 | //path_bytes := []byte{52, 4} 78 | 79 | r.Handle(path_bytes, &p3) 80 | 81 | s := gologix.NewServer(&r) 82 | go s.Serve() 83 | 84 | t := time.NewTicker(time.Second) 85 | 86 | for { 87 | <-t.C 88 | inInstance.Count++ 89 | p3.InMutex.Lock() 90 | log.Printf("PLC Input: %v", inInstance) 91 | p3.InMutex.Unlock() 92 | p3.OutMutex.Lock() 93 | log.Printf("PLC Output: %v", outInstance) 94 | p3.OutMutex.Unlock() 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /examples/Server_Class3/Read1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class3/Read1.png -------------------------------------------------------------------------------- /examples/Server_Class3/Read2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class3/Read2.png -------------------------------------------------------------------------------- /examples/Server_Class3/Write1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class3/Write1.png -------------------------------------------------------------------------------- /examples/Server_Class3/Write2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danomagnum/gologix/f43b3c1387aba59befcdfce6019ee09594394d95/examples/Server_Class3/Write2.png -------------------------------------------------------------------------------- /examples/Server_Class3/main.go: -------------------------------------------------------------------------------- 1 | // This example shows how to create a server that can handle incoming "class 3" cip messages. 2 | // These are messages generated in a program from a msg instruction. 3 | // From the PLCs perspective we are just another PLC. When it does a write request to a specific tag we accept that data 4 | // and when it does a read we send it the data associated with that tag on our end. 5 | // 6 | // Because all cip messages come in on the same port, we have to support various independent messages coming in from multiple controllers. 7 | // otherwise you'd never be able to communicate with more than one controller per computer. As you'll see below this is actually 8 | // fairly simple and takes advantage of the CIP routing path in a msg instruction. All messages come to our IP address and then we split them 9 | // up based on the cip path after that. The example uses 1, 0 and 1,1 which equates to backplane slot 0 and backplane slot 1 respectively. 10 | // 11 | // If you look at the screenshots of MSG insructions in this folder you'll see how the read and write are setup. Note that on the 12 | // connection tab of the msg setup the path is "gologix, 1, 0" this is because there is a generic ethernet module in the IO config 13 | // with the same address as the computer used for the screenshots. You can just type the IP address in here instead of "gologix". 14 | // 15 | // Note that you won't be able to have multiple servers on a single computer. They bind to the EIP ports on TCP and UDP so you'll need 16 | // to multiplex multiple connections through one program. 17 | package main 18 | 19 | import ( 20 | "log" 21 | "os" 22 | "time" 23 | 24 | "github.com/danomagnum/gologix" 25 | ) 26 | 27 | func main() { 28 | 29 | //////////////////////////////////////////////////// 30 | // First we set up the tag providers. 31 | // 32 | // Each one will have a path and an object that fulfills the gologix.TagProvider interface 33 | // We set those up and then pass them to the Router object. 34 | // here we're using the build in map tag provider which just maps cip reads and writes to/from a go map 35 | // 36 | // In theory, it should be easy to make a tag provider that works with a sql database or pumps messages onto a channel 37 | // or whatever else you might need. 38 | //////////////////////////////////////////////////// 39 | 40 | r := gologix.PathRouter{} 41 | 42 | // one memory based tag provider at slot 0 on the virtual "backplane" 43 | // a message to 2,xxx.xxx.xxx.xxx,1,0 (compact logix) will get this provider (where xxx.xxx.xxx.xxx is the ip address) 44 | // the message path before the IP address in the msg instruction will be different based on the actual controller 45 | // you're using, but the part after the IP address is what this matches 46 | p1 := gologix.MapTagProvider{} 47 | path1, err := gologix.ParsePath("1,0") 48 | if err != nil { 49 | log.Printf("problem parsing path. %v", err) 50 | os.Exit(1) 51 | } 52 | r.Handle(path1.Bytes(), &p1) 53 | 54 | // set up some default tags. 55 | // using TagWrite() and TagRead() are treadsafe if needed. 56 | // otherwise you can lock p1.Mutex and manipulate p1.Data yourself 57 | p1.TagWrite("testtag1", int32(12345)) 58 | p1.TagWrite("testtag2", float32(543.21)) 59 | p1.TagWrite("testtag3", []int32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 60 | p1.TagWrite("testdint", int32(12)) 61 | p1.TagWrite("testint", int16(3)) 62 | p1.TagWrite("teststring", "Hello World") 63 | 64 | // a different memory based tag provider at slot 1 on the virtual "backplane" this would be "2,xxx.xxx.xxx.xxx,1,1" in the msg connection path 65 | p2 := gologix.MapTagProvider{} 66 | path2, err := gologix.ParsePath("1,1") 67 | if err != nil { 68 | log.Printf("problem parsing path. %v", err) 69 | os.Exit(1) 70 | } 71 | r.Handle(path2.Bytes(), &p2) 72 | 73 | s := gologix.NewServer(&r) 74 | go s.Serve() 75 | 76 | t := time.NewTicker(time.Second * 5) 77 | for { 78 | <-t.C 79 | p1.Mutex.Lock() 80 | log.Printf("Data 1: %v", p1.Data) 81 | p1.Mutex.Unlock() 82 | 83 | p2.Mutex.Lock() 84 | log.Printf("Data 2: %v", p2.Data) 85 | p2.Mutex.Unlock() 86 | 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /examples/Server_ProducedTag/main.go: -------------------------------------------------------------------------------- 1 | // This example shows how to set up a class 1 IO connection. To support multiple connections you should use the "Ethernet Bridge" module 2 | // in the IO tree. Then you should add one "CIP Mdoule" per virtual IO rack position. 3 | // 4 | // I think multiple readers should work. Multiple writers would also appear to work but they would step on each other. 5 | // 6 | // You should create your own class that fulfills the TagProvider interface with the IORead and IOWrite methods completed where you handle the 7 | // serializing and deserializing of data properly. 8 | // 9 | // I think you should be able to have class 3 tag providers AND class 1 tag providers at the same time for the same path, BUT you'll have to 10 | // combine their logic into a single class since the router will resolve all messages to the same place. For this reason it might be easiest 11 | // to keep class 3 tag providers and class 1 tag providers segregated to different "slots" on the "backplane" 12 | // 13 | // Note that you won't be able to have multiple servers on a single computer. They bind to the EIP ports on TCP and UDP so you'll need 14 | // to multiplex multiple connections through one program. 15 | // 16 | // In the current inarnation of this server it doesn't matter what assembly instance IDs you select in the controller, although you could create your own 17 | // TagProvider that changed behavior based on that. 18 | package main 19 | 20 | import ( 21 | "log" 22 | "os" 23 | "time" 24 | 25 | "github.com/danomagnum/gologix" 26 | ) 27 | 28 | // these types will be the input and output data section for the io connection. 29 | // the input/output nomenclature is from the PLC's point of view - Input goes to the PLC and output 30 | // comes to us. 31 | // 32 | // the size (in bytes) of these structures has to match the size you set up in the IO tree for the IO connection. 33 | // presumably you can also use other formats than bytes for the data type, but the sizes still have to match. 34 | type InStr struct { 35 | Data [9]byte 36 | Count byte 37 | } 38 | type OutStr struct { 39 | Data [10]byte 40 | } 41 | 42 | func main() { 43 | 44 | //////////////////////////////////////////////////// 45 | // First we set up the tag providers. 46 | // 47 | // Each one will have a path and an object that fulfills the gologix.TagProvider interface 48 | // We set those up and then pass them to the Router object. 49 | // here we're using the build in io tag provider which just has 10 bytes of inputs and 10 bytes of outputs 50 | // 51 | //////////////////////////////////////////////////// 52 | 53 | r := gologix.PathRouter{} 54 | 55 | // define the Input and Output instances. (Input and output here is from the plc's perspective) 56 | inInstance := InStr{} 57 | outInstance := OutStr{} 58 | 59 | p3 := gologix.IOProvider[InStr, OutStr]{ 60 | In: &inInstance, 61 | Out: &outInstance, 62 | } 63 | 64 | path3, err := gologix.ParsePath("1,0") 65 | if err != nil { 66 | log.Printf("problem parsing path. %v", err) 67 | os.Exit(1) 68 | } 69 | 70 | path_bytes := path3.Bytes() 71 | 72 | // if you want to use a "generic ethernet module" instead of a "generic ethernet bridge" you can use this path 73 | // I'm not sure if this is always the case or if it's just the way I have it set up. Either way if you set up the gologix server 74 | // in your hardware tree and get an error of the form "no tag provider for path [52 4]" but with different numbers, let me know 75 | // and try those instead. 76 | //path_bytes := []byte{52, 4} 77 | 78 | r.Handle(path_bytes, &p3) 79 | 80 | s := gologix.NewServer(&r) 81 | go s.Serve() 82 | 83 | t := time.NewTicker(time.Second) 84 | 85 | for { 86 | <-t.C 87 | inInstance.Count++ 88 | p3.InMutex.Lock() 89 | log.Printf("PLC Input: %v", inInstance) 90 | p3.InMutex.Unlock() 91 | p3.OutMutex.Lock() 92 | log.Printf("PLC Output: %v", outInstance) 93 | p3.OutMutex.Unlock() 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /examples/SimpleRead/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for readng an INT tag named "TestInt" in the controller. 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | // client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 28 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 29 | // if that happens (about a minute) 30 | defer client.Disconnect() 31 | 32 | // define a variable with a type that matches the tag you want to read. In this case it is an INT so 33 | // int16 or uint16 will work. 34 | var tag1 int16 35 | // call the read function. 36 | // note that tag names are case insensitive. 37 | // also note that for atomic types and structs you need to use a pointer. 38 | // for slices you don't use a pointer. 39 | err = client.Read("testint", &tag1) 40 | if err != nil { 41 | log.Printf("error reading testint. %v", err) 42 | } 43 | // do whatever you want with the value 44 | log.Printf("tag1 has value %d", tag1) 45 | 46 | var dat struct { 47 | Field1 int32 48 | Field2 float32 49 | } 50 | err = client.Read("Program:Gologix_Tests.ReadUDT", &dat) 51 | if err != nil { 52 | log.Printf("error reading testint. %v", err) 53 | } 54 | log.Printf("dat has value %+v", dat) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /examples/UDTReadWrite/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for readng a UDT tag named "TestUDT" in the controller. 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | // client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a deffered disconnect. If you don't disconnect you might have trouble reconnecting because 28 | // you won't have sent the close forward open. You'll have to wait for the CIP connection to time out 29 | // if that happens (about a minute) 30 | defer client.Disconnect() 31 | 32 | // define a struct that matches the tag you want to read. 33 | // the struct must have the same fields and types as the UDT in the controller. 34 | // the name of the struct MUST match the name of the UDT in the controller. 35 | // the name of any sub-structures must also match the name of the UDT in the controller. 36 | // field names don't matter and don't have to match. They do have to be exported though so the 37 | // reflect package can access them to set the values. 38 | type myUDT struct { 39 | Field1 int32 40 | Field2 float32 41 | } 42 | var tag1 myUDT 43 | // call the read function. 44 | // note that tag names are case insensitive. 45 | // also note that you need to use a pointer. 46 | err = client.Read("TestUDT", &tag1) 47 | if err != nil { 48 | log.Printf("error reading testudt. %v", err) 49 | } 50 | // do whatever you want with the value 51 | log.Printf("tag1 has value %+v", tag1) 52 | 53 | // 54 | // Writing example. 55 | // 56 | tag1.Field1 = 5 57 | tag1.Field2 = 12.4 58 | 59 | err = client.Write("TestUDT", tag1) 60 | if err != nil { 61 | log.Printf("error writing testudt. %v", err) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /examples/Write/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // Demo program for writing an INT value to a tag named "WriteUDTs[5].Field1" in the controller. 10 | func main() { 11 | var err error 12 | 13 | // setup the client. If you need a different path you'll have to set that. 14 | client := gologix.NewClient("192.168.2.241") 15 | 16 | // for example, to have a controller on slot 1 instead of 0 you could do this 17 | //client.Path, err = gologix.Serialize(gologix.CIPPort{PortNo: 1}, gologix.CIPAddress(1)) 18 | // or this 19 | // client.Path, err = gologix.ParsePath("1,1") 20 | 21 | // connect using parameters in the client struct 22 | err = client.Connect() 23 | if err != nil { 24 | log.Printf("Error opening client. %v", err) 25 | return 26 | } 27 | // setup a disconnect. If you don't disconnect you might have trouble reconnecting 28 | defer client.Disconnect() 29 | 30 | // the variable you want to write needs to be the proper type for the controller tag. See GoVarToLogixType() for info 31 | 32 | mydint := int32(12345) 33 | 34 | err = client.Write("WriteUDTs[5].Field1", mydint) 35 | if err != nil { 36 | log.Printf("error writing to tag 'WriteUDTs[5].Field1'. %v", err) 37 | } 38 | 39 | // no error = write OK. 40 | 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/danomagnum/gologix 2 | 3 | go 1.21 4 | 5 | require github.com/npat-efault/crc16 v0.0.0-20161013170008-4128ccbe47c3 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/npat-efault/crc16 v0.0.0-20161013170008-4128ccbe47c3 h1:LreEMrgwmSTNPbtao3jPZjwrjRYrlYTDg0kTMPOgSHg= 2 | github.com/npat-efault/crc16 v0.0.0-20161013170008-4128ccbe47c3/go.mod h1:1E9pLoYv14Va+AZbH8ywpTseVh5R4rwkRla445GfE1U= 3 | -------------------------------------------------------------------------------- /ioi_test.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // these tests came from the tag names in 1756-PM020H-EN-P 9 | // it only tests the request path portion of each tag addressing example 10 | // it also only tests symbolic paths. 11 | func TestIOI(t *testing.T) { 12 | var tests = []struct { 13 | path string 14 | t CIPType 15 | want []byte 16 | }{ 17 | { 18 | "profile[0,1,257]", 19 | CIPTypeDINT, 20 | []byte{ 21 | 0x91, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x00, // symbolic segment for "profile" 22 | 0x28, 0x00, // member segment for 0 23 | 0x28, 0x01, // member segment for 1 24 | 0x29, 0x00, 0x01, 0x01, // member segment for 257 25 | }, 26 | }, 27 | { 28 | "profile[1,2,258]", 29 | CIPTypeDINT, 30 | []byte{ 31 | 0x91, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x00, // symbolic segment for "profile" 32 | 0x28, 0x01, // member segment for 1 33 | 0x28, 0x02, // member segment for 2 34 | 0x29, 0x00, 0x02, 0x01, // member segment for 258 35 | }, 36 | }, 37 | { 38 | "profile[300,2,258]", 39 | CIPTypeDINT, 40 | []byte{ 41 | 0x91, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x00, // symbolic segment for "profile" 42 | 0x29, 0x00, 0x2c, 0x01, // member segment for 300 43 | 0x28, 0x02, // member segment for 2 44 | 0x29, 0x00, 0x02, 0x01, // member segment for 258 45 | }, 46 | }, 47 | { 48 | "dwell3.acc", 49 | CIPTypeDINT, 50 | []byte{ 51 | 0x91, 0x06, 0x64, 0x77, 0x65, 0x6C, 0x6C, 0x33, // symbolic segment for "dwell3" 52 | 0x91, 0x03, 0x61, 0x63, 0x63, 0x00, // member segment for ACC 53 | }, 54 | }, 55 | { 56 | "struct3.today.rate", 57 | CIPTypeStruct, 58 | []byte{ 59 | 0x91, 0x07, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x33, 0x00, // symbolic segment for "struct3" 60 | 0x91, 0x05, 0x74, 0x6F, 0x64, 0x61, 0x79, 0x00, // symbolic segment for today 61 | 0x91, 0x04, 0x72, 0x61, 0x74, 0x65, // symbolic segment for rate 62 | }, 63 | }, 64 | { 65 | "my2dstruct4[1].today.hourlycount[3]", 66 | CIPTypeINT, 67 | []byte{ 68 | 0x91, 0x0B, 0x6d, 0x79, 0x32, 0x64, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x34, 0x00, // symbolic segment for my2dstruct4 69 | 0x28, 0x01, // index 1 70 | 0x91, 0x05, 0x74, 0x6F, 0x64, 0x61, 0x79, 0x00, //today 71 | 0x91, 0x0B, 0x68, 0x6F, 0x75, 0x72, 0x6C, 0x79, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x00, // hourlycount 72 | 0x28, 0x03, // index 3 73 | }, 74 | }, 75 | { 76 | "My2DstRucT4[1].ToDaY.hoURLycOuNt[3]", 77 | CIPTypeINT, 78 | []byte{ 79 | 0x91, 0x0B, 0x6d, 0x79, 0x32, 0x64, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x34, 0x00, // symbolic segment for my2dstruct4 80 | 0x28, 0x01, // index 1 81 | 0x91, 0x05, 0x74, 0x6F, 0x64, 0x61, 0x79, 0x00, //today 82 | 0x91, 0x0B, 0x68, 0x6F, 0x75, 0x72, 0x6C, 0x79, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x00, // hourlycount 83 | 0x28, 0x03, // index 3 84 | }, 85 | }, 86 | } 87 | client := Client{} 88 | 89 | for _, tt := range tests { 90 | 91 | testname := fmt.Sprintf("tag: %s", tt.path) 92 | t.Run(testname, func(t *testing.T) { 93 | res, err := client.newIOI(tt.path, tt.t) 94 | if err != nil { 95 | t.Errorf("IOI Generation error. %v", err) 96 | } 97 | if !check_bytes(res.Buffer, tt.want) { 98 | t.Errorf("Wrong Value for result. \nWanted %v. \nGot %v", to_hex(tt.want), to_hex(res.Buffer)) 99 | } 100 | }) 101 | } 102 | 103 | } 104 | 105 | func to_hex(b []byte) []string { 106 | out := make([]string, len(b)) 107 | 108 | for i, v := range b { 109 | out[i] = fmt.Sprintf("% X", v) 110 | } 111 | return out 112 | 113 | } 114 | 115 | func TestIOIToBytesAndBackAgain(t *testing.T) { 116 | tests := []struct { 117 | Tag string 118 | Type CIPType 119 | }{ 120 | {"test", CIPTypeDINT}, 121 | {"test[2]", CIPTypeDINT}, 122 | {"test[2,3]", CIPTypeDINT}, 123 | {"test[3000,3]", CIPTypeDINT}, 124 | {"test.tester", CIPTypeDINT}, 125 | {"test[2,3].tester", CIPTypeDINT}, 126 | } 127 | client := Client{} 128 | 129 | for _, tt := range tests { 130 | 131 | testname := fmt.Sprintf("tag: %s", tt.Tag) 132 | t.Run(testname, func(t *testing.T) { 133 | res, err := client.newIOI(tt.Tag, tt.Type) 134 | if err != nil { 135 | t.Errorf("IOI Generation error. %v", err) 136 | } 137 | item := newItem(cipItem_Null, res) 138 | path, err := getTagFromPath(&item) 139 | if err != nil { 140 | t.Errorf("problem parsing path from byte item") 141 | } 142 | if path != tt.Tag { 143 | t.Errorf("Wrong Value for result. \nWanted %v. \nGot %v", tt.Tag, path) 144 | } 145 | }) 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /l5x/types.go: -------------------------------------------------------------------------------- 1 | package l5x 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func L5xTypeToGoType(typestr string, valuestr string) (any, error) { 9 | switch typestr { 10 | case "REAL": 11 | value, _ := strconv.ParseFloat(valuestr, 64) 12 | return float64(value), nil 13 | case "DINT": 14 | value, _ := strconv.ParseInt(valuestr, 10, 32) 15 | return int32(value), nil 16 | case "BOOL", "BIT": 17 | value, _ := strconv.ParseBool(valuestr) 18 | return bool(value), nil 19 | case "INT": 20 | value, _ := strconv.ParseInt(valuestr, 10, 16) 21 | return int16(value), nil 22 | case "STRING": 23 | return valuestr, nil 24 | case "SINT": 25 | value, _ := strconv.ParseInt(valuestr, 10, 8) 26 | return int8(value), nil 27 | case "LINT": 28 | value, _ := strconv.ParseInt(valuestr, 10, 64) 29 | return int64(value), nil 30 | case "BYTE": 31 | value, _ := strconv.ParseInt(valuestr, 10, 8) 32 | return uint8(value), nil 33 | case "WORD": 34 | value, _ := strconv.ParseInt(valuestr, 10, 16) 35 | return uint16(value), nil 36 | case "DWORD": 37 | value, _ := strconv.ParseInt(valuestr, 10, 32) 38 | return uint32(value), nil 39 | case "LWORD": 40 | value, _ := strconv.ParseInt(valuestr, 10, 64) 41 | return uint64(value), nil 42 | 43 | default: 44 | return nil, fmt.Errorf("unknown type %s", typestr) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lgxtypes/control.go: -------------------------------------------------------------------------------- 1 | package lgxtypes 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | type CONTROL struct { 9 | LEN int32 10 | POS int32 11 | EN bool // bit 31 12 | EU bool // bit 30 13 | DN bool // bit 29 14 | EM bool // bit 28 15 | ER bool // bit 27 16 | UL bool // bit 26 17 | IN bool // bit 25 18 | FD bool // bit 24 19 | } 20 | 21 | func (t CONTROL) Pack(w io.Writer) (int, error) { 22 | 23 | var CtrlWord uint32 24 | if t.EN { 25 | CtrlWord |= 1 << 31 26 | } 27 | if t.EU { 28 | CtrlWord |= 1 << 30 29 | } 30 | if t.DN { 31 | CtrlWord |= 1 << 29 32 | } 33 | if t.EM { 34 | CtrlWord |= 1 << 28 35 | } 36 | if t.ER { 37 | CtrlWord |= 1 << 27 38 | } 39 | if t.UL { 40 | CtrlWord |= 1 << 26 41 | } 42 | if t.IN { 43 | CtrlWord |= 1 << 25 44 | } 45 | if t.FD { 46 | CtrlWord |= 1 << 24 47 | } 48 | 49 | err := binary.Write(w, binary.LittleEndian, CtrlWord) 50 | if err != nil { 51 | return 0, err 52 | } 53 | 54 | err = binary.Write(w, binary.LittleEndian, t.LEN) 55 | if err != nil { 56 | return 4, err 57 | } 58 | err = binary.Write(w, binary.LittleEndian, t.POS) 59 | if err != nil { 60 | return 8, err 61 | } 62 | 63 | return 12, nil 64 | } 65 | 66 | func (t *CONTROL) Unpack(r io.Reader) (int, error) { 67 | var CtrlWord uint32 68 | err := binary.Read(r, binary.LittleEndian, &CtrlWord) 69 | if err != nil { 70 | return 0, err 71 | } 72 | 73 | t.EN = CtrlWord&(1<<31) != 0 74 | t.EU = CtrlWord&(1<<30) != 0 75 | t.DN = CtrlWord&(1<<29) != 0 76 | t.EM = CtrlWord&(1<<28) != 0 77 | t.ER = CtrlWord&(1<<27) != 0 78 | t.UL = CtrlWord&(1<<26) != 0 79 | t.IN = CtrlWord&(1<<25) != 0 80 | t.FD = CtrlWord&(1<<24) != 0 81 | 82 | err = binary.Read(r, binary.LittleEndian, &(t.LEN)) 83 | if err != nil { 84 | return 4, err 85 | } 86 | 87 | err = binary.Read(r, binary.LittleEndian, &(t.POS)) 88 | if err != nil { 89 | return 8, err 90 | } 91 | 92 | return 12, nil 93 | } 94 | 95 | func (CONTROL) TypeAbbr() (string, uint16) { 96 | return "CONTROL,DINT,DINT,DINT", 0x0F81 97 | } 98 | -------------------------------------------------------------------------------- /lgxtypes/counter.go: -------------------------------------------------------------------------------- 1 | package lgxtypes 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | type COUNTER struct { 9 | PRE int32 10 | ACC int32 11 | CU bool // bit 31 12 | CD bool // bit 30 13 | DN bool // bit 29 14 | OV bool // bit 28 - set if we wrap over 2,147,483,648 to -2,147,483,648 15 | UN bool // bit 27 - set if we wrap over -2,147,483,648 to 2,147,483,648 16 | } 17 | 18 | func (t COUNTER) Pack(w io.Writer) (int, error) { 19 | 20 | var CtrlWord uint32 21 | if t.CU { 22 | CtrlWord |= 1 << 31 23 | } 24 | if t.CD { 25 | CtrlWord |= 1 << 30 26 | } 27 | if t.DN { 28 | CtrlWord |= 1 << 29 29 | } 30 | if t.OV { 31 | CtrlWord |= 1 << 28 32 | } 33 | if t.UN { 34 | CtrlWord |= 1 << 27 35 | } 36 | 37 | err := binary.Write(w, binary.LittleEndian, CtrlWord) 38 | if err != nil { 39 | return 0, err 40 | } 41 | 42 | err = binary.Write(w, binary.LittleEndian, t.PRE) 43 | if err != nil { 44 | return 4, err 45 | } 46 | err = binary.Write(w, binary.LittleEndian, t.ACC) 47 | if err != nil { 48 | return 8, err 49 | } 50 | 51 | return 12, nil 52 | } 53 | 54 | func (t *COUNTER) Unpack(r io.Reader) (int, error) { 55 | var CtrlWord uint32 56 | err := binary.Read(r, binary.LittleEndian, &CtrlWord) 57 | if err != nil { 58 | return 0, err 59 | } 60 | 61 | t.CU = CtrlWord&(1<<31) != 0 62 | t.CD = CtrlWord&(1<<30) != 0 63 | t.DN = CtrlWord&(1<<29) != 0 64 | t.OV = CtrlWord&(1<<28) != 0 65 | t.UN = CtrlWord&(1<<27) != 0 66 | 67 | err = binary.Read(r, binary.LittleEndian, &(t.PRE)) 68 | if err != nil { 69 | return 4, err 70 | } 71 | 72 | err = binary.Read(r, binary.LittleEndian, &(t.ACC)) 73 | if err != nil { 74 | return 8, err 75 | } 76 | 77 | return 12, nil 78 | } 79 | 80 | func (COUNTER) TypeAbbr() (string, uint16) { 81 | return "COUNTER,DINT,DINT,DINT", 0x0F82 82 | } 83 | -------------------------------------------------------------------------------- /lgxtypes/readme.md: -------------------------------------------------------------------------------- 1 | Many of the pre-defined types in a logix controller don't play by the same rules as UDTs or the basic atomic types. This packge attempts to define as much about these pre defined types as is known to make them easier to work with. Unfortunately I don't have enough detailed information about how they are done to implement using them inside UDTs. If anyone has knowledge of how the abbreviated type code crc is calculated for a structure with a pre defined type inside it, please let me know. 2 | 3 | 4 | 5 | I've found out that all builtin types have template instance numbers between 0x0F00 and 0xFFF. Instead of having a "real" CRC calculated for them, they use the instance number. The CRC string being used for the types is still based off the actual structures, but it also includes the hidden items of the templates. -------------------------------------------------------------------------------- /lgxtypes/string.go: -------------------------------------------------------------------------------- 1 | package lgxtypes 2 | 3 | type STRING struct { 4 | Len int32 5 | Data [82]int8 6 | } 7 | 8 | func (STRING) TypeAbbr() (string, uint16) { 9 | return "STRING,DINT,SINT[82]", 0x0FCE 10 | } 11 | -------------------------------------------------------------------------------- /lgxtypes/timer.go: -------------------------------------------------------------------------------- 1 | package lgxtypes 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | type TIMER struct { 9 | PRE int32 10 | ACC int32 11 | // the control bits are actually in a DINT called "Control" and they 12 | // don't end up in the TypeAbbr string. 13 | EN bool // bit 31 14 | TT bool // bit 30 15 | DN bool // bit 29 16 | 17 | // These bits were added in anticipation of using timers with SFCs (Sequential Function Charts). 18 | // However, at this time SFCs do not use timer structures, so these bits are not used and are currently undefined. 19 | // -- I don't remember where I saw the above quote. 20 | FS bool // bit 28 Unused 21 | LS bool // bit 27 Unused 22 | OV bool // bit 26 Unused 23 | ER bool // bit 25 Unused 24 | } 25 | 26 | func (t TIMER) Pack(w io.Writer) (int, error) { 27 | 28 | var CtrlWord uint32 29 | if t.EN { 30 | CtrlWord |= 1 << 31 31 | } 32 | if t.TT { 33 | CtrlWord |= 1 << 30 34 | } 35 | if t.DN { 36 | CtrlWord |= 1 << 29 37 | } 38 | 39 | err := binary.Write(w, binary.LittleEndian, CtrlWord) 40 | if err != nil { 41 | return 0, err 42 | } 43 | 44 | err = binary.Write(w, binary.LittleEndian, t.PRE) 45 | if err != nil { 46 | return 4, err 47 | } 48 | err = binary.Write(w, binary.LittleEndian, t.ACC) 49 | if err != nil { 50 | return 8, err 51 | } 52 | 53 | return 12, nil 54 | } 55 | 56 | func (t *TIMER) Unpack(r io.Reader) (int, error) { 57 | var CtrlWord uint32 58 | err := binary.Read(r, binary.LittleEndian, &CtrlWord) 59 | if err != nil { 60 | return 0, err 61 | } 62 | 63 | t.EN = CtrlWord&(1<<31) != 0 64 | t.TT = CtrlWord&(1<<30) != 0 65 | t.DN = CtrlWord&(1<<29) != 0 66 | 67 | err = binary.Read(r, binary.LittleEndian, &(t.PRE)) 68 | if err != nil { 69 | return 4, err 70 | } 71 | 72 | err = binary.Read(r, binary.LittleEndian, &(t.ACC)) 73 | if err != nil { 74 | return 8, err 75 | } 76 | 77 | return 12, nil 78 | } 79 | 80 | func (TIMER) TypeAbbr() (string, uint16) { 81 | return "TIMER,DINT,DINT,DINT", 0x0F83 82 | } 83 | -------------------------------------------------------------------------------- /license.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 danomagnum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /listallprograms.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func (client *Client) ListAllPrograms() error { 8 | client.Logger.Debug("listing all programs") 9 | 10 | // for generic messages we need to create the cip path ourselves. The serialize function can be used to do this. 11 | path, err := Serialize(CipObject_Programs, CIPInstance(0)) 12 | if err != nil { 13 | client.Logger.Warn("could not serialize path", "error", err) 14 | return err 15 | } 16 | 17 | number_of_attr_to_receive := 1 18 | attr_28_program_name := 28 19 | msg_data, err := Serialize(uint16(number_of_attr_to_receive), uint16(attr_28_program_name)) 20 | if err != nil { 21 | client.Logger.Warn("could not serialize message data", "error", err) 22 | return err 23 | } 24 | 25 | resp, err := client.GenericCIPMessage(CIPService_GetInstanceAttributeList, path.Bytes(), msg_data.Bytes()) 26 | if err != nil { 27 | client.Logger.Warn("problem reading programs", "error", err) 28 | return err 29 | } 30 | 31 | results := make(map[string]*KnownProgram) 32 | 33 | for { 34 | var hdr listprograms_resp_header 35 | err = resp.DeSerialize(&hdr) 36 | if err != nil { 37 | client.Logger.Debug("got last item") 38 | break 39 | } 40 | 41 | // read the name 42 | name := make([]byte, hdr.NameLen) 43 | err = resp.DeSerialize(&name) 44 | if err != nil { 45 | client.Logger.Warn("could not read name", "error", err) 46 | return err 47 | } 48 | 49 | // convert the name to a string 50 | namestr := strings.TrimSpace(string(name)) 51 | results[namestr] = &KnownProgram{ID: CIPInstance(hdr.InstanceID), Name: namestr} 52 | } 53 | 54 | client.KnownPrograms = results 55 | 56 | return nil 57 | } 58 | 59 | type listprograms_resp_header struct { 60 | InstanceID uint16 61 | Padding uint16 62 | NameLen uint32 63 | } 64 | -------------------------------------------------------------------------------- /listidentity.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | func (client *Client) ListIdentity() (*listIdentityResponeBody, error) { 10 | client.Logger.Debug("listing identity") 11 | 12 | _, data, err := client.send_recv_data(cipCommandListIdentity) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | items, err := readItems(data) 18 | if err != nil { 19 | return nil, fmt.Errorf("couldn't parse items. %w", err) 20 | } 21 | 22 | if len(items) != 1 { 23 | return nil, fmt.Errorf("expected 1 item, got %d", len(items)) 24 | } 25 | 26 | // The response only contains one item. 27 | response := listIdentityResponeBody{} 28 | err = response.ParseFromBytes(items[0].Data) 29 | if err != nil { 30 | return nil, fmt.Errorf("problem reading list identity response. %w", err) 31 | } 32 | 33 | return &response, nil 34 | } 35 | 36 | type listIdentityResponeBody struct { 37 | EncapProtocolVersion uint16 38 | SocketAddress listIdentitySocketAddress 39 | Vendor VendorId 40 | DeviceType DeviceType 41 | ProductCode uint16 42 | Revision uint16 43 | Status uint16 44 | SerialNumber uint32 45 | ProductName string 46 | State uint8 47 | } 48 | 49 | type listIdentitySocketAddress struct { 50 | Family uint16 51 | Port uint16 52 | Address uint32 53 | Zero0 uint32 54 | Zero1 uint32 55 | } 56 | 57 | func (s *listIdentityResponeBody) ParseFromBytes(data []byte) error { 58 | buf := bytes.NewBuffer(data) 59 | 60 | err := binary.Read(buf, binary.LittleEndian, &s.EncapProtocolVersion) 61 | if err != nil { 62 | return fmt.Errorf("problem reading list identity response. field EncapProtocolVersion %w", err) 63 | } 64 | 65 | // For some reason this field in particular is big endian. 66 | err = binary.Read(buf, binary.BigEndian, &s.SocketAddress) 67 | if err != nil { 68 | return fmt.Errorf("problem reading list identity response. field SocketAddress %w", err) 69 | } 70 | 71 | err = binary.Read(buf, binary.LittleEndian, &s.Vendor) 72 | if err != nil { 73 | return fmt.Errorf("problem reading list identity response. field Vendor %w", err) 74 | } 75 | err = binary.Read(buf, binary.LittleEndian, &s.DeviceType) 76 | if err != nil { 77 | return fmt.Errorf("problem reading list identity response. field DeviceType %w", err) 78 | } 79 | err = binary.Read(buf, binary.LittleEndian, &s.ProductCode) 80 | if err != nil { 81 | return fmt.Errorf("problem reading list identity response. field ProductCode %w", err) 82 | } 83 | err = binary.Read(buf, binary.LittleEndian, &s.Revision) 84 | if err != nil { 85 | return fmt.Errorf("problem reading list identity response. field Revision %w", err) 86 | } 87 | err = binary.Read(buf, binary.LittleEndian, &s.Status) 88 | if err != nil { 89 | return fmt.Errorf("problem reading list identity response. field Status %w", err) 90 | } 91 | err = binary.Read(buf, binary.LittleEndian, &s.SerialNumber) 92 | if err != nil { 93 | return fmt.Errorf("problem reading list identity response. field SerialNumber %w", err) 94 | } 95 | 96 | productNameLength := uint8(0) 97 | err = binary.Read(buf, binary.LittleEndian, &productNameLength) 98 | if err != nil { 99 | return fmt.Errorf("problem reading list identity response. field ProductNameLength %w", err) 100 | } 101 | 102 | productName := make([]byte, productNameLength) 103 | err = binary.Read(buf, binary.LittleEndian, &productName) 104 | if err != nil { 105 | return fmt.Errorf("problem reading list identity response. field ProductName %w", err) 106 | } 107 | s.ProductName = string(productName) 108 | 109 | err = binary.Read(buf, binary.LittleEndian, &s.State) 110 | if err != nil { 111 | return fmt.Errorf("problem reading list identity response. field State %w", err) 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /listidentity_test.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseListIdentityResponse(t *testing.T) { 8 | itemBytes := []byte{1, 0, 0, 2, 175, 18, 192, 168, 1, 65, 0, 0, 0, 0, 0, 0, 0, 0, 21, 5, 12, 0, 1, 0, 1, 2, 52, 0, 3, 232, 34, 23, 20, 73, 81, 32, 83, 101, 110, 115, 111, 114, 32, 78, 101, 116, 32, 83, 121, 115, 116, 101, 109, 3} 9 | response := listIdentityResponeBody{} 10 | err := response.ParseFromBytes(itemBytes) 11 | if err != nil { 12 | t.Fatalf("error parsing list identity response: %v", err) 13 | } 14 | 15 | expected := listIdentityResponeBody{ 16 | EncapProtocolVersion: 1, 17 | SocketAddress: listIdentitySocketAddress{ 18 | Family: 2, 19 | Port: 44818, 20 | Address: 3232235841, 21 | Zero0: 0, 22 | Zero1: 0, 23 | }, 24 | Vendor: 0x0515, 25 | DeviceType: 12, 26 | ProductCode: 1, 27 | Revision: 0x0201, 28 | Status: 0x0034, 29 | SerialNumber: 0x1722E803, 30 | ProductName: "IQ Sensor Net System", 31 | State: 3, 32 | } 33 | 34 | if response != expected { 35 | t.Fatalf("expected %v, got %v", expected, response) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /listservices.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | func (client *Client) ListServices() ([]CIPListService, error) { 10 | client.Logger.Debug("listing services") 11 | 12 | _, data, err := client.send_recv_data(cipCommandListServices) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | items, err := readItems(data) 18 | if err != nil { 19 | return nil, fmt.Errorf("couldn't parse items. %w", err) 20 | } 21 | 22 | services := make([]CIPListService, len(items)) 23 | for i, item := range items { 24 | err := services[i].ParseFromBytes(item.Data) 25 | if err != nil { 26 | return nil, fmt.Errorf("problem reading list services response. %w", err) 27 | } 28 | } 29 | 30 | return services, nil 31 | } 32 | 33 | type CIPListService struct { 34 | EncapProtocolVersion uint16 35 | Capabilities ServiceCapabilityFlags 36 | Name string 37 | } 38 | 39 | func (s *CIPListService) ParseFromBytes(data []byte) error { 40 | buf := bytes.NewBuffer(data) 41 | 42 | err := binary.Read(buf, binary.LittleEndian, &s.EncapProtocolVersion) 43 | if err != nil { 44 | return fmt.Errorf("problem reading list services response. field EncapProtocolVersion %w", err) 45 | } 46 | 47 | err = binary.Read(buf, binary.LittleEndian, &s.Capabilities) 48 | if err != nil { 49 | return fmt.Errorf("problem reading list services response. field CapFlags %w", err) 50 | } 51 | 52 | // The name field is a 16 byte null terminated string 53 | name := make([]byte, 16) 54 | err = binary.Read(buf, binary.LittleEndian, &name) 55 | if err != nil { 56 | return fmt.Errorf("problem reading list services response. field Name %w", err) 57 | } 58 | // Remove any trailing NULL characters from the name 59 | s.Name = string(bytes.TrimRight(name, "\x00")) 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /listservices_test.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseListServicesResponse(t *testing.T) { 8 | itemBytes := []byte{1, 0, 32, 1, 67, 111, 109, 109, 117, 110, 105, 99, 97, 116, 105, 111, 110, 115, 0, 0} 9 | 10 | response := CIPListService{} 11 | err := response.ParseFromBytes(itemBytes) 12 | if err != nil { 13 | t.Fatalf("error parsing list services response: %v", err) 14 | } 15 | 16 | expected := CIPListService{ 17 | EncapProtocolVersion: 1, 18 | Capabilities: ServiceCapabilityFlag_CipEncapsulation | ServiceCapabilityFlag_SupportsClass1UDP, 19 | Name: "Communications", 20 | } 21 | 22 | if response != expected { 23 | t.Fatalf("expected %v, got %v", expected, response) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /listsubtags.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "log" 8 | "strings" 9 | ) 10 | 11 | // the gist here is that we want to do a fragmented read (since there will undoubtedly be more than one packet's worth) 12 | // of the instance attribute list of the symbol objects. 13 | // 14 | // see 1756-PM020H-EN-P March 2022 page 39 15 | // also see https://forums.mrclient.com/index.php?/topic/40626-reading-and-writing-io-tags-in-plc/ 16 | func (client *Client) ListSubTags(Program *KnownProgram, start_instance uint32) ([]KnownTag, error) { 17 | 18 | new_kts := make([]KnownTag, 0, 100) 19 | client.Logger.Debug("readall", "start id", start_instance) 20 | 21 | // have to start at 1. 22 | if start_instance == 0 { 23 | start_instance = 1 24 | } 25 | 26 | reqitems := make([]CIPItem, 2) 27 | //reqitems[0] = cipItem{Header: cipItemHeader{ID: cipItem_Null}} 28 | reqitems[0] = newItem(cipItem_ConnectionAddress, &client.OTNetworkConnectionID) 29 | 30 | p, err := Serialize( 31 | Program.Bytes(), 32 | CipObject_Symbol, CIPInstance(start_instance), 33 | ) 34 | if err != nil { 35 | return new_kts, fmt.Errorf("couldn't build path. %w", err) 36 | } 37 | 38 | readmsg := msgCIPConnectedServiceReq{ 39 | SequenceCount: uint16(sequencer()), 40 | Service: CIPService_GetInstanceAttributeList, 41 | PathLength: byte(p.Len() / 2), 42 | } 43 | 44 | reqitems[1] = newItem(cipItem_ConnectedData, readmsg) 45 | err = reqitems[1].Serialize(p.Bytes()) 46 | if err != nil { 47 | return new_kts, fmt.Errorf("problem serializing path: %w", err) 48 | } 49 | number_of_attr_to_receive := 3 50 | attr1_symbol_name := 1 51 | attr2_symbol_type := 2 52 | attr8_arraydims := 8 53 | err = reqitems[1].Serialize([4]uint16{uint16(number_of_attr_to_receive), uint16(attr1_symbol_name), uint16(attr2_symbol_type), uint16(attr8_arraydims)}) 54 | if err != nil { 55 | return new_kts, fmt.Errorf("problem serializing item attribute list: %w", err) 56 | } 57 | 58 | itemdata, err := serializeItems(reqitems) 59 | if err != nil { 60 | return nil, fmt.Errorf("problem serializing items: %w", err) 61 | } 62 | hdr, data, err := client.send_recv_data(cipCommandSendUnitData, itemdata) 63 | if err != nil { 64 | return new_kts, err 65 | } 66 | _ = hdr 67 | _ = data 68 | 69 | // first six bytes are zero. 70 | padding := make([]byte, 6) 71 | _, err = data.Read(padding) 72 | if err != nil { 73 | return nil, fmt.Errorf("problem reading padding. %w", err) 74 | } 75 | 76 | resp_items, err := readItems(data) 77 | if err != nil { 78 | return new_kts, fmt.Errorf("couldn't parse items. %w", err) 79 | } 80 | 81 | // get ready to read tag info from item 1 data 82 | data2 := bytes.NewBuffer(resp_items[1].Data) 83 | //data2.Next(4) 84 | data_hdr := msgListInstanceHeader{} 85 | err = binary.Read(data2, binary.LittleEndian, &data_hdr) 86 | if err != nil { 87 | return nil, fmt.Errorf("problem reading tag header. %w", err) 88 | } 89 | 90 | tag_hdr := new(msgtagResultDataHeader) 91 | tag_ftr := new(TagInfo) 92 | for data2.Len() > 0 { 93 | 94 | err = binary.Read(data2, binary.LittleEndian, tag_hdr) 95 | if err != nil { 96 | return nil, fmt.Errorf("problem reading tag header. %w", err) 97 | } 98 | newtag_bytes := make([]byte, tag_hdr.NameLength) 99 | err = binary.Read(data2, binary.LittleEndian, &newtag_bytes) 100 | if err != nil { 101 | return nil, fmt.Errorf("problem reading tag header. %w", err) 102 | } 103 | newtag_name := fmt.Sprintf("program:%s.%s", Program.Name, string(newtag_bytes)) 104 | 105 | // the end of the tagname has to be aligned on a 16 bit word 106 | //tagname_alignment := tag_hdr.NameLength % 2 107 | //if tagname_alignment != 0 { 108 | //data2.Next(int(tagname_alignment)) 109 | //} 110 | err = binary.Read(data2, binary.LittleEndian, tag_ftr) 111 | if err != nil { 112 | return nil, fmt.Errorf("problem reading tag footer. %w", err) 113 | } 114 | 115 | if strings.ToLower(newtag_name) == "program:gologix_tests.testtimer" { 116 | log.Printf("found it.") 117 | } 118 | 119 | kt := KnownTag{ 120 | Name: newtag_name, 121 | Info: *tag_ftr, 122 | Instance: CIPInstance(tag_hdr.InstanceID), 123 | Parent: Program, 124 | } 125 | if tag_ftr.Dimension3 != 0 { 126 | kt.Array_Order = make([]int, 3) 127 | kt.Array_Order[2] = int(tag_ftr.Dimension3) 128 | kt.Array_Order[1] = int(tag_ftr.Dimension2) 129 | kt.Array_Order[0] = int(tag_ftr.Dimension1) 130 | } else if tag_ftr.Dimension2 != 0 { 131 | kt.Array_Order = make([]int, 2) 132 | kt.Array_Order[1] = int(tag_ftr.Dimension2) 133 | kt.Array_Order[0] = int(tag_ftr.Dimension1) 134 | } else if tag_ftr.Dimension1 != 0 { 135 | kt.Array_Order = make([]int, 1) 136 | kt.Array_Order[0] = int(tag_ftr.Dimension1) 137 | } else { 138 | kt.Array_Order = make([]int, 0) 139 | } 140 | if !isValidTag(string(newtag_bytes), *tag_ftr) { 141 | continue 142 | } 143 | client.KnownTags[strings.ToLower(newtag_name)] = kt 144 | new_kts = append(new_kts, kt) 145 | 146 | start_instance = tag_hdr.InstanceID 147 | 148 | } 149 | 150 | if data_hdr.Status == uint16(CIPStatus_PartialTransfer) { 151 | _, err = client.ListSubTags(Program, start_instance) 152 | if err != nil { 153 | return new_kts, fmt.Errorf("problem listing subtags. %w", err) 154 | } 155 | } 156 | 157 | return new_kts, nil 158 | } 159 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "log/slog" 5 | ) 6 | 7 | // LoggerInterface defines the minimum logging interface required for basic logging. 8 | // This interface provides the core logging methods but does not include contextual 9 | // logging capabilities. When used for connections, additional connection information 10 | // will not be included in log messages unless LoggerInterfaceWith is used. 11 | type LoggerInterface interface { 12 | // Debug logs a message at debug level with optional key-value pairs. 13 | Debug(msg string, args ...any) 14 | 15 | // Error logs a message at error level with optional key-value pairs. 16 | Error(msg string, args ...any) 17 | 18 | // Warn logs a message at warning level with optional key-value pairs. 19 | Warn(msg string, args ...any) 20 | 21 | // Info logs a message at info level with optional key-value pairs. 22 | Info(msg string, args ...any) 23 | } 24 | 25 | // LoggerInterfaceWith extends LoggerInterface with contextual logging capabilities. 26 | // When used for connections, this interface allows controllerIp details to be 27 | // automatically included in all log messages. 28 | type LoggerInterfaceWith interface { 29 | LoggerInterface 30 | 31 | // With returns a new logger with the given key-value pairs added to the context. 32 | // This is useful for adding connection-specific information to all subsequent logs. 33 | // 34 | // Used in connect.go 35 | With(args ...any) LoggerInterfaceWith 36 | } 37 | 38 | // Logger implements both LoggerInterface and LoggerInterfaceWith. 39 | // It wraps the standard library's slog.Logger to provide the implemented interfaces. 40 | type Logger struct { 41 | internalLogger *slog.Logger 42 | } 43 | 44 | func NewLogger() LoggerInterface { 45 | return &Logger{ 46 | internalLogger: slog.Default(), 47 | } 48 | } 49 | 50 | func (l *Logger) SetLogger(logger *slog.Logger) { 51 | l.internalLogger = logger 52 | } 53 | 54 | func (l *Logger) With(args ...any) LoggerInterfaceWith { 55 | return &Logger{ 56 | internalLogger: l.internalLogger.With(args...), 57 | } 58 | } 59 | 60 | func (l *Logger) Debug(msg string, args ...any) { 61 | l.internalLogger.Debug(msg, args...) 62 | } 63 | 64 | func (l *Logger) Error(msg string, args ...any) { 65 | l.internalLogger.Error(msg, args...) 66 | } 67 | 68 | func (l *Logger) Warn(msg string, args ...any) { 69 | l.internalLogger.Warn(msg, args...) 70 | } 71 | 72 | func (l *Logger) Info(msg string, args ...any) { 73 | l.internalLogger.Info(msg, args...) 74 | } 75 | -------------------------------------------------------------------------------- /messages.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // todo: move sequence to a different struct and combine msgCIPIOIHeader and CIPMultiIOIHeader 9 | type msgCIPIOIHeader struct { 10 | Sequence uint16 11 | Service CIPService 12 | Size byte 13 | } 14 | 15 | type msgCIPMultiIOIHeader struct { 16 | Service CIPService 17 | Size byte 18 | } 19 | 20 | // this is the generic connected message. 21 | // it goes into an item (always item[1]?) and is followed up with 22 | // a valid path. The item specifies the cipService that goes with the message 23 | type msgCIPConnectedServiceReq struct { 24 | SequenceCount uint16 25 | Service CIPService 26 | PathLength byte 27 | } 28 | 29 | type msgCIPConnectedMultiServiceReq struct { 30 | Sequence uint16 31 | Service CIPService 32 | PathSize byte 33 | Path [4]byte 34 | ServiceCount uint16 35 | } 36 | 37 | type msgCIPWriteIOIFooter struct { 38 | DataType uint16 39 | Elements uint16 40 | } 41 | 42 | func (ftr msgCIPWriteIOIFooter) Bytes() []byte { 43 | if ftr.DataType == uint16(CIPTypeSTRING) { 44 | b := []byte{0xA0, 0x02, 0xCE, 0x0F, 0x00, 0x00} 45 | binary.LittleEndian.PutUint16(b[4:], ftr.Elements) 46 | return b 47 | } 48 | 49 | b := bytes.Buffer{} 50 | binary.Write(&b, binary.LittleEndian, ftr) 51 | return b.Bytes() 52 | 53 | } 54 | func (ftr msgCIPWriteIOIFooter) Len() int { 55 | if ftr.DataType == uint16(CIPTypeSTRING) { 56 | return 6 57 | } 58 | 59 | return 4 60 | } 61 | 62 | type msgCIPIOIFooter struct { 63 | Elements uint16 64 | } 65 | 66 | type msgCIPResultHeader struct { 67 | InterfaceHandle uint32 68 | Timeout uint16 69 | } 70 | 71 | // This should be everything before the actual result value data 72 | // so you can read this off the buffer and be in the correct position to 73 | // read the actual value as the type indicated by Type 74 | type msgCIPReadResultData struct { 75 | SequenceCounter uint16 76 | Service CIPService 77 | Status [3]byte 78 | Type CIPType 79 | Unknown byte 80 | } 81 | -------------------------------------------------------------------------------- /partialread.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | // Read a list of tags of specified types. 10 | // 11 | // The result slice will be in the same order as the tag list. Each value in the list will be an 12 | // interface{} so you'll need to type assert to get the values back out. 13 | // 14 | // To read multiple tags at once without type assertion you can use ReadMulti() 15 | func (client *Client) ReadList(tagnames []string, types []any, elements []int) ([]any, error) { 16 | err := client.checkConnection() 17 | if err != nil { 18 | return nil, fmt.Errorf("could not start list read: %w", err) 19 | } 20 | n := 0 21 | n_new := 0 22 | total := len(tagnames) 23 | results := make([]any, 0, total) 24 | msgs := 0 25 | 26 | tags := make([]tagDesc, total) 27 | 28 | for i := range tagnames { 29 | typ, _ := GoVarToCIPType(types[i]) 30 | tags[i] = tagDesc{ 31 | TagName: tagnames[i], 32 | TagType: typ, 33 | Elements: elements[i], 34 | Struct: types[i], 35 | } 36 | } 37 | 38 | for n < total { 39 | msgs += 1 40 | n_new, err = client.countIOIsThatFit(tags[n:]) 41 | if err != nil { 42 | return nil, err 43 | } 44 | subresults, err := client.readList(tags[n : n+n_new]) 45 | n += n_new 46 | if err != nil { 47 | return nil, err 48 | } 49 | results = append(results, subresults...) 50 | 51 | } 52 | 53 | client.Logger.Debug("Multi Read", "messages", msgs, "tags", n) 54 | return results, nil 55 | } 56 | 57 | func (client *Client) countIOIsThatFit(tags []tagDesc) (int, error) { 58 | // first generate IOIs for each tag 59 | qty := len(tags) 60 | 61 | ioi_header := msgCIPConnectedMultiServiceReq{ 62 | Sequence: uint16(sequencer()), 63 | Service: CIPService_MultipleService, 64 | PathSize: 2, 65 | Path: [4]byte{0x20, 0x02, 0x24, 0x01}, 66 | ServiceCount: uint16(qty), 67 | } 68 | 69 | mainhdr_size := binary.Size(ioi_header) 70 | ioihdr_size := binary.Size(msgCIPMultiIOIHeader{}) 71 | ioiftr_size := binary.Size(msgCIPIOIFooter{}) 72 | 73 | b := bytes.Buffer{} 74 | // we now have to build up the jump table for each IOI. 75 | // and pack all the IOIs together into b 76 | jump_table := make([]uint16, qty) 77 | 78 | // how many ioi's fit in the message 79 | n := 1 80 | 81 | response_size := 0 82 | 83 | for i, tag := range tags { 84 | ioi, err := client.newIOI(tag.TagName, tag.TagType) 85 | if err != nil { 86 | return 0, err 87 | } 88 | 89 | jump_table[i] = uint16(b.Len()) 90 | 91 | h := msgCIPMultiIOIHeader{ 92 | Service: CIPService_Read, 93 | Size: byte(len(ioi.Buffer) / 2), 94 | } 95 | f := msgCIPIOIFooter{ 96 | Elements: 1, 97 | } 98 | 99 | // Calculate the size of the data once we add this ioi to the list. 100 | newSize := mainhdr_size // length of the multi-read header 101 | newSize += 2 * n // add in the jump table 102 | newSize += b.Len() // everything we have so far 103 | newSize += ioihdr_size + len(ioi.Buffer) + ioiftr_size // the new ioi data 104 | //newSize += 4 // Fudge for alignment if needed 105 | 106 | response_size += tags[i].TagType.Size() * tags[i].Elements 107 | if newSize >= int(client.ConnectionSize) || response_size >= int(client.ConnectionSize) { 108 | // break before adding this ioi to the list since it will push us over. 109 | // we'll continue with n iois (n only increments after an IOI is added) 110 | break 111 | } 112 | 113 | err = binary.Write(&b, binary.LittleEndian, h) 114 | if err != nil { 115 | return 0, fmt.Errorf("problem writing cip IO header to buffer. %w", err) 116 | } 117 | b.Write(ioi.Buffer) 118 | err = binary.Write(&b, binary.LittleEndian, f) 119 | if err != nil { 120 | return 0, fmt.Errorf("problem writing ioi buffer to msg buffer. %w", err) 121 | } 122 | 123 | n = i + 1 124 | } 125 | 126 | client.Logger.Debug("Packed Efficiency", "tags", n, "bytes", client.ConnectionSize) 127 | 128 | return n, nil 129 | 130 | } 131 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | // based on code from https://github.com/loki-os/go-ethernet-ip 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // The path is formatted like this. 13 | // byte 0: number of 16 bit words 14 | // byte 1: 000. .... path segment type (port segment = 0) 15 | // byte 1: ...0 .... extended link address (0 = false) 16 | // byte 1: .... 0001 port (backplane = 1) 17 | // byte 2: n/a 18 | // byte 3: 001. .... path segment type (logical segment = 1) 19 | // byte 3: ...0 00.. logical segment type class ID (0) 20 | // byte 3: .... ..00 logical segment format: 8-bit (0) 21 | // byte 4: path segment 0x20 22 | // byte 5: 001. .... path segment type (logical segment = 1) 23 | // byte 5: ...0 01.. logical segment type: Instance ID = 1 24 | // byte 5: .... ..00 logical segment format: 8-bit (0) 25 | // byte 6: path segment instance 0x01 26 | // so on... 27 | //msg.Path = [6]byte{0x01, 0x00, 0x20, 0x02, 0x24, 0x01} 28 | 29 | // bits 5,6,7 (counting from 0) are the segment type 30 | type segmentType byte 31 | 32 | const ( 33 | segmentTypeExtendedSymbolic segmentType = 0x91 34 | ) 35 | 36 | // represents the port number on a CIP device 37 | // 38 | // If you're going to serialize this class to bytes for transimssion be sure to use one of the gologix 39 | // serialization functions or call Bytes() to get the properly formatted data. 40 | type CIPPort struct { 41 | PortNo byte 42 | ExtensionLen byte 43 | } 44 | 45 | func (p CIPPort) Len() int { 46 | return 2 47 | } 48 | 49 | func (p CIPPort) Bytes() []byte { 50 | if p.ExtensionLen != 0 { 51 | return []byte{p.PortNo, p.ExtensionLen} 52 | 53 | } 54 | return []byte{p.PortNo} 55 | } 56 | 57 | // This function takes a CIP path in the format of 0,1,192.168.2.1,0,1 and converts it into the proper equivalent byte slice. 58 | // 59 | // The most common use is probably setting up the communication path on a new client. 60 | func ParsePath(path string) (*bytes.Buffer, error) { 61 | if path == "" { 62 | return new(bytes.Buffer), nil 63 | } 64 | // get rid of any spaces and square brackets 65 | path = strings.ReplaceAll(path, " ", "") 66 | path = strings.ReplaceAll(path, "[", "") 67 | path = strings.ReplaceAll(path, "]", "") 68 | // split on commas 69 | parts := strings.Split(path, ",") 70 | 71 | byte_path := make([]byte, 0, len(parts)) 72 | 73 | for _, part := range parts { 74 | // first see if this looks like an IP address. 75 | is_ip := strings.Contains(part, ".") 76 | if is_ip { 77 | // for some god forsaken reason the path doesn't use the ip address as actual bytes but as an ascii string. 78 | // we first have to set bit 5 in the previous byte to say we're using an extended address for this part. 79 | last_pos := len(byte_path) - 1 80 | last_byte := byte_path[last_pos] 81 | byte_path[last_pos] = last_byte | 1<<4 82 | l := len(part) 83 | byte_path = append(byte_path, byte(l)) 84 | string_bytes := []byte(part) 85 | byte_path = append(byte_path, string_bytes...) 86 | continue 87 | } 88 | // not an IP address 89 | val, err := strconv.Atoi(part) 90 | if err != nil { 91 | return nil, fmt.Errorf("problem converting %v to number. %w", part, err) 92 | } 93 | if val < 0 || val > 255 { 94 | return nil, fmt.Errorf("number out of range. %v", part) 95 | } 96 | byte_path = append(byte_path, byte(val)) 97 | } 98 | 99 | return bytes.NewBuffer(byte_path), nil 100 | } 101 | -------------------------------------------------------------------------------- /path_test.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestPath(t *testing.T) { 9 | var tests = []struct { 10 | path string 11 | want []byte 12 | }{ 13 | { 14 | "1,0,2,172.25.58.11,1,1", 15 | []byte{0x01, 0x00, 0x12, 0x0C, 0x31, 0x37, 0x32, 0x2E, 0x32, 0x35, 0x2E, 0x35, 0x38, 0x2E, 0x31, 0x31, 0x01, 0x01}, 16 | }, 17 | { 18 | "1,0,32,2,36,1", 19 | []byte{0x01, 0x00, 0x20, 0x02, 0x24, 0x01}, 20 | }, 21 | { 22 | "1,0", 23 | []byte{0x01, 0x00}, 24 | }, 25 | } 26 | 27 | for _, tt := range tests { 28 | 29 | testname := fmt.Sprintf("path: %s", tt.path) 30 | t.Run(testname, func(t *testing.T) { 31 | res, err := ParsePath(tt.path) 32 | if err != nil { 33 | t.Errorf("Error in pathgen for %s. %v", tt.path, err) 34 | } 35 | if !check_bytes(res.Bytes(), tt.want) { 36 | t.Errorf("Wrong Value for result. \nWanted %v. \nGot %v", tt.want, res) 37 | } 38 | }) 39 | } 40 | 41 | } 42 | 43 | func check_bytes(s0, s1 []byte) bool { 44 | if len(s1) != len(s0) { 45 | return false 46 | } 47 | for i := range s0 { 48 | if s0[i] != s1[i] { 49 | return false 50 | } 51 | 52 | } 53 | return true 54 | } 55 | 56 | func TestPathBuild(t *testing.T) { 57 | client := Client{} 58 | client.SocketTimeout = 0 59 | 60 | pmp_ioi, err := client.newIOI("Program:MainProgram", 16) 61 | if err != nil { 62 | t.Errorf("problem creating pmp ioi. %v", err) 63 | } 64 | 65 | tests := []struct { 66 | name string 67 | path []any 68 | want []byte 69 | }{ 70 | { 71 | name: "connection manager only", 72 | path: []any{CipObject_ConnectionManager}, 73 | want: []byte{0x20, 0x06}, 74 | }, 75 | { 76 | name: "backplane to slot 0", 77 | path: []any{CIPPort{PortNo: 1}, cipAddress(0)}, 78 | want: []byte{0x01, 0x00}, 79 | }, 80 | { 81 | name: "connection manager instance 1", 82 | path: []any{CipObject_ConnectionManager, CIPInstance(1)}, 83 | want: []byte{0x20, 0x06, 0x24, 0x01}, 84 | }, 85 | { 86 | name: "Symbol Object Instance 0", 87 | path: []any{CipObject_Symbol, CIPInstance(0)}, 88 | want: []byte{0x20, 0x6B, 0x24, 0x00}, 89 | }, 90 | { 91 | name: "Symbol Object Instance 0 of tag 'Program:MainProgram'", 92 | path: []any{pmp_ioi, CipObject_Symbol, CIPInstance(0)}, 93 | want: []byte{0x91, 0x13, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x3a, 0x6d, 0x61, 0x69, 94 | 0x6e, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x00, 0x20, 0x6B, 0x24, 0x00}, 95 | }, 96 | { 97 | name: "Template Attributes Instance 0x02E9", 98 | path: []any{CipObject_Template, CIPInstance(0x02E9)}, 99 | want: []byte{0x20, 0x6C, 0x25, 0x00, 0xE9, 0x02}, 100 | }, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | have, err := Serialize(tt.path...) 105 | if err != nil { 106 | t.Errorf("Problem building path. %v", err) 107 | } 108 | if !check_bytes(have.Bytes(), tt.want) { 109 | t.Errorf("ResultMismatch.\n Have %v\n Want %v\n", have.Bytes(), tt.want) 110 | } 111 | }) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /sendreceive.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | type eipHeader struct { 11 | Command CIPCommand 12 | Length uint16 13 | SessionHandle uint32 14 | Status uint32 15 | Context uint64 // 8 bytes you can do whatever you want with. They'll be echoed back. 16 | Options uint32 17 | } 18 | 19 | // prepares a bytes buffer for sending a message 20 | func (client *Client) sendMsgBuild(cmd CIPCommand, msgs ...any) ([]byte, error) { 21 | // calculate messageLen of all message parts 22 | messageLen := 0 23 | for _, msg := range msgs { 24 | messageLen += binary.Size(msg) 25 | } 26 | // build header based on size 27 | hdr := client.newEIPHeader(cmd, messageLen) 28 | 29 | // initialize a buffer and add the header to it. 30 | // the 24 is from the header size 31 | b := make([]byte, 0, messageLen+24) 32 | buf := bytes.NewBuffer(b) 33 | err := binary.Write(buf, binary.LittleEndian, hdr) 34 | if err != nil { 35 | return nil, fmt.Errorf("problem writing header to buffer. %w", err) 36 | } 37 | 38 | // add all message components to the buffer. 39 | for _, msg := range msgs { 40 | err = binary.Write(buf, binary.LittleEndian, msg) 41 | if err != nil { 42 | return nil, fmt.Errorf("problem writing msg to buffer. %w", err) 43 | } 44 | } 45 | 46 | return buf.Bytes(), nil 47 | } 48 | 49 | // send takes the command followed by all the structures that need 50 | // concatenated together. 51 | // 52 | // It builds the appropriate header for all the data, puts the packet together, and then sends it. 53 | func (client *Client) sendData(b []byte) error { 54 | // write the packet buffer to the tcp connection 55 | written := 0 56 | for written < len(b) { 57 | if client.SocketTimeout != 0 { 58 | err := client.conn.SetWriteDeadline(time.Now().Add(client.SocketTimeout)) 59 | if err != nil { 60 | return fmt.Errorf("problem setting write deadline: %w", err) 61 | } 62 | } else { 63 | err := client.conn.SetWriteDeadline(time.Now().Add(time.Second)) 64 | if err != nil { 65 | return fmt.Errorf("problem setting write deadline: %w", err) 66 | } 67 | } 68 | n, err := client.conn.Write(b[written:]) 69 | if err != nil { 70 | return fmt.Errorf("problem writing to socket: %w", err) 71 | } 72 | written += n 73 | } 74 | return nil 75 | 76 | } 77 | 78 | // sends one message and gets one response in a mutex-protected way. 79 | func (client *Client) send_recv_data(cmd CIPCommand, msgs ...any) (eipHeader, *bytes.Buffer, error) { 80 | buffer, err := client.sendMsgBuild(cmd, msgs...) 81 | if err != nil { 82 | return eipHeader{}, nil, fmt.Errorf("error preparing to send message: %w", err) 83 | } 84 | client.mutex.Lock() 85 | 86 | err = client.sendData(buffer) 87 | if err != nil { 88 | client.mutex.Unlock() 89 | err2 := client.Disconnect() 90 | if err2 != nil { 91 | return eipHeader{}, nil, fmt.Errorf("error disconnecting after send error %w: %w", err, err2) 92 | } 93 | return eipHeader{}, nil, fmt.Errorf("error sending data resulting in forced disconnect: %w", err) 94 | } 95 | 96 | hdr, buf, err := client.recvData() 97 | client.mutex.Unlock() 98 | if err != nil { 99 | err2 := client.Disconnect() 100 | if err2 != nil { 101 | return hdr, buf, fmt.Errorf("error disconnecting after recvError %w: %w", err, err2) 102 | } 103 | return hdr, buf, fmt.Errorf("error receiving data resulting in forced disconnect: %w", err) 104 | } 105 | return hdr, buf, nil 106 | } 107 | 108 | // recv_data reads the header and then the number of words it specifies. 109 | func (client *Client) recvData() (eipHeader, *bytes.Buffer, error) { 110 | 111 | hdr := eipHeader{} 112 | var err error 113 | if client.SocketTimeout != 0 { 114 | err = client.conn.SetReadDeadline(time.Now().Add(client.SocketTimeout)) 115 | if err != nil { 116 | return hdr, nil, fmt.Errorf("problem setting read deadline: %w", err) 117 | } 118 | } else { 119 | err = client.conn.SetReadDeadline(time.Now().Add(time.Second)) 120 | if err != nil { 121 | return hdr, nil, fmt.Errorf("problem setting read deadline: %w", err) 122 | } 123 | } 124 | err = binary.Read(client.conn, binary.LittleEndian, &hdr) 125 | if err != nil { 126 | return hdr, nil, fmt.Errorf("problem reading header from socket: %w", err) 127 | } 128 | if client.SocketTimeout != 0 { 129 | err = client.conn.SetReadDeadline(time.Now().Add(client.SocketTimeout)) 130 | if err != nil { 131 | return hdr, nil, fmt.Errorf("problem setting read deadline: %w", err) 132 | } 133 | } else { 134 | err = client.conn.SetReadDeadline(time.Now().Add(time.Second)) 135 | if err != nil { 136 | return hdr, nil, fmt.Errorf("problem setting read deadline: %w", err) 137 | } 138 | } 139 | data_size := hdr.Length 140 | data := make([]byte, data_size) 141 | if data_size > 0 { 142 | 143 | err = binary.Read(client.conn, binary.LittleEndian, &data) 144 | if err != nil { 145 | return hdr, nil, fmt.Errorf("problem reading socket payload: %w", err) 146 | } 147 | } 148 | buf := bytes.NewBuffer(data) 149 | return hdr, buf, err 150 | 151 | } 152 | 153 | func (client *Client) DebugCloseConn() { 154 | client.conn.Close() 155 | } 156 | 157 | func (client *Client) newEIPHeader(cmd CIPCommand, size int) (hdr eipHeader) { 158 | 159 | client.HeaderSequenceCounter++ 160 | 161 | hdr.Command = cmd 162 | hdr.Length = uint16(size) 163 | hdr.SessionHandle = client.SessionHandle 164 | hdr.Status = 0 165 | hdr.Context = client.Context 166 | hdr.Options = 0 167 | 168 | return 169 | 170 | } 171 | -------------------------------------------------------------------------------- /server_connections.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type serverConnection struct { 11 | ID uint16 12 | OT uint32 13 | TO uint32 14 | RPI time.Duration 15 | Path []byte 16 | Open bool 17 | } 18 | 19 | type serverConnectionManager struct { 20 | Connections []*serverConnection 21 | Lock sync.RWMutex 22 | Logger *slog.Logger 23 | } 24 | 25 | func (cm *serverConnectionManager) Init(logger *slog.Logger) { 26 | cm.Connections = make([]*serverConnection, 0, 32) 27 | cm.Logger = logger 28 | } 29 | func (cm *serverConnectionManager) Add(conn *serverConnection) { 30 | cm.Logger.Info("New Managed Connection.", "conn", *conn) 31 | cm.Lock.Lock() 32 | defer cm.Lock.Unlock() 33 | cm.Connections = append(cm.Connections, conn) 34 | } 35 | func (cm *serverConnectionManager) GetByID(ID uint16) (*serverConnection, error) { 36 | cm.Lock.RLock() 37 | defer cm.Lock.RUnlock() 38 | for _, conn := range cm.Connections { 39 | if conn.ID == ID { 40 | return conn, nil 41 | } 42 | } 43 | return nil, fmt.Errorf("couldn't find connection %v by ID", ID) 44 | } 45 | func (cm *serverConnectionManager) GetByOT(OT uint32) (*serverConnection, error) { 46 | cm.Lock.RLock() 47 | defer cm.Lock.RUnlock() 48 | for _, conn := range cm.Connections { 49 | if conn.OT == OT { 50 | return conn, nil 51 | } 52 | } 53 | return nil, fmt.Errorf("couldn't find connection %v by OT", OT) 54 | } 55 | func (cm *serverConnectionManager) GetByTO(TO uint32) (*serverConnection, error) { 56 | cm.Lock.RLock() 57 | defer cm.Lock.RUnlock() 58 | for _, conn := range cm.Connections { 59 | if conn.TO == TO { 60 | return conn, nil 61 | } 62 | } 63 | return nil, fmt.Errorf("couldn't find connection %v by TO", TO) 64 | } 65 | func (cm *serverConnectionManager) CloseByID(ID uint16) error { 66 | cm.Lock.Lock() 67 | defer cm.Lock.Unlock() 68 | for i, conn := range cm.Connections { 69 | if conn.ID == ID { 70 | conn.Open = false 71 | if len(cm.Connections) == 1 { 72 | cm.Connections = make([]*serverConnection, 0, 32) 73 | return nil 74 | } 75 | cm.Connections[i] = cm.Connections[len(cm.Connections)-1] 76 | cm.Connections = cm.Connections[:len(cm.Connections)-1] 77 | return nil 78 | } 79 | } 80 | return fmt.Errorf("couldn't find connection %v by ID", ID) 81 | } 82 | func (cm *serverConnectionManager) CloseByOT(OT uint32) error { 83 | cm.Lock.Lock() 84 | defer cm.Lock.Unlock() 85 | for i, conn := range cm.Connections { 86 | if conn.OT == OT { 87 | conn.Open = false 88 | if len(cm.Connections) == 1 { 89 | cm.Connections = make([]*serverConnection, 0, 32) 90 | return nil 91 | } 92 | cm.Connections[i] = cm.Connections[len(cm.Connections)-1] 93 | cm.Connections = cm.Connections[:len(cm.Connections)-1] 94 | return nil 95 | } 96 | } 97 | return fmt.Errorf("couldn't find connection %v by OT", OT) 98 | } 99 | func (cm *serverConnectionManager) CloseByTO(TO uint32) error { 100 | cm.Lock.Lock() 101 | defer cm.Lock.Unlock() 102 | for i, conn := range cm.Connections { 103 | if conn.TO == TO { 104 | conn.Open = false 105 | if len(cm.Connections) == 1 { 106 | cm.Connections = make([]*serverConnection, 0, 32) 107 | return nil 108 | } 109 | cm.Connections[i] = cm.Connections[len(cm.Connections)-1] 110 | cm.Connections = cm.Connections[:len(cm.Connections)-1] 111 | return nil 112 | } 113 | } 114 | return fmt.Errorf("couldn't find connection %v by TO", TO) 115 | } 116 | -------------------------------------------------------------------------------- /server_io.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | ) 9 | 10 | // this type satisfies the TagProvider interface to provide class 1 IO support 11 | // It has to be defined with an input and output struct that consist of only GoLogixTypes 12 | // It will then serialize the input data and send it to the PLC at the requested rate. 13 | // When the PLC sends an IO output message, that gets deserialized into the output structure. 14 | // 15 | // If you are going to access In or Out, be sure to lock the appropriate Mutex first to prevent data race. 16 | // Remember that they are pointers here so the locks also need to apply to the original data that was pointed at. 17 | // 18 | // it does not handle class 3 tag reads or writes. 19 | type IOProvider[Tin, Tout any] struct { 20 | InMutex sync.Mutex 21 | OutMutex sync.Mutex 22 | In *Tin 23 | Out *Tout 24 | } 25 | 26 | // this gets called with the IO setup forward open as the items 27 | func (p *IOProvider[Tin, Tout]) IORead() ([]byte, error) { 28 | p.InMutex.Lock() 29 | defer p.InMutex.Unlock() 30 | b := bytes.Buffer{} 31 | _, err := Pack(&b, *(p.In)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | dat := b.Bytes() 36 | return dat, nil 37 | } 38 | 39 | func (p *IOProvider[Tin, Tout]) IOWrite(items []CIPItem) error { 40 | if len(items) != 2 { 41 | return fmt.Errorf("expeted 2 items but got %v", len(items)) 42 | } 43 | if items[1].Header.ID != cipItem_ConnectedData { 44 | return fmt.Errorf("expected item 2 to be a connected data item but got %v", items[1].Header.ID) 45 | } 46 | var seq_counter uint32 47 | 48 | // according to wireshark only the least significant 4 bits are used. 49 | // 00.. ROO? 50 | // ..0. COO? 51 | // ...1 // Run/Idle (1 = run) 52 | var header uint16 53 | err := items[1].DeSerialize(&seq_counter) 54 | if err != nil { 55 | return fmt.Errorf("problem getting sequence counter. %w", err) 56 | } 57 | err = items[1].DeSerialize(&header) 58 | if err != nil { 59 | return fmt.Errorf("problem getting header. %w", err) 60 | } 61 | payload := make([]byte, items[1].Header.Length-6) 62 | err = items[1].DeSerialize(&payload) 63 | if err != nil { 64 | return fmt.Errorf("problem getting raw data. %w", err) 65 | } 66 | b := bytes.NewBuffer(payload) 67 | 68 | p.OutMutex.Lock() 69 | defer p.OutMutex.Unlock() 70 | 71 | _, err = Unpack(b, p.Out) 72 | if err != nil { 73 | return fmt.Errorf("problem unpacking data into output struct %w", err) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (p *IOProvider[Tin, Tout]) TagRead(tag string, qty int16) (any, error) { 80 | return 0, errors.New("not implemented") 81 | } 82 | 83 | func (p *IOProvider[Tin, Tout]) TagWrite(tag string, value any) error { 84 | return errors.New("not implemented") 85 | } 86 | 87 | // returns the most udpated copy of the output data 88 | // this output data is what the PLC is writing to us 89 | func (p *IOProvider[Tin, Tout]) GetOutputData() Tout { 90 | p.OutMutex.Lock() 91 | defer p.OutMutex.Unlock() 92 | t := *p.Out 93 | return t 94 | } 95 | 96 | // update the input data thread safely 97 | // this input data is what the PLC receives 98 | func (p *IOProvider[Tin, Tout]) SetInputData(newin Tin) { 99 | p.InMutex.Lock() 100 | defer p.InMutex.Unlock() 101 | p.In = &newin 102 | } 103 | -------------------------------------------------------------------------------- /server_iochannel.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | ) 9 | 10 | // this type satisfies the TagChannelProvider interface to provide class 1 IO support 11 | // It has to be defined with an input and output struct that consist of only GoLogixTypes 12 | // It will then serialize the input data and send it to the PLC at the requested rate. 13 | // When the PLC sends an IO output message, that gets deserialized and sent to all destination channels. 14 | // 15 | // get a new destintaion channel by calling GetOutputData. You can then receive from this to get data as it comes in. 16 | // 17 | // update the input data with the SetInputData(Tin) function 18 | // 19 | // it does not handle class 3 tag reads or writes. 20 | type IOChannelProvider[Tin, Tout any] struct { 21 | inMutex sync.Mutex 22 | in Tin 23 | outChannels []chan Tout 24 | } 25 | 26 | // this gets called with the IO setup forward open as the items 27 | func (p *IOChannelProvider[Tin, Tout]) IORead() ([]byte, error) { 28 | p.inMutex.Lock() 29 | defer p.inMutex.Unlock() 30 | b := bytes.Buffer{} 31 | _, err := Pack(&b, p.in) 32 | if err != nil { 33 | return nil, err 34 | } 35 | dat := b.Bytes() 36 | return dat, nil 37 | } 38 | 39 | func (p *IOChannelProvider[Tin, Tout]) IOWrite(items []CIPItem) error { 40 | if len(items) != 2 { 41 | return fmt.Errorf("expeted 2 items but got %v", len(items)) 42 | } 43 | if items[1].Header.ID != cipItem_ConnectedData { 44 | return fmt.Errorf("expected item 2 to be a connected data item but got %v", items[1].Header.ID) 45 | } 46 | var seq_counter uint32 47 | 48 | // according to wireshark only the least significant 4 bits are used. 49 | // 00.. ROO? 50 | // ..0. COO? 51 | // ...1 // Run/Idle (1 = run) 52 | var header uint16 53 | err := items[1].DeSerialize(&seq_counter) 54 | if err != nil { 55 | return fmt.Errorf("problem getting sequence counter. %w", err) 56 | } 57 | err = items[1].DeSerialize(&header) 58 | if err != nil { 59 | return fmt.Errorf("problem getting header. %w", err) 60 | } 61 | payload := make([]byte, items[1].Header.Length-6) 62 | err = items[1].DeSerialize(&payload) 63 | if err != nil { 64 | return fmt.Errorf("problem getting raw data. %w", err) 65 | } 66 | b := bytes.NewBuffer(payload) 67 | 68 | var out Tout 69 | _, err = Unpack(b, &out) 70 | if err != nil { 71 | return fmt.Errorf("problem unpacking data into output struct %w", err) 72 | } 73 | for i := range p.outChannels { 74 | select { 75 | case p.outChannels[i] <- out: 76 | default: 77 | return errors.New("problem sending IOWrite data. channel full?") 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (p *IOChannelProvider[Tin, Tout]) TagRead(tag string, qty int16) (any, error) { 85 | return 0, errors.New("not implemented") 86 | } 87 | 88 | func (p *IOChannelProvider[Tin, Tout]) TagWrite(tag string, value any) error { 89 | return errors.New("not implemented") 90 | } 91 | 92 | // returns the most udpated copy of the output data 93 | // this output data is what the PLC is writing to us 94 | func (p *IOChannelProvider[Tin, Tout]) GetOutputDataChannel() <-chan Tout { 95 | newout := make(chan Tout) 96 | p.outChannels = append(p.outChannels, newout) 97 | return newout 98 | } 99 | 100 | // update the input data thread safely 101 | // this input data is what the PLC receives 102 | func (p *IOChannelProvider[Tin, Tout]) SetInputData(newin Tin) { 103 | p.inMutex.Lock() 104 | defer p.inMutex.Unlock() 105 | p.in = newin 106 | } 107 | -------------------------------------------------------------------------------- /server_router.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // The Server uses PathRouter to resolve paths to tag providers 12 | // Its use is similar to a mux in an HTTP server where you add endpoints with 13 | // the .Handle() method. Instead of an http path you use a CIP route byte slice and 14 | // instead of a handler function you use an object that provides the TagProvider interface. 15 | type PathRouter struct { 16 | Path map[string]CIPEndpoint 17 | } 18 | 19 | func NewRouter() *PathRouter { 20 | p := new(PathRouter) 21 | p.Path = make(map[string]CIPEndpoint) 22 | return p 23 | } 24 | 25 | func (router *PathRouter) Handle(path []byte, p CIPEndpoint) { 26 | if router.Path == nil { 27 | router.Path = make(map[string]CIPEndpoint) 28 | } 29 | router.Path[string(path)] = p 30 | } 31 | 32 | // find the tag provider for a given cip path 33 | func (router *PathRouter) Resolve(path []byte) (CIPEndpoint, error) { 34 | tp, ok := router.Path[string(path)] 35 | if !ok { 36 | return nil, fmt.Errorf("path %v not recognized", path) 37 | } 38 | return tp, nil 39 | } 40 | 41 | // This interface specifies all the needed methods to handle incoming CIP messages. 42 | // currently supports Class1 IO messages and Class3 tag read/write messages. 43 | // if a type only handles some subset, it should return an error for those methods 44 | type CIPEndpoint interface { 45 | // These functions are called when a cip service attempts to use the write or read services. 46 | TagRead(tag string, qty int16) (any, error) 47 | TagWrite(tag string, value any) error 48 | 49 | // IORead is called every time the RPI triggers for an Input (from the PLC's perspective) IO message. 50 | // It should return the serialized bytes to send to the controller. 51 | IORead() ([]byte, error) 52 | 53 | // IOWrite is called every time a class 1 IO message comes in. The CIP items that came in with the message 54 | // are passed as arguments. You should check that you have the correct number of items (should be 2?) and 55 | // that they are the correct type. 56 | // 57 | // items[1] has the actual write payload and it should be a connectedData item. 58 | // it contains the following in the data section which you can deserialize with items[1].deserialize(xxx): 59 | // SequenceCounter uint32 60 | // Header uint16 61 | // Payload [items[1].Header.Length - 6]byte 62 | IOWrite(items []CIPItem) error 63 | } 64 | 65 | // This is a generic tag provider that can handle bi-directional class 3 tag reads and writes. 66 | // If a tag is written that does not exist, that will create it. 67 | // if a tag is read that does not exist, that will result in an error 68 | // it does not handle IO messages. 69 | // 70 | // The built-in MapTagProvider type also only provides rudimentary tag access. 71 | // It doesn't support addressing arrays directly 72 | // 73 | // It interprets "testtag[3]" as a tag with a literal "[3]" as a string at the end of the map key. 74 | // Same thing for nested UDT tags - it interprets the dots as literals in the map keys. 75 | // 76 | // So if you need those for testing you'll have to kind of fake out the tag mapping on the server end by 77 | // creating an individual entry in the map for each nested tag with the key being the full access path. 78 | type MapTagProvider struct { 79 | Mutex sync.Mutex 80 | Data map[string]any 81 | } 82 | 83 | func (p *MapTagProvider) IORead() ([]byte, error) { 84 | return nil, errors.New("not implemented") 85 | } 86 | func (p *MapTagProvider) IOWrite(items []CIPItem) error { 87 | return errors.New("not implemented") 88 | } 89 | 90 | // this is a thread-safe way to get the value for a tag. 91 | func (p *MapTagProvider) TagRead(tag string, qty int16) (any, error) { 92 | tag = strings.ToLower(tag) 93 | p.Mutex.Lock() 94 | defer p.Mutex.Unlock() 95 | if p.Data == nil { 96 | p.Data = make(map[string]any) 97 | } 98 | 99 | val, ok := p.Data[tag] 100 | if !ok { 101 | return nil, fmt.Errorf("tag %v not in map", tag) 102 | } 103 | 104 | t := reflect.ValueOf(val) 105 | 106 | if t.Kind() == reflect.Slice { 107 | if int(qty) <= t.Len() { 108 | values := reflect.Indirect(t) 109 | v := values.Slice(0, int(qty)) 110 | return v.Interface(), nil 111 | } 112 | return nil, fmt.Errorf("too many elements requested %v > %v", qty, t.Len()) 113 | } 114 | 115 | return val, nil 116 | } 117 | 118 | // this is a thread-safe way to write a value to a tag. 119 | func (p *MapTagProvider) TagWrite(tag string, value any) error { 120 | 121 | tag = strings.ToLower(tag) 122 | p.Mutex.Lock() 123 | defer p.Mutex.Unlock() 124 | if p.Data == nil { 125 | p.Data = make(map[string]any) 126 | } 127 | p.Data[tag] = value 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /tests/decode_l5x_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | "github.com/danomagnum/gologix/l5x" 10 | ) 11 | 12 | func TestDecodeL5X(t *testing.T) { 13 | var l5xData l5x.RSLogix5000Content 14 | 15 | f, err := os.Open("gologix_tests_Program.L5X") 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | b, err := io.ReadAll(f) 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | err = xml.Unmarshal(b, &l5xData) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | 29 | result, err := l5x.LoadTags(l5xData) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | t.Log(result) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/generic_cip_message_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | 8 | "github.com/danomagnum/gologix" 9 | ) 10 | 11 | // This test uses the GenericCIPMessage function to read attributes from a controller. In this case it is reading 12 | // the time object's usec since the unix epoch. 13 | func TestGenericCIPMessage1(t *testing.T) { 14 | tcs := getTestConfig() 15 | for _, tc := range tcs.TagReadWriteTests { 16 | t.Run(tc.PlcAddress, func(t *testing.T) { 17 | client := gologix.NewClient(tc.PlcAddress) 18 | err := client.Connect() 19 | if err != nil { 20 | t.Error(err) 21 | return 22 | } 23 | defer func() { 24 | err := client.Disconnect() 25 | if err != nil { 26 | t.Errorf("problem disconnecting. %v", err) 27 | } 28 | }() 29 | 30 | path, err := gologix.Serialize(gologix.CipObject_TIME, gologix.CIPInstance(1)) 31 | if err != nil { 32 | t.Errorf("could not serialize path: %v", err) 33 | return 34 | } 35 | r, err := client.GenericCIPMessage(gologix.CIPService_GetAttributeList, path.Bytes(), []byte{0x01, 0x00, 0x0B, 0x00}) 36 | if err != nil { 37 | t.Errorf("bad result: %v", err) 38 | return 39 | } 40 | type response_str struct { 41 | Attr_Count int16 42 | Attr_ID uint16 43 | Status uint16 44 | Usecs int64 // the microseconds since the unix epoch. 45 | } 46 | 47 | rs := response_str{} 48 | err = r.DeSerialize(&rs) 49 | if err != nil { 50 | t.Errorf("could not deserialize response structure: %v", err) 51 | return 52 | } 53 | 54 | log.Printf("result: us: %v / %v", rs.Usecs, time.UnixMicro(int64(rs.Usecs))) 55 | }) 56 | } 57 | } 58 | 59 | /* 60 | func TestGenericCIPMessage2(t *testing.T) { 61 | tc := getTestConfig() 62 | client := gologix.NewClient(tc.PLC_Address) 63 | err := client.Connect() 64 | if err != nil { 65 | t.Error(err) 66 | return 67 | } 68 | defer func() { 69 | err := client.Disconnect() 70 | if err != nil { 71 | t.Errorf("problem disconnecting. %v", err) 72 | } 73 | }() 74 | 75 | path, err := gologix.Serialize(gologix.CIPClass(0x04), gologix.CIPInstance(0x303), gologix.CIPAttribute(0x03)) 76 | if err != nil { 77 | t.Errorf("could not serialize path: %v", err) 78 | return 79 | } 80 | r, err := client.GenericCIPMessage(0x14, path.Bytes(), []byte{}) 81 | if err != nil { 82 | t.Errorf("bad result: %v", err) 83 | return 84 | } 85 | type response_str struct { 86 | Attr_Count int16 87 | Attr_ID uint16 88 | Status uint16 89 | Usecs int64 // the microseconds since the unix epoch. 90 | } 91 | 92 | rs := response_str{} 93 | err = r.DeSerialize(&rs) 94 | if err != nil { 95 | t.Errorf("could not deserialize response structure: %v", err) 96 | return 97 | } 98 | 99 | log.Printf("result: us: %v / %v", rs.Usecs, time.UnixMicro(int64(rs.Usecs))) 100 | 101 | } 102 | */ 103 | -------------------------------------------------------------------------------- /tests/list_identity_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | func TestListIdentity(t *testing.T) { 10 | // This test is a placeholder for the actual implementation 11 | tcs := getTestConfig() 12 | for _, tc := range tcs.GenericCIPTests { 13 | t.Run(tc.Address, func(t *testing.T) { 14 | client := gologix.NewClient(tc.Address) 15 | err := client.Connect() 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | defer func() { 21 | err := client.Disconnect() 22 | if err != nil { 23 | t.Errorf("problem disconnecting. %v", err) 24 | } 25 | }() 26 | 27 | identity, err := client.ListIdentity() 28 | if err != nil { 29 | t.Error(err) 30 | return 31 | } 32 | 33 | if identity == nil { 34 | t.Error("identity is nil") 35 | return 36 | } 37 | 38 | if identity.ProductName != tc.ProductName { 39 | t.Errorf("ProductName mismatch. Have %s. Want %s.", identity.ProductName, tc.ProductName) 40 | } 41 | 42 | version16 := uint16(tc.SoftwareVersionMinor)<<8 | uint16(tc.SoftwareVersionMajor) 43 | if identity.Revision != version16 { 44 | t.Errorf("Revision mismatch. Have %d. Want %d.", identity.Revision, version16) 45 | } 46 | 47 | if identity.SerialNumber != tc.SerialNumber { 48 | t.Errorf("SerialNumber mismatch. Have %d. Want %d.", identity.SerialNumber, tc.SerialNumber) 49 | } 50 | 51 | if identity.ProductCode != tc.ProductCode { 52 | t.Errorf("ProductCode mismatch. Have %d. Want %d.", identity.ProductCode, tc.ProductCode) 53 | } 54 | 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/list_services_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | func TestListServices(t *testing.T) { 10 | // This test is a placeholder for the actual implementation 11 | tcs := getTestConfig() 12 | for _, tc := range tcs.GenericCIPTests { 13 | t.Run(tc.Address, func(t *testing.T) { 14 | client := gologix.NewClient(tc.Address) 15 | err := client.Connect() 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | defer func() { 21 | err := client.Disconnect() 22 | if err != nil { 23 | t.Errorf("problem disconnecting. %v", err) 24 | } 25 | }() 26 | 27 | services, err := client.ListServices() 28 | if err != nil { 29 | t.Error(err) 30 | return 31 | } 32 | 33 | if services == nil { 34 | t.Error("services is nil") 35 | return 36 | } 37 | 38 | for i, service := range services { 39 | if service.Name != tc.Services[i].Name { 40 | t.Errorf("Name mismatch on entry %d. Have %s. Want %s.", i, service.Name, tc.Services[i].Name) 41 | } 42 | 43 | if service.Capabilities != gologix.ServiceCapabilityFlags(tc.Services[i].Capabilities) { 44 | t.Errorf("Capabilities mismatch on entry %d. Have %d. Want %d.", i, service.Capabilities, tc.Services[i].Capabilities) 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/listalltags_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/danomagnum/gologix" 8 | ) 9 | 10 | func TestList(t *testing.T) { 11 | 12 | tcs := getTestConfig() 13 | for _, tc := range tcs.TagReadWriteTests { 14 | t.Run(tc.PlcAddress, func(t *testing.T) { 15 | client := gologix.NewClient(tc.PlcAddress) 16 | err := client.Connect() 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | defer func() { 22 | err := client.Disconnect() 23 | if err != nil { 24 | t.Errorf("problem disconnecting. %v", err) 25 | } 26 | }() 27 | 28 | err = client.ListAllTags(0) 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | 34 | log.Printf("Tags: %+v\n", client.KnownTags["testdintarr"]) 35 | 36 | // check that we picked up all the test tags properly 37 | tests := make(map[string]gologix.KnownTag) 38 | tests["testdintarr"] = gologix.KnownTag{ 39 | Name: "TestDintArr", 40 | Info: gologix.TagInfo{ 41 | Type: gologix.CIPTypeDINT, 42 | }, 43 | Array_Order: []int{10}, 44 | } 45 | tests["testdint"] = gologix.KnownTag{ 46 | Name: "TestDint", 47 | Info: gologix.TagInfo{ 48 | Type: gologix.CIPTypeDINT, 49 | }, 50 | Array_Order: []int{}, 51 | } 52 | 53 | for k := range tests { 54 | t.Run(k, func(t *testing.T) { 55 | have := client.KnownTags[k] 56 | want := tests[k] 57 | 58 | if have.Name != want.Name { 59 | t.Errorf("Name Mismatch. Have %s. Want %s.", have.Name, want.Name) 60 | } 61 | if have.Info.Type != want.Info.Type { 62 | t.Errorf("Type Mismatch. Have %s. Want %s.", have.Info.Type, want.Info.Type) 63 | } 64 | if !compare_array_order(have.Array_Order, want.Array_Order) { 65 | t.Errorf("Array Order Mismatch. Have %v. Want %v.", have.Array_Order, want.Array_Order) 66 | 67 | } 68 | }) 69 | } 70 | 71 | for k := range client.KnownTags { 72 | if client.KnownTags[k].Name == "program:gologix_tests" { 73 | log.Printf("found %+v", client.KnownTags[k]) 74 | } 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/listmembers_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/danomagnum/gologix" 8 | ) 9 | 10 | func TestMembersList(t *testing.T) { 11 | t.Skip("controller specific test") 12 | 13 | tcs := getTestConfig() 14 | for _, tc := range tcs.TagReadWriteTests { 15 | t.Run(tc.PlcAddress, func(t *testing.T) { 16 | client := gologix.NewClient(tc.PlcAddress) 17 | err := client.Connect() 18 | if err != nil { 19 | t.Error(err) 20 | return 21 | } 22 | defer func() { 23 | err := client.Disconnect() 24 | if err != nil { 25 | t.Errorf("problem disconnecting. %v", err) 26 | } 27 | }() 28 | 29 | err = client.ListAllTags(0) 30 | 31 | if err != nil { 32 | t.Errorf("problem getting tag list: %v", err) 33 | return 34 | } 35 | 36 | kt, ok := client.KnownTags["program:gologix_tests.readudt"] 37 | if !ok { 38 | t.Errorf("TestUDT not found") 39 | return 40 | } 41 | 42 | members, err := client.ListMembers(uint32(kt.Info.Template_ID())) 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | 47 | log.Printf("Members: %+v", members) 48 | 49 | if members.Name != "TestUDT" { 50 | t.Errorf("expected name to be 'TestUDT', got '%v'", members.Name) 51 | } 52 | 53 | if len(members.Members) != 2 { 54 | t.Errorf("expected 2 members, got %v", len(members.Members)) 55 | } 56 | 57 | if members.Members[0].Name != "Field1" { 58 | t.Errorf("expected first field to be 'Field1', got '%v'", members.Members[0].Name) 59 | } 60 | 61 | if members.Members[1].Name != "Field2" { 62 | t.Errorf("expected second field to be 'Field2', got '%v'", members.Members[1].Name) 63 | } 64 | 65 | if members.Members[0].Info.Type != 0xC4 { 66 | t.Errorf("expected first field type to be 0xC4, got %v", members.Members[0].Info.Type) 67 | } 68 | 69 | if members.Members[1].Info.Type != 0xCA { 70 | t.Errorf("expected second field type to be 0xCA, got %v", members.Members[1].Info.Type) 71 | } 72 | 73 | if members.Members[0].Info.Offset != 0 { 74 | t.Errorf("expected first field offset to be 0, got %v", members.Members[0].Info.Offset) 75 | } 76 | if members.Members[1].Info.Offset != 4 { 77 | t.Errorf("expected second field offset to be 4, got %v", members.Members[1].Info.Offset) 78 | } 79 | }) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /tests/listsubtags_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | func TestSubList(t *testing.T) { 10 | 11 | tcs := getTestConfig() 12 | for _, tc := range tcs.TagReadWriteTests { 13 | t.Run(tc.PlcAddress, func(t *testing.T) { 14 | client := gologix.NewClient(tc.PlcAddress) 15 | err := client.Connect() 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | defer func() { 21 | err := client.Disconnect() 22 | if err != nil { 23 | t.Errorf("problem disconnecting. %v", err) 24 | } 25 | }() 26 | 27 | /* 28 | _, err = client.ListSubTags("Program:gologix_tests", 1) 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | */ 34 | // TODO: redo this. 35 | }) 36 | } 37 | 38 | } 39 | 40 | func compare_array_order(have, want []int) bool { 41 | if len(have) != len(want) { 42 | return false 43 | } 44 | 45 | for i := range have { 46 | if have[i] != want[i] { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | -------------------------------------------------------------------------------- /tests/multiple_conection_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | func TestMultipleConns(t *testing.T) { 10 | tcs := getTestConfig() 11 | for _, tc := range tcs.TagReadWriteTests { 12 | t.Run(tc.PlcAddress, func(t *testing.T) { 13 | client := gologix.NewClient(tc.PlcAddress) 14 | err := client.Connect() 15 | if err != nil { 16 | t.Error(err) 17 | return 18 | } 19 | defer func() { 20 | err := client.Disconnect() 21 | if err != nil { 22 | t.Errorf("problem disconnecting. %v", err) 23 | } 24 | }() 25 | 26 | client2 := gologix.NewClient(tc.PlcAddress) 27 | err = client2.Connect() 28 | if err != nil { 29 | t.Error(err) 30 | return 31 | } 32 | defer func() { 33 | err := client2.Disconnect() 34 | if err != nil { 35 | t.Errorf("problem disconnecting. %v", err) 36 | } 37 | }() 38 | 39 | tag := "Program:gologix_tests.ReadDints[0]" 40 | have := make([]int32, 5) 41 | want := []int32{4351, 4352, 4353, 4354, 4355} 42 | 43 | err = client.Read(tag, have) 44 | if err != nil { 45 | t.Errorf("Problem reading %s. %v", tag, err) 46 | return 47 | } 48 | for i := range want { 49 | if have[i] != want[i] { 50 | t.Errorf("index %d wanted %v got %v", i, want[i], have[i]) 51 | } 52 | } 53 | 54 | err = client2.Read(tag, have) 55 | if err != nil { 56 | t.Errorf("Problem reading %s. %v", tag, err) 57 | return 58 | } 59 | for i := range want { 60 | if have[i] != want[i] { 61 | t.Errorf("index %d wanted %v got %v", i, want[i], have[i]) 62 | } 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/read_list_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | // bug report (issue 8): read list fails if one of the tags is a string. 10 | func TestReadListWithString(t *testing.T) { 11 | tcs := getTestConfig() 12 | for _, tc := range tcs.TagReadWriteTests { 13 | t.Run(tc.PlcAddress, func(t *testing.T) { 14 | client := gologix.NewClient(tc.PlcAddress) 15 | err := client.Connect() 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | defer func() { 21 | err := client.Disconnect() 22 | if err != nil { 23 | t.Errorf("problem disconnecting. %v", err) 24 | } 25 | }() 26 | 27 | tags := make([]string, 5) 28 | types := make([]any, 5) 29 | elements := make([]int, 5) 30 | 31 | tags[0] = "program:gologix_tests.ReadBool" // false 32 | tags[1] = "program:gologix_tests.ReadDint" // 36 33 | tags[2] = "program:gologix_tests.ReadString" // "Somethig" 34 | tags[3] = "program:gologix_tests.ReadReal" // 93.45 35 | tags[4] = "program:gologix_tests.ReadDints[0]" // 4351 36 | 37 | types[0] = true 38 | types[1] = int32(0) 39 | types[2] = "" 40 | types[3] = float32(0) 41 | types[4] = int32(0) 42 | 43 | elements[0] = 1 44 | elements[1] = 1 45 | elements[2] = 1 46 | elements[3] = 1 47 | elements[4] = 1 48 | 49 | vals, err := client.ReadList(tags, types, elements) 50 | if err != nil { 51 | t.Errorf("shouldn't have failed but did. %v", err) 52 | return 53 | } 54 | 55 | v0, ok0 := vals[0].(bool) 56 | v1, ok1 := vals[1].(int32) 57 | v2, ok2 := vals[2].(string) 58 | v3, ok3 := vals[3].(float32) 59 | v4, ok4 := vals[4].(int32) 60 | 61 | if !ok0 || !ok1 || !ok2 || !ok3 || !ok4 { 62 | t.Errorf("A type cast failed.: %v %v %v %v %v ", ok0, ok1, ok2, ok3, ok4) 63 | } 64 | 65 | if v0 { 66 | t.Errorf("ReadBool should be false but wasn't.") 67 | } 68 | 69 | if v1 != 36 { 70 | t.Errorf("ReadDint should be 36 but was %v", v1) 71 | } 72 | 73 | if v2 != "Something" { 74 | t.Errorf("ReadString should be 'Something' but was '%v'", v2) 75 | } 76 | 77 | if v3 != 93.45 { 78 | t.Errorf("ReadReal should be 93.45 but was %v", v3) 79 | } 80 | 81 | if v4 != 4351 { 82 | t.Errorf("ReadDints[0] should be 4351 but was %v", v4) 83 | } 84 | }) 85 | } 86 | 87 | } 88 | 89 | // bug report (issue 8): read list fails if one of the tags is a string. 90 | func TestReadMultiWithString(t *testing.T) { 91 | tcs := getTestConfig() 92 | for _, tc := range tcs.TagReadWriteTests { 93 | t.Run(tc.PlcAddress, func(t *testing.T) { 94 | client := gologix.NewClient(tc.PlcAddress) 95 | err := client.Connect() 96 | if err != nil { 97 | t.Error(err) 98 | return 99 | } 100 | defer func() { 101 | err := client.Disconnect() 102 | if err != nil { 103 | t.Errorf("problem disconnecting. %v", err) 104 | } 105 | }() 106 | 107 | type customRead struct { 108 | ReadBool bool `gologix:"program:gologix_tests.ReadBool"` 109 | ReadDint int32 `gologix:"program:gologix_tests.ReadDint"` 110 | ReadString string `gologix:"program:gologix_tests.ReadString"` 111 | ReadReal float32 `gologix:"program:gologix_tests.ReadReal"` 112 | ReadDint0 int32 `gologix:"program:gologix_tests.ReadDints[0]"` 113 | } 114 | 115 | var cr customRead 116 | 117 | err = client.ReadMulti(&cr) 118 | if err != nil { 119 | t.Errorf("shouldn't have failed but did. %v", err) 120 | return 121 | } 122 | 123 | if cr.ReadBool { 124 | t.Errorf("ReadBool should be false but wasn't.") 125 | } 126 | 127 | if cr.ReadDint != 36 { 128 | t.Errorf("ReadDint should be 36 but was %v", cr.ReadDint) 129 | } 130 | 131 | if cr.ReadString != "Something" { 132 | t.Errorf("ReadString should be 'Something' but was '%v'", cr.ReadString) 133 | } 134 | 135 | if cr.ReadReal != 93.45 { 136 | t.Errorf("ReadReal should be 93.45 but was %v", cr.ReadReal) 137 | } 138 | 139 | if cr.ReadDint0 != 4351 { 140 | t.Errorf("ReadDints[0] should be 4351 but was %v", cr.ReadDint0) 141 | } 142 | }) 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /tests/readcontrol_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/danomagnum/gologix" 9 | "github.com/danomagnum/gologix/lgxtypes" 10 | ) 11 | 12 | func TestControl(t *testing.T) { 13 | var ctrl lgxtypes.CONTROL 14 | 15 | tcs := getTestConfig() 16 | for _, tc := range tcs.TagReadWriteTests { 17 | t.Run(tc.PlcAddress, func(t *testing.T) { 18 | client := gologix.NewClient(tc.PlcAddress) 19 | err := client.Connect() 20 | if err != nil { 21 | t.Error(err) 22 | return 23 | } 24 | defer func() { 25 | err := client.Disconnect() 26 | if err != nil { 27 | t.Errorf("problem disconnecting. %v", err) 28 | } 29 | }() 30 | 31 | wants := []lgxtypes.CONTROL{ 32 | {LEN: 8563, POS: 3324, EN: true, EU: false, DN: false, EM: false, ER: false, UL: false, IN: false, FD: false}, 33 | {LEN: 0, POS: 0, EN: false, EU: true, DN: false, EM: false, ER: false, UL: false, IN: false, FD: false}, 34 | {LEN: 0, POS: 0, EN: false, EU: false, DN: true, EM: false, ER: false, UL: false, IN: false, FD: false}, 35 | {LEN: 0, POS: 0, EN: false, EU: false, DN: false, EM: true, ER: false, UL: false, IN: false, FD: false}, 36 | {LEN: 0, POS: 0, EN: false, EU: false, DN: false, EM: false, ER: true, UL: false, IN: false, FD: false}, 37 | {LEN: 0, POS: 0, EN: false, EU: false, DN: false, EM: false, ER: false, UL: true, IN: false, FD: false}, 38 | {LEN: 0, POS: 0, EN: false, EU: false, DN: false, EM: false, ER: false, UL: false, IN: true, FD: false}, 39 | {LEN: 0, POS: 0, EN: false, EU: false, DN: false, EM: false, ER: false, UL: false, IN: false, FD: true}, 40 | } 41 | 42 | for i := range wants { 43 | //have, err := gologix.ReadPacked[udt2](client, "Program:gologix_tests.ReadUDT2") 44 | err = client.Read(fmt.Sprintf("Program:gologix_tests.TestControl[%d]", i), &ctrl) 45 | if err != nil { 46 | t.Errorf("problem reading %d. %v", i, err) 47 | return 48 | } 49 | 50 | compareControl(fmt.Sprintf("test %d", i), wants[i], ctrl, t) 51 | 52 | b := bytes.Buffer{} 53 | _, err = gologix.Pack(&b, ctrl) 54 | if err != nil { 55 | t.Errorf("problem packing data: %v", err) 56 | } 57 | var ctrl2 lgxtypes.CONTROL 58 | _, err = gologix.Unpack(&b, &ctrl2) 59 | if err != nil { 60 | t.Errorf("problem unpacking %d: %v", i, err) 61 | } 62 | 63 | compareControl(fmt.Sprintf("rebuild test %d", i), ctrl, ctrl2, t) 64 | } 65 | }) 66 | } 67 | 68 | } 69 | 70 | func compareControl(name string, want, have lgxtypes.CONTROL, t *testing.T) { 71 | 72 | if have.LEN != want.LEN { 73 | t.Errorf("%s LEN mismatch. Have %d want %d", name, have.LEN, want.LEN) 74 | } 75 | if have.POS != want.POS { 76 | t.Errorf("%s POS mismatch. Have %d want %d", name, have.POS, want.POS) 77 | } 78 | if have.EN != want.EN { 79 | t.Errorf("%s EN mismatch. Have %v want %v", name, have.EN, want.EN) 80 | } 81 | if have.EU != want.EU { 82 | t.Errorf("%s EU mismatch. Have %v want %v", name, have.EU, want.EU) 83 | } 84 | if have.DN != want.DN { 85 | t.Errorf("%s DN mismatch. Have %v want %v", name, have.DN, want.DN) 86 | } 87 | if have.EM != want.EM { 88 | t.Errorf("%s EM mismatch. Have %v want %v", name, have.EM, want.EM) 89 | } 90 | if have.ER != want.ER { 91 | t.Errorf("%s ER mismatch. Have %v want %v", name, have.ER, want.ER) 92 | } 93 | if have.UL != want.UL { 94 | t.Errorf("%s UL mismatch. Have %v want %v", name, have.UL, want.UL) 95 | } 96 | if have.IN != want.IN { 97 | t.Errorf("%s IN mismatch. Have %v want %v", name, have.IN, want.IN) 98 | } 99 | if have.FD != want.FD { 100 | t.Errorf("%s FD mismatch. Have %v want %v", name, have.FD, want.FD) 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /tests/readcounter_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/danomagnum/gologix" 8 | "github.com/danomagnum/gologix/lgxtypes" 9 | ) 10 | 11 | func TestCounterRead(t *testing.T) { 12 | var cnt lgxtypes.COUNTER 13 | 14 | tcs := getTestConfig() 15 | for _, tc := range tcs.TagReadWriteTests { 16 | t.Run(tc.PlcAddress, func(t *testing.T) { 17 | client := gologix.NewClient(tc.PlcAddress) 18 | err := client.Connect() 19 | if err != nil { 20 | t.Error(err) 21 | return 22 | } 23 | defer func() { 24 | err := client.Disconnect() 25 | if err != nil { 26 | t.Errorf("problem disconnecting. %v", err) 27 | } 28 | }() 29 | 30 | //have, err := gologix.ReadPacked[udt2](client, "Program:gologix_tests.ReadUDT2") 31 | err = client.Read("Program:gologix_tests.TestCounter", &cnt) 32 | 33 | if err != nil { 34 | t.Errorf("problem reading counter data: %v", err) 35 | return 36 | } 37 | 38 | const cntPre = 562855 39 | if cnt.PRE != cntPre { 40 | t.Errorf("Expected preset of %d but got %d ", cntPre, cnt.PRE) 41 | } 42 | 43 | const cntAcc = 632 44 | if cnt.ACC != cntAcc { 45 | t.Errorf("Expected ACC of %d but got %d", cntAcc, cnt.ACC) 46 | } 47 | 48 | if cnt.DN { 49 | t.Error("Expected counter !DN") 50 | } 51 | 52 | if cnt.CU { 53 | t.Error("Expected counter !CU") 54 | } 55 | 56 | if cnt.CD { 57 | t.Error("Expected counter !CD") 58 | } 59 | 60 | // make sure we can go the other way and recover it. 61 | b := bytes.Buffer{} 62 | _, err = gologix.Pack(&b, cnt) 63 | if err != nil { 64 | t.Errorf("problem packing data: %v", err) 65 | } 66 | var cnt2 lgxtypes.COUNTER 67 | _, err = gologix.Unpack(&b, &cnt2) 68 | if err != nil { 69 | t.Errorf("problem unpacking timer: %v", err) 70 | } 71 | 72 | if cnt.ACC != cnt2.ACC { 73 | t.Errorf("ACC didn't recover properly. %d != %d", cnt.ACC, cnt2.ACC) 74 | } 75 | 76 | if cnt.PRE != cnt2.PRE { 77 | t.Errorf("PRE didn't recover properly. %d != %d", cnt.PRE, cnt2.PRE) 78 | } 79 | 80 | if cnt.DN != cnt2.DN { 81 | t.Errorf("DN didn't recover properly. %v != %v", cnt.DN, cnt2.DN) 82 | } 83 | 84 | if cnt.CU != cnt2.CU { 85 | t.Errorf("CU didn't recover properly. %v != %v", cnt.CU, cnt2.CU) 86 | } 87 | 88 | if cnt.CD != cnt2.CD { 89 | t.Errorf("CD didn't recover properly. %v != %v", cnt.CD, cnt2.CD) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/readtimer_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | "time" 8 | 9 | "github.com/danomagnum/gologix" 10 | "github.com/danomagnum/gologix/lgxtypes" 11 | ) 12 | 13 | func TestTimerRead(t *testing.T) { 14 | var tmr lgxtypes.TIMER 15 | 16 | tcs := getTestConfig() 17 | for _, tc := range tcs.TagReadWriteTests { 18 | t.Run(tc.PlcAddress, func(t *testing.T) { 19 | client := gologix.NewClient(tc.PlcAddress) 20 | err := client.Connect() 21 | if err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | defer func() { 26 | err := client.Disconnect() 27 | if err != nil { 28 | t.Errorf("problem disconnecting. %v", err) 29 | } 30 | }() 31 | 32 | err = client.Write("Program:gologix_tests.trigger_Timer", false) 33 | if err != nil { 34 | t.Errorf("failed to turn off the timer before starting: %v", err) 35 | return 36 | } 37 | time.Sleep(time.Millisecond * 100) // delay to make sure the timer off bit was picked up by the program in the PLC 38 | //have, err := gologix.ReadPacked[udt2](client, "Program:gologix_tests.ReadUDT2") 39 | err = client.Read("Program:gologix_tests.TestTimer", &tmr) 40 | log.Printf("timer 1: %+v", tmr) 41 | 42 | if err != nil { 43 | t.Errorf("problem reading timer data: %v", err) 44 | return 45 | } 46 | 47 | if tmr.PRE != 2345 { 48 | t.Errorf("Expected preset of 2,345 but got %d ", tmr.PRE) 49 | } 50 | 51 | if tmr.ACC != 0 { 52 | t.Errorf("Expected ACC of 0 but got %d", tmr.ACC) 53 | } 54 | 55 | if tmr.DN { 56 | t.Error("Expected timer !DN") 57 | } 58 | 59 | if tmr.EN { 60 | t.Error("Expected timer !EN") 61 | } 62 | 63 | if tmr.TT { 64 | t.Error("Expected timer !TT") 65 | } 66 | 67 | err = client.Write("Program:gologix_tests.trigger_Timer", true) 68 | if err != nil { 69 | t.Errorf("problem starting timer: %v", err) 70 | } 71 | 72 | // the task this timer lives in is set to a 50 ms rate so we should expect 73 | // that after 500 ms we should be between 449 and 551 ms elapsed on the timer 74 | // (accounting for minimal networking latency) 75 | time.Sleep(time.Millisecond * 500) 76 | 77 | err = client.Read("Program:gologix_tests.TestTimer", &tmr) 78 | log.Printf("timer 2: %+v", tmr) 79 | if err != nil { 80 | t.Errorf("problem reading timer data: %v", err) 81 | return 82 | } 83 | 84 | if tmr.PRE != 2345 { 85 | t.Errorf("Expected preset of 2,345 but got %d ", tmr.PRE) 86 | } 87 | 88 | if tmr.ACC < 449 || tmr.ACC > 551 { 89 | t.Errorf("Expected ACC between 449 and 551 but got %d", tmr.ACC) 90 | } 91 | 92 | if tmr.DN { 93 | t.Error("Expected timer !DN") 94 | } 95 | err = client.Write("Program:gologix_tests.trigger_Timer", true) 96 | if err != nil { 97 | t.Errorf("problem starting timer: %v", err) 98 | } 99 | 100 | time.Sleep(time.Millisecond * 2000) 101 | 102 | err = client.Read("Program:gologix_tests.TestTimer", &tmr) 103 | log.Printf("timer 3: %+v", tmr) 104 | if err != nil { 105 | t.Errorf("problem reading timer data: %v", err) 106 | return 107 | } 108 | 109 | if tmr.PRE != 2345 { 110 | t.Errorf("Expected preset of 2,345 but got %d ", tmr.PRE) 111 | } 112 | 113 | if tmr.ACC < 2345 { 114 | t.Errorf("Expected ACC at least 2,345 but got %d", tmr.ACC) 115 | } 116 | 117 | if !tmr.DN { 118 | t.Error("Expected timer DN") 119 | } 120 | 121 | if !tmr.EN { 122 | t.Error("Expected timer EN") 123 | } 124 | 125 | if tmr.TT { 126 | t.Error("Expected timer !TT") 127 | } 128 | 129 | err = client.Write("Program:gologix_tests.trigger_Timer", false) 130 | if err != nil { 131 | t.Errorf("problem resetting timer: %v", err) 132 | } 133 | 134 | // make sure we can go the other way and recover it. 135 | b := bytes.Buffer{} 136 | _, err = gologix.Pack(&b, tmr) 137 | if err != nil { 138 | t.Errorf("problem packing data: %v", err) 139 | } 140 | var tmr2 lgxtypes.TIMER 141 | _, err = gologix.Unpack(&b, &tmr2) 142 | if err != nil { 143 | t.Errorf("problem unpacking timer: %v", err) 144 | } 145 | 146 | if tmr.ACC != tmr2.ACC { 147 | t.Errorf("ACC didn't recover properly. %d != %d", tmr.ACC, tmr2.ACC) 148 | } 149 | 150 | if tmr.PRE != tmr2.PRE { 151 | t.Errorf("PRE didn't recover properly. %d != %d", tmr.PRE, tmr2.PRE) 152 | } 153 | 154 | if tmr.DN != tmr2.DN { 155 | t.Errorf("DN didn't recover properly. %v != %v", tmr.DN, tmr2.DN) 156 | } 157 | 158 | if tmr.TT != tmr2.TT { 159 | t.Errorf("TT didn't recover properly. %v != %v", tmr.TT, tmr2.TT) 160 | } 161 | 162 | if tmr.EN != tmr2.EN { 163 | t.Errorf("EN didn't recover properly. %v != %v", tmr.EN, tmr2.EN) 164 | } 165 | }) 166 | } 167 | } 168 | 169 | func TestTimerStructRead(t *testing.T) { 170 | 171 | tcs := getTestConfig() 172 | for _, tc := range tcs.TagReadWriteTests { 173 | t.Run(tc.PlcAddress, func(t *testing.T) { 174 | client := gologix.NewClient(tc.PlcAddress) 175 | err := client.Connect() 176 | if err != nil { 177 | t.Error(err) 178 | return 179 | } 180 | defer func() { 181 | err := client.Disconnect() 182 | if err != nil { 183 | t.Errorf("problem disconnecting. %v", err) 184 | } 185 | }() 186 | 187 | x := struct { 188 | Field0 int32 189 | Flag1 bool 190 | Flag2 bool 191 | Timer lgxtypes.TIMER 192 | Field1 int32 193 | }{} 194 | 195 | //have, err := gologix.ReadPacked[udt2](client, "Program:gologix_tests.ReadUDT2") 196 | err = client.Read("Program:gologix_tests.TestTimerStruct", &x) 197 | if err != nil { 198 | t.Errorf("problem reading timer data: %v", err) 199 | return 200 | } 201 | 202 | if x.Timer.PRE != 8765 { 203 | t.Errorf("Expected preset of 8765 but got %d ", x.Timer.PRE) 204 | } 205 | 206 | if x.Field0 != 44444 { 207 | t.Errorf("Expected field0 of 44444 but got %d", x.Field0) 208 | } 209 | 210 | if x.Field1 != 55555 { 211 | t.Errorf("Expected field1 of 55555 but got %d", x.Field1) 212 | } 213 | }) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tests/realhardware_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danomagnum/gologix" 9 | ) 10 | 11 | func TestRealHardware(t *testing.T) { 12 | flag.Parse() 13 | tcs := getTestConfig() 14 | for _, tc := range tcs.TagReadWriteTests { 15 | t.Run(tc.PlcAddress, func(t *testing.T) { 16 | client := gologix.NewClient(tc.PlcAddress) 17 | err := client.Connect() 18 | if err != nil { 19 | t.Error(err) 20 | return 21 | } 22 | defer func() { 23 | err := client.Disconnect() 24 | if err != nil { 25 | t.Errorf("problem disconnecting. %v", err) 26 | } 27 | }() 28 | //client.ReadAll(1) 29 | //client.read_single("program:Shed.Temp1", CIPTypeREAL, 1) 30 | //ReadAndPrint[float32](client, "program:Shed.Temp1") 31 | read[int32](t, client, "program:gologix_tests.ReadDint") // 36 32 | 33 | // these two tests don't work yet 34 | read[bool](t, client, "program:gologix_tests.ReadDint.0") // should be false 35 | read[bool](t, client, "program:gologix_tests.ReadDint.2") // should be true 36 | 37 | read[int32](t, client, "program:gologix_tests.ReadDints[0]") 38 | read[int32](t, client, "program:gologix_tests.ReadDints[2]") 39 | read[int32](t, client, "program:gologix_tests.ReadUDT.Field1") 40 | read[int16](t, client, "program:gologix_tests.ReadInt") 41 | //v, err := client.read_single("Program:gologix_tests.ReadDintArr[1]", CIPTypeDINT, 2) 42 | //if err != nil { 43 | //log.Printf("Problem with reading two elements of array. %v\n", err) 44 | //} else { 45 | //log.Printf("two element value: %v\n", v) 46 | //} 47 | 48 | /* 49 | v2, err := readArray[int32](client, "Program:gologix_tests.ReadDintArr", 9) 50 | if err != nil { 51 | t.Errorf("Problem with reading two elements of array. %v\n", err) 52 | } 53 | for i := range v2 { 54 | want := int32(4351 + i) 55 | if v2[i] != want { 56 | t.Errorf("Problem with reading nine elements of array. got %v want %v\n", v2[i], want) 57 | } 58 | } 59 | */ 60 | /* 61 | v3, err := readArray[TestUDT](client, "Program:gologix_tests.ReadUDTArr[2]", 2) 62 | if err != nil { 63 | t.Errorf("Problem with reading two elements of array. %v\n", err) 64 | } 65 | want := TestUDT{16, 15.0} 66 | if v3[0] != want { 67 | t.Errorf("problem reading two elements of array. got %v want %v", v3[0], want) 68 | } 69 | want = TestUDT{14, 13.0} 70 | if v3[1] != want { 71 | t.Errorf("problem reading two elements of array. got %v want %v", v3[1], want) 72 | } 73 | test_strarr := false 74 | // string array read is untested: 75 | if test_strarr { 76 | v4, err := readArray[string](client, "Program:gologix_tests.ReadStrArr", 3) 77 | if err != nil { 78 | t.Errorf("Problem with reading two elements of array. %v\n", err) 79 | } else { 80 | t.Logf("two element value new method: %v\n", v4) 81 | } 82 | } 83 | */ 84 | 85 | //ReadAndPrint[bool](client, "Program:gologix_tests.ReadBool") 86 | //ReadAndPrint[float32](client, "Program:gologix_tests.ReadReal") 87 | read[string](t, client, "program:gologix_tests.ReadString") 88 | 89 | var ut TestUDT 90 | err = client.Read("program:gologix_tests.ReadUDT", &ut) 91 | if err != nil { 92 | t.Errorf("Problem reading udt. %v\n", err) 93 | } 94 | 95 | //tags := []string{"Program:gologix_tests.ReadInt", "TestReal"} 96 | tags := MultiReadStr{} 97 | 98 | err = client.ReadMulti(&tags) 99 | if err != nil { 100 | t.Errorf("Error reading multi. %v\n", err) 101 | } 102 | }) 103 | } 104 | 105 | } 106 | 107 | func read[T gologix.GoLogixTypes](t *testing.T, client *gologix.Client, path string) { 108 | var have T 109 | err := client.Read(path, &have) 110 | if err != nil { 111 | t.Errorf("Problem reading %s. %v", path, err) 112 | } 113 | //t.Logf("%s: %v", path, value) 114 | } 115 | 116 | type MultiReadStr struct { 117 | TI int16 `gologix:"Program:gologix_tests.ReadInt"` 118 | TD int32 `gologix:"Program:gologix_tests.ReadDint"` 119 | TR float32 `gologix:"Program:gologix_tests.ReadReal"` 120 | } 121 | 122 | type TestUDT struct { 123 | Field1 int32 124 | Field2 float32 125 | } 126 | 127 | func TestReadKnown(t *testing.T) { 128 | 129 | tcs := getTestConfig() 130 | for _, tc := range tcs.TagReadWriteTests { 131 | t.Run(tc.PlcAddress, func(t *testing.T) { 132 | client := gologix.NewClient(tc.PlcAddress) 133 | err := client.Connect() 134 | if err != nil { 135 | t.Error(err) 136 | return 137 | } 138 | defer func() { 139 | err := client.Disconnect() 140 | if err != nil { 141 | t.Errorf("problem disconnecting. %v", err) 142 | } 143 | }() 144 | 145 | err = client.ListAllTags(0) 146 | if err != nil { 147 | t.Error(err) 148 | return 149 | } 150 | 151 | log.Printf("Tags: %+v\n", client.KnownTags["program:gologix_tests.readint"]) 152 | 153 | v := int16(0) 154 | err = client.Read("Program:gologix_tests.ReadInt", &v) 155 | 156 | if err != nil { 157 | t.Errorf("problem reading. %v", err) 158 | } 159 | 160 | if v != 999 { 161 | t.Errorf("problem reading 'TestInt'. got %v want %v", v, 999) 162 | 163 | } 164 | }) 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /tests/reconnection_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | func TestReconnection(t *testing.T) { 10 | tcs := getTestConfig() 11 | for _, tc := range tcs.TagReadWriteTests { 12 | t.Run(tc.PlcAddress, func(t *testing.T) { 13 | client := gologix.NewClient(tc.PlcAddress) 14 | client.KeepAliveAutoStart = false 15 | err := client.Connect() 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | defer func() { 21 | err := client.Disconnect() 22 | if err != nil { 23 | t.Errorf("problem disconnecting. %v", err) 24 | } 25 | }() 26 | tag := "Program:gologix_tests.ReadDints[0]" 27 | have := make([]int32, 5) 28 | want := []int32{4351, 4352, 4353, 4354, 4355} 29 | 30 | // first we'll do a read which should succeed 31 | err = client.Read(tag, have) 32 | if err != nil { 33 | t.Errorf("Problem reading %s. %v", tag, err) 34 | return 35 | } 36 | for i := range want { 37 | if have[i] != want[i] { 38 | t.Errorf("index %d wanted %v got %v", i, want[i], have[i]) 39 | } 40 | } 41 | 42 | // then we'll close the socket. 43 | client.DebugCloseConn() 44 | 45 | // then read again. This should fail, but cause a "proper" disconnect. 46 | err = client.Read(tag, have) 47 | if err == nil { 48 | t.Errorf("read should have failed but didn't.") 49 | return 50 | } 51 | 52 | // now read should work again because AutoConnect = true on the client. 53 | err = client.Read(tag, have) 54 | if err != nil { 55 | t.Errorf("Problem reading after reconnect %s. %v", tag, err) 56 | return 57 | } 58 | for i := range want { 59 | if have[i] != want[i] { 60 | t.Errorf("index %d wanted %v got %v", i, want[i], have[i]) 61 | } 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestNoReconnection(t *testing.T) { 68 | tcs := getTestConfig() 69 | for _, tc := range tcs.TagReadWriteTests { 70 | t.Run(tc.PlcAddress, func(t *testing.T) { 71 | client := gologix.NewClient(tc.PlcAddress) 72 | client.AutoConnect = false 73 | err := client.Connect() 74 | if err != nil { 75 | t.Error(err) 76 | return 77 | } 78 | defer func() { 79 | err := client.Disconnect() 80 | if err != nil { 81 | t.Errorf("problem disconnecting. %v", err) 82 | } 83 | }() 84 | tag := "Program:gologix_tests.ReadDints[0]" 85 | have := make([]int32, 5) 86 | want := []int32{4351, 4352, 4353, 4354, 4355} 87 | 88 | // first we'll do a read which should succeed 89 | err = client.Read(tag, have) 90 | if err != nil { 91 | t.Errorf("Problem reading %s. %v", tag, err) 92 | return 93 | } 94 | for i := range want { 95 | if have[i] != want[i] { 96 | t.Errorf("index %d wanted %v got %v", i, want[i], have[i]) 97 | } 98 | } 99 | 100 | // then we'll close the socket. 101 | client.DebugCloseConn() 102 | 103 | // then read again. This should fail, but cause a "proper" disconnect. 104 | err = client.Read(tag, have) 105 | if err == nil { 106 | t.Errorf("read should have failed but didn't.") 107 | return 108 | } 109 | 110 | // now read should not work again because AutoConnect = false on the client. 111 | err = client.Read(tag, have) 112 | if err == nil { 113 | t.Errorf("read should have failed again but didn't.") 114 | return 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/test_config template.json: -------------------------------------------------------------------------------- 1 | { 2 | "README_0": "rename this file to 'test_config.json' and set up the parameters to match your setup.", 3 | "README_1a": "you can add as many PLCs as you want to the list. The program will cycle through them and test each one against all the tests.", 4 | "README_1b": "TagReadWriteTests contains the tests for reading and writing tags. These should define known PLCs that have the ", 5 | "README_1c": "gologix program imported into them.", 6 | "TagReadWriteTests": [ 7 | { 8 | "PLC_Address": "192.168.2.241", 9 | "ProductCode": 65, 10 | "SoftwareVersionMajor": 20, 11 | "SoftwareVersionMinor": 19, 12 | "SerialNumber": 1615551361, 13 | "ProductName": "1769-L35E/B LOGIX5335E" 14 | } 15 | ], 16 | "README_2a": "The GenericCIPTests is used to identify the devices on the network to run a series of CIP tests against..", 17 | "README_2b": "The program will cycle through them and test each one.", 18 | "GenericCIPTests": [ 19 | { 20 | "Device_Address": "192.168.2.241", 21 | "ProductCode": 65, 22 | "SoftwareVersionMajor": 20, 23 | "SoftwareVersionMinor": 19, 24 | "SerialNumber": 1615551361, 25 | "ProductName": "1769-L35E/B LOGIX5335E", 26 | "Services": [ 27 | { 28 | "Name": "Communications", 29 | "Capabilities": 288 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tests/test_setup.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | type TestConfig struct { 10 | TagReadWriteTests []struct { 11 | PlcAddress string `json:"PLC_Address"` 12 | ProductCode uint16 `json:"ProductCode"` 13 | SoftwareVersionMajor byte `json:"SoftwareVersionMajor"` 14 | SoftwareVersionMinor byte `json:"SoftwareVersionMinor"` 15 | SerialNumber uint32 `json:"SerialNumber"` 16 | ProductName string `json:"ProductName"` 17 | } `json:"TagReadWriteTests"` 18 | 19 | GenericCIPTests []struct { 20 | Address string `json:"Device_Address"` 21 | Vendor uint16 `json:"Vendor"` 22 | DeviceType uint16 `json:"DeviceType"` 23 | ProductCode uint16 `json:"ProductCode"` 24 | SoftwareVersionMajor uint16 `json:"SoftwareVersionMajor"` 25 | SoftwareVersionMinor uint16 `json:"SoftwareVersionMinor"` 26 | Status uint16 `json:"Status"` 27 | SerialNumber uint32 `json:"SerialNumber"` 28 | ProductName string `json:"ProductName"` 29 | State uint8 `json:"State"` 30 | Services []struct { 31 | Name string `json:"Name"` 32 | Capabilities uint16 `json:"Capabilities"` 33 | } `json:"Services"` 34 | } `json:"GenericCIPTests"` 35 | } 36 | 37 | func getTestConfig() TestConfig { 38 | 39 | f, err := os.Open("test_config.json") 40 | if err != nil { 41 | panic(fmt.Sprintf("couldn't open file: %v", err)) 42 | } 43 | defer f.Close() 44 | dec := json.NewDecoder(f) 45 | 46 | var tc TestConfig 47 | 48 | dec.Decode(&tc) 49 | 50 | return tc 51 | } 52 | -------------------------------------------------------------------------------- /tests/write_multi_test.go: -------------------------------------------------------------------------------- 1 | package gologix_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danomagnum/gologix" 7 | ) 8 | 9 | func TestWriteMulti(t *testing.T) { 10 | tcs := getTestConfig() 11 | for _, tc := range tcs.TagReadWriteTests { 12 | t.Run(tc.PlcAddress, func(t *testing.T) { 13 | client := gologix.NewClient(tc.PlcAddress) 14 | err := client.Connect() 15 | if err != nil { 16 | t.Error(err) 17 | return 18 | } 19 | defer func() { 20 | err := client.Disconnect() 21 | if err != nil { 22 | t.Errorf("problem disconnecting. %v", err) 23 | } 24 | }() 25 | 26 | want := TestUDT{Field1: 215, Field2: 25.1} 27 | 28 | err = client.Write("Program:gologix_tests.WriteUDTs[2]", want) 29 | if err != nil { 30 | t.Errorf("error writing. %v", err) 31 | } 32 | 33 | have := TestUDT{} 34 | err = client.Read("Program:gologix_tests.WriteUDTs[2]", &have) 35 | if err != nil { 36 | t.Errorf("error reading. %v", err) 37 | } 38 | 39 | if have != want { 40 | t.Errorf("have %v. Want %v", have, want) 41 | 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /udt_write.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "reflect" 8 | ) 9 | 10 | // convert tagged struct to a map in the format of {"fieldTag": fieldvalue} 11 | func multi_to_dict(data any) (map[string]interface{}, error) { 12 | //TODO: handle nested structs and arrays 13 | // convert the struct to a dict of FieldName: FieldValue 14 | d := make(map[string]interface{}) 15 | 16 | vs := reflect.ValueOf(data) 17 | fs := reflect.TypeOf(data) 18 | for i := 0; i < vs.NumField(); i++ { 19 | v := vs.Field(i) 20 | f := fs.Field(i) 21 | fulltag := f.Tag.Get("gologix") 22 | switch v.Kind() { 23 | case reflect.Struct: 24 | d2, err := udt_to_dict(fulltag, v.Interface()) 25 | if err != nil { 26 | return nil, fmt.Errorf("problem parsing %s. %w", fulltag, err) 27 | } 28 | for k := range d2 { 29 | d[k] = d2[k] 30 | } 31 | case reflect.Array: 32 | //TODO 33 | default: 34 | d[fulltag] = v.Interface() 35 | } 36 | 37 | } 38 | 39 | return d, nil 40 | 41 | } 42 | 43 | // convert a struct to a map in the format of {"tag.fieldName": fieldvalue} 44 | func udt_to_dict(tag string, data any) (map[string]interface{}, error) { 45 | //TODO: handle nested structs and arrays 46 | // convert the struct to a dict of FieldName: FieldValue 47 | d := make(map[string]interface{}) 48 | 49 | vs := reflect.ValueOf(data) 50 | fs := reflect.TypeOf(data) 51 | for i := 0; i < vs.NumField(); i++ { 52 | v := vs.Field(i) 53 | f := fs.Field(i) 54 | fulltag := fmt.Sprintf("%s.%s", tag, f.Name) 55 | switch v.Kind() { 56 | 57 | } 58 | switch v.Kind() { 59 | case reflect.Struct: 60 | d2, err := udt_to_dict(fulltag, v.Interface()) 61 | if err != nil { 62 | return nil, fmt.Errorf("problem parsing %s. %w", fulltag, err) 63 | } 64 | for k := range d2 { 65 | d[k] = d2[k] 66 | } 67 | case reflect.Array: 68 | //TODO 69 | default: 70 | d[fulltag] = v.Interface() 71 | } 72 | 73 | } 74 | 75 | return d, nil 76 | 77 | } 78 | 79 | // Write multiple tags at once where the tagnames are the keys of a map and the values are the corresponding 80 | // values. 81 | // 82 | // To write multiple tags with a struct, see WriteMulti() 83 | func (client *Client) WriteMap(tag_str map[string]interface{}) error { 84 | 85 | // build the tag list from the structure 86 | tags := make([]string, 0) 87 | types := make([]CIPType, 0) 88 | for k := range tag_str { 89 | ct, _ := GoVarToCIPType(tag_str[k]) 90 | types = append(types, ct) 91 | tags = append(tags, k) 92 | } 93 | 94 | // first generate IOIs for each tag 95 | qty := len(tags) 96 | iois := make([]*tagIOI, qty) 97 | for i, tag := range tags { 98 | var err error 99 | iois[i], err = client.newIOI(tag, types[i]) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | 105 | reqitems := make([]CIPItem, 2) 106 | reqitems[0] = newItem(cipItem_ConnectionAddress, &client.OTNetworkConnectionID) 107 | 108 | ioi_header := msgCIPConnectedMultiServiceReq{ 109 | Sequence: uint16(sequencer()), 110 | Service: CIPService_MultipleService, 111 | PathSize: 2, 112 | Path: [4]byte{0x20, 0x02, 0x24, 0x01}, 113 | ServiceCount: uint16(qty), 114 | } 115 | 116 | b := bytes.Buffer{} 117 | // we now have to build up the jump table for each IOI. 118 | // and pack all the IOIs together into b 119 | jump_table := make([]uint16, qty) 120 | jump_start := 2 + qty*2 // 2 bytes + 2 bytes per jump entry 121 | for i := 0; i < qty; i++ { 122 | jump_table[i] = uint16(jump_start + b.Len()) 123 | ioi := iois[i] 124 | h := msgCIPMultiIOIHeader{ 125 | Service: CIPService_Write, 126 | Size: byte(len(ioi.Buffer) / 2), 127 | } 128 | f := msgCIPWriteIOIFooter{ 129 | DataType: uint16(types[i]), 130 | Elements: 1, 131 | } 132 | //f := msgCIPIOIFooter{ 133 | //Elements: 1, 134 | //} 135 | err := binary.Write(&b, binary.LittleEndian, h) 136 | if err != nil { 137 | return fmt.Errorf("problem writing udt item header to buffer. %w", err) 138 | } 139 | b.Write(ioi.Buffer) 140 | ftr_buf, err := Serialize(f) 141 | if err != nil { 142 | return fmt.Errorf("problem serializing footer for item %d: %w", i, err) 143 | } 144 | err = binary.Write(&b, binary.LittleEndian, ftr_buf.Bytes()) 145 | if err != nil { 146 | return fmt.Errorf("problem writing udt item footer to buffer. %w", err) 147 | } 148 | item_buf, err := Serialize(tag_str[tags[i]]) 149 | if err != nil { 150 | return fmt.Errorf("problem serializing %v: %w", tags[i], err) 151 | } 152 | err = binary.Write(&b, binary.LittleEndian, item_buf.Bytes()) 153 | if err != nil { 154 | return fmt.Errorf("problem writing udt tag name to buffer. %w", err) 155 | } 156 | } 157 | 158 | // right now I'm putting the IOI data into the cip Item, but I suspect it might actually be that the readsequencer is 159 | // the item's data and the service code actually starts the next portion of the message. But the item's header length reflects 160 | // the total data so maybe not. 161 | reqitems[1] = CIPItem{Header: cipItemHeader{ID: cipItem_ConnectedData}} 162 | err := reqitems[1].Serialize(ioi_header, jump_table, &b) 163 | if err != nil { 164 | return fmt.Errorf("problem serializing item header: %w", err) 165 | } 166 | 167 | itemdata, err := serializeItems(reqitems) 168 | if err != nil { 169 | return err 170 | } 171 | hdr, data, err := client.send_recv_data(cipCommandSendUnitData, itemdata) 172 | if err != nil { 173 | return err 174 | } 175 | _ = hdr 176 | _ = data 177 | //TODO: do something with the result here! 178 | 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /udt_write_test.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestMultiWriteToDict(t *testing.T) { 9 | 10 | type TestUDT struct { 11 | Field1 int32 12 | Field2 float32 13 | } 14 | 15 | type test_str struct { 16 | TestSint byte `gologix:"TestSint"` 17 | TestInt int16 `gologix:"TestInt"` 18 | TestDint int32 `gologix:"TestDint"` 19 | TestReal float32 `gologix:"TestReal"` 20 | TestDintArr0 int32 `gologix:"testdintarr[0]"` 21 | TestDintArr0_0 bool `gologix:"testdintarr[0].0"` 22 | TestDintArr0_9 bool `gologix:"testdintarr[0].9"` 23 | TestDintArr2 int32 `gologix:"testdintarr[2]"` 24 | TestUDTField1 int32 `gologix:"testudt.field1"` 25 | TestUDTField2 float32 `gologix:"testudt.field2"` 26 | TestUDTArr2Field1 int32 `gologix:"testudtarr[2].field1"` 27 | TestUDTArr2Field2 float32 `gologix:"testudtarr[2].field2"` 28 | } 29 | 30 | type test_str2 struct { 31 | TestSint byte `gologix:"TestSint"` 32 | TestInt int16 `gologix:"TestInt"` 33 | TestDint int32 `gologix:"TestDint"` 34 | TestReal float32 `gologix:"TestReal"` 35 | TestDintArr0 int32 `gologix:"testdintarr[0]"` 36 | TestDintArr0_0 bool `gologix:"testdintarr[0].0"` 37 | TestDintArr0_9 bool `gologix:"testdintarr[0].9"` 38 | TestDintArr2 int32 `gologix:"testdintarr[2]"` 39 | TestUDT TestUDT `gologix:"testudt"` 40 | TestUDTArr2Field1 int32 `gologix:"testudtarr[2].field1"` 41 | TestUDTArr2Field2 float32 `gologix:"testudtarr[2].field2"` 42 | } 43 | 44 | read := test_str{ 45 | TestSint: 117, 46 | TestInt: 999, 47 | TestDint: 36, 48 | TestReal: 93.45, 49 | TestDintArr0: 4351, 50 | TestDintArr0_0: true, 51 | TestDintArr0_9: false, 52 | TestDintArr2: 4353, 53 | TestUDTField1: 85456, 54 | TestUDTField2: 123.456, 55 | TestUDTArr2Field1: 16, 56 | TestUDTArr2Field2: 15.0, 57 | } 58 | read2 := test_str2{ 59 | TestSint: 117, 60 | TestInt: 999, 61 | TestDint: 36, 62 | TestReal: 93.45, 63 | TestDintArr0: 4351, 64 | TestDintArr0_0: true, 65 | TestDintArr0_9: false, 66 | TestDintArr2: 4353, 67 | TestUDT: TestUDT{85456, 123.456}, 68 | TestUDTArr2Field1: 16, 69 | TestUDTArr2Field2: 15.0, 70 | } 71 | wants := map[string]interface{}{ 72 | "testsint": byte(117), 73 | "testint": int16(999), 74 | "testdint": int32(36), 75 | "testreal": float32(93.45), 76 | "testdintarr[0]": int32(4351), 77 | "testdintarr[0].0": true, 78 | "testdintarr[0].9": false, 79 | "testdintarr[2]": int32(4353), 80 | "testudt.field1": int32(85456), 81 | "testudt.field2": float32(123.456), 82 | "testudtarr[2].field1": int32(16), 83 | "testudtarr[2].field2": float32(15.0), 84 | } 85 | 86 | have, err := multi_to_dict(read) 87 | if err != nil { 88 | t.Errorf("problem creating dict. %v", err) 89 | } 90 | for k := range have { 91 | if wants[strings.ToLower(k)] != have[k] { 92 | t.Errorf("key %s is not a match. Have %v want %v", k, have[k], wants[k]) 93 | } 94 | 95 | } 96 | 97 | have, err = multi_to_dict(read2) 98 | if err != nil { 99 | t.Errorf("problem creating dict 2. %v", err) 100 | } 101 | for k := range have { 102 | if wants[strings.ToLower(k)] != have[k] { 103 | t.Errorf("key %s is not a match. Have %v want %v", k, have[k], wants[k]) 104 | } 105 | 106 | } 107 | 108 | } 109 | 110 | func TestStructToDict(t *testing.T) { 111 | 112 | type TestUDT struct { 113 | Field1 int32 114 | Field2 float32 115 | } 116 | 117 | udt := TestUDT{Field1: 15, Field2: 5.1} 118 | d, err := udt_to_dict("prefix", udt) 119 | if err != nil { 120 | t.Errorf("problem creating dict. %v", err) 121 | } 122 | want := map[string]interface{}{ 123 | "prefix.Field1": int32(15), 124 | "prefix.Field2": float32(5.1), 125 | } 126 | 127 | for k := range d { 128 | if d[k] != want[k] { 129 | t.Errorf("want %v got %v", want[k], d[k]) 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /vendors_test.go: -------------------------------------------------------------------------------- 1 | package gologix 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestKnownVendorName(t *testing.T) { 8 | vendor := VendorId(1) 9 | name := vendor.Name() 10 | if name != "Rockwell Automation/Allen-Bradley" { 11 | t.Errorf("expected 'Rockwell Automation/Allen-Bradley', got '%s'", name) 12 | } 13 | } 14 | 15 | func TestUnknownVendorName(t *testing.T) { 16 | vendor := VendorId(9999) 17 | name := vendor.Name() 18 | if name != "Reserved" { 19 | t.Errorf("expected 'Reserved', got '%s'", name) 20 | } 21 | } 22 | --------------------------------------------------------------------------------