Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Gliffy Diagram
nameApi Change - Chris Copy
pagePin319

General

  • I ensured all of our classes have the "Plc" prefix, as I did encounter situations in which very generic type names had collisions and I had to use the fully qualified class name (including the package name). The "Plc" prefix reduces this risk.This model is now simplified as it currently doesn't support reading of multi-values. There are multiple options to add Multi-Values, but I haven't decided on which option I like best:
  • Introduced a PlcFieldRequest / Response classes, allowing PlcRequests and Responses that don't use field information (When calling functions, for example)
  • Removed the "RequestItems". Now the data for the plc read responses is contained in the PlcReadResponse/PlcWriteRequest itself.
  • Added the concept of a named-field which is in general a Field description combined with a string alias.
  • All sub-types of the PlcFieldRequest/Response use these aliases to reference the field they want to access. Resulting in an API that is more similar to JDBCs RecordSet
  • Rename PlcRequest/ResponseItem to PlcSingleValueRequest/ResponseItem and add a Multi-Value counterpart, wich handles the multi-value.
  • Add a PlcMultiValueRequest/ResponseItem, which simply references multiple PlcRequest/ResponseItem objects and provides a "get(index)" method.
  • Add a getNumValues() (Not insisting on this name) method and give each of the accessors an "index" property (eventually additionally a no-args version which defaults to index 0) (I think I prefer this one but didn't want to blow up the class-diagram too much).

PlcField (Aka Address)

  • I Renamed Address to PlcField
  • I still think there is no immediate need to define any fields or methods in this interface as I haven't seen any common ground yet. Even if we are currently adding "TYPE" information to most fields, in Modbus the type is directly tied to the address you are reading. So If you are reading a "Register" or a "Coil", the datatype is fixed.

PlcFieldSet

PlcNamedField

  • This class is only used for constructing the PlcFieldRequest objects as it allows passing in pairs of name and field into the PlcFieldRequests constructor
  • New Class building a unity of named fields.
  • It's main reason is to wrap all the information needed to clone PlcRequests easily. While it's no problem at all to reuse PlcReadRequests, reusing PlcWriteRequests would be more tricky without this.

PlcConnection

  • I removed the "getLister" method as I haven't seen any protocol or use-case where this actually made sense.

  • I added a getFunctionExecutor method that should return a component for executing functions on PLCs that support this.
  • I changed the "parseAddress" to "prepareField". This abstract method has to be implemented by every Driver and it handles parsing of the field description for each protocol. Some times I could imagine, that more work is involved than simple parsing of a string (For Beckhoff ADS and EtherNet/IP this method could actively connect to the corresponding resource so in future read operations only the connection-id has to be provided)

PlcRequest & PlcResponse

  • As mentioned before, I added a "prepareQuery" field which sort of correlates to the "prepareStatement" method used in JDBC. It is used to parse a query string and produce a corresponding PlcRequest object. 
    • A query like this (For S7): "SELECT '%DB8.DBX3:INT' AS parameterX, '%DB9.DBW4:DOUBLE' AS parameterY, '%DB2.DBW2:BYTE[10]' AS others" would return a PlcReadRequest Object with initialized PlcReadRequestItems. 
    • A query like this (For S7): "SELECT '%DB8.DBX3:INT' AS parameterX, '%DB9.DBW4:DOUBLE' AS parameterY, '%DB2.DBW2:BYTE[10]' AS others WITH INTERVAL 1000" would return a PlcSubscriptionRequest Object with initialized PlcSubscriptionRequestItems. 
    • A query like this (For S7): "UPDATE '%DB8.DBX3:INT' = :parameterX, '%DB9.DBW4:DOUBLE' = :parameterY, '%DB2.DBW2:BYTE[10]' = :others" would return a PlcWriteRequest Object with initialized PlcWriteRequestItems. 
    • ... (Actual format should be discussed).
    • The parser for this would be implemented as Antlr4 Parser used in the base PlcConnection class and is inherited by the concrete driver implementations. The driver dependent part is the parsing of the field itself, but this could be handled by the abstract prepareField method (This should actually be quite easy for me to implement).

PlcRequest & PlcResponse

  • I changed the List for referencing the items with a Map, so we can directly reference items.
  • I removed the "single-item" convenience methods.
  • I added getItem(name) methods for directly referencing a given item.
  • I am not that happy with the WriteRequests: Currently you can create one ReadRequest instance and reuse it as often as you like, however when writing, a new object has to be created every time. 

PlcRequestItem & PlcResponseItem

  • introduced a new Level PlcFieldRequest/Response which deals with request/responses which target fields (such as read, write, subscribe).
  • The requests contain a Map of Named-Fields
  • The responses contain a reference to the corresponding request
  • The responses contain a response code for every field in the request
  • A PlcReadResponse also contains a map of byte data that contains the raw data.
  • (The data should be in a general format shared by all drivers, so the drivers requiring special formats need to convert this when constructing the objects)

PlcReadResponse & PlcWriteRequest

  • The getObject() is a convenience method to return values of the natural type determined by the field definition itself. This is the smallest possible type fit to contain the PLC type.
  • The setObject() is a convenience method. Actually thinking about it a little bit more, we might even have just the setObject version and get rid of the rest.
  • A "typeConverter" is passed in by each driver, that handles the encoding/decoding of values for that particular driver.
  • I removed the datatype both from the properties as well as the generic type as I think type information - if needed - should be inside the field object.
  • A request item now has a name, which allows to access it via it's name.
  • PlcReadRequestItem no longer has a "size", as that's part of the field now. Some protocols ... such as Modbus (I think) ... don't support this concept.
  • NOTE: I would suggest to not remove PlcReadRequestItem or PlcWriteResponseItem class entirely, even if we could simply make PlcRequestItem and PlcResponseItem non-abstract. For simplicity of the API I would opt for keeping it ... so we always have matching pairs of PlcXYZRequestItem and PlcXYZResponseItems.

Example Code

Here comes some example code to demonstrate how a Programm using this API could look like.

Code Block
languagejava
    public static void main(String[] args) throws Exception {
        PlcDriverManager manager = new PlcDriverManager();
        try (PlcConnection conn = manager.getConnection("s7://192.168.167.211/0/0")) {
            Optional<PlcReader> optionalReader = conn.getReader();
            if (!optionalReader.isPresent()) {
                return;
            }
 
            PlcReader Reader = optionalReader.get();
 
			// Example using prepareField and prepareNamedField ...
        PlcReadRequest request = new PlcReadRequest(
				new PlcReadRequestItem            conn.prepareNamedField("parameterX", conn.prepareQuery("%DB8.DBX3:INT")),
				new PlcReadRequestItem            conn.prepareNamedField("parameterY", conn.prepareQuery("%DB9.DBW4:DOUBLE")),
				new PlcReadRequestItem            new PlcNamedField("others", conn.prepareQueryprepareField("%DB2.DBW2:BYTE[10]")));
 
            CompletableFuture<PlcReadResponse> future = reader.read(request);
 
            PlcReadResponse response = future.get();
 

			        // Access an item by it's name.
            System.out.println("Parameter X: " + response.getInteger("parameterX"));


			 
        // Iterate over all items
			        for(PlcReadResponseItemString itemfieldName: response.getItemsgetFieldNames()) {
				// Conditionally handle an item by it's name.switch(fieldName) {
				if(case "parameterY".equals(item.getName()) {
					// If using getInteger without an index, the index 0 is used.
					: 
	                System.out.println("Parameter y: " + itemresponse.getDouble(fieldName));
				}	break;
				elsecase if("others".equals(item.getName()) {
					System.out.println("Others:");
					// Handle multi-value items.
					: 
	                for(int i = 0; i < itemresponse.getNumValues(fieldName); i++) {
						    	                // Intentionally using short instead of byte 
						
        	            // In this case it will return Byte[i] but convert that into a Short.
						            	        System.out.println(" - " + itemresponse.getShort(fieldName, i));
					}
                	}
					}break;
			}
        }
    }
}