├── .gitignore ├── CMakeLists.txt ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── CodeOfConduct.md ├── LICENSE ├── MAINTAINERS.md ├── PLCnEngTestProject └── SampleRuntimeTest.pcwex ├── Picture ├── 01_Dependencies.gif └── 02_Architecture.gif ├── README.md ├── RuntimeTemplate ├── .proj ├── CMakeLists.txt ├── Runtime.acf.config ├── Runtime.acf.settings ├── Runtime.cpp └── TemplateDescription.xml ├── SECURITY.md ├── data ├── runtime.acf.config └── runtime.acf.settings ├── getting-started ├── Part-01 │ └── README.md ├── Part-02 │ └── README.md ├── Part-03 │ └── README.md ├── Part-04 │ └── README.md ├── Part-05 │ └── README.md ├── Part-06 │ └── README.md ├── Part-07 │ └── README.md ├── Part-08 │ └── README.md ├── Part-09 │ └── README.md ├── Part-10 │ └── README.md └── Part-99 │ └── README.md ├── src ├── CSampleRTThread.cpp ├── CSampleRTThread.h ├── CSampleRuntime.cpp ├── CSampleRuntime.h ├── CSampleSubscriptionThread.cpp ├── CSampleSubscriptionThread.h ├── PLCnextSampleRuntime.cpp └── Utility.h └── tools ├── build-runtime.sh └── start_debug.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/settings.json 2 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project(runtime) 4 | 5 | if(NOT CMAKE_BUILD_TYPE) 6 | set(CMAKE_BUILD_TYPE Release) 7 | endif() 8 | 9 | ################# create target ####################################################### 10 | 11 | set (WILDCARD_SOURCE *.cpp) 12 | set (WILDCARD_HEADER *.h *.hpp *.hxx) 13 | 14 | file(GLOB_RECURSE Headers src/${WILDCARD_HEADER}) 15 | file(GLOB_RECURSE Sources src/${WILDCARD_SOURCE}) 16 | add_executable(runtime ${Headers} ${Sources}) 17 | 18 | ####################################################################################### 19 | 20 | ################# project include-paths ############################################### 21 | 22 | target_include_directories(runtime 23 | PUBLIC 24 | $) 25 | 26 | ####################################################################################### 27 | 28 | ################# include arp cmake module path ####################################### 29 | 30 | list(INSERT CMAKE_MODULE_PATH 0 "${ARP_TOOLCHAIN_CMAKE_MODULE_PATH}") 31 | 32 | ####################################################################################### 33 | 34 | ################# set link options #################################################### 35 | # WARNING: Without --no-undefined the linker will not check, whether all necessary # 36 | # libraries are linked. When a library which is necessary is not linked, # 37 | # the firmware will crash and there will be NO indication why it crashed. # 38 | ####################################################################################### 39 | 40 | target_link_options(runtime PRIVATE LINKER:--no-undefined) 41 | 42 | ####################################################################################### 43 | 44 | ################# add link targets #################################################### 45 | 46 | find_package(ArpDevice REQUIRED) 47 | find_package(ArpProgramming REQUIRED) 48 | 49 | target_link_libraries(runtime PRIVATE ArpDevice ArpProgramming 50 | Arp.System.ModuleLib Arp.System.Module 51 | Arp.Plc.AnsiC Arp.System.Lm.Services) 52 | 53 | ####################################################################################### 54 | 55 | ################# install ############################################################ 56 | 57 | string(REGEX REPLACE "^.*\\(([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*$" "\\1" _ARP_SHORT_DEVICE_VERSION ${ARP_DEVICE_VERSION}) 58 | install(TARGETS runtime 59 | LIBRARY DESTINATION ${ARP_DEVICE}_${_ARP_SHORT_DEVICE_VERSION}/$/lib 60 | ARCHIVE DESTINATION ${ARP_DEVICE}_${_ARP_SHORT_DEVICE_VERSION}/$/lib 61 | RUNTIME DESTINATION ${ARP_DEVICE}_${_ARP_SHORT_DEVICE_VERSION}/$/bin) 62 | unset(_ARP_SHORT_DEVICE_VERSION) 63 | 64 | ####################################################################################### -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This is an open source project, and we appreciate your help! 4 | 5 | We use the GitHub issue tracker to discuss new features and bugs. 6 | 7 | In addition to the issue tracker, the PLCnext Technology [Forum](https://www.plcnext-community.net/index.php?option=com_easydiscuss&view=categories&Itemid=221&lang=en) is the best way to get into contact with the project's maintainers. 8 | 9 | To contribute code or documentation for available projects, please use an issue with an *easy-pick* label. 10 | Our [maintainers](MAINTAINERS.md) will review and implement the changes. 11 | 12 | If you are interested in contributing to the PLCnext platform organisation, please contact CommunityExamples@phoenixcontact.com. 13 | After reviewing, we will decide together, if your project will be forked from our organisation or if you get a member and transfer the repository to the organisation. -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | This is the official list of the ##PROJECTNAME## project contributors. 4 | 7 | 8 | * Phoenix Contact GmbH & Co. KG 9 | - Stefen Schlette 10 | - Eduard Münz 11 | - Oliver Warneke 12 | 13 | * Max Mustermann 14 | -------------------------------------------------------------------------------- /CodeOfConduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [OSSPLCnext@phoenixcontact.com](mailto:OSSPLCnext@phoenixcontact.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Phoenix Contact GmbH & Co KG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # MAINTAINER 2 | 3 | Marcel Luhmann - OSSPLCnext@phoenixcontact.com 4 | 5 | Oliver Warneke - OSSPLCnext@phoenixcontact.com -------------------------------------------------------------------------------- /PLCnEngTestProject/SampleRuntimeTest.pcwex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PLCnext/SampleRuntime/c2477920ed847cb458c8d926e0f8cb517a4876c4/PLCnEngTestProject/SampleRuntimeTest.pcwex -------------------------------------------------------------------------------- /Picture/01_Dependencies.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PLCnext/SampleRuntime/c2477920ed847cb458c8d926e0f8cb517a4876c4/Picture/01_Dependencies.gif -------------------------------------------------------------------------------- /Picture/02_Architecture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PLCnext/SampleRuntime/c2477920ed847cb458c8d926e0f8cb517a4876c4/Picture/02_Architecture.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PLCnext Technology - SampleRuntime 2 | 3 | [![Feature Requests](https://img.shields.io/github/issues/PLCnext/SampleRuntime/feature-request.svg)](https://github.com/PLCnext/SampleRuntime/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) 4 | [![Bugs](https://img.shields.io/github/issues/PLCnext/SampleRuntime/bug.svg)](https://github.com/PLCnext/SampleRuntime/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3Abug) 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 6 | [![Web](https://img.shields.io/badge/PLCnext-Website-blue.svg)](https://www.phoenixcontact.com/plcnext) 7 | [![Community](https://img.shields.io/badge/PLCnext-Community-blue.svg)](https://www.plcnext-community.net) 8 | 9 | ## Guide details 10 | 11 | |Description | Value | 12 | |--------------|--------------| 13 | |Created | 26.07.2019 | 14 | |Last modified | 12.05.2022 | 15 | |Controller | AXC F 2152 | 16 | |Firmware | 2022.0.4 LTS | 17 | |SDK | 2022.0.4 LTS | 18 | 19 | Hardware requirements 20 | 21 | Starter kit- AXC F 2152 STARTERKIT - (Order number 1046568), consisting of: 22 | 23 | 1. I/O module AXL F DI8/1 DO8/1 1H (Order number 2701916). 24 | 1. I/O module AXL F AI2 AO2 1H (Order number 2702072). 25 | 1. Interface module UM 45-IB-DI/SIM8 (Order number 2962997). 26 | 1. Patch cable FL CAT5 PATCH 2,0 (Order number 2832289). 27 | 28 | Host machine software requirements: 29 | 30 | 1. Linux operating system. 31 | 1. Software Development Kit (SDK) for at least one PLCnext Control target. 32 | 33 | For steps involving the use of PLCnext Engineer: 34 | 35 | 1. Windows 10 operating system. 36 | 1. PLCnext Engineer version 2022.0. 37 | 38 | ## Introduction 39 | 40 | The term "runtime" is used to describe a specific type of PLCnext application. PLCnext runtime applications usually provide an alternative to the real-time control components provided by the Automation Runtime Platform (ARP) - i.e. the Execution and Synchronisation Manager (ESM) and the Embedded Common Language Runtime (eCLR). 41 | 42 | One example of a runtime application is [Codesys](https://www.plcnextstore.com/#/48) from 3S-Smart Software Solutions GmbH. Codesys is a popular PLC runtime that can be installed on PLCnext Control hardware as an alternative to the PLCnext runtime. 3S also provides an IDE for configuring the Codesys runtime, which is their equivalent to PLCnext Engineer. 43 | 44 | This article provides a step by step guide to building your own runtime application for PLCnext Control. It concludes with an example of a complete "Sample Runtime" application. 45 | 46 | ## Runtime applications in the PLCnext system architecture 47 | 48 | The [PLCnext Technology Info Center](http://plcnext-infocenter.s3-website.eu-central-1.amazonaws.com/PLCnext_Technology_InfoCenter/PLCnext_Technology_InfoCenter/Home.htm) includes the following architecture diagram. The location of runtime applications is indicated by the red rectangle. 49 | 50 | ![Architecture](Picture/02_Architecture.gif) 51 | 52 | Runtime applications are often the best solution for the following situations: 53 | 54 | - Porting existing software to a PLCnext Control. 55 | 56 | - The development of new PLCnext Control applications in languages like Rust, Java, or Python. 57 | 58 | For **new** PLCnext Control applications written in **C++**, it is recommended to use the Application Component Framework (ACF) to develop Components and Programs in a shared object library, which can be loaded directly by the Automation Runtime Platform (ARP) via the Program Library Manager (PLM) and utilise the real-time Execution and Synchronisation Manager (ESM). These types of applications are called "Extension Components". An example of how to configure a C++ program to run on the ESM, without using PLCnext Engineer, is given in the [CppExamples project](https://github.com/PLCnext/CppExamples/blob/master/Examples/NoEngineer/README.MD). 59 | 60 | The Info Center includes [more information](http://plcnext-infocenter.s3-website.eu-central-1.amazonaws.com/PLCnext_Technology_InfoCenter/PLCnext_Technology_InfoCenter/Programming/Cpp/Cpp-programming.htm) on how C++ programs like runtime applications and function extensions interact with the PLCnext runtime. 61 | 62 | ## PLCnext Control - Runtime integration 63 | 64 | Generally, runtime applications have the following requirements: 65 | - Access to physical I/O modules. 66 | - Access to PLC system services, e.g. reading PLC status information. 67 | - The ability to run deterministic, real-time processes on the operating system. 68 | 69 | Software that does not have any of the above requirements - including most general-purpose open-source software - can usually be cross-compiled for PLCnext Control without any modification, or even installed on a PLCnext Control using a pre-built binary for a compatible architecture (e.g. ARMv7). One example of this type of application is [Node.js](https://www.plcnextstore.com/#/47). While these types of applications share some common features with PLCnext runtime applications, this guide deals with applications that utilise features unique to PLCnext Control hardware and firmware. 70 | 71 | In order for an external application to use the services and the shared memory of the ARP firmware, the application must be started by the PLCnext runtime, and it must also initialise part of the ACF. Due to the configuration of the ARP firmware, the application is started in a separate process. 72 | 73 | In order to initialize the ACF, the application must call the `ArpSystemModule_Load` function. When the initialization is completed, a number of required components are started in the application process (e.g. `Arp.Plc.DomainProxy` and `Arp.Plc.AnsiC`). These components must be defined in the runtime configuration file. 74 | 75 | When the startup phase is completed, a set of functions enables the application to communicate with the `Arp.Plc.AnsiC` component. The respective calls are translated into RSC calls to the ARP firmware. Any changes of the controller state are passed on to the application using a callback function. 76 | 77 | Relationships between these dependencies are represented in the following diagram: 78 | 79 | ![Dependencies](Picture/01_Dependencies.gif) 80 | 81 | ## Before getting started ... 82 | 83 | Please note that the application developed in this series of articles uses: 84 | - `C++`, but the same principles can be used to develop similar applications in most popular programming languages. 85 | - `CMake` and `Ninja`, but other build systems can be used. 86 | - `Bash` scripts, but other scripting languages can be used. 87 | - Microsoft Visual Studio Code, but any editor or IDE can be used. 88 | - A PLC with IP address 192.168.1.10, but any valid IP address can be used. 89 | 90 | ## Getting Started 91 | 92 | In the following series of technical articles, you will build your own runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 93 | 94 | |\#| Topic | Objectives | 95 | | --- | ------ | ------ | 96 | |[01](getting-started/Part-01/README.md)| [Hello PLCnext](getting-started/Part-01/README.md)| Create a simple application in C++, and run it on a PLCnext Control.| 97 | |[02](getting-started/Part-02/README.md)| [PLCnext Control integration](getting-started/Part-02/README.md)| Use the PLCnext Control to start and stop the application automatically.
Create PLCnext ANSI-C component instances.
Write messages to the application log file. | 98 | |[03](getting-started/Part-03/README.md)| [Reading and writing Axioline I/O](getting-started/Part-03/README.md)| Use the PLCnext ANSI-C library to read digital inputs from an Axioline I/O module,
write digital outputs, and read Axioline diagnostic data. | 99 | |[04](getting-started/Part-04/README.md)| [Getting PLCnext Control status via callbacks](getting-started/Part-04/README.md)| Use notifications from the PLCnext Control to start and stop data exchange with Axioline I/O modules. | 100 | |[05](getting-started/Part-05/README.md)| [Using RSC Services](getting-started/Part-05/README.md)| Use the "Device Status" RSC service to log the PLC board temperature. | 101 | |[06](getting-started/Part-06/README.md)| [Creating real-time threads](getting-started/Part-06/README.md)| Perform cyclic I/O processing on a real-time thread, while non-real-time operations continue on another thread. | 102 | |[07](getting-started/Part-07/README.md)| ["Sample Runtime" application](getting-started/Part-07/README.md)| Build and explore a complete runtime application which can be used as the starting point for professional PLCnext runtime projects. | 103 | |[08](getting-started/Part-08/README.md)| [Configure Axioline I/O modules](getting-started/Part-08/README.md)| Parameterise an Axioline serial communication module, and read the configuration back from the module.| 104 | |[09](getting-started/Part-09/README.md)| [Disable unnecessary system services](getting-started/Part-09/README.md)| Learn how to disable Profinet, Ethernet/IP™, HMI and/or OPC-UA services on the PLC.| 105 | |[10](getting-started/Part-10/README.md)| [OPC UA](getting-started/Part-10/README.md)| Make data in the runtime application available through an OPC UA server.| 106 | || [Explore unlimited possibilities ...](getting-started/Part-99/README.md)| Get ideas for other interesting features you can implement in your own PLCnext runtime application.| 107 | 108 | --- 109 | 110 | ## How to get support 111 | 112 | You can get support in the forum of the [PLCnext Community](https://www.plcnext-community.net/forum/). 113 | 114 | --- 115 | 116 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 117 | 118 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 119 | -------------------------------------------------------------------------------- /RuntimeTemplate/.proj: -------------------------------------------------------------------------------- 1 |  2 | 3 | runtime_project 4 | 0.1 5 | $(name) 6 | -------------------------------------------------------------------------------- /RuntimeTemplate/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project($(name)) 4 | 5 | if(NOT CMAKE_BUILD_TYPE) 6 | set(CMAKE_BUILD_TYPE Release) 7 | endif() 8 | 9 | ################# create target ####################################################### 10 | 11 | file(GLOB_RECURSE Headers CONFIGURE_DEPENDS src/*.h src/*.hpp src/*.hxx) 12 | file(GLOB_RECURSE Sources CONFIGURE_DEPENDS src/*.cpp) 13 | add_executable(${CMAKE_PROJECT_NAME} ${Headers} ${Sources}) 14 | 15 | ####################################################################################### 16 | 17 | ################# set install directories ############################################# 18 | 19 | string(REGEX REPLACE "^.*\\(([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*$" "\\1" _ARP_SHORT_DEVICE_VERSION ${ARP_DEVICE_VERSION}) 20 | set(BIN_INSTALL_DIR ${ARP_DEVICE}_${_ARP_SHORT_DEVICE_VERSION}/${CMAKE_BUILD_TYPE}) 21 | 22 | ####################################################################################### 23 | 24 | ################# project include-paths ############################################### 25 | 26 | target_include_directories(${CMAKE_PROJECT_NAME} 27 | PRIVATE 28 | $) 29 | 30 | ####################################################################################### 31 | 32 | ################# include arp cmake module path ####################################### 33 | 34 | list(INSERT CMAKE_MODULE_PATH 0 "${ARP_TOOLCHAIN_CMAKE_MODULE_PATH}") 35 | 36 | ####################################################################################### 37 | 38 | ################# set link options #################################################### 39 | # WARNING: Without --no-undefined the linker will not check, whether all necessary # 40 | # libraries are linked. When a library which is necessary is not linked, # 41 | # the firmware will crash and there will be NO indication why it crashed. # 42 | ####################################################################################### 43 | 44 | target_link_options(${CMAKE_PROJECT_NAME} PRIVATE LINKER:--no-undefined) 45 | 46 | ####################################################################################### 47 | 48 | ################# add link targets #################################################### 49 | 50 | find_package(ArpDevice REQUIRED) 51 | find_package(ArpProgramming REQUIRED) 52 | 53 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ArpDevice ArpProgramming 54 | Arp.System.ModuleLib Arp.System.Module) 55 | 56 | ####################################################################################### 57 | 58 | ################# install ############################################################# 59 | 60 | install(TARGETS ${CMAKE_PROJECT_NAME} RUNTIME DESTINATION ${BIN_INSTALL_DIR}) 61 | unset(_ARP_SHORT_DEVICE_VERSION) 62 | 63 | ####################################################################################### -------------------------------------------------------------------------------- /RuntimeTemplate/Runtime.acf.config: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /RuntimeTemplate/Runtime.acf.settings: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /RuntimeTemplate/Runtime.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2019-2022 Phoenix Contact GmbH & Co. KG. All rights reserved. 3 | // Licensed under the MIT. See LICENSE file in the project root for full license information. 4 | // SPDX-License-Identifier: MIT 5 | // 6 | #include "Arp/System/ModuleLib/Module.h" 7 | #include "Arp/System/Commons/Logging.h" 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace std; 13 | using namespace Arp::System::Commons::Diagnostics::Logging; 14 | 15 | int main(int argc, char** argv) 16 | { 17 | // Use syslog for logging until the PLCnext logger is ready 18 | openlog ("$(name)", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); 19 | 20 | // Process command line arguments 21 | string acfSettingsRelPath(""); 22 | 23 | if(argc != 2) 24 | { 25 | syslog (LOG_ERR, "Invalid command line arguments. Only the relative path to the acf.settings file must be passed"); 26 | return -1; 27 | } 28 | else 29 | { 30 | acfSettingsRelPath = argv[1]; 31 | syslog(LOG_INFO, string("Arg Acf settings file path: " + acfSettingsRelPath).c_str()); 32 | } 33 | 34 | char szExePath[PATH_MAX]; 35 | ssize_t count = readlink("/proc/self/exe", szExePath, PATH_MAX); 36 | string strDirPath; 37 | if (count != -1) { 38 | strDirPath = dirname(szExePath); 39 | } 40 | string strSettingsFile(strDirPath); 41 | strSettingsFile += "/" + acfSettingsRelPath; 42 | syslog(LOG_INFO, string("Acf settings file path: " + strSettingsFile).c_str()); 43 | 44 | // Intialise PLCnext module application 45 | // Arguments: 46 | // arpBinaryDir: Path to Arp binaries 47 | // applicationName: The name of the process defined in $(name).acf.config 48 | // acfSettingsPath: Path to *.acf.settings document to set application up 49 | if (ArpSystemModule_Load("/usr/lib", "$(name)", strSettingsFile.c_str()) != 0) 50 | { 51 | syslog (LOG_ERR, "Could not load Arp System Module"); 52 | return -1; 53 | } 54 | syslog (LOG_INFO, "Loaded Arp System Module"); 55 | closelog(); 56 | 57 | Log::Info("Hello PLCnext"); 58 | 59 | // Endless loop with sleep. 60 | // When the plcnext process shuts down, it will also kill this process. 61 | while (true) 62 | { 63 | sleep(1); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /RuntimeTemplate/TemplateDescription.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Create a new runtime project. 10 | 11 | 12 | 13 | 14 | creates a new runtime project in the directory 'CustomRuntime' 15 | 16 | 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Vulnerablitiys 2 | 3 | Phoenix Contact provides a process to report security issues. 4 | Please visit the website for the [Phoenix Contact PSIRT](https://phoenixcontact.com/psirt) process and follow the instructions. -------------------------------------------------------------------------------- /data/runtime.acf.config: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /data/runtime.acf.settings: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /getting-started/Part-01/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 1 (optional) - Hello PLCnext 4 | 5 | The application developed in this series will start with a PLCnext CLI template for runtime-type projects. In this first step, you will create and use a simpler template to create a "Hello World" console application, and run it on a PLCnext Control device. 6 | 7 | While this step is recommended, it is not required for the next step, so you can skip it if you want. 8 | 9 | Procedure: 10 | 11 | 1. Work through the procedure in the following Makers Blog post on the PLCnext Community website: 12 | 13 | [PLCnext CLI Templates](https://www.plcnext-community.net/makersblog/plcnext-cli-templates/) 14 | 15 | That's it. :) 16 | 17 | You are now familiar with the process of adding a project template to the PLCnext CLI, and building and deploying a simple executable project using that template. The next step does something similar, but using a template for a "runtime"-type application. 18 | 19 | --- 20 | 21 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 22 | 23 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 24 | -------------------------------------------------------------------------------- /getting-started/Part-02/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 2 - PLCnext Control integration 4 | 5 | In the previous article, a simple "Hello World" application was run on a PLCnext Control device. 6 | 7 | In order for a runtime application to access PLCnext Control services like Axioline I/O, a specific set of Automation Runtime Platform (ARP - i.e. PLCnext) components must be running. Most of these components are started automatically by the ARP, but two additional components - required for ANSI-C access - must be started in our application process. 8 | 9 | Using an Application Component Framework (ACF) configuration file, we will instruct the PLCnext to automatically start and stop our runtime application with the ARP, and to load the required application-specific PLCnext components. 10 | 11 | Then, using an ACF settings file, we will specify a directory where we want application log files to be created and stored. 12 | 13 | The required files will be created automatically using a new PLCnext CLI project template. 14 | 15 | 1. Install the "runtime" PLCnext CLI project template 16 | 17 | Copy the `RuntimeTemplate` directory and its contents from this repository to the PLCnext CLI Templates directory on your development machine, e.g. 18 | 19 | ```bash 20 | cp -r RuntimeTemplate ~/plcncli/Templates 21 | ``` 22 | 23 | 1. Include the new template in a CLI Templates file 24 | 25 | For example, create a file called `CustomTemplates.xml` in the PLCnext CLI `Templates` directory, containing the following: 26 | 27 | ```xml 28 | 29 | 30 | RuntimeTemplate/TemplateDescription.xml 31 | 32 | ``` 33 | 34 | 1. If a new Templates file was created, register it with the PLCnext CLI 35 | 36 | ```bash 37 | plcncli set setting TemplateLocations ./Templates/CustomTemplates.xml --add 38 | ``` 39 | 40 | 1. Check that the new template has been installed correctly. 41 | 42 | ```bash 43 | plcncli new 44 | ``` 45 | 46 | You should see `runtime_project` listed as an option: 47 | ```text 48 | plcncli 22.0.0 LTS (22.0.0.952) 49 | Copyright (c) 2018 PHOENIX CONTACT GmbH & Co. KG 50 | 51 | ERROR(S): 52 | No verb selected. 53 | : 54 | runtime_project Create a new runtime project. 55 | : 56 | ``` 57 | 58 | 1. Create a new project called `Runtime` 59 | 60 | ```bash 61 | cd ~ 62 | plcncli new runtime_project --name Runtime 63 | ``` 64 | 65 | This will automatically create a number of files, including: 66 | 67 | **data/Runtime.acf.config** 68 | 69 | In this configuration file, the `Processes` element tells the Application Component Framework (ACF) to start and stop our `Runtime` application with the plcnext process. The `Libraries` and `Components` elements tell the ACF to start special PLCnext Control components that our application will use to (for example) access Axioline I/O through an ANSI-C interface. 70 | 71 | **data/Runtime.acf.settings** 72 | 73 | This file gives the ACF additional information in order to run our application. 74 | 75 | The value of the `logDir` attribute in the `LogSettings` element is the path where log files for this application will be created, relative to the application's working directory. 76 | 77 | **src/Runtime.cpp** 78 | 79 | - Runtime applications must call the PLCnext Control function `ArpSystemModule_Load`, which notifies the ACF that the runtime application has started successfully. Through this call, the application also provides the path to an `.acf.settings` file. If this function is not called, the runtime application will not be able to access any PLCnext services, and it will be stopped after a short timeout period. 80 | 81 | - The template assumes that the name of the `.acf.settings` file is passed as a command-line argument. The absolute path to this file is added by the code. 82 | 83 | - Runtime applications must never end. The PLCnext Control expects runtime applications to keep working until it tells them to stop. This template includes an infinite loop that simply sleeps for 1 second while waiting for the PLCnext Control to kill the runtime process. 84 | 85 | - Like all other PLCnext Control functions, the logging function is not available until after the call to `ArpSystemModule_Load`, and until then this template uses `syslog` to log messages. 86 | 87 | 1. Set the application target(s) 88 | 89 | ```bash 90 | cd ~/Runtime 91 | plcncli set target --name AXCF2152 --version 2022.0 --add 92 | ``` 93 | 94 | 1. Build the project to generate the `Runtime` executable. 95 | 96 | ```bash 97 | plcncli build 98 | ``` 99 | 100 | 1. Deploy the executable to the PLC. 101 | 102 | ```bash 103 | ssh admin@192.168.1.10 'mkdir -p projects/Runtime' 104 | scp bin/AXCF2152_22.0.4.144/Release/Runtime admin@192.168.1.10:~/projects/Runtime 105 | ``` 106 | 107 | Note: If you receive a "Text file busy" message in response to this command, then the file is probably locked by the PLCnext Control. In this case, stop the plcnext process on the PLC with the command `sudo /etc/init.d/plcnext stop` before copying the file. 108 | 109 | 1. Deploy the `Runtime.acf.settings` file to your project directory on the PLC. 110 | 111 | ```bash 112 | scp data/Runtime.acf.settings admin@192.168.1.10:~/projects/Runtime 113 | ``` 114 | 115 | The destination directory is the one we specified in the call to `ArpSystemModule_Load`. 116 | 117 | 1. Deploy the `Runtime.acf.config` file to the PLC's `Default` project directory. 118 | 119 | ```bash 120 | scp data/Runtime.acf.config admin@192.168.1.10:~/projects/Default 121 | ``` 122 | 123 | All `.acf.config` files in this directory are processed by the ACF when the plcnext process starts up, via the `Default.acf.config` file in the same directory. 124 | 125 | 1. Open a secure shell session on the PLC: 126 | 127 | ```bash 128 | ssh admin@192.168.1.10 129 | ``` 130 | 131 | 1. Create the `logs` directory for our application: 132 | 133 | ```bash 134 | mkdir /opt/plcnext/projects/Runtime/logs 135 | ``` 136 | 137 | 1. Restart the plcnext process: 138 | 139 | ```bash 140 | sudo /etc/init.d/plcnext restart 141 | ``` 142 | 143 | 1. Give the plcnext process a short time to start our application, and then check that our application has started successfully by examining the plcnext log file: 144 | 145 | ```bash 146 | cat /opt/plcnext/logs/Output.log | grep Runtime 147 | ``` 148 | 149 | The result should be something like: 150 | 151 | ```text 152 | 12.05.22 13:35:28.218 Arp.System.Acf.Internal.Sm.ProcessesController INFO - Process 'Runtime' started successfully. 153 | 12.05.22 13:35:33.577 Arp.System.Acf.Internal.Sm.ProcessesController INFO - Library 'Arp.Plc.AnsiC.Library' in process 'Runtime' loaded. 154 | 12.05.22 13:35:33.583 Arp.System.Acf.Internal.Sm.ProcessesController INFO - Library 'Arp.Plc.Domain.Library' in process 'Runtime' loaded. 155 | 12.05.22 13:35:33.594 Arp.System.Acf.Internal.Sm.ProcessesController INFO - Library 'Arp.System.UmRscAuthorizator.Library' in process 'Runtime' loaded. 156 | 12.05.22 13:35:33.734 Arp.System.Acf.Internal.Sm.ComponentsController INFO - Component 'Arp.Plc.AnsiC' in process 'Runtime' created. 157 | 12.05.22 13:35:33.735 Arp.System.Acf.Internal.Sm.ComponentsController INFO - Component 'Arp.Plc.DomainProxy.IoAnsiCAdaption' in process 'Runtime' created. 158 | 12.05.22 13:35:33.737 Arp.System.Acf.Internal.Sm.ComponentsController INFO - Component 'Arp.System.UmRscAuthorizator@Runtime' in process 'Runtime' created. 159 | ``` 160 | 161 | 1. Check the contents of the application log file: 162 | 163 | ```bash 164 | cat /opt/plcnext/projects/Runtime/logs/Runtime.log 165 | ``` 166 | 167 | You should see a number of formatted output messages, including a message similar to the following: 168 | 169 | ```text 170 | 12.05.22 13:35:27.719 root INFO - Hello PLCnext 171 | ``` 172 | 173 | --- 174 | 175 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 176 | 177 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 178 | -------------------------------------------------------------------------------- /getting-started/Part-03/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 3 - Reading and writing Axioline I/O 4 | 5 | A requirement of most PLCnext runtime applications is that they must exchange process data with input and output modules on the local Axioline bus. 6 | 7 | The Axioline bus is controlled by a PLCnext Control system component, and this Axioline component must be configured using Technology Independent Configuration (.tic) files. 8 | 9 | In this article, we will use [PLCnext Engineer](http://phoenixcontact.net/product/1046008) to generate a set of TIC files for a specific arrangement of Axioline I/O modules. Note that it is possible to configure the Axioline bus without using PLCnext Engineer - a description of how to do this is beyond the scope of this series, but will be covered in a future technical article in the PLCnext Community. 10 | 11 | 1. In PLCnext Engineer, [create a new project](https://youtu.be/I-FeT3p6cGA) that includes: 12 | - A PLCnext Control with the correct firmware version. 13 | - Axioline I/O modules corresponding to your physical hardware arrangement. 14 | 15 | Make sure that there are no Programs or Tasks scheduled to run on either ESM1 or ESM2, and no connections in the PLCnext Port List. 16 | 17 | 1. Download the PLCnext Engineer project to the PLC. 18 | 19 | This creates a set of .tic files on the PLC that results in the automatic configuration of the Axioline bus when the plcnext process starts. 20 | 21 | 1. Determine I/O port names from the .tic files created by PLCnext Engineer. 22 | 23 | Examine the .tic file(s) in the PLC directory `/opt/plcnext/projects/PCWE/Io/Arp.Io.AxlC`. The files of interest have long, cryptic file names (e.g. `1da1f65d-b76f-4364-bfc4-f59474ccfdad.tic`). In these files, look for elements labelled "IO:Port", and note down the "NodeId", "Name" and "DataType" of each port. These will be needed for our application. 24 | 25 | 1. Modify the file `Runtime.cpp` so it looks like the following: 26 |
27 | (click to see/hide code) 28 | 29 | ```cpp 30 | // 31 | // Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 32 | // Licensed under the MIT. See LICENSE file in the project root for full license information. 33 | // SPDX-License-Identifier: MIT 34 | // 35 | #include "Arp/System/ModuleLib/Module.h" 36 | #include "Arp/System/Commons/Logging.h" 37 | #include "Arp/Plc/AnsiC/Gds/DataLayout.h" 38 | #include "Arp/Plc/AnsiC/Io/FbIoSystem.h" 39 | #include "Arp/Plc/AnsiC/Io/Axio.h" 40 | 41 | #include 42 | #include 43 | #include 44 | 45 | using namespace Arp; 46 | using namespace Arp::System::Commons::Diagnostics::Logging; 47 | 48 | // Read data from a fieldbus input frame 49 | void readInputData(char* pValue, size_t valueSize) 50 | { 51 | TGdsBuffer* gdsBuffer = NULL; 52 | 53 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DI8", &gdsBuffer)) 54 | { 55 | size_t offset = 0; 56 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DI8", &offset)) 57 | { 58 | // Begin read operation, memory buffer will be locked 59 | char* readDataBufferPage; 60 | if(ArpPlcGds_BeginRead(gdsBuffer, &readDataBufferPage)) 61 | { 62 | // Copy data from GDS buffer 63 | char* dataAddress = readDataBufferPage + offset; 64 | memcpy(pValue, dataAddress, valueSize); 65 | 66 | // Unlock buffer 67 | if(!ArpPlcGds_EndRead(gdsBuffer)) 68 | { 69 | Log::Error("ArpPlcGds_EndRead failed"); 70 | } 71 | } 72 | else 73 | { 74 | Log::Error("ArpPlcGds_BeginRead failed"); 75 | ArpPlcGds_EndRead(gdsBuffer); 76 | } 77 | } 78 | else 79 | { 80 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 81 | } 82 | } 83 | else 84 | { 85 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 86 | } 87 | 88 | // Release the GDS buffer and free internal resources 89 | if(gdsBuffer != NULL) 90 | { 91 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 92 | { 93 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 94 | } 95 | } 96 | } 97 | 98 | // Write data to a fieldbus output frame 99 | void writeOutputData(const char* pValue, size_t valueSize) 100 | { 101 | TGdsBuffer* gdsBuffer = NULL; 102 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DO8", &gdsBuffer)) 103 | { 104 | size_t offset = 0; 105 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DO8", &offset)) 106 | { 107 | // Begin write operation, memory buffer will be locked 108 | char* dataBufferPage = NULL; 109 | if(ArpPlcGds_BeginWrite(gdsBuffer, &dataBufferPage)) 110 | { 111 | // Copy data to GDS buffer 112 | char* dataAddress = dataBufferPage + offset; 113 | memcpy(dataAddress, pValue, valueSize); 114 | 115 | // Unlock buffer 116 | if(!ArpPlcGds_EndWrite(gdsBuffer)) 117 | { 118 | Log::Error("ArpPlcGds_EndWrite failed"); 119 | } 120 | } 121 | else 122 | { 123 | Log::Error("ArpPlcGds_BeginWrite failed"); 124 | ArpPlcGds_EndWrite(gdsBuffer); 125 | } 126 | } 127 | else 128 | { 129 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 130 | } 131 | } 132 | else 133 | { 134 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 135 | } 136 | 137 | // Release the GDS buffer and free internal resources 138 | if(gdsBuffer != NULL) 139 | { 140 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 141 | { 142 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 143 | } 144 | } 145 | } 146 | 147 | // Read data Axioline status data 148 | void readAxiolineStatus(char* pValue, size_t valueSize) 149 | { 150 | TGdsBuffer* gdsBuffer = NULL; 151 | 152 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/AXIO_DIAG_STATUS_REG", &gdsBuffer)) 153 | { 154 | size_t offset = 0; 155 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/AXIO_DIAG_STATUS_REG", &offset)) 156 | { 157 | // Begin read operation, memory buffer will be locked 158 | char* readDataBufferPage; 159 | if(ArpPlcGds_BeginRead(gdsBuffer, &readDataBufferPage)) 160 | { 161 | // Copy data from GDS buffer 162 | char* dataAddress = readDataBufferPage + offset; 163 | memcpy(pValue, dataAddress, valueSize); 164 | 165 | // Unlock buffer 166 | if(!ArpPlcGds_EndRead(gdsBuffer)) 167 | { 168 | Log::Error("ArpPlcGds_EndRead failed"); 169 | } 170 | } 171 | else 172 | { 173 | Log::Error("ArpPlcGds_BeginRead failed"); 174 | ArpPlcGds_EndRead(gdsBuffer); 175 | } 176 | } 177 | else 178 | { 179 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 180 | } 181 | } 182 | else 183 | { 184 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 185 | } 186 | 187 | // Release the GDS buffer and free internal resources 188 | if(gdsBuffer != NULL) 189 | { 190 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 191 | { 192 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 193 | } 194 | } 195 | } 196 | 197 | int main(int argc, char** argv) 198 | { 199 | // Ask plcnext for access to its services 200 | // Use syslog for logging until the PLCnext logger is ready 201 | openlog ("runtime", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); 202 | 203 | // Process command line arguments 204 | string acfSettingsRelPath(""); 205 | 206 | if(argc != 2) 207 | { 208 | syslog (LOG_ERR, "Invalid command line arguments. Only relative path to the acf.settings file must be passed"); 209 | return -1; 210 | } 211 | else 212 | { 213 | acfSettingsRelPath = argv[1]; 214 | syslog(LOG_INFO, string("Arg Acf settings file path: " + acfSettingsRelPath).c_str()); 215 | } 216 | 217 | char szExePath[PATH_MAX]; 218 | ssize_t count = readlink("/proc/self/exe", szExePath, PATH_MAX); 219 | string strDirPath; 220 | if (count != -1) { 221 | strDirPath = dirname(szExePath); 222 | } 223 | string strSettingsFile(strDirPath); 224 | strSettingsFile += "/" + acfSettingsRelPath; 225 | syslog(LOG_INFO, string("Acf settings file path: " + strSettingsFile).c_str()); 226 | 227 | // Intialize PLCnext module application 228 | // Arguments: 229 | // arpBinaryDir: Path to Arp binaries 230 | // applicationName: Arbitrary Name of Application 231 | // acfSettingsPath: Path to *.acf.settings document to set application up 232 | if (ArpSystemModule_Load("/usr/lib", "runtime", strSettingsFile.c_str()) != 0) 233 | { 234 | syslog (LOG_ERR, "Could not load Arp System Module"); 235 | return -1; 236 | } 237 | syslog (LOG_INFO, "Loaded Arp System Module"); 238 | closelog(); 239 | 240 | Log::Info("Hello PLCnext"); 241 | 242 | // Wait for Axioline configuration to be completed 243 | // before attempting to access I/O 244 | sleep(30); 245 | 246 | // Declare a process data item 247 | uint8_t value = 0; 248 | uint16_t axio_status = 0; 249 | 250 | while (true) 251 | { 252 | // Read Axioline Status 253 | readAxiolineStatus((char*)&axio_status, sizeof(axio_status)); 254 | 255 | Log::Info("Axioline Status: {0:#04x}", axio_status); 256 | 257 | // Read process inputs 258 | readInputData((char*)&value, sizeof(value)); 259 | 260 | Log::Info("Read value of: {0:#04x}", value); 261 | 262 | // Perform application-specific processing 263 | // In this case, simply invert the process data bits 264 | value = ~value; 265 | 266 | // Write process outputs 267 | writeOutputData((char*)&value, sizeof(value)); 268 | 269 | // Wait a short time before repeating 270 | sleep(1); 271 | } 272 | } 273 | ``` 274 | 275 |
276 | 277 | Notes on the above code: 278 | - After the call to `ArpSystemModule_Load`, we must wait for the Axioline bus configuration to be completed before attempting to access I/O data. In this case we use a timer, but there is a smarter way to do this - as we shall see in a later article. 279 | - In this example, the I/O read and write operations are performed in separate functions. 280 | - The required format of I/O port names is {Bus Type}/{NodeId}.{Name}, where {Bus Type} = "Arp.Io.AxlC", and {NodeId} and {Name} were obtained from the .tic file on the PLC. 281 | - In this example, we have hard-coded the I/O details (including the port name) in the read and write functions, but in a real application these types of functions should be general-purpose. 282 | - The read/write process takes place in a number of steps: 283 | 1. Get a pointer to the Global Data Space (GDS) buffer. 284 | 1. Get the offset to the I/O variable in the GDS. 285 | 1. Lock the GDS buffer for reading or writing. 286 | 1. Copy data to or from the GDS. 287 | 1. Free resources. 288 | 289 | Since the layout of the GDS is fixed during the startup of the plcnext process, it is not necessary to repeatedly retrieve references to a given port, as is done in this example. Instead, GDS references could be retrived once for each port, and then cached in a collection. This is demonstrated in the "Sample Runtime" application, later in this series. 290 | - Axioline Status information is retrieved in a similar way to I/O data, using the `readAxiolineStatus` function. Once again, in a real application this function should be general-purpose. The following is a complete list of Axioline system variables that can be read in this way: 291 | 292 | | Name | Type | 293 | |:-----------------------------------------|:-----| 294 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG        | WORD | 295 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_HI      | BYTE | 296 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_LOW     | BYTE | 297 | | Arp.Io.AxlC/AXIO_DIAG_PARAM_REG_HI       | BYTE | 298 | | Arp.Io.AxlC/AXIO_DIAG_PARAM_REG_LOW | BYTE | 299 | | Arp.Io.AxlC/AXIO_DIAG_PARAM_2_REG_HI     | BYTE | 300 | | Arp.Io.AxlC/AXIO_DIAG_PARAM_2_REG_LOW    | BYTE | 301 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_PW      | BOOL | 302 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_PF      | BOOL | 303 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_BUS     | BOOL | 304 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_RUN     | BOOL | 305 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_ACT     | BOOL | 306 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_RDY     | BOOL | 307 | | Arp.Io.AxlC/AXIO_DIAG_STATUS_REG_SYSFAIL | BOOL | 308 | 309 | These variables are described in the User Manual *Axioline F: Diagnostic registers, and error messages*, available for download from the Phoenix Contact website. 310 | 311 | An important point to note is that, in this project, I/O data is transferred automatically from the GDS to Axioline I/O modules every 500μs. This should be considered for real-time tasks with very short cycle times (in the order of milliseconds). 312 | 313 | 1. Modify the relevant section of the CMakeLists.txt file, so it looks like the following: 314 | 315 | ```cmake 316 | ################# add link targets #################################################### 317 | 318 | find_package(ArpDevice REQUIRED) 319 | find_package(ArpProgramming REQUIRED) 320 | 321 | target_link_libraries(runtime PRIVATE ArpDevice ArpProgramming 322 | Arp.System.ModuleLib Arp.System.Module 323 | Arp.Plc.AnsiC) 324 | 325 | ####################################################################################### 326 | ``` 327 | 328 | The `Arp.Plc.AnsiC` library implements the ANSI-C function used to access I/O process data. 329 | 330 | 1. Build the project to generate the `Runtime` executable. 331 | 332 | ```bash 333 | plcncli build 334 | ``` 335 | 336 | 1. Deploy the executable to the PLC. 337 | 338 | ```bash 339 | scp bin/AXCF2152_22.0.4.144/Release/Runtime admin@192.168.1.10:~/projects/Runtime 340 | ``` 341 | 342 | Note: If you receive a "Text file busy" message in response to this command, then the file is probably locked by the PLCnext Control. In this case, stop the plcnext process on the PLC with the command `sudo /etc/init.d/plcnext stop` before copying the file. 343 | 344 | It is assumed that the ACF config and settings files (described in a previous article) are already on the PLC. 345 | 346 | 1. Open a secure shell session on the PLC: 347 | 348 | ```bash 349 | ssh admin@192.168.1.10 350 | ``` 351 | 352 | 1. Restart the plcnext process: 353 | 354 | ```bash 355 | sudo /etc/init.d/plcnext restart 356 | ``` 357 | 358 | After approximately 30 seconds, you should see the digital outputs set to the inverse of the digital input values. 359 | 360 | --- 361 | 362 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 363 | 364 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 365 | -------------------------------------------------------------------------------- /getting-started/Part-04/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 4 - Getting PLCnext Control status via callbacks 4 | 5 | It is possible to get notifications of PLCnext Control state changes via a callback function. In this article, we will use these notifications to start and stop process data exchange with the Axioline I/O modules. 6 | 7 | 1. Modify the file `Runtime.cpp` so it looks like the following: 8 |
9 | (click to see/hide code) 10 | 11 | ```cpp 12 | // 13 | // Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 14 | // Licensed under the MIT. See LICENSE file in the project root for full license information. 15 | // SPDX-License-Identifier: MIT 16 | // 17 | #include "Arp/System/ModuleLib/Module.h" 18 | #include "Arp/System/Commons/Logging.h" 19 | #include "Arp/Plc/AnsiC/Gds/DataLayout.h" 20 | #include "Arp/Plc/AnsiC/Io/FbIoSystem.h" 21 | #include "Arp/Plc/AnsiC/Io/Axio.h" 22 | #include "Arp/Plc/AnsiC/Domain/PlcOperationHandler.h" 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | using namespace Arp; 29 | using namespace Arp::System::Commons::Diagnostics::Logging; 30 | 31 | bool processing = false; 32 | 33 | // Callback function for the PLC state 34 | void plcOperationHandler(enum PlcOperation operation) 35 | { 36 | switch (operation) 37 | { 38 | case PlcOperation_Load: 39 | Log::Info("Call of PLC Load"); 40 | break; 41 | 42 | case PlcOperation_Setup: 43 | Log::Info("Call of PLC Setup"); 44 | break; 45 | 46 | case PlcOperation_StartCold: 47 | Log::Info("Call of PLC Start Cold"); 48 | processing = true; 49 | break; 50 | 51 | case PlcOperation_StartWarm: 52 | Log::Info("Call of PLC Start Warm"); 53 | // When this state-change occurs, the firmware is ready to serve requests. 54 | // TODO: request pointers to RSC services, if necessary. 55 | processing = true; 56 | break; 57 | 58 | case PlcOperation_StartHot: 59 | Log::Info("Call of PLC Start Hot"); 60 | processing = true; 61 | break; 62 | 63 | case PlcOperation_Stop: 64 | Log::Info("Call of PLC Stop"); 65 | processing = false; 66 | break; 67 | 68 | case PlcOperation_Reset: 69 | Log::Info("Call of PLC Reset"); 70 | processing = false; 71 | break; 72 | 73 | case PlcOperation_Unload: 74 | Log::Info("Call of PLC Unload"); 75 | processing = false; 76 | break; 77 | 78 | case PlcOperation_None: 79 | Log::Info("Call of PLC None"); 80 | break; 81 | 82 | default: 83 | Log::Error("Call of unknown PLC state"); 84 | break; 85 | } 86 | } 87 | 88 | // Read data from a fieldbus input frame 89 | void readInputData(char* pValue, size_t valueSize) 90 | { 91 | TGdsBuffer* gdsBuffer = NULL; 92 | 93 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DI8", &gdsBuffer)) 94 | { 95 | size_t offset = 0; 96 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DI8", &offset)) 97 | { 98 | // Begin read operation, memory buffer will be locked 99 | char* readDataBufferPage; 100 | if(ArpPlcGds_BeginRead(gdsBuffer, &readDataBufferPage)) 101 | { 102 | // Copy data from GDS buffer 103 | char* dataAddress = readDataBufferPage + offset; 104 | memcpy(pValue, dataAddress, valueSize); 105 | 106 | // Unlock buffer 107 | if(!ArpPlcGds_EndRead(gdsBuffer)) 108 | { 109 | Log::Error("ArpPlcGds_EndRead failed"); 110 | } 111 | } 112 | else 113 | { 114 | Log::Error("ArpPlcGds_BeginRead failed"); 115 | ArpPlcGds_EndRead(gdsBuffer); 116 | } 117 | } 118 | else 119 | { 120 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 121 | } 122 | } 123 | else 124 | { 125 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 126 | } 127 | 128 | // Release the GDS buffer and free internal resources 129 | if(gdsBuffer != NULL) 130 | { 131 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 132 | { 133 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 134 | } 135 | } 136 | } 137 | 138 | // Write data to a fieldbus output frame 139 | void writeOutputData(const char* pValue, size_t valueSize) 140 | { 141 | TGdsBuffer* gdsBuffer = NULL; 142 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DO8", &gdsBuffer)) 143 | { 144 | size_t offset = 0; 145 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DO8", &offset)) 146 | { 147 | // Begin write operation, memory buffer will be locked 148 | char* dataBufferPage = NULL; 149 | if(ArpPlcGds_BeginWrite(gdsBuffer, &dataBufferPage)) 150 | { 151 | // Copy data to GDS buffer 152 | char* dataAddress = dataBufferPage + offset; 153 | memcpy(dataAddress, pValue, valueSize); 154 | 155 | // Unlock buffer 156 | if(!ArpPlcGds_EndWrite(gdsBuffer)) 157 | { 158 | Log::Error("ArpPlcGds_EndWrite failed"); 159 | } 160 | } 161 | else 162 | { 163 | Log::Error("ArpPlcGds_BeginWrite failed"); 164 | ArpPlcGds_EndWrite(gdsBuffer); 165 | } 166 | } 167 | else 168 | { 169 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 170 | } 171 | } 172 | else 173 | { 174 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 175 | } 176 | 177 | // Release the GDS buffer and free internal resources 178 | if(gdsBuffer != NULL) 179 | { 180 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 181 | { 182 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 183 | } 184 | } 185 | } 186 | 187 | int main(int argc, char** argv) 188 | { 189 | // Register the status update callback 190 | // This is important to get the status of the "firmware ready" event, "PlcOperation_StartWarm" 191 | ArpPlcDomain_SetHandler(plcOperationHandler); 192 | 193 | // Ask plcnext for access to its services 194 | // Use syslog for logging until the PLCnext logger is ready 195 | openlog ("runtime", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); 196 | 197 | // Process command line arguments 198 | string acfSettingsRelPath(""); 199 | 200 | if(argc != 2) 201 | { 202 | syslog (LOG_ERR, "Invalid command line arguments. Only relative path to the acf.settings file must be passed"); 203 | return -1; 204 | } 205 | else 206 | { 207 | acfSettingsRelPath = argv[1]; 208 | syslog(LOG_INFO, string("Arg Acf settings file path: " + acfSettingsRelPath).c_str()); 209 | } 210 | 211 | char szExePath[PATH_MAX]; 212 | ssize_t count = readlink("/proc/self/exe", szExePath, PATH_MAX); 213 | string strDirPath; 214 | if (count != -1) { 215 | strDirPath = dirname(szExePath); 216 | } 217 | string strSettingsFile(strDirPath); 218 | strSettingsFile += "/" + acfSettingsRelPath; 219 | syslog(LOG_INFO, string("Acf settings file path: " + strSettingsFile).c_str()); 220 | 221 | // Intialize PLCnext module application 222 | // Arguments: 223 | // arpBinaryDir: Path to Arp binaries 224 | // applicationName: Arbitrary Name of Application 225 | // acfSettingsPath: Path to *.acf.settings document to set application up 226 | if (ArpSystemModule_Load("/usr/lib", "runtime", strSettingsFile.c_str()) != 0) 227 | { 228 | syslog (LOG_ERR, "Could not load Arp System Module"); 229 | return -1; 230 | } 231 | syslog (LOG_INFO, "Loaded Arp System Module"); 232 | closelog(); 233 | 234 | // Declare a process data item 235 | uint8_t value = 0; 236 | 237 | while (true) 238 | { 239 | if (processing) 240 | { 241 | // Read process inputs 242 | readInputData((char*)&value, sizeof(value)); 243 | 244 | // Log::Info("Read value of: {0:#04x}", value); 245 | 246 | // Perform application-specific processing 247 | // In this case, simply invert the process data bits 248 | value = ~value; 249 | 250 | // Write process outputs 251 | writeOutputData((char*)&value, sizeof(value)); 252 | } 253 | 254 | // Wait a short time before repeating 255 | sleep(1); 256 | } 257 | } 258 | ``` 259 | 260 |
261 | 262 | Notes on the above code: 263 | - The startup delay timer from the earlier example has been removed. 264 | - The boolean variable `processing` has been added. This is used to enable and disable I/O processing in the `main` function. 265 | - A callback function `plcOperationHandler` has been defined, which sets and resets the `processing` flag based on the state of the PLC. 266 | - The callback function is registered with the PLCnext Control **before** the `ArpSystemModule_Load` function is called. This ensures that all PLC state changes will be captured during startup. 267 | 268 | 1. Build the project to generate the `Runtime` executable. 269 | 270 | ```bash 271 | plcncli build 272 | ``` 273 | 274 | 1. Deploy the executable to the PLC. 275 | 276 | ```bash 277 | scp bin/AXCF2152_22.0.4.144/Release/Runtime admin@192.168.1.10:~/projects/Runtime 278 | ``` 279 | 280 | Note: If you receive a "Text file busy" message in response to this command, then the file is probably locked by the PLCnext Control. In this case, stop the plcnext process on the PLC with the command `sudo /etc/init.d/plcnext stop` before copying the file. 281 | 282 | It is assumed that the ACF config and settings files (described in a previous article) are already on the PLC. 283 | 284 | 1. Open a secure shell session on the PLC: 285 | 286 | ```bash 287 | ssh admin@192.168.1.10 288 | ``` 289 | 290 | 1. Restart the plcnext process: 291 | 292 | ```bash 293 | sudo /etc/init.d/plcnext restart 294 | ``` 295 | 296 | 1. Check the log file to see messages for each PLC state change. 297 | 298 | ```bash 299 | cat /opt/plcnext/projects/Runtime/logs/Runtime.log 300 | ``` 301 | 302 | --- 303 | 304 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 305 | 306 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 307 | -------------------------------------------------------------------------------- /getting-started/Part-05/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 5 - Using RSC Services 4 | 5 | There are a number of examples that demonstrate how to subscribe to RSC Services from PLCnext C++ Function Extension components. These components, which inherit from the PLCnext class `ComponentBase`, include a `SubscribeServices` method that is called by the PLCnext Control when it is ready to receive subscriptions to RSC services. 6 | 7 | For runtime applications, which do not include a class that inherits from `ComponentBase`, the right time to subscribe to RSC services is when the PLC notifies us of the "Start Warm" state. 8 | 9 | The example below subscribes to the "Device Status" RSC service, which provides live values for a number of PLC variables, and uses this service to write the PLC board temperature to the application log file. 10 | 11 | 1. Modify the file `Runtime.cpp` so it looks like the following: 12 |
13 | (click to see/hide code) 14 | 15 | ```cpp 16 | // 17 | // Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 18 | // Licensed under the MIT. See LICENSE file in the project root for full license information. 19 | // SPDX-License-Identifier: MIT 20 | // 21 | #include "Arp/System/ModuleLib/Module.h" 22 | #include "Arp/System/Commons/Logging.h" 23 | #include "Arp/Plc/AnsiC/Gds/DataLayout.h" 24 | #include "Arp/Plc/AnsiC/Io/FbIoSystem.h" 25 | #include "Arp/Plc/AnsiC/Io/Axio.h" 26 | #include "Arp/Plc/AnsiC/Domain/PlcOperationHandler.h" 27 | #include "Arp/System/Rsc/ServiceManager.hpp" 28 | #include "Arp/Device/Interface/Services/IDeviceStatusService.hpp" 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | using namespace Arp; 35 | using namespace Arp::System::Rsc; 36 | using namespace Arp::Device::Interface::Services; 37 | using namespace Arp::System::Commons::Diagnostics::Logging; 38 | 39 | bool initialised = false; // The RSC service is available 40 | bool processing = false; // Axioline I/O is available 41 | 42 | IDeviceStatusService::Ptr deviceStatusService; // Reference to the RSC service 43 | RscVariant<512> rscBoardTemp; // The value returned from the RSC Service 44 | int8 boardTemp; // The value as an integer 45 | 46 | // Initialise PLCnext RSC services 47 | void initServices() 48 | { 49 | if(!initialised) 50 | { 51 | Log::Info("Call of initServices"); 52 | 53 | deviceStatusService = ServiceManager::GetService(); 54 | 55 | if(deviceStatusService != NULL) 56 | { 57 | Log::Info("Subscribed to Device Status service"); 58 | initialised = true; 59 | } 60 | else 61 | { 62 | Log::Error("Could not subscribe to Device Status service"); 63 | } 64 | } 65 | } 66 | 67 | // Callback function for the PLC state 68 | void plcOperationHandler(enum PlcOperation operation) 69 | { 70 | switch (operation) 71 | { 72 | case PlcOperation_Load: 73 | Log::Info("Call of PLC Load"); 74 | break; 75 | 76 | case PlcOperation_Setup: 77 | Log::Info("Call of PLC Setup"); 78 | break; 79 | 80 | case PlcOperation_StartCold: 81 | Log::Info("Call of PLC Start Cold"); 82 | processing = true; 83 | break; 84 | 85 | case PlcOperation_StartWarm: 86 | Log::Info("Call of PLC Start Warm"); 87 | // When this state-change occurs, the firmware is ready to serve requests. 88 | initServices(); 89 | processing = true; 90 | break; 91 | 92 | case PlcOperation_StartHot: 93 | Log::Info("Call of PLC Start Hot"); 94 | processing = true; 95 | break; 96 | 97 | case PlcOperation_Stop: 98 | Log::Info("Call of PLC Stop"); 99 | processing = false; 100 | break; 101 | 102 | case PlcOperation_Reset: 103 | Log::Info("Call of PLC Reset"); 104 | processing = false; 105 | break; 106 | 107 | case PlcOperation_Unload: 108 | Log::Info("Call of PLC Unload"); 109 | processing = false; 110 | break; 111 | 112 | case PlcOperation_None: 113 | Log::Info("Call of PLC None"); 114 | break; 115 | 116 | default: 117 | Log::Error("Call of unknown PLC state"); 118 | break; 119 | } 120 | } 121 | 122 | // Read data from a fieldbus input frame 123 | void readInputData(char* pValue, size_t valueSize) 124 | { 125 | TGdsBuffer* gdsBuffer = NULL; 126 | 127 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DI8", &gdsBuffer)) 128 | { 129 | size_t offset = 0; 130 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DI8", &offset)) 131 | { 132 | // Begin read operation, memory buffer will be locked 133 | char* readDataBufferPage; 134 | if(ArpPlcGds_BeginRead(gdsBuffer, &readDataBufferPage)) 135 | { 136 | // Copy data from GDS buffer 137 | char* dataAddress = readDataBufferPage + offset; 138 | memcpy(pValue, dataAddress, valueSize); 139 | 140 | // Unlock buffer 141 | if(!ArpPlcGds_EndRead(gdsBuffer)) 142 | { 143 | Log::Error("ArpPlcGds_EndRead failed"); 144 | } 145 | } 146 | else 147 | { 148 | Log::Error("ArpPlcGds_BeginRead failed"); 149 | ArpPlcGds_EndRead(gdsBuffer); 150 | } 151 | } 152 | else 153 | { 154 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 155 | } 156 | } 157 | else 158 | { 159 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 160 | } 161 | 162 | // Release the GDS buffer and free internal resources 163 | if(gdsBuffer != NULL) 164 | { 165 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 166 | { 167 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 168 | } 169 | } 170 | } 171 | 172 | // Write data to a fieldbus output frame 173 | void writeOutputData(const char* pValue, size_t valueSize) 174 | { 175 | TGdsBuffer* gdsBuffer = NULL; 176 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DO8", &gdsBuffer)) 177 | { 178 | size_t offset = 0; 179 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DO8", &offset)) 180 | { 181 | // Begin write operation, memory buffer will be locked 182 | char* dataBufferPage = NULL; 183 | if(ArpPlcGds_BeginWrite(gdsBuffer, &dataBufferPage)) 184 | { 185 | // Copy data to GDS buffer 186 | char* dataAddress = dataBufferPage + offset; 187 | memcpy(dataAddress, pValue, valueSize); 188 | 189 | // Unlock buffer 190 | if(!ArpPlcGds_EndWrite(gdsBuffer)) 191 | { 192 | Log::Error("ArpPlcGds_EndWrite failed"); 193 | } 194 | } 195 | else 196 | { 197 | Log::Error("ArpPlcGds_BeginWrite failed"); 198 | ArpPlcGds_EndWrite(gdsBuffer); 199 | } 200 | } 201 | else 202 | { 203 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 204 | } 205 | } 206 | else 207 | { 208 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 209 | } 210 | 211 | // Release the GDS buffer and free internal resources 212 | if(gdsBuffer != NULL) 213 | { 214 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 215 | { 216 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 217 | } 218 | } 219 | } 220 | 221 | int main(int argc, char** argv) 222 | { 223 | // Register the status update callback 224 | // This is important to get the status of the "firmware ready" event, "PlcOperation_StartWarm" 225 | ArpPlcDomain_SetHandler(plcOperationHandler); 226 | 227 | // Ask plcnext for access to its services 228 | // Use syslog for logging until the PLCnext logger is ready 229 | openlog ("runtime", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); 230 | 231 | // Process command line arguments 232 | string acfSettingsRelPath(""); 233 | 234 | if(argc != 2) 235 | { 236 | syslog (LOG_ERR, "Invalid command line arguments. Only relative path to the acf.settings file must be passed"); 237 | return -1; 238 | } 239 | else 240 | { 241 | acfSettingsRelPath = argv[1]; 242 | syslog(LOG_INFO, string("Arg Acf settings file path: " + acfSettingsRelPath).c_str()); 243 | } 244 | 245 | char szExePath[PATH_MAX]; 246 | ssize_t count = readlink("/proc/self/exe", szExePath, PATH_MAX); 247 | string strDirPath; 248 | if (count != -1) { 249 | strDirPath = dirname(szExePath); 250 | } 251 | string strSettingsFile(strDirPath); 252 | strSettingsFile += "/" + acfSettingsRelPath; 253 | syslog(LOG_INFO, string("Acf settings file path: " + strSettingsFile).c_str()); 254 | 255 | // Intialize PLCnext module application 256 | // Arguments: 257 | // arpBinaryDir: Path to Arp binaries 258 | // applicationName: Arbitrary Name of Application 259 | // acfSettingsPath: Path to *.acf.settings document to set application up 260 | if (ArpSystemModule_Load("/usr/lib", "runtime", strSettingsFile.c_str()) != 0) 261 | { 262 | syslog (LOG_ERR, "Could not load Arp System Module"); 263 | return -1; 264 | } 265 | syslog (LOG_INFO, "Loaded Arp System Module"); 266 | closelog(); 267 | 268 | // Declare a process data item 269 | uint8_t value = 0; 270 | 271 | while (true) 272 | { 273 | if (initialised) 274 | { 275 | // Get the board temperature and write it to file 276 | rscBoardTemp = deviceStatusService->GetItem("Status.Board.Temperature.Centigrade"); 277 | rscBoardTemp.CopyTo(boardTemp); 278 | Log::Info("Current PLC board temperature is {0:d}°C", boardTemp); 279 | } 280 | 281 | if (processing) 282 | { 283 | // Read process inputs 284 | readInputData((char*)&value, sizeof(value)); 285 | 286 | // Log::Info("Read value of: {0:#04x}", value); 287 | 288 | // Perform application-specific processing 289 | // In this case, simply invert the process data bits 290 | value = ~value; 291 | 292 | // Write process outputs 293 | writeOutputData((char*)&value, sizeof(value)); 294 | } 295 | 296 | // Wait a short time before repeating 297 | sleep(1); 298 | } 299 | } 300 | ``` 301 | 302 |
303 | 304 | Notes on the above code: 305 | - The boolean variable `initialised` has been added. This is used to enable and disable calls to RSC services in the `main` function. 306 | - The function `initServices` has been defined, which subscribes to the Device Status RSC service. 307 | - In the `plcOperationHandler` callback function, the `initServices` function is called from the "Start Warm" case. 308 | - In the `main` functions infinite `while` loop, the current board PLC temperature is obtained from the Device Status service and written to the application log file. 309 | 310 | 1. Build the project to generate the `Runtime` executable. 311 | 312 | ```bash 313 | plcncli build 314 | ``` 315 | 316 | 1. Deploy the executable to the PLC. 317 | 318 | ```bash 319 | scp bin/AXCF2152_22.0.4.144/Release/Runtime admin@192.168.1.10:~/projects/Runtime 320 | ``` 321 | 322 | Note: If you receive a "Text file busy" message in response to this command, then the file is probably locked by the PLCnext Control. In this case, stop the plcnext process on the PLC with the command `sudo /etc/init.d/plcnext stop` before copying the file. 323 | 324 | It is assumed that the ACF config and settings files (described in a previous article) are already on the PLC. 325 | 326 | 1. Open a secure shell session on the PLC: 327 | 328 | ```bash 329 | ssh admin@192.168.1.10 330 | ``` 331 | 332 | 1. Restart the plcnext process: 333 | 334 | ```bash 335 | sudo /etc/init.d/plcnext restart 336 | ``` 337 | 338 | 1. Check the log file to see messages containing the current PLC board temperature. 339 | 340 | ```bash 341 | cat /opt/plcnext/projects/Runtime/logs/Runtime.log 342 | ``` 343 | 344 | --- 345 | 346 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 347 | 348 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 349 | -------------------------------------------------------------------------------- /getting-started/Part-06/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 6 - Creating real-time threads 4 | 5 | So far, our application has been doing all its work on a single thread that is running with a "standard" (non-real-time) priority. At the end of each execution cycle, our application sleeps for (approximately) 1 second, giving cycle times that are completely non-deterministic. This is fine for applications with low performance requirements, but many industrial automation applications require deterministic, real-time performance. 6 | 7 | PLCnext Control includes the ability to schedule threads with real-time priorities, resulting in deterministic cycle times with very little jitter. This feature is used by the PLCnext Control's own Execution and Synchronisation Manager (ESM), and this feature can also be used in our own application. 8 | 9 | Most industrial applications will include real-time and non-real-time threads. Real-time threads should only be used for time-critical functions and - just like in traditional IEC programming - consideration must be given as to whether any operation on a real-time thread will block for long enough to over-run the required cycle time. 10 | 11 | Some operations that are not suitable for real-time threads include: 12 | - Access to RSC services on the PLCnext Control. 13 | - File I/O, including calls to the `Arp::Log` function on the PLCnext Control. 14 | 15 | In this article, we will move the cyclic I/O processing on to a real-time thread. 16 | 17 | 1. Modify the file `Runtime.cpp` so it looks like the following: 18 |
19 | (click to see/hide code) 20 | 21 | ```cpp 22 | // 23 | // Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 24 | // Licensed under the MIT. See LICENSE file in the project root for full license information. 25 | // SPDX-License-Identifier: MIT 26 | // 27 | #include "Arp/System/ModuleLib/Module.h" 28 | #include "Arp/System/Commons/Logging.h" 29 | #include "Arp/Plc/AnsiC/Gds/DataLayout.h" 30 | #include "Arp/Plc/AnsiC/Io/FbIoSystem.h" 31 | #include "Arp/Plc/AnsiC/Io/Axio.h" 32 | #include "Arp/Plc/AnsiC/Domain/PlcOperationHandler.h" 33 | #include "Arp/System/Rsc/ServiceManager.hpp" 34 | #include "Arp/Device/Interface/Services/IDeviceStatusService.hpp" 35 | 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | using namespace Arp; 43 | using namespace Arp::System::Rsc; 44 | using namespace Arp::Device::Interface::Services; 45 | using namespace Arp::System::Commons::Diagnostics::Logging; 46 | 47 | bool initialised = false; // The RSC service is available 48 | bool processing = false; // Axioline I/O is available 49 | 50 | IDeviceStatusService::Ptr deviceStatusService; // Reference to the RSC service 51 | RscVariant<512> rscBoardTemp; // The value returned from the RSC Service 52 | int8 boardTemp; // The value as an integer 53 | 54 | // Initialise PLCnext RSC services 55 | void initServices() 56 | { 57 | if(!initialised) 58 | { 59 | Log::Info("Call of initServices"); 60 | 61 | deviceStatusService = ServiceManager::GetService(); 62 | 63 | if(deviceStatusService != NULL) 64 | { 65 | Log::Info("Subscribed to Device Status service"); 66 | initialised = true; 67 | } 68 | else 69 | { 70 | Log::Error("Could not subscribe to Device Status service"); 71 | } 72 | } 73 | } 74 | 75 | // Callback function for the PLC state 76 | void plcOperationHandler(enum PlcOperation operation) 77 | { 78 | switch (operation) 79 | { 80 | case PlcOperation_Load: 81 | Log::Info("Call of PLC Load"); 82 | break; 83 | 84 | case PlcOperation_Setup: 85 | Log::Info("Call of PLC Setup"); 86 | break; 87 | 88 | case PlcOperation_StartCold: 89 | Log::Info("Call of PLC Start Cold"); 90 | processing = true; 91 | break; 92 | 93 | case PlcOperation_StartWarm: 94 | Log::Info("Call of PLC Start Warm"); 95 | // When this state-change occurs, the firmware is ready to serve requests. 96 | initServices(); 97 | processing = true; 98 | break; 99 | 100 | case PlcOperation_StartHot: 101 | Log::Info("Call of PLC Start Hot"); 102 | processing = true; 103 | break; 104 | 105 | case PlcOperation_Stop: 106 | Log::Info("Call of PLC Stop"); 107 | processing = false; 108 | break; 109 | 110 | case PlcOperation_Reset: 111 | Log::Info("Call of PLC Reset"); 112 | processing = false; 113 | break; 114 | 115 | case PlcOperation_Unload: 116 | Log::Info("Call of PLC Unload"); 117 | processing = false; 118 | break; 119 | 120 | case PlcOperation_None: 121 | Log::Info("Call of PLC None"); 122 | break; 123 | 124 | default: 125 | Log::Error("Call of unknown PLC state"); 126 | break; 127 | } 128 | } 129 | 130 | // Read data from a fieldbus input frame 131 | void readInputData(char* pValue, size_t valueSize) 132 | { 133 | TGdsBuffer* gdsBuffer = NULL; 134 | 135 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DI8", &gdsBuffer)) 136 | { 137 | size_t offset = 0; 138 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DI8", &offset)) 139 | { 140 | // Begin read operation, memory buffer will be locked 141 | char* readDataBufferPage; 142 | if(ArpPlcGds_BeginRead(gdsBuffer, &readDataBufferPage)) 143 | { 144 | // Copy data from GDS buffer 145 | char* dataAddress = readDataBufferPage + offset; 146 | memcpy(pValue, dataAddress, valueSize); 147 | 148 | // Unlock buffer 149 | if(!ArpPlcGds_EndRead(gdsBuffer)) 150 | { 151 | Log::Error("ArpPlcGds_EndRead failed"); 152 | } 153 | } 154 | else 155 | { 156 | Log::Error("ArpPlcGds_BeginRead failed"); 157 | ArpPlcGds_EndRead(gdsBuffer); 158 | } 159 | } 160 | else 161 | { 162 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 163 | } 164 | } 165 | else 166 | { 167 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 168 | } 169 | 170 | // Release the GDS buffer and free internal resources 171 | if(gdsBuffer != NULL) 172 | { 173 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 174 | { 175 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 176 | } 177 | } 178 | } 179 | 180 | // Write data to a fieldbus output frame 181 | void writeOutputData(const char* pValue, size_t valueSize) 182 | { 183 | TGdsBuffer* gdsBuffer = NULL; 184 | if(ArpPlcIo_GetBufferPtrByPortName("Arp.Io.AxlC", "Arp.Io.AxlC/0.~DO8", &gdsBuffer)) 185 | { 186 | size_t offset = 0; 187 | if(ArpPlcGds_GetVariableOffset(gdsBuffer, "Arp.Io.AxlC/0.~DO8", &offset)) 188 | { 189 | // Begin write operation, memory buffer will be locked 190 | char* dataBufferPage = NULL; 191 | if(ArpPlcGds_BeginWrite(gdsBuffer, &dataBufferPage)) 192 | { 193 | // Copy data to GDS buffer 194 | char* dataAddress = dataBufferPage + offset; 195 | memcpy(dataAddress, pValue, valueSize); 196 | 197 | // Unlock buffer 198 | if(!ArpPlcGds_EndWrite(gdsBuffer)) 199 | { 200 | Log::Error("ArpPlcGds_EndWrite failed"); 201 | } 202 | } 203 | else 204 | { 205 | Log::Error("ArpPlcGds_BeginWrite failed"); 206 | ArpPlcGds_EndWrite(gdsBuffer); 207 | } 208 | } 209 | else 210 | { 211 | Log::Error("ArpPlcGds_GetVariableOffset failed"); 212 | } 213 | } 214 | else 215 | { 216 | Log::Error("ArpPlcIo_GetBufferPtrByPortName failed"); 217 | } 218 | 219 | // Release the GDS buffer and free internal resources 220 | if(gdsBuffer != NULL) 221 | { 222 | if(!ArpPlcIo_ReleaseGdsBuffer(gdsBuffer)) 223 | { 224 | Log::Error("ArpPlcIo_ReleaseGdsBuffer failed"); 225 | } 226 | } 227 | } 228 | 229 | // This function will be executed on a real-time thread 230 | void* realTimeCycle(void* p) 231 | { 232 | using clock = std::chrono::steady_clock; 233 | 234 | // Set the cycle time 235 | const auto cycle_time = std::chrono::microseconds{100000}; 236 | auto next_cycle = clock::now() + cycle_time; 237 | 238 | // Declare a process data item 239 | uint8_t value = 0; 240 | 241 | while (true) 242 | { 243 | if (processing) 244 | { 245 | // Read process inputs 246 | readInputData((char*)&value, sizeof(value)); 247 | 248 | // Perform application-specific processing 249 | // In this case, simply invert the process data bits 250 | value = ~value; 251 | 252 | // Write process outputs 253 | writeOutputData((char*)&value, sizeof(value)); 254 | } 255 | 256 | // Wait for the next cycle 257 | std::this_thread::sleep_until(next_cycle); 258 | next_cycle += cycle_time; 259 | } 260 | } 261 | 262 | // This function creates a real-time thread and then 263 | // continues with non-real-time operations 264 | int main(int argc, char** argv) 265 | { 266 | // Variables required to create the real-time thread 267 | pthread_t realTimeThread; 268 | struct sched_param param; 269 | param.sched_priority = 80; 270 | pthread_attr_t attr; 271 | 272 | // Register the status update callback 273 | // This is important to get the status of the "firmware ready" event, "PlcOperation_StartWarm" 274 | ArpPlcDomain_SetHandler(plcOperationHandler); 275 | 276 | // Ask plcnext for access to its services 277 | // Use syslog for logging until the PLCnext logger is ready 278 | openlog ("runtime", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); 279 | 280 | // Process command line arguments 281 | string acfSettingsRelPath(""); 282 | 283 | if(argc != 2) 284 | { 285 | syslog (LOG_ERR, "Invalid command line arguments. Only relative path to the acf.settings file must be passed"); 286 | return -1; 287 | } 288 | else 289 | { 290 | acfSettingsRelPath = argv[1]; 291 | syslog(LOG_INFO, string("Arg Acf settings file path: " + acfSettingsRelPath).c_str()); 292 | } 293 | 294 | char szExePath[PATH_MAX]; 295 | ssize_t count = readlink("/proc/self/exe", szExePath, PATH_MAX); 296 | string strDirPath; 297 | if (count != -1) { 298 | strDirPath = dirname(szExePath); 299 | } 300 | string strSettingsFile(strDirPath); 301 | strSettingsFile += "/" + acfSettingsRelPath; 302 | syslog(LOG_INFO, string("Acf settings file path: " + strSettingsFile).c_str()); 303 | 304 | // Intialize PLCnext module application 305 | // Arguments: 306 | // arpBinaryDir: Path to Arp binaries 307 | // applicationName: Arbitrary Name of Application 308 | // acfSettingsPath: Path to *.acf.settings document to set application up 309 | if (ArpSystemModule_Load("/usr/lib", "runtime", strSettingsFile.c_str()) != 0) 310 | { 311 | syslog (LOG_ERR, "Could not load Arp System Module"); 312 | return -1; 313 | } 314 | syslog (LOG_INFO, "Loaded Arp System Module"); 315 | closelog(); 316 | 317 | // Create the real-time thread 318 | if(pthread_attr_init(&attr) == 0) 319 | { 320 | if(pthread_attr_setschedpolicy(&attr, SCHED_FIFO) == 0) 321 | { 322 | if(pthread_attr_setschedparam(&attr, ¶m) == 0) 323 | { 324 | if(pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED) == 0) 325 | { 326 | // This call will fail due to insufficient permissions, 327 | // if the process was not configured with the correct capabilities. 328 | // Make sure to set the capabilities on the executable 329 | // after *every* deployment to the target! 330 | if(pthread_create(&realTimeThread, &attr, realTimeCycle, NULL) != 0) 331 | { 332 | Log::Error("Error calling pthread_create (realtime thread)"); 333 | } 334 | } 335 | else 336 | { 337 | Log::Error("Error calling pthread_attr_setinheritsched"); 338 | } 339 | } 340 | else 341 | { 342 | Log::Error("Error calling pthread_attr_setschedparam"); 343 | } 344 | } 345 | else 346 | { 347 | Log::Error("Error calling pthread_attr_setschedpolicy"); 348 | } 349 | } 350 | else 351 | { 352 | Log::Error("Error calling pthread_attr_init"); 353 | } 354 | 355 | // Continue with non-real-time operations 356 | while (true) 357 | { 358 | if (initialised) 359 | { 360 | // Get the board temperature and write it to file 361 | rscBoardTemp = deviceStatusService->GetItem("Status.Board.Temperature.Centigrade"); 362 | rscBoardTemp.CopyTo(boardTemp); 363 | Log::Info("Current PLC board temperature is {0:d}°C", boardTemp); 364 | } 365 | 366 | // Wait a short time before repeating 367 | sleep(1); 368 | } 369 | } 370 | ``` 371 | 372 |
373 | 374 | Notes on the above code: 375 | - Real-time operations have been moved in to a separate function called `realTimeCycle`. 376 | - The real-time thread is created and configured in the `main` function, which then continues with non-real-time operations. 377 | - Attempting to set the thread priority will fail unless the executable has been configured with the correct capabilities (see steps below). 378 | - A real-time priority in the range 67 to 82 should be selected to avoid conflicts with the PLCnext runtime. This is the same range used by ESM tasks. 379 | - In this example, the real-time thread is executed with a fixed cycle time of 100 milliseconds, entirely independently of the cycle time of the main (non-real-time) thread. Note that it is currently recommended to exchange data with the Axioline GDS buffer no more than once per millisecond. 380 | - Neither thread safety nor thread synchronisation have been considered in this example. These are important issues that must be considered in real-world projects. 381 | 382 | 1. Build the project to generate the `Runtime` executable. 383 | 384 | ```bash 385 | plcncli build 386 | ``` 387 | 388 | 1. Deploy the executable to the PLC. 389 | 390 | ```bash 391 | scp bin/AXCF2152_22.0.4.144/Release/Runtime admin@192.168.1.10:~/projects/Runtime 392 | ``` 393 | 394 | Note: If you receive a "Text file busy" message in response to this command, then the file is probably locked by the PLCnext Control. In this case, stop the plcnext process on the PLC with the command `sudo /etc/init.d/plcnext stop` before copying the file. 395 | 396 | It is assumed that the ACF config and settings files (described in a previous article) are already on the PLC. 397 | 398 | 1. Open a secure shell session on the PLC: 399 | 400 | ```bash 401 | ssh admin@192.168.1.10 402 | ``` 403 | 404 | 1. Set the capabilities on the executable: 405 | 406 | ```bash 407 | sudo setcap cap_net_bind_service,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_sys_boot,cap_sys_nice,cap_sys_time+ep projects/Runtime/Runtime 408 | ``` 409 | 410 | This is required for the application to be able to set the real-time thread priority. If the admin user is not allowed to execute this command, then please ask your system administrator to grant the admin user this privilege, or else execute the command as a user with the required privilege (e.g. root). 411 | 412 | 1. Restart the plcnext process: 413 | 414 | ```bash 415 | sudo /etc/init.d/plcnext restart 416 | ``` 417 | 418 | 1. Run htop to check the priority of all processes. 419 | 420 | ```bash 421 | htop 422 | ``` 423 | 424 | You should see an instance of your application running with a priority of -81. 425 | 426 | --- 427 | 428 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 429 | 430 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 431 | -------------------------------------------------------------------------------- /getting-started/Part-07/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 7 - "Sample Runtime" application 4 | 5 | Sample Runtime is a complete C++ project that implements a PLCnext runtime application. It utilises all the features used in previous articles in this series, and introduces the following new features: 6 | 7 | 1. Licence Manager RSC service - used to protect against the use of the application on unauthorised devices. 8 | 9 | 1. Subscription RSC service - used to receive notifications each time there is a change in the value of a subscribed GDS variables. 10 | 11 | 1. Axioline status register - used to monitor the status of the Axioline bus. 12 | 13 | 1. Enhanced real-time task management, including monitoring cycle time over-runs. 14 | 15 | The source files for this application are located in the [src](https://github.com/PLCnext/SampleRuntime/tree/master/src) directory of this repository. 16 | 17 | Support files for this application - which were used in earlier articles in this series - are also available in this repository: 18 | - `.acf.config` and `.acf.settings` files are located in the [data](https://github.com/PLCnext/SampleRuntime/tree/master/data) folder. 19 | - The CMakeLists.txt file is located in the [root](https://github.com/PLCnext/SampleRuntime) of the repository. 20 | - The build script is located in the [tools](https://github.com/PLCnext/SampleRuntime/tree/master/tools) folder. 21 | 22 | ### Building and installing the application 23 | 24 | Once you you have completed all parts of the series up to this point, you can build the Sample Runtime application as follows: 25 | 26 | 1. In your project's `src` directory, delete the file `Runtime.cpp`. 27 | 28 | 1. Copy all the source files for the Sample Runtime project into your project's `src` directory. 29 | 30 | 1. Add a reference to the library `Arp.System.Lm.Services` in the CMakeLists.txt file. This provides the Licence Manager RSC service implementation. 31 | 32 | 1. Build the project to generate the `Runtime` executable. 33 | 34 | ```bash 35 | plcncli build 36 | ``` 37 | 38 | To run this example on a PLCnext Control device: 39 | 40 | 1. On a Windows 10 machine, download the PLCnext Engineer project from the [PLCnEngTestProject](https://github.com/PLCnext/SampleRuntime/tree/master/PLCnEngTestProject) directory. 41 | 42 | This project includes an IEC 61131 project with GDS ports that are used by the the Sample Runtime. 43 | 44 | 1. In PLCnext Engineer, open the project, set the IP address of the PLC, and adjust the Axioline hardware configuration if necessary. 45 | 46 | 1. Download the PLCnext Engineer project to the PLC. 47 | 48 | 1. Copy the `Runtime` executable to the PLC. 49 | 50 | ```bash 51 | scp bin/AXCF2152_22.0.4.144/Release/Runtime admin@192.168.1.10:~/projects/Runtime 52 | ``` 53 | 54 | Note: If you receive a "Text file busy" message in response to this command, then the file is probably locked by the PLCnext Control. In this case, stop the plcnext process on the PLC with the command `sudo /etc/init.d/plcnext stop` before copying the file. 55 | 56 | It is assumed that the ACF config and settings files are already on the PLC. 57 | 58 | 1. Open a secure shell session on the PLC: 59 | 60 | ```bash 61 | ssh admin@192.168.1.10 62 | ``` 63 | 64 | 1. Set the capabilities on the executable: 65 | 66 | ```bash 67 | sudo setcap cap_net_bind_service,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_sys_boot,cap_sys_nice,cap_sys_time+ep projects/Runtime/Runtime 68 | ``` 69 | 70 | If the admin user does not have the privilege to run this command, then you will need to grant this privilege. One way to do this is to log in as root and add a file to the directory `/etc/sudoers.d` containing this line: 71 | 72 | ```text 73 | admin ALL=(ALL) /usr/sbin/setcap 74 | ``` 75 | 76 | 1. Restart the plcnext process: 77 | 78 | ```bash 79 | sudo /etc/init.d/plcnext restart 80 | ``` 81 | 82 | 1. Check the contents of the application log file: 83 | 84 | ```bash 85 | cat /opt/plcnext/projects/Runtime/logs/Runtime.log 86 | ``` 87 | 88 | You should see messages containing I/O values being written to the log files 10 times each second. 89 | 90 | 1. Activate the fifth input on the digital input card. 91 | 92 | You should see the sixth digital output come on. 93 | 94 | The Sample Runtime application is now running. 95 | 96 | Note that there is a shell script in the [tools](https://github.com/PLCnext/SampleRuntime/tree/master/tools) folder called `start_debug.sh`, which can be used as a template for automating the deployment, configuration and startup of the runtime application during project development. 97 | 98 | --- 99 | 100 | ### Structure of the application 101 | 102 | The Sample Runtime application includes the following source files: 103 | 104 | | File | Contents | 105 | | --- | --- | 106 | | PLCnextSampleRuntime.cpp | The `main` entry point for the application | 107 | | CSampleRuntime.cpp / .h: | `CSampleRuntime` class | 108 | | CSampleSubscriptionThread.cpp / .h: | `CSampleSubscriptionThread` class | 109 | | CSampleRTThread.cpp / .h: | `CSampleRTThread` class | 110 | | Utility.h: | Common definitions | 111 | 112 | When the Sample Runtime application starts, the `main` function calls `ArpSystemModule_Load` and then creates a single instance of `CSampleRuntime`. 113 | 114 | The remaining sections describe the three classes used in this application. It is recommended that this be read alongside the corresponding source code. 115 | 116 | --- 117 | 118 | ### CSampleRuntime 119 | 120 | This class is responsible for starting worker threads and handling PLC state changes. It contains one `CSampleSubscriptionThread` object, and one `CSampleRTThread` object. 121 | 122 | When constructed, a CSampleRuntime object simply calls `ArpPlcDomain_SetHandler` to register the function named `PlcOperationHandler`. Subsequent operations are performed when the PLCnext Control calls `PlcOperationHandler` to signal a change of state: 123 | - **Start Warm**: On this event, `CSampleRuntime` performs some [initialisation](#initialisation), then tells the `CSampleSubscriptionThread` object and the `CSampleRTThread` object to start processing. 124 | 125 | - **Start Cold** or **Start Hot**: `CSampleRuntime` tells the `CSampleSubscriptionThread` object and the `CSampleRTThread` object to start processing. 126 | 127 | - **Stop**, **Reset** or **Unload**: `CSampleRuntime` tells the `CSampleSubscriptionThread` object and the `CSampleRTThread` object to stop processing. 128 | 129 | ### Initialisation 130 | 131 | When the PLC signals the "Start Warm" event, the following initialisation steps occur: 132 | - The `CSampleRuntime` object subscribes to a number of RSC services: 133 | - Device Info service. 134 | - Device Status service. 135 | - Licence Status service. 136 | 137 | - Data is read from the Device Info and Device Status services: 138 | - PLC vendor name 139 | - Load on one CPU core 140 | - Memory usage 141 | - PLC board temperature 142 | 143 | In this application, data is simply read during initialisation as a demonstration of how to use these services. 144 | 145 | - The Licence Status service is used to check that there is a valid licence for this application on this PLC. This mechanism can be used to protect against the use of the application on unauthorised devices. Currently, the only way to install an application licence on a PLC is by installing the application from the PLCnext Store. 146 | 147 | In this example, the application cannot be instaled from PLCnext Store (because it doesn't exist there!), and so no valid licence will be found on the PLC. This application simply ignores the result of the licence check, but a real-world application can (for example) shut down or continue to operate in a "restricted" mode if no valid licence is found. 148 | 149 | - The `CSampleRTThread` object is initialised. This involves the creation of two worker threads: 150 | - A real-time thread, which executes the `RTStaticCycle` member function. 151 | - A default priority thread, which executes the `StaticLoggingCycle` member function. 152 | 153 | These two member functions are described below. 154 | 155 | - The `CSampleSubscriptionThread` object is initialised. This involves the creation of a single worker thread that executes the `StaticCycle` member function. This member function is described below. 156 | 157 | --- 158 | 159 | ### CSampleRTThread 160 | 161 | During initialisation (in the `Init` member function), the `CSampleRTThread` object creates a real-time thread for executing time-critical operations. It also creates a non-real-time thread to execute "slow" operations that, if run on the real-time thread, would affect the performance of the time-critical parts of the application. 162 | 163 | When commanded to start processing (via the `StartProcessing` member function), the object: 164 | - Gets pointers to the Axioline Input and Output buffers in the Global Data Space. 165 | 166 | - Uses the `AddInput` and `AddOutput` functions to populate two `std::map`s with instances of the `RAWIO` struct. This struct, defined in `CSampleRTThread.h`, contains all the data required to access a single Axioline I/O point, including the offset to the I/O data in the GDS buffer. 167 | 168 | - Adds a special Axioline input variable called `AXIO_DIAG_STATUS_REG`. This variables contains the current status of the Axioline bus, and can be used for diagnostics and error detection, e.g. to detect when an Axioline module has failed. Details of how to interpret values for this variable are given in the document "UM EN AXL F SYS DIAG", available for download from the Phoenix Contact website. 169 | 170 | Note that, since the structure of the Global Data Space is fixed during the startup of the PLCnext runtime, information about the location of I/O in the Global Data Space only needs to be obtained once, rather than every scan cycle. This provides a significant efficiency improvement over the way that I/O reads and writes were handled in the example shown earlier in this series. 171 | 172 | Cyclic processing on the real-time thread is perfomed by the `RTStaticCycle` member function, which in turn calls the `RTCycle` member function. The main purpose of the `RTCycle` function is to schedule the start of the next scan cycle using the `clock_nanosleep` function. This provides a precise period for the processing of real-time operations. This function also checks for "real-time violations", i.e. any instances where the execution of the function takes longer than the specified cycle time. 173 | 174 | During each scan cycle, the `RTCycle` function also calls these three functions: 175 | - `ReadInputData` 176 | - `DoLogic` 177 | - `WriteOutputData` 178 | 179 | `DoLogic` is the core of the real-time application, where process-specific logic is implemented. In this case, some basic binary operations are performed on a few digital inputs and outputs. 180 | 181 | Cyclic processing on the non-real-time thread is performed by the `StaticLoggingCycle` member function, which in turn calls the `LoggingCycle` member function. This simply writes the current value of each I/O variable to the application log file, approximately every 100 milliseconds. 182 | 183 | --- 184 | 185 | ### CSampleSubscriptionThread 186 | 187 | This class demonstrates the use of the "Subscription" RSC service for reading GDS variables. This service is an alternative to the "Data Access" RSC service, which can be used to "poll" the values of GDS variables. The Subscripton service, on the other hand, will notify the RSC client when the value of any subscribed GDS variables changes, eliminating the need for polling. 188 | 189 | This application uses the Subscription service to read the value of three GDS variables. These variables have been declared in the PLCnext Engineer project that accompanies this example. 190 | 191 | The Subscription service differs from most other RSC services, in that it employs delegates (also known as callback functions) to send notifications to subscribers. 192 | 193 | In order to use the Subscription service: 194 | 195 | 1. Declare a shared pointer to store a service handle: 196 | 197 | ```cpp 198 | ISubscriptionService::Ptr m_pSubscriptionService; 199 | ``` 200 | 201 | 2. Get the handle for an ISubscriptionService method: 202 | 203 | ```cpp 204 | CSampleRuntime::Init() { 205 | m_pSubscriptionService = ServiceManager::GetService(); //get ISubscriptionService 206 | } 207 | ``` 208 | 209 | 3. Create a subscription for the port variables: 210 | 211 | ```cpp 212 | CreateSubscription() { 213 | m_uSubscriptionId = m_pSubscriptionService->CreateSubscription(SubscriptionKind::HighPerformance); 214 | m_pSubscriptionService->AddVariable(m_uSubscriptionId, GDSPort1) 215 | } 216 | ``` 217 | 218 | 4. Read and assign port variables: 219 | 220 | ```cpp 221 | CSampleRuntime::ReadSubscription() { 222 | RSCReadVariableValues(m_zSubscriptionValues) 223 | m_zSubscriptionValues[nCount].CopyTo(m_gdsPort1) 224 | } 225 | ``` 226 | 227 | 5. Delete the subscription: 228 | 229 | ```cpp 230 | CSampleRuntime::DeleteSubscription() { 231 | m_pSubscriptionService->DeleteSubscription(m_uSubscriptionId) 232 | } 233 | ``` 234 | 235 | --- 236 | 237 | ## How to get support 238 | 239 | You can get support in the forum of the [PLCnext Community](https://www.plcnext-community.net/forum/). 240 | 241 | --- 242 | 243 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 244 | 245 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 246 | -------------------------------------------------------------------------------- /getting-started/Part-08/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 8 - Configure Axioline I/O modules 4 | 5 | This example extends the example in [Part 5](../Part-05/README.md) of this series, which used the "Device Status" RSC service to write the PLC board temperature to the application log file. 6 | 7 | The "Acyclic Communication" RSC service can be used to read and write configuration information to/from individual Axioline I/O modules. For example, Axioline digital input modules have an adjustable "Filter time" parameter, and Axioline serial communication modules have a configurable protocol (RS-232, 422, 485), baud rate, etc. 8 | 9 | There are at least two other ways to write configuration data to Axioline I/O modules: 10 | - Use the module "Settings" page in PLCnext Engineer. 11 | - Connect the I/O module to an Axioline Bus Coupler, and use Startup+ software to write configuration data to the module. 12 | 13 | Configuration data is retained in the I/O module's solid-state memory, even after power is removed. 14 | 15 | ### Procedure 16 | 17 | This example shows how to read and write configuration information to the serial communication module "AXL F RS UNI 1H" (order number 2688666). 18 | 19 | Before using the Acyclic Communication service, please note the following: 20 | 21 | - **Do not** write configuration data to an Axioline I/O module more times than is necessary. For example, do not write configuration data on every execution of a cyclic program. Like all solid state memory, the memory in Axioline modules has the capacity for a finite number of read/write cycles. 22 | 23 | To configure an Axioline serial communication module from a runtime application: 24 | 25 | 1. Connect a "AXL F RS UNI 1H" module to the PLCnext Control. In this example, this module must be the first one (i.e. left-most) on the Axioline bus. 26 | 27 | 1. In the PLCnext Engineer project (prepared in [Part 3](../Part-03/README.md) of this series), replace all the Axioline I/O modules with a single "AXL F RS UNI 1H" module, and download the project to the PLC. 28 | 29 | 1. Modify the file `Runtime.cpp` so it looks like the following: 30 |
31 | (click to see/hide code) 32 | 33 | ```cpp 34 | // 35 | // Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 36 | // Licensed under the MIT. See LICENSE file in the project root for full license information. 37 | // SPDX-License-Identifier: MIT 38 | // 39 | #include "Arp/System/ModuleLib/Module.h" 40 | #include "Arp/System/Commons/Logging.h" 41 | #include "Arp/Plc/AnsiC/Domain/PlcOperationHandler.h" 42 | #include "Arp/System/Rsc/ServiceManager.hpp" 43 | #include "Arp/Io/Axioline/Services/IAcyclicCommunicationService.hpp" 44 | #include "Arp/Io/Axioline/Services/PdiParam.hpp" 45 | #include "Arp/Io/Axioline/Services/PdiResult.hpp" 46 | 47 | #include 48 | #include 49 | 50 | using namespace Arp; 51 | using namespace Arp::System::Rsc; 52 | using namespace Arp::Io::Axioline::Services; 53 | using namespace Arp::System::Commons::Diagnostics::Logging; 54 | 55 | bool initialised = false; // The RSC service is available 56 | bool configured = false; // The serial module is configured 57 | bool processing = false; // Axioline I/O is available 58 | 59 | IAcyclicCommunicationService::Ptr acyclicCommunicationService; // Reference to the RSC service 60 | PdiParam pdiParam; // PDI parameters 61 | vector writeData(16); // Data written to the serial module 62 | vector readData; // Data read from the serial module 63 | PdiResult transferResult; // Result of the PDI data transfer 64 | 65 | // Configure serial module 66 | void configSerial() 67 | { 68 | Log::Info("Call of configSerial"); 69 | 70 | if(acyclicCommunicationService != NULL) 71 | { 72 | // The basic communication settings - baud rate, data bits, parity and 73 | // stop bits - are written to an AXL F RS UNI 1H module installed on the local Axioline bus. 74 | 75 | // Module access data. 76 | pdiParam.Slot = 1; // Serial module is the first module on the Axioline bus 77 | pdiParam.Subslot = 0; 78 | pdiParam.Index = 0x0080; // Parameter table 79 | pdiParam.Subindex = 0; 80 | 81 | // Serial module configuration data. 82 | // For the meaning of configuration codes, refer to data sheet 8533_en_04 "AXL F RS UNI 1H", pp 26-30. 83 | writeData[0] = 0x40; // DTR control via Process Data; RS-232 interface; Transparent protocol. 84 | writeData[1] = 0x74; // Baud rate (9600), Data bits (8), Parity (None) and Stop bits (1). 85 | writeData[2] = 0x0D; // First delimiter = CR. 86 | writeData[3] = 0x0A; // Second delimiter = LF. 87 | writeData[4] = 0x24; // If a character is received with an error (e.g. parity error), the character '$' is stored. 88 | writeData[5] = 0x00; // Not used. 89 | writeData[6] = 0x00; // Not used. 90 | writeData[7] = 0x00; // Not used. 91 | writeData[8] = 0x00; // Reserved. 92 | writeData[9] = 0x00; // Data exchange via Process Data. 93 | writeData[10] = 0x00; // Lead time. 94 | writeData[11] = 0x00; // Lag time. 95 | writeData[12] = 0x00; // Reserved. 96 | writeData[13] = 0x00; // Reserved. 97 | writeData[14] = 0x00; // Reserved. 98 | writeData[15] = 0x00; // Reserved. 99 | 100 | // Write configuration data to the serial module 101 | transferResult = acyclicCommunicationService->PdiWrite(pdiParam, writeData); 102 | 103 | if (transferResult.ErrorCode == 0) 104 | { 105 | Log::Info("Successfully configured serial module"); 106 | configured = true; 107 | 108 | // Read configuration data back from the module 109 | transferResult = acyclicCommunicationService->PdiRead(pdiParam, readData); 110 | 111 | if (transferResult.ErrorCode == 0) 112 | { 113 | if (readData.size() != 16) 114 | { 115 | Log::Error("Unexpected response from serial module. Expected 16 bytes, got {0}", readData.size()); 116 | } 117 | else 118 | { 119 | Log::Info("Configuration: 0x{0:04X}, 0x{1:04X}, 0x{2:04X}, 0x{3:04X}, ...", readData[0], readData[1], readData[2], readData[3]); 120 | } 121 | } 122 | else 123 | { 124 | // For the meaning of error codes, refer to user manual 8663_en_03 "AXL F SYS DIAG", Table 2-3. 125 | Log::Error("Could not read configuration from serial module. Error code: 0x{0:04X} Additional info: 0x{1:04X}", transferResult.ErrorCode, transferResult.AddInfo); 126 | configured = false; 127 | } 128 | } 129 | else 130 | { 131 | // For the meaning of error codes, refer to user manual 8663_en_03 "AXL F SYS DIAG", Table 2-3. 132 | Log::Error("Could not configure serial module. Error code: 0x{0:04X} Additional info: 0x{1:04X}", transferResult.ErrorCode, transferResult.AddInfo); 133 | configured = false; 134 | } 135 | } 136 | else 137 | { 138 | Log::Error("Could not configure serial module - RSC service not available"); 139 | configured = false; 140 | } 141 | } 142 | 143 | // Initialise PLCnext RSC services 144 | void initServices() 145 | { 146 | if(!initialised) 147 | { 148 | Log::Info("Call of initServices"); 149 | 150 | acyclicCommunicationService = ServiceManager::GetService(); 151 | 152 | if(acyclicCommunicationService != NULL) 153 | { 154 | Log::Info("Subscribed to Acyclic Communication service"); 155 | initialised = true; 156 | } 157 | else 158 | { 159 | Log::Error("Could not subscribe to Acyclic Communication service"); 160 | } 161 | 162 | configSerial(); 163 | } 164 | } 165 | 166 | // Callback function for the PLC state 167 | void plcOperationHandler(enum PlcOperation operation) 168 | { 169 | switch (operation) 170 | { 171 | case PlcOperation_Load: 172 | Log::Info("Call of PLC Load"); 173 | break; 174 | 175 | case PlcOperation_Setup: 176 | Log::Info("Call of PLC Setup"); 177 | break; 178 | 179 | case PlcOperation_StartCold: 180 | Log::Info("Call of PLC Start Cold"); 181 | processing = true; 182 | break; 183 | 184 | case PlcOperation_StartWarm: 185 | Log::Info("Call of PLC Start Warm"); 186 | // When this state-change occurs, the firmware is ready to serve requests. 187 | initServices(); 188 | processing = true; 189 | break; 190 | 191 | case PlcOperation_StartHot: 192 | Log::Info("Call of PLC Start Hot"); 193 | processing = true; 194 | break; 195 | 196 | case PlcOperation_Stop: 197 | Log::Info("Call of PLC Stop"); 198 | processing = false; 199 | break; 200 | 201 | case PlcOperation_Reset: 202 | Log::Info("Call of PLC Reset"); 203 | processing = false; 204 | break; 205 | 206 | case PlcOperation_Unload: 207 | Log::Info("Call of PLC Unload"); 208 | processing = false; 209 | break; 210 | 211 | case PlcOperation_None: 212 | Log::Info("Call of PLC None"); 213 | break; 214 | 215 | default: 216 | Log::Error("Call of unknown PLC state"); 217 | break; 218 | } 219 | } 220 | 221 | int main(int argc, char** argv) 222 | { 223 | // Register the status update callback 224 | // This is important to get the status of the "firmware ready" event, "PlcOperation_StartWarm" 225 | ArpPlcDomain_SetHandler(plcOperationHandler); 226 | 227 | // Ask plcnext for access to its services 228 | // Use syslog for logging until the PLCnext logger is ready 229 | openlog ("runtime", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); 230 | 231 | // Process command line arguments 232 | string acfSettingsRelPath(""); 233 | 234 | if(argc != 2) 235 | { 236 | syslog (LOG_ERR, "Invalid command line arguments. Only relative path to the acf.settings file must be passed"); 237 | return -1; 238 | } 239 | else 240 | { 241 | acfSettingsRelPath = argv[1]; 242 | syslog(LOG_INFO, string("Arg Acf settings file path: " + acfSettingsRelPath).c_str()); 243 | } 244 | 245 | char szExePath[PATH_MAX]; 246 | ssize_t count = readlink("/proc/self/exe", szExePath, PATH_MAX); 247 | string strDirPath; 248 | if (count != -1) { 249 | strDirPath = dirname(szExePath); 250 | } 251 | string strSettingsFile(strDirPath); 252 | strSettingsFile += "/" + acfSettingsRelPath; 253 | syslog(LOG_INFO, string("Acf settings file path: " + strSettingsFile).c_str()); 254 | 255 | // Intialize PLCnext module application 256 | // Arguments: 257 | // arpBinaryDir: Path to Arp binaries 258 | // applicationName: Arbitrary Name of Application 259 | // acfSettingsPath: Path to *.acf.settings document to set application up 260 | if (ArpSystemModule_Load("/usr/lib", "runtime", strSettingsFile.c_str()) != 0) 261 | { 262 | syslog (LOG_ERR, "Could not load Arp System Module"); 263 | return -1; 264 | } 265 | syslog (LOG_INFO, "Loaded Arp System Module"); 266 | closelog(); 267 | 268 | // Cyclic processing 269 | while (true) 270 | { 271 | if (processing) 272 | { 273 | // Exchange data with serial module here 274 | } 275 | // Wait a short time before repeating 276 | sleep(1); 277 | } 278 | } 279 | ``` 280 | 281 |
282 | 283 | Notes on the above code: 284 | - This example uses the blocking functions `PdiRead` and `PdiWrite`. It is not recommended to call these functions from a high-frequency cyclic task (even once!), otherwise the required cycle time is likely to be exceeded. 285 | - In this example, after the serial communication module is configured, no process data is exchanged with the module (this is a necessary requirement for a practical serial communication application). Process data exchange would typically take place an endless loop, for example in the `while(true)` loop in the `main` function. 286 | 287 | 1. Build the project to generate the `Runtime` executable. 288 | 289 | ```bash 290 | plcncli build 291 | ``` 292 | 293 | 1. Deploy the executable to the PLC. 294 | 295 | ```bash 296 | scp bin/AXCF2152_22.0.4.144/Release/Runtime admin@192.168.1.10:~/projects/Runtime 297 | ``` 298 | 299 | Note: If you receive a "Text file busy" message in response to this command, then the file is probably locked by the PLCnext Control. In this case, stop the plcnext process on the PLC with the command `sudo /etc/init.d/plcnext stop` before copying the file. 300 | 301 | It is assumed that the ACF config and settings files (described in a previous article) are already on the PLC. 302 | 303 | 1. Open a secure shell session on the PLC: 304 | 305 | ```bash 306 | ssh admin@192.168.1.10 307 | ``` 308 | 309 | 1. Restart the plcnext process: 310 | 311 | ```bash 312 | sudo /etc/init.d/plcnext restart 313 | ``` 314 | 315 | 1. Check the log file. You should see messages similar to the following, indicating successful writing and reading of configuration data: 316 | 317 | ```text 318 | 23.07.19 06:02:12.466 root INFO - Subscribed to Acyclic Communication service 319 | 23.07.19 06:02:12.481 root INFO - Successfully configured serial module 320 | 23.07.19 06:02:12.494 root INFO - Configuration: 0x0040, 0x0074, 0x000D, 0x000A, ... 321 | ``` 322 | 323 | 1. On the serial communication module, the orange LED labelled "33" should be illuminated, indicating that the module is configured for RS-232 communication. 324 | 325 | --- 326 | 327 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 328 | 329 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 330 | -------------------------------------------------------------------------------- /getting-started/Part-09/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. 2 | 3 | ## Part 9 - Disable unnecessary system services 4 | 5 | There are many services enabled by default on a PLCnext Control, however a particular runtime application may only use a small number of these services. For example, both Profinet and Ethernet/IP™ fieldbus services are enabled by default on an AXC F 2152 and, while it is not necessary to disable them, it does save some system resources if these services are not started. 6 | 7 | This procedure shows how to disable any or all of the following four services on a PLCnext Control: 8 | 9 | | Service | Required for | 10 | | ------------ | ----------------------------------------------------- | 11 | | Profinet | Communication to Profinet controllers and/or devices. | 12 | | Ethernet/IP™ | Communication to Ethernet/IP™ scanners. | 13 | | HMI | Browser-based HMI hosted from the PLCnext Control | 14 | | OPC-UA | Communication with OPC-UA clients | 15 | 16 | --- 17 | 18 | **IMPORTANT NOTE** 19 | 20 | Disabling services on a PLCnext Control is done at the users own risk. 21 | 22 | Phoenix Contact performs all system tests with the default configuration. Any changes to this configuration may adversely affect the stability of the system. 23 | 24 | Users should always perform complete and thorough tests on their PLCnext Control application to ensure the required performance level. 25 | 26 | --- 27 | 28 | ### Procedure 29 | 30 | The [procedure for disabling system services](https://www.plcnext.help/te/WBM/Configuration_System_Services.htm) is given in the PLCnext Info Center. 31 | 32 | --- 33 | 34 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 35 | 36 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 37 | -------------------------------------------------------------------------------- /getting-started/Part-10/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. Each article builds on tasks that were completed in earlier articles, so it is recommended to follow the series in sequence. 2 | 3 | ## Part 10 - OPC UA 4 | 5 | OPC UA is the industry standard for exchanging non-real-time data between industrial automation software components. It would therefore be useful if data in a PLCnext runtime application could be accessed via an OPC UA server. 6 | 7 | An OPC UA server for a custom runtime application could be implemented in a number of ways. However, since there is already a full-featured OPC UA server included with every PLCnext Control, it makes sense to use this OPC UA server for applications running on the same PLC. 8 | 9 | This article provides a procedure for the following two tasks: 10 | 11 | 1. Creating custom data items in the OPC UA server address space. 12 | 13 | 1. Exchanging data between the runtime and custom data items in the OPC UA server address space. 14 | 15 | ### Technical Background 16 | 17 | On a PLCnext Control, the OPC UA server address space is populated by ACF and PLM components and programs. Since the OPC UA server address space cannot be populated directly from a runtime application, we must create a separate shared object library containing an ACF component that defines data items from our runtime application that should appear in the OPC UA server address space. We must then instruct the PLCnext runtime to create the ACF component at startup. 18 | 19 | An ACF component that populates the OPC UA address space also creates a GDS port variable for each OPC UA data item that it creates. Each of these GDS port variables is linked to its associated OPC UA data item, so applications that can read and write GDS variables can use these variables to (indirectly) read and write the corresponding OPC UA data items. 20 | 21 | The PLCnext Command-Line Interface (CLI) includes a template for an ACF project, so we will use this as the starting point in the creation of an ACF component that we will use to create OPC UA data items and their associated GDS variables. 22 | 23 | ### Procedure 24 | 25 | 1. Using the PLCnext CLI, create a new project based on the standard template and set the target for the build. In this example, the project is called `RuntimeOpc`, but you can call it whatever you want. 26 | 27 | ``` 28 | plcncli new acfproject -n RuntimeOpc 29 | cd RuntimeOpc 30 | plcncli set target --add -n AXCF2152 -v 2022.0 31 | ``` 32 | 33 | 1. Open the Component .hpp file in your favourite editor, and add the following GDS Port definition in the section indicated by the auto-generated comment: 34 | 35 | ```cpp 36 | //#attributes(Hidden|Opc) 37 | struct DATA { 38 | //#attributes(Input) 39 | int32 Runtime_RW = 0; 40 | //#attributes(Output) 41 | int32 Runtime_RO = 0; 42 | }; 43 | 44 | //#port 45 | DATA data; 46 | ``` 47 | 48 | This will create two OPC UA data items; one read/write (Input) and one read-only (Output), each holding a 32 bit integer value. Corresponding GDS variables will also be created using this information. 49 | 50 | This `struct` can include any number of elements. The data type of each element must be taken from the C++ column of the ["Available data types"](https://www.plcnext.help/te/PLCnext_Runtime/Available_data_types.htm) table in the PLCnext Info Center. 51 | 52 | 1. Auto-generate the remaining C++ source code for the project. From the root directory of the RuntimeOpc project: 53 | 54 | ``` 55 | plcncli generate code 56 | ``` 57 | 58 | 1. Build the project. From the root directory of the project: 59 | 60 | ``` 61 | plcncli build 62 | ``` 63 | 64 | 1. In the project root directory, create a new directory called `data`. 65 | 66 | 1. In the project `.acf.config` file, replace the Library and Component entries with the following: 67 | 68 | ```xml 69 | 70 | ``` 71 | 72 | ```xml 73 | 74 | ``` 75 | 76 | This instructs the PLCnext runtime to create one instance of the RuntimeOpcComponent component, called Runtime, from the RuntimeOpc shared object library that we have just built. 77 | 78 | 1. Download the application to the PLC. From the project root directory copy the following files: 79 | 80 | - Shared object library containing the ACF component. 81 | 82 | ``` 83 | ssh admin@192.168.1.10 'mkdir -p projects/RuntimeOpc' 84 | scp bin/AXCF2152_22.0.3.129/Release/lib/libRuntimeOpc.so admin@192.168.1.10:~/projects/RuntimeOpc 85 | ``` 86 | 87 | - ACF configuration file. 88 | 89 | ``` 90 | scp src/RuntimeOpcLibrary.acf.config admin@192.168.1.10:~/projects/Default 91 | ``` 92 | 93 | 1. On the PLC, edit the file `/opt/plcnext/projects/PCWE/Services/OpcUA/PCWE.opcua.config`. In the `` section, change the value `` to ``. That section of the file should then look like: 94 | 95 | ```xml 96 | 97 | 98 | 99 | ``` 100 | 101 | 1. Restart the PLCnext runtime: 102 | 103 | ``` 104 | sudo /etc/init.d/plcnext restart 105 | ``` 106 | 107 | 1. In an OPC UA client like UaExpert from Unified Automation: 108 | - In the Address Space pane, open the branch PLCnext -> Runtime 109 | - Add the two new data items to the Data Access View pane. 110 | - Attempt to change the value of both data items. It is not possible to change the value of the Output (read-only) data item. 111 | 112 | These OPC UA variables can now be used by the runtime application. 113 | 114 | ### Next steps 115 | 116 | GDS variables corresponding to the new OPC UA data items have been created and are available to any application that can access the Global Data Space. The runtime application can exchange data with these GDS variables using the "Data Access" and/or "Subscription" RSC services. 117 | 118 | --- 119 | 120 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 121 | 122 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 123 | -------------------------------------------------------------------------------- /getting-started/Part-99/README.md: -------------------------------------------------------------------------------- 1 | This is part of a [series of articles](https://github.com/PLCnext/SampleRuntime) that demonstrate how to create a runtime application for PLCnext Control. 2 | 3 | ## Explore unlimited possibilites ... 4 | 5 | This article contains some ideas for additional features that could be added to a PLCnext runtime application. Some of these can be implemented using patterns demonstrated in this series of articles. Others will be the subject of future articles in the PLCnext Community. 6 | 7 | ### 1. Use Profinet I/O 8 | 9 | It is possible to communicate with Profinet I/O devices in a similar way to the Axioline I/O used in this application. 10 | 11 | ### 2. Configure and start the Axioline bus from the application 12 | 13 | The "Axioline Master" RSC service can be used to configure any arrangement of Axioline I/O modules directly from a runtime application, without the use of PLCnext Engineer. This is demonstrated in the [BusConductor](https://github.com/PLCnext/BusConductor) application, and the technique used by that app could easily be applied here. 14 | 15 | ### 3. Control the PLC state 16 | 17 | This series has shown how to react to changes in the PLC state using a callback from the PLCnext ANSI-C interface. 18 | 19 | The ANSI-C interface also provides functions to start, stop and reset the PLC if necessary. 20 | 21 | For non-real-time applications, the "PLC Manager" service provides similar control functions through an RSC service. 22 | 23 | ### 4. Send and receive notifications from other processes 24 | 25 | The "Notification Manager" RSC service can be used to send notifications to subscribers, and to receive notifications from other processes. 26 | 27 | For example, the runtime application could use the Notification Manager to receive notifications about changes of PLC state (running, stopped, etc). 28 | 29 | ### 5. Read and Write GDS variables 30 | 31 | The "Data Access" RSC service can be used to read and write GDS variables that are defined by other PLCnext components. This provides a simple, reliable and consistent interprocess data transfer mechanism. 32 | 33 | ### 6. Connect to MQTT brokers 34 | 35 | The "MQTT Client" RSC service is not included on the PLCnext Control by default, but it can be installed if required. The MQTT Client service can be used to publish and subscribe to topics on local or remote MQTT brokers. 36 | 37 | ### 7. Use other languages to develop PLCnext runtime applications 38 | 39 | This examples in this series were written entirely in C++, however it is possible to write similar applications in most modern languages that have a compiler suitable for your selected PLCnext Control (e.g. ARMv7 hard float). 40 | 41 | There are various ways to interface applications written in other languages with services on the PLCnext Control: 42 | - The application can be built as a shared-object library with an ANSI-C interface. These C-style functions can then be called from a custom ACF component written in C++. 43 | - If you do not want to write any C++ code at all, then your application will need to be able to call C-style functions itself. It can then access Axioline I/O (and the PLC status) through the ANSI-C libraries on the PLCnext Control. 44 | - If the language is also able to call C++ functions, then the application can get access to the complete set of RSC services on the PLCnext Control. 45 | 46 | ### 8. Package as a PLCnext Store app 47 | 48 | There is a category of PLCnext Store app called "Runtime", which is intended for runtime applications just like the one demonstrated in this series. Check out the [runtime applications that are already in the store](https://www.plcnextstore.com/eu/search?type=2). 49 | 50 | An example of a [runtime app for the PLCnext Store](https://github.com/PLCnext/PLCnextAppExamples/tree/public/DemoApps/DemoPlcnextExtensionsApps/PlcnextSampleRuntimeApp) is shown in the [PLCnextAppExamples](https://github.com/PLCnext/PLCnextAppExamples) Github repository. 51 | 52 | --- 53 | 54 | ## Contact us 55 | If you would like help with your idea for a PLCnext Control application, or if you just want to share your ideas or solutions with others, please get in touch on the [PLCnext Community forum](https://www.plcnext-community.net/forum/). 56 | 57 | --- 58 | 59 | Copyright © 2020-2022 Phoenix Contact Electronics GmbH 60 | 61 | All rights reserved. This program and the accompanying materials are made available under the terms of the [MIT License](http://opensource.org/licenses/MIT) which accompanies this distribution. 62 | -------------------------------------------------------------------------------- /src/CSampleRTThread.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PLCnext/SampleRuntime/c2477920ed847cb458c8d926e0f8cb517a4876c4/src/CSampleRTThread.cpp -------------------------------------------------------------------------------- /src/CSampleRTThread.h: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | * Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | * 6 | * CSampleRTThread.cpp 7 | * 8 | * Created on: Jul 12, 2019 9 | * Author: Steffen Schlette 10 | * 11 | ******************************************************************************/ 12 | 13 | #ifndef CSAMPLERTTHREAD_H_ 14 | #define CSAMPLERTTHREAD_H_ 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #include "Arp/System/Core/Arp.h" 21 | #include "Arp/System/Commons/Logging.h" 22 | #include "Arp/Plc/AnsiC/Gds/DataLayout.h" 23 | #include "Arp/Plc/AnsiC/Io/FbIoSystem.h" 24 | #include "Arp/Plc/AnsiC/Io/Axio.h" 25 | #include "Utility.h" 26 | 27 | using namespace Arp; 28 | using namespace std; 29 | 30 | // time calculation helpers 31 | void timeAdd(struct timespec& zValue, long lAdd); 32 | int timeCmp(struct timespec& zFirst, struct timespec& zSecond); 33 | 34 | /// structure to handle a metadata and value of a single I/O 35 | struct RAWIO 36 | { 37 | // definition of I/O 38 | string strID; // ID of I/O 39 | size_t nOffset = 0; // offset in bus-frame in byte 40 | unsigned char ucBitMask = 0; // bitmask in case of a boolean value 41 | bool bIsBool = false; // true, if it is a boolean 42 | 43 | // current value 44 | unsigned char* pValue = NULL; // pointer to data, if it is no boolean 45 | size_t zSize = 0; // data size in bytes 46 | bool bValue = false; // value if it is a boolean 47 | }; 48 | 49 | class CSampleRTThread 50 | { 51 | public: 52 | CSampleRTThread(); 53 | virtual ~CSampleRTThread(); 54 | 55 | bool Init(); 56 | static void* RTStaticCycle(void* p); 57 | void RTCycle(); 58 | static void* StaticLoggingCycle(void* p); 59 | void LoggingCycle(); 60 | 61 | bool StartProcessing(); 62 | bool StopProcessing(); 63 | 64 | private: 65 | // workerthread for cycle 66 | pthread_t m_zRTCycleThread; 67 | pthread_t m_zLoggingThread; // log status of I/Os in extra thread 68 | 69 | bool m_bInitialized; // class already initialized? 70 | bool m_bDoCycle; // shall the subscription cycle run? 71 | bool m_bFirstRTCycle; // is it the first cycle? 72 | 73 | // GDS buffers for raw I/O access 74 | TGdsBuffer* m_pGdsInBuffer; 75 | TGdsBuffer* m_pGdsOutBuffer; 76 | TGdsBuffer* m_pGdsAxioDiagBuffer; 77 | 78 | // some sample I/O IDs 79 | String m_strInByte; 80 | String m_strIn04; 81 | String m_strIn05; 82 | String m_strIn06; 83 | String m_strIn07; 84 | String m_strOut04; 85 | String m_strOut05; 86 | String m_strOut06; 87 | String m_strOut07; 88 | 89 | // maps with ID of I/Os for easy access 90 | std::map m_zInputsMap; 91 | std::map m_zOutputsMap; 92 | std::map m_zAxioDiagVarsMap; 93 | 94 | void LogIO(RAWIO& zRawIO); 95 | bool AddInput(std::string strID, size_t zSize, bool bIsBool); 96 | bool AddOutput(std::string strID, size_t zSize, bool bIsBool); 97 | bool AddAxioDiagVar(std::string strID, size_t zSize, bool bIsBool); 98 | 99 | // example usage of direct access to fieldbus-frame 100 | bool ReadInputData(void); 101 | bool ReadAxioDiagVars(void); 102 | bool DoLogic(void); 103 | bool WriteOutputData(void); 104 | 105 | bool ReadValue(const char* pFrame, RAWIO& zIO); 106 | bool WriteValue(char* pFrame, RAWIO& zIO); 107 | }; 108 | 109 | #endif /* CSAMPLERTTHREAD_H_ */ 110 | -------------------------------------------------------------------------------- /src/CSampleRuntime.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | * Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | * 6 | * CSampleRuntime.cpp 7 | * 8 | * Created on: Jan 3, 2019 9 | * Author: Steffen Schlette 10 | * 11 | ******************************************************************************/ 12 | 13 | #include "CSampleRuntime.h" 14 | 15 | #include "Arp/System/Rsc/ServiceManager.hpp" 16 | #include "Arp/Plc/AnsiC/ArpPlc.h" 17 | #include "Arp/Plc/AnsiC/Device/Device.h" 18 | #include "Arp/Plc/AnsiC/Domain/PlcManager.h" 19 | 20 | using namespace Arp::System::Rsc; 21 | 22 | PlcOperation CSampleRuntime::m_zPLCMode = PlcOperation_None; 23 | 24 | CSampleRuntime::CSampleRuntime() 25 | : m_bInitialized(false), 26 | m_szVendorName(NULL), 27 | m_byCpuLoad(0), 28 | m_byMemoryUsage(0), 29 | m_i8BoardTemp(0) 30 | { 31 | // announce the status-update callback 32 | // this is important to get the status of the "firmware-ready"-event PlcOperation_StartWarm 33 | ArpPlcDomain_SetHandler(PlcOperationHandler); 34 | } 35 | 36 | CSampleRuntime::~CSampleRuntime() 37 | { 38 | } 39 | 40 | /// @brief: init/connect to the PLCnext services and start the cycle-threads 41 | /// @return true: success, false: failure 42 | bool CSampleRuntime::Init() 43 | { 44 | if(m_bInitialized) 45 | { 46 | return(true); 47 | } 48 | 49 | Log::Info("Call of CSampleRuntime::Init"); 50 | 51 | bool bRet = false; 52 | 53 | // the firmware needs to be in the state PlcOperation_StartWarm before we can acquire services 54 | m_pDeviceInfoService = ServiceManager::GetService(); 55 | m_pDeviceStatusService = ServiceManager::GetService(); 56 | m_pLicenseStatusService = ServiceManager::GetService(); 57 | 58 | if((m_pDeviceStatusService != NULL) && 59 | (m_pDeviceInfoService != NULL) && 60 | (m_pLicenseStatusService != NULL) 61 | ) 62 | { 63 | if(GetDeviceStatus() == true) 64 | { 65 | // check current license 66 | unsigned int uFirmCode = 0; // you get FirmCode+ProductCode after creating a new app in the PLCnext store 67 | unsigned int uProductCode = 0; // hardcode these numbers in your application 68 | unsigned int uFeatureCode = 0; // FeatureCode is not yet supported 69 | 70 | if(m_pLicenseStatusService->GetLicenseStatus(uFirmCode, uProductCode, uFeatureCode) == true) 71 | { 72 | // valid license on the device 73 | } 74 | else 75 | { 76 | // no valid license on the device -> switch to demo mode, do not just quit the app! 77 | } 78 | 79 | if(m_zRTThread.Init() == true) 80 | { 81 | if(m_zSubscriptionThread.Init() == true) 82 | { 83 | m_bInitialized = true; 84 | bRet = true; 85 | } 86 | } 87 | } 88 | } 89 | 90 | return(bRet); 91 | } 92 | 93 | /// @brief start processing of I/O data in the different threads 94 | /// e.g. after a new PLCnext Engineer Program was loaded 95 | /// @return true: success, false: failure 96 | bool CSampleRuntime::StartProcessing() 97 | { 98 | Log::Info("Start processing"); 99 | 100 | bool bRet = false; 101 | if(m_zRTThread.StartProcessing() == true) 102 | { 103 | if(m_zSubscriptionThread.StartProcessing() == true) 104 | { 105 | bRet = true; 106 | } 107 | } 108 | 109 | return(bRet); 110 | } 111 | 112 | /// @brief stop processing of I/O data in the different threads 113 | /// e.g. if a new PLCnext Engineer Program should be loaded 114 | /// @return true: success, false: failure 115 | bool CSampleRuntime::StopProcessing() 116 | { 117 | Log::Info("Stop processing"); 118 | 119 | bool bRet = false; 120 | if(m_zRTThread.StopProcessing() == true) 121 | { 122 | if(m_zSubscriptionThread.StopProcessing() == true) 123 | { 124 | bRet = true; 125 | } 126 | } 127 | 128 | return(bRet); 129 | } 130 | 131 | /// @brief: retrieve some information from the PLCnext runtime system. 132 | /// @return true: success, false: failure 133 | bool CSampleRuntime::GetDeviceStatus() 134 | { 135 | bool bRet = false; 136 | 137 | // get some values from device info service (static data) 138 | m_rscVendorName = m_pDeviceInfoService->GetItem("General.VendorName"); 139 | m_szVendorName = m_rscVendorName.GetChars(); 140 | 141 | // get some values from device status service (dynamic data) 142 | m_rscCpuLoad = m_pDeviceStatusService->GetItem("Status.Cpu.0.Load.Percent"); 143 | m_rscCpuLoad.CopyTo(m_byCpuLoad); 144 | m_rscBemoryUsage = m_pDeviceStatusService->GetItem("Status.Memory.Usage.Percent"); 145 | m_rscBemoryUsage.CopyTo(m_byMemoryUsage); 146 | m_rscBoardTemp = m_pDeviceStatusService->GetItem("Status.Board.Temperature.Centigrade"); 147 | m_rscBoardTemp.CopyTo(m_i8BoardTemp); 148 | 149 | bRet = true; 150 | 151 | return(bRet); 152 | } 153 | 154 | /// @brief callback function for the PLC state 155 | /// @param operation current mode of operation 156 | void PlcOperationHandler(enum PlcOperation operation) 157 | { 158 | CSampleRuntime::m_zPLCMode = operation; 159 | 160 | switch (operation) 161 | { 162 | break; 163 | case PlcOperation_Load: 164 | Log::Info("Call of PLC Load"); 165 | break; 166 | case PlcOperation_Setup: 167 | Log::Info("Call of PLC Setup"); 168 | break; 169 | case PlcOperation_StartCold: 170 | Log::Info("Call of PLC Start Cold"); 171 | // when this state-change occurred, the PLCnext runtime is ready to serve requests. 172 | // Now we can request the needed service-interfaces. 173 | // Plc may by stopped by system watchdog so that is possible to start plc cold on system start 174 | if(g_pRT->Init() == false) 175 | { 176 | Log::Error("Error during initialization"); 177 | } 178 | g_pRT->StartProcessing(); 179 | 180 | break; 181 | case PlcOperation_StartWarm: 182 | Log::Info("Call of PLC Start Warm"); 183 | 184 | // when this state-change occurred, the PLCnext runtime is ready to serve requests. 185 | // Now we can request the needed service-interfaces. 186 | if(g_pRT->Init() == false) 187 | { 188 | Log::Error("Error during initialization"); 189 | } 190 | g_pRT->StartProcessing(); 191 | break; 192 | case PlcOperation_StartHot: 193 | Log::Info("Call of PLC Start Hot"); 194 | g_pRT->StartProcessing(); 195 | break; 196 | case PlcOperation_Stop: 197 | Log::Info("Call of PLC Stop"); 198 | g_pRT->StopProcessing(); 199 | break; 200 | case PlcOperation_Reset: 201 | Log::Info("Call of PLC Reset"); 202 | g_pRT->StopProcessing(); 203 | break; 204 | case PlcOperation_Unload: 205 | Log::Info("Call of PLC Unload"); 206 | g_pRT->StopProcessing(); 207 | break; 208 | case PlcOperation_None: 209 | Log::Info("Call of PLC None"); 210 | break; 211 | default: 212 | Log::Error("Call of unknown PLC state"); 213 | break; 214 | } 215 | } 216 | 217 | -------------------------------------------------------------------------------- /src/CSampleRuntime.h: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | * Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | * 6 | * CSampleRuntime.h 7 | * 8 | * Created on: Jan 3, 2019 9 | * Author: Steffen Schlette 10 | * 11 | ******************************************************************************/ 12 | 13 | #ifndef CSAMPLERUNTIME_H_ 14 | #define CSAMPLERUNTIME_H_ 15 | 16 | #include "Utility.h" 17 | #include "CSampleRTThread.h" 18 | #include "CSampleSubscriptionThread.h" 19 | 20 | #include 21 | #include "Arp/Device/Interface/Services/IDeviceStatusService.hpp" 22 | #include "Arp/Device/Interface/Services/IDeviceInfoService.hpp" 23 | #include "Arp/System/Lm/Services/ILicenseStatusService.hpp" 24 | #include "Arp/System/Lm/Services/LicenseStatusServiceProxyFactory.hpp" 25 | #include "Arp/Plc/AnsiC/Domain/PlcOperationHandler.h" 26 | 27 | using namespace std; 28 | using namespace Arp; 29 | using namespace Arp::Device::Interface::Services; 30 | using namespace Arp::System::Lm::Services; 31 | 32 | // callback-function for state changes 33 | void PlcOperationHandler(enum PlcOperation operation); 34 | 35 | class CSampleRuntime; 36 | extern CSampleRuntime* g_pRT; // global pointer to sample runtime to make it call-able from callback-function 37 | 38 | class CSampleRuntime 39 | { 40 | public: 41 | CSampleRuntime(); 42 | virtual ~CSampleRuntime(); 43 | 44 | static PlcOperation m_zPLCMode; // current mode of operation 45 | 46 | bool Init(); 47 | bool StartProcessing(); 48 | bool StopProcessing(); 49 | 50 | private: 51 | bool m_bInitialized; // already initializes? 52 | 53 | CSampleRTThread m_zRTThread; 54 | CSampleSubscriptionThread m_zSubscriptionThread; 55 | 56 | // RSC services 57 | IDeviceInfoService::Ptr m_pDeviceInfoService; 58 | IDeviceStatusService::Ptr m_pDeviceStatusService; 59 | ILicenseStatusService::Ptr m_pLicenseStatusService; 60 | 61 | bool GetDeviceStatus(); 62 | 63 | // some static and dynamic data of the PLCnext runtime 64 | RscVariant<512> m_rscVendorName; 65 | RscVariant<512> m_rscCpuLoad; 66 | RscVariant<512> m_rscBemoryUsage; 67 | RscVariant<512> m_rscBoardTemp; 68 | const char* m_szVendorName; 69 | Arp::byte m_byCpuLoad; 70 | Arp::byte m_byMemoryUsage; 71 | int8 m_i8BoardTemp; 72 | }; 73 | 74 | #endif /* CSAMPLERUNTIME_H_ */ 75 | -------------------------------------------------------------------------------- /src/CSampleSubscriptionThread.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | * Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | * 6 | * CSampleSubscriptionThread.cpp 7 | * 8 | * Created on: Jul 12, 2019 9 | * Author: Steffen Schlette 10 | * 11 | ******************************************************************************/ 12 | 13 | #include "CSampleSubscriptionThread.h" 14 | 15 | #include "Arp/System/Rsc/ServiceManager.hpp" 16 | 17 | using namespace Arp::System::Rsc; 18 | 19 | // a PLCnext Engineer Project should exist on the device with this simple structure: 20 | // A program-instance named "MyProgramInst" which has three Ports of datatype bool: VarA (IN Port), VarB (IN Port) and VarC (Out Port) 21 | static const char GDSPort1[] = "Arp.Plc.Eclr/MyProgramInst.VarA"; 22 | static const char GDSPort2[] = "Arp.Plc.Eclr/MyProgramInst.VarB"; 23 | static const char GDSPort3[] = "Arp.Plc.Eclr/MyProgramInst.VarC"; 24 | 25 | CSampleSubscriptionThread::CSampleSubscriptionThread() : 26 | m_zCycleThread(), 27 | m_bInitialized(false), 28 | m_bDoCycle(false), 29 | m_uSubscriptionId(0), 30 | m_gdsPort1(0), 31 | m_gdsPort2(0), 32 | m_gdsPort3(0) 33 | { 34 | } 35 | 36 | CSampleSubscriptionThread::~CSampleSubscriptionThread() 37 | { 38 | } 39 | 40 | /// @brief Init and start the subscription thread 41 | /// @return true: success, false: failure 42 | bool CSampleSubscriptionThread::Init() 43 | { 44 | if(m_bInitialized) 45 | { 46 | // already initialized 47 | return(true); 48 | } 49 | 50 | Log::Info("Call of CSampleSubscriptionThread::Init"); 51 | 52 | bool bRet = false; 53 | 54 | // the firmware needs to be in the state PlcOperation_StartWarm before we can acquire services 55 | m_pSubscriptionService = ServiceManager::GetService(); 56 | m_pDataAccessService = ServiceManager::GetService(); 57 | 58 | if((m_pSubscriptionService != NULL) && 59 | (m_pDataAccessService != NULL)) 60 | { 61 | // create a simple worker thread for RSC-subscriptions 62 | if(pthread_create(&m_zCycleThread, NULL, CSampleSubscriptionThread::StaticCycle, this) == 0) 63 | { 64 | m_bInitialized = true; 65 | bRet = true; 66 | } 67 | else 68 | { 69 | Log::Error("Error calling pthread_create (non-realtime thread)"); 70 | } 71 | } 72 | else 73 | { 74 | Log::Error("Error getting service (non-realtime thread)"); 75 | } 76 | 77 | return(bRet); 78 | } 79 | 80 | /// @brief The thread will run continuously after creation but the processing of 81 | /// I/Os can be started and stopped e.g. if a new PLCnext Engineer Program was loaded 82 | /// @return true: success, false: failure 83 | bool CSampleSubscriptionThread::StartProcessing() 84 | { 85 | Log::Info("Subcription: Start processing"); 86 | 87 | bool bRet = false; 88 | try 89 | { 90 | if(CreateSubscription()) 91 | { 92 | m_bDoCycle = true; 93 | bRet = true; 94 | } 95 | } 96 | catch(Arp::Exception &e) 97 | { 98 | Log::Error("StartProcessing - Exception occured in CreateSubscription: {0}", e); 99 | } 100 | catch(...) 101 | { 102 | Log::Error("StartProcessing - Unknown Exception occured"); 103 | } 104 | 105 | return(bRet); 106 | } 107 | 108 | /// @brief The thread will run continuously after creation but the processing of 109 | /// I/Os can be started and stopped e.g. if a new PLCnext Engineer Program was loaded 110 | /// @return true: success, false: failure 111 | bool CSampleSubscriptionThread::StopProcessing() 112 | { 113 | Log::Info("Subcription: Stop processing"); 114 | 115 | bool bRet = false; 116 | 117 | // this is not threadsafe! In a real application you need to wait for end of cycle 118 | // before deleting the subscription 119 | m_bDoCycle = false; 120 | 121 | if(DeleteSubscription()) 122 | { 123 | bRet = true; 124 | } 125 | 126 | return(bRet); 127 | } 128 | 129 | /// @brief static function for worker thread-entry of the cycle function 130 | /// @param p pointer to thread object 131 | void* CSampleSubscriptionThread::StaticCycle(void* p) 132 | { 133 | if(p != NULL) 134 | { 135 | ((CSampleSubscriptionThread*)p)->Cycle(); 136 | } 137 | else 138 | { 139 | Log::Error("Null pointer in StaticCycle"); 140 | } 141 | return(NULL); 142 | } 143 | 144 | /// @brief loop to process values of subscription 145 | void CSampleSubscriptionThread::Cycle() 146 | { 147 | Log::Info("Call of CSampleSubscriptionThread::Cycle"); 148 | 149 | while(true) 150 | { 151 | if(m_bDoCycle) 152 | { 153 | // you can check the log messages in the local log-file of this application, usually in a subfolder named "Logs" 154 | Log::Info("************* Subscription-Thread values ******"); 155 | 156 | ReadSubscription(); 157 | 158 | Log::Info("{0}: Value: {1}", GDSPort1, m_gdsPort1 ? "true" : "false"); 159 | Log::Info("{0}: Value: {1}", GDSPort2, m_gdsPort2 ? "true" : "false"); 160 | Log::Info("{0}: Value: {1}", GDSPort3, m_gdsPort3 ? "true" : "false"); 161 | } 162 | 163 | WAIT100ms; 164 | } 165 | } 166 | 167 | /// @brief create a GDS subscription for three ports. 168 | /// @return true: success, false: failure 169 | bool CSampleSubscriptionThread::CreateSubscription() 170 | { 171 | Log::Info("Call of CSampleSubscriptionThread::CreateSubscription"); 172 | 173 | bool bRet = false; 174 | 175 | // create subscription, check the description of SubscriptionKind for the different realtime classes 176 | m_uSubscriptionId = m_pSubscriptionService->CreateSubscription(SubscriptionKind::HighPerformance); 177 | 178 | if(m_uSubscriptionId != 0) 179 | { 180 | // add the variables 181 | if(m_pSubscriptionService->AddVariable(m_uSubscriptionId, GDSPort1) == DataAccessError::None) 182 | { 183 | if(m_pSubscriptionService->AddVariable(m_uSubscriptionId, GDSPort2) == DataAccessError::None) 184 | { 185 | if(m_pSubscriptionService->AddVariable(m_uSubscriptionId, GDSPort3) == DataAccessError::None) 186 | { 187 | // subscribe the build subscription and save the returned record info for further reading 188 | // of the subscription to know the order of result 189 | const uint64 sampleRate = 1000000; // us == 1s 190 | if(m_pSubscriptionService->Subscribe(m_uSubscriptionId, sampleRate) == DataAccessError::None) 191 | { 192 | // get information about the order / layout of the vector of read variable values 193 | if(RSCReadVariableInfos(m_zSubscriptionInfos) == DataAccessError::None) 194 | { 195 | bRet = true; 196 | } 197 | else 198 | { 199 | Log::Error("Unable to read variable information"); 200 | } 201 | } 202 | else 203 | { 204 | Log::Error("ISubscriptionservice::Subscribe returned error"); 205 | } 206 | } 207 | else 208 | { 209 | Log::Error("Unable to subscribe variable {0}", GDSPort3); 210 | } 211 | } 212 | else 213 | { 214 | Log::Error("Unable to subscribe variable {0}", GDSPort2); 215 | } 216 | } 217 | else 218 | { 219 | Log::Error("Unable to subscribe variable {0}", GDSPort1); 220 | } 221 | } 222 | else 223 | { 224 | Log::Error("ISubscriptionservice::CreateSubscription returned error"); 225 | } 226 | return bRet; 227 | } 228 | 229 | /// @brief delete the subscription 230 | /// @return true: success, false: failure 231 | bool CSampleSubscriptionThread::DeleteSubscription() 232 | { 233 | Log::Info("Call of CSampleSubscriptionThread::DeleteSubscription"); 234 | 235 | bool bRet = false; 236 | 237 | if(m_uSubscriptionId != 0) 238 | { 239 | if(m_pSubscriptionService->DeleteSubscription(m_uSubscriptionId) == DataAccessError::None) 240 | { 241 | bRet = true; 242 | } 243 | else 244 | { 245 | Log::Error("ISubscriptionservice::DeleteSubscription returned error"); 246 | } 247 | 248 | m_uSubscriptionId = 0; 249 | } 250 | return(bRet); 251 | } 252 | 253 | /// @brief poll the subscribed values and parse them 254 | /// @return true: success, false: failure 255 | bool CSampleSubscriptionThread::ReadSubscription() 256 | { 257 | bool bRet = false; 258 | 259 | try 260 | { 261 | // Read the subscription 262 | if(RSCReadVariableValues(m_zSubscriptionValues) == DataAccessError::None) 263 | { 264 | // The order of the subscription info vector is the same as the subscription values vector, 265 | // we can iterate over both in the same loop 266 | if(m_zSubscriptionValues.size() == m_zSubscriptionInfos.size()) // sanity-check 267 | { 268 | for(std::size_t nCount = 0; nCount < m_zSubscriptionValues.size(); ++nCount) 269 | { 270 | if(m_zSubscriptionValues[nCount].GetType() != RscType::Void) 271 | { 272 | 273 | if(strcmp(m_zSubscriptionInfos[nCount].Name.CStr(), GDSPort1) == 0) 274 | { 275 | m_zSubscriptionValues[nCount].CopyTo(m_gdsPort1); 276 | } 277 | if(strcmp(m_zSubscriptionInfos[nCount].Name.CStr(), GDSPort2) == 0) 278 | { 279 | m_zSubscriptionValues[nCount].CopyTo(m_gdsPort2); 280 | } 281 | if(strcmp(m_zSubscriptionInfos[nCount].Name.CStr(), GDSPort3) == 0) 282 | { 283 | m_zSubscriptionValues[nCount].CopyTo(m_gdsPort3); 284 | } 285 | } 286 | else 287 | { 288 | Log::Info("Subscription {0} is not available yet. Initial value of its variable will be used!", m_zSubscriptionInfos[nCount].Name); 289 | } 290 | } 291 | bRet = true; 292 | } 293 | else 294 | { 295 | Log::Error("Inconsistent size of subscription info and subscription values vector"); 296 | } 297 | } 298 | else 299 | { 300 | Log::Error("ISubscriptionService::ReadValues returned error"); 301 | } 302 | } 303 | catch(Arp::Exception &e) 304 | { 305 | Log::Error("ReadSubscription - Arp Exception! {0}", e); 306 | } 307 | catch(...) 308 | { 309 | Log::Error("ReadSubscription - Unknown Exception occured"); 310 | } 311 | 312 | 313 | return(bRet); 314 | } 315 | 316 | // create a delegate (callback-function) for reading the vector of subscription values 317 | DataAccessError CSampleSubscriptionThread::RSCReadVariableValues(vector>& values) 318 | { 319 | ISubscriptionService::ReadValuesValuesDelegate readSubscriptionValuesDelegate = 320 | ISubscriptionService::ReadValuesValuesDelegate::create([&](IRscReadEnumerator>& readEnumerator) 321 | { 322 | values.clear(); 323 | 324 | size_t valueCount = readEnumerator.BeginRead(); 325 | values.reserve(valueCount); 326 | 327 | RscVariant<512> current; 328 | for (size_t i = 0; i < valueCount; i++) 329 | { 330 | readEnumerator.ReadNext(current); 331 | values.push_back(current); 332 | } 333 | readEnumerator.EndRead(); 334 | }); 335 | 336 | return m_pSubscriptionService->ReadValues(m_uSubscriptionId, readSubscriptionValuesDelegate); 337 | } 338 | 339 | // create a delegate (callback-function) for reading the vector of subscription information 340 | DataAccessError CSampleSubscriptionThread::RSCReadVariableInfos(vector& values) 341 | { 342 | ISubscriptionService::GetVariableInfosVariableInfoDelegate getSubscriptionInfosDelegate = 343 | ISubscriptionService::GetVariableInfosVariableInfoDelegate::create([&](IRscReadEnumerator& readEnumerator) 344 | { 345 | values.clear(); 346 | 347 | size_t valueCount = readEnumerator.BeginRead(); 348 | values.reserve(valueCount); 349 | 350 | VariableInfo current; 351 | for (size_t i = 0; i < valueCount; i++) 352 | { 353 | readEnumerator.ReadNext(current); 354 | values.push_back(current); 355 | } 356 | readEnumerator.EndRead(); 357 | }); 358 | 359 | return m_pSubscriptionService->GetVariableInfos(m_uSubscriptionId, getSubscriptionInfosDelegate); 360 | } 361 | -------------------------------------------------------------------------------- /src/CSampleSubscriptionThread.h: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | * Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | * 6 | * CSampleSubscriptionThread.h 7 | * 8 | * Created on: Jul 12, 2019 9 | * Author: Steffen Schlette 10 | * 11 | ******************************************************************************/ 12 | 13 | #include 14 | #include "Arp/Plc/Gds/Services/ISubscriptionService.hpp" 15 | #include "Arp/Plc/Gds/Services/IDataAccessService.hpp" 16 | #include "Arp/System/Commons/Logging.h" 17 | #include "Utility.h" 18 | 19 | using namespace std; 20 | using namespace Arp; 21 | using namespace Arp::Plc::Gds::Services; 22 | 23 | #ifndef CSAMPLESUBSCRIPTIONTHREAD_H_ 24 | #define CSAMPLESUBSCRIPTIONTHREAD_H_ 25 | 26 | class CSampleSubscriptionThread 27 | { 28 | public: 29 | CSampleSubscriptionThread(); 30 | virtual ~CSampleSubscriptionThread(); 31 | 32 | bool Init(); 33 | static void* StaticCycle(void* p); 34 | void Cycle(); 35 | 36 | // start and stop our own processing 37 | bool StartProcessing(); 38 | bool StopProcessing(); 39 | 40 | private: 41 | 42 | pthread_t m_zCycleThread; 43 | 44 | bool m_bInitialized; // class already initialized? 45 | bool m_bDoCycle; // shall the subscription cycle run? 46 | 47 | ISubscriptionService::Ptr m_pSubscriptionService; 48 | IDataAccessService::Ptr m_pDataAccessService; 49 | 50 | // example usage of GDS subscription 51 | bool CreateSubscription(); 52 | bool DeleteSubscription(); 53 | bool ReadSubscription(); 54 | DataAccessError RSCReadVariableValues(vector>& values); 55 | DataAccessError RSCReadVariableInfos(vector& values); 56 | 57 | // subscriptions 58 | uint32 m_uSubscriptionId; 59 | vector> m_zSubscriptionValues; 60 | vector m_zSubscriptionInfos; 61 | 62 | // data fields 63 | bool m_gdsPort1; 64 | bool m_gdsPort2; 65 | bool m_gdsPort3; 66 | }; 67 | 68 | #endif /* CSAMPLESUBSCRIPTIONTHREAD_H_ */ 69 | -------------------------------------------------------------------------------- /src/PLCnextSampleRuntime.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | * Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | * 6 | * PLCnextSampleRuntime.cpp 7 | * 8 | * Created on: Jan 3, 2019 9 | * Author: Steffen Schlette 10 | * 11 | ******************************************************************************/ 12 | 13 | 14 | /*************************************************************************/ 15 | /* INCLUDES */ 16 | /*************************************************************************/ 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include "Arp/System/ModuleLib/Module.h" 27 | 28 | #include "CSampleRuntime.h" 29 | 30 | using namespace std; 31 | 32 | CSampleRuntime* g_pRT; 33 | 34 | int main(int argc, char** argv) { 35 | 36 | // Ask plcnext for access to its services 37 | // Use syslog for logging until the PLCnext logger is ready 38 | openlog ("PLCnextSampleRuntime", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); 39 | 40 | syslog (LOG_INFO, "PLCnextSampleRuntime started"); 41 | 42 | // Process command line arguments 43 | string acfSettingsRelPath(""); 44 | 45 | if(argc != 2) 46 | { 47 | syslog (LOG_ERR, "Invalid command line arguments. Only relative path to the acf.settings file must be passed"); 48 | return -1; 49 | } 50 | else 51 | { 52 | acfSettingsRelPath = argv[1]; 53 | syslog(LOG_INFO, string("Arg Acf settings file path: " + acfSettingsRelPath).c_str()); 54 | } 55 | 56 | char szExePath[PATH_MAX]; 57 | ssize_t count = readlink("/proc/self/exe", szExePath, PATH_MAX); 58 | string strDirPath; 59 | if (count != -1) { 60 | strDirPath = dirname(szExePath); 61 | } 62 | string strSettingsFile(strDirPath); 63 | strSettingsFile += "/" + acfSettingsRelPath; 64 | syslog(LOG_INFO, string("Acf settings file path: " + strSettingsFile).c_str()); 65 | 66 | // Intialize PLCnext module application 67 | // Arguments: 68 | // arpBinaryDir: Path to Arp binaries 69 | // applicationName: Arbitrary Name of Application 70 | // acfSettingsPath: Path to *.acf.settings document to set application up 71 | if (ArpSystemModule_Load("/usr/lib", "runtime", strSettingsFile.c_str()) != 0) 72 | { 73 | syslog (LOG_ERR, "Could not load Arp System Module Application"); 74 | return -1; 75 | } 76 | syslog (LOG_INFO, "Loaded Arp System Module Application"); 77 | closelog(); 78 | 79 | g_pRT = new CSampleRuntime(); 80 | 81 | // loop forever 82 | for (;;) 83 | { 84 | WAIT1s; 85 | } 86 | 87 | return 0; 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/Utility.h: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | * Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | * 6 | * Utility.h 7 | * 8 | * Created on: Jul 12, 2019 9 | * Author: Steffen Schlette 10 | * 11 | ******************************************************************************/ 12 | 13 | #ifndef UTILITY_H_ 14 | #define UTILITY_H_ 15 | 16 | #define WAIT100ms usleep(100000); // 100ms 17 | #define WAIT1s usleep(1000000); // 1s 18 | #define WAIT10s usleep(10000000); // 10s 19 | 20 | typedef void * (*THREADFUNCPTR)(void *); 21 | 22 | #endif /* UTILITY_H_ */ 23 | -------------------------------------------------------------------------------- /tools/build-runtime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #// 3 | #// Copyright (c) 2019 Phoenix Contact GmbH & Co. KG. All rights reserved. 4 | #// Licensed under the MIT. See LICENSE file in the project root for full license information. 5 | #// SPDX-License-Identifier: MIT 6 | #// 7 | #// 8 | #// NOTE: This script is not required for the latest version of this project. 9 | while getopts t:a:n: option 10 | do 11 | case "${option}" 12 | in 13 | t) TOOLCHAIN=${OPTARG};; 14 | a) ARPVERSION=${OPTARG};; 15 | n) TARGETNAME=${OPTARG};; 16 | esac 17 | done 18 | 19 | VERSION=$(echo $ARPVERSION | grep -oP [0-9]+[.][0-9]+[.][0-9]+[.][0-9]+) 20 | echo "Version:${VERSION}" 21 | 22 | # Get the directory of this script 23 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 24 | 25 | echo CMAKE Configure 26 | cmake -G "Ninja" \ 27 | -DBUILD_TESTING=OFF \ 28 | -DUSE_ARP_DEVICE=ON \ 29 | -DCMAKE_STAGING_PREFIX="${DIR}/../deploy/" \ 30 | -DCMAKE_INSTALL_PREFIX=/usr/local \ 31 | -DCMAKE_PREFIX_PATH="${DIR}/../deploy/${TARGETNAME}_${VERSION}" \ 32 | -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ 33 | "-DARP_TOOLCHAIN_ROOT=${TOOLCHAIN}" \ 34 | "-DARP_DEVICE=${TARGETNAME}" \ 35 | "-DARP_DEVICE_VERSION=${ARPVERSION}" \ 36 | "-DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN}/toolchain.cmake" \ 37 | -S "${DIR}/../." \ 38 | -B "${DIR}/../build/${TARGETNAME}_${VERSION}" 39 | 40 | cmake --build "${DIR}/../build/${TARGETNAME}_${VERSION}" \ 41 | --config Debug \ 42 | --target all -- -j 3 43 | 44 | cmake --build "${DIR}/../build/${TARGETNAME}_${VERSION}" \ 45 | --config Debug --target install -- -j 3 46 | -------------------------------------------------------------------------------- /tools/start_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TARGET_IP="192.168.1.10" 4 | TARGET_USER="admin" 5 | TARGET_PASSWORD="123456" 6 | APP="Runtime" 7 | REMOTE_PATH="/opt/plcnext/projects/Runtime" 8 | 9 | echo "Killing gdbserver, if it is running" 10 | sshpass -p "${TARGET_PASSWORD}" ssh ${TARGET_USER}@${TARGET_IP} 'kill -9 `pidof gdbserver`' 11 | 12 | echo "Stopping plcnext firmware\n" 13 | sshpass -p "${TARGET_PASSWORD}" ssh ${TARGET_USER}@${TARGET_IP} '/etc/init.d/plcnext stop' 14 | 15 | echo "Killing application, if it is running\n" 16 | sshpass -p "${TARGET_PASSWORD}" ssh ${TARGET_USER}@${TARGET_IP} ${COMMAND} 'kill -9 `pidof '${APP}'`' 17 | 18 | echo "Copy application to target\n" 19 | # you can also copy a whole directory with the scp -r option, if the application 20 | # consists of several files 21 | sshpass -p "${TARGET_PASSWORD}" scp ${APP} ${TARGET_USER}@${TARGET_IP}:${REMOTE_PATH}/${APP} 22 | 23 | echo "Set needed capabilities e.g. to execute realtime threads\n" 24 | sshpass -p "${TARGET_PASSWORD}" ssh ${TARGET_USER}@${TARGET_IP} ${COMMAND} 'setcap cap_net_bind_service,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_sys_boot,cap_sys_nice,cap_sys_time+ep '${REMOTE_PATH}/${APP} 25 | 26 | echo "Starting plcnext firmware\n" 27 | sshpass -p "${TARGET_PASSWORD}" ssh ${TARGET_USER}@${TARGET_IP} '/etc/init.d/plcnext start' 28 | 29 | sleep 3s 30 | 31 | echo "Attaching gdbserver" 32 | sshpass -p "${TARGET_PASSWORD}" ssh ${TARGET_USER}@${TARGET_IP} ${COMMAND} 'gdbserver :2345 --attach `pidof '${APP}'`' 33 | 34 | echo "Finished" 35 | --------------------------------------------------------------------------------