From 5939fd2611a5d0ee8394ba9e6315caea44458a9a Mon Sep 17 00:00:00 2001 From: Pat Thoyts Date: Thu, 22 Jun 2023 09:23:14 +0100 Subject: [PATCH] Emit spectra from a wdf file continuously. Supports registration with a discovery server. Emits spectra once per second updating the ilist and spectrum index. Xlist is updated for the first spectrum only. Cross platform for Windows and Linux. --- .editorconfig | 14 +++ .gitignore | 1 - .vscode/c_cpp_properties.json | 30 +++++ CMakeLists.txt | 44 +++++-- LICENSE | 202 ++++++++++++++++++++++++++++++ README.md | 9 ++ opc_app.h | 40 ++++++ opc_util.c | 149 ++++++++++++++++++++++ server.c | 224 ++++++++++++++++++++++------------ wdf_utils.c | 104 ++++++++++++++++ wdf_utils.h | 15 +++ 11 files changed, 746 insertions(+), 86 deletions(-) create mode 100644 .editorconfig create mode 100644 LICENSE create mode 100644 README.md create mode 100644 opc_app.h create mode 100644 opc_util.c create mode 100644 wdf_utils.c create mode 100644 wdf_utils.h diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4efb941 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +indent_style = space +trim_trailing_whitespace = true + +[*.{c,h,cpp,json}] +indent_size = 4 + +[CMakeLists.txt] +indent_size = 4 + +[{Makefile, makefile,*.mk}] +indent_style = tab diff --git a/.gitignore b/.gitignore index 77b8785..6f31401 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ build/ -open62541/ .vscode/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 3664dd8..f6fa22f 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -17,6 +17,36 @@ "cStandard": "c17", "cppStandard": "c++14", "intelliSenseMode": "linux-clang-x64" + }, + { + "name": "Win32", + "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "includePath": [ + "open62541/include", + "open62541/arch", + "open62541/deps", + "open62541/plugins/include", + "build/open62541/src_generated", + "wdf-linux/wdflib" + ], + "defines": ["WIN32", "STRICT", "WIN32_LEAN_AND_MEAN"], + "cStandard": "c11", + "intelliSenseMode": "${default}", + "compileCommandsInCppPropertiesJson": "${workspaceFolder}/build/compile_commands.json", + "cppStandard": "c++11", + "mergeConfigurations": false, + "browse": { + "path": [ + "open62541/include", + "open62541/arch", + "open62541/deps", + "open62541/plugins/include", + "build/open62541/src_generated", + "wdf-linux/wdflib", + "${workspaceFolder}" + ], + "limitSymbolsToIncludedHeaders": true + } } ], "version": 4 diff --git a/CMakeLists.txt b/CMakeLists.txt index e92203e..775312a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,25 @@ +# Copyright (c) 2023 Renishaw plc. All rights reserved. +# +# Either use open62541 as a submodule or we can use it via vcpkg to build the open62541 port. +# +# For vcpkg use: +# (start a Visual Studio x64 shell) +# vcpkg install open62541 +# vcpkg list +# +# For an alternate architecture (default is windows-x86) use: +# vcpkg install open62541:x64-windows +# +# To use this in a Visual Studio project - make this available +# vcpkg integrate install +# For CMake, add this to the cmake command: +# -DCMAKE_TOOLCHAIN_FILE=e:\Code\vcpkg\scripts\buildsystems\vcpkg.cmake +# +# TLDR: +# cmake -DAPP_USE_OPEN62541_SUBMODULE=OFF \ +# -DCMAKE_TOOLCHAIN_FILE=e:\Code\vcpkg\scripts\buildsystems\vcpkg.cmake .. -A x64 +# cmake --build . --config Release + cmake_minimum_required(VERSION 3.12) project(opc_server @@ -7,21 +29,29 @@ project(opc_server ) set (TARGET ${PROJECT_NAME}) +set (SOURCES server.c opc_util.c opc_app.h wdf_utils.c wdf_utils.h) add_subdirectory(wdf-linux/wdflib wdflib) -option (MY_INTERNAL_OPEN62541 "Use internal open62541 library" ON) -if (MY_INTERNAL_OPEN62541) +option (APP_USE_OPEN62541_SUBMODULE "Use the open62541 submodule" ON) +if (APP_USE_OPEN62541_SUBMODULE) + set (UA_ENABLE_DISCOVERY ON CACHE BOOL "Enable UA Discovery") + set (UA_ENABLE_PUBSUB ON CACHE BOOL "Enable PubSub protocol support") add_subdirectory(open62541) # target open62541::open62541 else() set (CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) # prefer open62541's own config file - find_package(open62541 REQUIRED - COMPONENTS Events FullNamespace - PATHS /opt/open62541 - ) + find_package(open62541 CONFIG REQUIRED COMPONENTS Events PATHS /opt/open62541) endif() -add_executable(${TARGET} server.c) +add_executable(${TARGET} ${SOURCES}) target_compile_features(${TARGET} PUBLIC c_std_11) target_include_directories(${TARGET} PUBLIC open62541::open62541 ${wdflib_SOURCE_DIR}) target_link_libraries(${TARGET} PUBLIC open62541::open62541 wdflib) + +if (MSVC) + set_property(TARGET ${TARGET} + PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreadedDLL$<$:Debug>") + target_compile_definitions(${TARGET} PUBLIC + STRICT WIN32_LEAN_AND_MEAN + _CRT_DECLARE_NONSTDC_NAMES _CRT_NONSTDC_NO_WARNINGS _CRT_SECURE_NO_WARNINGS) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..29bf376 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# WDF OPC server demo + +Provides an OPC server that reads a wdf data file and continuously emits +the data one specrum at a time. + +Used for testing OPC clients. + +Depends on the open65241 library which can be provided as a submodule or +a pre-installed package (eg using vcpkg on Windows) diff --git a/opc_app.h b/opc_app.h new file mode 100644 index 0000000..65a4600 --- /dev/null +++ b/opc_app.h @@ -0,0 +1,40 @@ +// Copyright (c) 2023 Renishaw plc. All rights reserved. +// + +#ifndef _opc_app_h_INCLUDE +#define _opc_app_h_INCLUDE + +#include +#include +#include + +typedef struct { + UA_Server *server; + UA_Client *regClient; + UA_Int64 regId; + UA_Int16 ns; + const char *regUri; + int filehandle; + WdfHeader hdr; + uint64_t spectrumIndex; + float *xlist; + float *ilist; +} App; + +UA_StatusCode add_float(App *app, UA_NodeId parentNode, + const char *name, const char *displayName, UA_Float value); +UA_StatusCode add_int32(App *app, UA_NodeId parentNode, + const char *name, const char *displayName, UA_Int32 value); +UA_StatusCode add_int64(App *app, UA_NodeId parentNode, + const char *name, const char *displayName, UA_Int64 value); +UA_StatusCode add_float_vector(App *app, UA_NodeId parentNode, + const char *name, const char *displayName, size_t len, UA_Float *value); +UA_StatusCode add_object_node(App *app, UA_NodeId parentNode, + const char *name, const char *displayName, + UA_NodeId *newNodeId); +UA_StatusCode update_scalar(App *app, UA_NodeId nodeId, void *valuePtr, const UA_DataType *typePtr); +UA_StatusCode update_float_vector(App *app, UA_NodeId nodeId, size_t len, const float *vector); +UA_StatusCode update_xlist(App *app); +UA_StatusCode update_ilist(App *app); + +#endif // _opc_app_h_INCLUDE diff --git a/opc_util.c b/opc_util.c new file mode 100644 index 0000000..c70f3c5 --- /dev/null +++ b/opc_util.c @@ -0,0 +1,149 @@ +// Copyright (c) 2023 Renishaw plc. All rights reserved. +// + +#include "opc_app.h" +#include "wdf_utils.h" + +UA_StatusCode add_float(App *app, UA_NodeId parentNode, const char *name, const char *displayName, UA_Float value) +{ + UA_VariableAttributes attr = UA_VariableAttributes_default; + if (displayName != NULL) + attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName); + UA_Variant_setScalar(&attr.value, &value, &UA_TYPES[UA_TYPES_FLOAT]); + + char id[64] = {0}; + strcat(id, "renishaw.spd."); + strcat(id, name); + + UA_NodeId nodeId = UA_NODEID_STRING(app->ns, id); + UA_NodeId parentRefId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + UA_NodeId varType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE); + UA_QualifiedName browseName = UA_QUALIFIEDNAME_ALLOC(app->ns, name); + + return UA_Server_addVariableNode( + app->server, nodeId, parentNode, parentRefId, browseName, + varType, attr, NULL, NULL); +} + +UA_StatusCode add_int32(App *app, UA_NodeId parentNode, const char *name, const char *displayName, UA_Int32 value) +{ + UA_VariableAttributes attr = UA_VariableAttributes_default; + if (displayName != NULL) + attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName); + UA_Variant_setScalar(&attr.value, &value, &UA_TYPES[UA_TYPES_INT32]); + + char id[64] = {0}; + strcat(id, "renishaw.spd."); + strcat(id, name); + + UA_NodeId nodeId = UA_NODEID_STRING_ALLOC(app->ns, id); + UA_NodeId parentRefId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + UA_NodeId varType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE); + UA_QualifiedName browseName = UA_QUALIFIEDNAME_ALLOC(app->ns, name); + + return UA_Server_addVariableNode( + app->server, nodeId, parentNode, parentRefId, browseName, + varType, attr, NULL, NULL); +} + +UA_StatusCode add_int64(App *app, UA_NodeId parentNode, const char *name, const char *displayName, UA_Int64 value) +{ + UA_VariableAttributes attr = UA_VariableAttributes_default; + if (displayName != NULL) + attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName); + UA_Variant_setScalar(&attr.value, &value, &UA_TYPES[UA_TYPES_INT64]); + + char id[64] = {0}; + strcat(id, "renishaw.spd."); + strcat(id, name); + + UA_NodeId nodeId = UA_NODEID_STRING_ALLOC(app->ns, id); + UA_NodeId parentRefId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + UA_NodeId varType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE); + UA_QualifiedName browseName = UA_QUALIFIEDNAME_ALLOC(app->ns, name); + + return UA_Server_addVariableNode( + app->server, nodeId, parentNode, parentRefId, browseName, + varType, attr, NULL, NULL); +} + +UA_StatusCode add_object_node(App *app, + UA_NodeId parentNode, + const char *name, const char *displayName, + UA_NodeId *newNodeId) +{ + UA_ObjectAttributes attr = UA_ObjectAttributes_default; + return UA_Server_addObjectNode(app->server, + UA_NODEID_STRING_ALLOC(app->ns, name), + parentNode, + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + UA_QUALIFIEDNAME_ALLOC(app->ns, displayName), + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), + attr, NULL, newNodeId); +} + +UA_StatusCode add_float_vector(App *app, UA_NodeId parentNode, const char *name, const char *displayName, size_t len, UA_Float *value) +{ + UA_Int32 dims = (UA_Int32)len; + UA_VariableAttributes attr = UA_VariableAttributes_default; + attr.valueRank = UA_VALUERANK_ONE_DIMENSION; + attr.arrayDimensions = &dims; + attr.arrayDimensionsSize = 1; + if (displayName != NULL) + attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName); + UA_Variant_setArray(&attr.value, value, len, &UA_TYPES[UA_TYPES_FLOAT]); + + char id[64] = {0}; + strcat(id, "renishaw.spd."); + strcat(id, name); + + UA_NodeId nodeId = UA_NODEID_STRING_ALLOC(app->ns, id); + UA_NodeId parentRefId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + UA_NodeId varType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE); + UA_QualifiedName browseName = UA_QUALIFIEDNAME_ALLOC(app->ns, name); + + return UA_Server_addVariableNode( + app->server, nodeId, parentNode, parentRefId, browseName, + varType, attr, NULL, NULL); +} + +UA_StatusCode update_float_vector(App *app, UA_NodeId nodeId, size_t len, const float *vector) +{ + UA_Variant value; + UA_Variant_setArray(&value, (void *)vector, len, &UA_TYPES[UA_TYPES_FLOAT]); + return UA_Server_writeValue(app->server, nodeId, value); +} + +UA_StatusCode update_scalar(App *app, UA_NodeId nodeId, void *valuePtr, const UA_DataType *typePtr) +{ + UA_Variant value; + UA_Variant_setScalar(&value, valuePtr, typePtr); + return UA_Server_writeValue(app->server, nodeId, value); +} + +UA_StatusCode update_xlist(App *app) +{ + UA_StatusCode status = UA_STATUSCODE_BADNOTFOUND; + WdfBlock section = {0}; + uint64_t pos = wdf_find_section(app->filehandle, WDF_BLOCKID_XLIST, WDF_BLOCKID_ANY, §ion); + if (pos != (uint64_t)-1) + { + ssize_t count = wdf_read_xlist(app->filehandle, app->hdr.npoints, app->xlist); + if (count > 0) + { + UA_NodeId node = UA_NODEID_STRING_ALLOC(app->ns, "renishaw.spd.xlist"); + status = update_float_vector(app, node, app->hdr.npoints, app->xlist); + } + } + return status; +} + +UA_StatusCode update_ilist(App *app) +{ + WdfBlock section = {0}; + uint64_t pos = wdf_find_section(app->filehandle, WDF_BLOCKID_DATA, WDF_BLOCKID_ANY, §ion); + wdf_read_spectrum(app->filehandle, pos, app->hdr.npoints, app->spectrumIndex, app->ilist); + UA_NodeId node = UA_NODEID_STRING_ALLOC(app->ns, "renishaw.spd.ilist"); + return update_float_vector(app, node, app->hdr.npoints, app->ilist); +} + diff --git a/server.c b/server.c index 5220b19..7f73ac3 100644 --- a/server.c +++ b/server.c @@ -1,126 +1,194 @@ +// Copyright (c) 2023 Renishaw plc. All rights reserved. +// + #include #include #include -#include +#include +#ifdef WIN32 +#include +#else #include +#endif +#include #include +#include "opc_app.h" +#include "wdf_utils.h" -typedef struct { - UA_Server *server; - UA_Int16 ns; - UA_NodeId renishawNode; -} App; +#define DISCOVERY_SERVER_ENDPOINT "opc.tcp://localhost:4840" static volatile UA_Boolean is_running = true; +#ifdef WIN32 +BOOL WINAPI on_interrupt(DWORD ctrlType) +{ + UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c"); + is_running = false; + return TRUE; +} +#else static void on_interrupt(int signo) { UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c"); is_running = false; } +#endif static int usage(int exitCode) { - fprintf(stderr, "usage: opc_server ?-port NUM? FILENAME\n"); + fprintf(stderr, "usage: opc_server ?-port NUM? ?-register? FILENAME\n"); exit(exitCode); } -static UA_StatusCode add_float(App *app, const char *name, const char *displayName, UA_Float value) +// Register the server with LDS +// periodic server register after 10 Minutes, delay first register for 500ms +UA_StatusCode registerServer(App *app) { - UA_VariableAttributes attr = UA_VariableAttributes_default; - if (displayName != NULL) - attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName); - UA_Variant_setScalar(&attr.value, &value, &UA_TYPES[UA_TYPES_FLOAT]); - - char id[64] = {0}; - strcat(id, "renishaw.spd."); - strcat(id, name); - - UA_NodeId nodeId = UA_NODEID_STRING(app->ns, id); - UA_NodeId parentId = app->renishawNode; - UA_NodeId parentRefId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); - UA_NodeId varType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE); - UA_QualifiedName browseName = UA_QUALIFIEDNAME_ALLOC(app->ns, name); - - return UA_Server_addVariableNode( - app->server, nodeId, parentId, parentRefId, browseName, - varType, attr, NULL, NULL); + app->regClient = UA_Client_new(); + UA_StatusCode status = UA_ClientConfig_setDefault(UA_Client_getConfig(app->regClient)); + if (UA_StatusCode_isGood(status)) + { + status = UA_Server_addPeriodicServerRegisterCallback(app->server, + app->regClient, app->regUri, 10 * 60 * 1000, 500, &app->regId); + } + return status; } -static UA_StatusCode add_int32(App *app, const char *name, const char *displayName, UA_Int32 value) +static void getSpectrumCallback(UA_Server *server, void *clientData) { - UA_VariableAttributes attr = UA_VariableAttributes_default; - if (displayName != NULL) - attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName); - UA_Variant_setScalar(&attr.value, &value, &UA_TYPES[UA_TYPES_INT32]); - - char id[64] = {0}; - strcat(id, "renishaw.spd."); - strcat(id, name); - - UA_NodeId nodeId = UA_NODEID_STRING_ALLOC(app->ns, id); - UA_NodeId parentId = app->renishawNode; - UA_NodeId parentRefId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); - UA_NodeId varType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE); - UA_QualifiedName browseName = UA_QUALIFIEDNAME_ALLOC(app->ns, name); - - return UA_Server_addVariableNode( - app->server, nodeId, parentId, parentRefId, browseName, - varType, attr, NULL, NULL); + App *app = (App *)clientData; + + if (app->spectrumIndex == 0) + update_xlist(app); + update_ilist(app); + + UA_NodeId node = UA_NODEID_STRING_ALLOC(app->ns, "renishaw.spd.index"); + update_scalar(app, node, &app->spectrumIndex, &UA_TYPES[UA_TYPES_INT64]); + + ++app->spectrumIndex; + if (app->spectrumIndex > app->hdr.ncollected) + app->spectrumIndex = 0; } -int main(int argc, char *argv[]) +int main(int argc, char** argv) { + App application = {0}; + App *app = &application; unsigned short port = 4840; const char *filename = argv[argc - 1]; - if (argc == 4 && strcmp("-port", argv[1]) == 0) - port = (unsigned short)strtoul(argv[2], NULL, 0); - else if (argc != 2) - usage(1); - if (filename[0] == '-') - usage(1); - + { + int n = 1; + for (; n < argc; ++n) + { + if (strncmp("-port", argv[n], 5) == 0) + { + port = (unsigned short)strtoul(argv[n+1], NULL, 0); + ++n; + } + else if (strncmp("-register", argv[n], 8) == 0) + { + app->regUri = DISCOVERY_SERVER_ENDPOINT; + } + else + break; + } + if (argc - n != 1) + usage(1); + if (filename[0] == '-') + usage(2); + } + +#ifdef WIN32 + SetConsoleCtrlHandler(on_interrupt, TRUE); +#else signal(SIGINT, on_interrupt); signal(SIGTERM, on_interrupt); +#endif - App application = {0}; - App *app = &application; - - WdfHeader wdf = {0}; - FILE *fp = fopen(filename, "rb"); - if (fp != NULL) - fread(&wdf, sizeof(WdfHeader), 1, fp); - else + memset(&app->hdr, 0, sizeof(WdfHeader)); + app->filehandle = wdf_open(filename, &app->hdr); + if (app->filehandle < 0) + { fprintf(stderr, "failed to read wdf file\n"); - fclose(fp); + exit(1); + } + + size_t len = sizeof(float) * app->hdr.npoints; + app->xlist = (float *)UA_malloc(len); + memset(app->xlist, 0, len); + app->ilist = (float *)UA_malloc(len); + memset(app->ilist, 0, len); + app->spectrumIndex = 0; app->server = UA_Server_new(); UA_ServerConfig *config = UA_Server_getConfig(app->server); UA_StatusCode status = UA_ServerConfig_setMinimal(config, port, NULL); - app->ns = UA_Server_addNamespace(app->server, "renishaw.spd"); + app->ns = UA_Server_addNamespace(app->server, "urn:renishaw.spd"); - // Add new object node for our data as Renishaw - UA_ObjectAttributes rAttr = UA_ObjectAttributes_default; - status = UA_Server_addObjectNode(app->server, UA_NODEID_STRING(app->ns, "renishaw"), - UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), - UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), - UA_QUALIFIEDNAME(app->ns, "Renishaw"), - UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), - rAttr, NULL, &app->renishawNode); + UA_String_clear(&config->applicationDescription.applicationUri); + config->applicationDescription.applicationUri = + UA_String_fromChars("urn:renishaw.spd.demo"); +#ifdef UA_ENABLE_DISCOVERY_MULTICAST + config->mdnsConfig.mdnsServerName = UA_String_fromChars("Renishaw Demo"); +#endif + // Add new object node for our data as Renishaw + UA_NodeId mainNode, spdNode; + if (UA_StatusCode_isGood(status)) + status = add_object_node(app, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), "renishaw", "Renishaw", &mainNode); if (UA_StatusCode_isGood(status)) - status = add_int32(app, "nspectra", "Capacity", wdf.nspectra); + status = add_object_node(app, mainNode, "renishaw.spd", "SPD", &spdNode); if (UA_StatusCode_isGood(status)) - status = add_int32(app, "ncollected", "Count", wdf.ncollected); + status = add_int32(app, spdNode, "nspectra", "Capacity", (int32_t)app->hdr.nspectra); if (UA_StatusCode_isGood(status)) - status = add_int32(app, "npoints", "Points", wdf.npoints); + status = add_int32(app, spdNode, "ncollected", "Count", (int32_t)app->hdr.ncollected); if (UA_StatusCode_isGood(status)) - status = add_float(app, "laserwavenum", "Laser Wavenumber", wdf.laserwavenum); - + status = add_int32(app, spdNode, "npoints", "Points", app->hdr.npoints); + if (UA_StatusCode_isGood(status)) + status = add_float(app, spdNode, "laserwavenum", "Laser Wavenumber", app->hdr.laserwavenum); + if (UA_StatusCode_isGood(status)) + { + UA_NodeId spectrumNode; + status = add_object_node(app, spdNode, "renishaw.spd.spectrum", "Spectrum", &spectrumNode); + if (UA_StatusCode_isGood(status)) + status = add_int64(app, spectrumNode, "index", "Index", app->spectrumIndex); + if (UA_StatusCode_isGood(status)) + status = add_float_vector(app, spectrumNode, "xlist", "XList", app->hdr.npoints, app->xlist); + if (UA_StatusCode_isGood(status)) + status = add_float_vector(app, spectrumNode, "ilist", "IList", app->hdr.npoints, app->ilist); + } + if (UA_StatusCode_isGood(status)) + { + if (app->regUri != NULL) + { + UA_StatusCode regStatus = registerServer(app); + if (UA_StatusCode_isBad(regStatus)) + { + UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, + "Could not create periodic job for server register. StatusCode %s", + UA_StatusCode_name(regStatus)); + UA_Client_disconnect(app->regClient); + UA_Client_delete(app->regClient); + } + } + } + + /* Add a repeated callback to the server */ + if (UA_StatusCode_isGood(status)) + status = UA_Server_addRepeatedCallback(app->server, getSpectrumCallback, app, 1000, NULL); + if (UA_StatusCode_isGood(status)) status = UA_Server_run(app->server, &is_running); - + else + { + UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "failed with status code %s", UA_StatusCode_name(status)); + } + + if (app->filehandle > 0) + wdf_close(app->filehandle); + UA_free(app->xlist); + UA_free(app->ilist); UA_Server_delete(app->server); return UA_StatusCode_isGood(status) ? EXIT_SUCCESS : EXIT_FAILURE; -} \ No newline at end of file +} diff --git a/wdf_utils.c b/wdf_utils.c new file mode 100644 index 0000000..1789d9c --- /dev/null +++ b/wdf_utils.c @@ -0,0 +1,104 @@ +// Copyright (c) 2023 Renishaw plc. All rights reserved. +// + +#ifdef WIN32 +# ifndef _CRT_SECURE_NO_WARNINGS +# define _CRT_SECURE_NO_WARNINGS +# endif +# include +#else +# define _LARGEFILE64_SOURCE +# include +# define _lseeki64 lseek64 +# define _open open +# define _close close +# define _read read +# define _O_RDONLY O_RDONLY +# define _O_BINARY 0 +# define _S_IREAD S_IREAD +# define _S_IWRITE S_IWRITE +#endif +#include +#include +#include +#include +#include "opc_app.h" + +int wdf_open(const char *filename, WdfHeader *wdfPtr) +{ + int fd = _open(filename, _O_RDONLY | _O_BINARY, _S_IREAD | _S_IWRITE); + if (fd != -1) { + int cb = _read(fd, wdfPtr, sizeof(WdfHeader)); + if (wdfPtr->signature != WDF_BLOCKID_FILE) { + fprintf(stderr, "error: invalid file format\n"); + close(fd); + fd = -1; + } + } + return fd; +} + +int wdf_close(int fd) +{ + return _close(fd); +} + +uint64_t wdf_find_section(int fd, uint32_t id, uint32_t uid, WdfBlock *blockPtr) +{ + WdfBlock block = {0}; + uint64_t offset = 0, size = 0; + + size = _lseeki64(fd, 0, SEEK_END); + while (offset < size) + { + int cb = 0; + if (_lseeki64(fd, offset, SEEK_SET) < 0) + break; + if ((cb = _read(fd, &block, sizeof(block))) != sizeof(block)) break; + if (id == block.id) + { + if (uid == WDF_BLOCKID_ANY || uid == block.uid) + { + if (blockPtr != NULL) + memcpy(blockPtr, &block, sizeof(WdfBlock)); + return offset; + } + } + offset += block.size; + } + return (uint64_t)-1; +} + +/// @brief read the spectral xlist data +/// @param[in] fd - the C library file handle for the open file +/// @param[in] length - number of elements to be read +/// @param[out] xlist - pointer to array to be filled +/// @return -1 on error or the number of bytes read into the spectrum +ssize_t wdf_read_xlist(int fd, uint32_t length, float *xlist) +{ + ssize_t r = -1; + WdfBlock section = {0}; + WdfDataType dataType = WdfDataType_Arbitrary; + WdfDataUnits dataUnits = WdfDataUnits_Arbitrary; + uint64_t base = wdf_find_section(fd, WDF_BLOCKID_XLIST, WDF_BLOCKID_ANY, §ion); + if (base != (uint64_t)-1) + { + _lseeki64(fd, base, SEEK_SET); + r = _read(fd, §ion, sizeof(WdfBlock)); + if (r > 0) + r = _read(fd, &dataType, sizeof(WdfDataType)); + if (r > 0) + r = _read(fd, &dataUnits, sizeof(WdfDataUnits)); + if (r > 0) + r = _read(fd, xlist, sizeof(float) * length); + } + return r; +} + +int wdf_read_spectrum(int fd, uint64_t base, uint32_t length, uint64_t spectrum, float *ilist) +{ + int r = -1; + if (_lseeki64(fd, base + 16 + (sizeof(float) * length * spectrum), SEEK_SET) != -1) + r = _read(fd, ilist, sizeof(float)*length); + return r; +} diff --git a/wdf_utils.h b/wdf_utils.h new file mode 100644 index 0000000..90741cb --- /dev/null +++ b/wdf_utils.h @@ -0,0 +1,15 @@ +// Copyright (c) 2023 Renishaw plc. All rights reserved. +// + +#ifndef _wdf_utils_h_INCLUDE +#define _wdf_utils_h_INCLUDE + +#include + +int wdf_open(const char *filename, WdfHeader *wdfPtr); +int wdf_close(int fd); +uint64_t wdf_find_section(int fd, uint32_t id, uint32_t uid, WdfBlock *blockPtr); +ssize_t wdf_read_xlist(int fd, uint32_t length, float *xlist); +int wdf_read_spectrum(int fd, uint64_t base, uint32_t length, uint64_t spectrum, float *ilist); + +#endif // _wdf_utils_h_INCLUDE -- 2.23.0