├── .travis.yml ├── .gitignore ├── src ├── main │ └── java │ │ └── com │ │ └── hurence │ │ └── opc │ │ ├── Quality.java │ │ ├── SessionProfile.java │ │ ├── ua │ │ ├── OpcUaOperations.java │ │ ├── OpcUaSessionProfile.java │ │ ├── OpcUaQualityExtractor.java │ │ ├── OpcUaConnectionProfile.java │ │ ├── UaVariantMarshaller.java │ │ └── OpcUaSession.java │ │ ├── da │ │ ├── OpcDaOperations.java │ │ ├── OpcDaConnectionProfile.java │ │ ├── OpcDaItemProperties.java │ │ ├── OpcDaSessionProfile.java │ │ ├── OpcDaQualityExtractor.java │ │ ├── JIVariantMarshaller.java │ │ └── OpcDaSession.java │ │ ├── auth │ │ ├── Credentials.java │ │ ├── NtlmCredentials.java │ │ ├── UsernamePasswordCredentials.java │ │ └── X509Credentials.java │ │ ├── OpcContainerInfo.java │ │ ├── ConnectionState.java │ │ ├── exception │ │ └── OpcException.java │ │ ├── OpcTagAccessRights.java │ │ ├── OpcSession.java │ │ ├── OpcTagProperty.java │ │ ├── OperationStatus.java │ │ ├── AbstractOpcOperations.java │ │ ├── OpcObjectInfo.java │ │ ├── OpcOperations.java │ │ ├── OpcData.java │ │ ├── OpcTagInfo.java │ │ └── ConnectionProfile.java └── test │ └── java │ └── com │ └── hurence │ └── opc │ ├── ua │ ├── TestOpcServer.java │ ├── TestNamespace.java │ └── OpcUaTemplateTest.java │ ├── RxTests.java │ └── da │ └── OpcDaTemplateTest.java ├── CHANGELOG.md ├── pom.xml ├── LICENSE └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | # IntelliJ 25 | *.iml 26 | **/.idea/* 27 | 28 | # Maven 29 | **/target/* 30 | **/dependency-reduced-pom.xml 31 | 32 | 33 | # Misc 34 | *.tar.gz 35 | **/_build/ 36 | *.DS_Store 37 | *.versionsBackup 38 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/Quality.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | /** 21 | * The quality of a value 22 | */ 23 | public enum Quality { 24 | Good, 25 | Uncertain, 26 | Bad, 27 | Unknown 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/SessionProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | /** 21 | * Base class carrying information about a session. 22 | * 23 | * @author amarziali 24 | */ 25 | public abstract class SessionProfile { 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ua/OpcUaOperations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.hurence.opc.OpcOperations; 21 | 22 | /** 23 | * OPC-UA {@link OpcOperations} interface. 24 | * 25 | * @author amarziali 26 | */ 27 | public interface OpcUaOperations extends OpcOperations { 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/da/OpcDaOperations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | import com.hurence.opc.OpcOperations; 21 | 22 | /** 23 | * OPC-DA {@link com.hurence.opc.OpcOperations} 24 | * 25 | * @author amarziali 26 | */ 27 | public interface OpcDaOperations extends OpcOperations { 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/auth/Credentials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.auth; 19 | 20 | /** 21 | * Generic interface to be subclassed by implementations. 22 | * No operations defined here 23 | * 24 | * @author amarziali 25 | */ 26 | public interface Credentials { 27 | 28 | /** 29 | * Anonymous credentials. 30 | */ 31 | Credentials ANONYMOUS_CREDENTIALS = new Credentials() { 32 | }; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcContainerInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | /** 21 | * Base class to be used for branches and opc folders. 22 | * 23 | * @author amarziali 24 | */ 25 | public class OpcContainerInfo extends OpcObjectInfo { 26 | 27 | public OpcContainerInfo(String id) { 28 | super(id); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "OpcContainerInfo{} " + super.toString(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ConnectionState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | /** 21 | * The connection state machine. 22 | * 23 | * @author amarziali 24 | */ 25 | public enum ConnectionState { 26 | 27 | /** 28 | * The connection is handshaking but not yet established. 29 | */ 30 | CONNECTING, 31 | /** 32 | * Connection is in place. 33 | */ 34 | CONNECTED, 35 | /** 36 | * The client is disconnecting. 37 | */ 38 | DISCONNECTING, 39 | /** 40 | * The client has disconnected. 41 | */ 42 | DISCONNECTED 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/exception/OpcException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.hurence.opc.exception; 18 | 19 | /** 20 | * Base Exception. 21 | * 22 | * @author amarziali 23 | */ 24 | public class OpcException extends RuntimeException { 25 | 26 | /** 27 | * Constructor with a explaination message. 28 | * 29 | * @param message the message. 30 | */ 31 | public OpcException(String message) { 32 | super(message); 33 | } 34 | 35 | /** 36 | * Constructor with an explaination message and a parent exception. 37 | * 38 | * @param message the message 39 | * @param cause the parent cause. 40 | */ 41 | 42 | public OpcException(String message, Throwable cause) { 43 | super(message, cause); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/auth/NtlmCredentials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.auth; 19 | 20 | /** 21 | * Windows logon credentials. 22 | * 23 | * @author amarziali 24 | */ 25 | public class NtlmCredentials extends UsernamePasswordCredentials { 26 | 27 | /** 28 | * The logon domain. 29 | */ 30 | private String domain; 31 | 32 | /** 33 | * Get the logon domain 34 | * 35 | * @return the domain. 36 | */ 37 | public String getDomain() { 38 | return domain; 39 | } 40 | 41 | /** 42 | * Sets the logon domain. 43 | * 44 | * @param domain the domain. 45 | */ 46 | public void setDomain(String domain) { 47 | this.domain = domain; 48 | } 49 | 50 | /** 51 | * Sets the logon domain. 52 | * 53 | * @param domain the domain. 54 | * @return itself 55 | */ 56 | public NtlmCredentials withDomain(String domain) { 57 | setDomain(domain); 58 | return this; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "NtlmCredentials{" + 64 | "domain='" + domain + '\'' + 65 | "} " + super.toString(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcTagAccessRights.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | /** 21 | * Opc tag access rights. 22 | * 23 | * @author amarziali 24 | */ 25 | public class OpcTagAccessRights { 26 | /** 27 | * Item is readable. 28 | */ 29 | private boolean readable = true; 30 | /** 31 | * Item is writable. 32 | */ 33 | private boolean writable = true; 34 | 35 | /** 36 | * Is item readable 37 | * 38 | * @return 39 | */ 40 | public boolean isReadable() { 41 | return readable; 42 | } 43 | 44 | /** 45 | * Set the readable permission. 46 | * 47 | * @param readable 48 | */ 49 | public void setReadable(boolean readable) { 50 | this.readable = readable; 51 | } 52 | 53 | /** 54 | * Is item writable. 55 | * 56 | * @return 57 | */ 58 | public boolean isWritable() { 59 | return writable; 60 | } 61 | 62 | /** 63 | * Set the writable permission. 64 | * 65 | * @param writeable 66 | */ 67 | public void setWritable(boolean writeable) { 68 | this.writable = writeable; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return "OpcTagAccessRights{" + 74 | "readable=" + readable + 75 | ", writable=" + writable + 76 | '}'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/da/OpcDaConnectionProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | import com.hurence.opc.ConnectionProfile; 21 | 22 | /** 23 | * OPC-DA specific connection information. 24 | * Please remember that either comClsId or comProgId are mandatory and must be set. 25 | * 26 | * @author amarziali 27 | */ 28 | public class OpcDaConnectionProfile extends ConnectionProfile { 29 | 30 | /** 31 | * The CLS UUID of the com application. 32 | */ 33 | private String comClsId; 34 | /** 35 | * The com program id. 36 | */ 37 | private String comProgId; 38 | 39 | 40 | public String getComClsId() { 41 | return comClsId; 42 | } 43 | 44 | public void setComClsId(String comClsId) { 45 | this.comClsId = comClsId; 46 | } 47 | 48 | public String getComProgId() { 49 | return comProgId; 50 | } 51 | 52 | public void setComProgId(String comProgId) { 53 | this.comProgId = comProgId; 54 | } 55 | 56 | 57 | public OpcDaConnectionProfile withComClsId(String comClsId) { 58 | setComClsId(comClsId); 59 | return this; 60 | } 61 | 62 | public OpcDaConnectionProfile withComProgId(String comProgId) { 63 | setComProgId(comProgId); 64 | return this; 65 | } 66 | 67 | 68 | @Override 69 | public String toString() { 70 | return "OpcDaConnectionProfile{" + 71 | "comClsId='" + comClsId + '\'' + 72 | ", comProgId='" + comProgId + '\'' + 73 | '}'; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import io.reactivex.Flowable; 21 | import io.reactivex.Single; 22 | 23 | import java.time.Duration; 24 | import java.util.List; 25 | 26 | /** 27 | * Represents a session to manipulate a group of tags. 28 | * Multiple sessions may share a single connection (multiplexing). 29 | * 30 | * @author amarziali 31 | */ 32 | public interface OpcSession extends AutoCloseable { 33 | 34 | /** 35 | * Synchronously reads a list of tags and return as soon as possible. 36 | * May throw {@link com.hurence.opc.exception.OpcException} in case of issues. 37 | * 38 | * @param tags the list of tags. 39 | * @return the values that have been read. 40 | */ 41 | Single> read(String... tags); 42 | 43 | /** 44 | * Synchronously writes a list of tags and return as soon as possible. 45 | * May throw {@link com.hurence.opc.exception.OpcException} in case of issues. 46 | * 47 | * @param data the data to be written. 48 | * @return the status of each write operation. 49 | */ 50 | Single> write(OpcData... data); 51 | 52 | /** 53 | * Continuously read a stream of data for a tag. 54 | * When stream is requested, a subscription is done and values are output only in case they change. 55 | * May throw {@link com.hurence.opc.exception.OpcException} in case of issues. In this case the streaming will be interrupted. 56 | * 57 | * @param tagId the tas to be read. 58 | * @param samplingInterval the sampling interval. 59 | * @return a {@link Flowable} stream of {@link OpcData} 60 | */ 61 | Flowable stream(String tagId, Duration samplingInterval); 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ua/OpcUaSessionProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.hurence.opc.SessionProfile; 21 | 22 | import javax.annotation.Nonnull; 23 | import java.time.Duration; 24 | 25 | /** 26 | * OPC-UA {@link SessionProfile} 27 | * 28 | * @author amarziali 29 | */ 30 | public class OpcUaSessionProfile extends SessionProfile { 31 | 32 | /** 33 | * The data publication interval (we ask the server to publish at this rate). 34 | */ 35 | private Duration publicationInterval = Duration.ofSeconds(1); 36 | 37 | /** 38 | * Get The data publication interval (we ask the server to publish at this rate). 39 | * 40 | * @return a {@link Duration} 41 | */ 42 | public Duration getPublicationInterval() { 43 | return publicationInterval; 44 | } 45 | 46 | /** 47 | * Set data publication interval (we ask the server to publish at this rate). 48 | * 49 | * @param publicationInterval the never null publication interval. 50 | */ 51 | public void setPublicationInterval(@Nonnull Duration publicationInterval) { 52 | this.publicationInterval = publicationInterval; 53 | } 54 | 55 | /** 56 | * Set data publication interval (we ask the server to publish at this rate). 57 | * 58 | * @param publicationInterval the never null publication interval. 59 | * @return itself. 60 | */ 61 | public OpcUaSessionProfile withPublicationInterval(@Nonnull Duration publicationInterval) { 62 | setPublicationInterval(publicationInterval); 63 | return this; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return "OpcUaSessionProfile{" + 69 | "publicationInterval=" + publicationInterval + 70 | "} " + super.toString(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcTagProperty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import java.util.Objects; 21 | 22 | /** 23 | * Represents an opc tag property (metadata). 24 | * 25 | * @param The data type. 26 | * @author amarziali 27 | */ 28 | public class OpcTagProperty { 29 | 30 | /** 31 | * The property key (or id). 32 | */ 33 | private final String key; 34 | /** 35 | * The description. 36 | */ 37 | private final String description; 38 | /** 39 | * The value. 40 | */ 41 | private final T value; 42 | 43 | /** 44 | * Construct an immutable {@link OpcTagProperty} 45 | * 46 | * @param key the property name. 47 | * @param description the detailed description. 48 | * @param value the property value. 49 | */ 50 | public OpcTagProperty(String key, String description, T value) { 51 | this.key = key; 52 | this.description = description; 53 | this.value = value; 54 | } 55 | 56 | public String getKey() { 57 | return key; 58 | } 59 | 60 | public String getDescription() { 61 | return description; 62 | } 63 | 64 | public T getValue() { 65 | return value; 66 | } 67 | 68 | @Override 69 | public boolean equals(Object o) { 70 | if (this == o) return true; 71 | if (o == null || getClass() != o.getClass()) return false; 72 | OpcTagProperty that = (OpcTagProperty) o; 73 | return Objects.equals(key, that.key); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | 79 | return Objects.hash(key); 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return "OpcTagProperty{" + 85 | "key='" + key + '\'' + 86 | ", description='" + description + '\'' + 87 | ", value=" + value + 88 | '}'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/auth/UsernamePasswordCredentials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.auth; 19 | 20 | import java.util.Objects; 21 | 22 | /** 23 | * Username/Password {@link Credentials} information. 24 | * 25 | * @author amarziali 26 | */ 27 | public class UsernamePasswordCredentials implements Credentials { 28 | 29 | /** 30 | * The user name. 31 | */ 32 | private String user; 33 | /** 34 | * The password to authenticate the user on the provided domain. 35 | */ 36 | private String password; 37 | 38 | public String getUser() { 39 | return user; 40 | } 41 | 42 | public void setUser(String user) { 43 | this.user = user; 44 | } 45 | 46 | public String getPassword() { 47 | return password; 48 | } 49 | 50 | public void setPassword(String password) { 51 | this.password = password; 52 | } 53 | 54 | public UsernamePasswordCredentials withUser(String user) { 55 | setUser(user); 56 | return this; 57 | } 58 | 59 | public UsernamePasswordCredentials withPassword(String password) { 60 | setPassword(password); 61 | return this; 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) return true; 67 | if (o == null || getClass() != o.getClass()) return false; 68 | UsernamePasswordCredentials that = (UsernamePasswordCredentials) o; 69 | return Objects.equals(user, that.user) && 70 | Objects.equals(password, that.password); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | 76 | return Objects.hash(user, password); 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return "UsernamePasswordCredentials{" + 82 | "user='" + user + '\'' + 83 | ", password='***hidden***'" + 84 | '}'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ua/OpcUaQualityExtractor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.hurence.opc.OperationStatus; 21 | import com.hurence.opc.Quality; 22 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 23 | import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; 24 | 25 | import java.util.Optional; 26 | 27 | /** 28 | * Opc UA quality decoder. 29 | * 30 | * @author amarziali 31 | */ 32 | public class OpcUaQualityExtractor { 33 | 34 | /** 35 | * Convert quality from opc-ua to common format. 36 | * 37 | * @param statusCode the UA status code. 38 | * @return the decoded {@link Quality} 39 | */ 40 | public static final Quality quality(final StatusCode statusCode) { 41 | Quality ret = Quality.Unknown; 42 | if (statusCode.isGood()) { 43 | ret = Quality.Good; 44 | } else if (statusCode.isUncertain() || statusCode.isOverflowSet()) { 45 | ret = Quality.Uncertain; 46 | } else if (statusCode.isBad()) { 47 | ret = Quality.Bad; 48 | } 49 | return ret; 50 | } 51 | 52 | /** 53 | * Translates the ua status code to the {@link OperationStatus} 54 | * 55 | * @param statusCode the ua status code. 56 | * @return the resulting {@link OperationStatus} 57 | */ 58 | public static final OperationStatus operationStatus(final StatusCode statusCode) { 59 | OperationStatus.Level level = OperationStatus.Level.WARNING; 60 | if (statusCode.isBad()) { 61 | level = OperationStatus.Level.ERROR; 62 | } else if (statusCode.isGood()) { 63 | level = OperationStatus.Level.INFO; 64 | } 65 | Optional lookup = StatusCodes.lookup(statusCode.getValue()); 66 | return new OperationStatus(level, statusCode.getValue(), 67 | Optional.ofNullable(lookup.orElseGet(() -> new String[2])[1])); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## Table of contents 5 | 6 | - [[3.0.0-rc1] (2019-01-16)](#300-rc1-2019-01-16) 7 | - [Features](#features) 8 | - [Bugfixes](#bugfixes) 9 | - [Breaking changes](#breaking-changes) 10 | - [[2.0.1] (2018-12-28)](#201-2018-12-28) 11 | - [Bugfixes](#bugfixes-1) 12 | - [[2.0.0] (2018-07-05)](#200-2018-07-05) 13 | - [Features](#features-1) 14 | - [Bugfixes](#bugfixes-2) 15 | - [[1.2.0] (2018-06-29)](#120-2018-06-29) 16 | - [Features](#features-2) 17 | - [Breaking changes](#breaking-changes-1) 18 | - [[1.1.2] (2018-05-02)](#112-2018-05-02) 19 | - [Features](#features-3) 20 | - [[1.0.0] (2018-04-16)](#100-2018-04-16) 21 | - [Features](#features-4) 22 | 23 | ## [3.0.0-rc1] (2019-01-16) 24 | 25 | ### Features 26 | - **OPC Simple goes ReactiveX** 27 | 28 | ### Bugfixes 29 | - Browse next tree level may be incomplete for OPC-UA 30 | 31 | ### Breaking changes 32 | - Breaking changes on all APIs in order to support reactive patterns. 33 | 34 | ## [2.0.1] (2018-12-28) 35 | 36 | ### Bugfixes 37 | - Migrate jinterop artifacts to maven central 38 | 39 | ## [2.0.0] (2018-07-05) 40 | ### Features 41 | - Use an improved threading model for subscriptions 42 | ### Bugfixes 43 | - Make opc-simple compliant with KEP. MinimumSamplingRate rate is returned as integer and not double as supposed. 44 | - Improve general stability 45 | 46 | ### [1.2.0] (2018-06-29) 47 | 48 | ### Features 49 | - OPC-UA implementation with Eclipse Milo. 50 | - Support to Anonymous, UserPass and X509 51 | - Support to both tag refresh rate and publication window 52 | - Improved testing and documentation. 53 | 54 | ### Breaking changes 55 | - OpcOperations become an interface. Implementations are now OpcUaTemplate and OpcDaTemplate 56 | - Opc Auto connect now removes unnecessary cast thanks to dynamic proxies. 57 | 58 | ### [1.1.2] (2018-05-02) 59 | 60 | ### Features 61 | 62 | - Support try with resources to automatically close Sessions and Connections 63 | - Support error codes in OpcData 64 | - Support stateful sessions. Multiplex Groups in a single connection. 65 | - Use native java types instead of JIVariant. 66 | 67 | ### [1.0.0] (2018-04-16) 68 | 69 | ### Features 70 | 71 | - OPC-DA implementation with JInterop and Utgard 72 | 73 | [Unreleased]: https://github.com/Hurence/opc-simple/compare/master...develop 74 | [1.0.0]: https://github.com/Hurence/opc-simple/compare/7051ad30c02643a50827668b6f50066744e1204c...1.0.0 75 | [1.1.2]: https://github.com/Hurence/opc-simple/compare/1.0.0...1.1.2 76 | [1.2.0]: https://github.com/Hurence/opc-simple/compare/1.1.2...1.2.0 77 | [2.0.0]: https://github.com/Hurence/opc-simple/compare/2.0.0...1.2.0 78 | [2.0.1]: https://github.com/Hurence/opc-simple/compare/2.0.0...2.0.1 79 | [3.0.0-rc1]: https://github.com/Hurence/opc-simple/compare/3.0.0-rc1...2.0.1 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OperationStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import java.util.Objects; 21 | import java.util.Optional; 22 | 23 | /** 24 | * The status of an operation. 25 | * 26 | * @author amarziali 27 | */ 28 | public class OperationStatus { 29 | 30 | /** 31 | * The importance of a status. 32 | */ 33 | public enum Level { 34 | INFO, 35 | WARNING, 36 | ERROR 37 | } 38 | 39 | /** 40 | * The status level. 41 | */ 42 | private final Level level; 43 | /** 44 | * The full code. 45 | */ 46 | private final long code; 47 | /** 48 | * The code meaning (the message). 49 | */ 50 | private final Optional messageDetail; 51 | 52 | 53 | /** 54 | * Construct a new instance. 55 | * 56 | * @param level the {@link Level} 57 | * @param code the full status code. 58 | * @param messageDetail the status code human readable meaning. 59 | */ 60 | public OperationStatus(Level level, long code, Optional messageDetail) { 61 | this.level = level; 62 | this.code = code; 63 | this.messageDetail = messageDetail; 64 | } 65 | 66 | /** 67 | * The status level. 68 | * 69 | * @return a {@link Level} 70 | */ 71 | public Level getLevel() { 72 | return level; 73 | } 74 | 75 | /** 76 | * The full code. 77 | * 78 | * @return the original error code. 79 | */ 80 | public long getCode() { 81 | return code; 82 | } 83 | 84 | /** 85 | * The code meaning. Optional but never null. 86 | * 87 | * @return the {@link Optional} code description. 88 | */ 89 | public Optional getMessageDetail() { 90 | return messageDetail; 91 | } 92 | 93 | @Override 94 | public boolean equals(Object o) { 95 | if (this == o) return true; 96 | if (o == null || getClass() != o.getClass()) return false; 97 | OperationStatus that = (OperationStatus) o; 98 | return code == that.code && 99 | level == that.level; 100 | } 101 | 102 | @Override 103 | public int hashCode() { 104 | return Objects.hash(level, code); 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return "OperationStatus{" + 110 | "level=" + level + 111 | ", code=" + code + 112 | ", messageDetail='" + messageDetail + '\'' + 113 | '}'; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/auth/X509Credentials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.auth; 19 | 20 | import java.security.PrivateKey; 21 | import java.security.cert.Certificate; 22 | import java.security.cert.X509Certificate; 23 | import java.util.Objects; 24 | 25 | /** 26 | * X509 (Private key + Certificate) {@link Credentials} information. 27 | * Used to authenticate securely without exchanging passwords. 28 | * 29 | * @author amarziali 30 | */ 31 | public class X509Credentials implements Credentials { 32 | 33 | /** 34 | * The issued public X509 {@link X509Certificate}. 35 | */ 36 | private X509Certificate certificate; 37 | 38 | /** 39 | * The {@link PrivateKey} private key. 40 | */ 41 | private PrivateKey privateKey; 42 | 43 | 44 | /** 45 | * Get the issued public X509 {@link X509Certificate}. 46 | * 47 | * @return the {@link X509Certificate} 48 | */ 49 | public X509Certificate getCertificate() { 50 | return certificate; 51 | } 52 | 53 | /** 54 | * Set the issued public X509 {@link Certificate}. 55 | * 56 | * @param certificate the X509 {@link Certificate} to set 57 | */ 58 | public void setCertificate(X509Certificate certificate) { 59 | this.certificate = certificate; 60 | } 61 | 62 | /** 63 | * Get the {@link PrivateKey} private key. 64 | * 65 | * @return the private key. 66 | */ 67 | public PrivateKey getPrivateKey() { 68 | return privateKey; 69 | } 70 | 71 | /** 72 | * Set the {@link PrivateKey} private key. 73 | * 74 | * @param privateKey the private key to set. 75 | */ 76 | public void setPrivateKey(PrivateKey privateKey) { 77 | this.privateKey = privateKey; 78 | } 79 | 80 | /** 81 | * Set the issued public X509 {@link X509Certificate}. 82 | * 83 | * @param certificate the X509 {@link X509Certificate} to set 84 | * @return itself. 85 | */ 86 | public X509Credentials withCertificate(X509Certificate certificate) { 87 | setCertificate(certificate); 88 | return this; 89 | } 90 | 91 | /** 92 | * Set the {@link PrivateKey} private key. 93 | * 94 | * @param privateKey the private key to set. 95 | * @return itself. 96 | */ 97 | public X509Credentials withPrivateKey(PrivateKey privateKey) { 98 | setPrivateKey(privateKey); 99 | return this; 100 | } 101 | 102 | @Override 103 | public boolean equals(Object o) { 104 | if (this == o) return true; 105 | if (o == null || getClass() != o.getClass()) return false; 106 | X509Credentials that = (X509Credentials) o; 107 | return Objects.equals(certificate, that.certificate) && 108 | Objects.equals(privateKey, that.privateKey); 109 | } 110 | 111 | @Override 112 | public int hashCode() { 113 | return Objects.hash(certificate, privateKey); 114 | } 115 | 116 | @Override 117 | public String toString() { 118 | return "X509Credentials{" + 119 | "certificate=" + certificate + 120 | ", privateKey='****hidden****'" + 121 | '}'; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/AbstractOpcOperations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import com.hurence.opc.exception.OpcException; 21 | import io.reactivex.Completable; 22 | import io.reactivex.Observable; 23 | import io.reactivex.subjects.BehaviorSubject; 24 | 25 | import java.util.Optional; 26 | 27 | /** 28 | * Abstract base class for {@link OpcOperations} 29 | * 30 | * @author amarziali 31 | */ 32 | public abstract class AbstractOpcOperations implements OpcOperations { 33 | 34 | 35 | /** 36 | * The connection state 37 | */ 38 | private final BehaviorSubject connectionState = BehaviorSubject.createDefault(ConnectionState.DISCONNECTED); 39 | 40 | 41 | /** 42 | * Atomically check a state and set next state. 43 | * 44 | * @param next of empty won't set anything. 45 | * @return the connection state. 46 | */ 47 | protected synchronized ConnectionState getStateAndSet(Optional next) { 48 | ConnectionState ret = connectionState.getValue(); 49 | next.ifPresent(connectionState::onNext); 50 | return ret; 51 | } 52 | 53 | 54 | @Override 55 | public final Observable getConnectionState() { 56 | return connectionState; 57 | } 58 | 59 | 60 | @Override 61 | public boolean isChannelSecured() { 62 | return false; 63 | } 64 | 65 | /** 66 | * Wait until connection is released. 67 | * 68 | * @return a {@link Completable} task. 69 | */ 70 | protected Completable waitUntilDisconnected() { 71 | return getConnectionState() 72 | .takeWhile(connectionState -> connectionState != ConnectionState.DISCONNECTED) 73 | .filter(connectionState -> connectionState == ConnectionState.CONNECTED || connectionState == ConnectionState.DISCONNECTED) 74 | .switchMapCompletable(connectionState -> { 75 | switch (connectionState) { 76 | case CONNECTED: 77 | return Completable.error(new OpcException("Client still in connected state")); 78 | default: 79 | return Completable.complete(); 80 | } 81 | }); 82 | } 83 | 84 | /** 85 | * Wait until connection is established. 86 | * 87 | * @return a {@link Completable} task. 88 | */ 89 | protected Completable waitUntilConnected() { 90 | return getConnectionState() 91 | .takeWhile(connectionState -> connectionState != ConnectionState.CONNECTED) 92 | .filter(connectionState -> connectionState == ConnectionState.CONNECTED || connectionState == ConnectionState.DISCONNECTED) 93 | .switchMapCompletable(connectionState -> { 94 | switch (connectionState) { 95 | case DISCONNECTED: 96 | return Completable.error(new OpcException("Client disconnected while waiting for connection handshake")); 97 | default: 98 | return Completable.complete(); 99 | } 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcObjectInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import java.util.Objects; 21 | import java.util.Optional; 22 | 23 | /** 24 | * Basic information (metadata) of an object (can be any item folder or item). 25 | * 26 | * @author amarziali 27 | */ 28 | public abstract class OpcObjectInfo> { 29 | 30 | /** 31 | * Construct a new instance. 32 | * 33 | * @param id the unique id. 34 | */ 35 | public OpcObjectInfo(String id) { 36 | this.id = id; 37 | } 38 | 39 | /** 40 | * The item id. 41 | */ 42 | private final String id; 43 | /** 44 | * The item name. 45 | */ 46 | private String name; 47 | /** 48 | * The item description (if available). 49 | */ 50 | private Optional description = Optional.empty(); 51 | 52 | 53 | /** 54 | * Get the item id. 55 | * 56 | * @return the item unique id. 57 | */ 58 | public String getId() { 59 | return id; 60 | } 61 | 62 | /** 63 | * Gets the description. The field is optional but never null. 64 | * 65 | * @return a optional description. 66 | */ 67 | public Optional getDescription() { 68 | return description; 69 | } 70 | 71 | 72 | /** 73 | * Set the optionally empty description. 74 | * 75 | * @param description a never null {@link Optional} description. 76 | */ 77 | public void setDescription(Optional description) { 78 | this.description = description; 79 | } 80 | 81 | /** 82 | * Gets the item name 83 | * 84 | * @return the never null item name. 85 | */ 86 | public String getName() { 87 | return name; 88 | } 89 | 90 | 91 | /** 92 | * Sets the item name. 93 | * 94 | * @param name the never null item name. 95 | */ 96 | public void setName(String name) { 97 | this.name = name; 98 | } 99 | 100 | 101 | /** 102 | * Sets the item name. 103 | * 104 | * @param name the never null item name. 105 | * @return itself 106 | */ 107 | public T withName(String name) { 108 | setName(name); 109 | return (T) this; 110 | } 111 | 112 | 113 | /** 114 | * Set the optionally empty description. 115 | * 116 | * @param description a never null {@link Optional} description. 117 | * @return itself 118 | */ 119 | public T withDescription(String description) { 120 | setDescription(Optional.ofNullable(description)); 121 | return (T) this; 122 | } 123 | 124 | 125 | @Override 126 | public boolean equals(Object o) { 127 | if (this == o) return true; 128 | if (o == null || getClass() != o.getClass()) return false; 129 | OpcObjectInfo that = (OpcObjectInfo) o; 130 | return Objects.equals(id, that.id); 131 | } 132 | 133 | @Override 134 | public int hashCode() { 135 | return Objects.hash(id); 136 | } 137 | 138 | 139 | @Override 140 | public String toString() { 141 | return "OpcObjectInfo{" + 142 | "id='" + id + '\'' + 143 | ", name='" + name + '\'' + 144 | ", description=" + description + 145 | '}'; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcOperations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import io.reactivex.Completable; 21 | import io.reactivex.Flowable; 22 | import io.reactivex.Observable; 23 | import io.reactivex.Single; 24 | 25 | import javax.annotation.Nonnull; 26 | 27 | /** 28 | * Base Interface to describe OPC releated operations 29 | * 30 | * @author amarziali 31 | */ 32 | public interface OpcOperations 33 | extends AutoCloseable { 34 | 35 | /** 36 | * Establish a connection to an OPC server. 37 | * May throw {@link com.hurence.opc.exception.OpcException} in case of issues. 38 | * 39 | * @param connectionProfile the connection information 40 | */ 41 | Single> connect(@Nonnull T connectionProfile); 42 | 43 | /** 44 | * Disconnects from the OPC server. 45 | */ 46 | Completable disconnect(); 47 | 48 | 49 | /** 50 | * Check whenever the connection has been established under a secure layer (e.g. ssl). 51 | * 52 | * @return true if the connection transport layer can be considered as secure. False otherwise. 53 | */ 54 | boolean isChannelSecured(); 55 | 56 | /** 57 | * Retrieves observable connected to the state of the current connection. 58 | * 59 | * @return the {@link ConnectionState} as an {@link Observable} 60 | */ 61 | Observable getConnectionState(); 62 | 63 | /** 64 | * Retrieves the list of tags. 65 | * May throw {@link com.hurence.opc.exception.OpcException} in case of issues. 66 | * 67 | * @return a {@link Flowable} stream of {@link OpcTagInfo} 68 | */ 69 | Flowable browseTags(); 70 | 71 | /** 72 | * Inspects the OPC tree starting from the provided tree (empty is the root) and returns only the next level. 73 | * May throw {@link com.hurence.opc.exception.OpcException} in case of issues. 74 | * 75 | * @param rootTagId the root tag to begin exploring from. 76 | * @return a {@link Flowable} stream of {@link OpcObjectInfo} (may also be {@link OpcTagInfo} in case is a leaf) 77 | */ 78 | Flowable fetchNextTreeLevel(@Nonnull String rootTagId); 79 | 80 | /** 81 | * Fetch metadata of provided tags. 82 | * May throw {@link com.hurence.opc.exception.OpcException} in case of issues. 83 | * 84 | * @param tagIds the id of tags to fetch. 85 | * @return a {@link Flowable} stream of {@link OpcTagInfo} 86 | */ 87 | Flowable fetchMetadata(@Nonnull String... tagIds); 88 | 89 | /** 90 | * Create a new {@link OpcSession} and attach to the current connection. 91 | * The session needs then to be released. See {@link OpcOperations#releaseSession(OpcSession)} 92 | * 93 | * @param sessionProfile the information about the session to be created. 94 | * @return the session. 95 | */ 96 | Single createSession(@Nonnull U sessionProfile); 97 | 98 | 99 | /** 100 | * Clear up the session and detatch it from the current session. 101 | * 102 | * @param session the session to be destroyed. 103 | * @return a {@link Completable} operation. 104 | */ 105 | Completable releaseSession(@Nonnull V session); 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/da/OpcDaItemProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | /** 21 | * OPC-DA property indexes. 22 | * 23 | * @author amarziali 24 | */ 25 | @SuppressWarnings("unused") 26 | public interface OpcDaItemProperties { 27 | /** 28 | * tem Canonical DataType 29 | * (VARTYPE stored in an I2) 30 | */ 31 | int MANDATORY_DATA_TYPE = 1; 32 | /** 33 | * "Item Value" (VARIANT) 34 | * Note the type of value returned is as indicated by the "Item Canonical DataType above and depends on the item. 35 | * This will behave like a read from DEVICE. 36 | */ 37 | int MANDATORY_ITEM_VALUE = 2; 38 | /** 39 | * Item Quality 40 | * (OPCQUALITY stored in an I2). This will behave like a read from DEVICE. 41 | */ 42 | int MANDATORY_ITEM_QUALITY = 3; 43 | /** 44 | * Item Timestamp 45 | * (will be converted from FILETIME). This will behave like a read from DEVICE. 46 | */ 47 | int MANDATORY_ITEM_TIMESTAMP = 4; 48 | /** 49 | * Item Access Rights 50 | * (OPCACCESSRIGHTS stored in an I4) 51 | */ 52 | int MANDATORY_ITEM_ACCESS_RIGHTS = 5; 53 | /** 54 | * Server Scan Rate in Milliseconds. 55 | * This represents the fastest rate at which the server could obtain data from the underlying data source. 56 | *

57 | * The nature of this source is not defined but is typically a DCS system, a SCADA system, a PLC via a COMM port or network, 58 | * a Device Network, etc. This value generally represents the 'best case' fastest RequestedUpdateRate which could be used 59 | * if this item were added to an OPCGroup. 60 | *

61 | * The accuracy of this value (the ability of the server to attain 'best case' performance) can be greatly affected by system load and other factors. 62 | */ 63 | int MANDATORY_SERVER_SCAN_RATE = 6; 64 | 65 | /** 66 | * "EU Units"e.g. 67 | * "DEGC" or "GALLONS" 68 | */ 69 | int RECOMMENDED_EU_UNITS = 100; 70 | /** 71 | * The item description. 72 | */ 73 | int RECOMMENDED_ITEM_DESCRIPTION = 101; 74 | /** 75 | * "High EU" 76 | * Present only for 'analog' data. This represents the highest value likely to be obtained in normal operation 77 | * and is intended for such use as automatically scaling a bargraph display. e.g. 1400.0 78 | */ 79 | int RECOMMENDED_HIGH_EU = 102; 80 | /** 81 | * "Low EU" 82 | * Present only for 'analog' data. This represents the lowest value likely to be obtained in normal operation and 83 | * is intended for such use as automatically scaling a bargraph display. 84 | * e.g. -200.0 85 | */ 86 | int RECOMMENDED_LOW_EU = 103; 87 | /** 88 | * "High Instrument Range" 89 | * Present only for 'analog' data. This represents the highest value that can be returned by the instrument. 90 | * e.g. 9999.9 91 | */ 92 | int RECOMMENDED_HIGH_INSTRUMENT_RANGE = 104; 93 | /** 94 | * "Low Instrument Range" 95 | * Present only for 'analog' data. This represents the lowest value that can be returned by the instrument. 96 | * e.g. -9999.9 97 | */ 98 | int RECOMMENDED_LOW_INSTRUMENT_RANGE = 105; 99 | 100 | /** 101 | * Access rights read bitmask. 102 | */ 103 | int OPC_ACCESS_RIGHTS_READABLE = 0x1; 104 | /** 105 | * Access rights write bitmask. 106 | */ 107 | int OPC_ACCESS_RIGHTS_WRITABLE = 0x2; 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/da/OpcDaSessionProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | import com.hurence.opc.SessionProfile; 21 | 22 | import java.time.Duration; 23 | import java.util.Collections; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | import java.util.Optional; 27 | 28 | /** 29 | * {@link SessionProfile} with OPC-DA customizations. 30 | * 31 | * @author amarziali 32 | */ 33 | public class OpcDaSessionProfile extends SessionProfile { 34 | 35 | /** 36 | * If set, server cache will be ignored and the value will be read directly from the device. Defaults to false. 37 | */ 38 | private boolean directRead; 39 | 40 | 41 | /** 42 | * The item requested refresh interval. The server may not support as low as you set. 43 | * In this case the value will be refreshed at the server rate. 44 | * If you need a very low refresh delay, please consider use direct read mode. 45 | * Defaults to 1 second. 46 | */ 47 | private Duration refreshInterval = Duration.ofSeconds(1); 48 | 49 | /** 50 | * The client can negotiate with the server which data type should be used for a tag. 51 | */ 52 | private Map dataTypeOverrideMap = new HashMap<>(); 53 | 54 | /** 55 | * Forces a datatype for a tag. 56 | * 57 | * @param tagId the tag id. 58 | * @param dataType the data type code (See {@link org.jinterop.dcom.core.JIVariant} 59 | * @return itself 60 | */ 61 | public OpcDaSessionProfile withDataTypeForTag(String tagId, short dataType) { 62 | dataTypeOverrideMap.put(tagId, dataType); 63 | return this; 64 | } 65 | 66 | /** 67 | * Get the data type for a certain tag. 68 | * 69 | * @param tagId the tag id 70 | * @return the forced data type if set. Otherwise an empty {@link Optional} 71 | */ 72 | public Optional dataTypeForTag(String tagId) { 73 | return Optional.ofNullable(dataTypeOverrideMap.get(tagId)); 74 | } 75 | 76 | /** 77 | * Get the whole data override mapping. 78 | * 79 | * @return an unmodifiable map. 80 | */ 81 | public Map getDataTypeOverrideMap() { 82 | return Collections.unmodifiableMap(dataTypeOverrideMap); 83 | } 84 | 85 | /** 86 | * Gets the refresh interval. 87 | * 88 | * @return a never null {@link Duration} 89 | */ 90 | public final Duration getRefreshInterval() { 91 | return refreshInterval; 92 | } 93 | 94 | /** 95 | * Sets the refresh interval. 96 | * 97 | * @param refreshInterval the refresh interval (non null). 98 | */ 99 | public final void setRefreshInterval(Duration refreshInterval) { 100 | if (refreshInterval == null) { 101 | throw new IllegalArgumentException("The refresh interval must be any non null valid value."); 102 | } 103 | this.refreshInterval = refreshInterval; 104 | } 105 | 106 | /** 107 | * Sets the refresh interval. 108 | * 109 | * @param refreshInterval the refresh interval (non null). 110 | * @return itself. 111 | */ 112 | public final OpcDaSessionProfile withRefreshInterval(Duration refreshInterval) { 113 | setRefreshInterval(refreshInterval); 114 | return this; 115 | } 116 | 117 | public boolean isDirectRead() { 118 | return directRead; 119 | } 120 | 121 | public void setDirectRead(boolean directRead) { 122 | this.directRead = directRead; 123 | } 124 | 125 | public OpcDaSessionProfile withDirectRead(boolean directRead) { 126 | setDirectRead(directRead); 127 | return this; 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return "OpcDaSessionProfile{" + 133 | "directRead=" + directRead + 134 | ", refreshInterval=" + refreshInterval + 135 | ", dataTypeOverrideMap=" + dataTypeOverrideMap + 136 | "} " + super.toString(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import java.time.Instant; 21 | import java.util.Objects; 22 | 23 | /** 24 | * OPC data model. 25 | * 26 | * @author amarziali 27 | */ 28 | public class OpcData { 29 | 30 | 31 | /** 32 | * The tag (item) id. 33 | */ 34 | private String tag; 35 | /** 36 | * The timestamp of last data change. Can virtually track changes up to nanoseconds. 37 | */ 38 | private Instant timestamp; 39 | /** 40 | * The quality of data. Value is server dependent. It's meaningful if you read data not directly from a device but 41 | * rather from the server cache (default mode). 42 | */ 43 | private Quality quality; 44 | /** 45 | * The value of the data. 46 | */ 47 | private T value; 48 | 49 | /** 50 | * The status of the operation that generated this data. 51 | */ 52 | private OperationStatus operationStatus; 53 | 54 | /** 55 | * Default ctor. 56 | */ 57 | public OpcData() { 58 | 59 | } 60 | 61 | 62 | /** 63 | * Construct an object with parameters (useful for write). 64 | * 65 | * @param tag the tag (item) id. 66 | * @param timestamp the timestamp of last data change. 67 | * @param value the value. 68 | */ 69 | public OpcData(String tag, Instant timestamp, T value) { 70 | this.tag = tag; 71 | this.timestamp = timestamp; 72 | this.value = value; 73 | } 74 | 75 | 76 | /** 77 | * Construct an object with parameters. 78 | * 79 | * @param tag the tag (item) id. 80 | * @param timestamp the timestamp of last data change. 81 | * @param quality the quality of the data (set by the server). 82 | * @param value the value. 83 | * @param operationStatus the status of the operation that generated the data. 84 | */ 85 | public OpcData(String tag, Instant timestamp, Quality quality, T value, OperationStatus operationStatus) { 86 | this.tag = tag; 87 | this.timestamp = timestamp; 88 | this.quality = quality; 89 | this.value = value; 90 | this.operationStatus = operationStatus; 91 | } 92 | 93 | 94 | public String getTag() { 95 | return tag; 96 | } 97 | 98 | public void setTag(String tag) { 99 | this.tag = tag; 100 | } 101 | 102 | public Instant getTimestamp() { 103 | return timestamp; 104 | } 105 | 106 | public void setTimestamp(Instant timestamp) { 107 | this.timestamp = timestamp; 108 | } 109 | 110 | public T getValue() { 111 | return value; 112 | } 113 | 114 | public void setValue(T value) { 115 | this.value = value; 116 | } 117 | 118 | public Quality getQuality() { 119 | return quality; 120 | } 121 | 122 | public void setQuality(Quality quality) { 123 | this.quality = quality; 124 | } 125 | 126 | public OperationStatus getOperationStatus() { 127 | return operationStatus; 128 | } 129 | 130 | public void setOperationStatus(OperationStatus operationStatus) { 131 | this.operationStatus = operationStatus; 132 | } 133 | 134 | 135 | @Override 136 | public boolean equals(Object o) { 137 | if (this == o) return true; 138 | if (o == null || getClass() != o.getClass()) return false; 139 | OpcData opcData = (OpcData) o; 140 | return Objects.equals(tag, opcData.tag) && 141 | Objects.equals(timestamp, opcData.timestamp) && 142 | quality == opcData.quality && 143 | Objects.equals(value, opcData.value) && 144 | Objects.equals(operationStatus, opcData.operationStatus); 145 | } 146 | 147 | @Override 148 | public int hashCode() { 149 | 150 | return Objects.hash(tag, timestamp, quality, value, operationStatus); 151 | } 152 | 153 | @Override 154 | public String toString() { 155 | return "OpcData{" + 156 | "tag='" + tag + '\'' + 157 | ", timestamp=" + timestamp + 158 | ", quality=" + quality + 159 | ", value=" + value + 160 | ", operationStatus=" + operationStatus + 161 | '}'; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ua/OpcUaConnectionProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.hurence.opc.ConnectionProfile; 21 | import com.hurence.opc.auth.X509Credentials; 22 | 23 | /** 24 | * Connection profile for OPC-UA. 25 | * 26 | * @author amarziali 27 | */ 28 | public class OpcUaConnectionProfile extends ConnectionProfile { 29 | 30 | 31 | /** 32 | * THe client ID URI. 33 | *

34 | * If not provided and the certificate contains an alternative name URI within the extensions (ASN 2.5.29.18) 35 | * the client id will be automatically taken from it. 36 | *

37 | * Defaults to 'urn:hurence:opc:simple' 38 | */ 39 | private String clientIdUri = "urn:hurence:opc:simple"; 40 | 41 | /** 42 | * The client name (Defaults to 'OPC-SIMPLE'). 43 | */ 44 | private String clientName = "OPC-SIMPLE"; 45 | 46 | /** 47 | * In case the connection is through a secure layer, 48 | * a X509 credentials (both Certificate and keyPairs) have to be provided. 49 | */ 50 | private X509Credentials secureChannelEncryption; 51 | 52 | 53 | /** 54 | * Get the client URI. 55 | *

56 | * Defaults to 'urn:hurence:opc:simple' 57 | * 58 | * @return a valid client resource identifier. 59 | */ 60 | public String getClientIdUri() { 61 | return clientIdUri; 62 | } 63 | 64 | /** 65 | * Set the client URI. 66 | * 67 | * @param clientIdUri a valid client resource identifier. 68 | */ 69 | public void setClientIdUri(String clientIdUri) { 70 | this.clientIdUri = clientIdUri; 71 | } 72 | 73 | /** 74 | * Gets the client name. 75 | * 76 | * @return the client name (Defaults to 'OPC-SIMPLE'). 77 | */ 78 | public String getClientName() { 79 | return clientName; 80 | } 81 | 82 | /** 83 | * Sets the client name 84 | * 85 | * @param clientName the client name. 86 | */ 87 | public void setClientName(String clientName) { 88 | this.clientName = clientName; 89 | } 90 | 91 | /** 92 | * In case the communication should go though a secure channel, the user should set a valid X509 Credentials. 93 | * 94 | * @return the X509 Credentials or null if communication is insecure. 95 | */ 96 | public X509Credentials getSecureChannelEncryption() { 97 | return secureChannelEncryption; 98 | } 99 | 100 | /** 101 | * In case the communication should go though a secure channel, the user should set a valid X509 Credentials. 102 | * 103 | * @param secureChannelEncryption The {@link X509Credentials} or null to go insecurely. 104 | */ 105 | public void setSecureChannelEncryption(X509Credentials secureChannelEncryption) { 106 | this.secureChannelEncryption = secureChannelEncryption; 107 | } 108 | 109 | /** 110 | * Set the client URI. 111 | * 112 | * @param clientIdUri a valid client resource identifier. 113 | * @return itself. 114 | */ 115 | public OpcUaConnectionProfile withClientIdUri(String clientIdUri) { 116 | setClientIdUri(clientIdUri); 117 | return this; 118 | } 119 | 120 | /** 121 | * Set the client name. 122 | * 123 | * @param clientName the client name 124 | * @return itself. 125 | */ 126 | public OpcUaConnectionProfile withClientName(String clientName) { 127 | setClientName(clientName); 128 | return this; 129 | } 130 | 131 | /** 132 | * In case the communication should go though a secure channel, the user should set a valid X509 Credentials. 133 | * 134 | * @param secureChannelEncryption The {@link X509Credentials} or null to go insecurely. 135 | * @return itself. 136 | */ 137 | public OpcUaConnectionProfile withSecureChannelEncryption(X509Credentials secureChannelEncryption) { 138 | setSecureChannelEncryption(secureChannelEncryption); 139 | return this; 140 | } 141 | 142 | @Override 143 | public String toString() { 144 | return "OpcUaConnectionProfile{" + 145 | "clientIdUri='" + clientIdUri + '\'' + 146 | ", clientName='" + clientName + '\'' + 147 | ", secureChannelEncryption=" + secureChannelEncryption + 148 | "} " + super.toString(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/OpcTagInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import java.lang.reflect.Type; 21 | import java.time.Duration; 22 | import java.util.HashSet; 23 | import java.util.Optional; 24 | import java.util.Set; 25 | 26 | /** 27 | * Holds metadata about a tag 28 | * 29 | * @author amarziali 30 | */ 31 | public class OpcTagInfo extends OpcObjectInfo { 32 | 33 | public OpcTagInfo(String id) { 34 | super(id); 35 | } 36 | 37 | /** 38 | * The java {@link Type} corresponding to the item data type. 39 | */ 40 | private Type type; 41 | 42 | /** 43 | * The server scan rate (if available) 44 | */ 45 | private Optional scanRate = Optional.empty(); 46 | 47 | /** 48 | * The access rights (always non null). 49 | */ 50 | private final OpcTagAccessRights accessRights = new OpcTagAccessRights(); 51 | /** 52 | * The item properties if any. See {@link OpcTagProperty} for further details. 53 | */ 54 | private Set properties = new HashSet<>(); 55 | 56 | /** 57 | * Sets the value data type. 58 | * 59 | * @param type the Java {@link Type} linked to the value. 60 | * Use {@link Void} if the item does not carry any value. 61 | */ 62 | public void setType(Type type) { 63 | this.type = type; 64 | } 65 | 66 | /** 67 | * The item properties. 68 | * 69 | * @return a {@link Set} of {@link OpcTagProperty} 70 | */ 71 | public Set getProperties() { 72 | return properties; 73 | } 74 | 75 | /** 76 | * Sets the item properties. 77 | * 78 | * @param properties a never null {@link Set} of {@link OpcTagProperty} 79 | */ 80 | public void setProperties(Set properties) { 81 | this.properties = properties; 82 | } 83 | 84 | /** 85 | * Gets the optional scan rate. 86 | * 87 | * @return the optionally empty (but never null) scan rate {@link Duration} 88 | */ 89 | public Optional getScanRate() { 90 | return scanRate; 91 | } 92 | 93 | /** 94 | * Gets the item data type. 95 | * 96 | * @return the Java {@link Type} of the item value. 97 | */ 98 | public Type getType() { 99 | return type; 100 | } 101 | 102 | /** 103 | * Sets the scan rate. 104 | * 105 | * @param scanRate the {@link Optional} never null {@link Duration} 106 | */ 107 | public void setScanRate(Optional scanRate) { 108 | this.scanRate = scanRate; 109 | } 110 | 111 | 112 | /** 113 | * Adds a property to this item. 114 | * 115 | * @param property the not null {@link OpcTagProperty} 116 | * @return itself. 117 | */ 118 | public synchronized OpcObjectInfo addProperty(OpcTagProperty property) { 119 | properties.add(property); 120 | return this; 121 | } 122 | 123 | /** 124 | * Sets the value data type. 125 | * 126 | * @param type the Java {@link Type} linked to the value. 127 | * Use {@link Void} if the item does not carry any value. 128 | * @return itself 129 | */ 130 | public OpcTagInfo withType(Type type) { 131 | setType(type); 132 | return this; 133 | } 134 | 135 | /** 136 | * Sets the scan rate. 137 | * 138 | * @param scanRate the {@link Optional} never null {@link Duration} 139 | * @return itself 140 | */ 141 | public OpcTagInfo withScanRate(Duration scanRate) { 142 | setScanRate(Optional.ofNullable(scanRate)); 143 | return this; 144 | } 145 | 146 | /** 147 | * Gets the never null access rights. 148 | * 149 | * @return the {@link OpcTagAccessRights} 150 | */ 151 | public OpcTagAccessRights getAccessRights() { 152 | return accessRights; 153 | } 154 | 155 | /** 156 | * Sets the read access rights. 157 | * 158 | * @param readable true if item value can be read. 159 | * @return itself 160 | */ 161 | public OpcTagInfo withReadAccessRights(boolean readable) { 162 | getAccessRights().setReadable(readable); 163 | return this; 164 | } 165 | 166 | /** 167 | * Sets the write access rights. 168 | * 169 | * @param writable true if item value can be written. 170 | * @return itself 171 | */ 172 | public OpcTagInfo withWriteAccessRights(boolean writable) { 173 | getAccessRights().setWritable(writable); 174 | return this; 175 | } 176 | 177 | @Override 178 | public String toString() { 179 | return "OpcTagInfo{" + 180 | "type=" + type + 181 | ", scanRate=" + scanRate + 182 | ", accessRights=" + accessRights + 183 | ", properties=" + properties + 184 | "} " + super.toString(); 185 | } 186 | 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ConnectionProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import com.hurence.opc.auth.Credentials; 21 | 22 | import java.net.URI; 23 | import java.time.Duration; 24 | 25 | /** 26 | * Base class to describe opc server related properties. 27 | * 28 | * @author amarziali 29 | */ 30 | public abstract class ConnectionProfile> { 31 | 32 | 33 | /** 34 | * The connection {@link URI} 35 | */ 36 | private URI connectionUri; 37 | 38 | /** 39 | * The timeout used to read/write. 40 | */ 41 | private Duration socketTimeout; 42 | 43 | /** 44 | * The interval used to ping the remote server and check for health. (defaults to 10 seconds) 45 | */ 46 | private Duration keepAliveInterval = Duration.ofSeconds(10); 47 | 48 | /** 49 | * The authentication credentials. 50 | *

51 | * Defaults to {@link Credentials#ANONYMOUS_CREDENTIALS} 52 | */ 53 | private Credentials credentials = Credentials.ANONYMOUS_CREDENTIALS; 54 | 55 | 56 | /** 57 | * Set the connection URI and return itself. 58 | * 59 | * @param connectionUri the URI. 60 | * @return itself. 61 | */ 62 | public final T withConnectionUri(URI connectionUri) { 63 | setConnectionUri(connectionUri); 64 | return (T) this; 65 | } 66 | 67 | 68 | /** 69 | * Set the socket timeout and return itselg. 70 | * 71 | * @param socketTimeout the socket timeout. 72 | * @return itself. 73 | */ 74 | public final T withSocketTimeout(Duration socketTimeout) { 75 | setSocketTimeout(socketTimeout); 76 | return (T) this; 77 | } 78 | 79 | /** 80 | * Set the interval used to ping the remote server and check for health. 81 | * 82 | * @param keepAliveInterval a valid non null {@link Duration} 83 | * @return itself 84 | */ 85 | public final T withKeepAliveInterval(Duration keepAliveInterval) { 86 | setKeepAliveInterval(keepAliveInterval); 87 | return (T) this; 88 | } 89 | 90 | /** 91 | * Set the {@link Credentials} to use for authentication. 92 | * It can be any subclass. The connector must however support the related authentication method. 93 | * 94 | * @param credentials the credentials. 95 | * @return itself. 96 | */ 97 | public final T withCredentials(Credentials credentials) { 98 | setCredentials(credentials); 99 | return (T) this; 100 | } 101 | 102 | 103 | /** 104 | * Get the global socket timeout. 105 | * 106 | * @return the socket timeout. 107 | */ 108 | public final Duration getSocketTimeout() { 109 | return socketTimeout; 110 | } 111 | 112 | /** 113 | * Set the global socket timeout. 114 | * 115 | * @param socketTimeout the max allowed timeout. 116 | */ 117 | public final void setSocketTimeout(Duration socketTimeout) { 118 | this.socketTimeout = socketTimeout; 119 | } 120 | 121 | /** 122 | * Get The interval used to ping the remote server and check for health. 123 | * 124 | * @return a {@link Duration} 125 | */ 126 | public Duration getKeepAliveInterval() { 127 | return keepAliveInterval; 128 | } 129 | 130 | /** 131 | * Set the interval used to ping the remote server and check for health. (defaults to 10 seconds) 132 | * 133 | * @param keepAliveInterval a never null {@link Duration} 134 | */ 135 | public void setKeepAliveInterval(Duration keepAliveInterval) { 136 | if (keepAliveInterval == null) { 137 | throw new IllegalArgumentException("keepAliveInterval must be a valid non null duration"); 138 | } 139 | this.keepAliveInterval = keepAliveInterval; 140 | } 141 | 142 | /** 143 | * Gets the connection URI. 144 | * 145 | * @return a valid {@link URI} 146 | */ 147 | public URI getConnectionUri() { 148 | return connectionUri; 149 | } 150 | 151 | /** 152 | * Sets the connection URI 153 | * 154 | * @param connectionUri a valid {@link URI} (the scheme will depend to the underlying implementation. 155 | */ 156 | public void setConnectionUri(URI connectionUri) { 157 | this.connectionUri = connectionUri; 158 | } 159 | 160 | /** 161 | * Get the credentials to authenticate with. 162 | * 163 | * @return the {@link Credentials} 164 | */ 165 | public Credentials getCredentials() { 166 | return credentials; 167 | } 168 | 169 | /** 170 | * Set the {@link Credentials} to use for authentication. 171 | * It can be any subclass. The connector must however support the related authentication method. 172 | * 173 | * @param credentials the credentials. 174 | */ 175 | public void setCredentials(Credentials credentials) { 176 | this.credentials = credentials; 177 | } 178 | 179 | 180 | @Override 181 | public String toString() { 182 | return "ConnectionProfile{" + 183 | "connectionUri=" + connectionUri + 184 | ", socketTimeout=" + socketTimeout + 185 | ", keepAliveInterval=" + keepAliveInterval + 186 | ", credentials=" + credentials + 187 | '}'; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 22 | 4.0.0 23 | 24 | com.hurence.opc 25 | opc-simple 26 | 3.0.0-rc1 27 | OPC made simple. 28 | https://github.com/Hurence/opc-simple 29 | 30 | Hurence - Big Data Experts. 31 | http://hurence.com 32 | 33 | 34 | 35 | amarziali 36 | Andrea Marziali 37 | marziali.andrea@gmail.com 38 | Hurence 39 | http://www.hurence.com 40 | 41 | 42 | 43 | 44 | Apache License, Version 2.0 45 | http://www.apache.org/licenses/LICENSE-2.0 46 | 47 | 48 | 49 | 50 | 0.2.0 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.openscada.utgard 58 | org.openscada.opc.lib 59 | 1.5.0 60 | 61 | 62 | 63 | org.openscada.utgard 64 | org.openscada.opc.dcom 65 | 1.5.0 66 | 67 | 68 | 69 | org.openscada.jinterop 70 | org.openscada.jinterop.core 71 | 2.1.8 72 | 73 | 74 | 75 | org.openscada.jinterop 76 | org.openscada.jinterop.deps 77 | 1.5.0 78 | 79 | 80 | 81 | jcifs 82 | jcifs 83 | 1.3.17 84 | 85 | 86 | 87 | org.bouncycastle 88 | bcprov-jdk15on 89 | 1.60 90 | 91 | 92 | org.bouncycastle 93 | bcpkix-jdk15on 94 | 1.60 95 | 96 | 97 | 98 | 99 | 100 | org.eclipse.milo 101 | sdk-client 102 | ${milo.version} 103 | 104 | 105 | 106 | 107 | org.eclipse.milo 108 | sdk-server 109 | ${milo.version} 110 | test 111 | 112 | 113 | 114 | 115 | 116 | org.slf4j 117 | slf4j-api 118 | 1.7.12 119 | 120 | 121 | 122 | io.reactivex.rxjava2 123 | rxjava 124 | 2.2.4 125 | 126 | 127 | 128 | 129 | 130 | junit 131 | junit 132 | 4.12 133 | test 134 | 135 | 136 | 137 | ch.qos.logback 138 | logback-classic 139 | 1.1.3 140 | test 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | maven-compiler-plugin 149 | 150 | 1.8 151 | 1.8 152 | 153 | 154 | 155 | org.apache.maven.plugins 156 | maven-javadoc-plugin 157 | 158 | 159 | attach-javadoc 160 | 161 | jar 162 | 163 | 164 | 165 | 166 | 167 | org.apache.maven.plugins 168 | maven-source-plugin 169 | 170 | 171 | attach-sources 172 | 173 | jar 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /src/test/java/com/hurence/opc/ua/TestOpcServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.google.common.collect.ImmutableList; 21 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 22 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 23 | import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig; 24 | import org.eclipse.milo.opcua.sdk.server.identity.CompositeValidator; 25 | import org.eclipse.milo.opcua.sdk.server.identity.UsernameIdentityValidator; 26 | import org.eclipse.milo.opcua.sdk.server.identity.X509IdentityValidator; 27 | import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil; 28 | import org.eclipse.milo.opcua.stack.core.application.DefaultCertificateManager; 29 | import org.eclipse.milo.opcua.stack.core.application.InsecureCertificateValidator; 30 | import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; 31 | import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; 32 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 33 | import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo; 34 | import org.eclipse.milo.opcua.stack.core.util.CryptoRestrictions; 35 | import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder; 36 | import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator; 37 | import org.slf4j.Logger; 38 | import org.slf4j.LoggerFactory; 39 | 40 | import java.net.InetAddress; 41 | import java.net.ServerSocket; 42 | import java.security.KeyPair; 43 | import java.security.Security; 44 | import java.security.cert.X509Certificate; 45 | import java.util.EnumSet; 46 | import java.util.List; 47 | import java.util.UUID; 48 | import java.util.concurrent.CompletableFuture; 49 | 50 | import static com.google.common.collect.Lists.newArrayList; 51 | 52 | public class TestOpcServer implements AutoCloseable { 53 | 54 | private static final Logger logger = LoggerFactory.getLogger(TestOpcServer.class); 55 | 56 | static { 57 | CryptoRestrictions.remove(); 58 | Security.addProvider(new BouncyCastleProvider()); 59 | } 60 | 61 | public static void main(String[] args) throws Exception { 62 | final TestOpcServer server = new TestOpcServer(InetAddress.getLoopbackAddress(), null); 63 | server.getInstance().startup() 64 | .thenAccept(opcUaServer -> logger.info("Started server on {}", server.getBindEndpoint())).get(); 65 | final CompletableFuture future = new CompletableFuture<>(); 66 | Runtime.getRuntime().addShutdownHook(new Thread(() -> future.complete(null))); 67 | future.get(); 68 | 69 | 70 | } 71 | 72 | 73 | private final OpcUaServer instance; 74 | 75 | public TestOpcServer(InetAddress bindAddress, Integer port) throws Exception { 76 | UsernameIdentityValidator identityValidator = new UsernameIdentityValidator( 77 | true, 78 | authChallenge -> { 79 | String username = authChallenge.getUsername(); 80 | String password = authChallenge.getPassword(); 81 | 82 | boolean userOk = "user".equals(username) && "password1".equals(password); 83 | boolean adminOk = "admin".equals(username) && "password2".equals(password); 84 | 85 | return userOk || adminOk; 86 | } 87 | ); 88 | X509IdentityValidator x509IdentityValidator = new X509IdentityValidator(c -> true); 89 | String ba = bindAddress.getHostAddress(); 90 | List bindAddresses = newArrayList(); 91 | bindAddresses.add(ba); 92 | 93 | List endpointAddresses = newArrayList(); 94 | endpointAddresses.addAll(HostnameUtil.getHostnames(ba)); 95 | int bindPort = port != null ? port : findFreePort(bindAddress); 96 | 97 | // The configured application URI must match the one in the certificate(s) 98 | String applicationUri = "urn:hurence:opc:test-server:" + UUID.randomUUID(); 99 | KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048); 100 | 101 | OpcUaServerConfig serverConfig = OpcUaServerConfig.builder() 102 | .setApplicationUri(applicationUri) 103 | .setApplicationName(LocalizedText.english("Hurence OPC UA Test Server")) 104 | .setBindPort(bindPort) 105 | .setBindAddresses(bindAddresses) 106 | .setEndpointAddresses(endpointAddresses) 107 | .setBuildInfo( 108 | new BuildInfo( 109 | "urn:hurence:opc:test-server", 110 | "eclipse", 111 | "Hurence opc UA test server", 112 | OpcUaServer.SDK_VERSION, 113 | "", DateTime.now())) 114 | .setCertificateManager(new DefaultCertificateManager(keyPair, generateCertificate(keyPair, applicationUri))) 115 | .setCertificateValidator(new InsecureCertificateValidator()) 116 | .setIdentityValidator(new CompositeValidator(identityValidator, x509IdentityValidator)) 117 | .setProductUri("urn:hurence:opc:test-server") 118 | .setServerName("test") 119 | .setSecurityPolicies( 120 | EnumSet.of( 121 | SecurityPolicy.None, 122 | SecurityPolicy.Basic128Rsa15, 123 | SecurityPolicy.Basic256, 124 | SecurityPolicy.Basic256Sha256, 125 | SecurityPolicy.Aes128_Sha256_RsaOaep, 126 | SecurityPolicy.Aes256_Sha256_RsaPss)) 127 | .setUserTokenPolicies( 128 | ImmutableList.of( 129 | OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS, 130 | OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME, 131 | OpcUaServerConfig.USER_TOKEN_POLICY_X509)) 132 | .build(); 133 | 134 | instance = new OpcUaServer(serverConfig); 135 | registerSampleObjects(); 136 | logger.info("Created OPC-UA server running on opc.tcp://{}:{}", bindAddress, bindPort); 137 | } 138 | 139 | 140 | public String getBindEndpoint() { 141 | return instance.getEndpointDescriptions()[0].getEndpointUrl(); 142 | } 143 | 144 | public static X509Certificate generateCertificate(KeyPair keyPair, String appUri) throws Exception { 145 | SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair) 146 | .setCommonName("Hurence test") 147 | .setOrganization("Hurence") 148 | .setOrganizationalUnit("dev") 149 | .setLocalityName("Lyon") 150 | .setCountryCode("FR") 151 | .setApplicationUri(appUri); 152 | return builder.build(); 153 | 154 | } 155 | 156 | private int findFreePort(InetAddress address) throws Exception { 157 | try (ServerSocket s = new ServerSocket(0, -1, address)) { 158 | return s.getLocalPort(); 159 | } 160 | 161 | } 162 | 163 | 164 | private void registerSampleObjects() throws Exception { 165 | 166 | instance.getNamespaceManager().registerAndAdd(TestNamespace.URI, uShort -> new TestNamespace(uShort, instance)); 167 | 168 | 169 | } 170 | 171 | 172 | public OpcUaServer getInstance() { 173 | return instance; 174 | } 175 | 176 | @Override 177 | public void close() throws Exception { 178 | if (instance != null) { 179 | instance.shutdown().get(); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/da/OpcDaQualityExtractor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | import com.hurence.opc.OperationStatus; 21 | import com.hurence.opc.Quality; 22 | import org.openscada.opc.dcom.common.Result; 23 | 24 | import java.util.Optional; 25 | 26 | /** 27 | * Opc-DA status decoder. 28 | * 29 | * @author amarziali 30 | */ 31 | public class OpcDaQualityExtractor { 32 | 33 | public static final int OPC_QUALITY_MASK = 0xC0; 34 | public static final int OPC_STATUS_MASK = 0xFC; 35 | public static final int OPC_LIMIT_MASK = 0x03; 36 | // Values for QUALITY_MASK bit field 37 | public static final int OPC_QUALITY_UNCERTAIN = 0x40; 38 | public static final int OPC_QUALITY_GOOD = 0xC0; 39 | public static final int OPC_QUALITY_BAD = 0x0; 40 | 41 | // STATUS_MASK Values for Quality = BAD 42 | public static final int OPC_QUALITY_CONFIG_ERROR_CODE = 0x04; 43 | public static final int OPC_QUALITY_NOT_CONNECTED_ERROR_CODE = 0x08; 44 | public static final int OPC_QUALITY_DEVICE_FAILURE_CODE = 0x0c; 45 | public static final int OPC_QUALITY_SENSOR_FAILURE_CODE = 0x10; 46 | public static final int OPC_QUALITY_LAST_KNOWN_CODE = 0x14; 47 | public static final int OPC_QUALITY_COMM_FAILURE_CODE = 0x18; 48 | public static final int OPC_QUALITY_OUT_OF_SERVICE_CODE = 0x1C; 49 | public static final int OPC_QUALITY_WAITING_FOR_INITIAL_DATA_CODE = 0x20; 50 | // STATUS_MASK Values for Quality = UNCERTAIN 51 | public static final int OPC_QUALITY_UNCERTAIN_LAST_USABLE_VALUE_CODE = 0x44; 52 | public static final int OPC_QUALITY_SENSOR_CAL_CODE = 0x50; 53 | public static final int OPC_QUALITY_EGU_EXCEEDED_CODE = 0x54; 54 | public static final int OPC_QUALITY_UNCERTAIN_SUBNORMAL_CODE = 0x58; 55 | // STATUS_MASK Values for Quality = GOOD 56 | public static final int OPC_QUALITY_LOCAL_OVERRIDE_CODE = 0xD8; 57 | 58 | public static final String OPC_QUALITY_CONFIG_ERROR_DESC = "There is some server specific problem with the configuration."; 59 | public static final String OPC_QUALITY_NOT_CONNECTED_ERROR_DESC = "The input is required to be logically connected to something but is not."; 60 | public static final String OPC_QUALITY_DEVICE_FAILURE_DESC = "A device failure has been detected."; 61 | public static final String OPC_QUALITY_SENSOR_FAILURE_DESC = "A sensor failure had been detected"; 62 | public static final String OPC_QUALITY_LAST_KNOWN_DESC = "Communications have failed. However, the last known value is available"; 63 | public static final String OPC_QUALITY_COMM_FAILURE_DESC = "Communications have failed. There is no last known value is available."; 64 | public static final String OPC_QUALITY_OUT_OF_SERVICE_DESC = "The block is off scan or otherwise locked "; 65 | public static final String OPC_QUALITY_WAITING_FOR_INITIAL_DATA_DESC = "After Items are added to a group, it may take some time for the server to actually obtain values for these items."; 66 | 67 | public static final String OPC_QUALITY_UNCERTAIN_LAST_USABLE_VALUE_DESC = "Whatever was writing this value has stopped doing so. The returned value should be regarded as 'stale'."; 68 | public static final String OPC_QUALITY_SENSOR_CAL_DESC = "The sensor is known to be out of calibration."; 69 | public static final String OPC_QUALITY_EGU_EXCEEDED_DESC = "The returned value is outside the limits defined for this parameter"; 70 | public static final String OPC_QUALITY_UNCERTAIN_SUBNORMAL_DESC = "The value is derived from multiple sources and has less than the required number of good sources."; 71 | 72 | public static final String OPC_QUALITY_LOCAL_OVERRIDE_DESC = "The value has been Overridden."; 73 | 74 | /** 75 | * Extracts status from the raw value. 76 | * 77 | * @param value the raw value returned by the OPC-DA server (only first 32bit matters). 78 | * @return the {@link Quality}. Defaults to {@link Quality#Unknown} 79 | */ 80 | public static Quality quality(long value) { 81 | Quality ret; 82 | int extracted = (int) value & OPC_QUALITY_MASK; 83 | switch (extracted) { 84 | case OPC_QUALITY_GOOD: 85 | ret = Quality.Good; 86 | break; 87 | case OPC_QUALITY_UNCERTAIN: 88 | ret = Quality.Uncertain; 89 | break; 90 | case OPC_QUALITY_BAD: 91 | ret = Quality.Bad; 92 | break; 93 | default: 94 | ret = Quality.Unknown; 95 | break; 96 | } 97 | return ret; 98 | } 99 | 100 | /** 101 | * Extracts the information from a {@link Result}. 102 | * 103 | * @param result operation result. 104 | * @return the {@link OperationStatus} linked to the result. 105 | */ 106 | public static OperationStatus operationStatus(Result result) { 107 | 108 | if (result.isFailed()) { 109 | return operationStatus(result.getErrorCode()); 110 | } 111 | return new OperationStatus(OperationStatus.Level.INFO, result.getErrorCode(), Optional.empty()); 112 | } 113 | 114 | /** 115 | * Extracts the information from an encoded status value. 116 | * 117 | * @param value the encoded value 118 | * @return the {@link OperationStatus} linked to the value. 119 | */ 120 | public static OperationStatus operationStatus(long value) { 121 | OperationStatus.Level level; 122 | switch (quality(value)) { 123 | case Good: 124 | level = OperationStatus.Level.INFO; 125 | break; 126 | case Bad: 127 | level = OperationStatus.Level.ERROR; 128 | break; 129 | default: 130 | level = OperationStatus.Level.WARNING; 131 | break; 132 | } 133 | int code = (int) value & OPC_STATUS_MASK; 134 | String desc; 135 | 136 | switch (code) { 137 | case OPC_QUALITY_CONFIG_ERROR_CODE: 138 | desc = OPC_QUALITY_CONFIG_ERROR_DESC; 139 | break; 140 | case OPC_QUALITY_NOT_CONNECTED_ERROR_CODE: 141 | desc = OPC_QUALITY_NOT_CONNECTED_ERROR_DESC; 142 | break; 143 | case OPC_QUALITY_DEVICE_FAILURE_CODE: 144 | desc = OPC_QUALITY_DEVICE_FAILURE_DESC; 145 | break; 146 | case OPC_QUALITY_SENSOR_FAILURE_CODE: 147 | desc = OPC_QUALITY_SENSOR_FAILURE_DESC; 148 | break; 149 | case OPC_QUALITY_LAST_KNOWN_CODE: 150 | desc = OPC_QUALITY_LAST_KNOWN_DESC; 151 | break; 152 | case OPC_QUALITY_COMM_FAILURE_CODE: 153 | desc = OPC_QUALITY_COMM_FAILURE_DESC; 154 | break; 155 | case OPC_QUALITY_OUT_OF_SERVICE_CODE: 156 | desc = OPC_QUALITY_OUT_OF_SERVICE_DESC; 157 | break; 158 | case OPC_QUALITY_WAITING_FOR_INITIAL_DATA_CODE: 159 | desc = OPC_QUALITY_WAITING_FOR_INITIAL_DATA_DESC; 160 | break; 161 | case OPC_QUALITY_UNCERTAIN_LAST_USABLE_VALUE_CODE: 162 | desc = OPC_QUALITY_UNCERTAIN_LAST_USABLE_VALUE_DESC; 163 | break; 164 | case OPC_QUALITY_SENSOR_CAL_CODE: 165 | desc = OPC_QUALITY_SENSOR_CAL_DESC; 166 | break; 167 | case OPC_QUALITY_EGU_EXCEEDED_CODE: 168 | desc = OPC_QUALITY_EGU_EXCEEDED_DESC; 169 | break; 170 | case OPC_QUALITY_UNCERTAIN_SUBNORMAL_CODE: 171 | desc = OPC_QUALITY_UNCERTAIN_SUBNORMAL_DESC; 172 | break; 173 | // STATUS_MASK Values for Quality = GOOD 174 | case OPC_QUALITY_LOCAL_OVERRIDE_CODE: 175 | desc = OPC_QUALITY_LOCAL_OVERRIDE_DESC; 176 | break; 177 | default: 178 | desc = null; 179 | break; 180 | } 181 | 182 | return new OperationStatus(level, code, Optional.ofNullable(desc)); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/test/java/com/hurence/opc/RxTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc; 19 | 20 | import io.reactivex.Flowable; 21 | import io.reactivex.disposables.Disposable; 22 | import io.reactivex.exceptions.MissingBackpressureException; 23 | import io.reactivex.processors.BehaviorProcessor; 24 | import io.reactivex.processors.FlowableProcessor; 25 | import io.reactivex.processors.PublishProcessor; 26 | import io.reactivex.schedulers.Timed; 27 | import io.reactivex.subscribers.TestSubscriber; 28 | import org.junit.Assert; 29 | import org.junit.Ignore; 30 | import org.junit.Test; 31 | 32 | import java.util.List; 33 | import java.util.concurrent.*; 34 | import java.util.stream.Collectors; 35 | import java.util.stream.IntStream; 36 | 37 | /** 38 | * Some RX perf tests. 39 | */ 40 | @Ignore 41 | public class RxTests { 42 | 43 | @Test() 44 | public void hotFlowableBackpressureUnhandledTest() throws InterruptedException { 45 | final int count = 10000; 46 | Flowable test = Flowable.intervalRange(1, count, 0, 1, TimeUnit.MILLISECONDS) 47 | .publish().autoConnect(); 48 | //first one is slow 49 | TestSubscriber slowSubscriber = new TestSubscriber<>(); 50 | TestSubscriber fastSubscriber = new TestSubscriber<>(); 51 | test.filter(aLong -> aLong % 2 == 0) 52 | .flatMap(aLong -> Flowable.just(aLong).delay(1, TimeUnit.SECONDS)) 53 | .doOnError(Throwable::printStackTrace) 54 | .subscribe(slowSubscriber); 55 | //second is fast 56 | test.filter(aLong -> aLong % 2 == 1) 57 | .doOnError(Throwable::printStackTrace) 58 | .subscribe(fastSubscriber); 59 | fastSubscriber.await(); 60 | slowSubscriber.await(); 61 | 62 | fastSubscriber.assertTerminated(); 63 | fastSubscriber.assertError(MissingBackpressureException.class); 64 | slowSubscriber.assertTerminated(); 65 | slowSubscriber.assertError(MissingBackpressureException.class); 66 | 67 | } 68 | 69 | @Test() 70 | public void hotFlowableBackpressureHandledTest() throws InterruptedException { 71 | final int count = 10000; 72 | Flowable test = Flowable.intervalRange(1, count, 0, 1, TimeUnit.NANOSECONDS) 73 | .publish().autoConnect(2); 74 | //first one is slow 75 | TestSubscriber slowSubscriber = new TestSubscriber<>(); 76 | TestSubscriber fastSubscriber = new TestSubscriber<>(); 77 | test 78 | .onBackpressureDrop(aLong -> System.err.println("Element " + aLong + " dropped from slow consumer")) 79 | .filter(aLong -> aLong % 2 == 0) 80 | .flatMap(aLong -> Flowable.just(aLong).delay(1, TimeUnit.SECONDS)) 81 | .doOnError(Throwable::printStackTrace) 82 | .subscribe(slowSubscriber); 83 | //second is fast 84 | test 85 | .onBackpressureDrop(aLong -> System.err.println("Element " + aLong + " dropped from fast consumer")) 86 | .filter(aLong -> aLong % 2 == 1) 87 | .doOnError(Throwable::printStackTrace) 88 | .subscribe(fastSubscriber); 89 | fastSubscriber.await(); 90 | slowSubscriber.await(); 91 | 92 | fastSubscriber.assertTerminated(); 93 | fastSubscriber.assertNoErrors(); 94 | fastSubscriber.assertValueCount(count / 2); 95 | slowSubscriber.assertNoErrors(); 96 | slowSubscriber.assertTerminated(); 97 | 98 | } 99 | 100 | @Test 101 | public void testDecimate() throws InterruptedException { 102 | final int count = 1_000; 103 | Flowable test = Flowable.intervalRange(1, count, 0, 1, TimeUnit.MILLISECONDS) 104 | .publish().autoConnect(1); 105 | TestSubscriber subscriber = new TestSubscriber<>(); 106 | 107 | test 108 | .onBackpressureDrop() 109 | .throttleLatest(100, TimeUnit.MILLISECONDS) 110 | //.doOnNext(System.out::println) 111 | .subscribe(subscriber); 112 | 113 | subscriber.await(); 114 | subscriber.assertTerminated(); 115 | subscriber.assertNoErrors(); 116 | subscriber.assertValueCount(10); 117 | } 118 | 119 | @Test 120 | public void testStormOfResamples() throws Exception { 121 | 122 | final FlowableProcessor rateCounter = PublishProcessor.create(); 123 | 124 | rateCounter.window(1, TimeUnit.SECONDS) 125 | .flatMap(integerFlowable -> integerFlowable.scan((a, b) -> a + b)) 126 | .subscribe(r -> System.out.println("Current rate: " + r + " messages/second")); 127 | 128 | final Callable runnable = () -> { 129 | final int count = 1_000; 130 | Flowable> test = Flowable.intervalRange(1, count, 0, 10, TimeUnit.MILLISECONDS) 131 | .timestamp() 132 | .publish().autoConnect(100); 133 | TestSubscriber>> subscriber = new TestSubscriber<>(); 134 | BehaviorProcessor> processor = BehaviorProcessor.create(); 135 | 136 | Flowable.interval(1, TimeUnit.MILLISECONDS) 137 | .takeWhile(aLong -> !processor.hasComplete()) 138 | .withLatestFrom(processor, (aLong, longTimed) -> longTimed) 139 | .window(1, TimeUnit.SECONDS) 140 | .flatMap(timedFlowable -> timedFlowable.toList().toFlowable()) 141 | .doOnNext(timeds -> rateCounter.onNext(timeds.size())) 142 | .subscribe(subscriber); 143 | 144 | for (int i = 0; i < 100; i++) { 145 | test.subscribe(processor); 146 | } 147 | 148 | try { 149 | subscriber.await(); 150 | } catch (Exception e) { 151 | 152 | } 153 | subscriber.assertTerminated(); 154 | subscriber.assertNoErrors(); 155 | //System.out.println(subscriber.valueCount()); 156 | return subscriber.values().stream().flatMapToLong(timeds -> timeds.stream().mapToLong(o -> o.value())) 157 | .average().getAsDouble(); 158 | }; 159 | 160 | ExecutorService svc = Executors.newCachedThreadPool(); 161 | List> futures = IntStream.range(0, 1_000).mapToObj(i -> svc.submit(runnable)) 162 | .collect(Collectors.toList()); 163 | 164 | svc.shutdown(); 165 | svc.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); 166 | 167 | for (Future f : futures) { 168 | Assert.assertTrue(f.get() >= 10.0); 169 | } 170 | } 171 | 172 | @Test 173 | public void testTonsOfPubSub() throws Exception { 174 | 175 | final int publishers = 10_000; 176 | final int subscribers = 100; 177 | final PublishProcessor publisher = PublishProcessor.create(); 178 | final PublishProcessor receiver = PublishProcessor.create(); 179 | 180 | 181 | final int msgPerPublisherPerSecond = 1_000_000; 182 | Flowable flowable1 = Flowable.interval(1, TimeUnit.SECONDS) 183 | .flatMap(ignored -> Flowable.range(1, msgPerPublisherPerSecond)) 184 | .onBackpressureDrop() 185 | .publish() 186 | .autoConnect(); 187 | for (int i = 0; i < publishers; i++) { 188 | flowable1.subscribe(publisher); 189 | } 190 | 191 | 192 | for (int i = 0; i < subscribers; i++) { 193 | publisher 194 | 195 | //.doOnNext(System.err::println) 196 | .subscribe(receiver); 197 | } 198 | 199 | 200 | final Disposable disposable = receiver 201 | .window(1, TimeUnit.SECONDS) 202 | .flatMap(longFlowable -> longFlowable.count().toFlowable()) 203 | .take(20, TimeUnit.SECONDS) 204 | .doOnNext(System.err::println) 205 | .subscribe(); 206 | 207 | 208 | while (!disposable.isDisposed()) { 209 | Thread.sleep(100); 210 | } 211 | 212 | } 213 | } -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ua/UaVariantMarshaller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.hurence.opc.exception.OpcException; 21 | import org.eclipse.milo.opcua.sdk.client.OpcUaClient; 22 | import org.eclipse.milo.opcua.sdk.client.api.nodes.VariableNode; 23 | import org.eclipse.milo.opcua.sdk.core.ValueRanks; 24 | import org.eclipse.milo.opcua.stack.core.types.builtin.*; 25 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte; 26 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 27 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong; 28 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort; 29 | import org.eclipse.milo.opcua.stack.core.util.TypeUtil; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import java.lang.reflect.Array; 34 | import java.math.BigInteger; 35 | import java.util.Arrays; 36 | import java.util.Optional; 37 | import java.util.UUID; 38 | import java.util.function.Function; 39 | 40 | /** 41 | * OPC-UA Variant to Java primitives conversions 42 | * 43 | * @author amarziali 44 | */ 45 | public class UaVariantMarshaller { 46 | 47 | private static final Logger logger = LoggerFactory.getLogger(UaVariantMarshaller.class); 48 | 49 | 50 | /** 51 | * Find the java {@link Class} of data held by a opc-ua variable node. 52 | * 53 | * @param client the ua client. 54 | * @param node the variable node id. 55 | * @return the java class (can default to {@link String} if type is not built in or cannot be determined). 56 | */ 57 | public static Optional> findJavaClass(OpcUaClient client, NodeId node) { 58 | if (node == null || node.expanded() == null) { 59 | throw new OpcException("Impossible to guess type from empty node"); 60 | } 61 | try { 62 | VariableNode vn = client.getAddressSpace().createVariableNode(node); 63 | Integer valueRank = vn.getValueRank().get(); 64 | 65 | boolean isArray = ValueRanks.Scalar != valueRank; 66 | Class cls = null; 67 | NodeId dataType = vn.getDataType().exceptionally(e -> null).get(); 68 | if (dataType != null) { 69 | cls = TypeUtil.getBackingClass(dataType.expanded()); 70 | } 71 | 72 | if (cls == null) { 73 | Object value = vn.getValue().exceptionally(e -> null).get(); 74 | //try to convert to enumeration 75 | if (value instanceof Integer) { 76 | //here we have an enumeration 77 | cls = Integer.class; 78 | } 79 | } 80 | 81 | if (cls != null) { 82 | if (cls.equals(UInteger.class)) { 83 | cls = Long.class; 84 | } else if (cls.equals(UByte.class)) { 85 | cls = Short.class; 86 | } else if (cls.equals(UShort.class)) { 87 | cls = Integer.class; 88 | } else if (cls.equals(ULong.class)) { 89 | cls = BigInteger.class; 90 | } else if (cls.equals(DateTime.class)) { 91 | cls = Long.class; 92 | } else if (cls.equals(UUID.class)) { 93 | cls = String.class; 94 | } else if (cls.equals(Number.class)) { 95 | cls = Long.class; 96 | } else if (cls.equals(ByteString.class)) { 97 | cls = byte[].class; 98 | } else if (cls.equals(XmlElement.class)) { 99 | cls = String.class; 100 | } else if (cls.equals(NodeId.class)) { 101 | cls = String.class; 102 | } else if (cls.equals(ExpandedNodeId.class)) { 103 | cls = String.class; 104 | } else if (cls.equals(StatusCode.class)) { 105 | cls = Long.class; 106 | } else if (cls.equals(QualifiedName.class)) { 107 | cls = String.class; 108 | } else if (cls.equals(LocalizedText.class)) { 109 | cls = String.class; 110 | } else if (cls.equals(ExtensionObject.class)) { 111 | cls = Object.class; 112 | } else if (cls.equals(DataValue.class)) { 113 | cls = Object.class; 114 | } else if (cls.equals(Variant.class)) { 115 | cls = Object.class; 116 | } 117 | } 118 | 119 | //default case 120 | if (cls != null) { 121 | return Optional.of(isArray ? Array.newInstance(cls, 0).getClass() : cls); 122 | } 123 | } catch (Exception e) { 124 | logger.warn("Unable to map opc-ua type to java.", e); 125 | 126 | } 127 | return Optional.empty(); 128 | 129 | } 130 | 131 | private static Object convert(Object src, Function function) { 132 | boolean isArray = src.getClass().isArray(); 133 | if (isArray) { 134 | return Arrays.stream((Object[]) src).map(function).toArray(); 135 | } 136 | return function.apply(src); 137 | } 138 | 139 | 140 | /** 141 | * Find the data java type held by a opc-ua variable node. 142 | * It translates the variant and the builtin opc-ua types. 143 | * The method tries to return primitive types if possible. 144 | * 145 | * @param value the variable value 146 | * @return the converted values. 147 | */ 148 | public static Object toJavaType(Object value) { 149 | if (value == null) { 150 | return null; 151 | } 152 | Class cls = value.getClass().isArray() ? value.getClass().getComponentType() : value.getClass(); 153 | try { 154 | if (cls.equals(UInteger.class)) { 155 | return convert(value, a -> ((UInteger) a).longValue()); 156 | } else if (cls.equals(UByte.class)) { 157 | return convert(value, a -> ((UByte) a).shortValue()); 158 | } else if (cls.equals(UShort.class)) { 159 | return convert(value, a -> ((UShort) a).intValue()); 160 | } else if (cls.equals(ULong.class)) { 161 | return convert(value, a -> ((ULong) a).toBigInteger()); 162 | } else if (cls.equals(DateTime.class)) { 163 | return convert(value, a -> ((DateTime) a).getUtcTime()); 164 | } else if (cls.equals(UUID.class)) { 165 | return convert(value, a -> ((UUID) a).toString()); 166 | } else if (cls.equals(Number.class)) { 167 | return convert(value, a -> ((Number) a).longValue()); 168 | } else if (cls.equals(ByteString.class)) { 169 | return convert(value, a -> ((ByteString) a).bytes()); 170 | } else if (cls.equals(XmlElement.class)) { 171 | return convert(value, a -> ((XmlElement) a).toString()); 172 | } else if (cls.equals(NodeId.class)) { 173 | return convert(value, a -> ((NodeId) a).toParseableString()); 174 | } else if (cls.equals(ExpandedNodeId.class)) { 175 | return convert(value, a -> ((ExpandedNodeId) a).toParseableString()); 176 | } else if (cls.equals(StatusCode.class)) { 177 | return convert(value, a -> ((StatusCode) a).getValue()); 178 | } else if (cls.equals(QualifiedName.class)) { 179 | return convert(value, a -> ((QualifiedName) a).toParseableString()); 180 | } else if (cls.equals(LocalizedText.class)) { 181 | return convert(value, a -> ((LocalizedText) a).getText()); 182 | } else if (cls.equals(ExtensionObject.class)) { 183 | return convert(value, a -> ((ExtensionObject) a).decode()); 184 | } else if (cls.equals(DataValue.class)) { 185 | return convert(value, a -> toJavaType(((DataValue) a).getValue())); 186 | } else if (cls.equals(Variant.class)) { 187 | return convert(value, a -> toJavaType(((Variant) a).getValue())); 188 | } 189 | } catch (Exception e) { 190 | logger.warn("Unable to map value " + value + " to java type", e); 191 | } 192 | return value; 193 | } 194 | 195 | 196 | } 197 | 198 | 199 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/da/JIVariantMarshaller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | import org.jinterop.dcom.common.JIException; 21 | import org.jinterop.dcom.core.JIArray; 22 | import org.jinterop.dcom.core.JICurrency; 23 | import org.jinterop.dcom.core.JIString; 24 | import org.jinterop.dcom.core.JIVariant; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.math.BigDecimal; 29 | import java.time.Instant; 30 | import java.util.Arrays; 31 | import java.util.Date; 32 | 33 | /** 34 | * Variant to Java primitives conversions 35 | * 36 | * @author amarziali 37 | */ 38 | public class JIVariantMarshaller { 39 | 40 | private static final Logger logger = LoggerFactory.getLogger(JIVariantMarshaller.class); 41 | 42 | public static final String DEFAULT_MSG = "Using default case for variant conversion: {} : {} : {}"; 43 | 44 | 45 | /** 46 | * Extract the error code from the variant result if a SCODE is present. 47 | * 48 | * @param variant the variant 49 | * @return the error if any or null otherwise. 50 | * @throws JIException in case of any issue. 51 | */ 52 | public static Integer extractError(JIVariant variant) throws JIException { 53 | if (variant.getType() == JIVariant.VT_ERROR) { 54 | return variant.getObjectAsSCODE(); 55 | } 56 | return null; 57 | } 58 | 59 | 60 | /** 61 | * Converts a {@link JIVariant} to Java type. 62 | * 63 | * @param variant the variant to be converted. 64 | * @return a java object 65 | * @throws JIException in case of any issue. 66 | */ 67 | public static Object toJavaType(JIVariant variant) throws JIException { 68 | int type = variant.getType(); 69 | 70 | if ((type & JIVariant.VT_ARRAY) == JIVariant.VT_ARRAY) { 71 | JIArray array = variant.getObjectAsArray(); 72 | 73 | return jIArrayToJavaArray(array, type); 74 | } else { 75 | 76 | switch (type) { 77 | case JIVariant.VT_EMPTY: 78 | case JIVariant.VT_NULL: 79 | return null; 80 | case JIVariant.VT_ERROR: 81 | return extractError(variant); 82 | case JIVariant.VT_I1: 83 | return Byte.valueOf((byte) variant.getObjectAsChar()); 84 | case JIVariant.VT_I2: 85 | return Short.valueOf(variant.getObjectAsShort()); 86 | case JIVariant.VT_I4: 87 | case JIVariant.VT_INT: 88 | return Integer.valueOf(variant.getObjectAsInt()); 89 | case JIVariant.VT_I8: 90 | return Long.valueOf(variant.getObjectAsLong()); 91 | case JIVariant.VT_DATE: 92 | return Instant.ofEpochMilli(variant.getObjectAsDate().getTime()); 93 | case JIVariant.VT_R4: 94 | return Float.valueOf(variant.getObjectAsFloat()); 95 | case JIVariant.VT_R8: 96 | return Double.valueOf(variant.getObjectAsDouble()); 97 | case JIVariant.VT_UI1: 98 | return Byte.valueOf(variant.getObjectAsUnsigned().getValue().byteValue()); 99 | case JIVariant.VT_UI2: 100 | return Short.valueOf(variant.getObjectAsUnsigned().getValue().shortValue()); 101 | case JIVariant.VT_UI4: 102 | case JIVariant.VT_UINT: 103 | return Integer.valueOf(variant.getObjectAsUnsigned().getValue().intValue()); 104 | case JIVariant.VT_BSTR: 105 | return String.valueOf(variant.getObjectAsString2()); 106 | case JIVariant.VT_BOOL: 107 | return Boolean.valueOf(variant.getObjectAsBoolean()); 108 | case JIVariant.VT_CY: 109 | JICurrency currency = (JICurrency) variant.getObject(); 110 | 111 | BigDecimal cyRetVal = currencyToBigDecimal(currency); 112 | 113 | return cyRetVal; 114 | default: 115 | final String value = variant.getObject().toString(); 116 | logger.warn(DEFAULT_MSG, value, variant.getObject().getClass().getName(), Integer.toHexString(type)); 117 | return value; 118 | } 119 | } 120 | } 121 | 122 | private static BigDecimal currencyToBigDecimal(JICurrency currency) { 123 | BigDecimal cyRetVal = new BigDecimal(currency.getUnits() + ((double) currency.getFractionalUnits() / 10000)); 124 | return cyRetVal; 125 | } 126 | 127 | 128 | /** 129 | * Converts a {@link JIArray} to a Java array 130 | * 131 | * @param jIArray the array to be converted 132 | * @param type the type of array items 133 | * @return an array of java objects. 134 | */ 135 | public static Object[] jIArrayToJavaArray(JIArray jIArray, int type) { 136 | 137 | Object[] objArray = (Object[]) jIArray.getArrayInstance(); 138 | int arrayLength = objArray.length; 139 | 140 | switch (type ^ JIVariant.VT_ARRAY) { 141 | case JIVariant.VT_EMPTY: 142 | case JIVariant.VT_NULL: 143 | return new Void[objArray.length]; 144 | case JIVariant.VT_DATE: 145 | return Arrays.stream(objArray).map(d -> Instant.ofEpochMilli(((Date) d).getTime())).toArray(); 146 | //JInterop seems to be handling most of these to java types already... 147 | case JIVariant.VT_ERROR: 148 | case JIVariant.VT_I1: 149 | case JIVariant.VT_I2: 150 | case JIVariant.VT_I4: 151 | case JIVariant.VT_I8: 152 | case JIVariant.VT_INT: 153 | case JIVariant.VT_R4: 154 | case JIVariant.VT_R8: 155 | case JIVariant.VT_UI1: 156 | case JIVariant.VT_UI2: 157 | case JIVariant.VT_UI4: 158 | case JIVariant.VT_UINT: 159 | case JIVariant.VT_BOOL: 160 | return objArray; 161 | case JIVariant.VT_CY: 162 | BigDecimal[] cyRetVal = new BigDecimal[arrayLength]; 163 | for (int i = 0; i < arrayLength; i++) { 164 | cyRetVal[i] = currencyToBigDecimal((JICurrency) objArray[i]); 165 | } 166 | return cyRetVal; 167 | case JIVariant.VT_BSTR: 168 | String[] strRetVal = new String[arrayLength]; 169 | for (int i = 0; i < arrayLength; i++) { 170 | strRetVal[i] = ((JIString) objArray[i]).getString(); 171 | } 172 | return strRetVal; 173 | default: 174 | logger.warn(DEFAULT_MSG, jIArray, jIArray.getArrayClass().getName(), Integer.toHexString(type)); 175 | return objArray; 176 | } 177 | } 178 | 179 | 180 | /** 181 | * Returns the java type corresponding to the encoded variant type. 182 | * 183 | * @param type the data type 184 | * @return the matching java class 185 | */ 186 | public static Class findJavaClass(int type) { 187 | boolean isArray = false; 188 | if ((type & JIVariant.VT_ARRAY) == JIVariant.VT_ARRAY) { 189 | isArray = true; 190 | type = type ^ JIVariant.VT_ARRAY; 191 | } 192 | 193 | 194 | switch (type) { 195 | case JIVariant.VT_I1: 196 | return isArray ? Character[].class : Character.class; 197 | case JIVariant.VT_I2: 198 | case JIVariant.VT_UI2: 199 | return isArray ? Short[].class : Short.class; 200 | case JIVariant.VT_I4: 201 | case JIVariant.VT_INT: 202 | case JIVariant.VT_UI4: 203 | case JIVariant.VT_UINT: 204 | case JIVariant.VT_ERROR: 205 | return isArray ? Integer[].class : Integer.class; 206 | case JIVariant.VT_I8: 207 | return isArray ? Long[].class : Long.class; 208 | case JIVariant.VT_DATE: 209 | return isArray ? Instant[].class : Instant.class; 210 | case JIVariant.VT_R4: 211 | return isArray ? Float[].class : Float.class; 212 | case JIVariant.VT_R8: 213 | return isArray ? Double[].class : Double.class; 214 | case JIVariant.VT_UI1: 215 | return isArray ? Byte[].class : Byte.class; 216 | case JIVariant.VT_BOOL: 217 | return isArray ? Boolean[].class : Boolean.class; 218 | case JIVariant.VT_CY: 219 | return isArray ? BigDecimal[].class : BigDecimal.class; 220 | case JIVariant.VT_EMPTY: 221 | case JIVariant.VT_NULL: 222 | return isArray ? Void[].class : Void.class; 223 | default: 224 | return isArray ? String[].class : String.class; 225 | } 226 | } 227 | } 228 | 229 | 230 | -------------------------------------------------------------------------------- /src/test/java/com/hurence/opc/ua/TestNamespace.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | 21 | import org.eclipse.milo.opcua.sdk.core.AccessLevel; 22 | import org.eclipse.milo.opcua.sdk.core.Reference; 23 | import org.eclipse.milo.opcua.sdk.core.ValueRanks; 24 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 25 | import org.eclipse.milo.opcua.sdk.server.api.*; 26 | import org.eclipse.milo.opcua.sdk.server.api.nodes.VariableNode; 27 | import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.FolderNode; 28 | import org.eclipse.milo.opcua.sdk.server.model.nodes.variables.AnalogItemNode; 29 | import org.eclipse.milo.opcua.sdk.server.nodes.*; 30 | import org.eclipse.milo.opcua.sdk.server.nodes.delegates.AttributeDelegate; 31 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 32 | import org.eclipse.milo.opcua.stack.core.Identifiers; 33 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 34 | import org.eclipse.milo.opcua.stack.core.UaException; 35 | import org.eclipse.milo.opcua.stack.core.types.builtin.*; 36 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte; 37 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort; 38 | import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; 39 | import org.eclipse.milo.opcua.stack.core.types.structured.Range; 40 | import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; 41 | import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue; 42 | 43 | import java.util.ArrayList; 44 | import java.util.List; 45 | import java.util.Optional; 46 | import java.util.concurrent.CompletableFuture; 47 | import java.util.stream.Collectors; 48 | 49 | public class TestNamespace implements Namespace { 50 | 51 | 52 | public static final String URI = "urn:test:namespace"; 53 | 54 | private final UShort index; 55 | 56 | private final ServerNodeMap nodeMap; 57 | private final SubscriptionModel subscriptionModel; 58 | private final NodeFactory nodeFactory; 59 | 60 | public TestNamespace(final UShort index, final OpcUaServer server) { 61 | this.index = index; 62 | this.nodeMap = server.getNodeMap(); 63 | this.subscriptionModel = new SubscriptionModel(server, this); 64 | this.nodeFactory = new NodeFactory(nodeMap, server.getObjectTypeManager(), server.getVariableTypeManager()); 65 | registerItems(); 66 | } 67 | 68 | private void registerItems() { 69 | 70 | // create a folder 71 | 72 | final UaFolderNode folder = new UaFolderNode( 73 | this.nodeMap, 74 | new NodeId(this.index, 1), 75 | new QualifiedName(this.index, "TestFolder"), 76 | LocalizedText.english("Test folder")); 77 | 78 | // add our folder to the objects folder 79 | 80 | this.nodeMap.getNode(Identifiers.ObjectsFolder).ifPresent(node -> { 81 | ((FolderNode) node).addComponent(folder); 82 | }); 83 | 84 | // add single variable 85 | 86 | { 87 | final AnalogItemNode variable = nodeFactory.createVariable(new NodeId(this.index, "sint"), 88 | new QualifiedName(this.index, "SinT"), 89 | LocalizedText.english("Sinus of (t)"), 90 | Identifiers.AnalogItemType, 91 | AnalogItemNode.class); 92 | variable.setAttributeDelegate(new AttributeDelegate() { 93 | 94 | @Override 95 | public DataValue getValue(AttributeContext context, VariableNode node) throws UaException { 96 | return new DataValue(new Variant(Math.sin(2.0 * Math.PI * System.currentTimeMillis() / 1000.0)), 97 | StatusCode.GOOD, 98 | DateTime.now()); 99 | } 100 | }); 101 | 102 | 103 | variable.setInstrumentRange((new Range(-1.0, +1.0))); 104 | variable.setDataType(Identifiers.Double); 105 | variable.setValueRank(ValueRanks.Scalar); 106 | variable.setAccessLevel(UByte.valueOf(AccessLevel.getMask(AccessLevel.READ_ONLY))); 107 | variable.setUserAccessLevel(UByte.valueOf(AccessLevel.getMask(AccessLevel.READ_ONLY))); 108 | variable.setDescription(LocalizedText.english("Sinusoid signal")); 109 | folder.addOrganizes(variable); 110 | } 111 | 112 | // Dynamic Double 113 | { 114 | String name = "Double"; 115 | NodeId typeId = Identifiers.Double; 116 | Variant variant = new Variant(0.0); 117 | 118 | UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(nodeMap) 119 | .setNodeId(new NodeId(index, "HelloWorld/Dynamic/" + name)) 120 | .setAccessLevel(UByte.valueOf(AccessLevel.getMask(AccessLevel.READ_WRITE))) 121 | .setUserAccessLevel(UByte.valueOf(AccessLevel.getMask(AccessLevel.READ_WRITE))) 122 | .setBrowseName(new QualifiedName(index, name)) 123 | .setDisplayName(LocalizedText.english(name)) 124 | .setDataType(typeId) 125 | .setTypeDefinition(Identifiers.BaseDataVariableType) 126 | .build(); 127 | 128 | node.setValue(new DataValue(variant)); 129 | 130 | 131 | nodeMap.addNode(node); 132 | folder.addOrganizes(node); 133 | } 134 | 135 | 136 | } 137 | 138 | @Override 139 | public void read(ReadContext context, Double maxAge, TimestampsToReturn timestamps, List readValueIds) { 140 | final List results = new ArrayList<>(readValueIds.size()); 141 | for (final ReadValueId id : readValueIds) { 142 | final ServerNode node = this.nodeMap.get(id.getNodeId()); 143 | 144 | final DataValue value = node != null 145 | ? node.readAttribute(new AttributeContext(context), id.getAttributeId()) 146 | : new DataValue(StatusCodes.Bad_NodeIdUnknown); 147 | results.add(value); 148 | } 149 | // report back with result 150 | context.complete(results); 151 | } 152 | 153 | 154 | @Override 155 | public void write( 156 | final WriteContext context, 157 | final List writeValues) { 158 | 159 | final List results = writeValues.stream() 160 | .map(value -> { 161 | if (this.nodeMap.containsKey(value.getNodeId())) { 162 | try { 163 | nodeMap.getNode(value.getNodeId()).get().writeAttribute( 164 | new AttributeContext(context.getServer(), context.getSession().orElse(null)), 165 | value.getAttributeId(), 166 | value.getValue(), 167 | value.getIndexRange()); 168 | 169 | } catch (UaException e) { 170 | return e.getStatusCode(); 171 | } 172 | //server 173 | } else { 174 | return new StatusCode(StatusCodes.Bad_NodeIdUnknown); 175 | } 176 | return StatusCode.GOOD; 177 | }) 178 | .collect(Collectors.toList()); 179 | 180 | // report back with result 181 | 182 | context.complete(results); 183 | } 184 | 185 | @Override 186 | public CompletableFuture> browse(final AccessContext context, final NodeId nodeId) { 187 | final ServerNode node = this.nodeMap.get(nodeId); 188 | 189 | if (node != null) { 190 | return CompletableFuture.completedFuture(node.getReferences()); 191 | } else { 192 | final CompletableFuture> f = new CompletableFuture<>(); 193 | f.completeExceptionally(new UaException(StatusCodes.Bad_NodeIdUnknown)); 194 | return f; 195 | } 196 | } 197 | 198 | @Override 199 | public Optional getInvocationHandler(final NodeId methodId) { 200 | return Optional 201 | .ofNullable(this.nodeMap.get(methodId)) 202 | .filter(n -> n instanceof UaMethodNode) 203 | .map(n -> { 204 | final UaMethodNode m = (UaMethodNode) n; 205 | return m.getInvocationHandler() 206 | .orElse(new MethodInvocationHandler.NotImplementedHandler()); 207 | }); 208 | } 209 | 210 | @Override 211 | public void onDataItemsCreated(final List dataItems) { 212 | this.subscriptionModel.onDataItemsCreated(dataItems); 213 | } 214 | 215 | @Override 216 | public void onDataItemsModified(final List dataItems) { 217 | this.subscriptionModel.onDataItemsModified(dataItems); 218 | } 219 | 220 | @Override 221 | public void onDataItemsDeleted(final List dataItems) { 222 | this.subscriptionModel.onDataItemsDeleted(dataItems); 223 | } 224 | 225 | @Override 226 | public void onMonitoringModeChanged(final List monitoredItems) { 227 | this.subscriptionModel.onMonitoringModeChanged(monitoredItems); 228 | } 229 | 230 | @Override 231 | public UShort getNamespaceIndex() { 232 | return this.index; 233 | } 234 | 235 | @Override 236 | public String getNamespaceUri() { 237 | return URI; 238 | } 239 | 240 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/da/OpcDaSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | import com.hurence.opc.OpcData; 21 | import com.hurence.opc.OpcOperations; 22 | import com.hurence.opc.OpcSession; 23 | import com.hurence.opc.OperationStatus; 24 | import com.hurence.opc.exception.OpcException; 25 | import io.reactivex.Flowable; 26 | import io.reactivex.Single; 27 | import org.jinterop.dcom.common.JIException; 28 | import org.jinterop.dcom.core.JIVariant; 29 | import org.openscada.opc.dcom.common.KeyedResult; 30 | import org.openscada.opc.dcom.common.KeyedResultSet; 31 | import org.openscada.opc.dcom.common.ResultSet; 32 | import org.openscada.opc.dcom.da.OPCDATASOURCE; 33 | import org.openscada.opc.dcom.da.OPCITEMDEF; 34 | import org.openscada.opc.dcom.da.OPCITEMSTATE; 35 | import org.openscada.opc.dcom.da.WriteRequest; 36 | import org.openscada.opc.dcom.da.impl.OPCGroupStateMgt; 37 | import org.openscada.opc.dcom.da.impl.OPCItemMgt; 38 | import org.openscada.opc.dcom.da.impl.OPCServer; 39 | import org.openscada.opc.dcom.da.impl.OPCSyncIO; 40 | import org.slf4j.Logger; 41 | import org.slf4j.LoggerFactory; 42 | 43 | import java.lang.ref.WeakReference; 44 | import java.time.Duration; 45 | import java.util.*; 46 | import java.util.concurrent.TimeUnit; 47 | import java.util.concurrent.atomic.AtomicInteger; 48 | import java.util.concurrent.atomic.AtomicLong; 49 | import java.util.function.Function; 50 | import java.util.stream.Collectors; 51 | 52 | 53 | /** 54 | * OPC-DA implementation for {@link OpcOperations} 55 | * 56 | * @author amarziali 57 | */ 58 | public class OpcDaSession implements OpcSession { 59 | 60 | private static final Logger logger = LoggerFactory.getLogger(OpcDaSession.class); 61 | 62 | private OPCGroupStateMgt group; 63 | private Map> handlesMap = new HashMap<>(); 64 | private static final AtomicInteger clientHandleCounter = new AtomicInteger(); 65 | private OPCSyncIO syncIO; 66 | private OPCItemMgt opcItemMgt; 67 | private OPCDATASOURCE datasource; 68 | private final WeakReference creatingOperations; 69 | private final Map dataTypeMap; 70 | private final Map refcountMap = Collections.synchronizedMap(new HashMap<>()); 71 | private final Flowable masterFlowable; 72 | 73 | private OpcDaSession(OpcDaTemplate creatingOperations, OPCGroupStateMgt group, OPCDATASOURCE datasource, 74 | Map dataTypeMap) 75 | throws JIException { 76 | this.group = group; 77 | this.opcItemMgt = group.getItemManagement(); 78 | this.syncIO = group.getSyncIO(); 79 | this.datasource = datasource; 80 | this.creatingOperations = new WeakReference<>(creatingOperations); 81 | this.dataTypeMap = dataTypeMap; 82 | try { 83 | long refreshRate = group.getState().getUpdateRate(); 84 | logger.info("Using revised session refresh rate: {} milliseconds", refreshRate); 85 | //start emitting hot flowable. 86 | masterFlowable = Flowable.interval(refreshRate, TimeUnit.MILLISECONDS) 87 | .takeWhile(ignored -> this.group != null) 88 | .filter(ignored -> !refcountMap.isEmpty()) 89 | .flatMap(ignored -> read(refcountMap.keySet().toArray(new String[refcountMap.size()])) 90 | .flattenAsFlowable(opcData -> opcData) 91 | ).share(); 92 | 93 | } catch (JIException e) { 94 | throw new OpcException("Unable to get revised refresh interval", e); 95 | } 96 | } 97 | 98 | static OpcDaSession create(OPCServer server, OpcDaSessionProfile sessionProfile, OpcDaTemplate creatingOperations) { 99 | try { 100 | return new OpcDaSession(creatingOperations, 101 | server.addGroup(null, true, 102 | (int) sessionProfile.getRefreshInterval().toMillis(), clientHandleCounter.incrementAndGet(), 103 | null, null, 0), 104 | sessionProfile.isDirectRead() ? OPCDATASOURCE.OPC_DS_DEVICE : OPCDATASOURCE.OPC_DS_CACHE, 105 | sessionProfile.getDataTypeOverrideMap()); 106 | } catch (Exception e) { 107 | throw new OpcException("Unable to create an OPC-DA session", e); 108 | } 109 | } 110 | 111 | /** 112 | * @param opcServer 113 | */ 114 | public void cleanup(OPCServer opcServer) { 115 | logger.info("Cleaning session"); 116 | try { 117 | opcServer.removeGroup(group, true); 118 | } catch (JIException e) { 119 | logger.warn("Unable to properly remove group from opc server", e); 120 | if (handlesMap != null) { 121 | handlesMap.clear(); 122 | } 123 | handlesMap = null; 124 | group = null; 125 | opcItemMgt = null; 126 | syncIO = null; 127 | } 128 | } 129 | 130 | 131 | @Override 132 | public Single> read(String... tags) { 133 | return Single.fromCallable(() -> { 134 | if (group == null) { 135 | throw new OpcException("Unable to read tags. Session has been detached!"); 136 | } 137 | Map> tagsHandles = 138 | Arrays.stream(tags).collect(Collectors.toMap(Function.identity(), this::resolveItemHandles)); 139 | Map mapsToClientHandles = tagsHandles.entrySet().stream() 140 | .collect(Collectors.toMap(e -> e.getValue().getValue(), e -> e.getKey())); 141 | KeyedResultSet result; 142 | try { 143 | result = syncIO.read(datasource, tagsHandles.values().stream().map(Map.Entry::getKey).toArray(a -> new Integer[a])); 144 | return result.stream() 145 | .map(KeyedResult::getValue) 146 | .filter(value -> mapsToClientHandles.containsKey(value.getClientHandle())) 147 | .map(value -> { 148 | try { 149 | return new OpcData<>(mapsToClientHandles.get(value.getClientHandle()), 150 | value.getTimestamp().asBigDecimalCalendar().toInstant(), 151 | OpcDaQualityExtractor.quality(value.getQuality()), 152 | JIVariantMarshaller.toJavaType(value.getValue()), 153 | OpcDaQualityExtractor.operationStatus(value.getQuality())); 154 | } catch (JIException e) { 155 | throw new OpcException("Unable to read tag " + value, e); 156 | } 157 | }).collect(Collectors.toList()); 158 | 159 | } catch (JIException e) { 160 | throw new OpcException("Unable to read tags", e); 161 | } 162 | }); 163 | } 164 | 165 | 166 | @Override 167 | public Single> write(OpcData... data) { 168 | return Single.fromCallable(() -> { 169 | if (group == null) { 170 | throw new OpcException("Unable to write tags. Session has been detached!"); 171 | } 172 | try { 173 | ResultSet result = syncIO.write(Arrays.stream(data) 174 | .map(d -> new WriteRequest(resolveItemHandles(d.getTag()).getKey(), JIVariant.makeVariant(d.getValue()))) 175 | .toArray(a -> new WriteRequest[a])); 176 | return result.stream() 177 | .map(OpcDaQualityExtractor::operationStatus) 178 | .collect(Collectors.toList()); 179 | } catch (Exception e) { 180 | throw new OpcException("Unable to write data", e); 181 | } 182 | }); 183 | } 184 | 185 | 186 | private void incrementRefCount(String tagId) { 187 | if (refcountMap != null) { 188 | refcountMap.compute(tagId, (s, atomicLong) -> { 189 | if (atomicLong == null) { 190 | atomicLong = new AtomicLong(); 191 | } 192 | atomicLong.incrementAndGet(); 193 | return atomicLong; 194 | }); 195 | } 196 | } 197 | 198 | private void decrementRefCount(String tagId) { 199 | if (refcountMap != null) { 200 | refcountMap.compute(tagId, (s, atomicLong) -> { 201 | if (atomicLong != null && atomicLong.decrementAndGet() <= 0) { 202 | atomicLong = null; 203 | } 204 | return atomicLong; 205 | }); 206 | } 207 | } 208 | 209 | @Override 210 | public Flowable stream(String tagId, Duration samplingInterval) { 211 | if (masterFlowable == null) { 212 | return Flowable.error(new OpcException("Unable to read tags. Session has been detached!")); 213 | } 214 | //validate tag 215 | return Single.fromCallable(() -> resolveItemHandles(tagId)) 216 | .ignoreElement() 217 | .andThen(masterFlowable) 218 | .filter(opcData -> opcData.getTag().equals(tagId)) 219 | .distinctUntilChanged() 220 | .throttleLatest(samplingInterval.toNanos(), TimeUnit.NANOSECONDS) 221 | .doOnSubscribe(ignored -> incrementRefCount(tagId)) 222 | .doOnTerminate(() -> decrementRefCount(tagId)); 223 | } 224 | 225 | 226 | /** 227 | * Resolve tag names into server/client couple of Integer handles looking in a local cache to avoid resolving several time the same object. 228 | * 229 | * @param tag the tag to resolve. 230 | * @return a couple of Integers. First is the server handle. Second is the client handle. 231 | */ 232 | private synchronized Map.Entry resolveItemHandles(String tag) { 233 | Map.Entry handles = handlesMap.get(tag); 234 | if (handles == null) { 235 | OPCITEMDEF opcitemdef = new OPCITEMDEF(); 236 | opcitemdef.setActive(true); 237 | opcitemdef.setClientHandle(clientHandleCounter.incrementAndGet()); 238 | opcitemdef.setItemID(tag); 239 | opcitemdef.setRequestedDataType(dataTypeMap.getOrDefault(tag, (short) JIVariant.VT_EMPTY)); 240 | try { 241 | Integer serverHandle = opcItemMgt.add(opcitemdef).get(0).getValue().getServerHandle(); 242 | if (serverHandle == null || serverHandle == 0) { 243 | throw new OpcException("Received invalid handle from OPC server."); 244 | } 245 | handles = new AbstractMap.SimpleEntry<>(serverHandle, opcitemdef.getClientHandle()); 246 | } catch (Exception e) { 247 | throw new OpcException("Unable to add item " + tag, e); 248 | } 249 | handlesMap.put(tag, handles); 250 | } 251 | return handles; 252 | } 253 | 254 | @Override 255 | public void close() { 256 | if (creatingOperations != null && creatingOperations.get() != null) { 257 | try { 258 | creatingOperations.get().releaseSession(this).blockingAwait(); 259 | } finally { 260 | creatingOperations.clear(); 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main/java/com/hurence/opc/ua/OpcUaSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.hurence.opc.OpcData; 21 | import com.hurence.opc.OpcSession; 22 | import com.hurence.opc.OperationStatus; 23 | import com.hurence.opc.exception.OpcException; 24 | import io.reactivex.Flowable; 25 | import io.reactivex.Single; 26 | import io.reactivex.disposables.Disposable; 27 | import io.reactivex.processors.UnicastProcessor; 28 | import io.reactivex.subjects.CompletableSubject; 29 | import org.eclipse.milo.opcua.sdk.client.OpcUaClient; 30 | import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem; 31 | import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription; 32 | import org.eclipse.milo.opcua.stack.core.AttributeId; 33 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 34 | import org.eclipse.milo.opcua.stack.core.types.builtin.*; 35 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 36 | import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode; 37 | import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; 38 | import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest; 39 | import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters; 40 | import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; 41 | import org.slf4j.Logger; 42 | import org.slf4j.LoggerFactory; 43 | 44 | import java.lang.ref.WeakReference; 45 | import java.time.Duration; 46 | import java.time.Instant; 47 | import java.util.*; 48 | import java.util.concurrent.TimeUnit; 49 | import java.util.concurrent.atomic.AtomicInteger; 50 | import java.util.stream.Collectors; 51 | 52 | /** 53 | * The OCP-UA Session. 54 | * This object should be used to read/write/stream data from/to an UA server. 55 | * 56 | * @author amarziali 57 | */ 58 | public class OpcUaSession implements OpcSession { 59 | 60 | private static final Logger logger = LoggerFactory.getLogger(OpcUaSession.class); 61 | 62 | 63 | private static final AtomicInteger clientHandleCounter = new AtomicInteger(); 64 | private final Duration publicationInterval; 65 | private final WeakReference client; 66 | private final WeakReference creatingOperations; 67 | private UaSubscription subscription; 68 | private final CompletableSubject terminationSignal = CompletableSubject.create(); 69 | 70 | 71 | private OpcUaSession(OpcUaTemplate creatingOperations, 72 | OpcUaClient client, 73 | Duration publicationInterval) { 74 | this.client = new WeakReference<>(client); 75 | this.creatingOperations = new WeakReference<>(creatingOperations); 76 | this.publicationInterval = publicationInterval; 77 | } 78 | 79 | 80 | static OpcUaSession create(OpcUaTemplate creatingOperations, 81 | OpcUaClient client, 82 | OpcUaSessionProfile sessionProfile) { 83 | try { 84 | return new OpcUaSession(creatingOperations, client, sessionProfile.getPublicationInterval()); 85 | 86 | } catch (Exception e) { 87 | throw new OpcException("Unable to create an OPC-UA session", e); 88 | } 89 | } 90 | 91 | private synchronized UaSubscription subscription() { 92 | try { 93 | if (subscription == null && client.get() != null) { 94 | subscription = client.get().getSubscriptionManager().createSubscription(Math.round(publicationInterval.toNanos() / 1.0e6)).get(); 95 | } 96 | } catch (Exception e) { 97 | throw new OpcException("Unable to create subscription", e); 98 | } 99 | return this.subscription; 100 | } 101 | 102 | 103 | public void cleanup() { 104 | logger.info("Destroying UA session"); 105 | try { 106 | if (client.get() != null) { 107 | if (subscription != null) { 108 | client.get().getSubscriptionManager().deleteSubscription(subscription.getSubscriptionId()) 109 | .get(client.get().getConfig().getRequestTimeout().longValue(), TimeUnit.MILLISECONDS); 110 | logger.info("Released subscription {}", subscription.getSubscriptionId()); 111 | } 112 | 113 | } 114 | } catch (Exception e) { 115 | logger.warn("Unable to properly clear subscription " + subscription.getSubscriptionId(), e); 116 | } finally { 117 | subscription = null; 118 | client.clear(); 119 | terminationSignal.onComplete(); 120 | } 121 | } 122 | 123 | private Single fetchValidClient() { 124 | if (client.get() == null) { 125 | return Single.error(new OpcException("Unable to read items. OPC-UA Client has been garbage collected. Please use a fresher instance")); 126 | } 127 | return Single.just(client.get()); 128 | } 129 | 130 | 131 | private OpcData opcData(String tag, DataValue dataValue) { 132 | Instant instant = Instant.now(); 133 | DateTime dt = null; 134 | double picos = 0.0; 135 | if (dataValue.getSourceTime() != null) { 136 | dt = dataValue.getSourceTime(); 137 | if (dataValue.getSourcePicoseconds() != null) { 138 | picos = dataValue.getSourcePicoseconds().doubleValue(); 139 | } 140 | } else if (dataValue.getServerTime() != null) { 141 | dt = dataValue.getServerTime(); 142 | if (dataValue.getServerPicoseconds() != null) { 143 | picos = dataValue.getServerPicoseconds().doubleValue(); 144 | } 145 | } 146 | if (dt != null) { 147 | instant = dt.getJavaDate().toInstant() 148 | .plusNanos(Math.round(picos / 1.0e3)); 149 | } 150 | return new OpcData<>(tag, 151 | instant, 152 | OpcUaQualityExtractor.quality(dataValue.getStatusCode()), 153 | UaVariantMarshaller.toJavaType(dataValue.getValue()), 154 | OpcUaQualityExtractor.operationStatus(dataValue.getStatusCode())); 155 | } 156 | 157 | 158 | @Override 159 | public Single> read(String... tags) { 160 | return fetchValidClient() 161 | .flatMap(c -> Single.fromFuture( 162 | c.readValues(0.0, TimestampsToReturn.Both, Arrays.stream(tags).map(NodeId::parseSafe) 163 | .map(Optional::get).collect(Collectors.toList())) 164 | .thenApply(dataValues -> { 165 | if (dataValues.size() != tags.length) { 166 | throw new OpcException("Input tags does not match received tags. Aborting"); 167 | } 168 | List ret = new ArrayList<>(); 169 | for (int i = 0; i < dataValues.size(); i++) { 170 | try { 171 | ret.add(opcData(tags[i], dataValues.get(i))); 172 | 173 | } catch (Exception e) { 174 | logger.warn("Unable to properly map tag " + tags[i] + ". Skipping!", e); 175 | } 176 | } 177 | return ret; 178 | }))); 179 | } 180 | 181 | 182 | @Override 183 | public Single> write(OpcData... data) { 184 | return fetchValidClient() 185 | .flatMap(c -> Single.fromFuture(c.writeValues( 186 | Arrays.stream(data) 187 | .map(OpcData::getTag) 188 | .map(NodeId::parse) 189 | .collect(Collectors.toList()), 190 | Arrays.stream(data) 191 | .map(OpcData::getValue) 192 | .map(Variant::new) 193 | .map(DataValue::valueOnly) 194 | .collect(Collectors.toList()) 195 | ).thenApply(statusCodes -> statusCodes.stream() 196 | .map(OpcUaQualityExtractor::operationStatus) 197 | .collect(Collectors.toList())))); 198 | 199 | 200 | } 201 | 202 | @Override 203 | public Flowable stream(String tagId, Duration duration) { 204 | logger.info("Creating monitored item for tag {}", tagId); 205 | return Single.fromFuture(subscription().createMonitoredItems(TimestampsToReturn.Both, 206 | Collections.singletonList(new MonitoredItemCreateRequest( 207 | new ReadValueId(NodeId.parse(tagId), AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE), 208 | MonitoringMode.Reporting, 209 | new MonitoringParameters(UInteger.valueOf(clientHandleCounter.incrementAndGet()), 210 | (double) duration.toMillis(), 211 | null, 212 | UInteger.valueOf(Math.round(Math.ceil((double) publicationInterval.toNanos() / 213 | (double) duration.toNanos()))) 214 | , true))) 215 | ).toCompletableFuture()) 216 | .map(uaMonitoredItems -> uaMonitoredItems.stream() 217 | .findFirst() 218 | .orElseThrow(() -> new OpcException("Received empty response for subscription to tag " + tagId))) 219 | .toFlowable() 220 | .flatMap(uaMonitoredItem -> { 221 | logger.info("Subscription for item {} with revised polling time {}", 222 | uaMonitoredItem.getReadValueId().getNodeId().toParseableString(), 223 | uaMonitoredItem.getRevisedSamplingInterval()); 224 | 225 | UnicastProcessor ret = UnicastProcessor.create(); 226 | uaMonitoredItem.setValueConsumer((uaMonitoredItem1, dataValue) -> 227 | ret.onNext(opcData(uaMonitoredItem1.getReadValueId().getNodeId().toParseableString(), dataValue))); 228 | final Disposable disposable = terminationSignal.subscribe(() -> 229 | ret.onError(new OpcException("EOF reading from the stream. Client closed unexpectedly"))); 230 | return ret 231 | .doOnComplete(() -> { 232 | logger.info("Clearing subscription for item {}", uaMonitoredItem); 233 | removeSubscriptions(Collections.singletonList(uaMonitoredItem)); 234 | }).doFinally(() -> disposable.dispose()) 235 | .share(); 236 | 237 | }).takeWhile(ignored -> !terminationSignal.hasComplete()); 238 | } 239 | 240 | 241 | private void removeSubscriptions(List results) { 242 | if (subscription != null) { 243 | try { 244 | List removeResult = subscription.deleteMonitoredItems(results).get(); 245 | for (int i = 0; i < removeResult.size(); i++) { 246 | if (!removeResult.get(i).isGood()) { 247 | logger.warn("Unable to properly unsubscribe for item {}: {}", 248 | results.get(i).getReadValueId().getNodeId().toParseableString(), 249 | StatusCodes.lookup(removeResult.get(i).getValue())); 250 | } 251 | } 252 | } catch (Exception e) { 253 | logger.error("Unable to properly removed monitored items", e); 254 | } 255 | } 256 | } 257 | 258 | 259 | @Override 260 | public void close() throws Exception { 261 | if (creatingOperations.get() != null) { 262 | try { 263 | creatingOperations.get().releaseSession(this).blockingAwait(); 264 | } finally { 265 | creatingOperations.clear(); 266 | } 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OPC Simple 2 | 3 | OPC DA/UA made simple by [Hurence](https://www.hurence.com). 4 | 5 | An easy to use reactive and quite painless OPC UA/DA java library. 6 | 7 | Main benefits: 8 | 9 | - I support both OPC-DA and OPC-UA with a harmonized unique API. 10 | - I'm reactive (based on ReactiveX) and nonblocking operations makes me performing very fast. 11 | - I'm open source (Apache 2.0) 12 | - I'm portable (java based. No native code needed) 13 | 14 | ## Getting Started 15 | 16 | These instructions will help you to quick start using opc simple. 17 | 18 | ### Building 19 | 20 | You can build on your machine using maven and a jdk >= 1.8. 21 | 22 | Just trigger: 23 | 24 | ``` 25 | mvn clean install 26 | ``` 27 | 28 | ### Include in your project (with maven) 29 | 30 | 31 | Add The maven dependency 32 | ``` 33 | 34 | 35 | com.github.Hurence 36 | opc-simple 37 | 3.0.0-rc1 38 | 39 | 40 | ``` 41 | 42 | 43 | And the needed repositories 44 | 45 | ``` 46 | 47 | 48 | repository> 49 | jitpack.io 50 | https://jitpack.io 51 | 52 | 53 | ``` 54 | 55 | ### Examples 56 | 57 | A step by step series of examples to showcase basic use cases. 58 | 59 | 60 | #### Preamble 61 | 62 | The library is built as close as possible to the reactive manifesto paradigms and is based on the 63 | [RxJava](http://reactivex.io/) library. 64 | 65 | If you are not familiar with reactive programming, observer patterns, backpressure or with the rx-java 66 | library in general, you can have further readings on the 67 | [RxJava wiki](https://github.com/ReactiveX/RxJava/wiki/Additional-Reading) 68 | 69 | 70 | ##### Connect to an OPC-DA server 71 | 72 | As a prerequisite you should have an up an running OPC-DA server. In this example we'll use the 73 | [Matrikon OPC simulation server](https://www.matrikonopc.com/products/opc-drivers/opc-simulation-server.aspx). 74 | 75 | Please feel free to change connection settings reflecting your real environment. 76 | 77 | Follows a simple blocking example (see after below for more complex reactive examples). 78 | 79 | 80 | ```java 81 | 82 | 83 | //create a connection profile 84 | OpcDaConnectionProfile connectionProfile = new OpcDaConnectionProfile() 85 | //change with the appropriate clsid 86 | .withComClsId("F8582CF2-88FB-11D0-B850-00C0F0104305") 87 | .withCredentials(new NtLmCredentials() 88 | .withDomain("OPC-DOMAIN") 89 | .withUser("OPC") 90 | .withPassword("opc")) 91 | .withConnectionUri(new URI("opc.da://192.168.99.100")) 92 | .withSocketTimeout(Duration.of(5, ChronoUnit.SECONDS)); 93 | 94 | //Create an instance of a da operations 95 | OpcDaOperations opcDaOperations = new OpcDaTemplate(); 96 | //connect using our profile 97 | opcDaOperations.connect(connectionProfile).ignoreElement().blockingAwait(); 98 | 99 | 100 | ``` 101 | 102 | 103 | ##### Connect to an OPC-UA server 104 | 105 | As a prerequisite you should have an up an running OPC-UA server. In this example we'll use the 106 | [Prosys OPC-UA simulation server](https://www.prosysopc.com/products/opc-ua-simulation-server/). 107 | 108 | Please feel free to change connection settings reflecting your real environment. 109 | 110 | Follows a simple blocking example (see after below for more complex reactive examples). 111 | 112 | 113 | ```java 114 | 115 | 116 | //create a connection profile 117 | OpcUaConnectionProfile connectionProfile = new new OpcUaConnectionProfile() 118 | .withConnectionUri(URI.create("opc.tcp://localhost:53530/OPCUA/SimulationServer")) 119 | .withClientIdUri("hurence:opc-simple:client:test") 120 | .withClientName("Simple OPC test client") 121 | .withSocketTimeout(Duration.ofSeconds(5)); 122 | 123 | //Create an instance of a ua operations 124 | OpcUaOperations opcUaOperations = new OpcUaTemplate(); 125 | //connect using our profile 126 | opcUaOperations.connect(connectionProfile) 127 | .doOnError(throwable -> logger.error("Unable to connect", throwable)) 128 | .ignoreElement().blockingAwait(); 129 | 130 | 131 | 132 | ``` 133 | 134 | #### Browse a list of tags 135 | 136 | Assuming a connection is already in place, just browse the tags and print to stdout. 137 | 138 | Blocking example: 139 | 140 | ````java 141 | 142 | opcDaOperations.browseTags().foreachBlocking(System.out::println); 143 | //execution here is resumed when browse completed 144 | ```` 145 | 146 | Or in a "reactive way" 147 | 148 | ````java 149 | 150 | opcDaOperations.browseTags().subscribe(System.out::println); 151 | // code after is executed immediately without blocking (println is done asynchronously) 152 | System.out.println("I'm a reactive OPC-Simple application :-)"); 153 | 154 | ```` 155 | 156 | #### Browse the tree branch by branch 157 | 158 | Sometimes browsing the whole tree is too much time and resource consuming. 159 | As an alternative you can browse level by level. 160 | 161 | For instance you can browse what's inside the group _Square Waves_: 162 | ````java 163 | opcDaOperations.fetchNextTreeLevel("Square Waves") 164 | .subscribe(System.out::println); 165 | ```` 166 | 167 | #### Using Sessions 168 | 169 | Session are stateful abstractions sharing Connection. 170 | Hence multiple session can be created per connection. 171 | 172 | Session is the main entry point for the following actions: 173 | 174 | * Read 175 | * Write 176 | * Stream 177 | 178 | 179 | When creating a session you should specify some parameters depending on the OPC standard you are using (e.g. direct read from hardware for OPC-DA). 180 | 181 | Sessions should be created and released (beware leaks!) through the Connection object. 182 | 183 | > SessionProfile and OpcOperations interface extends AutoCloseable interface. 184 | > Hence you can use the handy *try-with-resources* syntax without taking care about destroying connection or sessions. 185 | 186 | 187 | Reactive tips: 188 | 189 | > - Close your sessions in a *doFinally* block if you want to avoid leaks and you do not need anymore the session 190 | > after downstream completes. 191 | > - You can use *flatmap* operator to chain flows after creation of a connection or a session. 192 | > - You can handle backpressure and tune up the scheduler to be used for observe/subscribe operations. 193 | > The library itself does not make any assumption on it. 194 | 195 | ##### Create an OPC-DA session 196 | 197 | An example (blocking version): 198 | 199 | ````java 200 | 201 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 202 | // direct read from device 203 | .withDirectRead(false) 204 | // refresh period 205 | .withRefreshInterval(Duration.ofMillis(100)); 206 | 207 | try (OpcSession session = opcDaOperations.createSession(sessionProfile).blockingGet()) { 208 | //do something useful with your session 209 | } 210 | ```` 211 | 212 | ##### Create an OPC-UA session 213 | 214 | An example (still blocking): 215 | 216 | ````java 217 | 218 | OpcUaSessionProfile sessionProfile = new OpcUaSessionProfile() 219 | //the publication window 220 | .withPublicationInterval(Duration.ofMillis(100)); 221 | 222 | 223 | try (OpcSession session = opcUaOperations.createSession(sessionProfile).blockingGet()) { 224 | //do something useful with your session 225 | } 226 | ```` 227 | 228 | ##### Create an OPC-UA session (reactive way) 229 | 230 | A more efficient nonblocking example here: 231 | 232 | ````java 233 | 234 | final OpcUaTemplate opcUaTemplate = new OpcUaTemplate() 235 | 236 | // first create a session with the desired profile 237 | opcUaTemplate.createSession(new OpcUaSessionProfile() 238 | .withPublicationInterval(Duration.ofMillis(100)))) 239 | // we got a single. Encapsulate in a flowable and chain 240 | .toFlowable() 241 | .flatMap(opcUaSession -> 242 | //do something more interesting with your session 243 | Flowable 244 | .empty() 245 | //avoid open session leaks 246 | .doFinally(opcUaSession::close) 247 | ) 248 | .subscribe(...); 249 | ```` 250 | 251 | #### Stream some tags readings 252 | 253 | Assuming a connection is already in place, just stream tags values 254 | and as soon as possible print their values to stdout. 255 | 256 | 257 | ````java 258 | 259 | final OpcDaTemplate opcDaTemplate = new OpcDaTemplate() 260 | 261 | // first create a session with the desired profile 262 | opcDaTemplate 263 | .createSession(new OpcDaSessionProfile() 264 | // direct read from device 265 | .withDirectRead(false) 266 | // refresh period 267 | .withRefreshInterval(Duration.ofMillis(100)) 268 | ) 269 | // we got a single. Encapsulate in a flowable and chain 270 | .toFlowable() 271 | .flatMap(opcUaSession -> 272 | // attach a stream to the session 273 | opcUaSession.stream("Square Waves.Real8", Duration.ofMillis(100)) 274 | // close the session upon completion or error 275 | .doFinally(opcUaSession::close) 276 | ) 277 | //buffer in case of backpressure (but you can also discard or keep latest) 278 | .onBackpressureBuffer() 279 | //avoid blocking current thread for iowaits 280 | .subscribeOn(Schedulers.io()) 281 | //take only first 100 elements 282 | .limit(100) 283 | //subscribe to events (upstream will start emitting events) 284 | .subscribe(opcData-> doSomethingWithData(opcData)); 285 | ```` 286 | 287 | #### Advanced: managing automatic reconnection 288 | 289 | With ReactiveX you can handle your stream as you want and even do some retry on error. 290 | 291 | A quick example: 292 | 293 | ````java 294 | 295 | //assumes connectionProfile and sessionProfile have already been defined. 296 | daTemplate 297 | //establish a connection 298 | .connect(connectionProfile) 299 | .toFlowable() 300 | .flatMap(client -> client.createSession(sessionProfile) 301 | //when ready create a subscription and start streaming some data 302 | .toFlowable() 303 | .flatMap(session -> 304 | session.stream("Saw-toothed Waves.UInt4", Duration.ofMillis(100)) 305 | ) 306 | //do not forget to close connections 307 | .doFinally(client::close) 308 | ) 309 | //log upstream failures 310 | .doOnError(throwable -> logger.warn("An error occurred. Retrying: " + throwable.getMessage())) 311 | // Retry anything in case something failed failed 312 | // You can use exp backoff or immediate as well 313 | .retryWhen(throwable -> throwable.delay(1, TimeUnit.SECONDS)) 314 | // handle schedulers 315 | .subscribeOn(Schedulers.io()) 316 | // handle backpressure 317 | .onBackpressureBuffer() 318 | // finally do something with this data :-) 319 | .subscribe(opcData-> doSomethingWithData(opcData)); 320 | 321 | ```` 322 | 323 | ### Integrate with other reactive frameworks 324 | 325 | Rx-Java uses its Scheduler and Threading models but sometimes there is the need to use another 326 | already in place thread pool. 327 | 328 | Here below you will find some examples. 329 | 330 | #### Integrate with Vert.x 331 | 332 | In order to best integrate with[Vert.x](https://vertx.io/) you should tell OPC simple to use the 333 | already in-place event loops provided by Vert.x 334 | 335 | First of all, you need to import the rx-fied version of Vertx: 336 | 337 | ``` 338 | 339 | io.vertx 340 | vertx-rx-java2 341 | 342 | 3.6.2 343 | 344 | ``` 345 | 346 | Then, as suggested by rx, you can override defaults schedulers in this way: 347 | 348 | ````java 349 | RxJavaPlugins.setComputationSchedulerHandler(s -> RxHelper.scheduler(vertx)); 350 | RxJavaPlugins.setIoSchedulerHandler(s -> RxHelper.blockingScheduler(vertx)); 351 | RxJavaPlugins.setNewThreadSchedulerHandler(s -> RxHelper.scheduler(vertx)); 352 | ```` 353 | 354 | The framework will do the rest to chose the right scheduler for blocking and computation operations. 355 | You can still use *subscribeOn* and *observeOn* to better tune the performances according to Rx-Java 356 | best practices. 357 | 358 | 359 | ## Authors 360 | 361 | * **Andrea Marziali** - *Initial work* - [amarziali](https://github.com/amarziali) 362 | 363 | See also the list of [contributors](https://github.com/Hurence/opc-simple/contributors) who participated in this project. 364 | 365 | ## License 366 | 367 | This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details 368 | 369 | ## Changelog 370 | 371 | Everything is tracked on a dedicate [CHANGELOG](CHANGELOG.md) file. 372 | 373 | ## Acknowledgments 374 | 375 | * Thanks to OpenSCADA and Utgard project contributors for their great work. 376 | * Thanks to Apache Milo for the great OPC-UA implementation. 377 | 378 | -------------------------------------------------------------------------------- /src/test/java/com/hurence/opc/da/OpcDaTemplateTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.da; 19 | 20 | import com.hurence.opc.OpcData; 21 | import com.hurence.opc.OpcSession; 22 | import com.hurence.opc.OpcTagInfo; 23 | import com.hurence.opc.OperationStatus; 24 | import com.hurence.opc.auth.NtlmCredentials; 25 | import com.hurence.opc.exception.OpcException; 26 | import io.reactivex.Flowable; 27 | import io.reactivex.schedulers.Schedulers; 28 | import io.reactivex.schedulers.Timed; 29 | import io.reactivex.subscribers.TestSubscriber; 30 | import org.jinterop.dcom.core.JIVariant; 31 | import org.junit.*; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | import java.net.URI; 36 | import java.time.Duration; 37 | import java.time.Instant; 38 | import java.time.temporal.ChronoUnit; 39 | import java.util.Arrays; 40 | import java.util.Collection; 41 | import java.util.List; 42 | import java.util.Random; 43 | import java.util.concurrent.TimeUnit; 44 | import java.util.stream.Collectors; 45 | 46 | /** 47 | * E2E test. You can run by spawning an OPC-DA test server and changing connection parameters to target it. 48 | * Currently the test is ignored during the build. 49 | * 50 | * @author amarziali 51 | */ 52 | @Ignore 53 | public class OpcDaTemplateTest { 54 | 55 | private final Logger logger = LoggerFactory.getLogger(OpcDaTemplateTest.class); 56 | 57 | 58 | private OpcDaOperations opcDaOperations; 59 | private OpcDaConnectionProfile connectionProfile; 60 | 61 | 62 | @Before 63 | public void init() throws Exception { 64 | opcDaOperations = new OpcDaTemplate(); 65 | connectionProfile = new OpcDaConnectionProfile() 66 | .withComClsId("F8582CF2-88FB-11D0-B850-00C0F0104305") 67 | .withCredentials(new NtlmCredentials() 68 | .withDomain("OPC-9167C0D9342") 69 | .withUser("OPC") 70 | .withPassword("opc")) 71 | .withConnectionUri(new URI("opc.da://192.168.99.100:135")) 72 | .withKeepAliveInterval(Duration.ofSeconds(5)) 73 | .withSocketTimeout(Duration.of(1, ChronoUnit.SECONDS)); 74 | 75 | opcDaOperations.connect(connectionProfile).ignoreElement().blockingAwait(); 76 | 77 | } 78 | 79 | @After 80 | public void done() throws Exception { 81 | opcDaOperations.disconnect().blockingAwait(); 82 | } 83 | 84 | 85 | @Test 86 | public void testBrowseTags() { 87 | logger.info("Received following tags {}", opcDaOperations.browseTags().toList().blockingGet()); 88 | } 89 | 90 | @Test 91 | public void testFetchLeaves() { 92 | opcDaOperations.fetchNextTreeLevel("Square Waves") 93 | .forEach(System.out::println); 94 | } 95 | 96 | @Test 97 | public void testFetchMetadata() { 98 | opcDaOperations.fetchMetadata("Random.Real8") 99 | .forEach(System.out::println); 100 | } 101 | 102 | 103 | @Test 104 | public void testSampling_Subscribe() throws Exception { 105 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 106 | .withDirectRead(false) 107 | .withRefreshInterval(Duration.ofMillis(300)); 108 | 109 | try (OpcSession session = opcDaOperations.createSession(sessionProfile).blockingGet()) { 110 | 111 | List received = session 112 | .stream("Square Waves.Real8", Duration.ofMillis(10)) 113 | .sample(1, TimeUnit.SECONDS) 114 | .limit(5) 115 | .map(a -> { 116 | Instant now = Instant.now(); 117 | System.out.println(a); 118 | return now; 119 | }).toList().blockingGet(); 120 | 121 | for (int i = 1; i < received.size(); i++) { 122 | Assert.assertTrue(received.get(i).toEpochMilli() - received.get(i - 1).toEpochMilli() >= 900); 123 | } 124 | 125 | } 126 | } 127 | 128 | @Test 129 | public void testSampling_Poll() throws Exception { 130 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 131 | .withDirectRead(false) 132 | .withRefreshInterval(Duration.ofMillis(300)); 133 | 134 | List> received = Flowable.combineLatest( 135 | Flowable.interval(10, TimeUnit.MILLISECONDS), 136 | opcDaOperations.createSession(sessionProfile).toFlowable() 137 | .flatMap(session -> session.stream("Square Waves.Real8", Duration.ofMillis(10)) 138 | .doFinally(session::close) 139 | 140 | ), (a, b) -> b) 141 | .sample(10, TimeUnit.MILLISECONDS) 142 | .timeInterval() 143 | .limit(100) 144 | .toList().blockingGet(); 145 | 146 | received.forEach(opcDataTimed -> logger.info("Received {}", opcDataTimed)); 147 | 148 | 149 | for (int i = 1; i < received.size(); i++) { 150 | Assert.assertTrue(received.get(i).time(TimeUnit.MILLISECONDS) - received.get(i - 1).time(TimeUnit.MILLISECONDS) < 15); 151 | } 152 | 153 | Assert.assertTrue(received.stream().map(a -> a.value().getValue()) 154 | .distinct().count() > 1); 155 | 156 | } 157 | 158 | @Test 159 | public void testStaticValues() throws Exception { 160 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 161 | .withDirectRead(false) 162 | .withRefreshInterval(Duration.ofMillis(300)); 163 | 164 | try (OpcDaSession writeSession = opcDaOperations.createSession(sessionProfile).blockingGet()) { 165 | 166 | //create a first stream to regularly write to a tag 167 | Flowable> writer = Flowable.interval(2, TimeUnit.SECONDS) 168 | .flatMap(ignored -> writeSession.write(new OpcData("Bucket Brigade.Real8", 169 | Instant.now(), new Random().nextDouble())).toFlowable()); 170 | 171 | 172 | Flowable flowable = opcDaOperations.createSession(sessionProfile) 173 | .toFlowable() 174 | .flatMap(session -> 175 | session.stream("Bucket Brigade.Real8", Duration.ofMillis(300)) 176 | .doFinally(session::close) 177 | ) 178 | .doOnNext(opcData -> logger.info("{}", opcData)) 179 | .subscribeOn(Schedulers.newThread()); 180 | TestSubscriber s1 = new TestSubscriber<>(); 181 | TestSubscriber s2 = new TestSubscriber<>(); 182 | writer.skipWhile(ignored -> !s2.hasSubscription()).limit(2).subscribe(s2); 183 | flowable.limit(2).subscribe(s1); 184 | s2.await(); 185 | s1.await() 186 | .assertComplete() 187 | .assertValueCount(2); 188 | } 189 | } 190 | 191 | 192 | @Test 193 | public void listenToTags() throws Exception { 194 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 195 | .withDirectRead(false) 196 | .withRefreshInterval(Duration.ofMillis(30)); 197 | 198 | try (OpcSession session = opcDaOperations.createSession(sessionProfile).blockingGet()) { 199 | TestSubscriber subscriber = new TestSubscriber<>(); 200 | List> flowables = Arrays.asList("Read Error.Int4", "Square Waves.Real8", "Random.ArrayOfString") 201 | .stream() 202 | .map(tagId -> session 203 | .stream(tagId, Duration.ofMillis(10)) 204 | .take(20) 205 | ).collect(Collectors.toList()); 206 | 207 | Flowable.merge(flowables) 208 | .subscribeOn(Schedulers.io()) 209 | .observeOn(Schedulers.computation()) 210 | .subscribe(subscriber); 211 | 212 | 213 | subscriber 214 | .await() 215 | .assertComplete() 216 | .assertValueCount(60) 217 | .dispose(); 218 | 219 | 220 | Assert.assertEquals(3, subscriber.values().stream().map(OpcData::getTag).distinct().count()); 221 | } 222 | 223 | } 224 | 225 | 226 | @Test 227 | public void testReadError() throws Exception { 228 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 229 | .withDirectRead(false) 230 | .withRefreshInterval(Duration.ofMillis(300)); 231 | 232 | TestSubscriber testSubscriber = new TestSubscriber<>(); 233 | 234 | opcDaOperations.createSession(sessionProfile) 235 | .flatMap(s -> 236 | s.read("Read Error.String") 237 | .flattenAsFlowable(a -> a) 238 | .takeUntil(data -> data.getOperationStatus().getLevel() == OperationStatus.Level.INFO) 239 | .firstOrError() 240 | .doFinally(s::close) 241 | ) 242 | .toFlowable() 243 | .doOnNext(opcData -> logger.info("Received {}", opcData)) 244 | .subscribe(testSubscriber); 245 | 246 | 247 | testSubscriber.await(); 248 | testSubscriber.assertComplete(); 249 | testSubscriber.assertValueCount(1); 250 | 251 | 252 | } 253 | 254 | 255 | @Test 256 | public void listenToArray() throws Exception { 257 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 258 | .withDirectRead(false) 259 | .withRefreshInterval(Duration.ofMillis(300)); 260 | 261 | TestSubscriber subscriber = new TestSubscriber<>(); 262 | 263 | opcDaOperations.createSession(sessionProfile) 264 | .toFlowable() 265 | .flatMap(session -> session.stream("Random.ArrayOfString", Duration.ofMillis(500))) 266 | .limit(10) 267 | .doOnNext(System.out::println) 268 | .subscribe(subscriber); 269 | 270 | subscriber.await() 271 | .assertComplete() 272 | .assertValueCount(10); 273 | 274 | 275 | } 276 | 277 | 278 | @Test 279 | public void listenToAll() throws Exception { 280 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 281 | .withDirectRead(false) 282 | .withRefreshInterval(Duration.ofMillis(10)); 283 | 284 | TestSubscriber subscriber = new TestSubscriber<>(); 285 | try (OpcDaSession session = opcDaOperations.createSession(sessionProfile).blockingGet()) { 286 | List tagList = opcDaOperations.browseTags().toList().blockingGet(); 287 | Flowable.merge(Flowable.fromArray(tagList.toArray(new OpcTagInfo[tagList.size()])) 288 | .map(tagInfo -> session.stream(tagInfo.getId(), Duration.ofMillis(100))) 289 | ) 290 | .doOnNext(data -> logger.info("{}", data)) 291 | .limit(1000) 292 | .subscribeOn(Schedulers.io()) 293 | .subscribe(subscriber); 294 | 295 | 296 | subscriber.await(); 297 | subscriber.assertComplete(); 298 | } 299 | 300 | } 301 | 302 | 303 | @Test 304 | public void forceDataTypeTest() throws Exception { 305 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 306 | .withDirectRead(false) 307 | .withRefreshInterval(Duration.ofSeconds(1)) 308 | .withDataTypeForTag("Bucket Brigade.Int4", (short) JIVariant.VT_R8); 309 | 310 | try (OpcDaSession session = opcDaOperations.createSession(sessionProfile).blockingGet()) { 311 | OpcData data = session.read("Bucket Brigade.Int4").blockingGet().get(0); 312 | System.out.println(data); 313 | Assert.assertNotNull(data); 314 | Assert.assertTrue(data.getValue() instanceof Double); 315 | } 316 | 317 | } 318 | 319 | 320 | @Test(expected = OpcException.class) 321 | public void testTagNotFound() throws Exception { 322 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 323 | .withDirectRead(false) 324 | .withRefreshInterval(Duration.ofMillis(300)); 325 | 326 | try (OpcSession session = opcDaOperations.createSession(sessionProfile).blockingGet()) { 327 | session.stream("I do not exist", Duration.ofMillis(500)) 328 | .blockingForEach(System.out::println); 329 | } 330 | } 331 | 332 | @Test 333 | public void testWriteValues() { 334 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 335 | .withDirectRead(false) 336 | .withRefreshInterval(Duration.ofMillis(300)); 337 | OpcDaSession session = null; 338 | 339 | try { 340 | session = opcDaOperations.createSession(sessionProfile).blockingGet(); 341 | Collection result = 342 | session.write(new OpcData<>("Square Waves.Real8", Instant.now(), 123.31)).blockingGet(); 343 | logger.info("Write result: {}", result); 344 | Assert.assertTrue(result 345 | .stream().noneMatch(operationStatus -> operationStatus.getLevel() != OperationStatus.Level.INFO)); 346 | 347 | 348 | } finally { 349 | opcDaOperations.releaseSession(session); 350 | } 351 | } 352 | 353 | @Test 354 | public void testWriteValuesFails() { 355 | OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 356 | .withDirectRead(false) 357 | .withRefreshInterval(Duration.ofMillis(300)); 358 | OpcDaSession session = null; 359 | 360 | try { 361 | session = opcDaOperations.createSession(sessionProfile).blockingGet(); 362 | Collection result = session.write(new OpcData("Square Waves.Real8", Instant.now(), "I'm not a number")).blockingGet(); 363 | logger.info("Write result: {}", result); 364 | Assert.assertFalse(result.stream() 365 | .noneMatch(operationStatus -> operationStatus.getLevel() != OperationStatus.Level.INFO)); 366 | } finally { 367 | opcDaOperations.releaseSession(session); 368 | } 369 | } 370 | 371 | @Test 372 | public void testAutoReconnect() throws Exception { 373 | 374 | final OpcDaTemplate daTemplate = new OpcDaTemplate(); 375 | final OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile() 376 | .withDirectRead(false) 377 | .withRefreshInterval(Duration.ofMillis(100)); 378 | final TestSubscriber subscriber = new TestSubscriber<>(); 379 | 380 | 381 | Flowable flowable = daTemplate 382 | //establish a connection 383 | .connect(connectionProfile) 384 | //log connection errors 385 | .doOnError(t -> logger.warn("Unable to connect. Retrying...: {}", t.getMessage())) 386 | .toFlowable() 387 | .flatMap(client -> client.createSession(sessionProfile) 388 | //when ready create a subscription and start streaming some data 389 | .toFlowable() 390 | .flatMap(session -> 391 | session.stream("Saw-toothed Waves.UInt4", Duration.ofMillis(100)) 392 | //do not forget to close connections 393 | .doFinally(client::close) 394 | ) 395 | ) 396 | //retry anything in case something failed failed 397 | .doOnError(throwable -> logger.warn("An error occurred. Retrying: " + throwable.getMessage())) 398 | .retryWhen(throwable -> throwable.delay(1, TimeUnit.SECONDS)) 399 | .subscribeOn(Schedulers.io()) 400 | //create an hot flowable 401 | .publish() 402 | .autoConnect(); 403 | 404 | //create a deferred stream to simulate a disconnection 405 | flowable.limit(20) 406 | .doOnComplete(() -> daTemplate.disconnect().blockingAwait()) 407 | //and attach it 408 | .subscribe(); 409 | 410 | //attach now the real consumer 411 | flowable 412 | //look just for 300 values 413 | .limit(50) 414 | .doOnNext(data -> logger.info("Received {}", data)) 415 | .subscribe(subscriber); 416 | 417 | 418 | subscriber.await(); 419 | subscriber.assertComplete(); 420 | subscriber.assertValueCount(50); 421 | subscriber.dispose(); 422 | 423 | } 424 | 425 | 426 | } 427 | -------------------------------------------------------------------------------- /src/test/java/com/hurence/opc/ua/OpcUaTemplateTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Hurence (support@hurence.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.hurence.opc.ua; 19 | 20 | import com.hurence.opc.*; 21 | import com.hurence.opc.auth.Credentials; 22 | import com.hurence.opc.auth.UsernamePasswordCredentials; 23 | import com.hurence.opc.auth.X509Credentials; 24 | import com.hurence.opc.exception.OpcException; 25 | import io.reactivex.Flowable; 26 | import io.reactivex.flowables.ConnectableFlowable; 27 | import io.reactivex.schedulers.Schedulers; 28 | import io.reactivex.subscribers.TestSubscriber; 29 | import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator; 30 | import org.junit.*; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | 34 | import java.net.InetAddress; 35 | import java.net.URI; 36 | import java.security.KeyPair; 37 | import java.security.cert.X509Certificate; 38 | import java.time.Duration; 39 | import java.time.Instant; 40 | import java.util.ArrayList; 41 | import java.util.Collection; 42 | import java.util.List; 43 | import java.util.Optional; 44 | import java.util.concurrent.TimeUnit; 45 | 46 | /** 47 | * {@link OpcUaTemplate} tests. 48 | * This suite spawns a fake OPC-UA test on localhost on any free port. 49 | * 50 | * @author amarziali 51 | */ 52 | public class OpcUaTemplateTest { 53 | 54 | private static final Logger logger = LoggerFactory.getLogger(OpcUaTemplateTest.class); 55 | 56 | private static TestOpcServer server; 57 | 58 | @BeforeClass 59 | public static void initServer() throws Exception { 60 | server = new TestOpcServer(InetAddress.getLoopbackAddress(), null); 61 | server.getInstance().startup().get(); 62 | } 63 | 64 | @AfterClass 65 | public static void teardownServer() { 66 | try { 67 | server.close(); 68 | } catch (Exception e) { 69 | //nothing to do here 70 | } 71 | } 72 | 73 | private X509Credentials createX509Credentials(String clientIdUri) { 74 | try { 75 | KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048); 76 | X509Certificate cert = TestOpcServer.generateCertificate(keyPair, clientIdUri); 77 | return new X509Credentials() 78 | .withCertificate(cert) 79 | .withPrivateKey(keyPair.getPrivate()); 80 | } catch (Exception e) { 81 | throw new RuntimeException(e); 82 | } 83 | } 84 | 85 | 86 | private OpcUaConnectionProfile createConnectionProfile() { 87 | return (new OpcUaConnectionProfile() 88 | .withConnectionUri(URI.create(server.getBindEndpoint())) 89 | .withClientIdUri("hurence:opc-simple:client:test") 90 | .withClientName("Simple OPC test client") 91 | .withSocketTimeout(Duration.ofSeconds(5)) 92 | ); 93 | } 94 | 95 | private OpcUaConnectionProfile createProsysConnectionProfile() { 96 | return (new OpcUaConnectionProfile() 97 | .withConnectionUri(URI.create("opc.tcp://localhost:53530/OPCUA/SimulationServer")) 98 | .withClientIdUri("hurence:opc-simple:client:test") 99 | .withClientName("Simple OPC test client") 100 | .withSocketTimeout(Duration.ofSeconds(5)) 101 | ); 102 | } 103 | 104 | @Test 105 | public void connectionUserPasswordSuccessTest() throws Exception { 106 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 107 | opcUaTemplate.connect(createConnectionProfile() 108 | .withCredentials(new UsernamePasswordCredentials() 109 | .withUser("user") 110 | .withPassword("password1")) 111 | ).ignoreElement().blockingAwait(); 112 | } 113 | } 114 | 115 | @Test 116 | public void connectionAnonymousSuccessTest() throws Exception { 117 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 118 | opcUaTemplate.connect(createConnectionProfile() 119 | .withCredentials(Credentials.ANONYMOUS_CREDENTIALS)) 120 | .ignoreElement().blockingAwait(); 121 | Assert.assertFalse(opcUaTemplate.isChannelSecured()); 122 | } 123 | } 124 | 125 | @Test 126 | public void connectionOnSecuredChannelSuccessTest() throws Exception { 127 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 128 | OpcUaConnectionProfile connectionProfile = createConnectionProfile(); 129 | opcUaTemplate.connect(connectionProfile 130 | .withCredentials(Credentials.ANONYMOUS_CREDENTIALS) 131 | .withSecureChannelEncryption(createX509Credentials(connectionProfile.getClientIdUri()))) 132 | .ignoreElement() 133 | .blockingAwait(); 134 | Assert.assertTrue(opcUaTemplate.isChannelSecured()); 135 | 136 | } 137 | } 138 | 139 | 140 | @Test(expected = OpcException.class) 141 | public void connectionUserPasswordFails() throws Exception { 142 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 143 | opcUaTemplate.connect(createConnectionProfile() 144 | .withCredentials(new UsernamePasswordCredentials() 145 | .withUser("user") 146 | .withPassword("badpassword")) 147 | ) 148 | .ignoreElement() 149 | .blockingAwait(); 150 | } 151 | } 152 | 153 | @Test 154 | public void connectionX509dSuccessTest() throws Exception { 155 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 156 | OpcUaConnectionProfile connectionProfile = createConnectionProfile(); 157 | opcUaTemplate.connect(connectionProfile 158 | .withCredentials(createX509Credentials(connectionProfile.getClientIdUri()))) 159 | .ignoreElement() 160 | .blockingAwait(); 161 | } 162 | } 163 | 164 | @Test 165 | public void testReactiveBrowse() throws Exception { 166 | final OpcUaTemplate opcUaTemplate = new OpcUaTemplate(); 167 | TestSubscriber subscriber = new TestSubscriber<>(); 168 | opcUaTemplate.connect(createConnectionProfile()) 169 | .toFlowable() 170 | .flatMap(client -> client.browseTags() 171 | .doFinally(client::close)) 172 | .subscribe(subscriber); 173 | 174 | subscriber.assertComplete(); 175 | subscriber.assertValueCount(229); 176 | subscriber.dispose(); 177 | Assert.assertEquals(ConnectionState.DISCONNECTED, opcUaTemplate.getConnectionState().blockingFirst()); 178 | 179 | } 180 | 181 | @Test 182 | public void testBrowse() throws Exception { 183 | 184 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 185 | Collection ret = 186 | opcUaTemplate.connect(createConnectionProfile()) 187 | .toFlowable() 188 | .flatMap(client -> client.browseTags()) 189 | .toList().blockingGet(); 190 | 191 | 192 | logger.info("{}", ret); 193 | Optional sint = ret.stream().filter(t -> "SinT".equals(t.getName())) 194 | .findFirst(); 195 | Assert.assertTrue(sint.isPresent()); 196 | Assert.assertFalse(ret.isEmpty()); 197 | 198 | 199 | } 200 | } 201 | 202 | @Test 203 | public void testFetchMetadata() throws Exception { 204 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 205 | 206 | Collection ret = 207 | opcUaTemplate.connect(createConnectionProfile()) 208 | .toFlowable() 209 | .flatMap(client -> client.fetchMetadata("ns=2;s=sint")) 210 | .toList().blockingGet(); 211 | 212 | logger.info("Metadata: {}", ret); 213 | Assert.assertEquals(1, ret.size()); 214 | 215 | } 216 | } 217 | 218 | @Test 219 | public void testRead() throws Exception { 220 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 221 | opcUaTemplate.connect(createConnectionProfile()) 222 | .ignoreElement() 223 | .andThen(opcUaTemplate.createSession(new OpcUaSessionProfile())) 224 | .flatMap(opcUaSession -> opcUaSession.read("ns=2;s=sint") 225 | .doFinally(opcUaSession::close)) 226 | .doOnSuccess(items -> logger.info("Read tag {}", items)) 227 | .blockingGet(); 228 | } 229 | } 230 | 231 | @Test 232 | public void testWrite() throws Exception { 233 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 234 | opcUaTemplate.connect(createConnectionProfile()) 235 | .ignoreElement() 236 | .blockingAwait(); 237 | OpcUaSession session = opcUaTemplate.createSession(new OpcUaSessionProfile() 238 | ).blockingGet(); 239 | List result = session.write( 240 | new OpcData("ns=2;s=HelloWorld/Dynamic/Double", Instant.now(), 3.1415d), 241 | new OpcData("ns=2;s=sint", Instant.now(), true) 242 | ).blockingGet(); 243 | logger.info("Write result: {}", result); 244 | Assert.assertFalse(result.isEmpty()); 245 | Assert.assertEquals(2, result.size()); 246 | Assert.assertEquals(OperationStatus.Level.INFO, result.get(0).getLevel()); 247 | Assert.assertEquals(OperationStatus.Level.ERROR, result.get(1).getLevel()); 248 | } 249 | } 250 | 251 | @Test 252 | public void testStream() throws Exception { 253 | final OpcUaTemplate opcUaTemplate = new OpcUaTemplate(); 254 | final TestSubscriber subscriber = new TestSubscriber<>(); 255 | opcUaTemplate.connect(createConnectionProfile()) 256 | .subscribeOn(Schedulers.newThread()) 257 | .flatMap(client -> client.createSession(new OpcUaSessionProfile() 258 | .withPublicationInterval(Duration.ofMillis(100)) 259 | )) 260 | .toFlowable() 261 | .flatMap(opcUaSession -> opcUaSession.stream("ns=2;s=sint", Duration.ofMillis(1)) 262 | .doFinally(opcUaSession::close) 263 | .onBackpressureBuffer() 264 | ).take(10000) 265 | .doFinally(opcUaTemplate::close) 266 | .subscribe(subscriber); 267 | 268 | subscriber 269 | .await() 270 | .assertComplete() 271 | .assertValueCount(10000); 272 | } 273 | 274 | 275 | @Test 276 | public void testfetchNextTreeLevel() throws Exception { 277 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 278 | TestSubscriber subscriber1 = new TestSubscriber<>(); 279 | TestSubscriber subscriber2 = new TestSubscriber<>(); 280 | 281 | ConnectableFlowable cf = opcUaTemplate.connect(createConnectionProfile()) 282 | .toFlowable().publish(); 283 | cf.flatMap(client -> client.fetchNextTreeLevel("ns=0;i=84")) 284 | .doOnNext(item -> logger.info(item.toString())) 285 | .subscribe(subscriber1); 286 | 287 | cf.flatMap(client -> client.fetchNextTreeLevel("ns=2;s=sint")) 288 | .subscribe(subscriber2); 289 | 290 | cf.connect(); 291 | 292 | subscriber1.await() 293 | .assertComplete() 294 | .assertValueCount(3); 295 | subscriber2.await() 296 | .assertComplete() 297 | .assertValueCount(0); 298 | } 299 | } 300 | 301 | 302 | @Test 303 | @Ignore 304 | public void testStreamFromProsys() throws Exception { 305 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 306 | opcUaTemplate.connect(createProsysConnectionProfile()).ignoreElement().blockingAwait(); 307 | try (OpcUaSession session = opcUaTemplate.createSession(new OpcUaSessionProfile() 308 | .withPublicationInterval(Duration.ofMillis(1000))).blockingGet()) { 309 | final List> values = new ArrayList<>(); 310 | session.stream("ns=5;s=Sawtooth1", Duration.ofMillis(10)) 311 | .limit(1000).map(a -> { 312 | System.out.println(a); 313 | return a; 314 | }).blockingForEach(values::add); 315 | 316 | logger.info("Stream result: {}", values); 317 | values.stream().map(OpcData::getTimestamp).forEach(System.err::println); 318 | 319 | } 320 | } 321 | 322 | } 323 | 324 | 325 | @Test 326 | public void testAutoReconnect() throws Exception { 327 | 328 | //start a new dedicated server 329 | TestOpcServer uaServer = new TestOpcServer(InetAddress.getLoopbackAddress(), null); 330 | try { 331 | uaServer.getInstance().startup().get(); 332 | 333 | TestSubscriber subscriber = new TestSubscriber<>(); 334 | 335 | //now build our stream 336 | Flowable flowable = new OpcUaTemplate() 337 | //establish a connection 338 | .connect(new OpcUaConnectionProfile() 339 | .withConnectionUri(URI.create(uaServer.getBindEndpoint())) 340 | .withSocketTimeout(Duration.ofSeconds(3)) 341 | .withKeepAliveInterval(Duration.ofSeconds(1))) 342 | //log connection errors 343 | .doOnError(t -> logger.warn("Unable to connect. Retrying...: {}", t.getMessage())) 344 | .toFlowable() 345 | .flatMap(client -> client.createSession(new OpcUaSessionProfile() 346 | .withPublicationInterval(Duration.ofMillis(100))) 347 | //when ready create a subscription and start streaming some data 348 | .toFlowable() 349 | .doOnNext(opcUaSession -> logger.info("Created new OPC UA session")) 350 | .flatMap(session -> 351 | session.stream("ns=2;s=sint", Duration.ofMillis(100)) 352 | //do not forget to close connections 353 | .doFinally(session::close) 354 | 355 | ) 356 | .doFinally(client::close) 357 | ) 358 | //retry anything in case something failed failed 359 | .doOnError(throwable -> logger.warn("An error occurred. Reconnecting: " + throwable.getMessage())) 360 | .retryWhen(throwable -> throwable.delay(1, TimeUnit.SECONDS)) 361 | .subscribeOn(Schedulers.io()) 362 | .takeWhile(ignored -> !subscriber.isTerminated()) 363 | //create an hot flowable 364 | .publish() 365 | .autoConnect(2); 366 | 367 | 368 | //create a deferred stream to simulate a disconnection 369 | flowable.take(20) 370 | .subscribeOn(Schedulers.newThread()) 371 | .doOnComplete(() -> 372 | new Thread(() -> { 373 | try { 374 | //get server down 375 | uaServer.close(); 376 | Thread.sleep(5000); 377 | //now bring server back up 378 | uaServer.getInstance().startup().get(); 379 | } catch (Exception e) { 380 | //nothing we can do here 381 | } 382 | 383 | }).start() 384 | ) 385 | //and attach it 386 | .subscribe(); 387 | 388 | //attach now the real consumer 389 | flowable 390 | //look just for 300 values 391 | .take(50) 392 | .doOnNext(data -> logger.info("Received {}", data)) 393 | .subscribe(subscriber); 394 | 395 | 396 | subscriber.await(); 397 | subscriber.assertComplete(); 398 | subscriber.assertValueCount(50); 399 | subscriber.dispose(); 400 | } finally { 401 | uaServer.close(); 402 | } 403 | 404 | } 405 | 406 | @Ignore 407 | @Test 408 | public void testReadFromProsys() throws Exception { 409 | try (OpcUaTemplate opcUaTemplate = new OpcUaTemplate()) { 410 | opcUaTemplate.connect(createProsysConnectionProfile()).ignoreElement().blockingAwait(); 411 | OpcUaSession session = opcUaTemplate.createSession(new OpcUaSessionProfile() 412 | ).blockingGet(); 413 | logger.info("Read tag {}", session.read("ns=5;s=Sawtooth1")); 414 | } 415 | } 416 | 417 | 418 | } 419 | --------------------------------------------------------------------------------