From 890193582aa1a56f1fb90235626ad44a8f251a57 Mon Sep 17 00:00:00 2001 From: svad05 Date: Fri, 8 Jul 2022 16:04:01 +0300 Subject: [PATCH] =?UTF-8?q?dev(none):=20=D0=A1=D0=BD=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D0=B8=20p-net?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- profinet_stack/p-net/.clang-format | 41 + profinet_stack/p-net/.git-blame-ignore-revs | 12 + profinet_stack/p-net/.gitattributes | 24 + .../p-net/.github/workflows/build.yml | 35 + .../.github/workflows/clang-analysis.yml | 69 + .../.github/workflows/codeql-analysis.yml | 46 + profinet_stack/p-net/.gitignore | 53 + profinet_stack/p-net/.gitmodules | 3 + profinet_stack/p-net/CHANGELOG.md | 125 + profinet_stack/p-net/CMakeLists.txt | 265 + profinet_stack/p-net/LICENSE.md | 23 + profinet_stack/p-net/README.md | 147 + profinet_stack/p-net/cmake/FindNetSNMP.cmake | 37 + .../p-net/cmake/FindNetSNMPAgent.cmake | 37 + profinet_stack/p-net/cmake/Linux.cmake | 116 + profinet_stack/p-net/cmake/STM32Cube.cmake | 83 + profinet_stack/p-net/cmake/rt-kernel.cmake | 83 + profinet_stack/p-net/doc/Makefile | 34 + profinet_stack/p-net/doc/abbreviations.rst | 375 ++ .../p-net/doc/additional_linux_details.rst | 389 ++ .../p-net/doc/api_documentation.rst | 182 + .../p-net/doc/applications_and_porting.rst | 140 + .../p-net/doc/capturing_packets.rst | 263 + profinet_stack/p-net/doc/codespell_ignore.txt | 3 + profinet_stack/p-net/doc/codesys_details.rst | 224 + profinet_stack/p-net/doc/compliancetest.rst | 911 +++ profinet_stack/p-net/doc/conf.py | 94 + .../p-net/doc/creating_gsdml_files.rst | 818 +++ profinet_stack/p-net/doc/doc_requirements.txt | 8 + .../p-net/doc/getting_started_LAN9662.rst | 380 ++ .../p-net/doc/getting_started_freertos.rst | 50 + .../p-net/doc/getting_started_rtkernel.rst | 380 ++ .../illustrations/AlarmStateMachines.drawio | 1 + .../doc/illustrations/AlarmStateMachines.png | Bin 0 -> 462053 bytes .../p-net/doc/illustrations/Cyclecounter.png | Bin 0 -> 90706 bytes .../illustrations/FlasherFunctionBlock.png | Bin 0 -> 18901 bytes .../p-net/doc/illustrations/NetworkLoad.png | Bin 0 -> 71982 bytes .../illustrations/PlcProgramLadderLogic.png | Bin 0 -> 89301 bytes .../illustrations/RaspberryPiLedButtons.jpg | Bin 0 -> 135398 bytes .../doc/illustrations/SimaticIoDevice.jpg | Bin 0 -> 72988 bytes .../doc/illustrations/TutorialOverview.drawio | 1 + .../doc/illustrations/TutorialOverview.png | Bin 0 -> 23042 bytes .../p-net/doc/illustrations/periodicity.png | Bin 0 -> 275250 bytes .../doc/illustrations/proneta_example.png | Bin 0 -> 27309 bytes .../doc/illustrations/rdrecLadderlogic.png | Bin 0 -> 44336 bytes .../p-net/doc/implementation_details.rst | 1026 ++++ profinet_stack/p-net/doc/index.rst | 50 + profinet_stack/p-net/doc/linuxtiming.rst | 116 + profinet_stack/p-net/doc/multiple_ports.rst | 272 + .../p-net/doc/network_topology_detection.rst | 1376 +++++ .../p-net/doc/prepare_raspberrypi.rst | 382 ++ profinet_stack/p-net/doc/profinet_basics.rst | 376 ++ profinet_stack/p-net/doc/profinet_details.rst | 643 ++ .../p-net/doc/sampleapp_details.rst | 237 + .../p-net/doc/spelling_wordlist.txt | 223 + .../doc/static/css/change_header_size.css | 3 + .../p-net/doc/static/css/fix_table_width.css | 15 + profinet_stack/p-net/doc/tutorial.rst | 565 ++ .../p-net/doc/use_with_siematic.rst | 699 +++ profinet_stack/p-net/doc/using_codesys.rst | 178 + .../p-net/doc/writing_documentation.rst | 43 + profinet_stack/p-net/doc/yoctobuild.rst | 21 + profinet_stack/p-net/include/pnet_api.h | 2257 +++++++ profinet_stack/p-net/options.h.in | 246 + profinet_stack/p-net/pnet_options.h.in | 33 + profinet_stack/p-net/pnet_version.h.in | 41 + .../p-net/samples/pn_dev/CMakeLists.txt | 21 + .../samples/pn_dev/GSDML-RT-LABS-STACK.bmp | Bin 0 -> 47670 bytes ...V2.4-RT-Labs-P-Net-Sample-App-20220324.xml | 301 + .../p-net/samples/pn_dev/app_data.c | 315 + .../p-net/samples/pn_dev/app_data.h | 140 + .../p-net/samples/pn_dev/app_gsdml.c | 260 + .../p-net/samples/pn_dev/app_gsdml.h | 184 + profinet_stack/p-net/samples/pn_dev/app_log.c | 52 + profinet_stack/p-net/samples/pn_dev/app_log.h | 75 + .../p-net/samples/pn_dev/app_utils.c | 741 +++ .../p-net/samples/pn_dev/app_utils.h | 428 ++ .../p-net/samples/pn_dev/sampleapp_common.c | 1622 +++++ .../p-net/samples/pn_dev/sampleapp_common.h | 156 + .../samples/pn_dev_lan9662/CMakeLists.txt | 23 + ...ML-V2.4-RT-Labs-P-Net-LAN9662-20220511.xml | 417 ++ .../pn_dev_lan9662/S90_start_profinet.sh | 68 + .../p-net/samples/pn_dev_lan9662/app_data.c | 442 ++ .../p-net/samples/pn_dev_lan9662/app_data.h | 121 + .../p-net/samples/pn_dev_lan9662/app_gsdml.c | 394 ++ .../p-net/samples/pn_dev_lan9662/app_gsdml.h | 199 + .../p-net/samples/pn_dev_lan9662/app_log.c | 52 + .../p-net/samples/pn_dev_lan9662/app_log.h | 75 + .../p-net/samples/pn_dev_lan9662/app_shm.c | 318 + .../p-net/samples/pn_dev_lan9662/app_shm.h | 104 + .../p-net/samples/pn_dev_lan9662/app_utils.c | 722 +++ .../p-net/samples/pn_dev_lan9662/app_utils.h | 382 ++ .../samples/pn_dev_lan9662/sampleapp_common.c | 1235 ++++ .../samples/pn_dev_lan9662/sampleapp_common.h | 148 + .../switchdev-profinet-example.sh | 79 + .../p-net/samples/pn_shm_tool/CMakeLists.txt | 17 + .../p-net/samples/pn_shm_tool/pn_shm_tool.c | 509 ++ .../samples/pn_shm_tool/shm_LED_handler.sh | 31 + .../samples/pn_shm_tool/shm_button_handler.sh | 39 + .../p-net/samples/pn_shm_tool/shm_echo_all.sh | 15 + .../p-net/samples/pn_shm_tool/shm_read_all.sh | 32 + .../pn_shm_tool/shm_write_all_inputs.sh | 29 + .../p-net/samples/pn_shm_tool/test.sh | 122 + profinet_stack/p-net/src/CMakeLists.txt | 90 + profinet_stack/p-net/src/common/pf_alarm.c | 3485 +++++++++++ profinet_stack/p-net/src/common/pf_alarm.h | 326 + .../p-net/src/common/pf_bg_worker.c | 132 + .../p-net/src/common/pf_bg_worker.h | 63 + profinet_stack/p-net/src/common/pf_cpm.c | 618 ++ profinet_stack/p-net/src/common/pf_cpm.h | 192 + .../p-net/src/common/pf_cpm_driver_sw.c | 547 ++ .../p-net/src/common/pf_cpm_driver_sw.h | 33 + profinet_stack/p-net/src/common/pf_dcp.c | 2010 +++++++ profinet_stack/p-net/src/common/pf_dcp.h | 145 + profinet_stack/p-net/src/common/pf_eth.c | 320 + profinet_stack/p-net/src/common/pf_eth.h | 111 + profinet_stack/p-net/src/common/pf_file.c | 323 + profinet_stack/p-net/src/common/pf_file.h | 120 + profinet_stack/p-net/src/common/pf_lldp.c | 2036 +++++++ profinet_stack/p-net/src/common/pf_lldp.h | 703 +++ profinet_stack/p-net/src/common/pf_ppm.c | 1096 ++++ profinet_stack/p-net/src/common/pf_ppm.h | 312 + .../p-net/src/common/pf_ppm_driver_sw.c | 293 + .../p-net/src/common/pf_ppm_driver_sw.h | 29 + profinet_stack/p-net/src/common/pf_ptcp.c | 20 + profinet_stack/p-net/src/common/pf_ptcp.h | 27 + .../p-net/src/common/pf_scheduler.c | 642 ++ .../p-net/src/common/pf_scheduler.h | 181 + profinet_stack/p-net/src/common/pf_snmp.c | 698 +++ profinet_stack/p-net/src/common/pf_snmp.h | 887 +++ profinet_stack/p-net/src/common/pf_udp.c | 74 + profinet_stack/p-net/src/common/pf_udp.h | 86 + .../p-net/src/device/pf_block_reader.c | 1048 ++++ .../p-net/src/device/pf_block_reader.h | 394 ++ .../p-net/src/device/pf_block_writer.c | 4649 +++++++++++++++ .../p-net/src/device/pf_block_writer.h | 920 +++ profinet_stack/p-net/src/device/pf_cmdev.c | 5224 +++++++++++++++++ profinet_stack/p-net/src/device/pf_cmdev.h | 499 ++ profinet_stack/p-net/src/device/pf_cmdmc.c | 35 + profinet_stack/p-net/src/device/pf_cmdmc.h | 53 + profinet_stack/p-net/src/device/pf_cmina.c | 1780 ++++++ profinet_stack/p-net/src/device/pf_cmina.h | 249 + profinet_stack/p-net/src/device/pf_cmio.c | 381 ++ profinet_stack/p-net/src/device/pf_cmio.h | 73 + profinet_stack/p-net/src/device/pf_cmpbe.c | 291 + profinet_stack/p-net/src/device/pf_cmpbe.h | 80 + profinet_stack/p-net/src/device/pf_cmrdr.c | 1195 ++++ profinet_stack/p-net/src/device/pf_cmrdr.h | 58 + profinet_stack/p-net/src/device/pf_cmrpc.c | 5103 ++++++++++++++++ profinet_stack/p-net/src/device/pf_cmrpc.h | 175 + .../p-net/src/device/pf_cmrpc_epm.c | 383 ++ .../p-net/src/device/pf_cmrpc_epm.h | 48 + .../p-net/src/device/pf_cmrpc_helpers.c | 57 + .../p-net/src/device/pf_cmrpc_helpers.h | 35 + profinet_stack/p-net/src/device/pf_cmrs.c | 20 + profinet_stack/p-net/src/device/pf_cmrs.h | 27 + profinet_stack/p-net/src/device/pf_cmsm.c | 339 ++ profinet_stack/p-net/src/device/pf_cmsm.h | 96 + profinet_stack/p-net/src/device/pf_cmsu.c | 317 + profinet_stack/p-net/src/device/pf_cmsu.h | 137 + profinet_stack/p-net/src/device/pf_cmwrr.c | 364 ++ profinet_stack/p-net/src/device/pf_cmwrr.h | 82 + profinet_stack/p-net/src/device/pf_diag.c | 1165 ++++ profinet_stack/p-net/src/device/pf_diag.h | 343 ++ profinet_stack/p-net/src/device/pf_fspm.c | 1421 +++++ profinet_stack/p-net/src/device/pf_fspm.h | 418 ++ profinet_stack/p-net/src/device/pf_pdport.c | 1535 +++++ profinet_stack/p-net/src/device/pf_pdport.h | 226 + profinet_stack/p-net/src/device/pf_port.c | 311 + profinet_stack/p-net/src/device/pf_port.h | 210 + profinet_stack/p-net/src/device/pnet_api.c | 766 +++ .../p-net/src/drivers/drivers.cmake | 17 + .../drivers/lan9662/add_inbound_vcap_rule.sh | 41 + .../src/drivers/lan9662/add_mera_lib.cmake | 42 + .../drivers/lan9662/add_outbound_vcap_rule.sh | 47 + .../src/drivers/lan9662/del_vcap_rule.sh | 20 + .../p-net/src/drivers/lan9662/driver.cmake | 154 + .../drivers/lan9662/include/driver_config.h | 49 + .../lan9662/include/pnet_lan9662_api.h | 89 + .../drivers/lan9662/pnet_driver_options.h.in | 52 + .../src/drivers/lan9662/set_profinet_leds | 37 + .../lan9662/src/pf_cpm_driver_lan9662.c | 473 ++ .../src/drivers/lan9662/src/pf_lan9662_mera.c | 2836 +++++++++ .../src/drivers/lan9662/src/pf_lan9662_mera.h | 335 ++ .../src/drivers/lan9662/src/pf_mera_trace.c | 129 + .../src/drivers/lan9662/src/pf_mera_trace.h | 38 + .../lan9662/src/pf_ppm_driver_lan9662.c | 328 ++ .../src/drivers/lan9662/src/pf_rte_uio.c | 182 + .../src/drivers/lan9662/src/pf_rte_uio.h | 59 + .../src/drivers/lan9662/src/pf_sram_uio.c | 343 ++ .../src/drivers/lan9662/src/pf_sram_uio.h | 68 + profinet_stack/p-net/src/pf_driver.h | 64 + profinet_stack/p-net/src/pf_includes.h | 75 + profinet_stack/p-net/src/pf_types.h | 3039 ++++++++++ profinet_stack/p-net/src/pnal.h | 552 ++ .../p-net/src/ports/STM32Cube/pnal.c | 302 + .../p-net/src/ports/STM32Cube/pnal_config.h | 49 + .../p-net/src/ports/STM32Cube/pnal_eth.c | 179 + .../p-net/src/ports/STM32Cube/pnal_sys.h | 35 + .../p-net/src/ports/STM32Cube/pnal_udp.c | 110 + .../STM32Cube/sampleapp_STM32F769I-DISCO.c | 43 + .../src/ports/STM32Cube/sampleapp_board.c | 25 + .../src/ports/STM32Cube/sampleapp_main.c | 111 + .../src/ports/linux/enable-rpi-gpio-pins.sh | 7 + .../ports/linux/mib/lldpConfigManAddrTable.c | 173 + .../ports/linux/mib/lldpConfigManAddrTable.h | 34 + .../src/ports/linux/mib/lldpLocManAddrTable.c | 198 + .../src/ports/linux/mib/lldpLocManAddrTable.h | 39 + .../src/ports/linux/mib/lldpLocPortTable.c | 221 + .../src/ports/linux/mib/lldpLocPortTable.h | 37 + .../src/ports/linux/mib/lldpLocalSystemData.c | 149 + .../src/ports/linux/mib/lldpLocalSystemData.h | 34 + .../src/ports/linux/mib/lldpRemManAddrTable.c | 250 + .../src/ports/linux/mib/lldpRemManAddrTable.h | 38 + .../p-net/src/ports/linux/mib/lldpRemTable.c | 300 + .../p-net/src/ports/linux/mib/lldpRemTable.h | 45 + .../src/ports/linux/mib/lldpXPnoLocTable.c | 233 + .../src/ports/linux/mib/lldpXPnoLocTable.h | 61 + .../src/ports/linux/mib/lldpXPnoRemTable.c | 273 + .../src/ports/linux/mib/lldpXPnoRemTable.h | 58 + .../ports/linux/mib/lldpXdot3LocPortTable.c | 235 + .../ports/linux/mib/lldpXdot3LocPortTable.h | 37 + .../ports/linux/mib/lldpXdot3RemPortTable.c | 277 + .../ports/linux/mib/lldpXdot3RemPortTable.h | 37 + .../p-net/src/ports/linux/mib/system_mib.c | 619 ++ .../p-net/src/ports/linux/mib/system_mib.h | 35 + profinet_stack/p-net/src/ports/linux/pnal.c | 604 ++ .../p-net/src/ports/linux/pnal_config.h | 54 + .../p-net/src/ports/linux/pnal_eth.c | 192 + .../p-net/src/ports/linux/pnal_filetools.c | 225 + .../p-net/src/ports/linux/pnal_filetools.h | 57 + .../p-net/src/ports/linux/pnal_snmp.c | 81 + .../p-net/src/ports/linux/pnal_sys.h | 39 + .../p-net/src/ports/linux/pnal_udp.c | 112 + .../src/ports/linux/pnet-sampleapp.service | 25 + .../p-net/src/ports/linux/sampleapp_main.c | 516 ++ .../src/ports/linux/set_network_parameters | 89 + .../p-net/src/ports/linux/set_profinet_leds | 36 + .../ports/linux/set_profinet_leds.raspberrypi | 42 + .../0001-rtkernel-for-Profinet.patch | 1476 +++++ .../ports/rt-kernel/mib/lldp-ext-dot3-mib.c | 532 ++ .../ports/rt-kernel/mib/lldp-ext-dot3-mib.h | 44 + .../ports/rt-kernel/mib/lldp-ext-pno-mib.c | 545 ++ .../ports/rt-kernel/mib/lldp-ext-pno-mib.h | 44 + .../p-net/src/ports/rt-kernel/mib/lldp-mib.c | 1174 ++++ .../p-net/src/ports/rt-kernel/mib/lldp-mib.h | 44 + .../src/ports/rt-kernel/mib/mib2_system.c | 418 ++ .../src/ports/rt-kernel/mib/mib2_system.h | 91 + .../p-net/src/ports/rt-kernel/mib/rowindex.c | 351 ++ .../p-net/src/ports/rt-kernel/mib/rowindex.h | 123 + .../p-net/src/ports/rt-kernel/pnal.c | 372 ++ .../p-net/src/ports/rt-kernel/pnal_config.h | 49 + .../p-net/src/ports/rt-kernel/pnal_eth.c | 172 + .../p-net/src/ports/rt-kernel/pnal_snmp.c | 75 + .../p-net/src/ports/rt-kernel/pnal_snmp.h | 75 + .../p-net/src/ports/rt-kernel/pnal_sys.h | 35 + .../p-net/src/ports/rt-kernel/pnal_udp.c | 110 + .../p-net/src/ports/rt-kernel/sampleapp_bsp.c | 25 + .../src/ports/rt-kernel/sampleapp_f769disco.c | 44 + .../src/ports/rt-kernel/sampleapp_main.c | 200 + .../ports/rt-kernel/sampleapp_xmc48relax.c | 45 + profinet_stack/p-net/test/CMakeLists.txt | 135 + profinet_stack/p-net/test/mocks.cpp | 512 ++ profinet_stack/p-net/test/mocks.h | 224 + profinet_stack/p-net/test/pf_test.cpp | 28 + profinet_stack/p-net/test/test_alarm.cpp | 488 ++ .../p-net/test/test_block_reader.cpp | 113 + profinet_stack/p-net/test/test_cmdev.cpp | 408 ++ profinet_stack/p-net/test/test_cmdmc.cpp | 29 + profinet_stack/p-net/test/test_cmina.cpp | 265 + profinet_stack/p-net/test/test_cmio.cpp | 29 + profinet_stack/p-net/test/test_cmpbe.cpp | 29 + profinet_stack/p-net/test/test_cmrdr.cpp | 493 ++ profinet_stack/p-net/test/test_cmrpc.cpp | 704 +++ profinet_stack/p-net/test/test_cmrs.cpp | 29 + profinet_stack/p-net/test/test_cmsm.cpp | 29 + profinet_stack/p-net/test/test_cmsu.cpp | 29 + profinet_stack/p-net/test/test_cmwdr.cpp | 29 + profinet_stack/p-net/test/test_cpm.cpp | 106 + profinet_stack/p-net/test/test_dcp.cpp | 380 ++ profinet_stack/p-net/test/test_diag.cpp | 1075 ++++ profinet_stack/p-net/test/test_eth.cpp | 29 + profinet_stack/p-net/test/test_file.cpp | 410 ++ profinet_stack/p-net/test/test_fspm.cpp | 180 + profinet_stack/p-net/test/test_lldp.cpp | 1154 ++++ profinet_stack/p-net/test/test_pnetapi.cpp | 429 ++ profinet_stack/p-net/test/test_port.cpp | 249 + profinet_stack/p-net/test/test_ppm.cpp | 137 + profinet_stack/p-net/test/test_ptcp.cpp | 29 + profinet_stack/p-net/test/test_scheduler.cpp | 534 ++ profinet_stack/p-net/test/test_snmp.cpp | 263 + .../p-net/test/utils_for_testing.cpp | 617 ++ profinet_stack/p-net/test/utils_for_testing.h | 317 + 293 files changed, 101139 insertions(+) create mode 100644 profinet_stack/p-net/.clang-format create mode 100644 profinet_stack/p-net/.git-blame-ignore-revs create mode 100644 profinet_stack/p-net/.gitattributes create mode 100644 profinet_stack/p-net/.github/workflows/build.yml create mode 100644 profinet_stack/p-net/.github/workflows/clang-analysis.yml create mode 100644 profinet_stack/p-net/.github/workflows/codeql-analysis.yml create mode 100644 profinet_stack/p-net/.gitignore create mode 100644 profinet_stack/p-net/.gitmodules create mode 100644 profinet_stack/p-net/CHANGELOG.md create mode 100644 profinet_stack/p-net/CMakeLists.txt create mode 100644 profinet_stack/p-net/LICENSE.md create mode 100644 profinet_stack/p-net/README.md create mode 100644 profinet_stack/p-net/cmake/FindNetSNMP.cmake create mode 100644 profinet_stack/p-net/cmake/FindNetSNMPAgent.cmake create mode 100644 profinet_stack/p-net/cmake/Linux.cmake create mode 100644 profinet_stack/p-net/cmake/STM32Cube.cmake create mode 100644 profinet_stack/p-net/cmake/rt-kernel.cmake create mode 100644 profinet_stack/p-net/doc/Makefile create mode 100644 profinet_stack/p-net/doc/abbreviations.rst create mode 100644 profinet_stack/p-net/doc/additional_linux_details.rst create mode 100644 profinet_stack/p-net/doc/api_documentation.rst create mode 100644 profinet_stack/p-net/doc/applications_and_porting.rst create mode 100644 profinet_stack/p-net/doc/capturing_packets.rst create mode 100644 profinet_stack/p-net/doc/codespell_ignore.txt create mode 100644 profinet_stack/p-net/doc/codesys_details.rst create mode 100644 profinet_stack/p-net/doc/compliancetest.rst create mode 100644 profinet_stack/p-net/doc/conf.py create mode 100644 profinet_stack/p-net/doc/creating_gsdml_files.rst create mode 100644 profinet_stack/p-net/doc/doc_requirements.txt create mode 100644 profinet_stack/p-net/doc/getting_started_LAN9662.rst create mode 100644 profinet_stack/p-net/doc/getting_started_freertos.rst create mode 100644 profinet_stack/p-net/doc/getting_started_rtkernel.rst create mode 100644 profinet_stack/p-net/doc/illustrations/AlarmStateMachines.drawio create mode 100644 profinet_stack/p-net/doc/illustrations/AlarmStateMachines.png create mode 100644 profinet_stack/p-net/doc/illustrations/Cyclecounter.png create mode 100644 profinet_stack/p-net/doc/illustrations/FlasherFunctionBlock.png create mode 100644 profinet_stack/p-net/doc/illustrations/NetworkLoad.png create mode 100644 profinet_stack/p-net/doc/illustrations/PlcProgramLadderLogic.png create mode 100644 profinet_stack/p-net/doc/illustrations/RaspberryPiLedButtons.jpg create mode 100644 profinet_stack/p-net/doc/illustrations/SimaticIoDevice.jpg create mode 100644 profinet_stack/p-net/doc/illustrations/TutorialOverview.drawio create mode 100644 profinet_stack/p-net/doc/illustrations/TutorialOverview.png create mode 100644 profinet_stack/p-net/doc/illustrations/periodicity.png create mode 100644 profinet_stack/p-net/doc/illustrations/proneta_example.png create mode 100644 profinet_stack/p-net/doc/illustrations/rdrecLadderlogic.png create mode 100644 profinet_stack/p-net/doc/implementation_details.rst create mode 100644 profinet_stack/p-net/doc/index.rst create mode 100644 profinet_stack/p-net/doc/linuxtiming.rst create mode 100644 profinet_stack/p-net/doc/multiple_ports.rst create mode 100644 profinet_stack/p-net/doc/network_topology_detection.rst create mode 100644 profinet_stack/p-net/doc/prepare_raspberrypi.rst create mode 100644 profinet_stack/p-net/doc/profinet_basics.rst create mode 100644 profinet_stack/p-net/doc/profinet_details.rst create mode 100644 profinet_stack/p-net/doc/sampleapp_details.rst create mode 100644 profinet_stack/p-net/doc/spelling_wordlist.txt create mode 100644 profinet_stack/p-net/doc/static/css/change_header_size.css create mode 100644 profinet_stack/p-net/doc/static/css/fix_table_width.css create mode 100644 profinet_stack/p-net/doc/tutorial.rst create mode 100644 profinet_stack/p-net/doc/use_with_siematic.rst create mode 100644 profinet_stack/p-net/doc/using_codesys.rst create mode 100644 profinet_stack/p-net/doc/writing_documentation.rst create mode 100644 profinet_stack/p-net/doc/yoctobuild.rst create mode 100644 profinet_stack/p-net/include/pnet_api.h create mode 100644 profinet_stack/p-net/options.h.in create mode 100644 profinet_stack/p-net/pnet_options.h.in create mode 100644 profinet_stack/p-net/pnet_version.h.in create mode 100644 profinet_stack/p-net/samples/pn_dev/CMakeLists.txt create mode 100644 profinet_stack/p-net/samples/pn_dev/GSDML-RT-LABS-STACK.bmp create mode 100644 profinet_stack/p-net/samples/pn_dev/GSDML-V2.4-RT-Labs-P-Net-Sample-App-20220324.xml create mode 100644 profinet_stack/p-net/samples/pn_dev/app_data.c create mode 100644 profinet_stack/p-net/samples/pn_dev/app_data.h create mode 100644 profinet_stack/p-net/samples/pn_dev/app_gsdml.c create mode 100644 profinet_stack/p-net/samples/pn_dev/app_gsdml.h create mode 100644 profinet_stack/p-net/samples/pn_dev/app_log.c create mode 100644 profinet_stack/p-net/samples/pn_dev/app_log.h create mode 100644 profinet_stack/p-net/samples/pn_dev/app_utils.c create mode 100644 profinet_stack/p-net/samples/pn_dev/app_utils.h create mode 100644 profinet_stack/p-net/samples/pn_dev/sampleapp_common.c create mode 100644 profinet_stack/p-net/samples/pn_dev/sampleapp_common.h create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/CMakeLists.txt create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/GSDML-V2.4-RT-Labs-P-Net-LAN9662-20220511.xml create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/S90_start_profinet.sh create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_data.c create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_data.h create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.c create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.h create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_log.c create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_log.h create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.c create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.h create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.c create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.h create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.c create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.h create mode 100644 profinet_stack/p-net/samples/pn_dev_lan9662/switchdev-profinet-example.sh create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/CMakeLists.txt create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/pn_shm_tool.c create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/shm_LED_handler.sh create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/shm_button_handler.sh create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/shm_echo_all.sh create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/shm_read_all.sh create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/shm_write_all_inputs.sh create mode 100644 profinet_stack/p-net/samples/pn_shm_tool/test.sh create mode 100644 profinet_stack/p-net/src/CMakeLists.txt create mode 100644 profinet_stack/p-net/src/common/pf_alarm.c create mode 100644 profinet_stack/p-net/src/common/pf_alarm.h create mode 100644 profinet_stack/p-net/src/common/pf_bg_worker.c create mode 100644 profinet_stack/p-net/src/common/pf_bg_worker.h create mode 100644 profinet_stack/p-net/src/common/pf_cpm.c create mode 100644 profinet_stack/p-net/src/common/pf_cpm.h create mode 100644 profinet_stack/p-net/src/common/pf_cpm_driver_sw.c create mode 100644 profinet_stack/p-net/src/common/pf_cpm_driver_sw.h create mode 100644 profinet_stack/p-net/src/common/pf_dcp.c create mode 100644 profinet_stack/p-net/src/common/pf_dcp.h create mode 100644 profinet_stack/p-net/src/common/pf_eth.c create mode 100644 profinet_stack/p-net/src/common/pf_eth.h create mode 100644 profinet_stack/p-net/src/common/pf_file.c create mode 100644 profinet_stack/p-net/src/common/pf_file.h create mode 100644 profinet_stack/p-net/src/common/pf_lldp.c create mode 100644 profinet_stack/p-net/src/common/pf_lldp.h create mode 100644 profinet_stack/p-net/src/common/pf_ppm.c create mode 100644 profinet_stack/p-net/src/common/pf_ppm.h create mode 100644 profinet_stack/p-net/src/common/pf_ppm_driver_sw.c create mode 100644 profinet_stack/p-net/src/common/pf_ppm_driver_sw.h create mode 100644 profinet_stack/p-net/src/common/pf_ptcp.c create mode 100644 profinet_stack/p-net/src/common/pf_ptcp.h create mode 100644 profinet_stack/p-net/src/common/pf_scheduler.c create mode 100644 profinet_stack/p-net/src/common/pf_scheduler.h create mode 100644 profinet_stack/p-net/src/common/pf_snmp.c create mode 100644 profinet_stack/p-net/src/common/pf_snmp.h create mode 100644 profinet_stack/p-net/src/common/pf_udp.c create mode 100644 profinet_stack/p-net/src/common/pf_udp.h create mode 100644 profinet_stack/p-net/src/device/pf_block_reader.c create mode 100644 profinet_stack/p-net/src/device/pf_block_reader.h create mode 100644 profinet_stack/p-net/src/device/pf_block_writer.c create mode 100644 profinet_stack/p-net/src/device/pf_block_writer.h create mode 100644 profinet_stack/p-net/src/device/pf_cmdev.c create mode 100644 profinet_stack/p-net/src/device/pf_cmdev.h create mode 100644 profinet_stack/p-net/src/device/pf_cmdmc.c create mode 100644 profinet_stack/p-net/src/device/pf_cmdmc.h create mode 100644 profinet_stack/p-net/src/device/pf_cmina.c create mode 100644 profinet_stack/p-net/src/device/pf_cmina.h create mode 100644 profinet_stack/p-net/src/device/pf_cmio.c create mode 100644 profinet_stack/p-net/src/device/pf_cmio.h create mode 100644 profinet_stack/p-net/src/device/pf_cmpbe.c create mode 100644 profinet_stack/p-net/src/device/pf_cmpbe.h create mode 100644 profinet_stack/p-net/src/device/pf_cmrdr.c create mode 100644 profinet_stack/p-net/src/device/pf_cmrdr.h create mode 100644 profinet_stack/p-net/src/device/pf_cmrpc.c create mode 100644 profinet_stack/p-net/src/device/pf_cmrpc.h create mode 100644 profinet_stack/p-net/src/device/pf_cmrpc_epm.c create mode 100644 profinet_stack/p-net/src/device/pf_cmrpc_epm.h create mode 100644 profinet_stack/p-net/src/device/pf_cmrpc_helpers.c create mode 100644 profinet_stack/p-net/src/device/pf_cmrpc_helpers.h create mode 100644 profinet_stack/p-net/src/device/pf_cmrs.c create mode 100644 profinet_stack/p-net/src/device/pf_cmrs.h create mode 100644 profinet_stack/p-net/src/device/pf_cmsm.c create mode 100644 profinet_stack/p-net/src/device/pf_cmsm.h create mode 100644 profinet_stack/p-net/src/device/pf_cmsu.c create mode 100644 profinet_stack/p-net/src/device/pf_cmsu.h create mode 100644 profinet_stack/p-net/src/device/pf_cmwrr.c create mode 100644 profinet_stack/p-net/src/device/pf_cmwrr.h create mode 100644 profinet_stack/p-net/src/device/pf_diag.c create mode 100644 profinet_stack/p-net/src/device/pf_diag.h create mode 100644 profinet_stack/p-net/src/device/pf_fspm.c create mode 100644 profinet_stack/p-net/src/device/pf_fspm.h create mode 100644 profinet_stack/p-net/src/device/pf_pdport.c create mode 100644 profinet_stack/p-net/src/device/pf_pdport.h create mode 100644 profinet_stack/p-net/src/device/pf_port.c create mode 100644 profinet_stack/p-net/src/device/pf_port.h create mode 100644 profinet_stack/p-net/src/device/pnet_api.c create mode 100644 profinet_stack/p-net/src/drivers/drivers.cmake create mode 100644 profinet_stack/p-net/src/drivers/lan9662/add_inbound_vcap_rule.sh create mode 100644 profinet_stack/p-net/src/drivers/lan9662/add_mera_lib.cmake create mode 100644 profinet_stack/p-net/src/drivers/lan9662/add_outbound_vcap_rule.sh create mode 100644 profinet_stack/p-net/src/drivers/lan9662/del_vcap_rule.sh create mode 100644 profinet_stack/p-net/src/drivers/lan9662/driver.cmake create mode 100644 profinet_stack/p-net/src/drivers/lan9662/include/driver_config.h create mode 100644 profinet_stack/p-net/src/drivers/lan9662/include/pnet_lan9662_api.h create mode 100644 profinet_stack/p-net/src/drivers/lan9662/pnet_driver_options.h.in create mode 100644 profinet_stack/p-net/src/drivers/lan9662/set_profinet_leds create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_cpm_driver_lan9662.c create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.c create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.h create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.c create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.h create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_ppm_driver_lan9662.c create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.c create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.h create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.c create mode 100644 profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.h create mode 100644 profinet_stack/p-net/src/pf_driver.h create mode 100644 profinet_stack/p-net/src/pf_includes.h create mode 100644 profinet_stack/p-net/src/pf_types.h create mode 100644 profinet_stack/p-net/src/pnal.h create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/pnal.c create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/pnal_config.h create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/pnal_eth.c create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/pnal_sys.h create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/pnal_udp.c create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/sampleapp_STM32F769I-DISCO.c create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/sampleapp_board.c create mode 100644 profinet_stack/p-net/src/ports/STM32Cube/sampleapp_main.c create mode 100644 profinet_stack/p-net/src/ports/linux/enable-rpi-gpio-pins.sh create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.h create mode 100644 profinet_stack/p-net/src/ports/linux/mib/system_mib.c create mode 100644 profinet_stack/p-net/src/ports/linux/mib/system_mib.h create mode 100644 profinet_stack/p-net/src/ports/linux/pnal.c create mode 100644 profinet_stack/p-net/src/ports/linux/pnal_config.h create mode 100644 profinet_stack/p-net/src/ports/linux/pnal_eth.c create mode 100644 profinet_stack/p-net/src/ports/linux/pnal_filetools.c create mode 100644 profinet_stack/p-net/src/ports/linux/pnal_filetools.h create mode 100644 profinet_stack/p-net/src/ports/linux/pnal_snmp.c create mode 100644 profinet_stack/p-net/src/ports/linux/pnal_sys.h create mode 100644 profinet_stack/p-net/src/ports/linux/pnal_udp.c create mode 100644 profinet_stack/p-net/src/ports/linux/pnet-sampleapp.service create mode 100644 profinet_stack/p-net/src/ports/linux/sampleapp_main.c create mode 100644 profinet_stack/p-net/src/ports/linux/set_network_parameters create mode 100644 profinet_stack/p-net/src/ports/linux/set_profinet_leds create mode 100644 profinet_stack/p-net/src/ports/linux/set_profinet_leds.raspberrypi create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/0001-rtkernel-for-Profinet.patch create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/pnal.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/pnal_config.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/pnal_eth.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/pnal_sys.h create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/pnal_udp.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/sampleapp_bsp.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/sampleapp_f769disco.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/sampleapp_main.c create mode 100644 profinet_stack/p-net/src/ports/rt-kernel/sampleapp_xmc48relax.c create mode 100644 profinet_stack/p-net/test/CMakeLists.txt create mode 100644 profinet_stack/p-net/test/mocks.cpp create mode 100644 profinet_stack/p-net/test/mocks.h create mode 100644 profinet_stack/p-net/test/pf_test.cpp create mode 100644 profinet_stack/p-net/test/test_alarm.cpp create mode 100644 profinet_stack/p-net/test/test_block_reader.cpp create mode 100644 profinet_stack/p-net/test/test_cmdev.cpp create mode 100644 profinet_stack/p-net/test/test_cmdmc.cpp create mode 100644 profinet_stack/p-net/test/test_cmina.cpp create mode 100644 profinet_stack/p-net/test/test_cmio.cpp create mode 100644 profinet_stack/p-net/test/test_cmpbe.cpp create mode 100644 profinet_stack/p-net/test/test_cmrdr.cpp create mode 100644 profinet_stack/p-net/test/test_cmrpc.cpp create mode 100644 profinet_stack/p-net/test/test_cmrs.cpp create mode 100644 profinet_stack/p-net/test/test_cmsm.cpp create mode 100644 profinet_stack/p-net/test/test_cmsu.cpp create mode 100644 profinet_stack/p-net/test/test_cmwdr.cpp create mode 100644 profinet_stack/p-net/test/test_cpm.cpp create mode 100644 profinet_stack/p-net/test/test_dcp.cpp create mode 100644 profinet_stack/p-net/test/test_diag.cpp create mode 100644 profinet_stack/p-net/test/test_eth.cpp create mode 100644 profinet_stack/p-net/test/test_file.cpp create mode 100644 profinet_stack/p-net/test/test_fspm.cpp create mode 100644 profinet_stack/p-net/test/test_lldp.cpp create mode 100644 profinet_stack/p-net/test/test_pnetapi.cpp create mode 100644 profinet_stack/p-net/test/test_port.cpp create mode 100644 profinet_stack/p-net/test/test_ppm.cpp create mode 100644 profinet_stack/p-net/test/test_ptcp.cpp create mode 100644 profinet_stack/p-net/test/test_scheduler.cpp create mode 100644 profinet_stack/p-net/test/test_snmp.cpp create mode 100644 profinet_stack/p-net/test/utils_for_testing.cpp create mode 100644 profinet_stack/p-net/test/utils_for_testing.h diff --git a/profinet_stack/p-net/.clang-format b/profinet_stack/p-net/.clang-format new file mode 100644 index 0000000..05e2731 --- /dev/null +++ b/profinet_stack/p-net/.clang-format @@ -0,0 +1,41 @@ +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveMacros: true +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: false + AfterExternBlock: false +ColumnLimit: 80 +ContinuationIndentWidth: 3 +IndentCaseLabels: false +IndentWidth: 3 +PenaltyBreakAssignment: 10 +PenaltyBreakBeforeFirstCallParameter: 30 +PenaltyBreakComment: 10 +PenaltyBreakString: 100 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 100000 +PointerAlignment: Middle +SortIncludes: false +SpaceBeforeParens: NonEmptyParentheses +UseTab: Never diff --git a/profinet_stack/p-net/.git-blame-ignore-revs b/profinet_stack/p-net/.git-blame-ignore-revs new file mode 100644 index 0000000..845f3e7 --- /dev/null +++ b/profinet_stack/p-net/.git-blame-ignore-revs @@ -0,0 +1,12 @@ +# This file contains a list of commits that are not likely what you +# are looking for in a blame, such as mass reformatting or renaming. +# You can set this file as a default ignore file for blame by running +# the following command. +# +# $ git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Run clang-format +723c3d2ea620328554e3f4ca6718d7359a807bdf + +# Clang format of earlier modifications +71e73f071cb6c42a3c8ec8a209e7e979a43cce5a diff --git a/profinet_stack/p-net/.gitattributes b/profinet_stack/p-net/.gitattributes new file mode 100644 index 0000000..75d1ad5 --- /dev/null +++ b/profinet_stack/p-net/.gitattributes @@ -0,0 +1,24 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Do not perform LF normalization on bash scripts +*.service text eol=lf +*.raspberrypi text eol=lf +*.sh text eol=lf +src/ports/linux/set_network_parameters text eol=lf +src/ports/linux/set_profinet_leds text eol=lf + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/profinet_stack/p-net/.github/workflows/build.yml b/profinet_stack/p-net/.github/workflows/build.yml new file mode 100644 index 0000000..21bcce8 --- /dev/null +++ b/profinet_stack/p-net/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build +on: [push, pull_request] +env: + BUILD_TYPE: Release +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - name: Apt update + run: sudo apt-get update + + - name: Install dependencies + run: sudo apt-get install doxygen libsnmp-dev + + - name: Configure + shell: bash + run: | + cmake -E make_directory ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -S ${{github.workspace}} \ + -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ + -DPNET_OPTION_SNMP=ON + + - name: Build + shell: bash + run: | + cmake --build ${{github.workspace}}/build --target all docs install -j4 + + - name: Test + shell: bash + run: | + cmake --build ${{github.workspace}}/build --target check diff --git a/profinet_stack/p-net/.github/workflows/clang-analysis.yml b/profinet_stack/p-net/.github/workflows/clang-analysis.yml new file mode 100644 index 0000000..590a62b --- /dev/null +++ b/profinet_stack/p-net/.github/workflows/clang-analysis.yml @@ -0,0 +1,69 @@ +name: Clang + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '23 13 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-20.04 + + steps: + - name: Install dependencies + run: sudo apt-get install clang-tools libsnmp-dev + + - name: Checkout repository + uses: actions/checkout@v2 + with: + submodules: true + + # Run clang analyzer. + # + # Detect if issues were found and upload SARIF report only in that + # case, because reports without entries are not accepted. See + # https://github.com/github/codeql-action/issues/390. The analyzer + # is run twice in order to get a SARIF report as well as an exit + # status when issues are detected, as the scan-build --status-bugs + # parameter doesn't work when the output format is SARIF. + + - name: Run analyzer + id: analyze + continue-on-error: true + run: | + cmake -E make_directory ${{github.workspace}}/build.clang + scan-build cmake \ + -B ${{github.workspace}}/build.clang \ + -S ${{github.workspace}} \ + -DPNET_OPTION_SNMP=ON \ + -DBUILD_TESTING=OFF \ + -DCMAKE_BUILD_TYPE=Release + scan-build \ + -sarif \ + -o ${{github.workspace}}/sarif \ + make \ + -C ${{github.workspace}}/build.clang \ + -j4 + cmake --build ${{github.workspace}}/build.clang --target clean + scan-build \ + --status-bugs \ + make \ + -C ${{github.workspace}}/build.clang \ + -j4 + + - name: Merge results if any + if: steps.analyze.outcome == 'failure' + uses: microsoft/sarif-actions@v0.1 + with: + command: 'merge sarif/*/*.sarif' + + - name: Upload results if any + if: steps.analyze.outcome == 'failure' + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: ${{github.workspace}}/merged.sarif diff --git a/profinet_stack/p-net/.github/workflows/codeql-analysis.yml b/profinet_stack/p-net/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..1965b90 --- /dev/null +++ b/profinet_stack/p-net/.github/workflows/codeql-analysis.yml @@ -0,0 +1,46 @@ +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '23 13 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + submodules: true + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + - name: Build + run: | + cmake -E make_directory ${{github.workspace}}/build.lgtm + cmake -B ${{github.workspace}}/build.lgtm -S ${{github.workspace}} \ + -DCMAKE_VERBOSE_MAKEFILE=ON \ + -DCMAKE_BUILD_TYPE=Release + cmake --build ${{github.workspace}}/build.lgtm -j4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/profinet_stack/p-net/.gitignore b/profinet_stack/p-net/.gitignore new file mode 100644 index 0000000..f783b9b --- /dev/null +++ b/profinet_stack/p-net/.gitignore @@ -0,0 +1,53 @@ +build*/ +*~ +*# +CMakeFiles/ +CMakeCache.txt +install +doc/_build +doc/_copied + +.cproject +.project + +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Editor files +.vscode/ + +# Sampleapp dummy LED files +pnet_led*.txt + +# Sampleapp dummy button files (typically named by user) +*button*.txt + +# Persistent data storage +pnet_data*.bin diff --git a/profinet_stack/p-net/.gitmodules b/profinet_stack/p-net/.gitmodules new file mode 100644 index 0000000..956a252 --- /dev/null +++ b/profinet_stack/p-net/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cmake/tools"] + path = cmake/tools + url = https://github.com/rtlabs-com/cmake-tools diff --git a/profinet_stack/p-net/CHANGELOG.md b/profinet_stack/p-net/CHANGELOG.md new file mode 100644 index 0000000..922d325 --- /dev/null +++ b/profinet_stack/p-net/CHANGELOG.md @@ -0,0 +1,125 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), + +## [Unreleased] + +## 2021-03-31 +### Changed +- Update GSDML file to conformance class B + +## 2021-03-24 +### Added +- Possibility to configure number of ports, without recompiling. + +## 2021-03-03 +### Changed +- Set thread priority and stack size via configuration + +## 2021-02-19 +### Changed +- Split options.h into two files. +- Simplify configuration + +## 2020-02-03 +### Added + - Improved support for multiple ports + +## 2021-01-29 +### Changed + - Move remaining Linux sample app files to src/ports/linux + +## 2020-01-11 +### Added + - Support for multiple ports + +## 2021-01-05 +### Added + - Additional API functions for low-level diagnostic handling + +## 2020-12-22 +### Changed + - Remove link status from configuration + +## 2020-12-15 +### Changed + - Use Profinet version 2.4 for the GSDML file + - Changed default installation path for sample_app on Raspberry Pi + +## 2020-12-11 +### Added + - Runtime data for multiple ports + - Functionality to search for scripts in multiple locations for Linux + +## 2020-12-07 +### Changed + - Rename pnet_set_state() to pnet_set_primary_state() + +## 2020-12-01 +### Added +- Changed informative callbacks to be void-functions. + +## 2020-11-30 +### Added +- SNMP functionality for Linux + +## 2020-11-20 +### Added +- Additional log level FATAL + +## 2020-11-19 +### Added +- SNMP functionality for rt-kernel + +## 2020-11-16 +### Added +- Support for multiple ports in configuration + +## 2020-11-10 +### Changed +- Remove LLDP TTL from configuration +- Remove LLDP chassis ID from configuration + +## 2020-11-03 +### Changed +- Improve API for adding diagnosis +- Use rtlabs-com/osal and rtlabs-com/cmake-tools as submodules + +## 2020-10-28 +### Changed +- Rename compile time constants for number of slots and subslots + +## 2020-09-19 +### Changed +- Modified public API for sending alarm ACK. + +## 2020-08-24 +### Changed +- Renamed configuration members related to I&M data + +## 2020-08-18 +### Added +- Add min_device_interval to the configuration +- Use cmake to set configurable build options + +## 2020-06-15 +### Added +- New user callback for LED state change. + +## 2020-06-11 +### Added +- New API function for application to perform a factory reset + +## 2020-04-09 +### Added +- New user callback to indicate reset requests from the IO-controller. + +## 2020-04-06 +### Changed +- Changed the user API to use a handle to the p-net stack, and to allow user + arguments to callbacks. + +## 2020-03-27 +### Changed +- Read MAC address also from Linux hardware. diff --git a/profinet_stack/p-net/CMakeLists.txt b/profinet_stack/p-net/CMakeLists.txt new file mode 100644 index 0000000..27c0e8d --- /dev/null +++ b/profinet_stack/p-net/CMakeLists.txt @@ -0,0 +1,265 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2018 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +cmake_minimum_required (VERSION 3.14) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/tools") +project (PROFINET VERSION 0.2.0) + +# Default settings if this is the main project +if (CMAKE_PROJECT_NAME STREQUAL PROFINET) + include(CTest) + + # Make option visible in ccmake, cmake-gui + option (BUILD_SHARED_LIBS "Build shared library" OFF) + + # Default to release build with debug info + if (NOT CMAKE_BUILD_TYPE) + set (CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING + "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel." + FORCE) + endif (NOT CMAKE_BUILD_TYPE) + + # Default to installing in build directory + if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX ${PROFINET_BINARY_DIR}/install + CACHE PATH "Default install path" FORCE) + endif() + + message(STATUS "Current build type is: ${CMAKE_BUILD_TYPE}") + message(STATUS "Current install path is: ${CMAKE_INSTALL_PREFIX}") + message(STATUS "Building for ${CMAKE_SYSTEM_NAME}") +endif() + +include(AddOsal) +include(GenerateExportHeader) +include(CMakeDependentOption) +include(GetGitRevision) + +# Always use standard .o suffix +set(CMAKE_C_OUTPUT_EXTENSION_REPLACE 1) +set(CMAKE_CXX_OUTPUT_EXTENSION_REPLACE 1) + +# Options. See options.h.in for more information. + +option (PNET_OPTION_FAST_STARTUP "" ON) +option (PNET_OPTION_PARAMETER_SERVER "" ON) +option (PNET_OPTION_IR "" ON) +option (PNET_OPTION_SR "" ON) +option (PNET_OPTION_REDUNDANCY "" ON) +option (PNET_OPTION_AR_VENDOR_BLOCKS "" ON) +option (PNET_OPTION_RS "" ON) +option (PNET_OPTION_MC_CR "" ON) +option (PNET_OPTION_SRL "" OFF) +option (PNET_OPTION_SNMP "" OFF) +option (PNET_OPTION_DRIVER_ENABLE "Enable drivers. Specific driver must be enabled." OFF ) + +# TODO: this should be handled in cc.h +option (PNET_USE_ATOMICS "Enable use of atomic operations (stdatomic.h)" OFF) + +set(PNET_MAX_AR 2 + CACHE STRING "Number of connections. Must be > 0. 'Automated RT Tester' uses 2, but only 1 connection AR is supported.") +set(PNET_MAX_API 1 + CACHE STRING "Number of Application Processes. Must be > 0") +set(PNET_MAX_CR 2 + CACHE STRING "Per AR. At least 2 (1 input and 1 output). If unsure, use 2.") +set(PNET_MAX_SLOTS 5 + CACHE STRING "Per API. Should be > 1 to allow at least one I/O module") +set(PNET_MAX_SUBSLOTS 3 + CACHE STRING "Per slot (DAP requires 2 + PNET_MAX_PHYSICAL_PORTS)") +set(PNET_MAX_DFP_IOCR 2 + CACHE STRING "Allowed values are 0 (zero) or 2") +set(PNET_MAX_PHYSICAL_PORTS 1 + CACHE STRING "Max number of physical ports") +set(PNET_MAX_LOG_BOOK_ENTRIES 16 + CACHE STRING "") +set(PNET_MAX_ALARMS 6 + CACHE STRING "Per AR and queue. One queue for high and one for low priority alarms. 'Automated RT Tester' uses 6.") +set(PNET_MAX_ALARM_PAYLOAD_DATA_SIZE 28 + CACHE STRING "Min 24 or PNET_MAX_DIAG_MANUF_DATA_SIZE + 12. Max is 1408") +set(PNET_MAX_DIAG_ITEMS 200 + CACHE STRING "Total, per device. Max is 65534 items") +set(PNET_MAX_DIAG_MANUF_DATA_SIZE 16 + CACHE STRING "Min 5 for tests. Max is 1396") +set(PNET_MAX_MC_CR 1 + CACHE STRING "Per AR") +set(PNET_MAX_AR_VENDOR_BLOCKS 1 + CACHE STRING "Must be > 0") +set(PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH 512 + CACHE STRING "") +set(PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH 0 + CACHE STRING "or 512 (bytes") +set(PNET_MAX_SESSION_BUFFER_SIZE 4500 + CACHE STRING "Max fragmented RPC request/response length. Max value 65535") +set(PNET_MAX_DIRECTORYPATH_SIZE 240 + CACHE STRING "Max size of directory path, including termination") +set(PNET_MAX_FILENAME_SIZE 30 + CACHE STRING "Max size of filename, including termination") +set(PNET_MAX_PORT_DESCRIPTION_SIZE 60 + CACHE STRING "Max size of port description, including termination") + +set(LOG_STATE_VALUES "ON;OFF") +set(LOG_LEVEL_VALUES "DEBUG;INFO;WARNING;ERROR;FATAL") + +set(LOG_LEVEL FATAL CACHE STRING "default log level") +set_property(CACHE LOG_LEVEL PROPERTY STRINGS ${LOG_LEVEL_VALUES}) + +set(PF_ETH_LOG ON CACHE STRING "pf_eth log") +set_property(CACHE PF_ETH_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_LLDP_LOG ON CACHE STRING "pf_lldp log") +set_property(CACHE PF_LLDP_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_SNMP_LOG ON CACHE STRING "pf_snmp log") +set_property(CACHE PF_SNMP_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_CPM_LOG ON CACHE STRING "pf_cpm log") +set_property(CACHE PF_CPM_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_PPM_LOG ON CACHE STRING "pf_ppm log") +set_property(CACHE PF_PPM_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_DCP_LOG ON CACHE STRING "pf_dcp log") +set_property(CACHE PF_DCP_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_RPC_LOG ON CACHE STRING "pf_rpc log") +set_property(CACHE PF_RPC_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_ALARM_LOG ON CACHE STRING "pf_alarm log") +set_property(CACHE PF_ALARM_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_AL_BUF_LOG ON CACHE STRING "pf_alarm_buffer log") +set_property(CACHE PF_AL_BUF_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PF_PNAL_LOG ON CACHE STRING "PNAL log") +set_property(CACHE PF_PNAL_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +set(PNET_LOG ON CACHE STRING "PNET log") +set_property(CACHE PNET_LOG PROPERTY STRINGS ${LOG_STATE_VALUES}) + +# Generate version numbers +configure_file ( + pnet_version.h.in + ${PROFINET_BINARY_DIR}/include/pnet_version.h + ) + +# Generate config options +configure_file ( + options.h.in + ${PROFINET_BINARY_DIR}/src/options.h + ) +configure_file ( + pnet_options.h.in + ${PROFINET_BINARY_DIR}/include/pnet_options.h + ) + +# Add platform-dependent targets early, so they can be configured by +# platform +add_library(profinet "") +add_executable(pn_dev "") + +if (PNET_OPTION_DRIVER_ENABLE) + include(${CMAKE_CURRENT_SOURCE_DIR}/src/drivers/drivers.cmake) +endif () + +if (CMAKE_PROJECT_NAME STREQUAL PROFINET AND BUILD_TESTING) + add_executable(pf_test "") +endif() + +# Platform configuration +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/${CMAKE_SYSTEM_NAME}.cmake) + +generate_export_header(profinet + BASE_NAME pnet + EXPORT_FILE_NAME ${PROFINET_BINARY_DIR}/include/pnet_export.h + ) + +set_target_properties (profinet pn_dev + PROPERTIES + C_STANDARD 99 + ) + +target_compile_features(profinet PUBLIC c_std_99) + +target_include_directories(profinet + PUBLIC + $ + $ + $ + PRIVATE + ${PROFINET_BINARY_DIR}/src + src + src/common + src/device + ) + +target_link_libraries(profinet PUBLIC osal) + +install ( + TARGETS profinet + EXPORT ProfinetConfig + DESTINATION lib + ) + +install( + EXPORT ProfinetConfig + DESTINATION cmake + ) + +install (FILES + include/pnet_api.h + ${PROFINET_BINARY_DIR}/include/pnet_export.h + ${PROFINET_BINARY_DIR}/include/pnet_options.h + ${PROFINET_BINARY_DIR}/include/pnet_version.h + DESTINATION include + ) + +add_subdirectory (src) +add_subdirectory (samples/pn_dev) + +if (CMAKE_PROJECT_NAME STREQUAL PROFINET AND BUILD_TESTING) + add_subdirectory (test) + include(AddGoogleTest) + add_gtest(pf_test) +endif() + +# Doxygen configuration +cmake_policy(SET CMP0057 NEW) +find_package(Doxygen) +if (CMAKE_PROJECT_NAME STREQUAL PROFINET AND DOXYGEN_FOUND) + set(DOXYGEN_OPTIMIZE_OUTPUT_FOR_C YES) + set(DOXYGEN_TYPEDEF_HIDES_STRUCT YES) + set(DOXYGEN_STRIP_CODE_COMMENTS NO) + set(DOXYGEN_MACRO_EXPANSION YES) + set(DOXYGEN_EXPAND_ONLY_PREDEF YES) + set(DOXYGEN_PREDEFINED PNET_EXPORT) + set(DOXYGEN_EXPAND_AS_DEFINED PNET_EXPORT) + set(DOXYGEN_COLLABORATION_GRAPH NO) + set(DOXYGEN_INCLUDE_GRAPH NO) + set(DOXYGEN_INCLUDED_BY_GRAPH NO) + set(DOXYGEN_RECURSIVE YES) + set(DOXYGEN_GENERATE_XML YES) + set(DOXYGEN_USE_MDFILE_AS_MAINPAGE README.md) + doxygen_add_docs(docs + README.md + include + src + ) +endif() + +include (InstallRequiredSystemLibraries) +set (CPACK_RESOURCE_FILE_LICENSE "${PROFINET_SOURCE_DIR}/LICENSE.md") +set (CPACK_PACKAGE_CONTACT info.profinet@rt-labs.com) +include (CPack) diff --git a/profinet_stack/p-net/LICENSE.md b/profinet_stack/p-net/LICENSE.md new file mode 100644 index 0000000..9e3de68 --- /dev/null +++ b/profinet_stack/p-net/LICENSE.md @@ -0,0 +1,23 @@ +# License + +This software is dual-licensed. + +## GPL version 3 + +This software is distributed under GPLv3. You are allowed to use this +software for an open-source project with a compatible license. + +Use of IPC interface(s) provided by the project is understood to be +ordinary use, not within the meaning of "covered work" as defined in +the GPLv3 license. + +[GNU GPL license v3](https://www.gnu.org/licenses/gpl-3.0.html) + +## Commercial license + +This software is also available under a commercial license with +options for support and maintenance. Please contact sales@rt-labs.com +for further details. + +If you intend to use this stack in a commercial product, you likely need to +buy a license. diff --git a/profinet_stack/p-net/README.md b/profinet_stack/p-net/README.md new file mode 100644 index 0000000..89e773f --- /dev/null +++ b/profinet_stack/p-net/README.md @@ -0,0 +1,147 @@ +p-net Profinet device stack +=========================== + +Web resources +------------- + +* Source repository: [https://github.com/rtlabs-com/p-net](https://github.com/rtlabs-com/p-net) +* Documentation: [https://rt-labs.com/docs/p-net](https://rt-labs.com/docs/p-net) +* Continuous integration: [https://github.com/rtlabs-com/p-net/actions](https://github.com/rtlabs-com/p-net/actions) +* RT-Labs (stack integration, certification services and training): [https://rt-labs.com](https://rt-labs.com) + + +[![Build Status](https://github.com/rtlabs-com/p-net/workflows/Build/badge.svg?branch=master)](https://github.com/rtlabs-com/p-net/actions?workflow=Build) +[![CodeQL](https://github.com/rtlabs-com/p-net/workflows/CodeQL/badge.svg?branch=master)](https://github.com/rtlabs-com/p-net/actions?workflow=CodeQL) + +p-net +----- +Profinet device stack implementation. Key features: +* Profinet v2.4 + * Conformance Class A and B + * Real Time Class 1 + * Multiple Ethernet ports +* Easy to use + * Extensive documentation and instructions on how to get started. + * Build and run sample application on Raspberry Pi in 30 minutes. +* Portable + * Written in C. + * Linux, RTOS or bare metal. + * Sources for supported port layers provided. + +The RT-Labs Profinet stack p-net is used for Profinet device +implementations. It is easy to use and provides a small footprint. It +is especially well suited for embedded systems where resources are +limited and efficiency is crucial. +The stack is supplied with full sources including porting +layers and a sample application. + +Also C++ (any version) is supported for application development. + +The main requirement on the platform +is that it can send and receive raw Ethernet Layer 2 frames. + +Features: + * Multiple Ethernet ports (for Linux only, so far) + * TCP/IP + * LLDP + * SNMP + * RT (real-time) + * Address resolution + * Parameterization + * Process IO data exchange + * Alarm handling + * Configurable number of modules and sub-modules + * Bare-metal or OS + * Porting layer provided + * Supports I&M0 - I&M4. The I&M data is supported for the device, but not for + individual modules. + +Limitations or not yet implemented: + +* This is a device stack, which means that the IO-controller/master/PLC side is + not supported. +* No media redundancy (No MRP support) +* Legacy startup mode is not fully implemented +* No support for RT_CLASS_UDP +* No support for DHCP +* No fast start-up +* No MC multicast device-to-device +* No support of shared device (connection to multiple controllers) +* Supports only full connections, not the limited "DeviceAccess" connection type. +* No iPar (parameter server) support +* No support for time synchronization +* No UDP frames at alarm (just the default alarm mechanism is implemented) +* No ProfiDrive or ProfiSafe profiles. + +This software is dual-licensed, with GPL version 3 and a commercial license. +If you intend to use this stack in a commercial product, you likely need to +buy a license. See LICENSE.md for more details. + + +Requirements +------------ +The platform must be able to send and receive raw Ethernet Layer 2 frames, +and the Ethernet driver must be able to handle full size frames. It +should also avoid copying data, for performance reasons. + +* cmake 3.14 or later + +For Linux: + +* gcc 4.6 or later +* See the "Real-time properties of Linux" page in the documentation on how to + improve Linux timing + +For rt-kernel: + +* Workbench 2020.1 or later + +An example of microcontroller we have been using is the Infineon XMC4800, +which has an ARM Cortex-M4 running at 144 MHz, with 2 MB Flash and 352 kB RAM. +It runs rt-kernel, and we have tested it with 9 Profinet slots each +having 8 digital inputs and 8 digital outputs (one bit each). The values are +sent and received each millisecond (PLC watchdog setting 3 ms). + + +Getting started +--------------- +See the tutorial in the documentation: [https://rt-labs.com/docs/p-net/tutorial.html](https://rt-labs.com/docs/p-net/tutorial.html) + +Note that you need to include submodules when cloning: + +``` +git clone --recurse-submodules https://github.com/rtlabs-com/p-net.git +``` + +Dependencies +------------ +Some of the platform-dependent parts are located in the OSAL repository and the +cmake-tools repository. + +* [https://github.com/rtlabs-com/osal](https://github.com/rtlabs-com/osal) +* [https://github.com/rtlabs-com/cmake-tools](https://github.com/rtlabs-com/cmake-tools) + +Those are downloaded automatically during install. + +The p-net stack contains no third party components. Its external dependencies are: + +* C-library +* An operating system (if used) +* For conformance class B you need an SNMP implementation. On Linux is + net-snmp (BSD License) used [http://www.net-snmp.org](http://www.net-snmp.org) + +Tools used for building, testing and documentation (not shipped in the resulting binaries): + +* cmake (BSD 3-clause License) [https://cmake.org](https://cmake.org) +* gtest (BSD-3-Clause License) [https://github.com/google/googletest](https://github.com/google/googletest) +* Sphinx (BSD license) [https://www.sphinx-doc.org](https://www.sphinx-doc.org) +* Doxygen (GPL v2) [https://www.doxygen.nl](https://www.doxygen.nl/index.html) +* clang-format (Apache License 2.0) [https://clang.llvm.org](https://clang.llvm.org/docs/ClangFormat.html) + + +Contributions +-------------- +Contributions are welcome. If you want to contribute you will need to +sign a Contributor License Agreement and send it to us either by +e-mail or by physical mail. More information is available +on [https://rt-labs.com/contribution](https://rt-labs.com/contribution). diff --git a/profinet_stack/p-net/cmake/FindNetSNMP.cmake b/profinet_stack/p-net/cmake/FindNetSNMP.cmake new file mode 100644 index 0000000..90460df --- /dev/null +++ b/profinet_stack/p-net/cmake/FindNetSNMP.cmake @@ -0,0 +1,37 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2020 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +include(FindPackageHandleStandardArgs) + +# Find Net-SNMP + +find_path(NetSNMP_INCLUDE_DIR net-snmp-includes.h + PATH_SUFFIXES net-snmp + ) +find_library(NetSNMP_LIBRARY netsnmp) +mark_as_advanced(NetSNMP_INCLUDE_DIR NetSNMP_LIBRARY) + +find_package_handle_standard_args(NetSNMP + REQUIRED_VARS NetSNMP_LIBRARY NetSNMP_INCLUDE_DIR + ) + +if (NetSNMP_FOUND AND NOT TARGET NetSNMP::NetSNMP) + add_library(NetSNMP::NetSNMP UNKNOWN IMPORTED) + set_target_properties(NetSNMP::NetSNMP PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION "${NetSNMP_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${NetSNMP_INCLUDE_DIRS}" + ) +endif() diff --git a/profinet_stack/p-net/cmake/FindNetSNMPAgent.cmake b/profinet_stack/p-net/cmake/FindNetSNMPAgent.cmake new file mode 100644 index 0000000..d2daedd --- /dev/null +++ b/profinet_stack/p-net/cmake/FindNetSNMPAgent.cmake @@ -0,0 +1,37 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2020 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +include(FindPackageHandleStandardArgs) + +# Find Net-SNMP agent + +find_path(NetSNMPAgent_INCLUDE_DIR net-snmp-agent-includes.h + PATH_SUFFIXES net-snmp/agent + ) +find_library(NetSNMPAgent_LIBRARY netsnmpagent) +mark_as_advanced(NetSNMPAgent_INCLUDE_DIR NetSNMPAgent_LIBRARY) + +find_package_handle_standard_args(NetSNMPAgent + REQUIRED_VARS NetSNMPAgent_LIBRARY NetSNMPAgent_INCLUDE_DIR + ) + +if (NetSNMPAgent_FOUND AND NOT TARGET NetSNMP::NetSNMPAgent) + add_library(NetSNMP::NetSNMPAgent UNKNOWN IMPORTED) + set_target_properties(NetSNMP::NetSNMPAgent PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION "${NetSNMPAgent_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${NetSNMPAgent_INCLUDE_DIRS}" + ) +endif() diff --git a/profinet_stack/p-net/cmake/Linux.cmake b/profinet_stack/p-net/cmake/Linux.cmake new file mode 100644 index 0000000..2a19f88 --- /dev/null +++ b/profinet_stack/p-net/cmake/Linux.cmake @@ -0,0 +1,116 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2018 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +if (PNET_OPTION_SNMP) + find_package(NetSNMP REQUIRED) + find_package(NetSNMPAgent REQUIRED) +endif() + +target_include_directories(profinet + PRIVATE + src/ports/linux + ) + +target_sources(profinet + PRIVATE + src/ports/linux/pnal.c + src/ports/linux/pnal_eth.c + src/ports/linux/pnal_udp.c + src/ports/linux/pnal_filetools.c + $<$:src/ports/linux/pnal_snmp.c> + $<$:src/ports/linux/mib/system_mib.c> + $<$:src/ports/linux/mib/lldpLocalSystemData.c> + $<$:src/ports/linux/mib/lldpLocPortTable.c> + $<$:src/ports/linux/mib/lldpConfigManAddrTable.c> + $<$:src/ports/linux/mib/lldpLocManAddrTable.c> + $<$:src/ports/linux/mib/lldpRemTable.c> + $<$:src/ports/linux/mib/lldpRemManAddrTable.c> + $<$:src/ports/linux/mib/lldpXdot3LocPortTable.c> + $<$:src/ports/linux/mib/lldpXdot3RemPortTable.c> + $<$:src/ports/linux/mib/lldpXPnoLocTable.c> + $<$:src/ports/linux/mib/lldpXPnoRemTable.c> + ) + +target_compile_options(profinet + PRIVATE + -Wall + -Wextra + -Werror + -Wno-unused-parameter + -ffunction-sections + -fdata-sections + INTERFACE + $<$:--coverage> + ) + +target_link_libraries(profinet + PUBLIC + $<$:NetSNMP::NetSNMPAgent> + $<$:NetSNMP::NetSNMP> + INTERFACE + $<$:--coverage> + ) + +target_include_directories(pn_dev + PRIVATE + samples/pn_dev + src/ports/linux + ) + +target_sources(pn_dev + PRIVATE + samples/pn_dev/sampleapp_common.c + samples/pn_dev/app_utils.c + samples/pn_dev/app_log.c + samples/pn_dev/app_gsdml.c + samples/pn_dev/app_data.c + src/ports/linux/sampleapp_main.c + ) + +target_compile_options(pn_dev + PRIVATE + -Wall + -Wextra + -Werror + -Wno-unused-parameter + -ffunction-sections + -fdata-sections + ) + +target_link_options(pn_dev + PRIVATE + -Wl,--gc-sections +) + +install (FILES + src/ports/linux/pnal_config.h + DESTINATION include + ) + +file(COPY + src/ports/linux/set_network_parameters + src/ports/linux/set_profinet_leds + src/ports/linux/set_profinet_leds.raspberrypi + DESTINATION + ${PROFINET_BINARY_DIR}/ + ) + +if (BUILD_TESTING) + set(GOOGLE_TEST_INDIVIDUAL TRUE) + target_include_directories(pf_test + PRIVATE + src/ports/linux + ) +endif() diff --git a/profinet_stack/p-net/cmake/STM32Cube.cmake b/profinet_stack/p-net/cmake/STM32Cube.cmake new file mode 100644 index 0000000..cf21b0c --- /dev/null +++ b/profinet_stack/p-net/cmake/STM32Cube.cmake @@ -0,0 +1,83 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2021 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +target_include_directories(profinet + PRIVATE + src/ports/STM32Cube + ) + +target_sources(profinet + PRIVATE + src/ports/STM32Cube/pnal.c + src/ports/STM32Cube/pnal_eth.c + src/ports/STM32Cube/pnal_udp.c + ) + +target_compile_options(profinet + PRIVATE + -Wall + -Wextra + -Werror + -Wno-unused-parameter + ) + +target_include_directories(pn_dev + PRIVATE + samples/pn_dev + src/ports/STM32Cube + ) + +if (EXISTS ${PROFINET_SOURCE_DIR}/src/ports/STM32Cube/sampleapp_${BOARD}.c) + set(BOARD_SOURCE sampleapp_${BOARD}.c) +else() + set(BOARD_SOURCE sampleapp_board.c) +endif() + +target_sources(pn_dev + PRIVATE + samples/pn_dev/sampleapp_common.c + samples/pn_dev/app_utils.c + samples/pn_dev/app_log.c + samples/pn_dev/app_gsdml.c + samples/pn_dev/app_data.c + src/ports/STM32Cube/sampleapp_main.c + src/ports/STM32Cube/${BOARD_SOURCE} + ) + +target_compile_options(pn_dev + PRIVATE + -Wall + -Wextra + -Werror + -Wno-unused-parameter + ) + +target_link_libraries(pn_dev PRIVATE cube-bsp) + +install (FILES + src/ports/STM32Cube/pnal_config.h + DESTINATION include + ) + +generate_bin(pn_dev) + +if (BUILD_TESTING) + target_include_directories(pf_test + PRIVATE + src/ports/STM32Cube + ) + target_link_libraries(pf_test PRIVATE cube-bsp) + generate_bin(pf_test) +endif() diff --git a/profinet_stack/p-net/cmake/rt-kernel.cmake b/profinet_stack/p-net/cmake/rt-kernel.cmake new file mode 100644 index 0000000..5e8c3fb --- /dev/null +++ b/profinet_stack/p-net/cmake/rt-kernel.cmake @@ -0,0 +1,83 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2018 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +target_include_directories(profinet + PRIVATE + src/ports/rt-kernel + ) + +target_sources(profinet + PRIVATE + src/ports/rt-kernel/pnal.c + src/ports/rt-kernel/pnal_eth.c + src/ports/rt-kernel/pnal_udp.c + src/ports/rt-kernel/pnal_snmp.c + src/ports/rt-kernel/mib/mib2_system.c + src/ports/rt-kernel/mib/lldp-mib.c + src/ports/rt-kernel/mib/lldp-ext-pno-mib.c + src/ports/rt-kernel/mib/lldp-ext-dot3-mib.c + src/ports/rt-kernel/mib/rowindex.c + ) + +target_compile_options(profinet + PRIVATE + -Wall + -Wextra + -Werror + -Wno-unused-parameter + ) + +target_include_directories(pn_dev + PRIVATE + samples/pn_dev + src/ports/rt-kernel + ) + +if (EXISTS ${PROFINET_SOURCE_DIR}/src/ports/rt-kernel/sampleapp_${BSP}.c) + set(BSP_SOURCE sampleapp_${BSP}.c) +else() + set(BSP_SOURCE sampleapp_bsp.c) +endif() + +target_sources(pn_dev + PRIVATE + samples/pn_dev/sampleapp_common.c + samples/pn_dev/app_utils.c + samples/pn_dev/app_log.c + samples/pn_dev/app_gsdml.c + samples/pn_dev/app_data.c + src/ports/rt-kernel/sampleapp_main.c + src/ports/rt-kernel/${BSP_SOURCE} + ) + +target_compile_options(pn_dev + PRIVATE + -Wall + -Wextra + -Werror + -Wno-unused-parameter + ) + +install (FILES + src/ports/rt-kernel/pnal_config.h + DESTINATION include + ) + +if (BUILD_TESTING) + target_include_directories(pf_test + PRIVATE + src/ports/rt-kernel + ) +endif() diff --git a/profinet_stack/p-net/doc/Makefile b/profinet_stack/p-net/doc/Makefile new file mode 100644 index 0000000..7c28e2f --- /dev/null +++ b/profinet_stack/p-net/doc/Makefile @@ -0,0 +1,34 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +htmlzipfile := pnet_docs_$(shell git describe --dirty --always --tags).zip + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +# Note that we remove the build status badge image before PDF creation. +%: Makefile + + @echo "Cleaning ..." + @rm -rf _copied/ + @rm -rf _build/ + @echo "Copying markdown files to _copied subdir" + @mkdir -p _copied + @cp ../README.md _copied/ + @cp ../LICENSE.md _copied/ + @cp ../CHANGELOG.md _copied/ + @(test $@ != pdf) || sed -i '/Build Status/d' _copied/README.md + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @(test $@ != html) || cd _build; echo "Zipping ..."; zip -r $(htmlzipfile) html/ > /dev/null diff --git a/profinet_stack/p-net/doc/abbreviations.rst b/profinet_stack/p-net/doc/abbreviations.rst new file mode 100644 index 0000000..1bc129d --- /dev/null +++ b/profinet_stack/p-net/doc/abbreviations.rst @@ -0,0 +1,375 @@ + +Terminology and abbreviations +============================= + +Abstract syntax + Describes the parts (and their subparts) of different messages. See also "Transfer syntax". +Configuration + IO-device and IO-controller definition in an engineering tool. +Commissioning + Device initialization (Configuration is downloaded to IO-controller). +Engineering tool + A desktop program for configuring PLC. For example Siemens TIA portal. +Parameterization + Write parameter values to all submodules. Parameterization is done via + the Write and Control commands. +Record data + Data that can be accessed with acyclic Profinet Read and Write. Addressed by + slot, subslot and index. +Transfer syntax + Coding of the different fields in a message. See also "Abstract syntax". + + +Abbreviations +------------- + +ACK + Positively acknowledge +AKA + Also Known As +ALPMI + Alarm Protocol Machine Initiator. Trigger alarms. +ALPMR + Alarm Protocol Machine Responder. Responds to incoming alarms. +ALPMX + General term for ALPMI and ALPMR +AMR + Asset Management Record +AP + Application Process +APDU + Application (layer) Protocol Data Unit +API + Application Process Identifier (uint32). Used to differentiate between for example user profiles. Sometimes named "Profile ID". +API + Application Programming Interface. Application implementers use the API of the p-net Profinet stack. +APMR + Acyclic Protocol Machine Receiver. Receives incoming alarm frames. +APMS + Acyclic Protocol Machine Sender. Sends alarm frames. +APMX + General term for APMR and APMS +APO + Application Process Object +AR + Application Relation. Consists of several communication relations (CR). Typically an IO AR, which is a connection to a PLC, but can also be an Supervisor AR. +ARP + Address Resolution Protocol, used to translate from an IP address to a MAC address. +AREP + Application Relationship End Point (uint32), pretty much an index into an array of AR. +ASDU + Application Service Data Unit +ASE + Application Service Element. For example logbook, time and diagnosis. +CBA + Component Based Automation, for the legacy Profinet CBA system. It used communication profile CP 3/3. +CC + Conformance Class (Performance level A to D) +CDML + Controller Description Markup Language. An XML file describing a controller, for certification. +CE + A marking for European Union declaration of conformity +CIDR + Classless Inter-Domain Routing. The CIDR notation ``/24`` tells how many bits of the netmask that should be enabled. +CIM + Communication Interface Management +CiR + Configuration in Run +CM + Context Management +CMDEV + Context Management Protocol Machine Device +CMI + CM Initiator +CMINA + Context Management Ip and Name Assignment protocol machine +CMIO + Context Management Input Output protocol machine +CMPBE + Context Management Parameter Begin End protocol machine +CMRDR + Context Management Read Record Responder protocol machine, responds to parameter read from the IO-controller. +CMRPC + Context Management RPC protocol machine +CMWRR + Context Management Write Record Responder protocol machine, responds to parameter write from the IO-controller. +CMSM + Context Management Surveillance protocol Machine, monitors the establishment of a connection. Also written as CM Server Protocol machine. +CP + Communication Profile. For example Profinet conformance class B is also known as CP 3/5. +CPF + Communication Profile Family. Profinet and Profibus is CPF = 3, Ethercat is CPF = 12. +CPM + Consumer Protocol Machine, for receiving cyclic data. +CPU + Central Processing Unit +CR + Communication Relation (Part of AR). +CREP + Communication Relationship EndPoint (uint32), pretty much an index into an array of input and output CR. +DA + Destination Address. It is the MAC address of the receiver. +DAP + Device Access Point +DCE + Distributed Computing Environment. Used with RPC. +DCP + Discovery and basic Configuration Protocol. Runs over Ethernet layer 2 (not IP or UDP). +DFP + Dynamic Frame Packing. Used with IRT protocol. +DHCP + Dynamic Host Configuration Protocol, for allocating IP addresses to devices. +DHT + Data Hold Timer +DLL + Data Link Layer +DLPDU + ? +DNS + Domain Name System, for converting from host name to IP address. +DT + Device Tool +DUT + Device Under Test +EMC + ElectroMagnetic Compatibility +EPM + EndPoint Mapper +ES + Engineering System +FACK + Fragment acknowledge +FAL + Fieldbus Application Layer +FD + Full Duplex +FDB + Forwarding Database, used in MRP +FO + Fiber Optics +FSPM + FAL Service Protocol Machines +FSU + Fast Start Up (Store communication parameters in IO devices) +GAP + ? +GSD + General Station Description. An XML file describing an IO-Device. +GSDML + GSD Markup Language +GUI + Graphical User Interface +HTTP + Hypertext Transfer Protocol +I&M + Identification & Maintenance. Has different blocks; IM0 to IM??. +ICMP + Internet Control Message Protocol. (Sent in an IP packet) +IDNA + Internationalized Domain Names for Applications +IE + Industrial Ethernet +IGMP + Internet Group Management Protocol. For multicast groups. Used in IPv4. +IO + Input Output +IOC + IO Controller. Typically a PLC. +IOD + IO Device. An input-output device controlled by a PLC via Profinet communication. +IOCS + IO Consumer Status. Reported by IO-device (for output data) and IO-controller (for input data), per subslot. (uint8) +IOCR + IO Communication Relation +IOPS + IO Provider Status. Describes validity of IO data per subslot. Sent by IO-device (for input data) or IO-controller (for output data) together with data. (uint8) +IOxS + General term for IOCS and IOPS. +IOS + IO Supervisor. Typically an engineering tool running on a personal computer. +IP + Internet Protocol +IP + Ingress Protection. For example IP65 is a housing class suitable for outdoor installation. +iPar + Individual Parameters. Backups of these parameters are typically stored in a separate parameter server. +IRT + Isochronous Real-Time +LAN + Local Area Network +LED + Light Emitting Diode +LLC + ? +LLDP + Link Layer Discovery Protocol, for neighbourhood detection. +LMPM + data Link layer Mapping Protocol Machine. Receives Ethernet frames. +LT + Length and Type field in Ethernet frame. Also known as EtherType. +MAC + Media Access Control +MAU + Medium Attachment Unit. Ethernet transceiver type. 0x0 = radio, 0x10 = Media type copper 100BaseTXFD +MC + Multicast (as opposed to unicast) +MC + Multicore (Codesys runtime variant for Raspberry Pi) +MCR + Multicast Communication Relationship +MDNS + Multicast DNS. A UDS based protocol for resolving hostname to IP address. Implemented by Bonjour and Avahi. +MIB + Management Information Base. A text file format describing Object Identifiers (OID) for Simple Network Management Protocol (SNMP) frames. +MIC + MRP interconnection Client. Routes traffic between two Ethernet rings (which are using MRP). +MIM + MRP Interconnection Manager. Controls the traffic between two Ethernet rings (which are using MRP). +MRA + Media redundancy manager with auto negotiation +MRC + Media redundancy client +MRM + Media redundancy manager +MRP + Media Redundancy Protocol. Use a normally off path in the Ethernet network, to form a ring topology. +MRPIC + Media Redundancy Protocol Interconnect (connects two MRP rings). Nodes have MIM or MIC roles. +MRPD + Media Redundancy for Planned Duplication. Send similar frames in both directions on an Ethernet ring. +MTU + Maximum Transfer Unit. The largest packet a network interface can handle. Typically 1500 bytes. This includes the IP header, but not the Ethernet header. +NACK + Negatively acknowledge +NDR + Network Data Representation. A header as first part of the DCE/RPC payload (sent via UDP). Contains info on how large the payload is, and how large responses that can be accepted. +NME + Network Management Engine +NRT + Non-Real Time Ethernet frames +NVM + Non-Volatile Memory. Typically flash memory chips. +OID + Object Identifier. Describes a field in a Simple Network Management Protocol (SNMP) frame. +OS + Operating System +OUI + Organizationally Unique Identifier. This is the three first bytes of the MAC address. The value for Profinet Multicast is 01:0E:CF. +PA + Process Automation (as opposed to production automation) +PCA + Provider, Consumer or Alarm. +PCF + Polymer Clad Fiber +PCP + Priority Code Point, for VLAN +PD + Physical Device. This is information related to an Ethernet port. +PDEV + Physical Device management. Physical interface and switch ports of a Profinet field device. +PDF + Portable Document Format +PDU + Protocol Data Unit +PI + PROFIBUS & PROFINET International. The Profinet interest group. See also PNO. +PICO + PI Certification Office +PITL + PI Test Laboratories. Performs certification testing. +PLC + Programmable Logic Controller. Often used as a Profinet IO-controller. +PN + See PROFINET +PNI + Primary Network Initialization. Siemens SinecPni is a tool for configuration of Profinet equipment. +PNIO + Profinet IO protocol +PNO + PROFIBUS Nutzerorganisation e.V, located in Germany. See also PI. +POF + Plastic Optical Fiber +PPM + Cyclic Provider Protocol Machine +PROFINET + Process Field Net +PS + ? +PTCP + Precision Transparent Clock Protocol +RED + Redundancy +RPC + Remote Procedure Call. The protocol DCE/RPC runs on UDP and is used for configuration of the IO-Device during startup. +RS + Reporting system +RSI + Remote Service Interface +RTA + RealTime Acyclic protocol, for alarms. +RTC + Real Time Class +RTC + RealTime Cyclic protocol +RTE + Real Time Ethernet +RTOS + Real Time Operating System +SA + Source Address. It is the MAC address of the sender. +SAM + Source Address of ? Uses to restrict incoming DCP communication to a single remote MAC address (for 3 seconds). +SCL + Structured Control Language. Siemens name for the structured text (ST) programming language for PLCs. +SDU + Service Data Unit. This is the payload of a PDU. +SMA + Sample Moving Average. A smoothing function in the Wireshark Ethernet frame capture software. +SNMP + Simple Network Management Protocol. For network topology detection. +SOE + Sequence Of Events +ST + Structured Text. A programming language for PLCs. +STP + Spanning Tree Protocol. An alternative to MRP (but slower) +STX + Structured Text. See ST. +TACK + Transport Acknowledge. Used for alarm transmission. +TED + Topology and Engineering Discovery +TIA + Totally Integrated Automation. An automation portal (engineering tool) by Siemens. +TIAP + See TIA. +TCI + Tool Calling Interface (The engineering tool can call specialized device-related tools) +TCP + Transmission Control Protocol, used on top of IP. +TLV + Type-Length-Value. A data structure in an LLDP Ethernet frame. +TPID + Tag protocol identifier, for VLAN. +TSDU + ? +TSN + Time-Sensitive Networking +TTL + Time to live. A field in an LLDP Ethernet frame. +UC + Unicast (as opposed to multicast) +UDP + User Datagram Protocol, used on top of IP. +USI + User Structure Identifier (unit16) Describes alarm payloads. +UUID + Universally Unique Identifier. A 128-bit number for uniquely identifying information. +VLAN + Virtual LAN +VID + VLAN identifier +WLAN + Wireless LAN +XML + eXtended Markup Language diff --git a/profinet_stack/p-net/doc/additional_linux_details.rst b/profinet_stack/p-net/doc/additional_linux_details.rst new file mode 100644 index 0000000..c57f6f4 --- /dev/null +++ b/profinet_stack/p-net/doc/additional_linux_details.rst @@ -0,0 +1,389 @@ +Additional Linux Details +======================== +The tutorial gives most information required for getting started on a Linux +machine. Some additional information is given here. + + +Detailed dependency installation info +------------------------------------- +You need to have cmake version at least 3.14 installed. It is available in +Ubuntu 19.04 and later. + +To install on Ubuntu 16.04 or 18.04, follow the instructions here: +https://www.kitware.com/ubuntu-cmake-repository-now-available/ + +It is also possible to install it via snap:: + + sudo snap install cmake --classic + +When using it, you might need to replace ``cmake`` with ``snap run cmake``. +If uninstalling the default ``cmake``, the snap-installed version should be +used by default after reboot. + +Optional dependencies (for development and documentation of the p-net stack):: + + sudo apt install -y \ + clang-tools \ + clang-format-10 \ + doxygen \ + graphviz + + +Run tests and generate documentation +------------------------------------ +Run tests (if you told cmake to configure it):: + + cmake --build build --target check + +Run a single test file:: + + build/pf_test --gtest_filter=CmrpcTest.CmrpcConnectReleaseTest + +Create Doxygen documentation:: + + cmake --build build --target docs + +The Doxygen documentation ends up in ``build/html/index.html`` + +See the "Writing documentation" page if you would like to install +the toolchain to build the entire Sphinx documentation. + +The clang static analyzer can also be used if installed. Create a new +build directory by running:: + + scan-build cmake -B build.scan-build -S p-net + scan-build cmake --build build + + +Setting Linux ephemeral port range +---------------------------------- +This is the range of random source ports used when sending UDP messages. +Profinet requires that the UDP source port should be >= 0xC000, which is 49152 +in decimal numbers. + +To change the ephemeral port range:: + + echo "49152 60999" > /proc/sys/net/ipv4/ip_local_port_range + +This should typically be done at system start up. + + +File size and memory usage on Linux +----------------------------------- +The resulting file size of the sample application binary is heavily dependent +on the compile time options, for example whether to include debug information. +In this example we use:: + + BUILD_SHARED_LIBS ON + CMAKE_BUILD_TYPE Release + LOG_LEVEL Warning + PNET_MAX_AR 2 + PNET_MAX_SLOTS 5 + PNET_MAX_SUBSLOTS 3 + +To get an estimate of the binary size, partially link it (use release, without +standard libraries):: + + p-net/build$ make all + p-net/build$ /usr/bin/cc -O3 -DNDEBUG CMakeFiles/pn_dev.dir/samples/pn_dev/sampleapp_common.o CMakeFiles/pn_dev.dir/src/ports/linux/sampleapp_main.o -o pn_dev libprofinet.a -nostdlib -r + +Resulting size:: + + p-net/build$ size pn_dev + text data bss dec hex filename + 244481 72 8 244561 3bb51 pn_dev + +See https://linux.die.net/man/1/size for information on how to use the command. +Also the rt-kernel page in this documentation has some description on how to +interpret the output. + +The size of the p-net stack can be estimated from the size of libprofinet, +built with the options given above:: + + p-net/build$ size libprofinet.so + text data bss dec hex filename + 230888 3304 8 234200 392d8 libprofinet.so + +An estimate of the p-net RAM usage can be made from the size of the ``pnet_t`` struct. +The sample application has a command line option to show this value, for the used +compile time options (for example the maximum number of modules allowed). + + +Debug intermittent segmentation faults during tests on Linux +------------------------------------------------------------ +Enable core dumps:: + + ulimit -c unlimited + +Run a test case until the problem occurs (in the build directory):: + + while ./pf_test --gtest_filter=DiagTest.DiagRunTest; do :; done + +Study the resulting core:: + + gdb pf_test core + + +SNMP (Conformance class B) +-------------------------- +Conformance class B requires SNMP support. Linux uses net-snmp as agent, +see http://www.net-snmp.org/. The name of the SNMP agent binary is ``snmpd``. + +P-Net for Linux implements a Net-SNMP subagent that handles the Profinet +mandatory MIB:s. Also +see :ref:`network-topology-detection` for information regarding SNMP. + +Enable SNMP by setting ``PNET_OPTION_SNMP`` to ``ON`` in the p-net compilation +options. Net-SNMP also needs to +be installed. On Ubuntu you can install the required packages using:: + + sudo apt install -y snmpd libsnmp-dev + +To show the installed version of ``snmpd``, use:: + + snmpd -v + + +Change snmpd command line arguments +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The p-net SNMP subagent will handle the system objects, so the default +SNMP "system" module should be disabled by adding the snmpd argument +``-I -system_mib``. Otherwise the subagent will complain about +"registering pdu failed" at startup. If you use systemd init system (for +example on Ubuntu Linux) you should change +``/lib/systemd/system/snmpd.service`` to read:: + + [Unit] + Description=Simple Network Management Protocol (SNMP) Daemon. + After=network.target + ConditionPathExists=/etc/snmp/snmpd.conf + + [Service] + Type=simple + ExecStartPre=/bin/mkdir -p /var/run/agentx + ExecStart=/usr/sbin/snmpd -LOw -u Debian-snmp -g Debian-snmp -I -system_mib,smux,mteTrigger,mteTriggerConf -f -p /run/snmpd.pid + ExecReload=/bin/kill -HUP $MAINPID + + [Install] + WantedBy=multi-user.target + +To see the status of the service:: + + systemctl status snmpd.service + journalctl -u snmpd.service -f + +To restart the service after modification:: + + sudo systemctl daemon-reload + sudo systemctl restart snmpd.service + +The command line arguments ``-u`` and ``-g`` are for the user id and group id +of the snmpd process will use after its initial startup. +They are not necessary to use snmpd. +However snmpd must be started with permissions to open relevant sockets, +typically root permissions. + +If you use "system V init" instead of systemd, then snmpd is typically started +by a script file named ``/etc/init.d/snmpd``. Change the snmpd command line +arguments in the file, typically via ``SNMPDOPTS``. Stop and start the +service with:: + + sudo /etc/init.d/snmpd stop + sudo /etc/init.d/snmpd start + + +Configuration file for snmpd +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The file ``snmpd.conf`` controls access to the snmp agent. It should be +set to listen on all interfaces and allow read-write access to the +Profinet MIB:s. On Ubuntu Linux you should change +``/etc/snmp/snmpd.conf`` to read:: + + master agentx + agentaddress udp:161 + view systemonly included .1.3.6.1.2.1.1 + view systemonly included .1.3.6.1.2.1.2.2 + view systemonly included .1.0.8802.1.1.2 + rocommunity public default -V systemonly + rwcommunity private default -V systemonly + +If your Linux distribution does give a long description for ``ifDesc`` you can +override it by adding a line to the ``snmpd.conf`` file. Adapt the interface +index (last digit in OID) and the interface name:: + + override 1.3.6.1.2.1.2.2.1.2.3 octet_str "enp0s31f6" + +See :ref:`network-topology-detection` for more details on SNMP and how to +verify the SNMP communication to the p-net stack. + + +Start your application after snmpd +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +You probably would like your application to wait for the ``snmpd`` application to +be up and running. If you use systemd as init system, modify this line in +the ``[Unit]`` part of your ``.service`` file:: + + After=network.target snmpd.service + +You might also need to add this to the ``[Service]`` part of your +``.service`` file:: + + ExecStartPre=/usr/bin/sleep 0.3 + +If the p-net application fails to connect to snmpd, a message "Warning: Failed +to connect to the agentx master agent" will be written to the p-net sample +app log file every 15 seconds. + + +Debugging snmpd settings +^^^^^^^^^^^^^^^^^^^^^^^^ +The command line argument ``-LOw`` for snmpd sets the snmpd logging to +standard out with log level "warning". +To change to log level debug, use ``-LOd``. You should also use the ``-D`` flag +to select which debug messages you are interested in. Use the ``-Dagentx`` +command line argument to debug the agentx communication between the snmpd and +the subagent in the p-net stack. A log example when asked for a single OID:: + + Connection from UDP: [192.168.0.30]:43833->[192.168.0.50]:161 + agentx/master: agentx master handler starting, mode = 0xa0 + agentx/master: request for variable (iso.3.6.1.2.1.1.4.0) + agentx/master: sending pdu (req=0x8,trans=0x7,sess=0x6) + agentx_build: packet built okay + agentx/master: got response errstat=0, (req=0x8,trans=0x7,sess=0x6) + agentx/master: agentx_got_response() beginning... + agentx/master: handle_agentx_response: processing: iso.3.6.1.2.1.1.4.0 + agentx/master: handle_agentx_response() finishing... + +Note that there might be a warning message "pcilib: Cannot open /proc/bus/pci" +in the snmpd log if you specify that it should use all interfaces. +That is because it will look also for (possibly non-existing) PCI interfaces. + +To trouble-shoot snmpd issues, verify that no other snmpd instances are running:: + + ps -ef | grep snmpd + +and verify that no other process is using UDP port 161:: + + sudo lsof -i udp -n -P + + +snmpd in a Yocto build +---------------------- +In an embedded Linux Yocto build, you would include the ``snmpd`` daemon by +using the ``net-snmp`` recipe. + + +Persistent logs +--------------- +To make the journalctl logs persistent between restarts:: + + sudo mkdir -p /var/log/journal + sudo systemd-tmpfiles --create --prefix /var/log/journal + +Remove all contents of the journalctl logs:: + + sudo journalctl --rotate + sudo journalctl --vacuum-time=1s + +The configurations for journalctl are located in ``/etc/systemd/journald.conf``. +If you do experiments with frequent reboots, it might be useful to change some +values:: + + SyncIntervalSec=10s + MaxRetentionSec=4h + + + +Boot time optimization +---------------------- +To improve the startup time of your Linux device, it is useful to study what +is delaying the start. If you use the "systemd" init system, you can use these +commands to analyze the startup:: + + systemd-analyze + systemd-analyze blame + systemd-analyze critical-chain pnet-sampleapp.service + +To decrease the startup time, disable services you don't use. On a Raspberry Pi +it might be for example:: + + sudo systemctl disable triggerhappy triggerhappy.socket apt-daily.timer apt-daily-upgrade.timer logrotate.timer rpi-display-backlight lightdm bluetooth hciuart rsync cups cups-browsed alsa-state avahi-daemon + +Other applications that you might disable for experimentation:: + + sudo systemctl disable snapd snapd.socket wpa_supplicant systemd-timesyncd dhcpcd + + +Debug the sample application on a Linux Laptop +---------------------------------------------- +It can be convenient to be able to run the sample application and the p-net +stack in a debugger tool. It is easy using gdb and the Visual Studio Code +editor. + +First make sure you can run the application from a terminal on your Linux +laptop. + +Next step is to be able to run it from the terminal within Visual Studio Code. +In case of compilation error messages, you can click on the code line given +in the terminal (within Visual Studio Code) and the corresponding file will +be opened. Hold the CTRL key while clicking on the line. + +To use debug features while running (for example breakpoints) you need to adapt +the settings file for Visual Studio Code. Click the "Run and Debug" icon +in the left side tool bar. Then click "Create a launch.json file". In the +"Select environment", use "C++ (GDB/LLDB)". + +Modify the ``launch.json`` file to point at the correct executable, working +directory and to use correct command line arguments. + +If you need to run the application with root permissions, you need to add a path in the +``"miDebuggerPath"`` field. It should point to a text file typically named +``gdb``, with this content:: + + pkexec /usr/bin/gdb "$@" + +Put the ``gdb`` file for example in the ``.vscode`` subdirectory within +the ``p-net`` directory. Set the executable flag:: + + chmod +x gdb + +An example of a ``launch.json`` file:: + + { + "version": "0.2.0", + "configurations": [ + + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/pn_dev", + "args": ["-vv", "-i", "enp0s31f6"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/build/", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "${workspaceFolder}/.vscode/gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + + ] + } + +The given ``"args"`` command line arguments in the example is for +increasing the verbosity level and to set the Ethernet interface name. +Adapt those and also paths to your particular setup. + +Use the ``CMAKE_BUILD_TYPE`` setting as ``Debug`` when running the executable +via the debugger. + +Start the debugging by clicking on the small green "Run" icon on the +"Run and Debug" page. It will stop at any breakpoint. Set a breakpoint in +any file by clicking on a line to the left of the line number. diff --git a/profinet_stack/p-net/doc/api_documentation.rst b/profinet_stack/p-net/doc/api_documentation.rst new file mode 100644 index 0000000..6c93259 --- /dev/null +++ b/profinet_stack/p-net/doc/api_documentation.rst @@ -0,0 +1,182 @@ +API documentation +================= +Here is a brief overview given for some of the most important functions, +structs etc in the public API for the p-net Profinet stack. + +For detailed documentation, read the ``include/pnet_api.h`` header file. It +also contains error code definitions etc. + +You can also build the Doxygen documentation for all functions by following +the instructions given on the page "Additional Linux Details". + +Study samples/pn_dev to get more information on how to use the API. + + +General +------- +All functions return either ``0`` (zero) for a successful call or ``-1`` for an +unsuccessful call. + + +Initialization and handling +--------------------------- +.. doxygenfunction:: pnet_init +.. doxygenfunction:: pnet_application_ready +.. doxygenfunction:: pnet_handle_periodic +.. doxygenfunction:: pnet_get_ar_error_codes +.. doxygenfunction:: pnet_ar_abort +.. doxygenfunction:: pnet_factory_reset +.. doxygenfunction:: pnet_show + + +Plug and pull modules/submodules +-------------------------------- +.. doxygenfunction:: pnet_plug_module +.. doxygenfunction:: pnet_plug_submodule +.. doxygenfunction:: pnet_pull_module +.. doxygenfunction:: pnet_pull_submodule + + +Periodic data +------------- +.. doxygenfunction:: pnet_input_set_data_and_iops +.. doxygenfunction:: pnet_input_get_iocs +.. doxygenfunction:: pnet_output_get_data_and_iops +.. doxygenfunction:: pnet_output_set_iocs +.. doxygenfunction:: pnet_set_provider_state + + +Redundant state etc +------------------- +.. doxygenfunction:: pnet_set_primary_state +.. doxygenfunction:: pnet_set_redundancy_state + + +Alarms and diagnostics +---------------------- +.. doxygenfunction:: pnet_create_log_book_entry +.. doxygenfunction:: pnet_alarm_send_process_alarm +.. doxygenfunction:: pnet_alarm_send_ack +.. doxygenfunction:: pnet_diag_std_add +.. doxygenfunction:: pnet_diag_std_update +.. doxygenfunction:: pnet_diag_std_remove + +.. doxygenfunction:: pnet_diag_usi_add +.. doxygenfunction:: pnet_diag_usi_update +.. doxygenfunction:: pnet_diag_usi_remove + + +Low-level diagnostic functions +------------------------------ +These are used internally in the functions above for handling diagnosis in +standard or in USI format. However they can be useful in situations where +detailed control is required. + +.. doxygenfunction:: pnet_diag_add +.. doxygenfunction:: pnet_diag_update +.. doxygenfunction:: pnet_diag_remove + + +Callbacks +--------- +The application should define call-back functions which are called by +the stack when specific events occurs within the stack. + +Note that most of these functions are mandatory in the sense that they must +exist and return ``0`` for a functioning stack. Some functions are required +to perform specific functionality. + +.. doxygentypedef:: pnet_connect_ind +.. doxygentypedef:: pnet_release_ind +.. doxygentypedef:: pnet_dcontrol_ind +.. doxygentypedef:: pnet_ccontrol_cnf +.. doxygentypedef:: pnet_state_ind +.. doxygentypedef:: pnet_reset_ind +.. doxygentypedef:: pnet_signal_led_ind +.. doxygentypedef:: pnet_read_ind +.. doxygentypedef:: pnet_write_ind +.. doxygentypedef:: pnet_exp_module_ind +.. doxygentypedef:: pnet_exp_submodule_ind +.. doxygentypedef:: pnet_new_data_status_ind +.. doxygentypedef:: pnet_alarm_ind +.. doxygentypedef:: pnet_alarm_cnf +.. doxygentypedef:: pnet_alarm_ack_cnf + + +Selected enums +-------------- +.. doxygenenum:: pnet_event_values +.. doxygenenum:: pnet_ioxs_values +.. doxygenenum:: pnet_submodule_dir +.. doxygenenum:: pnet_control_command +.. doxygenenum:: pnet_data_status_bits +.. doxygenenum:: pnet_diag_ch_prop_type_values +.. doxygenenum:: pnet_diag_ch_prop_dir_values +.. doxygenenum:: pnet_diag_ch_prop_maint_values +.. doxygenenum:: pnet_diag_ch_group_values +.. doxygenenum:: pnet_im_supported_values +.. doxygenenum:: pnal_eth_mau + + +Selected structs +---------------- +Network and device configuration. + +Configuration of the stack is performed by transferring a structure +in the call to ``pnet_init()``. + +Along with the configuration the initial (default) values of the +I&M data records are conveyed as well as the values used for +sending LLDP frames. + +.. doxygenstruct:: pnet_im_0 + :members: + :undoc-members: + +.. doxygenstruct:: pnet_im_1 + :members: + :undoc-members: + +.. doxygenstruct:: pnet_im_2 + :members: + :undoc-members: + +.. doxygenstruct:: pnet_im_3 + :members: + :undoc-members: + +.. doxygenstruct:: pnet_im_4 + :members: + :undoc-members: + +.. doxygenstruct:: pnet_cfg_device_id + :members: + :undoc-members: + +.. doxygenstruct:: pnet_if_cfg + :members: + :undoc-members: + +.. doxygenstruct:: pnet_port_cfg + :members: + :undoc-members: + +.. doxygenstruct:: pnet_ip_cfg + :members: + :undoc-members: + +.. doxygenstruct:: pnet_cfg + :members: + :undoc-members: + +.. doxygenstruct:: pnet_alarm_spec + :members: + :undoc-members: + +.. doxygenstruct:: pnet_alarm_argument + :members: + :undoc-members: + +.. doxygenstruct:: pnet_diag_source + :members: + :undoc-members: diff --git a/profinet_stack/p-net/doc/applications_and_porting.rst b/profinet_stack/p-net/doc/applications_and_porting.rst new file mode 100644 index 0000000..6cb3b50 --- /dev/null +++ b/profinet_stack/p-net/doc/applications_and_porting.rst @@ -0,0 +1,140 @@ +Creating applications and porting to new hardware +================================================= + +Create your own application +--------------------------- +If you prefer not to implement some of the callbacks, set the corresponding +fields in the configuration struct to NULL instead of a function pointer. +See the API documentation on which callbacks that are optional. + +To read output data from the PLC, use ``pnet_output_get_data_and_iops()``. +To write data to the PLC (which is input data in Profinet terminology), use +``pnet_input_set_data_and_iops()``. + + +Adapt to other hardware +----------------------- +You need to adapt the implementation to your specific hardware. + +This includes for example: + +* For Linux, adjust the networking and LED scripts in PNAL +* Implement the callback to control the "Signal" LED. + + +Required features of the hardware or operating system +----------------------------------------------------- + +* It should be possible to send and receive raw (layer 2) Ethernet frames +* It should be possible to store data between runs, in a file system or some nonvolatile memory +* For conformance class B, there needs to be an SNMP implementation that + the p-net stack can use. + + +General requirements +-------------------- +It should be possible to do a factory reset without an engineering tool. For +example a hardware switch can be used. Routing of Ethernet frames must not be +affected by a factory reset. + +A Profinet signal LED must be visible on the IO-Device. A callback is available +from p-net to control the signal LED, so you can implement your board specific +hardware. + +Connectors and cables should fulfill the requirements in "PROFINET Cabling and +Interconnection Technology". For recommended diagnosis indicators (for example +LEDs) see "Diagnosis for PROFINET". +Consider the housing (often IP65) and grounding of the device. +In general the MAC address of the device should be visible when installed. + +Hardware requirements for the Ethernet ports: + +* At least 100 Mbit/s full duplex +* Standard and crossover cables should be handled +* Auto polarity +* Auto negotiation (should be possible to turn off for fast startup) + + +Interacting with other parts of your application software +--------------------------------------------------------- +When the PLC sends a "factory reset" command, not only the p-net stack but +also the rest of your application should be reset to factory settings. + +If there is a method to do a factory reset of your application, it should also +reset the p-net stack. + +It is possible to set the IP-address, netmask and gateway address via Profinet +from a PLC. Consider how this affects other aspects of your application. + +Serial number, software version, model name etc should be available for the +p-net stack to use. + + +Minimum cycle time for your application and hardware +---------------------------------------------------- +At PLC configuration, there are two adjustable parameters regarding the +cyclic data timing for an IO-device. One basically controls the +frame-to-frame transmission interval (given as a multiple of the cycle time), +and the other one controls the PLC watchdog timeout setting. +The watchdog triggers an alarm when a number of consecutive cyclic data +frames from the IO-device are missing. + +During conformance testing, the PLC watchdog timeout setting is 3 x +frame-to-frame interval times, ie the PLC will trigger an alarm if three +consecutive cyclic data frames are missing. Thus the smallest allowed +frame-to-frame interval given in the GSDML file must take the smallest +useful PLC watchdog timeout setting into account. + +There can be different factors limiting the ability to send Profinet frames at +a regular pace: + +* The initial work at Profinet communication startup should not cause + delays in the cyclic data transmission once started. +* The hardware needs to keep up with the steady-state sending and receiving + of Profinet cyclic data. +* Other work done by the Profinet application, for example work done in + callbacks, might slow down the communication. +* Operating system jitter, which basically is that the operating system is + busy doing other work, will cause frame delays. +* Using a network interface connected via USB can cause additional frame + delays. + +For real-time operating systems (RTOS) for example rt-kernel, the +frame-to-frame transmission will have little jitter. + +For non-real-time operating systems, for example Linux, the limiting factor +is typically the jitter caused by the operating system. +See the page on Linux timing in this documentation for ideas on improving +Linux real-time properties. + + +Measure Profinet frame transmission jitter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Set the PLC watchdog time to 100 x frame interval, to avoid any alarms +from the PLC. +Use Wireshark to record cyclic data transmission for a few minutes. +Plot the maximum frame-to-frame transmission interval (see the page on +capturing Ethernet packets in this documentation). +This will show which PLC watchdog setting you need in order not to trigger +"missing frame" alarms. + +For example if the transmission interval is 1 ms but there sometimes is a 10 ms +delay between the frames, then you need to use the next standard PLC timeout +value which is 12 ms. In order to handle that with a watchdog setting of 3 x +frame interval, you need to use a minimum cycle time of 4 ms (as +will be stated in your GSDML file). + + +Example measurement results +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Measurements with p-net on a XMC4800 board running rt-kernel shows that the +hardware is able to send frames with a periodicity of 1 ms with little jitter. +The liming factor is that the PLC watchdog timeout value needs to be 12 ms, +due to the time it takes for the first cyclic data frame to be sent from +the IO-device. +A GSDML file should state that the smallest allowed cycle time is 4 ms. + +Similar measurements on a Raspberry Pi 3B+ show that it is possible to send +frames with a cycle time of 1 ms. However there is sometimes a frame-to-frame +interval of up to 40 ms, resulting in that a GSDML file should state that the +minimum frame interval is 16 ms. diff --git a/profinet_stack/p-net/doc/capturing_packets.rst b/profinet_stack/p-net/doc/capturing_packets.rst new file mode 100644 index 0000000..2a9cc10 --- /dev/null +++ b/profinet_stack/p-net/doc/capturing_packets.rst @@ -0,0 +1,263 @@ +Capturing and analyzing Ethernet packets +======================================== +In order to understand the Profinet traffic, it is useful to capture network +packets and analyze them in a tool like Wireshark. You typically run +Wireshark on your laptop, but in order to capture packets on for example +a Raspberry Pi you can use the program tcpdump (described below). + + +Install Wireshark on Linux +-------------------------- +To install a relatively new Wireshark version on Ubuntu:: + + sudo add-apt-repository ppa:wireshark-dev/stable + sudo apt update + sudo apt -y install wireshark + +In order to be able to capture packets you need to add yourself to the +``wireshark`` user group, or run the program as root:: + + sudo wireshark + +For details on how to add yourself to the ``wireshark`` user group, see +https://linuxhint.com/install_wireshark_ubuntu/ + +Wireshark is also available on Windows. Download it from https://www.wireshark.org/ + + +Install Wireshark on Windows +---------------------------- +Download Wireshark from https://www.wireshark.org/download.html + + +Filtering frames in Wireshark based on protocol +----------------------------------------------- +To show only frames with specific protocols, enter this into the "display +filter" text box:: + + lldp or snmp or pn_dcp or dcerpc or pn_io or syslog + +To hide frames with specific protocols:: + + not (pn_ptcp or arp or mdns or llmnr or ssdp or dhcp or dhcpv6 or icmpv6 or igmp or nbns or browser or ipv6) + ++--------------------------+----------------------------------------------------+ +| Filter expression | Wireshark protocol names | ++==========================+====================================================+ +| arp | ARP | ++--------------------------+----------------------------------------------------+ +| epm | EPM | ++--------------------------+----------------------------------------------------+ +| lldp | LLDP | ++--------------------------+----------------------------------------------------+ +| pn_dcp | PN-DCP | ++--------------------------+----------------------------------------------------+ +| pn_io | PNIO and PNIO_PS and PNIO-AL | ++--------------------------+----------------------------------------------------+ +| pn_io_controller | PNIO-CM (application ready) | ++--------------------------+----------------------------------------------------+ +| pn_io_device | PNIO-CM (connect, read, read implicit, write, | +| | param end, release) | ++--------------------------+----------------------------------------------------+ +| dcerpc | PNIO-CM and EPM | ++--------------------------+----------------------------------------------------+ +| pn_ptcp | PN-PTCP | ++--------------------------+----------------------------------------------------+ +| snmp | SNMP | ++--------------------------+----------------------------------------------------+ +| syslog | Syslog | ++--------------------------+----------------------------------------------------+ + + +Filter frames in Wireshark based on frame contents +-------------------------------------------------- +To show Profinet read and write commands for a specific index:: + + pn_io.index == 0x8071 + +To show Profinet read and write commands for a specific subslot:: + + pn_io.subslot_nr == 0x8001 + +To look for a specific OID in SNMP messages:: + + snmp.name == 1.3.6.1.2.1.1.3.0 + +Select frames with a specific DCE RPC Activity UUID:: + + dcerpc.dg_act_id == 38353218-0002-1010-ae64-ac6417366047 + + +Coloring rules in Wireshark +--------------------------- +To modify the coloring rules in Wireshark, use the menu View > "Coloring rules". +Deselect the rules you don't need. + +Create a new rule by clicking the ``+`` symbol in the lower left corner. +Enter the filter expression and the name of the new rule. Enable it using the +checkbox. With the rule still marked, click the "Background" button to modify +the background color. + ++------------+----------------------------------------------------------+ +| Rule name | Filter | ++============+==========================================================+ +| LLDP | lldp | ++------------+----------------------------------------------------------+ +| SNMP | snmp | ++------------+----------------------------------------------------------+ +| Syslog | syslog | ++------------+----------------------------------------------------------+ +| DCP | pn_dcp | ++------------+----------------------------------------------------------+ +| DCERPC | dcerpc | ++------------+----------------------------------------------------------+ +| PNIO ALARM | ``pn_rt.frame_id == 0xfc01 || pn_rt.frame_id == 0xfe01`` | ++------------+----------------------------------------------------------+ +| PNIO | pn_io | ++------------+----------------------------------------------------------+ + +Move the most specific lines to the top. + + +Parsing Profinet cyclic data with Wireshark +------------------------------------------- +It is possible to load a GSDML file into recent versions of Wireshark, for +parsing the cyclic data. +In the Wireshark menu, select Edit > Preferences > Protocols > PNIO. +Enter the directory where you have your GSDML file. + +For this functionality to work, the Wireshark capture must include the start-up +sequence. When a packet is interpreted according to a GSDML file, the name of +the GSDML file is displayed in the detail view of the packet. + + +Diagnosis data in Wireshark +--------------------------- +Note that Wireshark can not yet decode QualifiedChannelDiagnosisData. +See the ``dissect_AlarmUserStructure ()`` function in +the ``packet-dcerpc-pn-io.c`` file in the source code for Wireshark. + + +Show wall time in Wireshark +--------------------------- +In order to correlate captured Wireshark frames to other logs, it can be +convenient to display wall time in the time column in Wireshark. +Right-click the column header, and select "Edit Column". +For "Type" use "Time (format as specified)". Click OK. +In the main menu View > "Time Format Display" select "Time of day". + + +Show transmission time periodicity using Wireshark +-------------------------------------------------- +In order to study the periodicity of sent frames, in the filter heading on the +main screen select the MAC address of the p-net IO-device, for example:: + + eth.src == 54:ee:75:ff:95:a6 and pn_io + +In the column header, right-click and select "Column Preferences ...". Press "+" +to add a new column. Change "Title" to "Delta displayed" and "Type" to +"Delta time displayed". + + +Plot transmission time periodicity using Wireshark +-------------------------------------------------- +To plot the periodicity of sent frames, you first need to filter the displayed +frames in the main Wireshark window, as described above. + +Then use the menu "Statistics" -> "I/O Graph". + +* Display filter: ``eth.src == 54:ee:75:ff:95:a6 and pn_io`` +* Y Axis: AVG(Y Field) +* Y Field: ``frame.time_delta_displayed`` +* SMA (sample moving average) Period: None +* Interval: 10 or 100 ms. + +Adapt the MAC address to your p-net IO-device. +You need to zoom the Y-axis to an interesting range, maybe 0-10 ms. + +It is also interesting to add a line "MAX(Y Field)" and a line "MIN(Y Field)" +in the same graph as the first line. See below for an example where the +average frame interval is 1 ms. + +.. image:: illustrations/periodicity.png + +The lines should be interpreted as the average, minimum and maximum +packet-to-packet times during the interval (for example 100 ms). + + +Plot Profinet cycle counter +--------------------------- +The cycle counter should increase between each frame. You can use Wireshark to verify that. + +Use the menu “Statistics” -> “I/O Graph”. + +* Display filter: ``eth.src == 54:ee:75:ff:95:a6`` +* Style: Dot +* Y Axis: AVG(Y Field) +* Y Field: ``pn_rt.cycle_counter`` +* SMA (sample moving average) Period: None +* Interval: 1 ms + +Adapt the MAC address to your p-net IO-device. + +Zoom to the relevant time period, and y-axis range of approx 0 to 70000. +The frame cycle counter increases up to max 65535 before it wraps to 0. +It is important that the samples are evenly distributed in time (on the +horizontal axis). See the image below (captured during a trouble-shooting +session). + +.. image:: illustrations/Cyclecounter.png + + +Plot network load +----------------- +Use the menu “Statistics” -> “I/O Graph”. + +* Display filter: (none) +* Style: Line +* Y Axis: Bits +* Y Field: (none) +* SMA (sample moving average) Period: None +* Interval: 100 ms or 1 s + +See below for an example captured during a "Security Level 1" network load test, +where the network load is close to 100 Mbit/s (1E8 bits/s). + +.. image:: illustrations/NetworkLoad.png + + +tcpdump +------- +When running on an embedded Linux board, it can be convenient to run without +a graphical user interface. To capture packets for later display in Wireshark, +use the tool ``tcpdump``. + +Install it, for example like:: + + sudo apt-get install tcpdump + +Run it with:: + + sudo tcpdump -i enp0s31f6 -n -w outputfile.pcap + +Use the ``-i`` argument to specify Ethernet interface. + +Transfer the resulting file to your laptop, where you can open it in Wireshark. + +Note that Wireshark and tcpdump modifies the network interface settings on Linux. +The promiscuous bit will be enabled, as seen when looking at +``/sys/class/net/eth0/flags``. It will not be visible via ``ifconfig`` though. + + +Hardware for capturing packets on network +----------------------------------------- +Profinet is a point-to-point protocol. If the Profinet controller or device +software is running on your machine, you can use Wireshark (or tcpdump) +directly to capture the packets. + +If you would like to capture packets between other units (Profinet +controllers/devices) you need special hardware to do the capturing. A network +tap is a network switch with packet monitoring to send a copy of each packet +to another Ethernet connector. Connect the tap on the network link between the +IO-device and IO-controller. Connect the mirroring port to the machine where +you run Wireshark or tcpdump. diff --git a/profinet_stack/p-net/doc/codespell_ignore.txt b/profinet_stack/p-net/doc/codespell_ignore.txt new file mode 100644 index 0000000..9670d48 --- /dev/null +++ b/profinet_stack/p-net/doc/codespell_ignore.txt @@ -0,0 +1,3 @@ +ba +fo +inout diff --git a/profinet_stack/p-net/doc/codesys_details.rst b/profinet_stack/p-net/doc/codesys_details.rst new file mode 100644 index 0000000..d147275 --- /dev/null +++ b/profinet_stack/p-net/doc/codesys_details.rst @@ -0,0 +1,224 @@ +Codesys Details +=============== +This page contains additional information on how to use the Codesys soft PLC. + + +Adjust PLC timing settings +-------------------------- +It is possible to adjust the cycle time that the IO-controller (PLC) is using +for cyclic data communication with an IO-device. + +In the left menu, double-click the "P_Net_Sample_App", and open the "General" +tab. The "Communication" section shows the send clock in milliseconds, as read +from the GSDML file. By using the "Reduction ratio" you can slow down the +communication, by multiplying the cycle time by the factor given in the +dropdown. + +It is also possible to increase the watchdog time, after which the PLC will set +an alarm for missing incoming cyclic data. The watchdog will also shut down the +communication, and trigger a subsequent restart of communication. + +In case of problems, increase the reduction ratio (and timeout) value a lot, +and then gradually reduce it to find the smallest usable value. + + +Displaying errors +----------------- +Click on the IO-device in the tree structure in the left part of the screen. +Use the "Log" tab to display errors. + + +Connection status +----------------- +Go to the Profinet IO-device page, and see the "PNIO IEC objects" tab. Expand +the topmost row. The states of these boolean fields are shown: + +* xRunning: Periodic data is sent +* xBusy: The controller is trying to connect to the IO-device +* xError: Failure to connect to the IO-device + +If there is no connection at all to the IO-device, the state will shift to +xBusy from xError once every 5 seconds. + + +Change IP address of IO-device +------------------------------ +Change the IP address by double-clicking the "P_Net_Sample_App" node +in the left menu, using the "General" tab. Set the IP-address to new value. + +The IO-controller will send the new IP address in a "DCP Set" message to the +IO-device having the given station name. Then it will use ARP messages to +the IO-device to find its MAC address, and to detect IP address collisions. + + +Scan for devices, assign IP address, reset devices and change station name +-------------------------------------------------------------------------- + +Scan +^^^^ +In the left side menu, right-click the PN_Controller and select "Scan for +devices". The running IO-devices will show up, and it is possible to see which +modules are plugged into which slot. +Note that the texts in the columns "Device name" and "Device type" are from the +available GSDML files in Codesys. If a corresponding GSDML file not has been +loaded, the text "Attention: The device was not found in the repository" is +shown together with the Vendor-ID and Product-ID. + +This is implemented in Codesys by sending the "Ident request, all" DCP +message from the IO-controller. +It works also if there are no IO-devices loaded in the left side hierarchy menu. +An IO-device will respond with the "Ident OK" DCP message. Then the IO-controller +will do a "Read implicit request" for "APIData", on which the IO device +responds with the APIs it supports. A similar request for +"RealIdentificationData for one API" is done by the IO-controller, on which the +IO-device responds which modules (and submodules) are plugged into which slots +(and subslots). + +Factory reset +^^^^^^^^^^^^^ +To factory reset a device, select it in the list of scanned devices and click +the "reset" button. + +At a factory reset, the IO-controller sends a "Set request" DCP message +with suboption "Reset factory settings". After sending a response, the +IO-device will do the factory reset and also send a LLDP message with the +new values. Then the IO-controller sends a "Ident request, all", to which +the IO-device responds. + +Set name and IP +^^^^^^^^^^^^^^^ +To modify the station name or IP address, change the corresponding fields +in the list of scanned devices, and the click "Set name and IP". + +The IO-controller sends a "Set request" DCP message +with suboptions "Name of station" and "IP parameter". After sending a +response, the IO-device will change IP address and station name. It will +also send a LLDP message with the new values. Then the +IO-controller sends a "Ident request, all", to which the IO-device responds. + +Flash LED +^^^^^^^^^ +There is functionality to flash a LED on an IO-device. Select your device in +the list of scanned devices, and click the "Blink LED" button. The button +remains activated until you click it again. + +LED-blinking is done by sending the "Set request" DCP message with suboption +"Signal" once every 5 seconds as long as the corresponding button is activated. + +Identification & Maintenance (I&M) data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In order the read Identification & Maintenance (I&M) data, the device needs to +be present as an IO-device connected to the IO-controller in the left side menu. +Select the device in the list of scanned devices, and click the "I&M" button. + +Reading I&M data is done by the IO-controller by sending four "Read implicit" +request DCP messages, one for each of I&M0 to I&M3. + +When writing I&M data from Codesys, it will send a connect, write and release. + + +Writing and reading IO-device parameters +---------------------------------------- +The parameters for different submodules are written as part of normal startup. + +To adjust the values sent at startup, you must first be offline. +Then double-click the DIO_8xLogicLevel device in the left menu. Use the +General tab, and the Settings part of the page. Click the corresponding +value in the table, and adjust it. + +While offline (with regards to the PLC, it is possible to manually trigger +parameter reading and writing. Right-click the parameter you would like +to send, and select "Write to device". It is also possible to read a +parameter similarly. + +To read all or write all parameters, use the corresponding buttons. +When clicking the "Write All Values" icon, one write request is sent per +parameter. + + +Forcing cyclic data values +-------------------------- +To force an output value from the PLC, open the PLC_PRG window while online. +In the table on the top of the page, on the line you would like to force click the "Prepared value" +until it shows "TRUE". Use the line "out_pin_LED" if running the sample application. +Right-click on the line, and select "Force all values ...". The new value is sent from the PLC to the IO-device. +A red `F` symbol is shown. +To stop forcing the value, right-click it and select "Unforce all values ...". + + +Displaying alarms sent from IO-device +------------------------------------- +Incoming process alarms and diagnosis alarms appear on multiple places in +the Codesys desktop application. + +* Codesys Raspberry Pi: The "Log" tab show process and diagnosis alarms. +* PN_Controller: The "Log" tab show process and diagnosis alarms. +* IO-device: The "Log" tab show process and diagnosis alarms. The + “Status” tab shows alarms related to built-in (DAP) modules. +* Plugged module in IO-device: Process and diagnosis alarms are displayed on + the “Status” tab. + + +Setting output producer status (IOPS) +------------------------------------- +Normally Codesys will set the Output PS to GOOD (0x80 = 128) when running. +Clicking the "Output PS" checkbox on the "IOxS" tab on the Profinet IO-device +sets the value to BAD (0). + + +Enabling checking of peer stationname and port ID +------------------------------------------------- +It is possible to have the IO-device verify that it is connected to the +correct neighbour (peer) by checking its station name and port ID (as sent +in LLDP frames by the neighbour). + +Double-click the “P_Net_Sample_App” node in the left menu. On the "options" +tab in the resulting window, use the fields "Peer station" and "port". It +seems only possible to select station names from other devices or controllers +already available in the project. + +During startup the PLC will send the given values to the IO-Device via a +write command. If the correct neighbour is not present, an alarm will be sent +by the IO-device to the PLC. + + +Writing PLC programs +-------------------- +Documentation of available function blocks is found at +https://help.codesys.com/webapp/_pnio_f_profinet_io_configuration;product=core_ProfinetIO_Configuration_Editor + + +Using the Echo module +--------------------- +The echo module will receive an integer and a float from the PLC, and multiply them with a constant +value before sending them back to the PLC. The multiplier is module parameter, and can be adjusted +at startup. The integer is an unsigned 32 bit integer, and the float is a single precision float +(32 bits). + +To test it, unplug any existing modules, and plug one Echo module into slot 1. + +Enter this program:: + + PROGRAM PLC_PRG + VAR + out_echo_int: UDINT; + out_echo_float: REAL; + in_echo_int: UDINT; + in_echo_float: REAL; + temp_int: UDINT; + temp_float: REAL; + END_VAR + + out_echo_int := 1000001; + out_echo_float := 1001.23456; + + temp_int := in_echo_int; + temp_float := in_echo_float; + +On the Echo_module page, use the "PNIO Module I/O Mapping" to connect the +four program variables to the corresponding channels. Connect "in_echo_float" to +"Input float to controller" etc. + +Start the PLC, and go online to follow the values. You can force (described elsewhere) +the ``out_echo_int`` and ``out_echo_float`` values to study how the resulting +input values from the IO-device changes. diff --git a/profinet_stack/p-net/doc/compliancetest.rst b/profinet_stack/p-net/doc/compliancetest.rst new file mode 100644 index 0000000..3539562 --- /dev/null +++ b/profinet_stack/p-net/doc/compliancetest.rst @@ -0,0 +1,911 @@ +Compliance testing of IO-devices +================================ +All Profinet products need to be certified. That is done after testing by an +accredited PROFIBUS & PROFINET International test lab (PITL). +The full hardware, communication stack and application software is tested +together. This is also known as certification testing or conformance testing. + +Each manufacturer of Profinet equipment needs a Vendor ID, which is obtained +from the PI Certification Office. + +The tests checks the GSD file, the hardware interface and the Profinet +behavior. The tests should be performed on a device from series production. + +The Profinet equipment also needs to fulfill parts of the IEEE802 standards, +for example uniqueness of MAC addresses. This is not tested by the test lab. + +A detailed description of the certification process can be downloaded from +https://www.profibus.com/download/how-to-get-a-certificate-for-a-profinet-device/ + +See also the general requirements on the page "Creating applications and +porting to new hardware" in this documentation. + +Needed for certification: + +* EMC test report +* European Union declaration of conformity (for CE marking) +* The user manual must be available. +* A valid GSD file + + +Tool for pretesting +-------------------- +For Profinet members, the "Automated RT tester" tool is available for download. +It is useful for pretesting before a full compliance test. + +Profinet devices must fulfill "Security Level 1" with regards to the net +load. This is tested by a separate tool (not the "Automated RT tester"). + + +Installation of Automated RT tester on a Windows PC +--------------------------------------------------- +Unzip the downloaded file, and double-click the +"AutomatedRtTester_VERSION_Setup.exe" file. + +Verify that you use the latest available version. + + +Adjust the Ethernet network card on a Windows PC +------------------------------------------------ +To use the "Automated RT tester" tool you need to adjust the Ethernet interface +settings of your PC. In the properties for the driver, go to the "Advanced" +tab. For the "Packet priority & VLAN" select "Packet priority & VLAN Disable". +More details are given in the "Product Documentation" document for the tool. + +You also need to turn off LLDP protocol for the selected network interface +on the laptop running the "Automated RT tester" tool, +otherwise the test case “Different access ways port-to-port” will fail. +Both Windows and Simatic TIA ("PROFINET IO Protocol (DCP/LLDP)") can have +LLDP implemented. + +Adjust the settings of the Ethernet card of your personal computer to use +100 Mbit/s full duplex (otherwise the test case "Different access ways +port-to-port" will fail). + +Set the IP address to ``192.168.0.25`` and netmask to ``255.255.255.0``. + +Use a separate network for running tests with Advanced RT tester +(avoid running it on a network with unrelated devices). + + +Supported GSD versions +---------------------- +The "Automated RT tester" tool is compatible only with a few versions of GSDML +files. See the document "PN_Versions_for_certifications" included in the +download. + + +Create a project +---------------- +Use the menu File > New > “Device Test Project”. Follow the wizard. +Enter the MAC address of the IO-Device. If you use the Profinet switch +(device "B") you can press the button "Get MAC Address", as the ART-tester +queries the network for the alias ``port-003.b``. + +On the “Profinet settings” page, select the GSDML file for the IO-Device. + +Populate modules/submodules in slots/subslots by +marking a module in the left column, and then click the -> arrow. To insert a +submodule, mark the relevant submodule in the left column and mark the +appropriate slot in the right column. + +To remove from a slot or subslot, mark it (in the right column) and press the +<- arrow. + + +Adjust settings +--------------- +You need to adjust the device MAC address. This is done via the menu +Tools > Options > DUT. Enter the value and press OK. + +If the device does not have support for remote change of values, you might +need to adjust the device station name, IP address and subnet mask. +This is done via the menu Tools > Options > Setting. You might need to +click the "Show expert settings". + + +Run tests +--------- +Select the tests (in the left side menu) to run. Failing tests are time +consuming, so start with a single test to verify the communication. Disable all +tests by the left side menu. Press the "Deselect all" icon. Then open "Automated +test cases" > "Standard Setup" > DCP, and enable "DCP - DCP_IDN". Use the menu +Project > Run. + +When communication is verified, enable all relevant test cases. + +The ART tester tool stores Wireshark files (.pcap files) in the +project directory. See the ``EthernetDump`` subdirectory. + +To export test reports as PDF, click the small printer icon in the top +toolbar. + +Checklist before starting ART Tester tests. Use correct: + + * Cabling + * Software version and compile time options + * Ethernet interface settings for ART Tester laptop + * GSDML file + * Plugged modules in ART Tester + * Make sure no other device uses the IP address ``192.168.0.50`` + + +Additional hardware +------------------- +Some of the test cases requires additional hardware; a Profinet-enabled switch +("Device B") and an IO-controller ("Device A"). Also a remote controlled +power outlet can be used to simplify the tests. + ++-------------------------+-----------------------------+------------------------+ +| Item | IP address | Description | ++=========================+=============================+========================+ +| Device under test (DUT) | 192.168.0.50 | | ++-------------------------+-----------------------------+------------------------+ +| ART tester on PC | 192.168.0.25, 192.168.1.143 | | ++-------------------------+-----------------------------+------------------------+ +| PLC ("Device A") | 192.168.0.100 | | ++-------------------------+-----------------------------+------------------------+ +| Switch (“Device B”) | 192.168.0.99 | | ++-------------------------+-----------------------------+------------------------+ +| Neighbour (“Device D”) | 192.168.0.98 | To port 2 of DUT | ++-------------------------+-----------------------------+------------------------+ +| Neighbour (“Device E”) | 192.168.0.97 | To highest port of DUT | ++-------------------------+-----------------------------+------------------------+ +| Power outlet | 192.168.1.244 | Separate network | ++-------------------------+-----------------------------+------------------------+ + + +Profinet-enabled switch +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Some of the test cases for the Automated RT Tester requires an Profinet-enabled +switch. It is called "Device B" in the test setup documentation. + +The test specification of version V 2.41 recommends the use of a +Siemens Scalance X204IRT (article number 6GK5204-0BA00-2BA3). +The ART Tester is rather strict regarding the model of Siemens Profinet switch. +It should have IP address ``192.168.0.99``, netmask ``255.255.255.0`` and station name "b". +Use for example Codesys to scan for the device, and to adjust the IP settings. +Alternatively, use SinecPni to change the IP address (see the Simatic +page in this documentation). + +The switch has a web interface, but it is not necessary to do any setting +adjustments via the web interface. +Log in to the web interface by directing your web browser to its IP address. +User name "admin", factory default password "admin". + +Connection of the switch ports is described in the table below: + ++-------------+-----------------------------------------------+ +| Switch port | Connected to | ++=============+===============================================+ +| P1 | Personal computer running Automated RT Tester | ++-------------+-----------------------------------------------+ +| P2 | IO-controller ("Device A" port X1 P1) | ++-------------+-----------------------------------------------+ +| P3 | Device under test (DUT) running p-net | ++-------------+-----------------------------------------------+ + +The Automated RT tester will detect "Device B" by itself. No configuration is +required in the Automated RT tester menu. + +The setting "Use IEC V2.2 LLDP mode" available via the STEP7 Profinet setup +tool controls the format of the sent portID in LLDP frames. +With the "Use IEC V2.2 LLDP mode" enabled the portID is sent as ``port-001``, +while it is sent as ``port-001.b`` if disabled. The latter format is used in +Profinet 2.3 and newer. The ART tester requires the LLDP format to be in the +2.2 format, otherwise it will complain about portID length. +One way to restore the behavior to the 2.2 format is to do a factory reset +of the switch via the web interface or by pressing the SET button for more than +20 seconds (if the button not is disabled in the web interface). + + +Remote controlled power outlet +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The Automated RT Tester can control an "Anel Net-PwrCtrl" power outlet via Ethernet. +It must be connected via a separate Ethernet +interface on the personal computer. Use a static IP address ``192.168.1.243`` with +subnet mask to ``255.255.255.0`` on that interface. + +The Power outlet has a default IP address of ``192.168.0.244``, and it has a +built-in web server. Enter its IP address in your web browser to log in +(username and password printed on the hardware). +(You might need to temporary set your Ethernet interface to IP ``192.168.0.1`` +and subnet mask to ``255.255.255.0``) +Modify the IP settings (on the "Einstellung" page) to use a static IP address +of ``192.168.1.244``. +On the "Steuerung" page you can control the individual power outputs. + +Connect power for your device under test to connector number 3 on the power outlet. + +Test the functionality from Automated RT Tester by clicking on the symbol to the +left of the "PowerOutlet" text in the tool bar. The symbol to the right of the +"PowerOutlet" text shows a green check mark when the outputs are on, and a +black cross when the outputs are off (or when the power outlet not is connected). + ++--------------+------------------------------------------------------------+ +| Power outlet | Connected to | ++==============+============================================================+ +| 1 | PLC "A" | ++--------------+------------------------------------------------------------+ +| 2 | Profinet enabled switch "B" | ++--------------+------------------------------------------------------------+ +| 3 | Device under test (DUT) running p-net | ++--------------+------------------------------------------------------------+ +| 4 | Neighbour device "D", connected to DUT port 2 | ++--------------+------------------------------------------------------------+ +| 5 | Neighbour device "E", connected to DUT highest port number | ++--------------+------------------------------------------------------------+ + + +Hardware naming +^^^^^^^^^^^^^^^ +Different types of Siemens hardware are used for the conformance test. +In order to simplify how the different units should be connected together, +a list of Siemens naming conventions is provided here: + +* AI: Analog input module +* AQ: Analog output module +* BA: Basic +* BA: Busadapter (with RJ45 or fiber optic connectors) +* BU: BaseUnit (for mounting input and output modules) +* CM: Communication module +* CU: Ethernet connector (copper wires) +* DI: Digital input module +* DP: Profibus DP +* DQ: Digital output module +* F-: Fail safe +* FC: Fast Connect (A bus adapter for network cables) +* HF: High feature +* HS: High speed +* IM: Interface Module +* L+: +24 V DC +* M: Ground connection +* MLFB: Article number (order number) Maschinen Lesbare Fabrikate Bezeichnung +* MP: ? +* P: Port +* PN: Profinet +* R: Ring port for media redundancy +* SM: Special module +* SP: Scalable Peripherals +* ST: Standard +* TM: Technology module +* X: Interface + + +Siemens IO-device for verification of multi-port devices +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +It is called "Device D" in the test setup documentation. + ++--------------------------------------+-------------------------------------------+ +| Part | Comments | ++======================================+===========================================+ +| Interface module ET200 IM155-6PN/2HF | | ++--------------------------------------+-------------------------------------------+ +| Digital output module DQ 132 | In slot 1 (closest to interface module) | ++--------------------------------------+-------------------------------------------+ +| Digital input module DI 131 | In slot 2 | ++--------------------------------------+-------------------------------------------+ +| Base uint A0 (24 VDC, light colored) | One for each input/output module | ++--------------------------------------+-------------------------------------------+ +| Bus adapter | With two RJ45 connectors | ++--------------------------------------+-------------------------------------------+ +| Server module | Delivered with the interface module. Put | +| | it in slot 3. | ++--------------------------------------+-------------------------------------------+ + +See the Profinet test specification for part numbers. + +Light-colored bus adapters are used for supply voltage distribution. +The cyan-colored (auxiliary) terminals on bus-adapters are all connected together. +If you only use light-colored bus adapters, then the cyan-colored terminals on +one bus adapter are isolated from the corresponding terminals on other bus adapters. + +Connect +24 V to the red terminals of the interface module and the base units. +Connect 0 V to the blue terminals of the interface module and the base units. + +Connect a button via wires to the digital input (DI) module. Connect it between +DI.7 (pin 18) and and +24 V. The LED ".7" on the input module will be green +when the button is pressed. + +The LED ".7" on the digital output module (DQ) will be green when the output +is high (+24 V). + +Use the Ethernet connector P1R. + +.. image:: illustrations/SimaticIoDevice.jpg + +See the page on setting up a Simatic PLC in this documentation for +instructions on usage. + +Make sure that the LLDP frames are in the legacy LLDP format, where the PortId +is "port-001" instead of "port-001.d". This is done via the setting +"Use IEC V2.2 LLDP mode", available in the STEP7 Profinet setup tool for PLCs. + + +Set up Cisco SF352-08P switch +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +For multiport Profinet devices, also SNMP-communication to non-Profinet +devices is verified. This Cisco switch can be used for that purpose. +Replaces "Device D" in the test setup. + +The system LED flashes during startup, and lights steady when the switch is +properly set up and running. +Note that the boot time can be around 100 seconds. + +Connect an Ethernet cable to port G1. +Set your laptop IP address to ``192.168.1.143`` and netmask to be ``255.255.255.0``. +Log in to ``192.168.1.254``. Default username is ``cisco`` and password is ``cisco``. +Change password when prompted. + +Set the IP address via the left side menu "IP configuration" -> "IPv4 Management and Interfaces" -> "IPv4 Interface". +Click "Add" and enter the static IP address ``192.168.0.98``. Use netmask ``255.255.255.0``. +The switch will change IP address to a new subnet, so you might need to change your +laptop network setting before connecting to the new IP address. + +Adjust LLDP settings via menu Administration -> "Discovery - LLDP" -> Properties. +In the page top bar, set "Display mode" to Advanced. Set "Chassis ID Advertisement" +to "MAC Address". + +Via Administration -> "Discovery - LLDP" -> "Port settings" select port FE1 and +click Edit. Enable SNMP notification. Select the optional TLVs that start with "802.3". + +Via the menu Security -> "TCP/UDP Services", enable "SNMP Service". + +In the page top bar, set "Display mode" to Advanced. +Add a SNMP community via the left side menu SNMP -> Communities and +click Add. The community string should be "public". Set "SNMP Management +Station" to "All". Click "Apply" and "Close". + +In the top of the page click the "Save" icon. + +For the actual measurements, use the port 1 on the Cisco switch. + +Verify the SNMP communication to the Cisco switch:: + + snmpwalk -v1 -c public 192.168.0.98 + + +Tips and ideas +-------------- +If you end up with ``Pass with Hint "The device made a EPM Request from a +not Profinet port"``, that means that wrong source port was used when sending +UDP messages. See the page on Linux in this documentation on how to adjust the +ephemeral port range. + +If your software version indicates that it is a prototype version (letter "P") +the Automated RT Tester will mark this as pass with hint. + +The Automated RT Tester has a convenient feature for remotely setting the +station name, IP address, netmask and gateway of the device under test (DUT). +Use the menu Tools > "Set DUT name and IP". +It will change the settings of the IO device via DCP communication. It is also +possible to do a factory reset of the IO device. + +It can be useful to show the ART tester log messages in the target output. +On your Linux target run (replace interface name if necessary):: + + tcpdump -i br0 udp port 514 -v -a + + +Reduce timeout values to speed up testing +----------------------------------------- +It is possible to reduce the timeout values used by Automated RT Tester. This +can be convenient during development, in order to speed up the tests. +Use the menu Tools > Options, and enable "Show expert settings". The time +settings are found on the "Expert Settings" tab. +The times are given in milliseconds. +Remember to use the default values when doing pre-certification testing. + +These values have large impact on test execution times: + +* ApplicationReadyReqTimeout +* ConnectResTimeout +* DcpResetToFactoryTestSetupTime +* DutBootUpTime +* ReleaseArResTimeout +* StandardTestSetupBootTime +* WriteResTimeout + + +Relevant test cases for conformance class A +------------------------------------------- + ++-------------------------------------------------+-----------------------------------------------------------------+ +| Test case | Notes | ++=================================================+=================================================================+ +| DCP_1 | Power cycle 8 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_2 | Power cycle 2 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_3 | Power cycle 2 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_4 | Fast | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_ALIAS | Requires additional hardware ("Device B") | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_IDN | Fast. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_NAME_1 | Power cycle 4 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_NAME_2 | Power cycle 4 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_ResetToFactory | | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_OPTIONS_SUBOPTIONS | | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_Router | | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_Access | Fast. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP_VLAN | Power cycle 2 times | ++-------------------------------------------------+-----------------------------------------------------------------+ +| DCP IP-parameter Remanence | Power cycle 4 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Behavior Scenario 1 to 9 | Power cycle | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Behavior Scenario 10 | Power cycle 7 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Behavior Scenario 11 | | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Different Access Ways | Requires additional hardware ("Device B") | ++-------------------------------------------------+-----------------------------------------------------------------+ +| PDEV_CHECK_ONEPORT | Requires additional hardware ("Device B"). Power cycle 3 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Diagnosis | Requires additional hardware ("Device B"). Power cycle twice. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Alarm | Requires additional hardware ("Device B") | ++-------------------------------------------------+-----------------------------------------------------------------+ +| AR-ASE | Power cycle | ++-------------------------------------------------+-----------------------------------------------------------------+ +| IP_UDP_RPC_I&M_EPM | Power cycle | ++-------------------------------------------------+-----------------------------------------------------------------+ +| RTC | Requires additional hardware ("Device B") | ++-------------------------------------------------+-----------------------------------------------------------------+ +| VLAN | Fast. Use port-to-port set up. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Different access ways port-to-port | Use port-to-port set up | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Manual: DCP_Signal | Flash Signal LED. Fast. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Manual: Behavior of ResetToFactory | Power cycle 4 times. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Manual: Checking of sending RTC frames | Fast | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Not automated: DataHoldTimer | PLC required. Use network tap at DUT. | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Not automated: Interoperability | PLC required | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Not automated: Interoperability with controller | PLC required | ++-------------------------------------------------+-----------------------------------------------------------------+ +| Security Level 1 | PLC required | ++-------------------------------------------------+-----------------------------------------------------------------+ + + +Relevant test cases for conformance class B +------------------------------------------- +Set the GSDML file attributes ``ConformanceClass="B"`` and +``SupportedProtocols="SNMP;LLDP"``. + +* Behavior scenario 10 +* Topology discovery check, standard setup. Requires additional hardware (“Device B”). +* Topology discovery check, non-Profinet-neighbour setup +* Port-to-port +* Behavior of reset to factory (manual) + + +Additional test cases for multi-port devices +-------------------------------------------- +Requires additional hardware ("Device B", “Device D” and Cisco switch). + +* PDEV_RECORDS +* Topology discovery check, standard setup. +* Topology discovery check, non-Profinet setup. Uses Cisco switch. Power cycle twice. + +For "port-to-port" testing on devices with multiple ports, connect the port +directly to the ART tester laptop. Leave other ports on the device not +connected. + + +Relevant test cases for legacy startup mode +------------------------------------------- +Legacy startup mode is defined in Profinet version 2.2 and earlier. +Set the attribute ``StartupMode`` in the GSDML file to ``"Legacy;Advanced"``. +Also the attributes ``PNIO_Version`` and ``NumberOfAR`` affects the ART tester +behavior. + +* SM_Legacy +* Different Access Ways +* Different Access Ways port-to-port +* DCP +* AR-ASE +* IP_UDP_RPC_I&M_EPM +* Behavior +* FSU (if also supporting fast startup) +* Interoperability (use a legacy PLC) +* Interoperability with controller (use a legacy PLC) + + +Relevant test cases for fast startup (FSU) +------------------------------------------ +Set the parameters ``ParameterizationSpeedupSupported="true"`` and +``DCP_HelloSupported="true"``. The attribute ``PowerOnToCommReady="700"`` +describes the startup time in milliseconds. + +* FSU +* Different Access Ways +* Manual FSU test case +* Hardware (no auto-negotiation) + + +Relevant test cases for DHCP +---------------------------- +In the GSDML file, set the ``AddressAssignment`` attribute to ``"DCP;DHCP"``. + +* DHCP + +Relevant test cases for MRP +--------------------------- +In the GSDML file, adjust the ``MediaRedundancy`` and ``SupportedRole`` +attributes. + +* MRP_1 +* MRP_2 +* MRP_chk1 +* MRP_chk2 +* MRP_OFF1 +* MRP_OFF2 +* MRP_On1 +* MRP_On2 + +These are named "MRP_Pdev", and are described in +"TCS_00004126_MRP_Pdev_ctrl_not_automated_V2.42.1.pdf". + +The PLC (CPU 1516-3 PN/DP) and the neighbour device (Interface module IM +155-6PN/2) used in the standard setup both have support for MRP. + + +Other tests +----------- +Your GSDML file should pass the verification with the "GSDMLcheck" tool. + + +Details on tests with PLC +------------------------- + +Load PLC program +^^^^^^^^^^^^^^^^ +Verify that the sample application PLC program is working properly with your +IO-device. Button1 should be able to control the state of data LED (LED1). + +Interoperability +^^^^^^^^^^^^^^^^ +Run with PLC for 10 minutes without errors. +If the device under test has more than one port, there should be 5 IO-devices +connected to the non-PLC port. + +For the cyclic data, use one input module and one output module. +The PLC should be programmed to continuously read out record data. + +The timing should be the fastest allowed according to the GSDML +file, and use 3 "accepted update cycles without IO data". +Record startup and data exchange using Wireshark. + +In the Wireshark file, make sure IOPS and IOCS in the cyclic data from the +IO-device have the value GOOD after it has sent the "application ready" +message. +Also verify that there have been no alarms (sort the frames by protocol). + +* "Record data"? +* ExpectedIdentification is equal to the RealIdentification? +* How to create additional net load? (using DCP Identify all) +* Implicit read? + +Data Hold Timer +^^^^^^^^^^^^^^^ +Run with PLC. The timing should be the fastest allowed according to the GSDML +file, and use 3 "accepted update cycles without IO data". +Record startup and data exchange using Wireshark. + +Unplug network cable from the PLC. + +In the Wireshark file: + +* Count the number of cyclic data frames sent by the IO-device before the + alarm frame appears. It is allowed that 3-6 data frames are sent before + the alarm frame. +* At startup the first valid data frame should be sent within the data + hold time. +* The IOCS in the cyclic data from the IO-device should have the value GOOD + after the "application ready" message has been sent. +* Verify the data cycle time. + +Repeat the cable unplugging measurements with reduction ratios (1), 2, 4, 8 +and 16. With a cycle time of for example 1 ms this corresponds to a frame +send interval of 1 ms to 16 ms, and a data hold time of 3 ms to 48 ms. + +Check that a LLDP frame is sent within 5 seconds, and then every 5 seconds. +The TTL value in the LLDP frame should be 20 seconds. +The MAUtype, "autonegotiation supported" and "autonegotiation enabled" must +be correct. + +Interoperability with controller +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Run with PLC. The timing should be the fastest allowed according to the GSDML +file, and use 3 "accepted update cycles without IO data". +Record startup and data exchange using Wireshark. + +Verify that the outputs are according to the manual of your IO-device when +you do these actions (repeat several times): + +* PLC powered off +* PLC powered on. The program should be running. +* Switch the PLC to stop. +* Switch the PLC to run. +* Disconnect cable from PLC. +* Reconnect the PLC cable. + +In the Wireshark file, make sure IOPS and IOCS in the cyclic data from the +IO-device have the value GOOD after it has sent the "application ready" +message. + +* Record data? + + +Security Level 1 tester +----------------------- +A PLC program is used to both establish cyclic data communication, and to +continuously read out parameter values from the IO-device under test (DUT). +If the DUT has more than one port, a neighbour device "D" is connected to +port 2, and the PLC will control the digital inputs and outputs of device D. + +A program running on a Linux laptop will generate additional network load. +Depending on the result, the DUT will be assigned net load class 1 to 3. + +See also the guideline "PROFINET IO Security Level 1". + +It can be useful to run the other PLC-based tests before, to find out the +shortest cycle time useful when there is no additional network load. + +PLC program +^^^^^^^^^^^ +Use the STEP7 project from the test bundle, and import it into the TIA portal. +In Siemens TIA Portal, open the file "normal_d_V2.40.0_V15.1.zap15_1" as +an existing project. Give the path to a local directory that will be used +for the project. + +The block "Main [OB0]" will call the "FC001_Test_Programm", and it will also +read and set the digital inputs and outputs on neighbour device D. + +Data block "DB003_RECORD_Index" contains an array of record numbers (parameters) +that are to be read from the IO-device. It also contains a counter value +keeping track of which entry in the array that is being used right now. +The block "Startup" will initialize this counter value. + +The "FC001_Test_Programm" function will call "FB001_RDREC", and then +will the counter value be increased. + +The "FB001_RDREC" function block will read out a parameter from the IO-device +and store any error value. It should be maximum 100 ms from one response +until the next request is sent out by the PLC. + +Data block "DB002_RECORD_data" has a large array of bytes for storing the +record data read from the IO-device. + +Device D should have a digital input module and a digital output module. It +should have a cycle time of 1 ms and an IP address ``192.168.0.98``. +If the device "D" is not exactly the variant you have, you need to replace it +with another IO-device with digital inputs and outputs. Default connection: + +* Input ``%I0.0`` Ix_Req (Enables continuous readout of parameter values) +* Input ``%I0.1`` Ix_ACK (Acknowledges errors) +* Output ``%Q0.0`` Qx_Error +* Output ``%Q0.1`` Qx_Error_RDREC + +Delete the existing "dut" device. +Import the GSDML file of your device (the DUT), and insert your device. +Plug relevant modules into the slots. +Give it the station name ``dut``, and it should use the IP address +``192.168.0.50``. Connect it to the PLC via the "Network view". + +In the "Device view" select the DUT, and in the "Device overview" select the +DUT line. In Properties > "System constants" find the hardware identifier +number for the line "dut~HEAD". + +Adjust cyclic time setting of the DUT. + +In the "Main [OB1]" block make sure that the hardware identifier is set to +the relevant value:: + + Ihw_ID := "dut~HEAD", + +or to the value found above (for example):: + + Ihw_ID := 261, + +Create a watch table for the relevant entries. + +Compile the hardware configuration and the software, and download to the PLC. + +If the DUT only has one port the neighbour device "D" is not needed. +Normally one of the digital inputs of device "D" is enabling the continuous +reading out of parameter values, but you need instead to force the corresponding +PLC program variable to a high level. In the left side menu use "Watch and +force tables" (below the PLC), and add a new entry by clicking an empty row +in the "Name" column, and select "Ix_Req". When later in online mode, click +the small "Show/hide all modify columns" to show the column. Enter "TRUE" in +the "Modify value" on the line for "Ix_Req". Select that line by enabling +the corresponding check box. Press the "Modify all selected values once +and now". + + +Tester software for additional network load +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Install the tester software on an Ubuntu machine, or in a virtual Ubuntu +machine running on Windows. The IP address should be ``192.168.0.30``. +See the PDF in the "Security Level 1"/"tester" folder in the downloaded +test bundle. The program ends up in ``/root/Netload``. See the PDF +how to start the program. + +The SL1-tester has a number of template ``.pcap`` files, and rewrites those +files with the MAC address of the SL1-tester laptop and the DUT. +Rewriting is done using the Bittwist editor tool (``bittwiste``). +The MAC of the DUT is found with the ``arping`` Linux command. +Actual sending of frames is done with the ``packETHcli`` Linux command. + + +Set up hardware +^^^^^^^^^^^^^^^ +Set the station name of the DUT to ``dut`` and the IP address to ``192.168.0.50``. +Use a temporary station name, to be able to detect device reboots. + +Set the station name of device D to ``d`` and the IP address to ``192.168.0.98``. + +The digital input "Ix_Req" is used to enable continuous read out of parameter +values. Set it to high level to start the readout. +Verify that there is cyclic communication, and that there is repeated +acyclic data read out. + +Use a non-Profinet switch (no LLDP packet filtering) to connect the device +under test (DUT, port 1), the PLC and the personal computer running the Security +Level 1 tester software. Connect neighbour device D to port 2 of the DUT. + +Synchronize clock of the PLC with the the clock of the laptop running the +tester software, as we later read the diagnostic log of the PLC. + +Run the tests +^^^^^^^^^^^^^ +For a class B device with two ports you need to run one "normal" mode test and +one "faulty" mode test. After the "faulty" mode the communication should be +good again after the additional network load has stopped. +During the "normal" mode the communication should not be lost, and this is +verified by studying the diagnostic log of the PLC afterwards. + ++----------------+---------------+---------------+-----------------------------------+ +| Operation mode | Run time | Test cases | Bus load | ++================+===============+===============+===================================+ +| Normal | 1 hour 40 min | 101-118, 1-63 | Up to 10%, based on netload class | ++----------------+---------------+---------------+-----------------------------------+ +| Faulty | 1 hour 20 min | 1-63 | 100% | ++----------------+---------------+---------------+-----------------------------------+ + +For the "faulty" test set up, there is no difference between the different netload classes. + +In the TIA portal, make sure you are "Offline" with the PLC (otherwise there +will be even more additional network load). + +Make sure that the software you run on the DUT has the correct compile time settings, +for example log level. + +* Verify that the PLC clock setting is correct. +* Set the device name to ``dut`` with temporary setting +* Start the PLC program, and verify that the parameter readout is running + (using Wireshark). +* Start the SL1-tester in "faulty" mode. +* Verify that the PLC communication still is good after the SL1-tester is done. +* Start the SL1-tester in "normal" mode. +* Verify that there hasn't been any communication breakdown, by looking in + the PLC diagnostic buffer (via TIA portal). + +If the device under test has a 100 Mbit/s, use the 100 Mbit/s setting in the test +program otherwise 1 Gbit/s. + +Each test case has a duration of 1 minute, except case 15 - 16 and 116 - 117 +which runs for 3 minutes each. Test case 101 and 102 runs until the sequence +is completed. If the tests take longer than that, wrong settings have been +used for the SL1-tester. + +Test cases for Netload class I. Duration is 1 minute, except where noted otherwise: + +========= ===================================== ================== ================== +Test case Description "Normal" packets/s "Faulty" packets/s +========= ===================================== ================== ================== +101 DCP scanning (manual review) Approx 5 - +102 ARP scanning (manual review) Approx 5 - +103-104 Ping 10 - +105-108 UDP malformed 10 - +109 IP malformed 10 - +110-114 SNMP 10 - +115 UDP malformed 10 - +1-6 Configuration test protocol "CRAP" 820-15000 8000-150000 +7-8 Ping 10-15 110000-150000 +9-11 TCP 1-15 8000-150000 +12-14 UDP 8-150 8000-150000 +15-16 ARP, 3 minutes 50 150000 +17 ARP 15 150000 +18-20 UDP 0.8-15 8000-150000 +21 LLDP 8 87000 +22-23 Ping 10000-15000 110000-150000 +24-26 TCP 800-15000 8000-150000 +27-29 UDP 800-15000 8000-150000 +30 DHCPv6 7 70000 +31-33 ICMPv6 11-14 110000-140000 +34 LLMNR 10 98000 +35 MDNS 11 110000 +36 SSDP 6 62000 +37-42 Profinet cyclic data (RTC) 0.8-15 8000-150000 +43-44 Profinet alarm (RTA) 15 150000 +45-50 Profinet cyclic data (RTC) 800-15000 8000-150000 +51-52 Profinet alarm (RTA) 14000-15000 150000 +53-54 DCP 50-70 100000-150000 +55 PTCP 14 140000 +56-60 MRP 1400-1500 140000-150000 +61-63 PTCP 15-140 1100000-150000 +========= ===================================== ================== ================== + +Calculation example for 100 Mbit/s, which allows approximately 10 MByte/s payload: + +============= ========== +Packet size Packets/s +============= ========== +10 Bytes 1000000 +100 Bytes 100000 +1000 Bytes 10000 +============= ========== + +If there are problems during the "normal" mode, study the error LED on the +PLC to detect which scenario that is causing the malfunction. + +To find which of the test case that is causing any problems in the "Faulty" +test suite, you can increase the time between the test cases to 30 seconds. +Then the communication will have time to recover between the test cases, +and you can verify communication using the LEDs of the PLC. +If the error LED (on the PLC) does not turn off after a specific test case, then +you have found the test case that causes permanent communication breakdown. + +Modify this line in the ``/root/Netload/Scripts/Netload-Tester.sh`` script:: + + # -> Delay between the individual frame sequences + Delay="30s" + + +Troubleshooting +--------------- +For the "Get MAC Address" button in ART tester to work, you need to +have the "Device b" connected. + +If the test case "Different access ways port-to-port" fails, +verify that your laptop Ethernet interface speed is set to 100 Mbit/s and +that any Windows or Siemens (TIA portal) LLDP implementation on the +Ethernet interface is disabled. + +If you in the test case "Different access ways port-to-port" end up with +the error "NumberOfPeers is not valid" make sure that the device "D" is +disconnected from port 2 of your device under test. + +If the "Alarm" test case fails with the message "No valid Transport ACK on +the Alarm ACK was received", then the Ethernet receive task probably +has too low priority or too limited resources. +Note that if increasing the priority, several alarm frames might be +queued up before the main application have a chance to +handle them. As the ART Tester sends a burst of 6 alarm frames in the APMS +scenario, make sure that your alarm input queue can hold at least that number. + +If there are problems in test case Behavior scenario 10 regarding reading and +writing via SNMP after power cycling, verify that there is proper connection to +the SNMP daemon immediately at startup. + +If you get the error "Device did answer ping request although it shouldn't", the device is probably not powered +off during the test as required. Make sure your device is not powered via some USB cable, for example. + +If the Alarm test case fails with the message "No valid Transport ACK on the Alarm ACK was received", +then the Ethernet receieve task might have too low priority. diff --git a/profinet_stack/p-net/doc/conf.py b/profinet_stack/p-net/doc/conf.py new file mode 100644 index 0000000..6639cc7 --- /dev/null +++ b/profinet_stack/p-net/doc/conf.py @@ -0,0 +1,94 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import time + +# Workaround for issue https://github.com/sphinx-contrib/googleanalytics/issues/2 +# Note that a warning still will be issued "unsupported object from its setup() function" +# Remove this workaround when the issue has been resolved upstream +import sphinx.application +import sphinx.errors +sphinx.application.ExtensionError = sphinx.errors.ExtensionError + + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'p-net' +copyright = '2020, rt-labs' +author = 'rt-labs' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "breathe", + "recommonmark", + "rst2pdf.pdfbuilder", + "sphinx_rtd_theme", + "sphinxcontrib.spelling", + "sphinxcontrib.googleanalytics", +] + +needs_sphinx = "1.8" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +spelling_word_list_filename = "spelling_wordlist.txt" + +googleanalytics_id = "UA-4171737-2" + +googleanalytics_enabled = True + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['static'] + +html_last_updated_fmt = '%Y-%m-%d %H:%M' +breathe_projects = { project: "../build/xml/" } +breathe_default_project = project + +html_css_files = [ + '../../css/custom_rtd.css', # Requested by web developer + 'css/fix_table_width.css', + 'css/change_header_size.css' + ] + + +# -- Options for PDF output ------------------------------------------------- + +pdf_filename = "{}_{}".format(project.replace("-", ""), time.strftime("%Y-%m-%d")) +pdf_title = "{} Profinet device stack".format(project) +pdf_documents = [('index', pdf_filename, pdf_title, author),] + +pdf_break_level = 2 diff --git a/profinet_stack/p-net/doc/creating_gsdml_files.rst b/profinet_stack/p-net/doc/creating_gsdml_files.rst new file mode 100644 index 0000000..daf886e --- /dev/null +++ b/profinet_stack/p-net/doc/creating_gsdml_files.rst @@ -0,0 +1,818 @@ +Creating GSD (GSDML) files +========================== +A GSD (General Station Description) file is an XML file describing a +Profinet IO-Device. The XML-based language is called GSDML (GSD Markup Language). + +A single GSDML file describes a single VendorID and ProductID combination. +A family of devices can be described in the same file; Then they will share +the ProductID but have different DAP (head module) ID values. + +For Profinet members, the "Profinet GSD Checker" tool is available for +download. It contains GSD example files showing different aspects of the file +format. + +To create a custom GSD file, start from one of the examples. Edit the file in +an XML editor. Then run the resulting file in the "Profinet GSD Checker" tool +to verify its structure. + +Below is a brief introduction to the most common elements and attributes in +GSDML files given. It contains simplifications and does not describe special +cases. For full details on GSDML file syntax, see the official GSDML +specification. + +An example GSDML file is available in the samples/pn_dev directory of the p-net +repository: https://github.com/rtlabs-com/p-net/tree/master/samples/pn_dev + +A GSDML file is used to describe an IO-device. The corresponding file +for describing a controller is a CDML (Controller Description Markup Language) +file (with file ending ``.xml``). The CDML is used during controller certification +(not discussed here). + + +XML editors +----------- +It can be helpful to use an XML editor when working with GSD files. A few +examples are given here. + +**QXmlEdit**: +http://qxmledit.org/ (Windows and Linux) +It can be installed via Ubuntu Software Store. + +**CAM editor**: +http://camprocessor.sourceforge.net/wiki/ (Windows and Linux) + +**XML Notepad**: +https://github.com/microsoft/xmlnotepad (Windows) + + +Using "Profinet GSD Checker" tool on a Windows PC +------------------------------------------------- +The tool is available for Profinet members. + +The file is part of the conformance test tool download. Unzip the file +PROFINET_GSD_Checker_2.34.zip, and double-click the "setup" file. After +installation, enter ``profinet`` in the Windows search bar to start the program. + +Use the menu File > Open and select your GSD file. An overview of the file +is shown. In the left menu different parts of the file can be selected. + +To verify the file, use the menu File > Check. Click the "GSDCheck" bar at +the bottom of the screen to open a document check view in the right part of +the screen. Use the Icons to show the details of any error messages. + +If you later need to uninstall the tool, do that via Windows settings > Apps & +features. + + +File name +--------- +The current GSD version (as of writing) is 2.4. + +The file name format is specified in section 5.1 of the GSD specification. For example +``GSDML-V2.35-Vendor-Device-20171231.xml``. You can give it an optional timestamp +in the filename: ``-20171231-235900.xml``. + +Note that version for example 2.3 is newer than version 2.25. From the GSDML +specification: "V2.2 < V2.25 < V2.3 < V2.31" + + +In-file comments +---------------- + +.. highlight:: xml + +XML comments are written:: + + + + + +.. highlight:: none + + +Entering values +--------------- +Values are written in double quotes. Different attributes have different types: + +* ValueList: To enter several values (including a range of values), write ``"0..4 7 9"``. +* TokenList1: Semicolon separated values, for example ``"Legacy;Advanced"``. + + +File structure +-------------- +The GSD files are structured like:: + + ISO15745Profile + | + +--ProfileHeader + +--ProfileBody + | + +--DeviceIdentity + | | + | +--InfoText + | +--VendorName + | + +--DeviceFunction + | | + | +--Family + | + +--ApplicationProcess + | + +--DeviceAccessPointList + +--ModuleList + +--SubmoduleList + +--ValueList + +--LogBookEntryList + +--CategoryList + +--ChannelDiagList + +--ChannelProcessAlarmList + +--UnitDiagTypeList + +--GraphicsList + +--ExternalTextList + +The encoding is given in the first line of the XML file. Use ``utf-8`` if using non-ASCII characters in any of the text fields. +Do not change the ```` contents. + +In the ```` adjust the attributes ``VendorID``, ``DeviceID``. +Adjust the ``Value`` in ````. +In the ```` adjust the attributes for ``MainFamily`` (typically "I/O", should be from the list of allowed values) +and ``ProductFamily`` which is vendor specific. + +Those values are for example used by the engineering tool to display the device +in the hardware catalog. The catalog is typically sorted by +"Profinet IO"//// + +Details on the DeviceAccessPoint +-------------------------------- +This part of the file deals mainly with communication settings. + +DeviceAccessPointList element hierarchy:: + + DeviceAccessPointList + | + +--DeviceAccessPointItem + | + +--ModuleInfo + | | + | +--Name + | +--InfoText + | +--VendorName + | +--OrderNumber + | +--HardwareRelease + | +--SoftwareRelease + | + +--CertificationInfo + +--SubslotList + | | + | +--SubslotItem + | + +--IOConfigData + +--UseableModules + | | + | +--ModuleItemRef + | + +--ARVendorBlock + | | + | +--Request + | | + | +--Const + | + +--VirtualSubmoduleList + | | + | +--VirtualSubmoduleItem + | | + | +--ModuleInfo + | | | + | | +--Name + | | +--InfoText + | | + | +--IOData + | +--RecordDataList + | | + | +--ParameterRecordDataItem + | | + | +--Name + | +--Const + | +--Ref + | + +--SystemDefinedSubmoduleList + | | + | +--InterfaceSubmoduleItem + | | | + | | +--ApplicationRelations + | | | + | | +--TimingProperties + | | + | +--PortSubmoduleItem + | | + | +--MAUTypeList + | | + | +--MAUTypeItem + | + +--Graphics + | + +--GraphicItemRef + +The ```` element has the attributes: + +* ``ID="IDD_1"`` +* ``PNIO_Version="V2.4"`` Which version of Profinet specification it is + certified against. +* ``PhysicalSlots="0..4"`` Slot 0 is always used by the DAP (bus interface) + module. Relates to the ``PNET_MAX_SLOTS`` value in the p-net stack. +* ``ModuleIdentNumber="0x00000001"`` Unsigned32hex. +* ``MinDeviceInterval="32"`` Minimum cyclic data update interval, in number + of 31.25 us ticks. A value 32 corresponds to cyclic data sending and + receiving every millisecond. Unsigned16. It should match the value + ``min_device_interval`` in the p-net configuration. (It must be possible to + achieve this time using the values in the ``SendClock`` and ``ReductionRatio`` + attributes in another element). +* ``DNS_CompatibleName="pno-example-dap"`` (Default station name) +* ``FixedInSlots="0"`` The DAP module is always in slot 0 +* ``ObjectUUID_LocalIndex="0"`` +* ``DeviceAccessSupported="false"`` If a limited version of AR connection is allowed. +* ``NumberOfDeviceAccessAR="1"`` Number of Device Access connections. Should only + be given if ``DeviceAccessSupported`` is ``true``. Dependent on the ``PNET_MAX_AR`` + value in the p-net stack. +* ``MultipleWriteSupported="true"`` Multiple writes in a single request. + Mandatory ``true`` since V2.31. +* ``SharedDeviceSupported="false"`` False if not given +* ``SharedInputSupported="false"`` False if not given +* ``RequiredSchemaVersion="V2.3"`` This file has features requiring this schema + version. It must be at least 2.3 if legacy startup mode not is supported. +* ``CheckDeviceID_Allowed="true"`` If the VendorID and DeviceID are fine grained + enough to verify that the same type of device is used at replacement. +* ``NameOfStationNotTransferable="false"`` +* ``LLDP_NoD_Supported="true"`` Mandatory ``true`` since V2.31. +* ``ResetToFactoryModes="1..2"`` Bits describing reset possibilities. At least + "2" should be present. Reset modes 1 and 2 are supported by p-net. +* ``ParameterizationSpeedupSupported="true"`` For fast startup. +* ``PowerOnToCommReady="700"`` For fast startup, time to first data exchange + in milliseconds. Unsigned32. +* ``AddressAssignment="DCP"`` Can also be ``"DHCP"`` and ``"LOCAL"``. Defaults + to DCP if not given. + +General info on the Profinet IO-Device is given in ```` +subelements. For example the vendor name and order number are given. + +The ```` element has the attributes: + +* ``ConformanceClass="B"`` +* ``ApplicationClass=""`` Typically empty, but can be for example "FunctionalSafety" +* ``NetloadClass="I"`` + +With ```` elements it is possible to give names to subslots. Each +element has the attributes ``SubslotNumber`` and ``TextId``. + +The ```` element has the attributes: + +* ``MaxInputLength="24"`` Unsigned16, valid 0..1440 +* ``MaxOutputLength="24"`` Unsigned16, valid 0..1440 +* ``MaxDataLength="40"`` Defaults to MaxInputLength + MaxOutputLength. Unsigned16. + +The values are in bytes and are for all submodules. For details on how to +calculate these, see the GSDML specification. + +Which modules that can be used in the slots are given by the +```` elements. Each has the attribute ``ModuleItemTarget``, +which is a reference to a module (as described below). The attribute +``AllowedInSlots`` is a space separated list of slots that module type can be +used in. If the module type is permanently fixed in slots, then the attribute +``FixedInSlots`` is used instead. + +The ```` element is optional, and is used for global parameters. +These are sent from the IO-controller (PLC) during communication start. +The ```` element has the attributes ``Length`` (in bytes) and +``APStructureIdentifier="0"``. +Data is stored in the ```` element, with the attribute +``Data="0x00,0x00,0x00,0x01"``. + +The DAP (bus interface) module can have (non-removable = virtual) submodules. +See ```` below for a general description on submodules. +One specific detail for a DAP virtual submodule is that it has the +``Writeable_IM_Records="1 2 3"`` attribute, which informs about writable +Identification & Maintenance (I&M) records. Note that record 0 and 5 are +read only, so they should never appear in this list. +You must support writing to I&M1-3 for at least one of the DAP submodules. + +Other special submodules for DAP modules are ```` and +````, both subelements to ````. +Each interface defines for example clock synchronization, and the ports (of that +interface) define for example if they use radio or 100 Mbit/s copper cables. + +The subslot number for the first interface is 0x8000, and next interface (if +any) has subslot number 0x8100. Note that p-net only supports one interface. +The first port of the first interface has subslot 0x8001, +and next port of that interface has subslot number 0x8002. + + +Interfaces are described using the ```` element, which +has these attributes: + +* ``ID="IDS_I"`` +* ``SubmoduleIdentNumber="0x00000002"`` Unsigned32hex. +* ``SubslotNumber="32768"`` This is first interface (0x8000). Unsigned16. +* ``TextId="IDT_NAME_IS"`` +* ``SupportedRT_Classes="RT_CLASS_1"`` +* ``SupportedProtocols="SNMP;LLDP"`` Conformance class B must support SNMP. +* ``PTP_BoundarySupported="true"`` +* ``DCP_BoundarySupported="true"`` +* ``DCP_HelloSupported="true"`` Used for fast startup. + +If the ``DCP_HelloSupported`` attribute is set to ``"true"``, you must also +set the ``PowerOnToCommReady`` attribute of the ```` +element. + +The communication startup is described in the element ```` +with the attribute ``StartupMode``, which typically should be "Advanced" (the +alternative is "Legacy"). If supporting both modes, use a semicolon separated +list. The ``NumberOfAR`` attribute defaults to 1 if not given. + +There is typically one input-CR and one output-CR per AR, but in the GSDML file +it is possible to set the attributes ``NumberOfAdditionalInputCR``, +``NumberOfAdditionalOutputCR``, ``NumberOfAdditionalMulticastProviderCR`` and +``NumberOfMulticastConsumerCR``. Those attributes are not supported by p-net. + +The ```` element describes the sending of cyclic IO data. +The ``SendClock`` attribute contains a list of all supported send cycle times, +in units of 31.25 us. Defaults to "32", which corresponds to 1 ms. Note that the +list must contain the value ``32``. +The attribute ``ReductionRatio`` defines how much the sending can be slowed down, +and it seems that it must be “1 2 4 8 16 32 64 128 256 512”. +The actual speed of the device should be set by the ``MinDeviceInterval`` attribute in another element. + +Ethernet port properties are descried using the ````, which +has these attributes: + +* ``ID="IDS_P2"`` +* ``SubmoduleIdentNumber="0x00000003"`` Unsigned32hex. +* ``SubslotNumber="32770"`` This is second port on first interface (0x8002). Unsigned16. +* ``TextId="IDT_NAME_PS2"`` +* ``MaxPortRxDelay="350"`` Time delay in ns needed for receiving frames. Unsigned16. +* ``MaxPortTxDelay="160"`` Time delay in ns needed for sending frames. Unsigned16. + +Use an ```` element to describe the Medium Attachment Unit type, +which can be radio (0), copper at 100 Mbit/s (16), copper at 1000 Mbit/s (30) +or fiber optics etc. + +With the ```` element it is possible to add a logo or other symbol +to your device. It will appear in the engineering tool. Use a file in the .BMP file format. +The attribute ``Type`` should typically be ``DeviceSymbol``, and the ``GraphicItemTarget`` +is a reference to the file name given in the ```` (see below). + +Additional ports +---------------- +Additional physical ports are created by adding ```` nodes +to the ```` node. +The ID, submodule identity number and subslot number shall be unique for +all ports. + + +Details on the module list +-------------------------- +Profinet field devices can have different hardware modules, therefore there is +a need to be able to describe those modules. There are also field devices with +non-modifiable hardware, and they are sometimes called compact devices. Also +they are described using modules (fixed in slots, as mentioned above). + +ModuleList element hierarchy:: + + ModuleList + | + +--ModuleItem + | + +--ModuleInfo + | | + | +--Name + | +--TextId + | +--InfoText + | +--OrderNumber + | +--HardwareRelease + | +--SoftwareRelease + | + +--UseableSubmodules + | | + | +--SubmoduleItemRef + | + +--VirtualSubmoduleList + | + +--VirtualSubmoduleItem + | + +--ModuleInfo + | | + | +--Name + | +--InfoText + | + +--IOData + | + +--Input + | | + | +--DataItem + | + +--Output + | + +--DataItem + | + +--BitDataItem + +Each ```` element has the attributes ``ID`` (for example "IDM_1"), +``ModuleIdentNumber`` and ``PhysicalSubslots``. The last attribute is a space +separated list of its subslot numbers. + +The element ```` has information on the module name in its +subelements. The elements ```` and ```` have +``Value`` attributes. + +The value for should correspond to the configuration values +``im_sw_revision_prefix``, ``im_sw_revision_functional_enhancement``, +``im_sw_revision_bug_fix`` and ``im_sw_revision_internal_change``. + +Each ```` element has the attributes ``SubmoduleItemTarget`` +(which is a reference to a submodule) and ``AllowedInSubslots`` (which is a +space separated list of subslot numbers). + +Virtual submodules are submodules that are built-in into a module (no physical +submodule can be removed). If only virtual submodules are available, the +``PhysicalSubslots`` attribute is not given in ````. +For details on ````, see ```` below. + +The configuration value PNET_MAX_SUBSLOTS defines the maximum number of +submodules (for each module) that the p-net stack can handle. + + +Details on the submodule list +----------------------------- +Some submodules are permanent parts of modules, and are then called virtual +submodules. + +SubmoduleList element hierarchy:: + + SubmoduleList + | + +--SubmoduleItem + | + +--ModuleInfo + | | + | +--Name + | +--InfoText + | +--OrderNumber + | + +--IOData + | | + | +--Input + | | | + | | +--DataItem + | | + | +--Output + | | + | +--DataItem + | | + | +--BitDataItem + | + +--RecordDataList + | + +--ParameterRecordDataItem + | + +--Name + +--Ref + +--Const + +--MenuList + | + +--MenuItem + | + +--Name + +--MenuRef + +--ParameterRef + +Each ```` has the attributes ``ID`` (for example "IDS_1"), +``SubmoduleIdentNumber`` and ``MayIssueProcessAlarm`` (which can be "true" or +"false"). The element ```` might have an attribute ``CategoryRef``, +and also has subelements with information on the submodule name etc. + +The ```` and ```` elements have the optional attribute +``Consistency``, which can be "Item consistency" (default if not given) or +"All items consistency" (consistency between all input/output fields for +the submodule). + +The ```` elements have the attributes ``TextId`` and ``DataType`` +(which can be for example "Unsigned8", "Unsigned64", "Float32", "Integer8", +"Date", "VisibleString", "Boolean" or "TimeStamp"). The optional +attribute ``UseAsBits="true"`` is used when individual bits are to be displayed +in the engineering tool (only for the unsigned ``DataType`` variants). It is +recommended to use Unsigned8 when packing booleans. + +Use ```` elements to name the individual bits, by setting the +attributes ``TextId`` and ``BitOffset`` (which is a string, for example "0"). +The least significant bit has offset 0. + +A module parameter is typically adjustable from the IO-controller, and could +be used to set for example an input delay time. To describe parameters use +```` elements. They have the attributes +``Index="123"`` and ``Length="4"`` (in bytes). +Use the ```` subelement to give it a name. +To initialize the content, use the ```` element (if subelement +```` not is given). +The subelement ```` has these attributes: + +* ``DataType="Unsigned32"`` +* ``ByteOffset="0"`` +* ``DefaultValue="0"`` +* ``AllowedValues="0..99"`` +* ``Changeable="true"`` Whether changes of this parameter is allowed. +* ``Visible="true"`` Whether it should be visible in the engineering tool. +* ``TextId="DEMO_1"`` +* ``ValueItemTarget="IDV_InputDelay"`` Optional, to reference an enum (see ````). + +Each module parameter can contain several ```` subelements, as long as +they fit in the total ``Length`` size. + +It is possible to connect parameter values to enums for use in menus in +engineering tools. This is done via the ```` element (and +subelements). + +============================= ============== ============ =============== ====================== +Data type GSDML name Size (bytes) Name in Codesys Name in Simatic Step 7 +============================= ============== ============ =============== ====================== +Bit (part of lager data) (none) (none) BIT +Boolean (added in ver 2.32) Boolean 1 BOOL +Integer 8 bits Integer8 1 SINT SINT +Integer 16 bits Integer16 2 INT INT +Integer 32 bits Integer32 3 DINT DINT +Integer 64 bits Integer64 4 LINT LINT +Unsigned int 8 bits Unsigned8 1 USINT, BYTE USINT, BYTE +Unsigned int 16 bits Unsigned16 2 UINT, WORD UINT, WORD +Unsigned int 32 bits Unsigned32 4 UDINT, DWORD UDINT, DWORD +Unsigned int 64 bits Unsigned64 8 ULINT, LWORD ULINT, LWORD +Float Float32 4 REAL REAL +Double precision float Float64 8 LREAL LREAL +============================= ============== ============ =============== ====================== + + +Details on the value list +------------------------- +The value list is optional. It is a storage of enum values. + +ValueList element hierarchy:: + + ValueList + | + +--ValueItem + | + +--Help + | + +--Assignments + | + +--Assign + +Each enum is described in a ```` element with an ``ID`` attribute. +Each enum value is then given in an ```` element with attributes +``TextId`` and ``Content`` (with a numerical value given as a string, +for example ``"5"``). +It is also possible to give a help text by using the ```` element with +a ``TextId`` attribute. + + +Details on the LogBook entry list +---------------------------------- +This is optional, and is used to give human-readable descriptions to +manufacturer-specific error codes. + +LogBookEntryList element hierarchy:: + + LogBookEntryList + | + +--LogBookEntryItem + | + +--ErrorCode2Value + | | + | +--Name + | + +--ErrorCode2List + | + +--ErrorCode2Item + | + +--Name + +A ```` has an attribute ``Status="2130432"`` that is the +decimal version of the (hex) status value 0x208200. Those are the bytes +ErrorCode, ErrorDecode and ErrorCode1. The subelements ```` +and ```` connects it to a text entry. + +Some error conditions also require information from the ErrorCode2 byte. Then +the ```` element with attribute ``ErrorCode2="4"`` is used. + + +Details on the category list +---------------------------- +The category list is optional. It can be useful for storing categories like +"Digital input" and "Digital output". + +CategoryList element hierarchy:: + + CategoryList + | + +--CategoryItem + | + +--InfoText + +Each ```` element has the attributes ``ID`` and ``TextId``. +It has subelements ```` with the attribute ``TextId``. + +The category information is used in other elements by setting the attribute +``CategoryRef`` with the value given in the ``ID`` here. For example +```` elements can use category information. If a more detailed +categorization is required, then also the attribute ``SubCategory1Ref`` can +be used. + + +Details on the external text list +--------------------------------- +Human readable text strings are located here, and referenced to from the rest +of the XML file. This is for the strings to be easy to translate to other +languages. + +Remember to update the contents of all relevant texts when updating a GSDML +file. + +ExternalTextList element hierarchy:: + + ExternalTextList + | + +--PrimaryLanguage + | | + | +--Text + | + +--Language + | + +--Text + +Within each ```` element, the attributes ``TextId`` and ``Value`` +are used to store the information. + +Only ```` is mandatory, and corresponds to English. +If ```` is given, the actual +language is set by for example a ``xml:lang="fr"`` attribute. + +This is how the different text fields are used in engineering tools: + +======================= =================================================================================================================== +Item TIA portal and Codesys +======================= =================================================================================================================== +DeviceIdentity infotext Unclear? +DAP module name Device name in hardware catalog, on icon in network view and in Network overview table. Typically some model name. +DAP module infotext Device detailed description in hardware catalog. Use up to a few hundred characters. +DAP submodule name Codesys show this in the tree on the Device/IOxS page +DAP submodule infotext Unclear? +InterfaceSubmodule Interface name +PortSubmodule Port name +Module name List of plugged modules in Device Overview +Module infotext Catalog information in Properties -> General +Submodule name Codesys show this in the tree on the Device/IOxS page +Submodule infotext Unclear? +======================= =================================================================================================================== + + +Details on diagnosis +-------------------- +The elements ```` and ```` (with +subelements) are used to specify diagnosis functionality. + +Use the ```` element to describe diagnosis sent in the +standard format:: + + ChannelDiagList + | + +--ChannelDiagItem + | | + | +--Name + | +--ExtChannelDiagList + | | + | +--ExtChannelDiagItem + | | + | +--Name + | + +--SystemDefinedChannelDiagItem + | | + | +--ExtChannelDiagList + | | + | +--ExtChannelDiagItem + | | | + | | +--Name + | | +--ExtChannelAddValue + | | | + | | +--DataItem + | | + | +--ProfileExtChannelDiagItem + | | + | +--Name + | + +--ProfileChannelDiagItem + | + +--Name + +--ExtChannelDiagList + | + +--ProfileExtChannelDiagItem + | + +--Name + +To add a diagnosis with ChannelErrorType in the manufacturer specific range, +use the ```` element. Set the ChannelErrorType with the +attribute ``ErrorType="999"``, for example. Describe it using the +```` element. In the ```` element, use the +``ErrorType`` attribute for the ExtChannelErrorType. Describe the +ExtChannelErrorType using the ```` element. + +It is also possible to add your own ExtChannelErrorType to a standard +ChannelErrorType. Use the ```` element, +with the attribute ``ErrorType`` to specify the ChannelErrorType. Add +```` elements as we described in the previous paragraph. +The ExtChannelAddValue is specified with the ```` element, +and ```` subelements. Use ``Id`` and ``DataType`` attributes in the +subelements. + +Similarly use the ```` element to add +ExtChannelErrorType to diagnosis items defined in a profile. + +For diagnosis sent in the USI format (also known as manufacturer specific +format), use the ```` element:: + + UnitDiagTypeList + | + +--UnitDiagTypeItem + | | + | +--Name + | +--Ref + | + +--ProfileUnitDiagTypeItem + | + +--Name + +--Ref + +Add the USI value to the ```` element by using the +``UserStructureIdentifier`` attribute. +Specify the data content by setting attributes to the ```` element, +for example ``ByteOffset``, ``DataType``, ``DefaultValue`` and ``TextId``. + +It is also possible to add diagnosis in USI format for profiles, by using +the ```` element. Set the ``UserStructureIdentifier`` +and ``API`` attributes. + + +Details on the process alarm list +--------------------------------- +This is optional, and is used to give human-readable descriptions to +manufacturer-specific process alarms. + +ChannelProcessAlarmList element hierarchy:: + + ChannelProcessAlarmList + | + +--ChannelProcessAlarmItem + | | + | +--Name + | +--Help + | +--ExtChannelProcessAlarmList + | | + | +--ExtChannelProcessAlarmItem + | + +--SystemDefinedChannelProcessAlarmItem + | | + | +--ExtChannelProcessAlarmList + | | + | +--ExtChannelProcessAlarmItem + | +--ProfileExtChannelProcessAlarmItem + | + +--ProfileChannelProcessAlarmItem + | + +--Name + +--Help + +--ExtChannelProcessAlarmList + | + +--ExtChannelProcessAlarmItem + +--ProfileExtChannelProcessAlarmItem + +The element ```` is used to describe custom process +alarms. + +An extension to system defined process alarms is created by the element +````. Profiles can define process alarms +using the ```` element. + + +Details on the graphics list +--------------------------------- +File names for graphic items are mapped to IDs here. + +GraphicsList element hierarchy:: + + GraphicsList + | + +--GraphicItem + + +Within each ```` element, the attributes ``ID`` and ``GraphicFile`` +are used to store the information. + +The ``GraphicFile`` is the filename without the BMP file extension. diff --git a/profinet_stack/p-net/doc/doc_requirements.txt b/profinet_stack/p-net/doc/doc_requirements.txt new file mode 100644 index 0000000..ccdbbd9 --- /dev/null +++ b/profinet_stack/p-net/doc/doc_requirements.txt @@ -0,0 +1,8 @@ +sphinx +recommonmark +sphinx-rtd-theme +breathe +sphinxcontrib-googleanalytics +sphinxcontrib-spelling +rst2pdf +codespell diff --git a/profinet_stack/p-net/doc/getting_started_LAN9662.rst b/profinet_stack/p-net/doc/getting_started_LAN9662.rst new file mode 100644 index 0000000..3955914 --- /dev/null +++ b/profinet_stack/p-net/doc/getting_started_LAN9662.rst @@ -0,0 +1,380 @@ +LAN9662 +======= + +Introduction +------------ +Add description of LAN9662 when datasheet is available. + +For links to Microchip LAN9662 resources see section :ref:`lan9662-resources`. + +Known limitations: + +- One byte write operations are not supported from + LAN9662 RTE to the I/O FPGA. This means the sample submodule + Digital Output 1x8 is not working in full rte mode. +- Submodule default values are not applied on AR teardown + in full rte mode. + +LAN9662 Profinet Solution +------------------------- + +P-Net implements support for the networking hardware offload features in the LAN9662. +When hardware offload is enabled, Profinet cyclic data frames are handled by the LAN9662 Real +Time Engine (RTE). During AR / connection establishment the RTE is configured by the P-Net stack and +sending and receiving cyclic data is handed over the RTE. The P-Net stack still handles +all non-cyclic Profinet frames. + +The cyclic process data is defined by the Profinet device application. +In Profinet the device input data is sent to the controller using the Producer Protocol Machine (PPM) and +the output data is received using the the Consumer Protocol Machine (CPM). +For the PPM side the RTE is configured to read input data sources and build a frame +that is periodically sent to the controller. +For the CPM side the RTE is configured to extract output data in received +frames and write it to configured output sinks. + +The RTE process data can be mapped to SRAM or a QSPI HW device. + +When hardware offload is enabled all process data by default is mapped to SRAM +by P-Net and the application uses the P-Net API the same way as usual. + +To map the process data to a QSPI interface the application uses the P-Net API functions +``pnet_mera_input_set_rte_config()`` for input data and +``pnet_mera_output_set_rte_config()`` for output data. +When a QSPI data source or sink is configured the process data +is handled by the configured HW device and not by the application. +The P-Net stack still keeps a copy of the process data in SRAM +and the application can read the process data from this copy. + +All P-Net interactions with the LAN9662 RTE is done through the MERA library provided by Microchip. +Detailed information about the LAN9662 and the RTE can be found in the references in section :ref:`lan9662-resources`. + +Table below shows how the the P-Net API for handling the process data is affected when RTE is enabled. + ++---------------------------------+-----------+------------------------------------------------------+ +| API Function | RTE-SRAM | RTE-QSPI | ++=================================+===========+======================================================+ +| pnet_input_set_data_and_iops | Supported | Not supported. Input IO data from QSPI. | ++---------------------------------+-----------+------------------------------------------------------+ +| pnet_input_get_data_and_iops | Supported | Supported | ++---------------------------------+-----------+------------------------------------------------------+ +| net_input_get_iocs | Supported | Supported | ++---------------------------------+-----------+------------------------------------------------------+ +| pnet_output_get_data_and_iops | Supported | Supported for reading output IO data mapped to QSPI. | ++---------------------------------+-----------+------------------------------------------------------+ +| pnet_output_set_iocs | Supported | Supported | ++---------------------------------+-----------+------------------------------------------------------+ +| pnet_mera_output_set_rte_config | Not used | Used for configuration | ++---------------------------------+-----------+------------------------------------------------------+ +| pnet_mera_input_set_rte_config | Not used | Used for configuration | ++---------------------------------+-----------+------------------------------------------------------+ + + +Build-time configuration +------------------------ +Support for LAN9962 features in the P-Net stack is enabled using the following CMake options: + +- PNET_OPTION_DRIVER_ENABLE - Enable the P-Net support for hardware drivers in general +- PNET_OPTION_DRIVER_LAN9662 - Enable the support for the LAN9662 driver +- PNET_OPTION_LAN9662_SHOW_RTE_INFO - Print the RTE configuration to the application log + +The LAN9662 sample is built with the following P-Net options:: + + set(HAS_PNET ON) + set(BUILD_TESTING OFF) + set(PNET_OPTION_DRIVER_ENABLE ON) + set(PNET_OPTION_DRIVER_LAN9662 ON) + set(PNET_OPTION_LAN9662_SHOW_RTE_INFO OFF) + set(PNET_MAX_PHYSICAL_PORTS "2" CACHE STRING "LAN9962 2 Ports" FORCE) + set(PNET_MAX_SLOTS "13" CACHE STRING "LAN9962 sample application" FORCE) + set(PNET_MAX_SUBSLOTS "4" CACHE STRING "LAN9962 2 Ports" FORCE) + set(LOG_LEVEL "FATAL" CACHE STRING "Enable logging" FORCE) + + +Run-time configuration +---------------------- +The build-time configuration described in previous section enables the support for the LAN9662 driver. +The application decides if the hardware offload shall be used when p-net stack is initialized. +The hardware offload feature is enabled using the driver_enable parameter part of +the configuration (pnet_cfg_t) passed to P-Net during initialization. +When set to true, LAN9662 specific CPM and PPM drivers are used in the P-Net stack and handling of cyclic data frames +is handed off to the LAN9662 RTE during AR establishment. +The driver_config parameter contains base IDs for various resources in the LAN9662 RTE. +In a system where P-Net is the only feature using the RTE these don't need to be considered. +If not, the IDs must be set not to conflict with other parts of the system. + +Input data is mapped to QSPI using the operation ``pnet_input_set_rte_config()``. +Output data is mapped to QSPI using the operation ``pnet_output_set_rte_config()``. + + +EVB-LAN9662 +----------- +Both the default P-Net sample application and the LAN9662 sample application can be run on the EVB-LAN9662. +Utility scripts for configuration of LEDs and buttons and starting the applications are provided in the ``p-net/samples/pn_dev_lan9662/`` directory. +EVB-LAN9662 Features used by P-Net samples: + +- Shell is available on the EVB-LAN9662 USB connector marked ``CONSOLE``. Use 115200 baud, no flow control. +- The io-fpga is connected to the EVB-LAN9662-Carrier USB connector marked ``FPGA SPI``. +- Two ethernet ports are supported. Sample scripts show how to configure a network bridge. + +LAN9662 Sample Application +-------------------------- + +The application focus on the process data and its mapping to the RTE. +The source code is is found in ``/p-net/samples/pn_dev_lan9662/``. +The sample application builds for and runs on the EVB-LAN9662. + +It supports the following I/O-data: + +============== ======================= =========================================== ============ +[Slot,Subslot] Name Shared memory area FPGA addr [func] +============== ======================= =========================================== ============ +[1,1] Digital Input 1x8 /dev/shm/pnet-in-1-1-digital_input_1x8 0x100 [mem] +[2,1] Digital Output 1x8 /dev/shm/pnet-out-2-1-digital_output_1x8 0x104 [mem] +[3,1] Digital Input 1x64 /dev/shm/pnet-in-3-1-digital_input_1x64 0x108 [mem] +[4,1] Digital Input 2x32a /dev/shm/pnet-in-4-1-digital_input_2x32_a 0x110 [mem] +[5,1] Digital Input 2x32b /dev/shm/pnet-in-5-1-digital_input_2x32_b 0x118 [mem] +[6,1] Digital Input 1x800 /dev/shm/pnet-in-6-1-digital_input_1x800 0x120 [mem] +[7,1] Digital Output 1x64 /dev/shm/pnet-out-7-1-digital_out_1x64 0x184 [mem] +[8,1] Digital Output 2x32a /dev/shm/pnet-out-8-1-digital_output_2x32_a 0x18c [mem] +[9,1] Digital Output 2x32b /dev/shm/pnet-out-9-1-digital_output_2x32_b 0x194 [mem] +[10,1] Digital Output 1x800 /dev/shm/pnet-out-10-1-digital_output_1x800 0x19c [mem] +[11,1] Digital Input Port A Not supported 0x200 [gpios] +[12,1] Digital Output Port A Not supported 0x10 [gpios] +============== ======================= =========================================== ============ + +Note that the I/Os on slots 11 and 12 are available at the EVB-LAN9662 pin lists IN-A and OUT-A. +The sample application gsdml file is available at ``/p-net/samples/pn_dev_lan9662/``. + +The application has three modes of operation. The mode is a runtime configuration defined by the mode (-m) argument: + +-m none RTE disabled. Application process data mapped to shared memory. +-m cpu RTE enabled. RTE maps process data to SRAM. Application process data mapped to shared memory. +-m full RTE enabled. RTE maps process data to QSPI. Application process data mapped to io-fpga. + +**Mode none** + +In this mode the input/output data is mapped to shared memory. +The shared memory can be accessed using the pn_shm_tool or by another +application in the system. HW offload is disabled and the default data +path of the P-Net stack is used. + +The shared memory is accessed using the pn_shm_tool. Run ``/usr/bin/pn_shm_tool -h`` for further details. + +**Mode cpu** + +This mode shows how to use the P-Net with LAN9662 RTE SRAM data. +Also in this mode the input/output data is mapped to shared memory. +The shared memory can be accessed using the pn_shm_tool or by another +application in the system. HW offload is enabled and the cyclic data is +is handled by the LAN9662 RTE. To a user of the application it is no +difference to the "none"-mode. However P-Net copies application process +data to SRAM which is mapped to the cyclic data frames handled by the RTE. + +The shared memory is accessed using the pn_shm_tool. Run ``/usr/bin/pn_shm_tool -h`` for further details. + +**Mode full** + +This mode shows how to use P-Net with LAN9662 RTE QSPI data. +Shared memory is not used. The input/output data is mapped to +the IO-FPGA on the EVB-LAN9662. + +The fpga is accessed using the mera-iofpga-rw tool. Run ``mera-iofpga-rw -h`` for further details. +Note that Port A outputs and Port A inputs can be accessed without the mera-iofpga-rw tool since they are physically available on the EVB-LAN9662. + +Note that the mera-iofpga-rw tool is run on a host system, not on the LAN9662. +See :ref:`lan9662-resources` for further information on the io-fpga tool. + +Running the LAN9662 Sample Application +-------------------------------------- +Start the LAN992 sample application using the script ``switchdev-profinet-example.sh``. +The log from a scenario with a PLC using input port A and output port A is shown below. + +log:: + + switchdev-profinet-example.sh + Starting switchdev-profinet-example + [ 31.365807] EXT4-fs (mmcblk0p2): recovery complete + [ 31.371838] EXT4-fs (mmcblk0p2): mounted filesystem with ordered data mode. Opts: (null). Quota mode: disabled. + ANA_PGID[61] = 0x0000010f -> 0x000001ff + net.ipv6.conf.br0.disable_ipv6 = 1 + [ 31.647312] lan966x-switch e2000000.switch eth0: PHY [e200413c.mdio-mii:01] driver [Microchip INDY Gigabit Internal] (irq=POLL) + [ 31.658804] lan966x-switch e2000000.switch eth0: configuring for phy/gmii link mode + [ 31.786815] 8021q: adding VLAN 0 to HW filter on device eth0 + [ 31.877381] lan966x-switch e2000000.switch eth1: PHY [e200413c.mdio-mii:02] driver [Microchip INDY Gigabit Internal] (irq=POLL) + [ 31.888861] lan966x-switch e2000000.switch eth1: configuring for phy/gmii link mode + [ 32.016817] 8021q: adding VLAN 0 to HW filter on device eth1 + [ 32.032693] br0: port 1(eth0) entered blocking state + [ 32.037722] br0: port 1(eth0) entered disabled state + [ 32.042987] device eth0 entered promiscuous mode + [ 32.047909] br0: port 1(eth0) entered blocking state + [ 32.052820] br0: port 1(eth0) entered forwarding state + [ 32.068835] br0: port 2(eth1) entered blocking state + [ 32.073767] br0: port 2(eth1) entered disabled state + [ 32.079243] device eth1 entered promiscuous mode + [ 32.084021] br0: port 2(eth1) entered blocking state + [ 32.088977] br0: port 2(eth1) entered forwarding state + QSYS_SW_PORT_MODE[4] = 0x00005002 -> 0x00045000 + Starting LAN9662 Profinet sample application + RTE mode: full + + ** Starting P-Net sample application 0.2.0 ** + Number of slots: 13 (incl slot for DAP module) + P-net log level: 2 (DEBUG=0, FATAL=4) + App log level: 0 (DEBUG=0, FATAL=4) + Max number of ports: 2 + Network interfaces: br0,eth0,eth1 + Button1 file: + Button2 file: + Default station name: lan9662-dev + Management port: br0 12:A9:2D:16:93:83 + Physical port [1]: eth0 12:A9:2D:16:93:81 + Physical port [2]: eth1 12:A9:2D:16:93:82 + Hostname: vcoreiii + IP address: 0.0.0.0 + Netmask: 0.0.0.0 + Gateway: 0.0.0.0 + Storage directory: /tmp/pn_data + + Application RTE mode "full" + Slot [1,1] Digital Input 1x8 mapped to FPGA address 0x100 + Slot [2,1] Digital Output 1x8 mapped to FPGA address 0x104 + Slot [3,1] Digital Input 1x64 mapped to FPGA address 0x108 + Slot [4,1] Digital Input 2x32 a mapped to FPGA address 0x110 + Slot [5,1] Digital Input 2x32 b mapped to FPGA address 0x118 + Slot [6,1] Digital Input 1x800 mapped to FPGA address 0x120 + Slot [7,1] Digital Output 1x64 mapped to FPGA address 0x184 + Slot [8,1] Digital Output 2x32 a mapped to FPGA address 0x18c + Slot [9,1] Digital Output 2x32 b mapped to FPGA address 0x194 + Slot [10,1] Digital Output 1x800 mapped to FPGA address 0x19c + Slot [11,1] Digital Input Port A mapped to FPGA address 0x200 + Slot [12,1] Digital Output Port A mapped to FPGA address 0x10 + Profinet signal LED indication. New state: 0 + LED 2 new state 0 + Network script for br0: Set IP 0.0.0.0 Netmask 0.0.0.0 Gateway 0.0.0.0 Permanent: 1 Hostname: lan9662-dev Skip setting hostname: true + No valid default gateway given. Skipping setting default gateway. + LED 1 new state 0 + Plug DAP module and its submodules + Module plug indication API 0 + [0] Pull old module + [0] Plug module. Module ID: 0x1 "DAP 1" + Submodule plug indication API 0 + [0,1] Pull old submodule. + [0,1] Plug submodule. Submodule ID: 0x1 Data Dir: NO_IO In: 0 Out: 0 "DAP Identity 1" + Submodule plug indication API 0 + [0,32768] Pull old submodule. + [0,32768] Plug submodule. Submodule ID: 0x8000 Data Dir: NO_IO In: 0 Out: 0 "DAP Interface 1" + Submodule plug indication API 0 + [0,32769] Pull old submodule. + [0,32769] Plug submodule. Submodule ID: 0x8001 Data Dir: NO_IO In: 0 Out: 0 "DAP Port 1" + Submodule plug indication API 0 + [0,32770] Pull old submodule. + [0,32770] Plug submodule. Submodule ID: 0x8002 Data Dir: NO_IO In: 0 Out: 0 "DAP Port 2" + Waiting for PLC connect request + + [ 32.544399] br0: port 1(eth0) entered disabled state + [ 32.555613] br0: port 2(eth1) entered disabled state + [ 34.164904] lan966x-switch e2000000.switch eth1: Link is Up - 100Mbps/Full - flow control off + [ 34.173514] IPv6: ADDRCONF(NETDEV_CHANGE): eth1: link becomes ready + [ 34.179964] br0: port 2(eth1) entered blocking state + [ 34.184904] br0: port 2(eth1) entered forwarding state + [ 34.884950] lan966x-switch e2000000.switch eth0: Link is Up - 1Gbps/Full - flow control rx/tx + [ 34.893743] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready + [ 34.900201] br0: port 1(eth0) entered blocking state + [ 34.905155] br0: port 1(eth0) entered forwarding state + [ 37.214719] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #08!!! + Network script for br0: Set IP 192.168.0.50 Netmask 255.255.255.0 Gateway 192.168.0.50 Permanent: 0 Hostname: lan9662-dev Skip setting hostname: true + [ 39.225858] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #08!!! + [ 39.235956] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #08!!! + Module plug indication API 0 + [11] Pull old module + [11] Plug module. Module ID: 0x100b "DI Port A" + Submodule plug indication API 0 + [11,1] Pull old submodule. + [11,1] Plug submodule. Submodule ID: 0x200b Data Dir: INPUT In: 4 Out: 0 "Digital Input Port A" + [11,1,"Digital Input Port A"] Set RTE QSPI address 0x200 + Module plug indication API 0 + [12] Pull old module + [12] Plug module. Module ID: 0x100c "DO Port A" + Submodule plug indication API 0 + [12,1] Pull old submodule. + [12,1] Plug submodule. Submodule ID: 0x200c Data Dir: OUTPUT In: 0 Out: 4 "Digital Output Port A" + [12,1,"Digital Output Port A"] Set RTE QSPI address 0x10 + PLC connect indication. AREP: 1 + ANA_RT_VLAN_PCP[1].PCP_MASK = 0x00000000 -> 0x000000ff + ANA_RT_VLAN_PCP[1].VLAN_ID = 0x00000000 -> 0x00000000 + ANA_RT_VLAN_PCP[1].VLAN_PCP_ENA = 0x00000000 -> 0x00000001 + vcap add is1 10 3 s1_rt first 0 rt_vlan_idx 1 0x7 l2_mac 12:A9:2D:16:93:83 ff:ff:ff:ff:ff:ff rt_type 1 0x3 rt_frmid 32769 0xffff s1_rt rtp_id 5 fwd_ena 1 fwd_mas0 + key field first: value: 0 + key field rt_vlan_idx: value: 01 mask: 07 + key field l2_mac: value: 8393162da912 mask: ffffffffffff + key field rt_type: value: 01[ 39.549942] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #08!!! + mask: 03 + key field rt_frmid: value: 00008001 mask: 0000ffff + act field rtp_id: val[ 39.565901] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #08!!! + ue: 0x5 + act field fwd_ena: value: 0x1 + act field fwd_mask: value: 0x10 + Event indication PNET_EVENT_STARTUP AREP: 1 + PLC dcontrol message. AREP: 1 Command: PRM_END + Event indication PNET_EVENT_PRMEND AREP: 1 + [0,1,"DAP Identity 1"] Set input data and IOPS. Size: 0 IOPS: GOOD + [0,32768,"DAP Interface 1"] Set input data and IOPS. Size: 0 IOPS: GOOD + [0,32769,"DAP Port 1"] Set input data and IOPS. Size: 0 IOPS: GOOD + [0,32770,"DAP Port 2"] Set input data and IOPS. Size: 0 IOPS: GOOD + [11,1,"Digital Input Port A"] Set input data and IOPS. Size: 4 IOPS: GOOD + [12,1,"Digital Output Port A"] Set output IOCS: GOOD + vcap add is1 10 2 s1_rt first 0 l2_mac 12:A9:2D:16:93:83 ff:ff:ff:ff:ff:ff rt_vlan_idx 0 0x7 rt_frmid 32768 0xffff s1_rt rtp_id 4 rtp_subid 0 rte_inb_upd 1 fwd_e0 + key field first: value: 0 + key field l2_mac: value: 8393162da912 mask: ffffffffffff + key field rt_vlan_idx: value: 00 mask: 07 + key field rt_frmid: value: 00008000 mask: 0000ffff + act field rtp_id: value: 0x4 + act field rtp_subid: value: 0x0 + act field rte_inb_upd: value: 0x1 + act field fwd_ena: value: 0x1 + act field fwd_mask: value: 0x10 + [11,1,"Digital Input Port A"] PLC reports Consumer Status (IOCS) GOOD + Application will signal that it is ready for data, for AREP 1. + Event indication PNET_EV[ 39.883951] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #08!!! + ENT_APPLRDY AREP: 1 + Event indication PNET_EVENT_DATA AREP: 1 + Cyclic data transmission started + + PLC ccontrol message confirmation. AREP: 1 Status codes: 0 0 0 0 + [12,1,"Digital Output Port A"] PLC reports Provider Status (IOPS) GOOD + + + +Building the LAN9662 Sample Application +--------------------------------------- +Add step by step guide describing how to build the LAN9662 +sample application and which Microchip resources to download +when that information is available. + + +P-Net on LAN9962 Application Summary +------------------------------------ +- To map process data to QSPI the application must use the operations ``pnet_output_set_rte_config()`` and ``pnet_input_set_rte_config()`` +- If process data is handled by the application and not mapped to QSPI the hardware offload can be enabled and used without any change in the application. API usage is identical except that the hardware offload is enabled during stack initialization. +- ``p-net/samples/pn_dev_lan9662/switchdev-profinet-example.sh`` shows the required systems configurations for a 2 port Profinet device application. + +.. _lan9662-resources: + +LAN9662 Resources +----------------- + +Microchip provides pre-built buildroot images for the LAN9662 which is used +to run the Profinet sample application: + +- http://mscc-ent-open-source.s3-eu-west-1.amazonaws.com/index.html?prefix=public_root/ + +To build the Profinet sample application the following bsp and toolchain are used: + +- http://mscc-ent-open-source.s3-eu-west-1.amazonaws.com/public_root/bsp/mscc-brsdk-arm-2021.09.tar.gz +- http://mscc-ent-open-source.s3-eu-west-1.amazonaws.com/public_root/toolchain/mscc-toolchain-bin-2021.02-090.tar.gz + +Documentation and data sheets: + +- http://mscc-ent-open-source.s3-eu-west-1.amazonaws.com/public_root/bsp/mscc-brsdk-doc-2021.09.html +- Add more documents when LAN9662 documentation is available. + diff --git a/profinet_stack/p-net/doc/getting_started_freertos.rst b/profinet_stack/p-net/doc/getting_started_freertos.rst new file mode 100644 index 0000000..9a284ec --- /dev/null +++ b/profinet_stack/p-net/doc/getting_started_freertos.rst @@ -0,0 +1,50 @@ +Getting started using STM32Cube / FreeRTOS / lwIP +================================================= + +This tutorial uses the STM32F769i discovery kit, see +https://www.st.com/en/evaluation-tools/32f769idiscovery.html + +Preparations +------------ +Run the build on a Linux x86 machine. The build chain uses an ARM cross compiler. Install it:: + + sudo apt install gcc-arm-none-eabi + +Install ST-link on your Linux machine to have access to the serial port of the device. + + +Cloning STM32Cube firmware +-------------------------- + +First, clone the relevant STM32Cube repo for your platform. For +instance, to build for STM32F7 (fetching a specific tag only):: + + git clone https://github.com/STMicroelectronics/STM32CubeF7.git \ + -b v1.16.1 --single-branch --depth 1 + +Download and compile p-net +-------------------------- +Clone the source:: + + git clone --recurse-submodules https://github.com/rtlabs-com/p-net.git + +This will clone the repository with submodules. If you already cloned +the repository without the ``--recurse-submodules`` flag then run this +in the ``p-net`` folder:: + + git submodule update --init --recursive + +Then, when configuring specify CPU, board and path to the cloned git +repository:: + + CPU=cortex-m7fd BOARD=STM32F769I-DISCO CUBE_DIR=/path/to/cube cmake \ + -B build.cube \ + -DCMAKE_TOOLCHAIN_FILE=cmake/tools/toolchain/stm32cube.cmake \ + -G "Unix Makefiles" + cmake --build build.cube + +When the build completes you can find the sample-app binary in +``build.cube/pn_dev.bin``. Flash this binary to your board by +uploading it to the USB disk as usual for STM32 boards. + +A serial port will also be available via the USB cable. diff --git a/profinet_stack/p-net/doc/getting_started_rtkernel.rst b/profinet_stack/p-net/doc/getting_started_rtkernel.rst new file mode 100644 index 0000000..8c92607 --- /dev/null +++ b/profinet_stack/p-net/doc/getting_started_rtkernel.rst @@ -0,0 +1,380 @@ +Getting started using rt-kernel +=============================== + +Evaluation board details +------------------------ +This example uses the Infineon evaluation board "XMC4800 Relax EtherCAT Kit". +It has a Cortex®-M4 running at 144 MHz, and it has 352 kB RAM and 2 MB flash. +Details are found on +https://www.infineon.com/cms/en/product/evaluation-boards/kit_xmc48_relax_ecat_v1/ + +You can use other evaluation boards supported by rt-kernel, but you need to +adapt paths etc accordingly. + + +Install tools on a Linux Laptop +------------------------------- +Install (see description elsewhere): + +* Workbench (Version 2020.1 or later) +* Compiler +* rt-kernel, use variant ``rt-kernel-xmc4`` +* Segger J-link +* Run ``pip install pyyaml`` +* cmake (version 3.14 or later) + + +Copy the rt-kernel sources +-------------------------- +Run (adapt paths):: + + cp -r /opt/rt-tools/rt-kernel-xmc4 /home/jonas/projects/profinetstack/rtkernelcopy/ + +Patch the source by using the patch file available in the p-net repository. +Verify that you have the version of rt-kernel that the patch file is intended for. +Run this in the root folder of the rt-kernel directory:: + + patch -p1 < PATH_TO_PNET/src/ports/rt-kernel/0001-rtkernel-for-Profinet.patch + +You may want to change IP settings in ``rt-kernel-xmc4/bsp/xmc48relax/include/config.h``. + +In the root folder of the rt-kernel directory:: + + source setup.sh xmc48relax + make clean + make -j + + +Download and compile p-net +-------------------------- +Clone the source:: + + git clone --recurse-submodules https://github.com/rtlabs-com/p-net.git + +This will clone the repository with submodules. If you already cloned +the repository without the ``--recurse-submodules`` flag then run this +in the ``p-net`` folder:: + + git submodule update --init --recursive + +The CMake build files for rt-kernel need to know which BSP you want to +use, as well as the path to your rt-kernel source tree. Run the +following to create and configure the build:: + + RTK=/rt-kernel-xmc4 \ + BSP=xmc48relax \ + cmake -B build -S p-net \ + -DCMAKE_TOOLCHAIN_FILE=cmake/tools/toolchain/rt-kernel.cmake \ + -DCMAKE_ECLIPSE_EXECUTABLE=/opt/rt-tools/workbench/Workbench \ + -DCMAKE_ECLIPSE_GENERATE_SOURCE_PROJECT=TRUE -G "Eclipse CDT4 - Unix Makefiles" + +If you are using Windows, start the program "Command line" found in +the rt-labs rt-collab Toolbox installation (adapt paths to your setup):: + + cd /c/Users/rtlfrm/Documents/Profinet + + RTK=/c/Users/rtlfrm/Documents/Profinet/rt-kernel-xmc4 \ + BSP=xmc48relax \ + /c/Program\ Files/CMake/bin/cmake.exe -B build -S p-net \ + -DCMAKE_TOOLCHAIN_FILE=cmake/tools/toolchain/rt-kernel.cmake \ + -DCMAKE_ECLIPSE_EXECUTABLE=/opt/rt-tools/workbench/Workbench \ + -DCMAKE_ECLIPSE_GENERATE_SOURCE_PROJECT=TRUE -G "Eclipse CDT4 - Unix Makefiles" + +Depending on how you installed cmake, you might need to run ``snap run +cmake`` instead of ``cmake``. This creates build files in a folder +named ``build``. You can choose any name for the build folder, for +instance if you want to build for multiple targets. + +After running ``cmake`` you can run ``ccmake build`` or ``cmake-gui`` +in the build folder to change settings. + +Start Workbench:: + + /opt/rt-tools/workbench/Workbench + +Two Workbench projects have been created by cmake. One project is for building +the code, the other is for the p-net source code. You need at least to +import the project for building the code. + +Use the menu "File > Import". Select "General > Existing Projects". Click +"Browse" and select the ``build`` directory that was created earlier. + +Use the menu "Project > Build All" to build it. + +An OSAL layer hosted in a separate directory is used by p-net, and is installed +automatically during setup. For details see the tutorial page +in this documentation. + + +More Workbench settings +----------------------- +If you intend to edit the p-net source code in the Workbench tool, you +should also import the p-net source project. The files ``.project`` +and ``.cproject`` have been created in the p-net repo by cmake. + +Use the menu "File > Import". Select "General > Existing Projects". Click +"Browse" and select the ``p-net`` repo directory. + +After importing, right-click on the project and choose "New > Convert +to a C/C++ project". This will setup the project so that the indexer +works correctly and the Workbench revision control tools can be used. + +Run on target +------------- +Install J-link from https://www.segger.com/ +Start Segger J-link GDB debug server:: + + JLinkGDBServerExe + +Select "USB" and target device "XMC4800-2048" in the GUI. Use "Little Endian", +Target interface "SWD" and speed "Auto Selection". + +Run the compiled code on target by right-clicking the Profinet build project, +and selecting "Debug as > Hardware debugging". Select J-Link. +On the "Startup" tab enter ``monitor reset 0`` in the "Run commands". +Click Apply and Close. Select ``pn_dev.elf`` and click OK. +The download progress pop-up window should appear. + +The resulting ``.elf`` file contains the sample application, the p-net stack, +the rt-kernel, lwip and drivers. + +If you need to adjust debugger settings later, right-click the Profinet build +project, and select "Debug as > Debug configurations". Select the "Profinet... " +node. You might need to double click "Hardware Debugging" if the child node +does not appear. Typically these values have been automatically entered: + +* Tab "Main" C/C++ application: ``pn_dev.elf``. +* Tab "Debugger". Debugger type J-Link. GDB command: + ``${COMPILERS}/arm-eabi/bin/arm-eabi-gdb``. + +To be able to view register content, use the MMR tab in the debug view. Select +core "XMC4800". + +Open a terminal to view the debug output from the target, which will appear as +for example ``/dev/ttyACM0``. An example of a terminal program is picocom +(add yourself to the ``dialout`` user group to avoid using sudo):: + + sudo picocom -b 115200 /dev/ttyACM0 + +You can step-debug in the Workbench GUI. Press the small "Resume" icon to have +the target run continuously. + + +Adjust log level +---------------- +In order to learn the Profinet communication model, it is very informative to +adjust the log level to see the incoming and outgoing messages. See the +tutorial page for details on how to adjust the log level. + +However note that printing out log strings is slow, so you probably need +to decrease the cyclic data frequency (see PLC timing settings below). +It is recommended to use log level ERROR when running with short cycle times +on a microcontroller, in order not to interfere with the real-time +requirements of the Profinet communication. + + +Standalone rt-kernel project +---------------------------- +This creates standalone makefiles. + +Configure the build:: + + RTK=/rt-kernel-xmc4 \ + BSP=xmc48relax \ + cmake -B build -S p-net \ + -DCMAKE_TOOLCHAIN_FILE=cmake/tools/toolchain/rt-kernel.cmake \ + -G "Unix Makefiles" + +Build the code:: + + cmake --build build + + +Serial port baud rate +--------------------- +If you like to increase the baud rate of the serial port, change the value in +the file ``bsp/xmc48relax/src/xmc48relax.c``. For example change +``.baudrate = 115200,`` to ``.baudrate = 460800,``. + +To be able to run debug logging via serial cable, you need to increase the +baudrate to 460800 bits/s. + + +PLC timing settings +------------------- +The send clock is 1 ms in the GSDML file. + +If you do lots of printouts (which are slow) from the application on the +XMC4800 board, you might need to increase the reduction ratio in the PLC +settings to avoid timeout errors. + +In case of problems, increase the reduction ratio (and timeout) value a lot, +and then gradually reduce it to find the smallest usable value. + + +Using the built-in rt-kernel shell +---------------------------------- +Press Enter key to enter the built-in rt-kernel shell via the serial console. +To view a list of available commands, use:: + + help + +Example commands:: + + ls /disk1 + hexdump /disk1/pnet_data_ip.bin + rm /disk1/pnet_data_ip.bin + pnio_factory_reset + pnio_remove_files + pnio_show + + +Memory requirements for the tests +--------------------------------- +Note that the tests require a stack of at least 6 kB. You may have to increase +``CFG_MAIN_STACK_SIZE`` in your BSP ``include/config.h`` file. + + +Examining flash and RAM usage +----------------------------- +The flash and RAM usage is shown by the tool ``arm-eabi-size``. +In this example we use:: + + CMAKE_BUILD_TYPE Release + LOG_LEVEL Warning + PNET_MAX_AR 1 + PNET_MAX_SLOTS 5 + PNET_MAX_SUBSLOTS 3 + +To estimate the binary size, link partially (without standard +libraries). This example is for cortex-m4f MCU:s, such as XMC4800:: + + build$ make all + build$ /opt/rt-tools/compilers/arm-eabi/bin/arm-eabi-gcc -O3 -DNDEBUG -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 CMakeFiles/pn_dev.dir/samples/pn_dev/sampleapp_common.o CMakeFiles/pn_dev.dir/src/ports/rt-kernel/sampleapp_main.o -o pn_dev.elf libprofinet.a -nostartfiles -nostdlib -r + +Study the resulting executable:: + + build$ arm-eabi-size pn_dev.elf + text data bss dec hex filename + 127421 16 1388 128825 1f739 pn_dev.elf + +Values in bytes (including the rt-kernel RTOS). + +* text: code in flash +* data: Memory, statically initialized +* bss: Memory, zero-initialized. For example the stack. +* dec = text + data + bss +* hex = text + data + bss (in hexadecimal) + +The flash usage is text + data, as the RAM initialization values are stored in flash. + + +Run tests on XMC4800 target +--------------------------- +In order to compile the test code, make sure to use ``BUILD_TESTING`` and that +``TEST_DEBUG`` is disabled. Reduce ``PNET_MAX_FILENAME_SIZE`` to 30 bytes. +This is done via ccmake, which should be started in the build directory:: + + ccmake . + +Set ``CFG_MAIN_STACK_SIZE`` to at least 8192 in ``rt-kernel-xmc4/bsp/xmc48relax/include/config.h`` + +The resulting file after compiling is named ``pf_test.elf`` + +Add a new hardware debugging configuration, where the C/C++ application on the +"Main" tab is set to ``pn_dev.elf``. + +The test will run on the target board when starting hardware debugging. +You might need to press the Play button in the Workbench if you have enabled +breakpoints. + + +Run tests on the QEMU emulator +------------------------------ +On a Linux laptop, install the package ``rt-collab-qemu``. + +Patch the rt-kernel. Use the BSP "integrator", which is intended for emulation. +You need to increase the main stack size in ``rt-kernel/bsp/integrator/include/config.h``. +Modify ``CFG_MAIN_STACK_SIZE``. + +In the root folder of the rt-kernel directory:: + + source setup.sh integrator + make clean + make -j + +In the parent directory of ``p-net``, configure a new build directory:: + + RTK=/rt-kernel \ + BSP=integrator \ + cmake -B build.integrator -S p-net \ + -DCMAKE_TOOLCHAIN_FILE=cmake/tools/toolchain/rt-kernel.cmake \ + -G "Unix Makefiles" + +If necessary adjust the settings:: + + ccmake build.integrator/ + +Build only the ``pf_test`` binary:: + + cmake --build build.integrator/ -j --target pf_test + +Start the emulator:: + + /opt/rt-tools/qemu/bin/qemu-system-arm -M integratorcp -nographic -semihosting -kernel build.integrator/pf_test.elf + +If you add ``-s`` it it possible to connect with ``gdb`` to port 1234 from +Workbench. By adding ``-S`` qemu will wait for gdb to connect. + +To send a command line argument to the gtest binary, add ``--append "7Vzbl5o4HP5rPGf6IAcSbj462tvZaet2utvdpzmoUdkisZCZcfrXbwJByUXRFlA748MMCRjI9/3uiXTgYLl+mwSrxQc8RVEHmNN1Bw47AFjQceg/1vOU93iemXfMk3DKL9p23IY/EO8sLrsPpygVLiQYRyRciZ0THMdoQoS+IEnwo3jZDEfiXVfBHCkdt5MgUnu/hlOy4L2W29ueeIfC+YLf2gdefmIZFBfzmaSLYIofS13wdQcOEoxJfrRcD1DEwCtwyb/3ZsfZzYMlKCaHfOGrP/x08+f443C4+PY5HQcPQYK7Hh/mIYju+Yz505KnAoIE38dTxEaxOvD6cRESdLsKJuzsIyWd9i3IMuKnp0G62Fw7wzHhlFI+aTuMogGOcEI7YhzT/usgmfArAP/2KCAEJXE2BDDZMOpMi6dGCUHrUhef+VuEl4gkT/QSftaFnAUuhlbByuOWVMfkfYsSn3YhiAEXpPlm7C3W9IDDfQT00H0e0Nvnh7wCfP9m9OH9HvTNavRrQEqGCvR0UKlIAfN4pPD4P2YugRkFYxRxFFarKJwEJMRxPt5eFN3SXDjqJexoBzXRqBXcHBE26ALDUYDzNMBB52jcaJNDd4TA+VqB+3xygfN9SeB8VeBcncA1ppo9FanRh9uTAwV71ZqpEzCnKZwKAkSgTi9RMlC2fWqg1ADnLIDyrHOTKHCeqicDdXqJcrQSdde/GwYkMMJ4qmBGQ/8VO0zDeB6hPktLRJwyJzzCaZj5XjicULwQDdWuGU7UJUc30gUEM6SDKJxrL+/zE2NMCF5qwsE6iJGdh0aCLV1gBxpjRvUeB1JRIcYzGsu848dZUlmkgqYBiy6OrmlYpm25pY9XUxjtVuvBjuiwIbiLTFcOa+76UZAs7z5iEs54OHkhWpFgEvChYE1a0pODU1MNTdvVErAjGuW09SffjAR9vyy2uhldZTVlx3/X54VkGm14amMHVGN3lbugu0k8e9UBbkQf4Xqc0KM5O2LevHBSl0Fwmc8GHJbOgrbLITyg+obiKediOImCNA0nkm+SQBOqQPTLpRIPWofkH3bGMCHk7X8zn+U5RXu45pFd1ngqNUYoCem0GYXDvcYxxffJBB0QaqKpUHtViauoexR9CYqoIXgQK7Y6svgdRjiMSUkupJzFlUPHfEL8W6BUYZUG8j1fFLCiXQxEgmSOiDJQJjubaf+COOlD+ELpqVlQpYtScJNXgMpCVamzuWonKA1/BONsMCYSKzazbK7OdccZaqTzaBuyNQ57jcF+9ZJNxGa5gD97p1yR15kO07CBQK1Vi+R16cCQmpztR7xL1zJ64ph4NktRM8KjVkV/N39Sgw8BBxSCWvYh9u/oQ87ENUDfM3xbYByCnuE6ve3H+zlnAXrAsOzSOMpdBMvgtutJtDWOZ+xJCiU7V0/CxoUlYeqJkUiXnjZ7wqc1v6Kmm3QSfdqT5ZvaygDFmIhilJIEf0PSAqZuTbNK3JbhdMpuoy35iLXNXeupwGunVLBZein7Ft1yDGzKtxSiWuIOlrlL0tVVB9C5v+m+unASu7WxCKorBZtooB0W1cjOEljUBG8XRp5dE3lySdvTFOva1UA1urPL3FFPfHXpqleX5nmeIUVRtq3bCWD1WmVQE0qx3Sa6KvllpFGK4k3DhEZAec8jSolUfq25jGdJW7kgOHVF3T5gJ9f50Vi9kFUPX/LClS5lbnfhylYXrhS+GkiZiyS4SJf3Zss7ga+urp5XCu1IFRMoW9pDE2bb6RmaDGY7rFEupLWcMDtqJb+iepafvLzsp65Kmu1JcbJmX6h271lTVsFRw+TDrMIRpmAnbmeirMoyJ5B2SB6qrJYp7WWFvsRb0wqp+mR5lVtXxboA3SuHX8CpKWndxVY5prJa1Ua1bNSoj47pQ2+cNGuUvTRrb9101qr007l87/PT4KxU33YdUfU9R3Co0oCHGgLPNOVhW7UDhQsp24Hc9V5e2aMp17vZP3kq1+vqN+le1pbKmtNceaXx9LtVXKB1qdpKhs63nh9lchFqTyGjzHBNamgByeJa9qkZVquNz95YuvaZ5SmuWlD86VTzd1m0rW2jj+uZuqhHXpE7YouPOELXFgdobt3V1SRAz12XlZrDob93a0yXNVvonztJssGFmgXUdknSbJh/MakHm1TH3GdSlXJwfbRpfkv6QltNntCWy3m10eapWcaX/uCPF8Iq9czfq2dyJbc+wtSk4YWwwwjTVtg2hTLQFGFqAlHO3emZ0fCvCws+5D0ldQWMvmeISTrQpH+g+E1fKxtKPE3MyNQtZ818IS7TLWkXHtTt07c1UWRztOkWNPJUfWEJjLnf7zHhKHXTDCa24QuA1Xp7skjuvyThfI6SlD0yK8Op6X/RQ5+a3UjsXB1yZwtq7/y0YrIQMdTJAtG/7z91h+ghpEKw7yFW277aJDWlohfGc9rhbFtfmOMYdsGu9xRgKk6zKFs+WlBBRvF+odW/IKmJBEgUXUv3U1PdGw+8pkTX1/1M8ddF9zMKJoQJLsFnIr0D+s0ERxE1dsC8Gt0MNNW0F0k+JpV3KkVZ6zx/Irv49Rda+aA0mdO+0coy5dcGOCpyzbzRija37yTMi5HbNzvC1/8D \ No newline at end of file diff --git a/profinet_stack/p-net/doc/illustrations/AlarmStateMachines.png b/profinet_stack/p-net/doc/illustrations/AlarmStateMachines.png new file mode 100644 index 0000000000000000000000000000000000000000..22999565d53e71490219c986a60d0be4571d0ae2 GIT binary patch literal 462053 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�#sF}r^p{5cgBWMlt8 zyeCmoN=O-&N~#;==J0=u#0JXqZVZGGZXmO1^MNRuXLf^p_&p}ld@NPxQsdJ2r;|j_ zjcZ!bV~yWKg}7J9BB>NSA8+&|BZEwVcnn85`1uq8VzD|`di;KAr(AhX}LCk0e#m+dJEs{@43aq?#~23q-*|tlyUR_d(`*0wPlg! zHE$HofC$H*_-X7#J!s>Tg-mLt0@Nink3a^6wDE?Mn8R-2Z=0h!<24nB+gCDEsRUW@ z8B7srI;rJtlarHG0Ox5&5Y0(Nis*ZvCRzDO->c|o1~ahmwYs$nfd;47V?Kbz>)o9= z+*zJo%(nq>i7`C|;Rk0XP4|mqNsK78mVGAA&qpiEYO6LqLr}~4t%sSm6O>UAE8X^u zDV_%~2+T``WBZXFY9ZGo5ZVy0guW2?Pf4I|n&9R@?edlF}?c--Mr(7mIpV&ME9Y5Aiod;`MH*D zbEC|O>r`GtPIbg}R&xULU}C~`fI~wPR~oJ%`F!1czd2c0`^-CI7410mTG@)n0VA z%k!C|BN>#7m(-glpFXhd3&)m$onSAYlIstLEMnasbXFEuLsMc`Y*+AV{vTXeJw^`S zHOQZm)5RY@8Urd6pJK?}!`{3H>kg%39Kc$X*>6$6moEPX-IPh`6Y1lPx3Or5QtBr?@G*D z1e8vh#qpH1?4YIpU*mf(NmUt*dVwS7Ie8phUX%y&nX>*_&S?*6QVG$k8xWgEjm@2x$@IIsh%z94LAxicZet&y> z?Te3kTCtz8%+VWKE^<%E%By0d4za9sUxGCmHWnIOLkcMI5{$EkCr|E%V&Ykeh9?W2 zGr2`RANe1gND8#5<@Xs}&UGWej1S{Kk|l3-({#%4%$)L<;HVJkf`vsjA!zpC$~bd- z`#=WdAek}wSR_*j+8wGa9PG7w>fw+#L zP{B_Lec|uLKnJ3d9<{@>bQ7+Q8dSifjoMA*6G4dFeP-8;)D6F`L=6jYDP@>nFW{+= z07<0wu^x|+t1l`AteW2Y2j~Z2#(`n?pJi)|^bfgD3~Ydj_f^}}__+L)EBWng6FAin z-XchN%g4sPpv*(Go0MzT{Kau;!b3tTOvWIowNvYS(*YiCV7`k@;DsyJ(y2v~+@lMT z=HEhMVN++pORs?Yi(GD`AqHOS{9_E|iL4lCa3d{k0df|pdI2V|horT_i}MOs^0h9l z6HW0t{GwJ3+sb^6W|Ck`C+QcsPVqk_(|zj#2?#_(g}I8?mhm{g@Z4%kQ1lpV%4=%Y z;CrjUbx$+$4aux{MjvmrVIGb*7uCJ&cASZV9-Q+OI+sMoZ5$*hLz~GvebP z#eT(o!&*!tCYqoS3!KnP)$>cF7W%gam1h;40(wA4z$9TzATvRy3NI-o1Zkyy;W#0T zfdTA5dfo!QdZ1?!UDZ$y0px2-k#ZbR%djW_Yx}$C&ONA}*Wv#y8gqnJF&pZ?5R-ol zG3J4rK#V|C@6{y0HptO=Afr>hTU?w8s3n4gXpxV{fZS;mmp(i$(!Xm?ZHCLnSuPkz z0(PfpPC2CDkJfa&N4B`+H{wPk%GTC7)W9 zt7vWep_X4a_ z*$c=p%!ZNndep9qkSoL3tQqk95$-C8KZuR#fuQ02bkqEEh6HxQFwd{C!+-1lGjX!{ z48PIGI7*o42gmjRN$>`?LwtY8B~ZpQ(uB;AY!o7_UG+Y)iHBGnF(I47K2qcaP-`3+ zsBwUA5%DSyJRic94InWhrGhXpo5NvDl%K$@Pdhl!qn!dkyb&Ww6`IM2S-g{Ep%^R| zUJx9bLu=^i>$+AOzzLC8<6@3OU;7l)Ao;ma9K+Q@3w5mEFUs2T`#}emt5u7Ru}2J= z)z`mWpu{2qHA*R22r(?Of>}_0-Ot5?Pf>N$wnE-}U+vZ7zwcA_8MIAAz=}q#8QH!N zRF|Vs2e=4ofBDpaTj0$Q;E*8u2R5CV=tEzp%>q!|KLMK5zf-0tLA%6*8 zJ!B5reyYLb9bpe(wdy2_zX_4hQC<+U2P9H=BO(lA-5GmP{K;yRF~|MDvc{6M;)X!-^ey|CxqI5CPQALlRzLQPpc^sFjjgkHfbqJNyc zCV>Tz=AG9#35DOgk%uUCB*ucQlOPNV{!N8co#Bs-xGb`o_28e};AhwdiQJRs^Qg)+ zBk6|}IrB+fYiVJ^mUg=26EQ)ca38}0#ib~-+b!;E~2&G z(%pw7SGRdP9m7U2LT};=t#fI{5T9B>t?`pu=+zvT5U_{Soe0P|&8e8qvB#QR-D>oK z>Px{T7ARue3S$E>z?UUxbTZWB92=6GbAMxoQ&Fn(99_dqtg4 z9~gye;IaCu3m9Lc#*S61Ru#U0#~nUr)+cjFswI9oKEB#s_j^k! z$B$P-bxRXs0my0>X>$JRY)su}l6tRW+t1f*VAGl%{)yHkw;p3MLbmUU(?nu@RXO2>UAn zIjX0}0vJFDn(Tn_i*(efh6Wys$d1hJQw7h6V8m>(F~O_t|tYEYlAdC<@O{$`=!GJD*!UjPV7 zf%9Zq`@&Ck&ydy=_ilM$JAbMt*Woz zaA;{hr}k0SL;lG%CTGMV47sG1ipUp>h#NkFnm~C|J zo~I4r5fQ$4kevJXvjJ)9c)|V?(+Al*h-&~j*cH9QF&RV=9 z*f9QpOoq)bSgtJMioF0^IB5byL%s3AC%CEa*LLsyjulqtSR8lw`vVLkTVGI|_0XXQPJ~B;Qq!X@7p`(gYCnZ zH88a_p*IsjC&ulVV>>=I^|-BV{e!!AZvc&og%yU~_k8B5;gqK=G{R;?u`+1YzJ6)A-rJMa(nFHAMyABWPX z`K7Fdn&;acLu5fxue%p_S$&jrOYamGyMk3SPN_w$|^3LrH-L^{JUQE6);}t`U0oK`C&}LpJx4TYKj>;H(s`$U^&R_fCA^w@EbKev;GJot-Pp!yhOOrYZF!%EDrYET4`lZn2 zK$&bYjI3g?T+me-YC!Qo4ZN-%QV8j_g%j)N95H@%@0(O< zdIl9h+2TMruy+_oHp2tEZES7{ME2H?KIOy$TQ+tlG&Ho=ii>8l_5v|n&5!>0^pX`m zPbL6a^mxF5CCJ#ajxuH5bF4{gnpb`ukfvaQ4RD8$g$2_9UJ^I@F!(dGveu}(8|PZz z!&yKB!BW-O82ob0=N~`n1I-v6X+?KH$Kgl>Py;S$&Y96DyckBtPwgN{RfLM3b^%LI6928g z4?>s;fR0%|3pG3UZr(i%@5|%y)CL5`Sl{<&9C+z864qYz80iV$3T2oaCkE8Zl=cMb z65JNM(JFe`$Vf8VJ&SKCV0x5o0Avf(<}o;x)=CE9ulcc;-mC2ne!Q9M-bT*K%T44a zeljF}@hpH2qplaOGh#Vu_i&>l*KX6K7jq?s5_E2{69<|Q7=6(B_tD`CzTRX5uw_FO zU%|03SgV12SiYmb@M!^~*}XTi_@hb#uCWx%Gv@YkXr7jb>;% z*`2C`INr9}kfFJeZ{NN!!%bNzmo%f{N$~^^jA$7jN1TLMcG`(YqfK$Qy6w|Wc)Bdk z7pDBcJeBXzvk?j5mU9jzGq<0Y%yI!Dt^ynucl9Gx6Po4jKfhI)mN$6(i9pgVv@JHS zM7q6x_^=rk=vtav|e3e)m=tr@fau?DZAeRyAaRQhiFTk$!ja@Jw1Nu2!Dmj>22wCm8>;<_azefUB^ zc~oX2)XE0Jle%MCgGU$>@1Wx+-7o>IN_Z~9=M<4(+3mMfA`R1$Ens70eAF^6^S zql+667|!SsNx6$Lzv_f>@NhI+ydu=+7Fyqe$$r=Ib6> zC31NcMaVxH!Y^OZ5uh3aF%2fnyiZR*==DRAYlWX)%rS)HMNVd3*hql~7{*2W#9l>-vZpIc3|(V13dc z`Kc-fV(j6w(s)8(a^(M}EMP58Q-Q;S6_{TXt7I;Hd94t<2OYl!=lM8uELJDB1ZOp` zqxJ6^$J9%Xz$K#wO`<7DeR31SJf*5>&mVdW5m+Q9pVX#8uAnyaaI#8SGxLPp0cmnrhIj{J*Q?5yD=KWp;@@J?@Qb#ydL7K zL=nRO(Sy~}lcjfIGHmwNGDGq*y8Lc<_!?++goStP*gWX->5&J+58Xax6wK`5`1cYM~2S+`#_QNJVujPYwz1HJKMRRlZ>yR@@us8kfrgzwn zGkq14d)i>8x27O0DF0hZ8Bo1^xDxmCou7z==eBO%g=F;w%A{FnLrRU_Fl^_$B~c_u z*43$5t}JbG%LJx-$q9l!0uPt%L&xKb4$W~AWC+ceQYsG%vI&^a>C2YA25Ct>GVt|l z9Qw%kE@8nmIwOeOS4VtzyA%l+s2@4~GfpZzRoP z3hRy?jR&pI!8L^1GoDYbBHZy&Q?8NAzwp_F>~Bk=XU@&y=c%oSIr8_~eKh(){2sjn1Jy3{jTrHvfw~KwDB2){ z7nf73rJoPy4I(==SD0JP?-a~f)dz>;c~Pw-udRs0Tj$s5gXRJtcHn%_x8=1%<{aXd zYWFHb5L}8W#eXphhfKjgjmh8gzcfF6sI1h~)OTd0v-9(-@jXu3F5!P;J0)M@kgq5{ zv<1|ht4)o`>NI>Lkq=y3D(A6ncoR@g&mIzMc}(oO`V(YJk^(IfavlPb4bCEwMKP~m zhgS~b5>_h)R4SGggS4Lnp-EqFz)+hwh{is`o^O2PAp*})3b*aR7h}e)<^hISSR zqT^X6FZHsntdpVJNQGqQ+T60|>|wK91^AUOE}5X4DxOMx$Sa7D2eJ=fYRQ`ziqT5e z=e7X2ztt(456eijufS5A7u2{S9#}h-S$VHSAH6=+1q9|K2jW6yQAm6dq3y}ys6=Ok z33Z-WImJHAwK>soR!_j#r@$S8LD67XBjAlODHn+ae8)>0i4+)nqB=TIUVpXw`}OdC4NKjN+xMZjdCT>ocJG5& zZ;T$Sf$BNyw?uZs_`pPfXvk+~uMeW@`h7k@1hPzj-w`0Vuj}iTaOoA6LIK=;h9xXF zQPc))UeV^q&@TyYfPTrC_@w*i9-0!$8mYa}gFi7G^_vtp!xHnmPMZ^S@eQaY$E?Ci zd8few5HbMmVT7acBr$E{G@@lY`}3T%c1-^=Cg}k zc=K{!GvyMG1Xp7XSoHF!gas59DhZ}H7^e^S9qkdXKC_>SJ>o+fQ}sBZQoz`V07@gf zd60`vGOv7m){v2o#4wO;J<)VM=$=98U_25gE8}nmNj4EMF|@eykQa; z5CXp?aUE)6mg567%4lCynxyPbF=`VGSUC#I#_(XJ_jztey&m7qC*-HC#XtiZ%1 zw-Hi_cyk0FyoFhW_+(xGe6T`~a0`|j460=7-hZNONwC`sSqs1pTnJwx+g9z|hW5$G zu#*Hf%=cbc*YXNTJG$?G$Tli;7(gDs6^|k43m`giR})tO$Y#Q|B4KhRo*&D?b>_T2 z@JDx&;s6tol?*nnrW1{Gd3(0#0fdzskc+~7k&v((?bvgLfRbxiFFVP@qIy@ zLt*N)dKrGjCTNPvrm?Y(!yBUjZnk3pV5WkVF6(x zvj=}ub#4a;Dud_vi`E$0vg<03b?m^19o~P~=~*y|Q!2V%QOv@|_PW>-LnlBs-}4=8 zA-%0U1=ygiX~h9$`g#U+iirI0Md-gMz9fl-{};8;aC*j-k@ou7D*=200_%V%_L*y` zJBPt;_2iO1w(5ObscZIRd8j_q*&NynNoBgHz>%LDasl+xp;WN{zzQm3ovp5`B*AE) zP)j z9o>q5XUc|-osZ6O@G0OzHuIs@h<~FS!Vt=&rVqFh^ z6;WZ{r#WoVvScyz{x{qX6gC(Qv#y%noLX8v7>-kWt5bw~veE^XmT_o}wKCb$Lo4t3@9Rz(svD9te z(umhYbB-X15lSzby;Z--+I0+6{p)6y6Hwz}0KJW+*JIZM z0ArFBYmm_4#quWYi!ZM(nL)A!Kv6~FRbaJa}GNvi|6 z6$*!FSo1Vb%=7CIO-P5iceMnB=bEDI_t&?xLGDLL^yY~eOoC4&MR_(ejl<2Wd(Mdqa4Db@vw$hAjNLXpV9s-noq5({>-~^nErtbhNd(aI)Y7kS`T#y zR14|D0O#2@Eu3R0LKHw=brb#Vl&oe-^(IJI*cpRe}&Ako%mJa?HopBV&! zxr&vy@cLzam*ajpasVPJFk+uRrEgkz3FsYK?Eu|3=&v#~eH<7+;!&0_cmQfrIziDi zv44VDj!NSTol)yjbR@JW@;}&G2s=n2cDkZ&Jm&$j3s;r-+Y+0dm@#IjoMAg?!>C6P zs;~;cW$WBSe}~Q8o?O=7Ih1%5#sHE#48RE12e_p`jV> zHktlG0arRBqTd7RGQx2LdeXo@|2zTwg&zbfq4jrds7*i$ld50bH;VO04F$ONGsQov zx|22?+%c(xmAudf3@odmt~#MTqOl!=K)V22+3v*)bQ3h@+=pESM~e`Qv=1kFR_1XE zAM;vJL$q>7to}>Qt>JqEdM<9WbP6K_!_Mh*^f3T@#<|C^c5q+#Kh12wF!`qiBb=dO z3T*z99!$c${Z7Or7J7dswViT5zYi?~z9Gu@F=oy$9?R0zcLb_+aM(LcorpK@!9ueq z=qzu;&{_kM|D<&_kohol??!u*aa#pJV@@ay3MEBy&IXw}WW-9XkT?l9=qjmLL%10% zB{_7{1iZ=cG`4_@HorpYDYTk_GSECI;Z81aMibWRa%doEney6LQ-hBd!G41g4xZJWzyFQ zcZFCAhXvUsRB#QV#c`yy4ot#%Eh-wkMO-=l=u(YA$Y&}M0gu$IwdwreZ$4OooqBq) zUl}PQThR3sT%X`3OTQpQ5vTtEbm21&pIIkTId^#e#fM-nxa9Qq0bj*}EEd`!-oPyX zfcyM@rdzm+(AwPwOVtkolB8?lQ{KdNS<%uGlG+7<`K_May0hHY5h7+Q!!?Ko26x~; zBu>4&oGjdEP19BFpF-bQSoEX1rbe7T;q{p@o^oM>r&PGGs_{ybWLWc8H&)bH#X9f! z%XW}X52FwTV@JiubZlrzX|OlSwJtm72*zdC9i5PDrd)V3FW5_ByV%(O&*B1zJRb4Rqx=N?3p6zV+~RXb9|aTz2x4*NSoj>4hocJay;;a^<5cz{K|wymlVL;Fbxa{@x4NI5*Jc1qH)L4<-zT_KMbZ30~G zcT@m#MGi4(yFH^}Y)OKI%Dz*gIp$@F()SyjoYi5Gk^Y$D%i7vPr(W7X(}pL(4pv#q zho2-x16}_TNS2ww-%*1PlDM%&5b*Ol!tFvzam}>x_3P8!V^9JQw=LrDQ&#*~p0)|S zrvDgS0CAFL1Gy)=Zop;}jpyo(+fq6zl^qW+b1OdmhbMdZNCZG+J6;Ei*>NZ<{B2*x zhVeE1@#6VWnNdv0qAEzkbTLm4e+HJ7EXv&mVA-|ttRucH=1*{M71PY{HZ z1>ArCaaU939w=17FHQ0jfo&&1_hpUmvwBswtI{yF5XecRku;uedLnNT=~buMc$>zZ`Nr4OlYsQGWIaa4<_bD|8ci+Z_OktNmg% z>HlP&S~3L~yMOT|hST@BFR}b5d3-CTHuix9Ye;P2KdI|-$I3{Ai$~0on2;1O24tO2 z;D}V*m}gVPL-L%=d1c(`sJ0!rLXbSO#rpcvLpf;ULJV3DV7x4R-a#OR!~E?;e2b7S z+U|)F@ER(6mAApngXWvIdhF}hDy}f@U5Kf;ijl$_ib=}^*aaCT=Y8L5D(-<{_?%?YtP zKxHM&TG#vc;`Tyjjzhixg~E?n#`s2-LAFBH53GzSb3Kj|)SfaiJ#H~zc?Hcc#A$n{ zUDz2IZa`kcp6<_mYq@L=*VLdWP^P`^GoI@%ib$0rE$a`~GmIZmfF!wJ1ha{HKF@3d zt8;D;Z@-L#dHV5cEE(G}HC2w?Ex4xfTN;O;93H zoCyimchs;tYCSxfGRZTKv8ax+)oMPi8BImfG4J5mt9UXg2v#+R5{?4FR@zO?P%V^aBxGE2O= z3~xgbs%0ywV4^g%VxZg-hh>;9DDRUM*%yS1=W@wjhy^UX^+DTT2P{Ok-wsa@dGLfZ z`oyznxQ#|2#!7nCQ#=0*o}NsTHWzmY?VPKCBAF$dEVra#KABac11?JX}e7mXh~+oKW?!bN*@bc!k&|k80lsr@%oYVwC!$+%F4sW zrw0MFsBOa++P>wbt$D>f0uYvq%JXXn90wpRv_|AbB{GkwGc{bWXh+_`70c0xoWps9FA?YDyt_j_VDB{)< zH|pmS{as0o@a3hl_m2)`D09W1>?wBk&4n(+Irf7aW}npIS@VSzp(B@vTj_9b$)g*O zSd&GJekLXtv2@JhiuR>2_Z3Hv%2Q^?Nys;M2?P|s+6j%Lx0j?PNbXOTqXPos207B8 z5Dqn27f&SG*u$XHUn~HE__-}bCQju7>Smqd3&H{gPC;5SFr3CcAO_E({4xC7s0-_& z$1Z9@93{=Fk4j6;6B>Q+`u%SeI3j-gwZcSsDxe1lvGI4f{{g}IlnEJ89iAo#?T$Wu z^qyJ>`-#+|pcv}da%+}aCT@+vZ?mJGETZZEq%T@Z@854-Bu8A%^$^*AJ135cg1sPh zSN_*&VAiiPbJsq4^iK2W7UJr7-7f_~2BksV9*GrqpLgTlzdq-HhzwIxQ?xFtaPyYq z-6g9CHYFf33X+TsXoiX|A|M&4Fozo&6#pZJ0*RDRXj`HDLVvn z?;XFBtdmpj5a=3>?2x&9C$*GF0p~PLQwA{k)s2|Cj?+v%1g3`B_j3Mqn1loSL&#@a zuT%QBC5oQ0g{Ly|s`OAwo;=UvcZN@i-lRqHht-$7i?6$dW7k z3eG`hDo&2Bp$RZyx@2J}iU!pX;&`T}@T#oMzjycUs|?pOm_ktnNAu#3H~m7SMP%JD z1Cd-abrDa!ffU|cd@$oruqgfH-Y>w9nn9L#Cud-W`oJriZ`S}Wo2tBHR&)3!uyVWe3;biZK^0N5D5zED+Q*XOp?iJF#UX2RY^u;t`a;C^ie3ZY<6j=>iTx z+Jz@xBeDLx&SUt7MQz@31)gEjg4|euDKdY`bm8L|tVVq0MDVOmtAR9(0`mvvS zlldKK$AJs>tjeA&P+kJgGO%-Nb3`ofTfavkWd0d&oWY|l?0|cUGVv1~&Yd8#SXu4m zf0`rza3HZrHxZ}XFrkW+01+gMt5HN&ke%Id)<_Hs8a`8eH%%Ie%H0oMMX*7N_ne|M ztK$#z8gMyI+tEil5KAKv_;8usR-^KA0N{#jeUH6NPK2dmD*Acy)n}Kf-S@8Ck!J z(AvFgSMSKk+pVQU9CIQ@_}k(7uwJ;+>gF(!n`YcfC;mauwgoniAFbH8cP~BBHZL2K z?F0cTof%e@m48JUn!Uwfmh9?^-}9cYJ(^Ubk#wHvIRumH;hQ3{i zIh7leMOU63YYR({?b%sBUtcr5&!PPC{isP3_`b(?16Ep#cmEZKg(a4@S~if+Z#}3fHvb)@tfS; zmxEQEXpR60CJ>@k7m3Ms@=(QA4B%a2laSi*rv;}?&8o@bgSfD4+GO^C4bsrE-1J7- zQS4~;`w;e*M8}ODCR&~B4WlI|KmS@7sg<#zk~D6A3X$JK!0qli(PYdv!rQ@^ei<0B zCBJ?pMC>XTv|H8O)QlDk?qiKG9t=rB&-_t-v}u=RQ6X&G#tV&db85fcQI2W^>-M?) zIX9YzgC{cPfVIg(SMZq|&#C@TyMr{B3S{j`++X_^K70__;K<$?R7xNV1{d;~;V?e! z)hO4DlPmXM9o@#%h%>s!KWRV7Wdklx&7>&d(7g@L7%d+@UQy9|DxRsQUV0i%ZhNhDGrg0ls-{+n9-%;2 zsn!24uy^CHYCK7Um+ZkQa8|(_m5rkN;-U@}L2$cU_LfYqBw;Hl;SU~op_3PP`tjq( zMP$bWzCfR>CYzmZiYeOr!|^VDmjN$gz_NT(<1<_-jQ%ou%TI(Jd>$Hly}q4Jo%gNP zYp(tH%P^3TwYiWlZJvB}6E8aCQA8a#^{~ln5%DD#oM0iM?h$aqCO(swEbu3^8{vEN z277OY7LqsHP-wg{(&ROzSk&G~gE8=oHUIeSzCWwM}-MJrwCWz>1Y*d|e1xb)E z1Jdn~k-dKL(4Wx*{C*<~JEnz;c|A98y+V_{C&gOLQ;0iWIpMvFeCvCqg-+G=)%V%H z3+)rswAkD1#T&*JSnanVJDzu!`n{65#f8to3*SFQe~31*yc+iPdbDGs%Etp&A4Le& z)fG<_-dbaqp=FNcm8cyAc*8*z#UaJ!mZKvF2zHDVA6i>|tSo>iM5M;-^_e)(B$g-t z3zMe!>^XF3a|Ol`N`DvPBFgNoGk0Q7o&5N!Hu-?%pF9m=O2ie2BVQRl#}}}AsuVq- zR?~z~9GwxsK^-Tu1uZ2;T3kHjTUP#WY9TyiwP*cU*jF}Ji-aQd`e`W#5M_W&GwyX_=$UVHaJ_}}pNOjgH_WliF=Mz!bU&1Y+i zTh=u>%OwaSTLKvJnfy8I53ggB9RaqdCJ2t;z95TTSV+h%umrE_>jNidd)|ex!liD$ zI6q_5FEByY(J350Eac2-72g8@e6l-d-F24J-+(|;2B{T}k-JnSnV2d7Q~ct#>Epl6 z6`iYyUq#Wt~mb;MX#oExzdYm z0I)Kc;Qj@{IV`e5GM<)CtpHLHFim`}UpX>3=!09}D2HVnN!vRiH={Tb2TG`z098

vXaJuz3s67KmNPK1;f$B}=p=cA=*1j&xXrJ8e}5CbSs(0eXEQmb z{rJ$~V=pS%a{w3|()m?u)~qSMc=TSZQb%@rOK2?Qb`PaD#)TaEm0Eh_-{FE398r3^ z9X;R{h1w=oBeNqhIhpvOS(fRX&x3rQm+o&yKAqTW}+$leT25}>U2)wtq$bF`Sy+R2P?^YE}vJBFFIGO5@K z|6Xi8M(Yup9G-p?gcJ4OnljEZ7aC^p9fK`Gi9FyaE(4Zh-B6)*xFj%4# zbLi2ZC1x8Wz=0vsnAGNvJR^iGo^EdZL}c-pTYjGMcl!Q*6E^TXkWu@!^c5Kc82RxI zich=EAWCEMl*>e*tk5|kM!Yq40Nbq`G*%u}2fPO+TxfmgAcEhIuk&Y8PzxG00AxsM z!{=;fAx^>#{y~Y}P&UwMf zIZm#Lg1hk!G}@|LsHcG3rk@hPJ|w*>-_gk%PCBC?=XOfzL%&$zr#GBl<_5vV>gSwc zFx2}0k~V~Ohilwlnk^GkX4(~``~DpLbfCc*VWK2|5Pi?&qi`a$z-G4((@t5Wy`sG*li7DKy?LAc>FjY9hSU6V=FJCBd zTJ(;M1>!VXdelbfOrN2hYWegSP?fbAd{fx2U3809QY zw&d=lGeRF*%eA7m^n5WCNmdIW$CF2~`)ZOTpBUNgc~1gxOs7e_pR>iasnO_m@%-|; zv1jq!s?{mzoxsA7%|Iob;>DkHlC~!5gR`)tHN~<3$&lQoQ|u=u_~DF%@Nz9c};x>Kt7; zt00hsu0-Us;ZCo+^g}^V!co7*$HMxVp_Lgo2$TyfeybB_HxfkApBbgsyn*>byxp@v22&PhqLGyg@ogLIq8fFj^U0ShcZCLjZM!U zU`?Gg(xS6lIlnC{B)u3WNTUCQ6tXWrDL+q=z7A+U<&s*#;l?};4i2Z| zI>*)3zZ#z-H&^@hsN~bgg8FN7H_Qu!kOyfUl_w8LOGiZfBNYA?V&x3oF+iPlg$S;D zg$6*ntFECz83e}uF{>e&PzHr3YbMqGyQHG;-}i(=DgBtT@@>tUaa-qVCxr+8ov;Mbl1e6QU? zWLWG{=ScvgIYwF#ro0eiHgbuHVn%aYsK-Towhxjsxz2ohI~~mve@dXVNrq<5plrOR zx?1f2Rtz^Zm5Gxjgqg#cmwa)E?Aq4LlSER}z1{oja&213WbS{QMT>%(YL2J|H7&Wx zzW?+b+*vRsVT`wvRc+V{OA!#ty&Ag|cVz-$$>91lQ+0Qo&y~)*^(_jM+BL3$q?wN& zn@Fkz+^0J+pn#rM|4L#^!Kq=gm(?6c~B%-9Cv8}+HL?UNhe>WdV(SE#9ZgyxlWDC8xK4qTknxG0Bqc*-O3l*wM&n`E^R0639;gKC!RCsu_j^jEoHmH-!|vj>Ca<*8lp75 zeylcP%!T;gPkQ5FJxBNz%^D&lO&%kSs5u`7)@~~$5F+I~$ zJ+O4W5BIbuZk&G10Fa<8uhhuM7QlR2^aJ`u-4XGoVwbBVAOtr@1$uk(`KM=R(@u3n zSsUvtSG7*fp4@4-mWOWPXw@WV4J3G$*x!|MudZL5gq>sU;<+dpC%+H*px-P5Wn-l0 z5;2+PW!93@2jCQ%z@X>$P$vMc^71XKs-l5FtP&XJwXDyHz!#A-o6-1?jb!0mK?DJf z!yK-;zq8VE!{&xJA3o#PMOkQHcr_wH2x;Y=ZTQk?{3~W+&Q1v#VLPc*6yBt``Nv-%iTi zX#URJCorp*wJY%{agvG`sOBJ=>f~k1UR)0(c1nm_D}RTiEpyHS+^Fize221E zSJ-6UcqMBneFy+F-fY|)h zQ)6AvvwE)~eED_Ms^v+%Jg#6k1E6VdNY^IdW8us?~>VKZ-hT!%m)Zn!} zGiYWY#?emSP-xsBZP3%!wiyKIz&7UYEYm~D_DNc)q14TI6!82EwWYIahdWFj2eZ*2 z*M(EEn~O_vqz8-lW3yz6x5i5moUYw!JzRD7x* zDGo;SCEoy(2Y)Ed^x8UWoqx80W&!1hxPwOo53}ARW>lsnuoHIi)-w?~C!C>&Rf~qB zdadnA*-fX&MKdrE58qRoGbqCf$PFf|V;{9DVYs1kDL#00)L7Ge8nJ^BQ)i$I_I%!^ zc6V8jLI!sy85rOuN-(B=91`_H52hijkC8WupH7so2uhY~gVlr_glT|fL2ievGc(@m z0XIE128MqeEd({dYGXplnlc+(jvEPGSCai3J%e(TXz`b1P0EE z-&HAO50iv-6BU>s<=||*+`&!n5-wwtH>~3S*c+V{edSx#3v2aQ)16j5b6` z3)~=aV`vBZd8Cvyx>P_`tYGM=&IO}xcXxN%gbDIxaHG<7pK^>ZZd!A~o92@Tx<1Cz z9mDCINQs0K^L6Zx9`pd~d$V(L4kL-riX4mZ@bH=^+-R!I#py^)L|hi$KPxc4Ssy#bjC0DK^GjsvX=)DLq@qH={WxKiTt99)|7& zd`Rc6bFC?xplZ`TlSPK1f6E56MNjkcblVWQ`m(v^8ELf*!UPS?FQ2^q?)wR{&!o&A z%P4|vEKu$G)};BMfWV@;jIz;sE5tqjLCk1^mvB6g8z-Kf2_~{h^mybx^}IkGC|f;( z0c=?{m0%>&Ra%h6I#A$ z5^XCwtFu@oA@D21n%IiDs0?&@@c|3s&>gXLSH1@@0P>ZD>cF1&bDE6yawGMn!Rq*f zy1F4I`JMmu`**?F`4r+k#Yv%Ezg`iS7!5~=Wp-X(6+*NFk9kSJu8{DD@5(sx6hZkZ z%07pi#^AqC$R8+)MvCv|04##>f+!kOEAn^&Ek>;x^nL(XZ=KN!vaH9m)A90UMd-t} z1U+P!IHs;%1u*2gcrc5A-g=xy7TI2WeiPzeu$qS$#329N7B>&2WNA$j)#>@%ZDCe2 zC*BlOXTT|;Dy`;)H2?yB%)!N3^Tg`|0}K3r#H^Kb9CIScI|PwR!btEn96?ba;baZ_AxXcpV%6$c&}Rf{C&N2HHuw$JK@*82 z-;LT!sA~qf)|*ItGk^s%2v*{w=p7-`D95h@f}Fk;0u%gD9}F#T!uJNFGc&``q8vDm ztN;(PGpRLQpB}@*1^AAz#vH?ZFph89{WC0WBRas8EnB7RUzB_!oUm>UhPt7)uaY4h zfUV!9TnX5OtqR|}C|{U?7M<<i}r7?$Ctu-xIaKyG9a*wh( z@tSg$YWhpa37xK^p|NKnIZc}-dBL!g$1ZFGNssLtSI5M%pwtwxpz$&m#|~^AyLo={ zEwTBRlzOWoa}n4tJa$xQ%;9VwZ+C-r7ObVtSO)x%fJbP?1mxsS37DDVsAvL_06=aA zai&B}`dtDJVydE4S-1M5J3#eH+!&${;WeCP`YD5B%uYmTIXX5OinjclJq-Qz@p(zA zh$%<*8X22nK^l7o%O6bz&ECFnQw~>9k$+HR0s=71fl>m|b-URh05AWOh1u@D;Mbi! zR9L#8#UBd!T6XWcm*BNY;-Z$1{U%g<1_p-Htu&W_;84>)IpjM1G5Uk6?ks@DF>M%b=hNVu(w3GW+q&K*b=5O0#(mb;=7|20 zCw!0PN{yV_dij15GfY&ofYkdmfSdVZ%g361ekHhi}Fxlto)=4fNOiZXqEiYrR z$3o->iRnh>YD+i4zN#58tV{L=?ol{R3^Xffe!$=*B9!T6{&Dv*uPu>GVrfE9mWQ}vQ>qu%0P9k4`b)Nvrl&ZN+ zBesyei(7tO<^H~|5SxmKBA;$6cB-xBsKLERX)A}+KQ+{~ffy$!*#_uv_c@C`Lo}Pn zU?0)>NgI8t`@oa&=nK+XxSgj6T!2LycWp{z>5L?pN@xv5EC6WK?R&wP3(Ji7LNgHB zFBx{$9uw%NB2J^)x4j)kO)Y+L#ZMusJd$}Ufd-Ah*)W9R~icB5h zpz^l3-jqUeQfft*v6|X?4Do02{^CbX9^bDmg*!|B2e6t}uOn)Az7kvna=`jI6EOWE z&Y_zevGcCQh)=ngM0IgK%*iHk>+8R2yUg5REIWpk?+O)zn|q=lC;^5IDs~#2q9$6q6^dZR0y$=C!zyw*mY+jC%7K);r+ovH0;M zN(O+DrG^Va{>u=a+6%l1PX+mRbZ8^VmXxPxY0*oriuKJRF41*?4!gX*B>eaJ14E6v z66~9R?wfqT?po085qSH=361k3P(9Wn0Fpt`x$V-I5}WO@-sOgk9ZVnq7Ri|3cBmib zRvDfZ$;rvKLUjmoBBAIL=Mx?+FZhXadUWu5d9D<|vFO1;Y;mcxGUW~_tEjm3yktN! zF7u3V^=~QQKDTTW-kP>rV)l>zm*D6JfwV&xe54BkrC_JdN6uOF&huA%7iopZ3Fk#MniP>@PfbQ;`yZ z&1?b3TdjUTxw#UoQo|G=$&Dnkii63D4uyt@Q059Ju-zOAcT^qPyAK{v0~NjHu1{98 zffp>cmjc^-AriipFY2?uAb^=iZZVszvjhPt>Uu{J#L!ypBn%5!oh^k1m5I?-7nG?>rNsfSjShBm?d4xQ36D>pW_q}5Y)EsGTIKDK(D)Z z8FY7TpJc-TM|N)+7dvkz$q(kdf^@QPe?*_NI(jj zinOKVWlLE!3ErA_BUH+;WPM`$3%yN}JdjvYy+YZH=qDl|hfnyHv71rz=T0(+^rKOC z{y%)Z2{hJg8$J9qN>YSOl{APVvrr)#5Hg2EQ5i~-A!I5k35m#FAc1X1`FPS>C=g!|BFkqKB%FHz?$rcji5 z@-`8Q031E05~Kyrb&TR)iI+!U1sZY9!;-EN0JOyK7bJ03fMqh5FAMbckC7EE)m?P0 zq`Udi;rAv>m!@o3IVV~8n0;mssldY(qN zbKnu$lFmL{;YkB?-ZVj zXlP0wHbR}6AoXvBd6axNTN$i1vN51SIu;lMViT8RdIGp9(VL9zJ;60C;PL}lB9_;p6V7NRGt z(>twPNVrM(rG8ss4Uv$QOo$SE2IP|!&(kNTxokMl6A?0bZLXCufJcJ|*RpPGkYm0A zw(o-|y9jvTV`C|iw?i?}t?%cTN|ug*njq-7aUX)g>cwDy!j;8oNy={aYib!-^6W0G zP;r(+@;g}^Q!nMgRVM*z#@nM8!cjFP8KnSALUQD>l`r+$!Ns*zTs(NMamwx6RNyjI zwQyRjLXU&ukXWaM|9%J}73M(!7qb-4T!j+XU3*VEph4?77%Xw>@uED&cx7#`3=KdX z&>}Vg#Cl>n{qd;qO4+ybIv*zFtkruUd0Smie5-@Uot&L>`gOi;$ptyUypSI{vfAdN zJcjJ;J9Y>L>;@WF6;xP&t63??m@RzVy7mik7Vy$6zOnY!Zr!ZySW&+FPm397BH@F; zM{j>J(Vv;Pn$p zW+5~T(pTRT%=Egp2O*%m8U>eYyPkwePdA2#>$f%C_gw|iBf1Qz@w5$Z_2 zdbQ9%M;ok0P}nDpligcAy6J*siqaEIRBZQS8frc}f56W8FV08ai}qNd@xpMRZfXLx zq*=};Xyof5TeuB=8OhI%U~ly(ZLqMkteI5GMQ4g_wv12hkGRxrq_XIT1?=si_~^Ul zjVMxkzggitUh#fIHp#^^-z3% zC85-y^!|2n1f+VUEP*_IgQz%%sRB5_gUF`3V-pW(V=%l~1|;12^#*h!l1uv%z` zUO^O#JlX_OJdsvcTlot$-_CRa|Gr*v&mmqpTvq^l;)Vks;_pUX_>8|`o1kD_)NIeU zZ&9$!Tallb6_)kEdW*`qbU;h#ve$vtx0k(G0uW*} zB(ZwGow3X>oOOnp{yi{3G~+Z8#fA_;fmk$vb{-77?%7l0Kyx7cgu_(Ix%20RmTiHB zdX%14Tojzv`M8^d(CPvps?6Hu-BU!(Id9CFD0X-UzItT?}UXD7ScmyQR6+@JRSFjSt(~AsF0cq1>4}N+F1TpB1kslRQ? zgm9%iCDnin)-T7nXy-O?(D@^;#+Y(164xd!rQj|0OoKD(pvjU~6J^Zo-cyR&u6F73 zNvszzJ-{K%Y(1fnn2->IvdnQp3yO)2`|ar{zxo??V`YiEL&Sxd5?rC+-t@`C1f<1} zSqCjvEh{N0Fgg9*Q^ihs3BGl9KNw&u{s{2Q;)OAf7C{rqj4796tLCmi2t_vod$Dn0QVIJjPV;7rBnWEv50-y{Nc(?H?*&0-gW>+Z_KW2!Ogn(nbZ_=^O& zf7hcV9Br5W_7L@PY&Ab;+^@{>fopfAWAJ;B13i<800TJEO50&F-c`TN2d2>)aHLpm zGYv3_U7CLSNuK|5L)hZn@;rL60L67X3ptW7MJ;fi)&?h@sTE2qGlX+TnPZHE4kVmT62yL=FH%usfCskRaLlN><+zNt4A0jJK5{B$+^H zEJ1h_A7B)N+t9*)WH;!AwDuZfFLVqt2>F6`KXg79IB00?#V{F{=?%(IE3C$$j^zTT zuo{jiMvvcvd-FtVC-xj9Gi=_x=U^N!(k4X)h?|xLey~;JcW5$Gd~p`Rlzh|Lj)9yi zz)vk_RBP=^_L8c*I{k5R4n~XT`(1v&VV0yC#`O{&bN%y$sy#o!2=b`0>jC)e-!`&1 zfcl(nvL7PWd)kHYxe-kLiX-_;C57bPp|kg6gnkG9ggm5FsO}F}7t-pxBta@!njNO} zN1^6W=t#uuO3~RfCVGKjFJA)u2fBi#uo`lMa|l8+1)T=56#}U@N%tui(0}3r*#%Fa zKuLXd85ACb!(9syoTxc59Zb?n-GE5ofpLhbawl40J|zE}&}y8<26FOLOvCF)mY0>N*3m|mf>oNc zVUOT*>G>H3(4h{YcTi2y_E;w zWJ02p!4>KxFfXE4M2hdhNLE7^Tm@hEL*6V3XHwaSaCop zkhJ>LXY+AThzn0+k*KGyZ_g>sPslOEiHI0^of*!*(!Cm6Z|$W6vXe5-5tj4Q+Plbe zxah%>IPZ>;YOsu|j*8nJ3fKqpM&l2?eLcKjZS3v)L+Z_>Hk6WV5zBAWsptOB00eBV z-4K&JU}db-!ZqZ!P4c&&9NvM;WnO>S5%nugfqyC5lNE!zxw+-zwuu=4(1C7Yw5%Vv zSgo8w)yt*-Z~?Zo!C`Cq6Rt{Bk4f6Ds&k>_^!6qu1bRAZh2p;wP}&?mZRY0BfQ(0S`3vh*99+xeLz`*{aGG&RYZEF!UE-Yd}PR z?@bWQSOG%>Ph145TQDXpBm_g*(t2qG)azZHauXakf5i^{U6u1;3i&wX!Z4uVkwy#D zLmF^S*!M=MRtWV_&nJTd`;vk=xDND;;Y0OXj1U`=XIM>&x(O!)^s1!($*HN;lFvZo zQO~J@Z_{#%ZCu#lht{bRNV8;XWo6|}6kg_Gu$8^;a6nWvQ1?ttP0jII>v#A|FSI3Y zKn-INKn0|3if^Kmg6+;Y8fGM^Ma2uapv@FQfB&g@s4O4R)_Mo6tFFZk#I=R@} znSuWt#tdv|9OzHww8i2g%ybz+pH2IM%sztng}PBp{L3*+)se}P`@4YXh-g1C@B#Z_ z=^GPelgtgRd|19TLTclHl`b*I2 zVI{Fq<@X&o8MiLda?TD~QcyX3D!lewTiqY+DR{-rs5;jGFWdVkhIn3(8L_DqG&HMQ zNM(4qHBnIH#F4MNp{BJT{}B?G=^xVz$Y?=mpbxVOI=H-Iai{_6EEaHOhTNHm$PVj( z0Gg*`0Mrxx9r@&t*#tn5)9s09p8MOl`1oj1C}>$Qu&{6|^6K}^xzo9f&MvM%Jfw$w zpn2-vK5_9aJUn-v|3oboGBANK7AB}}0R}ZIc-7VyLDkxQ*(o%2$DR98Z&;v}(#$!9 z(Y!a@VpU(j(oWSPHYDos2qZrC-@2iVYBYf)eFXx8wyECC>cO$swDwk=GqO0_Wj1Db-^zF(p|urN!dh3uqeyeHW>o2TroI=n6)w|6?%Q_ ztw|ch;bGY%4S$-YcZ-dwSFJj|8WvPMkjDUsybV4C@qPiqpDZVo3chV$mB2eK{I$0* zn7g~-cFz|3Q*UZ7`WRJIRe4|*5hdPR6}D41JkC@DRTl;_)p18r1{~7Y53i3N&IjQI zYvUGp_y3Ypo~k+`36pgjn0co+BsIX+>EIyX5l;tKKnHhG6411WiHKc<+FuJhUWuQCx*Xo-7m(I6#u2N z9fl)!e0|FZEKpwt2uo{73tnnVDCmbzPq81^zn?_rb}=|(*A(>P%pS%nz3X4|U1lX= zot|v|6ju=Ww=T~q=;YAwJG4@?JB)>kvu zBhW0bT6YR88^vVt;P&G$B)Y@EzhtQIi32{r(DiHiLKm0Xaupo0+W zKN1DIB2dL|CH*#4`PI7{4c&U%;#*kQ%IxGTBqX%mC(sX?TT(#dz=cO@pa}wNyEW!1 zE9zhlL{o5L(Xr@Y9kKEU0sqHLdB`5LuuD0(DHV!jwZffPo01E=`A!xQSB<>VH*c8! zoDnDN^Ls@chm>Fry3jx3EBU^=_vN9Xi{N)b7-@X|RxX1O?DvZ7RX_K*eBCC=xT|5X z$A8e=gPQ%k=Yz0dYCXr%zLJd(qieLWnT?K}{VvX}F+eo3N3C(zS7Lqk_MJP)mRsUo zgki-R6uSw7&@ObNW53bT5Eym%z!x~m(ZbkaMfpA$5pZpG96!1k`pMB4)|GR8EV^-7 zrprH3A=CiSDSC@#@GyMaeI?yxpByOhg0o?m3ooU>q+4*P3uv3ovybqQPjG?8vre!Z zBrYzz;T_z~Wtg?r!CR+c?m|O~2MQcU@s$?{-;=1AamB+&$jpSh{}q=JsPwWh$g&jj z!tA{hyeH5sA490Wj4WMn>>WU_@dYNHq!4kMMa9c?Zv@3`UrWwz^tH?1OoNbj27@WG z21EMDF&rU8JUG5Z4bN%e{Y@REMFy&8+udNqoI;xFm)Ro~){&2|nlx@cFKry~P`U*lbDbzZD{;Xy# z#z_Y1=NBA|Mxj(5Gb|Su7ssJ%xFy%I<>1IbhqIGYCEE}%31WB#io`NvTZglA8=>qB zW`5%6T|k{P86C%!693Ra@YufGLi#}0%D znEqRralqXNA4(Bo#OGKKl5RbX3FhU-+2Z+W>4ZnoAz2M4W-ryncr*?Wu3-y=LQ!hv zhyFKH$SWz`gax}=jakDnh(v(LZI@Vy&Kv}U1U}Rt?fF0|;W}J*%>YuXkmcDzA@&O+ zYvP*Q-cY*WBG&o+d-Bvg0>t4yb7jyal6G>Kga2~yH#m82cM_Afn?^72+y6NMp79IH z^~~CdCL3V#w}IG87f@y=7d>>H2{( zY49bTLE(sN0avS%xM|*t)sfQc;9^o;L&Tzxu$$MLl<5PkUs1_(*5O<09=poqHP7HMb@LCh~0~UTXLZNAxM_-VG5}Iuq}cfM~f^l#E~G1d9Y3b(&F$Q zzqh@BO+64OR>mO&kYM`-^p=hXFv?;ZyNdI$8Y(?f4WX6&3SW`{Cw-9v(mUdQfm>{5 z(Uz}eBAo>g1(@nN)iBJ$!i}FViNZBin#M4p(_*KfAU&`sC1(w;85D^!z@&eJL;MBV zi>vkcnNcWB@X>#}s1PpUUwjP(UoJSF&etSp^t`dpI64opN6hEanVG>BU_vvQWI{}((kx-jRxsD_7WEJ>XRxGT70l3645f>Per{1RbVdLgJg{PU_0IieYq|8f`s9F(_8qcBWl&N7A0yTa_S!~!?_L{2(JOdqLI z^7_aM5!>-@4?uRAn70LAR9R0!RTGjn_^J=tJy^n;3kg`f_axk_Puds_1KcNib`s*f z3GJRpHiTy19j-#v<{i2$&t=vzS?-}Gq&s7eVzEEzvCK3q1RjR@!E`H8+YoRNFELK(V`0?Iwx_FfXB{fJRhgz?KQy4IF*m>y&!w^`iFk7E1Sn)B>ejkK72twk-JZKY3hc0wE1%9F;p% zVXd|iLlq3-3Q%t`gkiT=RGw4+IRsqb=pke=f`e9nH@?h@_7gy(mKSmWR@u4{H_-oo zZXgeFPd2UH;%+HF8i+X_yGWE?B;qY~izT4sL#>Ib7&9^0;xzf z11OV*aEz-_PTxWGo<45V=DOeM4;_&i!E1@wUt-G!gVE{*31`;}>o`DGPF8dVh{qgwD_JgZwsJan94iJFq5sm-*+KtreQ_djD5TX@27p?i8zCKS-1E|gU zw``HgiRdwkL~o6^tXgOQW^BYAAtI0|?ZIk4RFX?K@qfA6UBLvIkacw;`ia*VuOl+- z!n6CEPunFpQ4C>4mskCn^}=W?YuTiuo}gY=*!1k|&B`jWFXw^cx$6_Q@F4;zw!bYB z)f5aQxL(!K%X&KK%xpS_7eZ2}CqHlq2Z612RuFMwFY_-DmY0^Zq3$t_R6nb-hK7cL z@gxH4cHQ_OIzgg>2NtcTz@OF~9gduinKEz1(1uAFXQ5AcKRk+Iz%Gm>mt$H7G!zg` zjMiQlOn}zPGa_|7MCfqpW<#nPB=*xLCW&Lv{R=N--1NHqhoaBHC9hMm$IPiLWaIX2+uW0%+y6E?IEV%;yuw$W;33S7>I%|(}OZM6#9;cjry0gS_hgdP12aw=u|TDv_G= zFg1R^kP)pQHuw!nq7lG+!c@U=Crd)xrxsf09cr_cmAbN9JKb zq0^gyErv{J8FH9ndJr;2pkLSZo(C}qCo3Y*X;nKg1{LX80&A##;|TR0A$xoI>)c@6 zgGeM~`q=o1L1Ell-SyBpne=r2LU~D7Hx3sH`GZ66@Uix^NMTA+ztHJ;rnJJQjPd{`z)PXrH@a?|*X18I?1YXrLfQ{VF+9s{z%Kfspo z?cJa(H0hOurn>iH&;c!Q=j^FSF^f<)VHx=^M>j>ScnJM3QtPX>wp0D8ByX}_ynuq zm!0GqN~g|9YNXZtQ*&%R7!wgwg)iT}#o7MEG>KSi|MGP;%s9P7!xaK}iW`t_LRzT2 zK;Edtx=wWSS`FZ)#y1Z9jmT3fkjxmikApK6T)G6kRtbm^wEOqnJcjZM_R#jd2O1y+ zfYjX|xFSG@ax%q}9zlv;VTl6s76KeuDgQyg_QVNjNies1dJUEYV45aRe8yEKi-2PW z_o5sFr|?GmLqJxZ&kVr}-jQz%gkdW$6vcR_j7}q*+GN)x-V?3wUaW2SB{erUZOITW z=t4SX#uGSbm2AwMJy%erAk>Vwj29LbLVmmhVWz#v`7VI_;qx~vK4owY-DKaY1m+z$ zDO$bRKv*C|U1HJ(Au&q6FJNiD!rK)eAj$%VdziUx=e`e~{r}JP(2Fs8*O$F^OPb}rVh;j#Ud14vqYl!R^UgUqBZ8l z%wOGn?;4i9B^08c9r!UsOG``m##nWt{7&~!7WB(tm_>|OtD`y~{13n<;H_K$-l6x- zz5YF60=`&9!?R#u`r=RadDIUFNddqarVKhF_M>d?`;5_pXOZjJibz`~>?*^62EUVC zMbA#iYV!0sV5ot{#M01A%v}i|{(!w;HnwK5qqbw%NNSrEIlFBRt7q!x+fc*4)l%Qi z&JLyOv*xc`53R=Ol0}c;P)tP@p%YY3W5&@_qayyeAJ{*i0C_!>lO zhBZH`4U=f~>VP-(6dVr|^D%=AjS6j;TA{&XeTlfZxK&=H=}^KrzvjF_lT!_Wv1jH1 zi)qz1g7A__&MW5~5N%lTYL>%TFLNoa zR4d>#>msH5@{Snunl@+c=$(EYX!<6Y>^dl<)?1KufBY-!PFZpeh-R+M_w|4#kiw`! zloH0ACICLY0(h9S-Vacy@L1VBjwfD64@kU)aw5do&dRC;Fh3D|QQQz>|I%Ogcd6cc zjkyX+x7@2kkDAxtgA`{41%7!+V=jb|fG&zy#b1Vj-^EWEOW?Jg(lk(??2fORI#-OY zFLS&@dZGAG&y}Cu?*@AG;Y7Mw64hk;CV4nLAPcjFgX(JxmuVj@D}7Gs0o|>v{sKlq z{W-wNLBqV|P>6!a9s$PWBHFtYM?l!UX|W-iw^I4lXLTZ_oXtB~x9TI4{$Dlxb zXvp~>+BWQxKB~F~pgptmJe)0_{+H{qiiZVU5;V!|ZFS8UNMeEG(mxqlWV05Tna8$T z<8hK*bm#Vli(Uwd5<}}`(f=_iMsrsk=AZ|yFtBR8Xhpv@9smxeMz^ni^;yIoD#8?6 z*JE@sEai6D&aMoS9&uqWhFS0_bE=bJO9(Y($ywtXe(1mT-lZ=nrhOl4C5d7k`h#XW zROXcfq0=XZMBv`d$++5syZ5@^tG6v2ex3?V{{*g2y(?gqA8llsDTc&v>`VC6*bae8 zGUe=cgm|smQWjM!#1dpD`QDr-O4`4c^P$Op1HgFsn^Bw)%%;q9I3K=O1F(R)2^bQiOjrvYqsK!&8KuaK0 zwVpA&vag86dV`R@YHOL`iG)uRGe!UR;M>;4^sZ;LBZD;%+ks(846XZLI1MTL_nx>@ zc;{&bg`s6*3@WIJZiIz}MVZi!_3!@VwEiD1Ktq6}eiwe~XfrDb4#ge;l6 zwE=MvcD5%?W)QcT>$QQ3E7`8jK`>n9m5d}Juc9J$QvasT4Fqw>0q;&+BYX|$bH1M% z)7gc*N3@wkogS#Kj8Ryq-`3^cge3ViR^}b{8IWZ>yrSQ96d{~!$gNu7c?iSTAjuAk zIf+ao78>Ro9UPc36GjZOHu3yIWV(mI$+kZO^Qx0%d3d{X4kqm*|HF-vg?L8I6au1U2~8r)&gqF zY|OK|a^)3H7a32b6Tsp&*s0;khUk^5^LUZBZ)0yxiUDj4v!`0IboX6$H}sw07zx;- zgi3I92D`qXJhr(;5(d%RYSw%SfC(MpUL87sOE|d>Vk~%i8K7OopeMR9hG+84UxeP= zJOxcM4e2aOYS`h+xyv?}MCZovcNkH9!7(`UoE7^}Yqv^((+>ED{&!L?wNEmd5UzW$ zyx03Lwh{auRdXG>j@W(TfO?bn-g$)|09&&cOKsy2UlR2KevbsAR99uk&^*SaRHjaQmdmXgUGgxs; zLIc7xkpQYyE1V${KmdBna93bP%L`@@StExmwi2^#S(H^NKhgMP0COkAMpSX@k61{J zPGbVL$cs44G&@HxtK{Kg_(yGCgcFllcVJeaUA_7hM1;1N=5g={JG1!` z7+&GFaqV;cIr$#y%RDxF92}Am7^SNan)`4s!_>h`YL}0_zWX(b zh1%2o?ONfX4i0C|TOqH*LnKV9C4`^!cKx2)79kNi(YsMPQYz9~s#kbyY>W{^7W1Nu z)f{&$Q)6OoK>s3-Nu8wiwXb(}CPhWP>@rtXHF{0Lkg%DXnsz29xK4J=&z0+R zJUjFDg)C)@RFG5=v$GHHEcjT#uR&aq1!e6BUpS}(i2#(`D}dXute~P|*LtMHwGI1} zL&4AueDEN9YWf=-zddv<*|zlunu{X5vQiz|T(xc5RMB z?vD2Ms-Q1pwC{y&F@N@Piea-Tg(9%i`}mTVdeQUCC`D1Ob+>vyQYcgSo!_2p!Hq8=9J)K3*pPN?a5uE5g%w z{G8pic8qk#3w`L!z(07Ja&-V>*XXP)A>oEcrU>$B!;pz>$fV%#AxcM3XW&+Hr963(h#6pG zhj97Ib*nvY07%y99%;MU@%zy_H%(1#?GGm{3lF0elx!t#7KnLz^)G~pNBr;jxE}Vh zo=*~uY_yyA>vVNg03XqD-76}p)uoq?^%}jsHm8~zUlhSK0}Mol6Ix6C9z3&n9m<;Z zOF3C{J$*F?MTdXXmGY8#lz#^P8_^i^WaQ7YGBF)S{m#}=zLF2`^t44r5j7=^si$}0 z54x6&m{BO>wew5lnOpel(0S!lyF;H2&djl)yOS0?fr?jbY%WUp!V{>=B*5Qaq#_(R zM|I6Cw#?MSJjSO6&m$}}G*m>4mDp;ryjM@w)YQ{6S3M>3yrM$U5j1qxj`uKHiG{i( z`r}zm0|OQKd~!mTQ#K(4A&}>U(97-UmP~Sfo&(}=zUl%zGt!35QG<>WZJ-)_TxP+5 zwgIk}k9jksBzGRU&N-Ali{r;?6U)XbT~ALO4!C!zqzx0wv##|r%IWu#CcG4i_E(G; z_By(?!4RtA{d-{vqDrns@1h86pCjFz0ke5s-@&-ib#BC}E_dNXj@EZjJatw2uXC$M z+^7Z5o~6~dxL6vamDLekvHAIudT?>c%g^txl;-C4Bp%8(p&PnU2D0^{sc1+mKB=K0 zhuFZ^F@{y&DJi=mA}y}*L0zAOH~{9V40^`;_+!i|X=&e5^eFxxo;mAmGu}`n`g(F1 z<$}lDI?A6La~{`llo=TqKK`_SE&-UG8;B(e@M_~T?i=tH@oT_sQiAep#VpOEBjX4?!sM3+gc!D~2rr*#hzeG(bPDW}l5J;<@(Xe@o;VOfYq_0z5V1QA!m~>;r#@TT(K#;r#AT=v&FJmy)Ml)>51@d*)WuBrc(N-=141 zlV9sHk7*XC&&cT?cfSM2U>j%!z~4GMgL)ymoyX|PQ2T-O-ja0Vk{c^l_R{>oZUQwO zo#5&bNJC{X=ub%6E95uzV%rwwiycocKI!Os4HlYi_nV`DQ5^@;pD%BFtAHSvaNh79J9O0~| z+i^7~CkIBi`x@))gil_qH424^k9&1-G;y?xPF=N92G1M6jBfnqq5OKoHqE?0awSDz z6Vlap+3DG1tyRap9R!`xPqCHT!?1PCC2{bYe7ftmtn{fVNBIBvR|{T%(K&53bIS=$ z%|rZ!qwBl&pTXgdnX~$SjB@%M%f8^)rC^1%l_4`yy0 zXeuB24vu5~Ryn-YQCXRG$M)@}1p+l%WME#_wOedbByrmaLSzdgJCiwZIpfwz{c*z!xT&J$Uo_dn?)W3K+>|Uz zcYZQf<%*<-QSaHAAJF>pxhw@g@Pavq{vM|b=arG~qeTBEgj*WijEv08N0IN(8+Kl& z)l(X&m0S1Q(K+Mb-RvkNc)FcwmP-+S79M}-)WYNMT>H=C2TK-o>g;eDDk>^)>DdN` zFlP!VC@d7M?gcMm%w-7p)LBc*j^T*gn;ktaw{Q? zli0U!AbjWOkLCcB#@lQ{y?5>2{XPA8`&Rtyh{+eZxwZIN4zu^IC|hI5S8r&chN?X! zB}M3@0UGi6#7B-5yaRTawvh0VHfHQ@Ho`;dEkGklu6^v37L)H?*Xo+ye&EOZIe~6M z9-yY6xgSJ@$BElTvo)+)H*`JDD?5U|&T?{BpyDtFi-G0nx*Xmwd-(w`$82z>O%ixl zADoV46W=AJqlVYiIr)x_*7{<|xyOJ)v31N&cg(Bo3iN82fEYdLcN^JZH(O1` z;Ut3{hm7>}2hs4QRV{+yxs}5TQL(Xeml( z|NGzpbn9Af{xH7eQSH72tU*>gbm-uHh*=lA8x1u+trP=$+EPN4k6crVP9Me*1P~vv3!)u{f z$g~fiUoP@$)kyUEwW7!N#yGk65&v%DsHB zPo!jZgoO32hJDw8!lX?d*h7Tuj~sefwI^BEAK(9b`6g)FMrTKWEXi{PsKNLU)u?=* z4aIHSe>#%O$&(3Qh}@~y?eHgN@quL%l@A@l$&i}-7GQT(zJVF?sV{uRnCKxpH-)+D z?@0t2ITA$WmXn;C8Vf}3K7gibF}$CnfX+45)Hu|8 zvf0PF@KT#(Sqi%UC)U>n>QwO^e61b+Ig|(gg zWDRzYpyV6y$dR>1pSaH6wR-#! zRaXm9((d)5ycdJ8L3;=;+v0UU@8d%0+(IJn^9J78*>?&kqIar-bAs zo4AznGPKWx7c&<&eC{bki8>nog4&;_U*Jt^s;h6cNzp?rGsCd>TW9C#U(3nbvV!jT zAt$kN(BUdiuEnN}xVW1@mRT*ur~dq@Ma6_XP92d#t}s0DqeptKj?k8)oqYG1LjMAp z+=sRpzRfDOwziVeC$+Q=1CllV(X^zKKd4@+a^b8;;0Vk8J?oS2!IaYB-wLH*9DYc3 zS)Nh`=7OjS*cYQaDH-4RFGhC(4m|P?9^;u6J=sQ6E8{cTrp$Hf+ zp=6wfscwBf=eB@%rhv9IH8i%_I1ZBBGuy>6FJLN^30Ahj6$vPSQip;6kezk~-zXH< zit6eFh}i_5y~b1;v@JRu0VXCUMs{|1L)l4=-1GE|g3Byo$v1pBovwm}_;^7VJGAA4 zS2BB5T=pI&z=R1bUgxdyQ=`CA`KvhJ{-~py??HdwqH~8 z?3FyU&urziAZ-t>OIB<~*UJMvSZ|JvwRJ4ng)=9g%tDHZQCtAw>Z=eT0l1Lq6N&bL zn(M>0p~9med&R(Qp%zC{2G<}F9z3-;4c_ga6?VbqxuSp5!RYBibiAav!0P$yq`F5V z%9n@Cu8MA&s9vB&P=TU>PgGQtI%qe=P1L{L8+GoQ54E*2C!by9gnNPiyR+GV3K558 z6W{1T^_6<(o)TBrISuz)w^pvEraq%Jhb$$|&q1+iMY#>`py0D|3|!RCoQcK(si>(* z1X)tBM;ba+RFJr#??S=20w{o=EMn8AT2=!b0)V=?#8CcYYRb&vib1)zsLSJ=<|q`U z>!d$2@|lIHR~&4T*f7*joeoq}QyaXbSF!VAEzlYKI=v~)3T1f@XZK=Re$$)*G=v8>(@$_@FR4x? zlm4O^0-f6Lz?Dp+hKH{hh%H}6p}dwiDZ{7b;^E_q$E<=n;#LXKHJX1gHa3>SdbjY0 z#h6EGcT=rgX>?=R>GSwU*`NUW(2D61OT9t_`GQeeeF^Mu2JAFY*#S`?xC3?1S;$Qi zL5nnn?ItYJ9~~#`(QHq_>14;W8`aB=ai640Gms`3Yd9zrW*k$BVOp{)ES#c)gM;nL zbE+`+k|F%ddzND*INmdv33%xERl2TAgVK4IaWP5KZ6?*BztJ?`o;_SAPBaCDlWT~p z-CGcjL+OvvdiC#f`2O4=IXvXB+QZ8WU(>yz`>yW6f)r~PCpeU=9R%5A8`^@1rl$y^ z4?yulL5LJ>3O9qZ#>TP2t{{jyBI2O?ROr+EGXJf&_alH7miek2X7H$i?YKmzzPyWQ zr``J!E&M{;!c39zj}tH2d$aGt15nuLaUf2?K;^CZhmTK9j#t0m!^gJ+M_Id@052oM zOWY!2Vz&Bh%5Ob_p@1ngkp-XadyoW{5v&2#IjdpX6q-yAm5*Pczr7INi^X5`_q&f% zmMC$?k8>uF(`kd#nR|2Lbbenroq%u1vdZ>r0hlr}ZrY@bj$SsgZ7B-9U*#XB5w??G zO>t2&v{@pTQViqji#sPpC~o!$<$imTpLIMma8%efgobfgy&4=7(Te-;yUE2Put9vm z`A@+f?8?f@+3?*N9v^R%Fob|B7U~L3)J>uyBDDa}Svs!5q%Bf9NoI7_;1Pd?AHpNq|VUbx((|4oPleu|$)4D&y`X^_<%*&Z&ZXt{4)BhzuWuT|ew{%Qp|lkZ5CSers#% z$N9Ml>FAFuD1IB0eTSOY{{@aIPjNG{9{nhs{`n$2C<0WXTm{*IjLghv3=dS^@7cXO z7A-o~3K!}GcECbY4@~P>2tUGmmTWmpYJ|a-c-v+9W6DD(3>-X=kvvoa)zF;LLfw{G zRTC}|0Mr$2`v<9+UvgMq1la8P%%&a=N@eM1snP;m`e1VDBNr|`Z?db}zg?l@5Y@`% z%m1Rl(H}1{llu7HjxkpXC20Aq@IM8b-~_+D6fI?o0iJN&v5LXH7(8y9K)&_UOYbQN z$3(;I0W>Oy0FQ=0T=tn82rm2#$|PRH`za|SLlvi!)J>@ISw>s|3=r&1Hjz3#NpqK8 z-jyB50*ymoS=VE(#hC9E9)>NSsrmQVL>2oqmr%AJt?0TYcF1tsv0@y|mp{PhHa<{C zHsOi$U4u|2ts;WVVa{ovIPr1cP=OpE*(1m8VfO+-s50iA2damc`o*BA(gUBgxwUoR z;%>;{v$388{VIc`))B;*Z?Iv`^)yCQs{izf+a}cCW~^TeaLRVLPOe57Dt<56l_J1k zJni@D)${X41nlT>BbR>b*xcb{O?B)xqJ_bEgL5&yw&Jm443ZX&|CC_K_l^S~S^)UA zmM!L`(uKxOA#av^W|(KB-Jb`xBF=v_UY!6!MvrOD#>!T1>;=RadJ3D*)7vf=okB&b>BSb~lPE5dd(p9fytCLC9jxPYFkG+zJJi zwyiWwoNB)>rND>GrEEPbYcdIu2S^HY^uG$qbae&MDCq`%|MKNve0+TP12PLaF^~|> z^X2DH6$q{ox=R#XDC&WcksVp|w;w|o7l-1R8VG4ii=G|~>${XA!Qhj$`g*h0Oc8!P ziQK}%8eKJUGj3FFmPY0r>Uw_~m6S-46~1s9<64RlTQ!uFlwKG&_rSO@3T(qVZ!=ne z736idoOWiwDNL@eSt2u2`ZrqIEJn-c9auXjyKNrgUo||b-fl2^OdouVmZMVXh+onL zL`6|WB>^sKec{5`1MmUU0r|zc`)>#Z5ENTI(|tt(c(V`E&rKDk7SaQwZm}Ad(jzl& z1rVb*>m2-VyTRQ08Jz!fB{xp2|iI*x3frsJ=onI zkA)J=&CM(4Hgi%a@{uA0kU!1zX~ru}i7S@b!2bGEq2`}X&4_a;0rO3qhYgbFs!#umrf=~;ZzRv~=y=%5 zf1=%o9X8W&Y!reOS?H**Jc{;r-NP`Hk&vhZ-$cA}_&88>uzu>*ebG!~ihqCPiqaT1 z(Wrw%bW_Fs1h8ol$a=~gUL(pKE>uv)s-71Y*fu-%aZZ6^>Y>8W^1Etphw13Pi?8eI z?&EU%xE$r<<5TWSvF=C5cvj02{u*d086;0pD0E9cTj33=0SrHVAQRO%>&|l^Erwxk zsT1hXU(XZp{(VKoRuogQJG0A5VI`ZCo&CW5>546mt zKrcN3TuN2cXf{1VU0rgRjcgB8++maAzkPJsW{My~jI$1jbN}Z-dglDi8Rh09-+`u( z5uk@Gr%g*ZX@q9zFD$~ijnt%4#AxEbPg8Rm)UG{$YY+vlfI85pl-UN9Bc)EoZpI9{ zSD>{R^fcMna2%)+b>B=})5c-BvY_0OqZSr?cnv~Du8bQuD&gADqpL%Ck$v{v`Z8Kc z@Se?}q|rQeiq9sc)|WIQkQR^afxki*=3L3 zzFT}my6OJ(~kY{G- z`x>E>lS3&fn@B_1;us_){m(Sr?b4uyWUbiTMDcuxKK$%@bpp=;ss(>TG?_O?PZ`c3 zr%y>28G%~~v`|^V_>IJ^=}q2$nG5{xz~%8i(F^hlwzjQ$v~u~M2691`oS zLt^HCPH?m<_IoETv^r0!=r?RQf>y_$y_{|F`K}?)*HrFbNDxyUW%oH&OiW3)vp#0F z2+JzHyIx`}T!+ryNAf2sZ4uuwxJTl2FqAhlK>T7D8~zstm*6i~#Y3!4+Ss5M)94AmZpqphbBg+QOjlMFgoUR71K z1Ij|}p*TE7K(&I$MH$P$_tR}~1_@j?arOWExss`>XtI^9?T$djbLzTyJ9Bu-Pj^{@ z;=$E4h>9QcoL4CYJ@%L>a0A>VFfouE0dkJPm{CSw{SAZu+DyH26mW;E7}mxtCqj|fO2EE>=-|K zW#EA+SEKJLjrX#MVVs8JQW+Gflie1z;=0Th{Mn`C{~A$~=zn=iCe-)`8ILHG?{qP< z@I!mrCRK{+yl7_lnESnxhK9!xR@us1@~n6EPNp`PW9LWY5ecYzz7}khgSQJjal&mL z9h2MZMRynQhvQ&Q6U%%ANRb~v;@FCdi89&@Gz#xYY<0X(oC}f1mPNp2wr%bLNIOR? z*NnmC<38kLshQdUFP}3p=qM;qJJ{Q2q)bEYbCYyU^}Nn5d#2cQr^r~a4GLu)WmA~j zm|W3CW1UukE~ERR^)0lSq1blQF68d+{vA_j^9dai#lY+6?|~(j9VQCH?GLDSJX8YI{)X_m+-%^2g_AvDn!36?HYulXo-lL{%uI*3 z+d~u!N|JNnaOwKw0B^zA=1#2Mo!3BSbsG){2>6*_xIkJ|%JQvK`_}%&VS|Ri>Pxc> z2SvLzE(^&r87^CC6)jMc9u=K=k8sjAL9XZE$0KIQTl0JaxP1Rd}R2 zC{HiH**JSwHaZIaHFo9c3jhkj|VCjv@XiKUNg30-(4%e zAqEN=Gi$8CqK}wg3#VYv6h5HfJXEMH4GSIc2w0vFWmWP5c386fp41d~S8)wQAQ1sV z&=S2eFKEDOa$tMm4|rB$aP2)$y^q9f%F>cen9 z60n^0Mnj~-K}3xp3^xrsqjn3G4U<&$cnEr`jgaBU=w6TlOIQ~s!Jt7i%qhn%qx z8%Jw+%xfS^z%rVNpzZ_Cem$3MY~nyQSpd#~NAPtJIuq*?Ydn=i2gv0j8eW(J-3fol zmksIJBNT=X-Atq~q%7(51$OJXwpIBskTm6%w`bwPkX8{QZngDxjxc|q;$d_^##+Z0 zDPcC~rK3Ntq>IOJFk)}Z$qr4SiRxkmsD`%(_dF!6nXYw)h`kKW%gW&K>mHUMoc(5^ z{PMUvcQ{(L7J-cin@onBg#lK2!PTaKec?en);4KV-Q)d8yvz;=O`hg`! zyUQD;H=++o|9utXWfGuIm$|AXYi48Ef*E<8ge$Qp8Ekv;u--hUHn|@OKpsTYt@wOp zS06*Flb)GJwJJ3$%YSZm$W?eQ%y>2EI|TBbg>G;Z3+DP~D#28csCf=H5kjqQcP=qU zde5wmQG8c|a@84BewGElZ<$W7qEI+>r)Km=MKQ&x#qTnl056>rbs#$?>drySa7&97 zGt-rWmwB}>UgZnK_yDq4fNpd--rnAJmO^9SKbG_EJ4>hvkfe%Mz<6NgP`94su}I3& z;8%W&54?6w*KXj7JhQNCS&c>DkX7>lu#Qw*eAE>AjXw!OPR5cH@9V#L+-#^DT)>J? zQTqw=mK>?4=iYC?dJDElYs`QFfn@HA9elBoB*)s?mwzTP-|xqgs78t3!^1HX?bq%h zH~Kv0Z^9u<&$*)ntXWiSH#hKT;4(fnFwawcnKobFzHbQ`xI)pjIB-3JIc1cepPyZm zq<)c;ZlB1Pc_3y47pH9xjuFPZhtJ{5^XQ(3wD`CN3C5<-rjNeqD zr^sagw@Oha3vveTbQ2@SU9or{n_XwDw9lpK=lOTBtu{g5l-7@he&12XApUPLUz|~v z*t;v?=t9?ou=p#80DB@8KVT0T7!)+D{V0b3x2$1*pY4XS6QCK&O+SQi4rSJbt7$D8 zfs8P6a9CQ=9R2%*b8k(%;!hF655@Sj)9Nj!WOF)jVSKJMrJqb3v4-yxT!40_k9~!1 zpy}kT#}g+olT91TGjy5sxV3guitQd;MKf4P*nlk^Fz`MH117U?{oqnTf$(waGicbN zQYMy`Pe!bJRCRRZp^!8Vm-A-d>${qUX2m-npP3n_^pSSS%?{t_7w4F=82;!-gW8H`tcpn&&%u0(1SSv;tVka;Mu;dhS1xmY?G zTVeWQnV`m>$;ms8R%bZfP;! zbPc|rqr}QZ&+xI+@VlLQlSC8VYr@XP_O_$Lu2@1-}`UR(d^i{)3{*u;t4&y{Qu)|u4NNRivJATV>@YxRka+y-9MT=?EEvYHwjH> z+YlI-+Fkol1jmBd566v%)j7RTxAkO&hUY^tSXL&6<`vIc8BN*medOPKi`YUA$O88Z zc4@sEV=xsYqZANo3wn*#Ahi&D2F6?wlkdB)pffOke@lAH=350ECg#hjs74GTF+0fk ze+c^yaIE|O?W;odEVoFJC6VVc#e*4m+SZYem~>AkBf;RgxlO=P_*!U-xaZI<8Xl% zbi{Ua_VkqW#M~e^2b;xHxF4RZrax|GX!sH+Y{yg`D7nYdX~F&fx&N{{17<){j66009z>-%es7F<#4csPWl(x`woi@N_0*U70_>tKu9- zZ~36o9dCc_LdeG3hyhdf4Q?du;n~udSS8B19s^zNj*AWK4B#G7W_<%~6cDz?K=Z~v zfP)`+@=$T0S>Gju0-JEg!6C2w8BanO2yy4)qS0_uhf;MW#r%1vS;J|`V^yPZ=xqgx9UK3reX#LI&697XF_O=<3EbfXcI2)%ioDTsn7$Rhh|Q7any zs{wLI8^~u?P(PM8%hC!MG)|J7ji|b#Ht#ThEWc*wrqGF@;FWS88mTO7&1EqY!J>M1 zm#C;H`Qw6QB-Ifb;$P~08Z$TSPqa>L!!KX4!eR&RA_p#Op49*s#5{LgW@=1|qj}7c zY1YPVbOFX3!?PBgvrRaLLd+=kAjNx1ioN5lEiGr!03Fq;?1jID&W`Mg3XkvlO__9f zaP7gD4-Px;qy|sZkK{-`#nf$F;M$236ga-Ro(f#Yqep#y6pG^=l8BbgCL(}qNJR1V zDAu&y((p{4WE6k?OvqaiJFJJ=PXe&495(@5Y8+LKsfV<@yb$`;Z)3N})u}B%D~d6~ zXRJKmGlLj?ry?8usbbRvW;DNUY`iy*MgHPE?rs^ReB4ZqYv%{jNdOmGpry>Bnwh^) z4K{ale6bQ+DGRYAp_Gh{BVJg6ekI=$3%9nq0Sr*zfb=V2um&DAzgTfF-s>ke;oH43 z20q*gPx?BcQgl13d3wMwkr$)X*3@uSn~k9kCt?Pl!&es$_tpo%9r`T%P|DE;SOMm2 zCpZsI8etG{Gr6$F^Lo&1+T?YNjUwXi>nlREDmQe2Bfmj5Y3DB@Q`WjVm8KN1$TZ&G zHv5QvP7EY4I6+j|$k~%3w}g%yVrKj$gnI}0_+ETZi-5fpv@Nv%c^TBIU?25mNd5gX z(9VD|tH(3}*nBSo!@W}{iG2$9{#y7|VCubYy~L?g4n>6d106(&8gaqcgXFOkQ*7W(ufQsIa;D=K1}!8Idou?{M>Wly z;<52UKBm0?0ln=NcNb`GmD8-;%RYnw4Mr1(@xiBq%hbjXP$<9d2Nf6phge)t3C<0m z$!-Ct(EE0EHGzdCb?MT&urHMZ1Jf*=CQ4Z9WtSrSnO)l~P6fMUtoDr=K;T zLH-8st=QufGTugGCaJ9*S}_WaXV~!R7f60^06dH9yDdN(6a+*CDQRim(Sg0~8r+0bc^jd+n^ZW)8RET88Ot5LIEa82YZn zmlmv!AjuR$n?jaR+|TQBuhmqslr zhsE#isb_;k%!#ee7L)e(3=#KNl2ZBs>gYQ?aiEt!{r?261VSJQt?h4vx$*%<#*kXI zTcfoWJe=FHDuD}TKlP%=8qQl6kBXF(YlX{UYzY7H>jhp=(4fs8 zJQ7^-mjJF`4;+1fF5RldUG~BuPcV+aw;@D>$-WPF;nSO3Yuk)2k5g;UDIqW~YR8R!|6sppg5WfDm#%(L>~F%!_!Q;<-@Puw zgYPzG6Jfn%!wtr#@?wctPv=ww_rYN>gVg%=My0_h=ru;P1X_`=lnsEsUq&5nHD0a* z52tGyEYZQ3LS9hhykeIbnZ=RX_xKO!b_wYjU=^8v5lUkQF=x=6TBt?ERIAxn(PS;4 zoY38(ioVY&sh%)JQERW~G5b4D-2R`2C7~WHz0Ms5;}a87FyiTILYw9dad(`vtC!bj zwUJ3*;K>y!hwk6t2rMAQYsBCX*jb%ZDr#zb;9>bJB!rQ%@@oxXMnX*64L5PWKZHlJ z-5@n^_)gTWHK3plK_aI!WvQ*rgj3I6qzs0nC@Q<9p0ZFJ0qwcQe_p_(brtRR>-*T* zpGjXN0xE)U=1U*ggg|q>#47OR%U!7C+E{SDOT_zB~c*0XDMc#S55s9fXc!AOj8GIBiU;B-M5z5IBdxPjSn*eVOSUi z;*=LTuij-wOe&|k4eZozxQO!)Z%RYTRIu}Z_H4IlC(X*iwG<7#>hrMbC{nf7TElV| z_zDBb4}y9Vw21HkQpw#Jo%X~t!fWi9x_5N$MQG9gPj6mpEB=BjP$GR+TvzpW^=9;) ziWLve11Exm$=a>H;?*W)d;)D*0K_<)yLx-Y@jaXZ1o7Yqjqb%p4%MUV8Bxa2@BFoD z)%6#ZtAG;@3=RUj+rptL2s#?5+?8V~M4Hf_?7W5DfpqkTM$v)*x6SITjcN0@E-Kgg zAR{G}t@FegLo;Ax7YkIPuiZP@+YBBFQ8s?luY)J_r+h>oNS43P8aebk13q7cQT z_8adPq`DwXB)`AC1*ev4eQ%1~;6WFHQlX5TAfU8MTgHocdo5305dYg3;Qhfp2M^}I zH3=+$weVZi!Q1lI&QddsWR}|R!ycivW<53D(%W~!z`mqIfg_NAn6IQe;qBWCuZg_L$21j^Hy)Ue zeeAvUPz(_UMkYc{HSh{3cvH9^g?jfkaBp;X2(q(FbeUju#{r9{T+2){uTD(#a(yvo zh;=pF4qu_DO_Kxc$|w%q-C;mgLF7^CqV?x{QVf5xj?{RdEWoI=jn_~gob3xdYuGl| zH4pqCbUpbN)JsByzT!@((}b;+@A2h2;3r1x8m8B5!9k;}DqFiR>^f39Fl)BAXJ3MvqCoJtJDO$6?sW~R!rfB${#Ww_SrGxuaK(Ej&%eiS$b zUU+oSBq3x!-SJ5S!{=?4r04(~%EhV%>FPFhOdVox#7m+A#Z4`*`V_Z`pDIe8eAnH_txQ6Knscc|Sd2)9S6Mke@KyUY;CS=ZLSYtLzE&jIqi^b@hGZ z%)WjBe6I{s@z1bsY2(U3JjuoQ*@9@_24}|IOHoC7BX zJ@YtXR`BIN%L#RV**9!A^8Rjj_K`*puu%+fFy>?P0=0bDDc-RO186 zD9%0+*7v?C`mvK8FR(zT^Z>~8IQ$Su@4w@$iYSBw&W%A&(GA}?_^TR@lq3OM_P^P< zB2E3G562_d0ZRBuOUtAZCb`vAjz+1#d!(LBE9z}-NT%{L&<@J1JO>7hHYOTdQt-O) z$Nu~OKt=4a_4qjXJr96et$hBV1WW27%=n?z%e79z8{*09AxQ+CX`f1ZG_I;Ps1Hsi zYtgqu#{Hy3`xG2O`rJ5J5&(Po4z9tIgP46wk`}15z22PEWgW<7p718^UjitlzqoX>7*iZyp61ih`T|>n~a&Cfq~*8Z+lr z-3gJA&WIEW#@N*LbeHAw4$Mcvt`9L{b#QQao1R|B(t=VdI!Fm6PgS~2%11%38;8gY zptr7Ytf|yE>4l2zA9bpPLwZaQX*O2p`$3|pMR)Rkya-Zpr)_uO?b)Z`i9i~YuLd{q z)Yfkbpw-{Id-r|*N?#gG6$RLxdjISlpCeA#lIED~TIrwJ068e+K0Xq8$TtE&qdWar zczAis055PykTvgxFuH<0AGgieIvN^{fpbR+@i6|3jkU9yK!&eSFCBUrye}aTu7hDX zeF?HSfR9hH)eAFDNpvM-^D|CMp?o??B-0BroeD;-K3Ul?CU?dGWD3bu6iTGmYbxDs z`}XRd#5iv7ahP`RzKt0$ThfKq$F)xW!&O<{MgFznPeQ3AlmG*Yz3IcCDd^~%Sh*8Z zQqXD8u+Z-Q3XA+E9*DT%WAN%{KJ*>?$%GzQC(>xvjh1aVppKY|ZJn9fh}aB{I@C*J zg6NXnyu2=ebi-Qt`R7k0kBgkgk7EH}+A6v`;Rf^Y@5Ai-_V>e7 zoqd>?s>ttgft35BF?#}dDHpqVyasxnL<58_RN1eHtVCEyxih_l;R{?51-k-Dx;0?8 zDYgwmBEBI^4SS%gezAWBfKp&m_z-Ft`Ib`r-o)^QKR^SHOMGFeyu7|K zR-{LwKBOb(C*C*TGyESK9~KLm=Yhrt0~!Ey?loK$a(bLvAo$Vp?87gBKfi8of!E0u z@4j1A9PI(Kb~aBBMaR~|XW>36Ny&nwE8NSk0(@84&Zv}nQ=&cg%vE>^AQp!XsNJevg8b7-a$pOEQ?p}^+w zCv$8QOhwwtf=|xz8tqLV1x;aV&vKdgON3NbWQS#gCE6{jK0H^_{b&p3szv`KP zoD$ZRqHc?{Fy?~I!pDJX@Uy~JgoEl00-TYg##~lN|CVWnP2b+GR=6JW(~H+-ZaNja zRzzX>gXtx+9L!&ia#UCV=yJF7RbK+M0jb1cTJCqejYuu*J`o|19PfYp4F1jGJTk^#FU# z>jBk4VphhoixquOIWISMh;G=M0G$Q~OMY@3k9gc2Ou0-=lylxkf?)$nWrah0Nb_iqmCf3dRf zjW9dG$}hqsz}uV2RKelxtQMSI|Myi4$}mPaTphgqN-AvPw@>PqBI?QSoB#}^Ve=Kt z27ldgPof7?@x6A4K%6u*xd)@rlz6kUp&D8%LSGLK_3YeM0ZY;3#MWR#{I!u9A~ZRe z095@sR6}pRK7YP}_$ZCFE|E1FXcoHIxd06ZfrYMV(Mzf$wK+(@04L-JbPH?~HI~4c!q12=EZzWBv>L78j zLeKETgbpfDr1?`J8ZyLlt2kcqKb~73DG2qNQz~V!_7O&zz0~uWxA&K1CwQJedZ5r5 zPlT)7OO`H`)YFUjIj{{2-qa33y8as5IIo(ZVSKvdTd~~S==p>eARdil+EbXLef21V z;gVbnZEq?i>+K#fp-G@d_yLHWiy(+>DjN}RxESY z3=h-V>YN+*!i5!GS^SQu1SlvzJoe+A};HTNJrzy@b!Jg(_#xzWM9mOWBi8 z_j`zJZWexYfug-)-LBnxC#I+Sc6)xg0f&<6jDR-o;@3Zan$($iaK~txxBC5%6XQqS z@&WNJhzmZtyGh~*sa>*e|JdzihAitwU9KdrJG3ruE#=njKWpt17Eeq*hn^LwG`-9* zEs_d!kYQUo9O1HT(`X)Fiiv?N*cioJh8Lz;6)l@AV zZ{6-e3%ZvV&j8B`!;`GGcNiPKo};1`v9(RlIDKy?x)1Z485QvFbdUT95aEGRqTxrw zRcrZ&o=Wl(=aZWIDZnpVf_?QR9}{ma0mm*J*ktQ0i0;)pb1v_hGQL(J_YsT-D=oGn7*s1^Xz@O9rObucFObGBCElo|AoML7&GZA60bX*U<_+ zfAQk?zz#)W;m51C9yx=*oZJ~NMSVyV`@VI;T_%*<`<}MZ+6FAO&&-)zwwP^1wf+xU zGt);xn7#e#eShl}7_Ayu#Hya}7CpgVrxPcP1l|IuZ+gvUb5O<3k+v7M2yW=Y;TiBo z-*?SUp2XB1z$5;U@||+I_6JdIFFeCLRNg^EXI<{WD-)>{x=9KH#U1=s<(fumL!}~T z_2993>{pZ}A(_fR&a|Z@-NriTtS(&b!QCp+HNwBWOcthZDgaYROxFScUH&6^1_p*V zC+z_!z7W-I2D&_e`YfOT3j%j?|5~Rz!jYI2~(WxyK#Nw7I@;tpwxh6ZY zGO3E!D~Q`ev48sUG5U=~c#Uu1d!D~Sn7Emgi7P0>JsD)=;Bs5^XJR74#_+=rRfv0M zT#wbfdcr9^gcl58_vaJ*tk+wKcgLx> z3)vDg)a#yY^>FxQg~iD;*o{u`{${}GNo6c=LrkPBrcDo6*ynM@M|I3*8)f0LLHc#6 zKkRHsKbjn^g2#}NJ@5|6Ip1cqwyiJ43S*rI{X7+54e*}{HG`7Dp`0_ zoU6!=vTl`Ez=FHXcsg|(z7DR;5MN|JGk0lHKqh0ru&ct!$o-&n>ht$m^*4_Drh>vL zi97M2J`W_2-9TLW@j$ME+_7Qv=9Yn~hoE3ReE1pQ$M1CgXt`9Y!YqvQLQPla0a$E{ z+w!;ommjDmbI$O`W%Oe#hbSk;mJj!iE~tNfX$EKYV{eeBiljm;4IB0-keHtj^;q|j zXI~KT)uDZnOwd1}cwtMI~4Ta!Cfrg&Yye7K9qApW1)UlYPyW!3|5T(*|%rUY3x&ELHl>8_zoGi%NND~ zH;^=i06^^Bw=c&q4Rke*2p24Dm(j0O6>r_V-WzJ0f-%%-lO89Qp17hmwSbF8c;bQJ zp}Q-C5LYemvioRSJ!j<8ZzY-j{vpwksthBQ>vgG?D%MAOrRNfLZwMu8Gxf;@Q6p=n zh1ZPzubk;FuwH&YgVMWj46C}T>#uruLg_gGXEpB0bY&RzdSmm@j~mDB9{j}#4WkYn?uu&R}jRYK5QtU6S z>tps98Zs4IfOo=*3=o;p!_%^Nf88OZyJukQ`<8P07Vb}|vkH?;K{3l2B(?`-=57=U z##@`P;O2KaC~2wyP?*qQc!Eh)0JPMmW)WpNcP*d2T418x*ubg8OFIN9mp)D|%T)AI z9RhWY2pOoEc~TC##US`|e1O|T)TdJps1(-eC#J7Bk$BB!O{U^0Bin{iZA#x<@nUuU zrwI_%sEF3{bD zMy?sditkhW0s?i60W&~$nQ);lC%5S!WvIBcy@Q*-3{x6=rb2o%~nY5a0lY}1zC+x-xDtC__K~qE?`lghoKiW-G0rR{nmpJ4ajmv z;=&9eV*CN$XJ?%VykYCdc=^0;fFyjsCxWQ(i?5|Z2mdBG_+ZX4taA;>fo^s8qbE{^fK<7l(s1!2C#o5eZIU5vo)Ax({XywLA1_?iRcW>!^FFO2rkBip$mhIb_ zA-^5Akn4L6_GOAqy*LouTicodv<7b?A3>P-D9%X+cpL_ejO3+DzhV+8l)$-gNS5+p zB(wN6hAYCrq2Ayl?r(H}mmZUz1!AHv&8S0Il+xc^0AvqrlE68zqYI&BRk(griJz6^ z)E!)gnFcn?u0&o^r7ZMco;Q{Kb!9^qiis01&bX-o+F@ymg+b7uDpXQ#aF9tF zc2oKLd*6OU8W(12QDyjo`j+|T-m(c`w_Nb`Yx%GVHYZH4S*c%Q0s@fjzQ8$o9|F7^ z(1XvQBpyI5;E9Qqkf^8|_JCX{^f}g2XWi?LN%a?`#RE_`JisT3YqO`qC$8*TH%^zN zFA0wkVixF&gx<+SbXEw_#E77H{|YNf12sw}A4!;5mNSQ&P9+-B2~rxKkYe+2h8IkV z6tT?nAOeM<<(hC5wqy%*&~;`~U%_9WvT)E_M6^cd3oO0g4w)&?@!Jx_?mhG&*Pm+w~oWEna`jk@9?MHdvfJ3 zex&|&UjI}`S=5@5ln|tTF1#A^vl*oSN+)#y$1RvPPOmA{1%UDfdl!5JEZR#x)?fW7 z0-AzpKthZp*6E_!6>u2@#4GWbji0|(ll|w7;}G6Hl2iia?^SUlwl4=?nz5zt!4|iB;V#XyQBo zsT9CV_#ieMC-Z?ADH*3DeR>%EUb#Wjf=m3PfW)(1HGNZ?kmpGM^_6WV8&&hmzu&7DDCznvuwVU+iW%!|&nDa2foefc zJL$|4diuFd+n*Mz@xiv(4Lc8*(ve-pSR&yJ^*HC6(H0R=QK!_ttd+*ola|yde)zAk z%_3G-$-#)Ck^0FU`#o0` z6exQmv{9^HN=hmScKUejD^V)fm{;rNkB(w{K{lJwJKzG(pEE$WF5n0}S;f4m9oLaW zB2CB|*L{_X#{b`o4zkPmDjnvR;ED9T-DzHYsu}G=*FGeku2+U~WgKwjbbpDW75<0Yj2= z&>ihL?pqkvt^f1G*-=a0gQO0VRWo`6NCF#2!ZtOb{IH>^#NY?Cyy&;xIAEW#t*R+z zWC3OQl5m(|FW&$NOh&=0GF0k3mThSz`r-U4g!=LJBxZLpitWJ7F^28)!I{h&tOw$T z8vDE+9c)2?5xQ5Wc15O$cCu1b8<){#vX88-tVzN&J!=W6SFhUkgAH=S8wV8yy8T>& zZY2pC2cVMl!$WhWtT%pb5zhNSd=a_@Dl_ct3b(ki$V&pHnA|CIf!m_z#aBS4gy0IA zmoxP9RmyizTb|=FZQliMj5{WCQJ)5o>JF&J5Yj@_r(gMKjolhWE?jtu#Y6!)!}-in z7f7%`+8?TFTSPf=fRrsKo`Q}kxR-}VLEA_Li}*;pMs{=R$Mydm3&&Y$94GOwR60X- z@mbOW&q{;EwxbDi>F{Y~F_`Ow`(_J(r>?ZWK2TJ4a* z!rdoVUZFz?hg7|Z^dly2gFGiVxxxN;Llogoyi3P(v-%N7WNK{mb}HC{SBItS zIc)3;DOcj>$9D^K1~Pyu+{CXS{i-JQfIlx9S~Q0@(pq=aphIwPOzlbTN;O|d`j32e zj2Nu;Ev3|b9A9MUG?mR|O`EmgMB*=0ZulAtWx-;?4xMwbJa81CN4?T|b$XF~=BLS3 z{{+1zocR-6O=AlPh^R&stJ_oH;9OFF^^Y_FRTQ2&xo=SLOs`>nix103m}vp+hr{h* zjyxzhPk^3z(dc5h(^|DylhlMH#X@3vk}0=ZKSd02ufB0HC6i?y&_U?|-7w&Cq|(op ztVw6yo=YlTs_4`$HsniKVyj15&FQ?$NAu1BK*c;ZjV2HA++BPZ>g7sZBg$d=uC?S= z7b{!u-a)y$(wUx;dhgFZE&>D+`IrMu=9&La(~aEd)z>+5*-{AlX9K(D=U!fKwKcfT zoK>r2pSim30Y!R4*!Rxw%`CD7sB%P~fPm2|+)Y>-!SP+ru&z{P%EdcaHk1MK;JU@w zE?-u5486u=G5)rN*huJ~tV7w?!ZSTND9!|kCX;bHWOfE_*PL8jMulXmF(6~kISK=s zkYSvZ&v23dvwO$#b)=EXMXh$&p=lY3%Bb2-?D{*uW_?62cO8!drUSG|_;KV>gPX7i z;G1#wYR(I!mqC{~fNHA%Mg8PX{m)nMBer~;dkWE>?N@;f;mE%H4f7c?S5e~t$Pkcg z2uF{t$8UInhI0YxH>Vxkr6Jv6<6zjc0{IaT`f42KM$%_bjkW?kh-{H=O1V05`4~Eh z>Q?T=OVUppmI>;&g!AmwhCLL@rEIib_Url(uk$gve?xj{FRue=T%~M#)m-?4V>2^g zZJ73BQ5%6V&>9LGIfDtt$WEdPlfSNbqaQ9k+zAf4@Q-*Akic^bBhW=a4SN6vxnnzp z90u`CuIZF9deWS!0Tsg+&E|uoGd86Ms;VO`D+KK(s_R!NAHEtb_Xe@$ITi&2!1RQX zh%~ZslT0-!wQG}mQ+fqlCd?wBqvTHAqJ{?HVtts^?Ypnxc*lepg)0OD9p#~}qA$24 zMtbWFbg=GW5MToSOy`Z)C?Tsitd;<;U^sCa)9-Bp8PX_Yg)Uz7!0dA?`?b5bz)IDR zn?hS%6_G%Gk$`|+qxwkcFd5bqy{Y<{sQ|^{TQEWc<)wAx4;jN10M66X3CLQH&&gcw zOi$2PX?V`A5E2$Hb&`d5z|>Be6F}IX_Smkr=!s*02h+sq;V3&HG_S|HJ@K+o$Tkm; z)RN^I(Q+(w${W>UNvU-$=BH>^!A5buz;b;+SfaXR$^-{{98Inq-0Cya##tGS3RKotPAAk3xZr2(uGEOmwt+kw6H>S+OGI z?%l;(`EI_4lH>=Y&B9Ei+oQkfDU>~2wqBY5WY<)>U=A3XH@sb}~*U%V8By_{93J}^iCs_cvX;rO=yg|^Dtf_oV>RO$NcW7st zzn!NazS*sa9c7^sukY8~YM0yVDMUKT7xAz}&$jOdV2h{49@` zK`77ZhPxD?Z!`blEbQQcf{Q!t*&*Cg{q#Q?+EuZk9QrkK$gzF`+9aPnMfdJikztsH z*GB>z=?=e>jLs^!y*V|zo0_i17@UjWEA>Ie3+U-ecjEb)j{S`l9IPZ4BH#$8o-nN28htF zPfbnOWR7Y<9OQ#T^fmg*$Av*lY)HERC5x@&U`I%R%;OludjZD|N4E6CrbqJ2$e;#N z)W)pUnT~U%6BY+8&Rc6j7|Vy)Z2N|U!gDsplIRI|#?J%8!w^pTGiN5-8#O(f1u>slZsa;*PA=d4nv@p&hUuU&+oX&S+>T4mG-Do;s zgbo;9A^4odz~Ys4F;b2?Vp}P8Go`eNpiZ-u8mH0sQl%av3IXqRVJ1-$u^qN#9lh@t zssOAlk5pDXst>4VNLUeye$ymdXY`f0{gyB8euv{CpOA=%)Z@V_Nn8R;bhudLq8RFj zPh$2H2NUWkG^gKUr@?6`5aX-`%!r0ZOt(@Qad-8Y9W#Y;lVKj7pGgl6#sSV6!#kx8 zwt!jja)5|DLm2X8SBQ`y{Z?@!S#nY%uv=XNwgK*_Q2vBxoZz|&^nNajKH$E;YU``5 z=dEe~6#H@(zdrt$QOgoaSj?Y&eVH*5_rOWIfCjibQjJ;q-Ci{5rg9JPwlKjbT3DCy z{R{(0+&s#DI05DMqd!{-7I3r} zfTwAb{3PEMgc?4b*EbzJfYu&Ni$c!_?ei`;Qv~rrCvILWuE0X`2L?AgD;ii%iXaEW z;V|oU`lE+fvRSRYJrb)bt$#>kXW(^wWIj82kHDqgg%O7ze{;6q^W#?~>d2_AuTda> z`T7+w>d6M+xksmXwp#;bX-0sRO=fJyxQRW@W;1Hq_ma0{MNo{3dL8~)a{7MTSMS&5 z=N@g?*9?p~c-RQxGv9_6y`*+`auZ4qK>gvH+6eyUxn%*R@621fjJ0EQ_r?j(DM+en z?cJ(GW-o5rzu8(mC|Z^m1)5V!vzE-(trYw9I|TV; z(K29)R_fHWXf59^upr7OBd~FTE>4Z(q=*qgpz)<@HD~Z0cyEdKQ(u$MOaKO}-}E`<%26IZU;E?pPtHS-lFSr&zIGT3;X zY})bak7B4~5S+&=4Y!}$7`tf{f)f#RR;B@VAieKM55c=@Y@=YBcQsb=>^T>@?%36` zD-_5at#E`ujJObqJM&O?y#bo;lT0e2*R_8sBBqjzM=i0Fh}Jgu;hWVxq@2*01OedJ z(ACrDsKDLm9V|XoK^rY-+!2$qhs8ib(zIEYwP5~B8U+H|l}pYAu^cR{ zM;+M$N-Rf>v-}E$GiSi54|Yr<=`n%L|78$_Lf3Ek zy*zv7Iy_{Pjffdv-thgEt(erzcH0DaY~=!)&~x=93R_p8bzQkP5nk_CvooJ`eE1n5 zE0Q-)BTivw-Qml8Cq>e%h6$v2b|wIoCMJDHcDOv$dY=0!H&^HR@$+Z6#7jQFf3jAN z7{D2_^rj9vM+*4g=%vF)lCwvh)BQl6A(f&}zjj5qcSiPKJaF8o;J={x>EZg|1htr- zP@nA=9(7m+;p@=IPGZ;JbM>6FFmPCcU%8ndJ$lr=*VD+X+nLvPYFIX9?ZtyC7)g=^ z-lPFUJ?P0!6ssoa;um|_t40%_c>M3fd6-!3hw?Tc`|KncfF}HyBV!0NUWMaYV@x6@ z4W_i8-nya1C=i_aBmOg9&lhk0tgHcq=93@;906gn_KTGJspmAAeDgYq2W^PZL0hu{ zet!mS<2vBcMt+sz zym zllmd6cJf@N9R*mWL;VcwYdwG_t8`KiF7Wr&nEbB*R6JA)9a3Tai+x(&0yfDS)qw2L zd$8{)#+x^*++u@{@_yIK^YiahG>mn$a`&U+AjNL)dxugq8>TZAsq7wz)SDf18V3ZP zV(j`8gprQ!YN^SqLe9eI18*TW1;_JaiFr^wWFyR|Zi3@@Ayf)7$7?VLjE0xxjH*JL zjz@ZO+be7k&Sx`1GuB4MA6wJMJDEuXW=6Xeja0n>yOQ`ON=Qp zEC>t-D#Cj$)wJU|X&<2HajPc$A{b@djBYzI_G=c5n+<;^V7d^Jl*X#4{TG>7Slp04 zbW~EBoOxFzsH6BIXy51`UaLl@w8gB%SYomp2+)xo1##lOsPT<~GTC1!VPF6w>zG&? zGQ-iJI>XF))$(~X4#%40fHJ+Re5@KKiA~zv)+6D8=3g(Ce8AeC+KdI~*KYmb`BEJz zG1$*!`!OB2;7DsCxM4${BDXL(cn zFcyzK1LlV8??SQ_vEh`*O5v|Q$3%!6&axxOvl-Qv2PtKHP=0`n?TKPu7=;VDg2~I; zl?WGZKsYsr{&VwNG~&6;-Kt%H`Yu0)$M3oHi5}jvYC-(^J>!h$)loVI$N3^PBNof* zR0ar1170u^V%`Lb z3A-Ks_bVH%0Um57E^G?~$WcW#^=7pG3(($m-?GMRj0G-!g0>nYy>5M7wvbm`;* z69d}~Fgw2=WfLkL<`GF}u_2aBw_@}#uX{nSA3v zRL6pQbu_IQ(+84Cf<|^Ki>rB{v1QI_DBQa*r?TBy7=zip%*+L`=8gd_hL;b@M*dEi zFd}>eV2Y&kCNn&c7AE#VZv2uuy+2?)KBR4lM!5xDo2_FYR>%u8o*>hJ7%z~5g5c+s z&)7~3JcUz$W=W=5nYEY`uV$&LC0Sc%F@I{C*Wleq%ecu-rwi4XsTvId|X*Qm14%UVCo| z4%ZO+`qtCnH6nK$vYLWs5&1RhXRE(;4|}->+lE*!o+vH6u7{t&^$Gbi;jm<7jWZE^ z`S3uQYmKPaUh+M>jFf5~;gTb;V<@ag$m%rTfd#c^dlM8Qj8ROkU-4owYo)+$F7&j( z-^Av$E{VDQaRjh5;3m(wKF;p^3A+{SW{s@e((2M>DNv6F+N zK(mr}<`KES8DW{7sSzm|KvUl_KJe?#+T{Ow3vsYLj@Z6&Po-$0uM*jv0>Ot3AfGip z%?Ep^b7n*fl3PnZsEeR#aUANdmL<7$*_s`Pr%b*U7P#q}^aW6osBU%LqW8bu=ZGw#x~NqQY39vz?Prn*YH;xuAF5rntQZ}T<|`-> z%kH||w*aUJxphQ`G|`V1fM!A+Y0d%#Juon>cmhk;iJ>2Ufb4crgwCA2tXgYM!k1nG z{KOKfkMf=@&`YQdcI?tIc5B?RPYceWC6iO=KFS`afeF6)sSwdN?~EA%))f|>mY!eKUo%r3Jh-gyH`5=@u1^zZW^ zvMU;adK%U~g#j*^XRbG5J6V9o@>}5YJ3^C1gO<>En|of<+B-u?Ea1t!+U|--&Mg{A zstUxWwH?g|$+Qj+T?mP+1=OhLc;RFw(Tq{^Gep?}T--xgIsA+ah9N&{0XyI+z{W3d zQfNVBu$|!R-p3gyi|NWfw8n?irohOkP_IT!HPU&XBErdHyTQAZn+MyXQfkK!aSZX%zb6DORdJ#X3vEw@TZPjhq<-I8N9|)SvGsGdrch zVR+fEEfctlfC6)m=~Z1mY7W>srMCMwe~bE9UxR*vriOzDnTya$0ovH9-+O?>7rf#~ zOIA?$xSdHEH&}=AmL`p6IhrHJ>rWm%VjpU50u|xE`XaugP#o3%2_(tDe6E@c8oXEJ z<^8e+v^6rK7qgKG`+oJvMorX*%KH}XSGcy#q*5s>|`a$qlNKI&Bx5phLPcLCLU3$T~xFdxlF z@qHa}<^$Mpe^Bq@$FdTZJ?5NR5*v!Hn1e;e^h6qjZZQLN_H~DT$J1>;BIRY;BM`!p9(cluZ*w8KCPe8Y7f9_KTj)8ZC>-_ z(Y%V6>Q_ek8M7c=l%`ss@;)1tD|IW{sZijekK~dh9{l~ zh9_&ua5&=u$1Y+ai>^`GISxPo%53i{^L}09-|is!cvZDZqZ}u&RQAjW3KcTp4bB-X z6=7qFQ;bsK$G*G65oFDU)k51G$X6o4blNpUvSr7a^JpsGfX$_8R10pJun{2XvaaY& zb_!2llR0oy-om>_<_`V7T!nolA8I1L>* z3jknOls;X#Z&9%D*;I;=9tOn%m1hkMTOY-CazAi3@9b{exA1CPpyT>++8or9j{KC0 zNM&X+2x(3A?G(F?>HtA9I2c!X=T2786D1x;V5D`nmrT$RTWQi-^~v=Ex-Rr*w-gW& zwAXCV=(?krlPo(97S?|d*t5Wd-XNq2cdh@9bHE7rCRa}OXjB*UVqRM>^yz@adDsqX&rlw=Pq}o>0qY*Db>Dn={;|huH$B$RE zSVqE^n@6u&+Q9=cOh+Z)sHStlG`SIj{)ms429uvHW@TW=bD{S{!%d z6Mh2a`N~-E{!B83Ov8{D%Bs-)2kzsOg|ubY6~3`wcZ3eW0*+eh!XxVb<`KZLiG=9zYTs=gai}g}+at8?9r+naPKtNPYWT({CS|+qH zAF#>JQ!fQopv|~xVHe6_3wcci1%+=T5YQ3gL17o5i6HDdBeN?{o7*T{edkVd9|N>o&cnz-iz3B#FC4J&XXJ>;iko`xDXAG!Dl2OP;W>s8DAzxeIm=bh>T0nQx5@LmS2w;NPA69nnQ#Oyc!Fa8w^%(A%QWrAS2d!@mmPi>v zz^d}qHlh6^TJr953IDv)W*v(y!# zf*$~iRsiWJ_(D}JBQjyqwh$C-JlDjA-%(ol)3(i<_rccj7>PSsLbDilgldM06ejLk z2LQdGi)8Zq-QT=$g@qqjJpfJJ8sUNZsJIa{%KMU{f>T4T#n%>|l6cETSs?!2Znm%V zhej+3+5MQ>2w^y-Rn{%p{H}L%Kg&r+5FU`qM3ADW1t(^i$=?ltAfGuy*aOvEDU@rN za`@F}rrIq8!!?}5>!$T%vgBiI+t!*tJ+vc(rP6D?4mxZ&4M*^xq$bA@^0d`7`?S~9>W-9~gjB)_{<^V&_PN*;Mm zF8uv!Y;@2-BRELULro+af&N3uV%_sugWu~}lWM--AC6rxCcjr+> z?uNlJ;(%YtTPV>=U9$=YK5Y~~kmK&}-xK47+5PKHWoDJic^fS#YiAiDv(tb6`dfof z^uLncY^V;eB2+H=RcjhDhcRSWL8G+dB?GXYurA%4@OG~jsZun2(SmY?vtgN*$<;jE zbF=E3KC^S4;b_!wDCzc+?`Vq9G0N?U@fu|CweODBu+KI9|L|T%{m^(IVQ@4Zhw8Uw z6a}av`I8K3Nd;AUPOu}m4LHz?8_3pu0x-b<7ON57# zfN+naS#ugRgWQ%$DtC_;CyuP z%qP{gDj#SWf6vdHeV^d-(Tqgsi8;X+us~XR4VA_*8|mWs*cL}1Pr8Ir$VICnQZT;w z08wrBa3q+U+)2V14ei*^!(>~ODM3~9(O(X_hIP}z-Iz5$Iq3?Pzx|2z^P0SM&i;1O zmO&f00vTTKTi6BDRSCia-DyRlL zeBth8R-~FkZqbRaPBID6_l2HWE8ZK?t~Pm+%uoms&4))ZR;$6tui@?-GCwOu3YdUK zQhl5|6GF{{wFfq>TA@HdfuH)YZ%Ec7dqRJ<;w#51dJ1lcRT{=A(b{lPl{F_|)0(__ zsg2Ev#6(<4Ejqd#2%Thnl5yHZ8)N`s-DySrq?{&e9YX2`;(gW(>NyEe?D>K0a45CG zJWj)+RyV3VTwaO{MIM+Dk~Z2@_fD8mIW>4W7R;d`aRsM6;X)BNud+`hJL*XU99Vpx zP{wS>K5804_kO116G8GpxGdE)!Ya)(Ooxu@uUa&pWbzgd|S=- z!2`{;Zv1v`V`zIZjww9LiBjl;qU^4{T9!~&nFeew$K6I*^VHC0BaY*E4L>e`RJ!Gt z+QxRK^2)#NFJ{e{8u8Ug2^RD1jNgiKLBkj6{6~byv6BS=ImkIW+ky&;v+kSjCh_Bh z0s_#NGpL6O#sAHGXow`BSmu!Va^9>)Su8?U)>HDjv3y-nq7^J?# zXMO9!+}#Zweai4#v{?d&H2k)+9yn0q_aGI9nxdh+1dsiP%JY_}30rP2qq~$m`WlTg z$pdx|ewRS%?KUm}fu>6oBLV)&80R$)h>whMUl`Al0rMH=ICyN@_Zxya%$AGk7^Y3 zZj1xMHW40?4b8@No%0K{W9a0*?Q1vnQ2OMJ>bWylxuv7~<5P0K$q-e~4!ng|mC&pO zT6W{7n8=Ca%o625Pt1|7j?@Nw>cyu}X4GtY{Bb4ETHJAY-ONy=1PzOYEi-a743B>e z*?dIi$I#VB50oUOp(>CH-J-lD#7YL>qpls_g)wu#Gfi2lrHNQAq8EEa&!zeOVnaR} zx9eoIjLk8oE;-+gtMw8HL^icET^Xu^(zgVgb(@& z(df!rip>C}1P|+>oJ4jA)y^UT9qa#AQYH_F<&4&2YICvZ=2>#)5^(0s^n;5tg`yMIz%WyVoJ)@!Q}yn zb+Mk7WrYG!{{0+5LSVDPI?agoUc&moh~dHT-+-pTDBuZ8B?D6S5Y7Ms1L2vz>p{x; z{-hFAMvcVu#xFA9nKc6vFz@2O?L&oBeDn;`hAUoGqKpi(>;|Y~A?Kpw;DHK5Hy)=K<{yUKD{bP3`WbfpNi-F*)Fvk#ddD-Q334fg1;f z=BS7hx@wCt!`4Pv1D}O7|7v0cfJ^4MGN2M=6+H|L7~Q=Io<>9Vg;>Fsb;2N}X12`w zhUT4cR`2=)Clw--OW^1LTla&7E1)}pT2X@Mb{Wu}fE>4g096jU$b70kfp*=Re{rCW z;Xf^Nl7cNSh^$*84S+@_M`w&M#f&T#alp=!8=$Q*RYAn{4$Q-5_)egfaGb7-*HaA9 zTMF2ipp0#?0Q+6K{x7_zcRe%Hy)&jw!+-fIcZeTn`0SbH_-g&K8+NeH zTED(quqx?Nq*~G;RCpk~}2UwQZVfGk4>CKZua}cv|D0#zbdeCMQ z_X46FBGb+DHaH=HGyjO!DPzQb13T2urbEXW5cdb+2%UhDW#fM67!YsTViGale`R_K z*12a<#@Nu{0n6DD9nNe=p@1$=Uyqz(>&tDZZcNqcal=A-S9lQE0Qh9(jSN_|5?rR$ z01?hi;XDZoU~cx9kd*^`k(^5Z=^5rkT4p&L%`S&Z=DcR)SH&ze-;*sW^e7C0JDyzn z==M?_e&;^jWR(>me6|{ZSz)a3K*xaw;$eW@6*m;Y?;+i6!}Q~aWVV~&!lIJ;0~Oqg zd640~J!qLFlM_w_!K>j56fl#u`}|;?LK?T>F{l>tOy_ZqD3jS5F&+N#O>bvMAYGug07rha zsS@x8d76jjKg}w1#->fNR>L|jT@etzMyhCv-(L~Dm=%RZpvDBO4MhmLFav`f5g8ee zG9HLP3V@o+by*|4QuST`=mbIuTTee60CpxY(XM79yo(RJb zL{ufjg^ZM7cHrGXKhQMq!P<>}D?XrVrOJv;|A(YjA#yAPrHr3UGA?V(qAc(mc3TE`v8K=OD#1(h+}F| zi=D5Ysz#M=_V{Rrf4_Q!a>yszf!4P6gqswbHsO>Yyr*@Ul|@`JtIMj1&T4-hw-sEH zh^Ql|#VyL`w*OeMF0O)XvSgH_-4_`y5BS(fO(qZE)v|r*cvfIa$k4Q72s<@>;x`2Nj6bl<2ePE5qc;mOOWok2Y&)(dm|Ct+oa zX41z;ewi3v>N~e<<5sjUR>t+)9a#-D2#hN+TOA=zYW8Z{&IrqrI$?3(f>_~!y+ zDtf;Ypd}EhZ7S`rGcCY|p^3z3O%4wfU&?Ojxb9R+$L4gQW);j?0|`TEHTw9(&p|!$ zh0;eHZlZL~m?$_85^GAHPaoXur6>%f-Qj$-k3R-D!hq09=q2IT@_nZDY6b{G;ycJG z$)s1@q-2}sIEaY{?5Yna6n*$~8?Kh~L;32afMkq(YSIj&OCO9MZ>ZWuP=AMi9*%1T zr4z~G)3eWsmHXF4kI^hba~vb1R*OYuY}#nsbM)h0Z=RKcyYJUwCrAbJ-$Il(t@|)XgDcfF@YSx13^*7bQ{KSs#|`T z{1S+n+J*^ConTwxjdi(S3MCNYso{xz3bDZB3FD+cG@#pC1)7Jld*nJr5V%-Aj98f!D~J z|0aK$(^0&L#?JRfIj9cCKkr=b748!}*`5!<1`z1oQ7$OE(h5Tmn~o>XNTR5Ov3lB= z4>WoxQTJt4%F!i6)xHalV{_J|m-=yXz$Crr$Zx&$(?cPzaTBiNAX6>U&PD}7Tu+d#~xL*{X`Z0 zSRNnGXU{`WZy^1pW*Bx)s=9b9y{PaczKItn_)?3JhoUWfGT7#ossOPo4Sl{il3mCZ->)EYAmzf@!S(GZVs0ewPaGaPvg2605xSa1hy z+3GLHO)F!fhlN@34p_7({-LsmXGrw(Z=R zZdR$&QiA~loy(BI!o7oVm;6jm2`(HH);QnU7_0MY3oQHOG0ntK6PjT(Py9JdSRI7& zF($1R`H^?QAQkIZg8!87vOYIuqxh;r7>KWhCn^}Ge_7TA;{s1`0pi~}^%SV4S{veo zQC~>Y6HJ}tG2Ee4`vBB=Dl@X5%w+qquzE!m!RM3i=*IRSVUZ=&xoan&UH=pROK;c_ zhVI01Q_X;QdUbRSpo>(qSX9|ziJET_qS}&761B;M%z8K;>lwX~d>w@075Yx=$E5Fc z2MlUb&D!uhyT$j>Uf^42Ffl7a-}?ARNQJy8{=v+iP-*{7%$@fNwd(~c%d)ovp?0fX zhiis;^tr1E!~jECHu~D&3JKM6v=PloRT)}<&E3xp9Z_+Kg+_n)reZo?Z3ZNr8eybR zADJfs7HPY%#Jbr>3vceok6OP@Pkb?fTNi-zxRDW?5e+NV_+x;`3?e$-`~lUROvVCV zDMo*zjbtnGLzN&8Vz_SYF{>KeN|Rc0i~`eI%=>O5sV=bjXdp6~$4^vGqxl#OpHUTt zB~%5=PWCfz1G(Hg$t?g-(vuCiL2x^Pm%rSKLE5I!e;Bp?6^Xp&hegFprm^?b~0^S{dnnzLM71`K3&KM1&W=lzb|z49&{1Ex=jJNRRUd#8tc6e{2ue*RwL;uW5Li^W zU2;fN`5NClfkaBIvys3~E&v$uxsD$|CV5QqHPDZII>mL-rY*ZZ;jeAJj#Z9lyNF>M z_*YW^V?Z#baMHxT2%g$A^jUsT(wT^|bnFj4iW1&U)xr?DGC4_^s-hE?pQhydE9Fg-{=V zXi+S@V#PLe<~n)8W4XZ7cvr2GD<57>n45y__MJOhq_Z4Iaf^0)zrHih>A7-94-T-$ z5VNxo!i*qD4dR}j4ZurKb#DTzhGH1g{RHtrBXgKi}60?kY~E{52z zG{CV%Ni*N`D+GeMJ}(SA6ynYXpuPbAYE_~H{#=;pse5qqkWLMzT@xoj?5Fm<|HENr z<;!nf#z(thw4Z8tWp0-HB8c3-zyp-~vS2 zEW8!#*Q=L5_KCeUym+JaHg1Pc%WB+jcqQR`=Al}sso6B}KfP?m6kXYI2IJGCkx3Ju zdxWEY3B!J6pye;`VkKmLJ;3=4Oe=g#*Z$k%e|lmon}l=nQv6_q=9|7^pe}$RomQ)7 zT+Z^j39>r7_J|9NT37-EP3*!;i;+a!yTkGVx#c+^E}L?Ks>m^*R1%}`uqY=S;RQ~k zBenH94J~>&2uCl0ol1n6y9nA)o~fDkN@Ehv;*7?E*ej~;FU;_D3B1MpY0ajM`JOs3 z>1zP(w%tM`V{s)+R{r%~T#fXdetQ=?fBft9|9;z*4KS`{)e1@+edo&R<`msetmfk0 z_CKN8X?E~XhH3_WGs+y*M1L>00#mNM!wG|&y`_#4sHU!=2uW9??-5N*d6@vcL~2`7^=`$N*7X`mW_yX44-%!FIM;qR8d*T| zLwA{R_@L9IiBJ7--5Tl1othpl6Gx3vbY^gJ=l(Bx5o@q*gW19E1br|oe>bRIDCqZf zh>+lgmblE-xtD;4&9pju{(L?tT9Hv+;6g-MxDdjR-}$r~;)e!!`CP^wU>ZUywKvTs zGUhdsYEx%#kK-lFE3q)UW;~Q($7BJgjVUZJ0|;cz`-MDWVoYp@P~7$OgBk2h&y?m! zKje-95At7ewnvRI=GHaKb?*GF65w6Cu-hyrBM{<5%B?=2+H zcU%|@3PBSmAL6+=6IUTcdg?!}0x&qX>C*ovxhzVNgcDs3iE=178{-q(v z1v^p_+6#mc^2`Eu&uwvHeEG(Wsp?+KaFe4_XoRr33e?C@{Tw$ICrtnv@X zkeH4L_@*M`;In|Dq{-@V-UM1tnm!Wb(Ww;o8M`;YMUd$(FRY!=IpmQYv%-k&_usE# z@V!9hWX8(?aV20d{FpzG4Rs!P9(C~h0zQ6Sw;@ux{%(UrT`4V=xCG=X)GmA<)P2EEsp~RsjBb_=uGV8oA}7~9A7^& zZGvH&IrDg01d$8DIBB0*$qQ&GE9TckVA?mJYGa*!jz0GVc`l-pQsW}a7+o|y>|VcQ zaEU|q`{OBg&DI}*527pI`w{k^PjMp6_?aEEP@KL9b=cuCKSb*Na|n5aKn`3Rk<(WI z!xp9x3Y(H!_dfb)?q4Ieh9rni7&!d41PsY{Y+vIZ!L$=cRFS-8O|7u$v8pB{tpUD` zSF!4(V45jN8<2%BF6hPT=sY;AEd}|^*12#~j|ZFZ`Pi=BXyn^ z$wY{^7abBUBd>pErqpUw<9;nzHtoe)(vq0Wj8J}5mZ%?9Eg2DCz>8fACQi}M5yTzJF^Nn+ z7r=iDH0n_Mk}_3cM(9We_#(4avg~}6PA^9B#~I@?$^IH3L_(#P8KT1Zsn87Bj<|fYR>8~5i!-X+ zRO=SZMZ?Qt_4Il+x3yH$5GT)ZISu9^24b!ORQ)V84aGTaGABwhcHHi1iGl<1^!cl| zWMSTV@(}u%0rpEk2zT#RH^cV{{H|#*y>bM#W&2FcWFW$FT3Xkex931|^Q_Ds4ploT z4lmpZ_2X{O$+ez8m1g=C10UMhcBZ%%cZ(|~c}fzaIW-H_ zCOw>u>aCLtUp7=@-q#kVNFQD$toCrawdX2t!IuRb#Udxmjv*QpKL1;CtRVAqyNEO-w>`Z)`bazb#>#~g$(;px z!g^sTAaVs>Zu=!aRs8PBUZVI!{u`p%zYHwoQ+Q|5as4dOpumzUyT+xz3( z`I-6s`gvv}M0Cp22z<6~aI`uTrodj@>L2yT|FD|7o=&l!_T4&90lK2ooTd+&q%~)R&zVJmm$5FkFz(nH1%`W@o=xeYpNB3ECE-aK-oR3K!Xwm zFTl>uI4u+GIo7F83_wfj*hLj92ee-IQ#7-LeVE0-V%vYv>R-d zMH)@ezz|iUDAS_VxCIQ6hL0Z?&M^S=t6m;yw0S<@3}HQ)?EV4H!6CwNM$Ur>LX2{t z#xn$up-r6g#~({c>_LaS=+|wIxHjs-ug{)4mj_E-MbDfoAg^rnyfj7l=wJWUK4i>g zvzFIb$R>bjp%CR1-D|Hs;%&Kq@G$T5)T~SxAcpZir#J}t;fsVR8&~96#R5Hy92&1H z#$oN7%Q*TMFcT-GT}^?YGnOWbXs~~KvCW{Et0acS!I#*tB@KqKSk}7x&#hZ+V;4wO zlj0qa@X?JGuv?25ldZ+9C{1?>Zr$b|bMTf}AZc(%+7@^Iwy)4$eR*dXg-6bnBuO>bLY(qA95rb6h}{JRNY*L1e$n8RwQQnyn&W!{(ryDM6kS> zM;6eL-bZf+kH$n>%E~YbE_`UKpEerwfRh!;!lbeG%aZ@0)9Bm|bm~L%nH6}MSs?T5 zpE)ldG1(9;b?lUr~=7fxkJ+%|N&A#NF1 z^Hc+z=LZ30&47aa^{YU4dopEjtwvTw;V-Vm2&dtnozw(hDNQ24LIYI->!UY_j4GOg zq)|oVl$LSswk`ILkr_y;jTuqWFmz)zJHmo#9}Cy?;Swvty560|gtXNrP6&J4NpMJH zs%5rg!*$~tiE{mzp>3rbS5$PjvILOdCL3Au2<%9s==RG?0-`^QZ)f;Tq z*g#F*;5e_TV;d9kI@8uX^70=J);4h-Jl)M-cP(FBa3m^7pUtNt zS+yHf!sK#0z6|ZXf~b;zW|nlsa8F1`NH=uWX*OnA*jLH4^?}1ENa&wc!F*s&c;b;(5n_mzyVp|LNW(-47Jl!LA-oaG#4(=aNKS$dJQnB>r1*) zFELEjsR_ED z-n~|kF6E9Mz2E-e+O=yKSSwX_(O{dEhUjmqAb^JNz?831+5Gi=**JXNfq1mia$s46 zZBGTxLgRUb-dfP5_q3qhOiGW?@<*u_)_fhq98-~DuzFZ)^|96HRQ-`o#!ST&1!OOpYdxJ-(e`S5VgjS(ybcFf}jDbW76rulyRY&`r z`VnwHDPh`iA)eC|yIFDv)>c_*S)k`QgzhRW4^`2si$7-Jq7Q&R%QKc@ioC+FnQXMM z*+?=v9HrE6iK^K+Hc@W9xXfj`$5Hp*@?8YY6L=JBLJU_4>w)-EvT2)p~1B-4R~4OqSeJ_$vVYe2$5=s7WKCb3yJW z1Vf_RtrCRq0eP%po1A|>UcK^@ADgNOTX4HkczA@J#gw~w*Z%#13cWyVm6`i!L03d* z42f2Nw7ofa6K+(+x*riR)w#|sC|)%j>Y4qw4t z7zNu@((VId(R_4(>}nac1gOprPoxaUl?mcm9C(b-;OLVj<1LhY#n>t3!}8 zm2*b?iY+~xg@rq%m(x4|FN+b+m=ri$wzz^$MNv6yEzuWE*2Ldy5!d&vQX9cZTXHQ zc5cSeMxL6lHV4mTWD-F$Uua>p4WE=sI`K?s?Y!1Y97+;K`CZM)K(%r%2d|G|K7ku{ zs*+z8B#_nnY^JfXT>)8PfvOVj^evHY%+xC{r@bUC*XQymzfCaN=VNci zkj{2*+ZMAwQvgT!GGc^0f@Aac5_IY(;Qd8rHiy?QS$Omegie@i5aRq5`@%0T62%!j zuDB0Tr~v1)erWN;|MjpRz{Pdn>y46M$50`aw8%hP-xp6O4~r%+ukoKVjQwJ|ZXF~& z#u#2rA794R;A4J4tqMRs*o&)Ed5mmTbd)`t#Faww`m~hGz z!7wDvTTt^=^m1= zmblYFaen?6MT;&epa17)|2|S;Qf%Ic2!V?qKjXs5gtAO z;N8H!gds z0kil779o&NMg{(X_GGERFmp06h&9aa7rx`c>0F0M+=Dr3Gt`-v`o`v3<*hTObQ5L zt0RwQz)}9knvtT}!_n^vl2K>cAtQi@k#j$-{5cdY zxYfq;FI8;_u*DK)mU?}K+dm@~!_|du1k8rzXnugz;n8I>5?CM#utc% zBzA3|36tmS$Nw1mS8730iW%4nNN23B*`CBWj+E6*03&yo$0M9<`F`F&Dr>Dmd(simD9cH9~McbXb z#6?1Q7}h&(h4_t_p6T8{AR9NO6{toT6EJV>xqr-GVy%$Zc`#AC?CQ#3n>A~~j7aLP zY8QwTk!hD!rkhj-n(o}Z^ft|gTy1DMmugUuG8D-mjXw|)XG#Se8#0B1hg9!dJ% zlCtyX%=HBOU4P)o2+ZR}d|oGZ|L24#WJSAt(F5BSjr5?;@WxkYBX1%ym(|Pw*NC_d zK3Z9`*|Y&_>k4hm2kG<6h6VdQCYUqSq!5+n`z$>sAKNS-Nk-l~7gO%&4pvuWnoAM< zCVu)dUq}3|;;&JivVP6ShJ~$U&3F+$U(+zmJ!p^FeyTMV6GfTop%-pIjyjin^bP_( zF#=ik&|x1j72@H69kq97=h3Z88br}t(~hezuY2E-Ysin)3S|NBr~BBnv1`q&e;snP zb#F6spE>dGi3hE90~PEQ@<0J7M3R02Ar^p6lz>j*brxEDvW~t#Q*&;D6SyFBRaQMw zyLRn~lSd(Af++DLVMQ|=Bf14r$M0b|gQRYKU4qFqgx-DN^|6Mzz=S$x7l8vTUHDU+ zcE@;{>Fx`B@d~2h)|OX!c~^1%<-pb_J~-If(*=hKW-E_*_wK+T^cvHnl2;(^D08SG z6)IlhMfuPyD-Q?2x3itm40F|%eP~@RRQg{oSWY&Posx@nhKO2$z1+cax9V0aR z#Dkq#c?PZpcT>5rSC3!gBP@zE^smmJO*%gW01U+g7`MdYnc z;eS63Kk+Yhol)%#?}~$5#fav#81AbaU?QV z(;p7C5kq_X(5)bt;h6<}oYB-D)A=1CeJ4yxeTQPgU&v5oYjXht6pC1ZJSA9lpkGPX z?F09G1$K8a1IS}+vi6->&8xt&3x>#}P=8j&9OBnOxck4p@ zw2a1I*gf$a`#Aa0hlwP0;0r5WtXvgne`abY`;T*H)Y?1vjH%&>7DAN8IvzjrMeD0O zedbIfgg>nO0C0qOvOpt}LjVuEj+eIb{rmSxYtcUO!IwOzuk>25Ax3@R-Q6B|i>gd4 zXKLNk9z|TY%BCObFM(w)Q10_`>1BE3_w&9*@v;;g9DQ$M#OPYd(aoDWnNx{q>gAhFRGlCipKKzE>MA34NH;4 zyi)4Nzz75TX`x74LoY}Jv3R3B^Z5x1yyAcN1AIi!T6cSP&Ni!rH zZKzPw^s!7q&mUKhw{i`PuBq>-!Y~27jW1xYL(sE?tp%d2JIQWdpze{5alLxm3pLiGqAt zcC&R`eg#q~N)_?-?T8?{}emVsf3Q!d^-p=fi%D2BzVR;3}M8 zTVWARI2&W{@lsHluachwn5n{4)j@=n4+r(TK>Xp01Zxn>MjTmE^sP@fa%4|Ki(RpEnlC6r{$@|s9BzUBVSDk-wl{#~5t8Xh|Bi`Q9| z$FGxgPdC${ye}D9q`D4{xB$_lggd}{WIj#8)TAa3Uhcc#jj;kcDER5n2Bg>0rgIY@ z^wh=|=4c~%da%j)H~yc!AJ$+!e;Z_SR+SxQPGC0q74+zH5X?!ql9O9 z$)ZLiU)}bZm|Q(H8XW3P{XbqJBk&-)pwOhH`$~K-Uk>KKC=WUy!ki+r3b|lS*Z(Aa zu?P~3u08dHUI6}%6zRv{r#seVR6*q#MPA~Lm2tg6yvK7XA(RIW%2$2QP=eS41 zgwnX=4D$cLLe(_sucHnc(MWzYwI4Mwg{=^Cavp3R!j`O6m`JTlIH{xxqaBnNEUz3y z<{B;$l-z~gy)DPSQ|K7AW}rd3O}($+2?BqI z=ZxajFZ9-`#R!t4u@_S%O;j3RtpnITHNmCzr|tfOA}Jp={t>S7FzjD- z>eML<$FWSjB-G6_j+A2mCujh7pGsP$HgXPvk~cl7^jf4d=Sh6ciD2^mG(4}&#@%69 z`uX~<6tA&Sixk~M7YHs^k^B|JtO1RX=`)V{!r$iU&6euqSk`>-r&wt(L8OsG=#v)` z1w6~l#LFZ2X6_}|;eWx5REy~vs-N~n@O+&Yf%XKfin!U{c=9J?Ms zT!igcwst9uw|`Ny9$M^j3?A_4nMHezYTL}|d?%RGUcDLzfU1pn-n~&hcThk?4$Xv5 z$HuQkalF&$(A)34S65L9RG)EEqYK`Wku!!)MT?E#9HZ_7;&Fe#QSUNLPLBG^@h)Au zh0-YyFx7n);2utw8lxk8ap%L#pXof{elWhQf&VJp)7cF%n|;}S5=M+Ft(vMOU2wPA z64wEUNGvzh=gt&inUT&jKtkE2>W-G}+42|BHr161HQfg0J5}H1F&Ak)K=O)L~ zr%$igut9@9C}8$$C<|apV5?n-G}R^@)ySf)fm*3}Wh`l|an+^by*a54HD))4rMrMP zLBTq1H&@vW<`>JjByBF^ zjUY+>MPe)z^X00Zk2HZdF1b$>g5rZ!nAki|M0>ooOJ4TwvR|^A<6J-Qtk|Sov>J{vXU8hnBkM)A@P2IWx5X z(aUdHvV2ZLM%S}NZfhY}HnPjcAVy%$)yfE!C-3uJDLiqoSiyJSHVJ z9mS8k&u1({0Or350?D*$a7+b(TiWnj#8bCy@e<9Ijh*rD+E|WIP4x?%F5IWIbpF-Q zx14DmlTm)-Tr>}lUGGMfODsTSl%M`hanLH{AuaI2^#=pu#lBRslHvQu?nOXh?(1%* zP(v&Zlz-1r`ArwBtR8`J5Gz~nl?o-;shHk^4yz6rp1OKc==m-AJ;f>=)H_3L;7QXL zfL#h6MHGuBr)vf-L?nYuJ;61)18D^^@jlyBLJ%NDdv1s3?Qrd+PR85dUw@_=GQL=e z$+}~h56775rU0=c8Xv~C>or(=zdO|V?vdN*OpIyyMK-%(C=W)jx3Eq{UkCU`v;j)6 z4>(4p&o*ubL+|^;SN$T{G{gx%61@0w-9U8Azx*;b*u{dc@w0=Q;k`<-*tAPIAj!5$ z&uzrB$F+8I4nvSx3y)-4evQgvCutf7v0UL0bQZL4kh3})k^EfNfZ1PQP|&clrhXI5 z=i%&AOxkvCu63fe8OIyjS=714QQv%R!R(h!)IbN^ z(Blq5p1`|o*`v^ac-(M$DzxRRT?<`aC@aQgv_;Rt`MxBIS>{bB787E+;~@=!fdOGu zE$PNJX_#zp*iQvA7{oB<<(cqYAD3Z;xuyr)NMbFO2M4!TtC!$CKeQqtw%oK5h>XSKbijdUc{$7oGB&VG1g(!jd-q1ZP@ z&tom|uaigMYL(U^1?d6;W@Jn(=;LWu`$G;v38QK|l8s^AM2=yIK6Rss(QZT7Ou>L* zE%+zIG?5}+G7hBDP};NcU`YB;$}H%&YaLX{ea0R{m#C2jLU z(;&ZVgoQ=1&6D>-#|t<2KZg?>{+Jc5D+f0$hx@+V*9JUFR++VxA2~<-MhD=D|;3K-}11?!SyJj3moQoEoEMeV9lZY&l|eXDYY-mhw24w#3S}gm$_reXSvj z0p_zW!duMHIea*btih@GBPKfuXxGBH;D(G#7H-HavLW-k_TcR&m=b9uK6J?CvP)^( z;e_jnYAwR;7zF+2%G05c&e=N4G#Q8zZ4%RQDQ-wZ_Q^y019Q!!r`NAty?O|u+CBL_ z{|Ox8HLfs50hBmA?X`9?=+Yd5j<_;z&Iv1OEAo+VF!QJywJk%5q8+D)z)i&PFZ17T z{)({^k3PfMiC_ZZwdzbwGw^4%@LtmX&1)W%THrRaHrvdR_{g&`yoJ^%yLB%nev4J7BTMt2t&+-~|?yva@$2eV$)eHKYy6AcWnS?||Dv1w= z$Us8))$LhRBqmttz&X#A>wGz77p;Q_WC-6df6I(T%VcEqRw@MwfepgaH;1`0tK<5A zL3-~5H0LDHE~8Mf#+HCgJ=HNpVUA%+G$P5p4^K_*hPI_Di72#n?kkNW_r{0S0Q%+_ z%^5}{ThM)k)j8vD)Xu(VfN)W$rSInif?mhNgLYIkfuf*)o-H!vggqRGA*gDJucdK~ z0BltE6(S@0w-Cjqtp}_yABxIjHL!gL$hS)WIy#Y$4wpcPS!IAO&_93m)?&Z2SPH%q zfktygf(asxPGIhX+zI~_nt=BEkMdA&Bou6*WSFsXfwNmk-r`p`Vi3)?qMm|UauV~T8eFstOy@I2d zdS<6yNd>O_APm~3yBFXZRmg$(qMn15MZA>TKp|CTR{bYyMKkqka41ML=kD?E1Z_Y= zLnAK5Vq|+Swt?j~CS~=#IpXF3q$7I3v3QCwYUM*08~0^8FwsA<2i?FL;C!pc;;X-F zxjq!s=VMC&dt4zHN!zx8Jrl0CQgo+D+HyMzG ziSHbmh3XSZ{^qh>DzCsc{qSWS;e4>blS6TG7k8T0JkaO{=#&D|0L6XU_>qrcxOz@7 zt|;bNw1Jg?p9zQoj3PquAep<2Gxh?Nph9ErdpL_Pr-c?+pP)qlH_$%%$*JRG!;KL9 zTm`hhukrAqL(jnsxCTSlyoc~9r zGr(I`kljfbqwIEnvV_D|y_=_PZYoElTy^88i?|VBca*7$7&>Z*joXtX!1`k5RKdi4 zYYQ2{HlH<045*rQdkG}Jwk5Z|1xB*?Qses9L0}AhQY?aLGCxQHNrg2IEz#ZR-C4Dy z7}=hQ?T-Q)xmX}$I_xMXcY$UTp-JQ$qtb+mf!(FjABH02Xy7(aV5~O6{W}-n$&)9I zxVR!YY;_L-LVOj$)Naapd;P*~0GxC~tLwlEGIxpB5+aB0G+n$xcEzlE(~^mo19 z9pB&X%t^p`5qGl=bndH~(;tMaLSn-5WBLe4Hd66Z_w?5 zWL~>``448?TQK>KTmByP=sy}Y@RRKW6*iFqN&I&Vnlw z3n1OrO2Z05fA}@98Vot!jAf!60mc-;%0e^#M%u3FmYv&$h_BPT?b8v}yfNuBwTKO; zH7*=&lE{Z`mSl1Qjdpb%K@d>D9XCiAUo9X@+)k|TdXtxmL&4Q9b)-V5pfCo$nOOE` zyD&Sm00|s*U_N0}1y*lC3OE-to5=a9F!n3&o%-vqFMznV`ZBRRt~fF9Y7$9%!NKQf z#CmJjVvp8rv@7ajJ1{#f2>!$+2+MZNPbEA<5yr!EKUktT2yGre18=+Zukh-EiPqxX zF(kfwrm~6XT*)=bcRpksm>$4204Ubl1o8)BvHIh3xv#tfq&Y%;1_o-4sXYtV@Po_vMeHWxRH>=TChuFbX z8E-^6ogRQ8-7d@1zUayYrTZ}jF=mOwg0Rc|B@R>0Nv0$u7FTpLV980AJ&L`$nqLfK zc5KDS2b(Uu z3xdWy>{UV;0Cv?;;>Ag)oDs6p}pz!TVaq`uLP67xj<4Of-i^peyDjD@i5m4kSPyJ6eB@C(6VVQidm3{$8@M& z)XT8ZVCP==FQez&O1hmvv=xSk+198WSM|mFZTdv|U|JagP>y|k7JheiUtL@Df{ZUE z3T_V!3=9lW7^Bl(Njr(aG0ew=L=dgPP5sQ}K&r@Bad>{sCtBA7%KfTzTz^^^Oo0Nc zWAn+mxOKr(B50s7b@#EWw;OysO_U^Fgo`79l2r&2oB#$8e&lG|eyJXlD|wY#^vveD z_wgE%MsXew4|fne1aiz)uk+fqlCTy?8kg>RI(JWpVnw7{Zt(M4i(rddYLA8!lKTO7 z!fJ-bx3r}SBb*%Nnt&cU`Ds5Bx?ib%$}<2|fNA*F+-idB=>dH{ty-P0(ShHlUcLxk z+9)u1x(lByBOxZ}uy1O_K{ch3t-DG?kVhBjPDy|L^;d)&e6+*Jy2Gz|@p|Jw#EnnQ z`FjfFE^|o~rQq>OY3!YBRuSdHU`OAw1^6;~@z!87P|$5Y*?gRM_Le6g4HPl$ke}IY zl>#ze<29ZgH@;P2(xmYfrVAvF20KA)Xbo1SV(!CRZqo5$02}p(sbF?< zCO2jAZLC8WT|iQ*s8T__fmskbS=WO zee=gBh?n;br+zMrotd95F!r~BX z4U)#Gjt;*lDLvA;wR#MS@CT!IiEP^B<%2~Xdn#wbq1R%8xW!Ej(F^X((VAy#P*Q^L z1Cgm}oHOz~q+w4!pA3D0aUiT9G`%S!=ntb*ebLts&xkjff!W2FKd>8~graX|KNH(l zaOPFKQ?b<`+`I);7V~ic+fp@kH|XJjwsA->9$X#X_8>n$KA>GmZIA@%nFZahGen%B znVHZ!G+q|zZ5y^{+rd(o;c?*5IF^6-mpWcX+!0@8O=tq_biek2Xq5Z(#7W+p&*b44WE#HIG9a$S8514uMB;LdM+WtPnwhb-iCwT zbpz?>_(#+(+50+6M}>9EIn z+1GMQJf_k6P)l7fqPJvj(JxeYO}o)B#2`6a*Of+IhAyrd~N8 zPa{X=VaTN*G!Al33Fxx=$DZQ}w6%92dOUvoSbtXmMxG{$h7axjZ=z;jKcM>ML@zX1Aw;9?iP+fzc%ZK{;>g?q^a2Bl^chF3bF_BzsYq!=|@JCrE4u0SK>h|Zr=$2 zcE@Vg_(#+Pm~52xoFfGA;ip^2`pmunnotnqcDd~16FT4$f{Nq;#E@T^g!6u!3o$%O z;Y!UEug4^_K52ewX(@;JS%o)b3iYIUv-3~_XmC+1J9K}0@ zvdpeu!Jsib_FJt!4FTG^W93GtR4Qups<%)h$`Vd>c#N&>bY(A0+ z`*6B{!Z3<8HE)uQt}@sq~M2?QWoMMnp>b?leV;EXq|<^Cav|9E4rNv;)%K8A}oG? zHf1MBd78a9kO6k#5t?BYLjb!=_C#i)Mk(A76-80sf;kC{)d4D_{X~p;#equC_`nz5 zc}-10qV+cKLIG>zPZI@m^)Qrg2q!zCBf5C;-$UL#yD?J%W5831q8=iE6?y_mg`w* zgP-jOF+sW^t#wP%!&HRb1uUY*9q4}^AYLwjR|$lCk3Q$Wg1RK02))NP??_v7A_wcz zN%OIXx)sKg4s5tHWO?xYcy6up&YjHYSxbVx{I;x2`Im?T}ke{nQg^6saQ#da3@SJv?lBvq^$Dgy9OXs^S`|GcS6SXq! z`X{Sy9A9~C<+>YAGDq4Hy{n8mjDk+xSU21{dQ+&YrzhTVUxI$l?_771F+@KZzd!*= zxNZNErFO(KMQClGi2*?H!>+!%CTAz361n zWQO&bj#-QtRb?4HuoWy89t-*NYy*tOtQ(%sZq!AS@TBIT+l3yy_A$S^WX;8VL-g%w zw-Y^)a5FA0XVd;>d>596;>vos&pdnxuRM#LUv=BX_b9~^v}I6V?|16j-qqhiM*yp&Lunt30>I(rrh%rSZni%j=b`&hwz(n|t{gor6;!GP|pr6#DKirypDYY(_vY z@NMAXPi=MsU5Z{Qbz-SF&Fp?5*F9QnxEEt4?2W79a>ssNSQX@1ropXR$LtR9sW8cS zqrxPI80%}nyx}lvLj!f*1M59auNET5pai*H>fHDY{hN=i4u-h*9e2O#e+TTh;EbGb z={nhbnC7;^;}z#V@~WUbjjW*6Cmt^yY-1Z)fv?Cqy_niNIgR2 zG{Jb)GqB)1Xr-$#)@D5CM+1w#Y#Cs-4Se^ZYP@F>a!T-!p&|LUtrzyP2Lxo>CN+F} zst(cp)B27SqP{Ze=ie}k91$CkT17m=0dpQ6<>Xg=8tVMZAlo$P*i#?%Pi1KGUkIWs zqRJUyRJUzESQCa+5N)}(8iGy&D9u+aqwOm>|Zr_D1d3OvsYw;Hhl({hkx{AQN3;*3fcm%8&CKP0t*e$=bi3cJ;ao9A^6^WZC$ZIdkI#07 zsVZb*@;bQR$$Q5^t0s65xa}p_eQ$ws;!wZt=Uc(_82~D*1ZT}}1IU<2g9TR~Hwe92 zX-qcACAH|Z+teLr9dZ#(Tp7tLJIyuYZc}#M%~`JZ)rm!^){wLh_L?8ByJI1IR_WpSl5ty; z9ChKsa^npi(G^SrwW9+7%YrzRASvS=uO6+<#E{K6PFQ%_7`j;VuGgUCJZ_%rC|r<_ z;R#P=Q|{iXT!awC*|Uig4uFy0O4?pH9j)RnVBT$Baex;dU>%NP*L=?%1mJhGnKwB$zd*p@zd$RN!6>iOBU$)=|C2(WWd zr_~#K!A5%d#Q_Y$mv8J0Py0>n=t(+wSQQH0+4C)&MzI83J9jH-X=0DS3ZmE z<94n;YEzrVg$3MZ{*DA5Ri*MHtT*!Sl&if`dA zIwCvetg12;4caNg!)||uu-{bu1N&$5*Vp1WUMMzRpLrZA#YU%;W84jsHqT8sR9ALz z_5220+C=SSbc{2{Md(25x22G%VWnKY-5%?7JxH;4FJyNU^>DP}t?zS`=;(@mbElQQwsKsG{7R%`>EIai-N?Frt>IraxO{($dI z^IJ0B`--UyDk4hauu*rOGao#wyC}d)uoQ|;AK6@?!sEmAU|qS@23gv~<}IC#mF?kg z9D{>b8iy#a8cdd`2j8r`EEil5+o3OUW@uD449BX4&+Jd6H|g{|-sm2^T0WX-5?A74 zc6RpXbN8gkO}KQM3*bxd0_Lj{hch9#T##ji=ww&mBb|U{kXW5ADU^^bQa8m+#fF29 zzD4rv@+UE^Hh$mUA^7`Q)uV37aQVEywiYbn8|9JM(Dm6?Mn=ZZyr@XNaEdSjlja)- zfTKbcB<}NZ0JV_Xl&Un_D0&1KFVO6nd}jEe?NCidnmpWQ3nvEcWD8+bernAdOv~ix zf1vrK)gOYnfZGd%;uMe@4?)O89$=J+snWx7SwSm4Rbw$jIUzZ(drO}^KuaIbIkVjG zt+84@-{vK*n~$f&Z%@WKuZZX*y|jtJ66Hf(yOI@rh2j>{dUkz{Y=nev)K__sTTkq9 zL@B@@z|Za2x`to%?uF0ena<0O$PU9P&VH%_+iN_x#`gO!jdvCKjcUguh;(DFm=7Px z1_V~y{KUnZ>d@=@WJ}c^&unfVWK2A^aDdw07(J=oM5F7AfxM{*CON9lu+d#U%J~G! z@7cXWG03}NGUHgP47uCR>F34^R93E7ah(gB>hpo!MSK~DyrIEC2zUqcjrC!#wc;dz z2pvaFdt@xV3(N|^T)6gaR5Ebcf|6-tT_r;G^>Xak_5~RzbUy|nhLCx5<8$sc*Xh8Y zXj-*(>sCXocX}prWI6$iq7O=E$KXTomN2= zWC0piqH-m$%o7@8Cm_?xMglb7#)g# z!SIq(RHS9LsnO%Wv*aPvuGw`GmmA&qC5zGhP|AI{<|XWdqtILtD+WZi1YD2;aSbp* z8@jJsmpEjtV$U1@G5UOHBjYZQeqaWNZ3(0beFXww%^#n~i`PvQUnqKSyfKBsBgX*^ zl6X04>}x3Qt4v{rQi~deb-nrg*|U3VlpUd`HWUH{IR1^rtDNDaj}lbDg8$)PecY#Y z(R}Vr?H=ep_HEaIrhs~uPSm`D*HZ~TPMjJQ#+xNvgpgULpXoND9 zEEqVecjJv<_(1+LK(($QLVZ|q$pw+)-G$6LiugY?N{{l$`oN=0*uIF zrk(qorVgZ()~|p-eQLkliU#fMpCO;rHkAW+@sR+LYU_C~z4*hP)=o$v>d2>%ePlf`!N>UZE$7XxX4GUU*bw8>EejE_w;zjX)>{rl5cG_8mKD z=~3&m%g*~>qr8PbMcB5TUN@&+87~Os?YINWgJ?w>kW9U$@%cDUR3_ZC?~Dd%PNAz*BST!Amrc z3q*qx2w+St6Vv#M#l{H>o}euANkVut7q`f4e0xC`JDmoBgGErO7*gyt1;x4FM73=?0yu8wYTDY~oU^=X*G~Z~7 zxGi6YipVa-6J@=Ui2+!DLsL!9bF*oaWGg`h&NlA7X?7hJtUF>M2BxO0t1qA8)o*N) zo+qRgY~q3KPQIV#8v*`_cyWTBmSQetPqf>gQ*N@|kt_N$1nrMS?HC=0hmNCQNBOUO`Q-6qQUj&+!jfoA zQ!N`UA!krZG~b=#RW!8v7%&ywZ7Y*a6%D!({+SYK_Vl-5=4_76VepnD0Msa3*izjs z^K^I5eb}Q(+N+YLn?jIVi`$%j_s2$&s~!u^G2b%&O+* zOZ04e_ckwe_6fw=4K3LqjpU7#L?*puLc#TQM}S&*Yu^=L->{7H@WN2pucV~pH-}gL z`hFMSPGaV!fe58lx?aiV_5~M*n;7qI3^*mlfZ?Rk@yNJ_{krA@y?Nxk4vT!#IHbMp zQSr1rqn9jsx?XIIY5K z)U{1)!I8wdr_pf09K(U(;e7PO!rVrpbo!yXRkR%$838GLgQfv>t!sS|v>REs-p+@f zvZNo`j?1DRf*|=t@Bw?pJk4$-5fE$$ub4U9+%XBn*K)T7P1uYzmy0Vpaug8qE|Y2; z4J^u4c|$bEFbNoqm`d)rG)1^-{1$${0`R{Y1B|D|dHayT1!L!VW(1Ckqa54q?&WJlUGvG+|ck^fP7*tMi+YS zpmsFuBxah?sC~!>sb|LI&4p*~nDHhqlOVPLtv4ct7jK1C-x8ckQS76LDk|$CrS7 z44eXb^A*FkU;<7=puBANa9XI9Xa@av}BkMyJl+QA`h zt9e5OuDs92C>&9_9Xoc!ykVi{vlf9(N6fK=q{a=h3ixB>`bh-f#p7ThX>VTXc=a06 za8+A9>RDBUg-~bQ`i9*DWJ&Sb(*VVK-WqhnD9Q72t9Lg7H{m3zj=i?OfA15kxgWBL(4bb=yybp^IEXhrW!h&D8r076f7?2qv>qB)+c26r}3 zFus^PmBHO-5QmTWFO9`kTi~qC6&=7M#CmlwbcD0j|JUAkhDCLCZI3li5=)|p#vT*| zDT)G$qGH4Xhy@e{6$uuKH0h|oL=(k=B2|h&1V&Lx5D+N>Myb-IN+;5!BUM1)z1Pez zlBc}S_5J;>_xunCX3m_m&)$2jb+3Ef>$dTTW;gIuy`+P+Z9?L@q#~()Y6W)=oIniV zrZTlE-ZDyK+pwC!GBII``FQly1*}LNB4QTT;THX(bc;Yl^D7|f&CIrc>jUCy-hAZ2)!>{tL$Qg9gmr0z=sQjf*#X}rwLxX-=FzbY!XZb^u>Sfi> z-t~w8ruf_SqxKEh(;$4p}IZ4qIJ3UVU3RX>&y5xh$aTZyDy((uqw7Tim- z;(geC*M0S&u5JZzrL|9*^pW-46^Ibns|A@V@6n4$_o!Etx&)w{UX8l8ygrCZ2g)VE z`7gV*4Wu4qCWWw?)oTy*`H77di@6I=&A{aZf%G zzDTGK(c2EBq1}JoH2umjr)7>zYufAA!XLrc$nzeKL(a#n4Q+SNTxh4Fh>? zjji!fr36EpX^a%}(=`WP;|+}_hT zReTWWJR@cE2-x&lO~c8feQO?;Q)yN~HLx%}^>l0&j*f6g{xrNTjwB^uHKV=-;)LkK zV?BHRl{xT$({O-D!;CxxGD0+2;K6&K>Ot}P9Kr0{`j{7f##;7`Ub=3h4#)!?kF~_+ z5*&uPd}7)3v->f1;>2NOVu#m8Vq?~lZ6ppN(KX>2IyzP+JDkKM2fbD#tH!C#JoJuS zw_{snT%pe751?pxw-Bpde?tP?Z{hPZ(94!Y!SK`-6o9szfrs0T#uODgh}^eROhPJu zaNR|WHoF8gAU2>1im3{h(_I+jYs#PCAt>0G0ZeBjEI7PLfSdW;DeZ-8YurtRY=Jk9U20 z^NCesZ*R|fMkT)?W5kgH3>$<^_UwMim_xn$^371@OnDvL+-@Qs8GedUue={{2H8TH zRw@>bofb%nW-&f?b5+6l-|-O*w|47$&Wu12#Z?59B0f`3f?!y94&KH35nEKGz);0W z7K{b%zDzNkaOSiX0s{D5YR7pT>{>6n20U_A?50>vcj0>xo0KZEsO&;zzh!QF-#OR^ zvPD!?3%-C0y$Q@8f`ci*ZY~OOrJ`;A4GE23X=rF5df;y^3G*SNgd$cd|0cMYnu1Va z)~pz7;7LR`E?J5F+vXv)9~iu3-M?Lt-B280bxd}P zjm~8c)?G%i4OiTfsapQ!x)k@nCQ z02j($cXG9A3KpBdC#Xl-jNie*A*vKXa%glrx`*8z6AS|$U2rMM&d#m@<@k}qS|G;C zZ+96fX915U7V4&|vpPBt+{82rLOS~-&?EDsRsN?P z{EvHOb6n$d6f>OP9vg_nmYMOO(G!5#P_ABW|DlaqQ4oTj z*|dSE9*i7H((3RE)vJ zIRmo4RZSEqD1-vhMz1A|+FZ<5-66x-Amu=gZF6xW-Wj~-u0WRcuH2kge)p(-;P2O# zsR{YEO_X~hx1HfK;diDDV}ej$fKT)N|fr;3xM zbW!Nup6NxrV|XfTC#f_Qq)Hb68fYuW2DsAUYYPZaYhadaCuvP1JrMfL)n}?WyeSW; zUQzLD&MDk=imTwtm3y#OERrSe!M)s3Dg-!|4RC$6?9y^-2LTR+8m7gMz!h9E7qXxw z`y-$%7l2#W%B2^~P))Qs?|EX;R%cWjP{mKlPqvu)rf`nj6J9@wL=Uf;4}KLrS&run;*qMJw2) zv(&mtXEDl3N%4u0D=gP6a8hdaJ@ore(;ncSn~2E++f@gMZk} z8`y;`KMidKXC0WRVVX(UQKfvsHp%^db)F)!xUIW$O;P&>CS@?-aPM?!%a%h=>}p^c z$qK}U0rGi+T|p}#{KzDRDG4yJ8%u$sL~K$lS2xOC0nDjf zBH>HTG>B46OhI~RUqgNo0N5U=6r6#FMKtuH{YpwMaM82Sr&1Kqu?xs>8ioUK6sY%} z5^xdHjG*pARB)9V4~)PSM5;XipWr3$B!nBO0=PbNsZo4Ap>pYM)h~{X_V|yHFc#T& z{)h_m6$qXvB}!67halbth7Fw@@{XlqF1D74+rrfl7n&D!7&t`u@PcKx%;ATkP((kn2Dy*8pp%#}^(NIxNts{J zqjXdUN~v6)k5qJN{YtF!(jpUBC@K($wKs1;dW{9wQh@Y|ByPSs2(oeP!gJX6Dwm*8 z6fPx`IN;3#rCC4rleO9ZL~<7CJP(wx@f4cR zoeYpkG#gut)*AkKkCE>aZ90VxQD-fw&EN6P_#u&G%@6JYg4oCu%Tav1;$|OFiyGvJ zu7-L9a+J)_T#&pUBJ9Gmr;T3egz{`k4qt)psU`-8RRl%%yCRC!_&S!jlyAhgR-!AaTTdNeNgxM+WSrCtbvuHKHSirz7hJntf7kO>QZ271G^$x&Wn z-6-ftfCeMq4Zw68^mmSU>H1MKHf1ztT#R_|EV{9h=hxg95D+lg@&%w?sP=7-L=)U6 zt#?q&FAN5s^#{KrRsdyUy!lc!N$5AcZWxFMW%8mbs0!Y)owU~n{bOsk4Sf3Ec9^Q| z7@f}WsX(qTlA5&jRN9QHp`4721Ag*)Pe?2k&pm5!<+*dIfXxuvD0dXmZDBwZ3&(@ z3Aa`~1LNXRbi3hY4}wtp2tw7rRkoq?!UZ%ZZ4yI9&vOC7hlkM+^wG#kC;I3#gIA&h zd6QDse;f=&BQ!U&tzug=w(6?B1oJ2UM^$$%a?dGIu67Lm5PvIqH;2;m^7h-04(dK> zuR=hI&geRimqG6UP8NTw!3+C&lF3QKkd-xxIq`kZ#mi)qisQrIBh}dU^_gnhrX>J# z*FJH8nl42&>%w7Ry^9IA=-w;};2V3Pu4EbjekG8{#i|{8`&$pws4%{9f!!BL9=^#Z zA^@Z(geaI*B-s=MU(UNkchQXL&DAdwX04bLht4A|T_swCb9i;)qs%}5j@;an(syAg zet?y4Ue`RDf-WOceT$G-!!}bGR?|idXn0xP@9(WUb3<9jGz}{>EUkjAOGes13}Ge| z3AQv0G{dW0Y#fNqOKA~ssG@BL7WE=#_wv&3KlBWt(b3Rj4-~#uyAM*8o`C~G7o`d$ z%jqgZ_8~!IXl!V=93gR;R7>;Mny9B)iBgb5W3Wj@mHXk2(27;EB2s8B5{oybW4wSn zgR7|hN|ce(ezd070jUEwrxn04Bae0h?8uV62mz;_jffZVHQ4jkC81QO0pPMQ9v`dp zE3`t6%|QSvY%~hYn7>ulC4ja{r793Q-Z=%Lz?e0J^6i*oCrz-<@i3I696Al zfBVfNi;7HO1|_b)`T-7b)WRNQui84g=t>yV4)oWQL&*i(-Urh~SX*lLb^YKGO}nev zgp~n(v8b8Op!2f-7Y~Am(hETHN7a@Q@!rfA063@!96(XzDhE(3q$aacDCs?vO!jh^ z*s#sdf%k~^D1bu%1oTN_sV1Z@Zcxy3CuN8=7KNCtA;A{)$Lg(=cnpEGwdb|F-bCiX z{^%Zg0tJ4yTR^?HwZkn~RaBDFLTQA606ZU=;wOinc(5Y3jPw?=PEmE1O8SRP)CN+8 zKH3sQ zu8KgS&B6gUt&0A0*H8$y`cQ`9C?S(w>P!9EIqohCco4->gO$kfpBT175GvJ2Wv?fO zbBs~%;jQBc`Do&uh|SVH?eM2n?mBo6*4H@8LK}TMBRT~*w1NFVQ zP5=F3c|&*$i`^qw7~D)rEiTjB2$r#&1JI#8^0I<)*5mA6};Cs231JM zi`3MiMm6izo}6Z{fHTN4WW580u)*RI>eS)^$va`%H9natj>^x+EGq(9K}*FXbeAC} zVMq>4fg*q-8s3yEP^(h-JTh`Zz5D&o2ddX1tf{?+{YhUlV#(sgV#xVf#3vJR_xGvd zzzV>9uwAl?tc=Z8#Qr$;X#x~G6ngY+V~{yqDT!~_EOSHxM@BT zcA9LMPJ28A!?78afM;ryHi`fP{buM)@{`s>exM9m2LhI@^9ksz;qAhCi@~ZM8?|c)u?37S({un3 z53y2?G(G|3lG9=YB7*FEWbTRSa?^-~?p}aJ#B!Vd_zSY01F+Y3K|S(G@=UO7JsUL& z?qDji)p57YSApO`bd0!NZ|lDPlj^Mj?ZKZHxjxXn<1)F{1_rTk#?|LWqaTT)eh-16 zb?sFN8%m#G#GTx0ly&m2yqM8> zL8zF$E2j8?5l{V<~#u zSfWJf3G{uO|NULyBN+%i&p=^>^=nkwhAA*|fWX4g9$^plwTs5?Ast+e97U?0)D1^r zAJJs^VZqhJCjs(0^>LK5i)pQ!p?jnI1|_mGg;6#ZAB#%&=zP?&%g&WXKq z=Z-~t`C-B~smO{d`N#*Ose%N5Ln3-Tlp7?RNU)L~-kcAogJ`x;cTl>$bjxbu`ywys zrInYo{hfB#DDuvEvk`NzY_gj#e!a`wxOXEO6E0OS@Pfm7p}#HUmMFlU-|N<+^bz!X zjKxYUOVv5mTZC=FghsUkl|2!i5`&!5rln`UC%+qxxPd{bI{6URK(~mBW}ZmZG6(1p z<4BQ4`KgA=z>#a{$g1psqVa2GSELQyF)cDNf|kwXoDKTKMwoPe`W~8pY*u5}X;K>m zqxs0-_Xm+kMGLW_usg602Hqb)xBdfciPjPmD{^623tCDe)_^J?aee#jh{Wz2#qUN& zMpS&Bv%fv#TcpUZbXUMXO4=7=4RV;;x;a-~E(f+oarONNF)Bzw0m}9S1x#gxVqqZV zIptlTpb6}}ac{&HKmJ3+jIG(2=R?k@~ z|8q!D*s&wAThXCXFE$zCcO;}rp+u}bmW^|=?eLJ$on?1plW7rk?Sr#=7x@1T+z&jD zmJ(wa3<~86C!<2|VGAA)>FA*xH(`X*6htVPb4APW{u_9AJckM$QTu%dsBiW^vX5B#}^h5KqR-pColsgVG0T;;ua%1dez725DBOAqj$4a zMIz853So?Wut9o4_csHE=G&sHM>_ncFm>PeXw<_~S29bf^Z>6l&S?ELc;aS4pUAxn zE2*OWJm?qMo7lan26S*e5C9nd-PA-Bj;mFjFoJZK1(MrV>cP#t!Ll+!a^COKBe23| zr9vO!>UhY*IKKPMm02JW*JIhm_YOeiOc}ZrsrJMu0df_;^)*2$)i3|%KM$;1=tMy5 ziTQV7Ko1wbFUIqaGdy1db0wYHRemlaF38~pIeceH^3uxMk)A&zWjla$nl-S<`q8Frt z%T|wsgD03fM81PhuonS|0AdOc!u){|t8Rm+5Y>W`m>bo(;0l7@3}$>g&{^XNms5#a z?qQHspPT+n#9iX$5+@94HWz2-TGIrWeRm9*BK)*q!1dG?gX)ElX`--`b|M9;{(|Gz z6qNzXbAMlMcR*Wt%p37j`J&s|J#=sBqRx=NHOFgxdW(=Zi6bD=I0Ic35^{w$)COg_ z2SCFqlW<+}sV5pL=)ZmN8x%JW!Qm7nd=2R{kxZp;l&}G2N-a@gJ;#-Nd5GxPA?e*= z)`cQ>4i})Y)E+vgRYSfYerpVaFKC9oZB;{f2u$mb{li~4{fsc6R>4pQ^%g2wCijbm z3G#aRy}gOR?nnSuamIx~B#ex1{|w)+W7hZV=g6?)i>4dSdp#s{7XyDUt+T0V6>8}3 z68&uTGW$=zn~hGNwfKXFBtU{Q_+*w18euNt;S+))%A|*eD(UE2W>@8GGjo4$;YadUQyg;=70rClO2!9}W zwrlD%J3BRy%)9L>St^UJ-G*&E;^GdOyNzy)9cwPE)Wg8Iz$&V3Ml#qP4nougbVEEa z#~wt>GUE!^&m#DZ!1~-TQ0=XSxcin6n z@enLYWf+T`^QcpRWDWbK81J?tk;|wAOjfOybjK4euIVD+ux1wp>_9I_M2o+yjam*P zmN)y9+=PFAOgp8Dkx9ifmC~u=pbV?GAbQnQTMQ8mg&qmqLp~7E>0L8RCT>!#Vpvd-YA}#Y@(K0ix%+x^Q zt+C@fw$H=}m_+#n(_~`ICEdIN$w25%|0RQ+`{AXBpbsICD{n*~=lk`3h(L-9eFeAi zV2d60MelOb(zR5{r{XE64jN&Bnae4hni1D3rrjloQ4uSq^RXnf&Ih!jVN zBqv0ARw<+xcfRiG40K*?yLf@fQ>|!%E7DhOQ#Uig4gw5H*a5>)^s?#_vDbl;lsnat zs9Cv$Fj*T47K5wc?^YPD!I}VEr%X>DM}QAR302NzIiSZkTjYy5IlH2CgY9QW^q6#` zTjpCyC@U7eifc32AZ8kiW&JaBL&!gchkYCm3%rFOY!x!;Pf>MieeG*HFH>3XzHWvQ zSV3C>I3FL{dN}-eS=ElBne-U_0+vUZK=1KhcU3eTvE#ADV4nns!Lx5&5Moz0*P$r< z#4*T4{~Sq@ezX}9{hHuD$w3U{1EIb5wE3wMZs^jw)sUF|U?zlV zy(sTEiY7ws9cw4fX1{;{`>ncIE=1_AlQ{IqIY(hdKgr3--l67ClA6O$SB;voXfevT zMj=ZMGbPIctVCr>#Fs(#2j(5o^%~#KGhUPu`)81aJ z-*1nFwu2mV^uBO`r;`QhLJ;gGs}#rtOHS_3iUgj!a^YcK@*k*tTU?;=-hpikjQd<_pM_-LdK zjjt`rl=k{~znI4cC+8LAHNmQE|G^rP37t`iG)qn4vBtqUq#2~yKvV0%%9+hus0qP>n; zbJ^#_KlLl7S=yljKMy!ivr*CpqS7y|VNY9wmaS#4yM4Tyx?j&2y$EHKl=ONc*=V!-1R z!;zR2e7z6|{!)WQoy8Ob($b^mFxMe?2^+*S@l>@IReZ=l6>wtwX_l%hS4u0aiIVaMsD;D_C|kKs9&FD6eI ze4V&)D?dI)493!WguC64DI;vATk7jp1W#?@O}QrLs3kM=^9{CPliy1D84`6|Fk?FC zNp4^%dG;Sc`A>rDAY2#={TUYs8mNjyZY`Z-6Tf^1;xcy17Wuk&(T$(bY9iCs9_blc z4c5l#uOrgFX@ zG)vZ>4PdfgmJY)Ar|#-!2lQZeshg`r`(^;A)C@{QR@q*&kl2`=jqR;IVD19SWApQz zH$WK3(0bh%Hc(6Jd)`pgQpd-QZewllfw5(az|j|IyT`84reWz*VSgVSWVQu{?~UCh zG<=K1!3V#>@Wuy?0Toe3pa^dOZ5lkFKbo+TYS&T@{w?iS8v=u;XTB~O?kq9^%^ho{ ze}Dz7@|lZG2K}G7-=b2v?JIcDO;N4H?E}cy`IKtLtY^PHN2p+MXE9=v$}Mb!k8wbr z1oe>vk`mlaNfY0V=YJ|4C1E_u#p!erJ&5Y7Mo4nptH;po9RQ_6-vfg;3rgVQHc0{-n@o_0nU_xHyVLOwFAQ_ zop%iVO0uvJd^=!}6T0DrZrF4swY~XCAjB0`@t%VPg1j1g#m`HRi?hr+;gaRUP7R(>z!39U?Wc~63 zedK7}N72ce(R#mAxXK7GMRBOl+#!HzW@tDCcX$r~xVGwmd&uCkb(-m22oLyLA>711 zI(4KL1_K$GCAgOeFZHhbvk|~ff>Q_HWaE4l{X6?SFe2JzUS+C*OeWWxzk}2lNhFJ1 zDTM5FCV*I?W^|+Ai8==Y3|)1^=*Xi3+u>+c+Fig|n3Jz_q60oiiA2hVbP_Rhjh2P= zJu5&qib#{URR=9VqmT*r%)N%%pHb7d9F)7P!vgb;xIv0)<;l=GV&^2z<1l=L0f(P8 z*Ga~6qg$FZW%|1pihto75b2#~@9T60Lb6S<+*1Mq*CezCfY$RZvc#*TE)^YHsm~%) zg|kS;$r+6BK4hHBd@KD?p_Z^CS`_vC6w6dU`hc_*?{cMj#&IgPvx$Aj7yxGipv-LX z>l#orCk;$w?Hf7@=_1}WaCQL>f1lyUg3~FpY7muo@1eg6)S@6%V|~H~QB2_moTI*?(VLnZWe$VA5MwI*`(do)jc6 zd?4@}(0nx&VID`|lTLZfl$%bc7Xa;Feh1A$lt2W%nHxpL>LlO^bg^UUI{dZvyT3V6 zT`NhF#%N!EG8%7p%zDd0{zVz(4OKq;h&3RQ)zo3iT{jHod~G|f>u|sns6i$y6`>*n zuf-@_CG8}{a8+B_(ILOh=NruN5^rX!m{#Ju#-K46n>q{^o}DMlXFkF0<4+r#o$?<^ z+(#j7lZ$5esG%wmlvIw?Nj(iQU$ds%CDyYEI%m%0CY0o>ZBe%uy6-cgb0E7^EqQPl z{QS4R8l%{4c#nVvg?-3xwZ>>etnn6V*wcOw5<~Do{7;giYJh3$V3+DJW>Tz<>!A*x z>L!yxo3E+*O9R^1;xOR_7VA$z+vHzQ&YX`L6*rVM93=K82~8i0&^iImqc|$6x}gC_ zNoUgY?go210}GJ-L{HO&&EiC}45G3eRWV#=)1_^FP1il_^|~@;gzZ)APGfkV^Euh3 z1o{B)NKvz4Lz)L6ib6O9Z-r!-f+Y< zwxdXXd9u#-$kL~SMLsO95s@;`81 zv;Xa!=&E<|5ztgzF9es0ZMeRhhuE6L097(Iz}BDyf5$BD$4$TC(guhc&pns}6L}Ce zHz)zFVoF_(pgz79ix09wI28zKB(lRh6*4qm_I)~4?TDQHPI;si*kNgkG$!b2>C8+5 zsF(P?hqLk4&6fxi@b%g!PW)4UAEdmxx$I0^#v)YbM0$TX4QIY%R^Dllx4m7W;P!Yy zhL+t1ar_3p{2W}&*#jLNGw`*cLYsi{LaGU6O-MBp49=<-16*m1W)A=6NcomiFU)jw zL|672gQu>*8lG>KBLX8I`l6|sd0;%1AaIbdgS+EcxSKDXFqAl{+;4x+RWQ$4w7q+) z^OTe)VPuV++CguoKB)#ux#a%GT^f9Ptd8qA9e-Ide=S4aK6Qv2U9qn+(ZS9&{Z2+j z4ZTWHBQl51pRUkZd@dahR-Y2sb_8{3B|i6s;7!;^?j#KVbHtp{wfE9iLe#iN?Z`mh zmhnOmpyd-Y}1S+tCH-F!OM=oWQNH-4K9}PO%DFgFqQuAQ?O)CG?6gHKT>iL zps+NYtr;`ikciI|KhM1$0C`U1-WvOIY_{Dw`F;L_o!pl-X@%cUt2tYUD` zRB%&NjtQi#$@Uia1{e&1P?PZ!;&%bdySPYO_thA7OQDRyjY#MUl6owjHnN^iL8Fv& zA{Xml4n8qX5V%~R1oH5Jp=|kI0fk{xMqQ$iy~zZ=-XkXSFCWA&4y=Wi7Jg#3P%8x; z0XO8?8WhKnV|j=+0_vD82*0zi8TG8>F6I=qdf!zOo(4X>|F6G0z_kl0HbFFFX!}69x(eNZN?^KVVDnX&|5%ENFfelbIQXbjXoxz zs_em)A<2@7f|uh9xx-Q$^4wt-+H~jXvd_Z#Cvj{vZo<4H`FwuY2Sfu8N(Lc=;C|L? zF~swm&>RQqoaYtAI3JX^Px-G?PY2#0|J*+&k495t?NyOCRvun7z4l6gzIDKZ z1=-B&8nd;SXF|8{f3%=Y;qqG@?Mm}01+L%xbnWs#rcA$aaD&YxqelmWpDq{Fc^^77 z^2?@e(pzFG5-JiLt_&enf-tW!gu^Kh62MKaC0eFn~w_9}!cg!Kaxo68t?G;^q6R^SGmMV06T`fAi`QybPEMbnqBeq1 zRUmIpHoUQyhx6ndft*t+RLutRF|x^{7pRfsc}vUU0TBtWkQ+T3&LJy2!EB&X`XAY11T3N~4 z@+N+qvG(}HJ6JU7@ZlIX=aqdgyTR_{cICX1{-# zGo0zpyHxgnTr=B)EiU*`03| z46BrN>P3}Bk57*Uf&aJg{y$rIRdP(0K6m@YypU5u+$lJzYYpc!n%*7p493?3iVE8g;gs}wA>O8;t^KUR zs79Gldy=;a<*Vz!vQ835|1>A$Jz$*qZ~iv<%RHRPyA)V9-if|U;vpU0`aF&9m~z-{ zYToev{7&+!y(@2uJ4eQcuL!mnA;~ejHC!+O;G#fUp~=E*rbm2u-TU_f`}XfY6?eS< z1vSy6!$!#C&6y`DYyIh8gZ|F_esuaGywDnHA zRRu2HMu9go-HZ)KSlegmU3WYx;fb@R>b(7OZ`X@0s|MyUHZK2iZR`5kBcbp*OwHmm zYuo@|ZER~3V-@O$mbNB&PEMJ`GY$tPkFb83trr)f@4Zo0_Igwl25H=Bx+uGO^G&yc zKB%kG>fC>w_sDhx*7V-dnI8T5h}Nf0`MliR&(Frc>8X0D<b)f<_o-zO~!kmudV z3@7`-v3ctQf*(J=zvF}$zulT~i?&oRMl&YsTMP@WU_PGpUyD%~6Fw{d#wx)n^R$`W zqo}e_yo2+AQf^m#^Ua$Ev!aG+Ovx{+;UB#|#z(M-93A7sPs#YCo?dsIrLS3rGsK4v zYd~+2eFPpUweC2A{smxX{q7$(6jqg84hsrse`DUHPc98M; zE~yh*T4@%WoZM7{+}?ls^sY)eWe;yeofnO;u8A7UzcKQ7)6jQqc`)DZ-Me|~ocVo7 zYrLh&ebpP_F7#vCtmC-@^8UR(OT#}+61O}N%jyks!?SvpX1_;9LKLc8u=#_65Bxu&}_x%uO~Z2^J;Xg!Kih#OsA`Pn(UsRtAK z>Lz>J66u(tF$5S{uybj!SsrdHnMaEqkrQheZ<-6Ac>x6K_5_n{lGo zr~Y5w#{H~)#_{ClKdh1~R+#rpv9Pcx%4&_zZCS?r_vglkdNNG-`Mg zb+>o@kMCW!s?=mtXjNx_2A09Syr$A+em--VuqWzcjQ@B=ekNOea&)%(r<=vyiVwFo zW7r@Z`%jlIvF%WJP46eGWSF?Q&I|9H7oz!z|p(9 z?aW75ugXV^d6tO#%oyK&&-3QTf5l)}tovVmt1oX{^1r*7yf?~!#oL*Azhg}Bw0XbG zfBC-#9K822EZ8CWm)G>a3GUyQKm6Y%a>imp*!E>`{%;hyWASh2;$IGbg{3`-{#DqS P-le#6pF;Rg+L!+ap;TJ? literal 0 HcmV?d00001 diff --git a/profinet_stack/p-net/doc/illustrations/Cyclecounter.png b/profinet_stack/p-net/doc/illustrations/Cyclecounter.png new file mode 100644 index 0000000000000000000000000000000000000000..3981680d4b4ae3d24c43e62a6e55062c657b9bda GIT binary patch literal 90706 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 DCrChS literal 0 HcmV?d00001 diff --git a/profinet_stack/p-net/doc/illustrations/FlasherFunctionBlock.png b/profinet_stack/p-net/doc/illustrations/FlasherFunctionBlock.png new file mode 100644 index 0000000000000000000000000000000000000000..9ccfa2807975902d9995becc3e87ed981c019909 GIT binary patch literal 18901 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 literal 0 HcmV?d00001 diff --git a/profinet_stack/p-net/doc/illustrations/NetworkLoad.png b/profinet_stack/p-net/doc/illustrations/NetworkLoad.png new file mode 100644 index 0000000000000000000000000000000000000000..d211bc6f10b3e12d33726463347b15edad822f50 GIT binary patch literal 71982 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 literal 0 HcmV?d00001 diff --git a/profinet_stack/p-net/doc/illustrations/PlcProgramLadderLogic.png b/profinet_stack/p-net/doc/illustrations/PlcProgramLadderLogic.png new file mode 100644 index 0000000000000000000000000000000000000000..888863fea2972d609b2b601b19f81f654b513925 GIT binary patch literal 89301 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


f6zCPK3i5i`pR|7O7kLBcuRu)(|M=KzJ9+oF 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 Ubld-AI?zrAPgg&ebxsLQ0La(o3IG5A literal 0 HcmV?d00001 diff --git a/profinet_stack/p-net/doc/illustrations/RaspberryPiLedButtons.jpg b/profinet_stack/p-net/doc/illustrations/RaspberryPiLedButtons.jpg new file mode 100644 index 0000000000000000000000000000000000000000..757b5c42afa67ec4b571834651a06dceae8e273f GIT binary patch literal 135398 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%0AEB87zrdS8p#fgbR9kb*owyN*c>@qm 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 \ No newline at end of file diff --git a/profinet_stack/p-net/doc/illustrations/TutorialOverview.png b/profinet_stack/p-net/doc/illustrations/TutorialOverview.png new file mode 100644 index 0000000000000000000000000000000000000000..47d6e6a586b69ed5889d853df7bf86c1e7a1bcd2 GIT binary patch literal 23042 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%0DjL)v)Bo*kh4S0Sr#$vX$B&0N5f%}!fgV#7P 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 literal 0 HcmV?d00001 diff --git a/profinet_stack/p-net/doc/illustrations/proneta_example.png b/profinet_stack/p-net/doc/illustrations/proneta_example.png new file mode 100644 index 0000000000000000000000000000000000000000..a473002a64e6903566bc0e0cc5a51c1eb52b5d32 GIT binary patch literal 27309 zcmdqIWl$V%^fefW5IjH#2^t)R00BY>?mD>pV1eN7mf-FdAm{*tyUPT(;O=e%gAdNI z^Q-s2Rl8MNweNo24^y|g>*?z0+xIzr<(zP3MQL0tQmkjsp5e;MNT@!0h7Nf44DILZ zSEw_AAlX8cL337>7JF7PN`8Pkcwr%`Ao}cCbu9LS@k`V(rh|-*^Rs7o-T(d2`t2cR z&z@O{$x4W-dl&&C!F+WELrlm{{464zUqC^_nG(&0RaK8 zau7mJLPBEu8xG(Aj`BuKZ;sf1roO+#@7*Uo>u3TvAMM??u4yZNwJxNvWTFHgE4!nnp`qzFQU2dGPG-u@$a8G@4`s5b3$2Q@ z^Tf8Fc^=NCr)E8pT%7r@G%BDy=DV+cVgH8s_V3T*3LE}qGV#uC680$PNjt*6O*?1g z)01!b>b0TQvFtI`Ws#9G_2P(TrGBo`5yA(`9P{O)J=q_UCEM-&a5I85*A#;HCrd>w5=tYXAx{hry!XVDOLUwhEPA?GAd z)%R_$c0lT1RAZdj){PoVQ`u8`pIdrmsXzLh&X(qmC3s?n%kCQB3q{-ny%6-hR+#$@ zpbKswKq3n%eb*_SCjvaZ7RCv@f3u`K#{4^qY1$a|-}fZrDdVwkJ&qXnPE1G~!N|Ia z<=AWey?YTWY!>zV^Mt@&0*m;tefyPDZ9^exO$>LbffdWxUG>-?r`?J3J*7vO=Up>N z{h|LF#mxA+XxoyAiM{~ogW>e_v~g#^%l+AW#*|9k;Es0hjq9~{w6cGwo`;0b4=~-# zDAga}=U7y(I+R*AvY>Z}h>Sd4ATh6|kw+~C)qPLuWI#Y!^@uMAT)d~RcOyOHWcmsH zgAT>n_6qeVo>jjvx&a{}aEz9lx|poJFe6v$@+qWpJ`C)F&FV?fik?})d zX!Mta1U32JPpP$|T=YRby%>QKO(jEEkVIk5nU0RRc}yucMMl$uP0)xpmEX{}^P9w6 zu^?Xi(NCsFLifMVk>7#u#94XdA|oBw^2c(tvi;H!h}4if@+0@t+&-?@hhhDPVX>`; z;mWVMAy>J*^jdymE+EDX)YHFwKM|@83NCcK( zSaftR?UU!sdv5c#+lA1R4^Xy;L4lpJ3mi+vTB}~pC~`r?-~tJ8u@UK$He-3AD~ZeF zC8z;arGFo?iN)r?=MzZ;*=)tc{R#2hnoZkzwuLfC%i2XlxI|nkYV+2E|K`!}-){u& zs?Vcq+wHDi8i(~ep{y%rq5E^-!_8J~8pjs!QyM=Bm&L!o{n(cd`jHhLV6@SL4VQ>{n-<~oCwR&JD(R`#x3uY?xk3kDq8 zwGfklyqMWKj-1Gy|2Z~YRPZ}qw_k5AT37US<16(N#R>&6Qu)8Yf4C5mzh9?NPkr0< z<@s!*Ylq4O+LYV8O)n#Kl2E^JLKYwPfbEkZ5fvtJ`Nf+@cvk&W53Ebjf+o@Fpc4-Y$3>f&dUjrk%u{d6)WB_E#h~VhYdeS`GmaoaIMR#wLl)4F9WUA zNY~`L7cxhi3)RmC>Q?%PTDVIQEfh0@mAojxoz&o=g5DQqAy2qs536z+lf?0abyL#> zI1nAmY4L7MW5P(~{W`Y?W(%0D%yW?C4;HjOU5WC{%t&BlB+mWSods1!$=*~$xRT

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.ar_properties.startup_mode + +Section 17 "Startup Mode" in the guideline "PROFINET IRT Engineering" discusses +the differences between legacy and advanced startup modes. + +Sections in 61784-2 (profiles) describing Legacy Startup mode: + ++---------------+-------------------------------------------------------------+ +| Section | Description | ++===============+=============================================================+ +| 7.1.9.2 | "Index" ARFSUDataAdjust (0xE050) | ++---------------+-------------------------------------------------------------+ + +Sections in 61158-5-10 (services) describing Legacy Startup mode: + ++---------------+-------------------------------------------------------------+ +| Section | Description | ++===============+=============================================================+ +| 7.3.1.3.4 | "Attributes" Allowed values for Startup Mode and for CM | +| | Initiator Activity Timeout Factor. The fields "IR Info | +| | Block", "SR Info Block", "Redundancy Info" and "List of | +| | AR Vendor Blocks" only in advanced startup mode. | ++---------------+-------------------------------------------------------------+ +| 7.3.1.3.5.1.2 | "Connect" Usage of "IR Info Block" , "SR Info Block" and | +| | "AR Server Block" | ++---------------+-------------------------------------------------------------+ +| 7.3.1.3.5.11 | "Read Expected Fast Startup Data" Only in legacy mode, and | +| | only for fast startup. | ++---------------+-------------------------------------------------------------+ +| 7.3.1.3.5.12 | "Write Expected Fast Startup Data" Only in legacy mode, and | +| | only for fast startup. | ++---------------+-------------------------------------------------------------+ +| 7.3.1.3.6.3 | "IO controller during startup" Record data ARFSUDataAdjust | +| | (0xE050) supported only for legacy mode | ++---------------+-------------------------------------------------------------+ +| 7.3.1.4.4 | "Attributes" RT Class and startup mode combinations | ++---------------+-------------------------------------------------------------+ + +Sections in 61158-6-10 (protocol) describing Legacy Startup mode: +(search for "startupmode") + ++---------------+-------------------------------------------------------------+ +| Section | Description | ++===============+=============================================================+ +| 5.1.2 | "APDU abstract syntax" IODConnectReq with StartupMode:=1 | ++---------------+-------------------------------------------------------------+ +| 5.2.4.4.4 | "Record index" Index 0xE050 ARFSUDataAdjust | ++---------------+-------------------------------------------------------------+ +| 5.2.42.2.4 | "ARBlockReq" Checking of CMInitiatorActivityTimeoutFactor | ++---------------+-------------------------------------------------------------+ +| 5.6.3.1.1 | "General" Use Profinet v 2.2 for Legacy startup mode. | ++---------------+-------------------------------------------------------------+ +| 5.6.3.3.4 | "CMSU state table" Legacy startup mode implemented in CMSU, | +| | PPM and CPM. | ++---------------+-------------------------------------------------------------+ +| Figure A.2 | Startup in advanced mode | ++---------------+-------------------------------------------------------------+ +| Figure B.2 | Startup in legacy mode | ++---------------+-------------------------------------------------------------+ + + +Conformance class D +------------------- + +7.1.3.2.1 Communication (Class D should support one additional Device Access AR) + + +Messages and function calls at startup +-------------------------------------- + ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | Incoming | | Outgoing | | CMDEV | Application | Other | +| | message | | message | | state | | | ++==================+====================+=======================+============================================+=====================================+ +| Connect req | | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_exp_module_ind() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_exp_submodule_ind() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_connect_ind() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | W_CRES | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | | PPM starts sending cyclic data | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | | PF_CPM_STATE_FRUN | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | W_SUCNF | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_connect_ind() with PNET_EVENT_STARTUP | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | | CMSM timer started | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_new_data_status_ind() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | | PF_CPM_STATE_RUN | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | W_PEIND | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | Connect resp | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| Write req | | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_write_ind() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | Write resp | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| DControl req | | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | W_PERES | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_dcontrol_ind() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | W_ARDY | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | (PRMEND) | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_connect_ind() with PNET_EVENT_PRMEND | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | Run pnet_input_set_data_and_iops() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | DControl resp | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | Run pnet_application_ready() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | (APPLRDY) | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_connect_ind() with PNET_EVENT_APPLRDY | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | CControl req | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | W_ARDYCNF | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| CControl resp | | | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | WDATA | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_ccontrol_cnf() | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | | pnet_connect_ind() with PNET_EVENT_DATA | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ +| | | DATA | | | ++------------------+--------------------+-----------------------+--------------------------------------------+-------------------------------------+ + + +Useful functions +---------------- +Show lots of details of the stack state:: + + pnet_show(net, 0xFFFF); + + +Coding rules +------------ +In order to be platform independent, use ``CC_ASSERT()`` instead of ``assert()``. +Use ``ASSERT()`` for rt-kernel-specific code. + +Include headers in sorted groups in this order: + +* Interface header (Corresponding to the .c file) +* Headers from same project +* Headers from the operating system +* Standard C headers + +New files should have the rt-labs standard header comment, with description of +the license and "Copyright YYYY rt-labs AB, Sweden." + +Avoid "Yoda conditions":: + + if (3 == a) { /* ... */ } + +Use C-style comments in C files, C or C++ comments in C++ files:: + + /* C style comment */ + + // C++ style comment + +Declare (and initialize) internal variables in the beginning of a function. + +Function names should start with the file name, for example functions in +``src/common/pf_ppm.c`` are named ``pf_ppm_xxx``. + +Typically functions should return 0 on success and -1 on error. + +Name functions and variables using "snake_case", for example +``pf_lldp_get_chassis_id ()`` and ``min_device_interval``. + +Avoid to start pointer names with ``p_``. It can be useful in some special situations +but we will gradually remove those names from the p-net stack. + +For error handling, the use of ``goto`` is acceptable. + +If possible, avoid the modulo operator as it is slow on some platforms. + +Instead of:: + + if (have_dhcp == true){...} + if (!have_dhcp){...} + +use:: + + if (have_dhcp){...} + if (have_dhcp == false){...} + +(Note that this not yet is fully implemented in the stack.) + +Run clang-format on staged files before committing:: + + $ git add . + $ git clang-format + +This will format the commit using clang-format. Examine and stage +modified files before finalizing the commit. + + +Static code analyzer +-------------------- +To install the clang static analyzer tool:: + + sudo apt-get install clang-tools + +Run it using:: + + rm -rf build + scan-build cmake -B build + scan-build -o clangreports make -C build -j4 + +The resulting HTML reports will end up in the ``clangreports`` subdirectory. + + +Workflow +-------- +We have chosen to host the code on Github to ease the collaboration between +users and different developers, and we take advantage of the standard +Github workflow: + +* Open a Github issue on https://github.com/rtlabs-com/p-net/ for each + separate bug found. +* Fork the repository to your own account on Github, and make a local + clone on your laptop. +* Create a branch with a descriptive name. +* Commit your fix to the branch. Add the line ``Closes #123`` (for example) + in the commit message, to indicate which Github issue it closes. +* Push the branch to your Github account. +* Create a pull request to https://github.com/rtlabs-com/p-net +* After review the fix will be merged. diff --git a/profinet_stack/p-net/doc/index.rst b/profinet_stack/p-net/doc/index.rst new file mode 100644 index 0000000..02a03ce --- /dev/null +++ b/profinet_stack/p-net/doc/index.rst @@ -0,0 +1,50 @@ +.. p-net documentation master file, created by + sphinx-quickstart on Fri Jan 31 12:13:48 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to p-net's documentation! +================================= +Here you will find a tutorial, the API reference as well as other detailed +documentation. + + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + _copied/README.md + profinet_basics.rst + tutorial.rst + additional_linux_details.rst + getting_started_rtkernel.rst + getting_started_freertos.rst + using_codesys.rst + codesys_details.rst + use_with_siematic.rst + capturing_packets.rst + sampleapp_details.rst + profinet_details.rst + prepare_raspberrypi.rst + multiple_ports.rst + creating_gsdml_files.rst + network_topology_detection.rst + applications_and_porting.rst + linuxtiming.rst + compliancetest.rst + yoctobuild.rst + getting_started_LAN9662.rst + abbreviations.rst + api_documentation.rst + _copied/LICENSE.md + _copied/CHANGELOG.md + implementation_details.rst + writing_documentation.rst + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/profinet_stack/p-net/doc/linuxtiming.rst b/profinet_stack/p-net/doc/linuxtiming.rst new file mode 100644 index 0000000..303df9a --- /dev/null +++ b/profinet_stack/p-net/doc/linuxtiming.rst @@ -0,0 +1,116 @@ +Real-time properties of Linux +============================= +Regular Linux is not a real-time operating system. Profinet have rather strict +timing constraints. For example a Simatic PLC (with default settings) will +send an alarm if no incoming Profinet frame is received for 7-8 ms. So it is +important that Linux is responsive enough. + +There are a few methods that can be used to improve the Linux responsiveness. +The most important is to use FIFO scheduling and to isolate the application on +a separate CPU core. + + +Use USE_SCHED_FIFO option +------------------------- +Select the FIFO Linux kernel scheduling option. This is done by passing +``-DUSE_SCHED_FIFO=ON`` command line argument to cmake. + +Display real time properties of a process (should typically be ``SCHED_FIFO`` +for best result):: + + pi@pndevice-pi:~$ chrt -p $(pidof pn_dev) + pid 438's current scheduling policy: SCHED_OTHER + pid 438's current scheduling priority: 0 + +To show all threads, with scheduling mechanism:: + + ps -efLc + +or for the sample application:: + + ps -efLc | grep pn_dev + +The column "NLWP" shows number of threads, and the column "LWP" shows the thread ID. +In the column "CLS" is the thread scheduling policy shown. It can be for example +``TS`` for time sharing, ``RR`` for round robin or ``FF`` for FIFO scheduling. + +To show the names of the threads, use custom output of the ``ps`` command:: + + pi@raspberrypi:~$ ps H -o 'pid ppid nlwp tid lwp comm cls pri psr time' $(pidof pn_dev) + PID PPID NLWP TID LWP COMMAND CLS PRI PSR TIME + 1018 1 8 1018 1018 pn_dev TS 19 2 00:00:00 + 1018 1 8 1020 1020 os_eth_task FF 50 0 00:00:00 + 1018 1 8 1021 1021 os_eth_task FF 50 0 00:00:00 + 1018 1 8 1022 1022 os_eth_task FF 50 0 00:00:00 + 1018 1 8 1023 1023 p-net_bg_worker FF 45 0 00:00:08 + 1018 1 8 1029 1029 pn_snmp FF 41 1 00:00:00 + 1018 1 8 1030 1030 os_timer FF 70 3 00:01:37 + 1018 1 8 1031 1031 pn_dev FF 55 2 00:01:54 + +The "PSR" column shows which processor (core) the thread is running on. + + +Run the application on a separate processor core +------------------------------------------------ +It is possible to tell the Linux kernel not to put any processes on a specific +processor core. This assumes that you have more than one core in your CPU. +Check it using:: + + cat /proc/cpuinfo + +By setting the ``isolcpus=2`` Linux boot command, the kernel will not put any +processes on CPU core number 2 automatically. This option is typically set from +the boot loader. + +The best way to check which CPU cores that are currently isolated:: + + cat /sys/devices/system/cpu/isolated + +To see which kernel command line options that have been used:: + + cat /proc/cmdline + +Put your Profinet application on the isolated CPU core. It is done using:: + + taskset -c 2 pn_dev + +where ``-c 2`` tells which CPU core to use. + +Display which CPU core a process is running on:: + + pi@pndevice-pi:~$ taskset -c -p $(pidof pn_dev) + pid 443's current affinity list: 2 + +Real-time patches +----------------- +By applying the real-time patches (PREEMPT_RT) the real-time properties can +be improved. + +For more details, see: + +* https://wiki.linuxfoundation.org/realtime/start +* https://rt.wiki.kernel.org/index.php/Main_Page + +It is important to write you application so that it can use the benefits of +the real-time patches. This includes running in a separate thread and setting +the priorities properly. + +For the real-time patches to have an effect on p-net, set the ``USE_SCHED_FIFO`` +cmake option. + + +Increase application cycle time +------------------------------- +For testing, you can increase the cycle time from the PLC in order to reduce +the time-out problems. Also the allowed number of missed frames can be +increased in the PLC settings. + + +Network interface hardware +-------------------------- +If your Ethernet network interface is connected via USB, there can be an +additional latency. This can affect the frame-to-frame interval for +transmitted Profinet frames. + +For example the Ethernet interface in a Raspberry Pi 3 is connected via +an USB bus internally. diff --git a/profinet_stack/p-net/doc/multiple_ports.rst b/profinet_stack/p-net/doc/multiple_ports.rst new file mode 100644 index 0000000..8ec1360 --- /dev/null +++ b/profinet_stack/p-net/doc/multiple_ports.rst @@ -0,0 +1,272 @@ +Multiple ports +============== +This section describes how to configure the p-net stack, sample application +and system for multiple network interfaces or ports. +So far, multiple port has only been tested using the Linux port. + + +Terminology +----------- + +Interface + Abstract group of ports. In Profinet context, interface typically doesn't mean a + specific network interface. This is a common cause of confusion. +Port + Network interface. The physical connectors are referred to as "physical ports". + A "management port" is the network interface to which a controller / PLC connects. + +In the example described in this section, ``br0`` is the management port +and ``eth0`` and ``eth1`` are the physical ports. The interface consists of +``br0``, ``eth0`` and ``eth1``. + + +Configuration of bridge using Linux +----------------------------------- +Tested with Raspberry PI 3B+ and USB Ethernet dongle USB3GIG. + + +-------------+ + | br0 | + +------+------+ + | eth0 | eth1 | + +------+------+ + + +p-net supports multiple Ethernet ports. To use multiple ports, these +shall be grouped into a bridge. In such a configuration the management port / main network interface +is the bridge and the Ethernet interfaces are named physical ports. + +To create a bridge and add network interfaces to it, create the following files and content:: + + # /etc/systemd/network/bridge-br0.netdev + [NetDev] + Name=br0 + Kind=bridge + + # /etc/systemd/network/bridge-br0.network + [Match] + Name=br0 + + # /etc/systemd/network/br0-member-eth0.network + [Match] + Name=eth0 + [Network] + Bridge=br0 + + # /etc/systemd/network/br0-member-eth1.network + [Match] + Name=eth1 + [Network] + Bridge=br0 + +Enable and start network daemon:: + + sudo systemctl enable systemd-networkd + sudo systemctl start systemd-networkd + +Run ``ifconfig -a`` to check that the bridge is up and its network interfaces are all up:: + + pi@pndevice-pi:~$ ifconfig -a + br0: flags=4419 mtu 1500 + inet 192.168.0.50 netmask 255.255.255.0 broadcast 0.0.0.0 + ether 3e:9b:37:c6:3e:ee txqueuelen 1000 (Ethernet) + RX packets 209875 bytes 11260238 (10.7 MiB) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 201263 bytes 12927207 (12.3 MiB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + + eth0: flags=4419 mtu 1500 + inet6 fe80::ba27:ebff:fef3:9ab2 prefixlen 64 scopeid 0x20 + ether b8:27:eb:f3:9a:b2 txqueuelen 1000 (Ethernet) + RX packets 209581 bytes 12999322 (12.3 MiB) + RX errors 0 dropped 2285 overruns 0 frame 0 + TX packets 205044 bytes 14541592 (13.8 MiB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + + eth1: flags=4419 mtu 1500 + inet6 fe80::a2ce:c8ff:fe1e:1745 prefixlen 64 scopeid 0x20 + ether a0:ce:c8:1e:17:45 txqueuelen 1000 (Ethernet) + RX packets 6019 bytes 1764099 (1.6 MiB) + RX errors 0 dropped 2320 overruns 0 frame 0 + TX packets 6662 bytes 721472 (704.5 KiB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + + lo: flags=73 mtu 65536 + inet 127.0.0.1 netmask 255.0.0.0 + inet6 ::1 prefixlen 128 scopeid 0x10 + loop txqueuelen 1000 (Local Loopback) + RX packets 38 bytes 3952 (3.8 KiB) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 38 bytes 3952 (3.8 KiB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + + wlan0: flags=4163 mtu 1500 + inet 192.168.42.92 netmask 255.255.255.0 broadcast 192.168.42.255 + inet6 fe80::d4bf:5c2c:48c0:bf1e prefixlen 64 scopeid 0x20 + ether b8:27:eb:a6:cf:e7 txqueuelen 1000 (Ethernet) + RX packets 1883 bytes 135753 (132.5 KiB) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 126 bytes 20075 (19.6 KiB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + +No IP address has to be assigned to the bridge or any of the Ethernet interfaces +in order for the bridge to forward packets from one Ethernet interface to +the other. However the bridge needs to be in ``UP`` state. + +To show bridge status:: + + pi@pndevice-pi:~$ bridge -d link + 2: eth0: mtu 1500 master br0 state forwarding priority 32 cost 4 + hairpin off guard off root_block off fastleave off learning on flood on mcast_flood on neigh_suppress off vlan_tunnel off isolated off + 3: br0: mtu 1500 master br0 + 5: eth1: mtu 1500 master br0 state forwarding priority 32 cost 19 + hairpin off guard off root_block off fastleave off learning on flood on mcast_flood on neigh_suppress off vlan_tunnel off isolated off + +You can also use the ``brctl`` Linux command from the ``bridge-utils`` package:: + + pi@pndevice-pi:~$ brctl show + bridge name bridge id STP enabled interfaces + br0 8000.3e9b37c63eee no eth0 + eth1 + +Another useful Linux command is ``networkctl``:: + + pi@pndevice-pi:~$ networkctl + IDX LINK TYPE OPERATIONAL SETUP + 1 lo loopback carrier unmanaged + 2 eth0 ether degraded configured + 3 br0 bridge degraded unmanaged + 4 eth1 ether degraded configured + 5 wlan0 wlan routable unmanaged + + 5 links listed. + +To disable the creation of the bridge at reboot:: + + sudo systemctl disable systemd-networkd + +If you do not use systemd, then a script ``enable_bridge.sh`` might be handy:: + + ip link add name br0 type bridge + ip link set br0 up + ip link set eth0 up + ip link set eth1 up + ip link set eth0 master br0 + ip link set eth1 master br0 + +And correspondingly ``disable_bridge.sh``:: + + ip link set eth0 nomaster + ip link set eth1 nomaster + ip link set br0 down + ip link delete br0 type bridge + + +Configuration of p-net stack and sample application +--------------------------------------------------------- +To run p-net and the sample application with multiple ports a couple +of things need to be done. Note that the settings described in the +following sections are changed by running ``ccmake .`` in the build folder, +and then ``options.h`` will be regenerated. + +Reconfigure setting ``PNET_MAX_PHYSICAL_PORTS`` to the actual number of physical +ports available in the system. For this example the value shall be set to 2. + +Reconfigure setting ``PNET_MAX_SUBSLOTS``. Each additional port will require an +additional subslot. For this example the value should be be set to 4. + +Another way to set the options is to set them on the cmake command line:: + + -DPNET_MAX_PHYSICAL_PORTS=2 -DPNET_MAX_SUBSLOTS=4 + +The configuration field ``.num_physical_ports`` must be set accordingly. For +the Linux sample application this is set automatically by parsing the command +line arguments, which need to be adjusted. + +Example of initial log when starting the demo application with a multi port configuration:: + + pi@pndevice-pi:~/profinet/build $ sudo ./pn_dev -v + ** Starting Profinet demo application ** + Number of slots: 5 (incl slot for DAP module) + P-net log level: 0 (DEBUG=0, FATAL=4) + App verbosity level: 1 + Nbr of ports: 2 + Network interfaces: br0,eth0,eth1 + Button1 file: + Button2 file: + Station name: rt-labs-dev + Management port: br0 C2:38:F3:A6:0A:66 + Physical port [1]: eth0 B8:27:EB:67:14:8A + Physical port [2]: eth1 58:EF:68:B5:11:0F + Current hostname: pndevice-pi + Current IP address: 192.168.0.50 + Current Netmask: 255.255.255.0 + Current Gateway: 192.168.0.1 + Storage directory: /home/pi/profinet/build + +Update GSDML file +----------------- +The sample app GSDML file contains a commented out block that defines +a second physical port. In the sample application GSDML file, search for "IDS_P2" +and enable commented out lines as described in the GSDML file. + +Note that you will have to the reload GSDML file in all tools you are using and +also the Automated RT tester any time the file is changed. + + +Running ART tester with multiple ports +-------------------------------------- +Use the MAC-address of ``br0`` when running ART tester. + + +Routing traffic with multiple ports on Linux +--------------------------------------------- +To see the MAC addresses and IP addresses of the neighbours, use the ``arp`` +Linux command:: + + pi@pndevice-pi:~$ arp + Address HWtype HWaddress Flags Mask Iface + 192.168.0.99 ether 20:87:56:ff:aa:83 C br0 + 192.168.42.1 ether b4:fb:e4:51:09:72 C wlan0 + 192.168.0.98 ether ac:4a:56:f4:02:89 C br0 + 192.168.0.30 ether 54:ee:75:ff:95:a6 C br0 + +To see the IP routing table, use the ``route`` Linux command:: + + pi@pndevice-pi:~$ route + Kernel IP routing table + Destination Gateway Genmask Flags Metric Ref Use Iface + default 192.168.42.1 0.0.0.0 UG 305 0 0 wlan0 + 192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 br0 + 192.168.42.0 0.0.0.0 255.255.255.0 U 305 0 0 wlan0 + + +Adding multiple Ethernet ports on a microcontroller +--------------------------------------------------- +Typically a microcontroller has only one Ethernet controller built in. One way +to provide more Ethernet ports is to use an external Ethernet switch chip. +However the Profinet stack must be able to control from which Ethernet port +a frame is sent from. This is because the outgoing LLDP frames should have +different contents on different ports (important for neighbourhood detection). +Similarly, the stack must be able to figure out on which port an incoming +LLDP frame did arrive, in order to determine the closest neighbour for each +Ethernet port. + +Traffic not intended for the device should be routed between the external +Ethernet ports. + +There are Ethernet switch chips with functionality to route traffic to +different Ethernet ports from a single Ethernet controller in a microcontroller. +This is done by "tail tagging", where a few additional bytes are put in the +end of outgoing frames. The switch chip will remove those bytes, but will +use the information to route the frame to the correct outgoing Ethernet port. + +A detailed description of the concept is given on +https://www.segger.com/products/connectivity/emnet/technology/port-multiplication/ + +Examples of useful Ethernet switch chips (not tested): + +* Microchip KSZ8863 +* Microchip KSZ8873 +* Microchip KSZ8895 + +The p-net stack does not implement support for tail tagging by default. diff --git a/profinet_stack/p-net/doc/network_topology_detection.rst b/profinet_stack/p-net/doc/network_topology_detection.rst new file mode 100644 index 0000000..b84fd32 --- /dev/null +++ b/profinet_stack/p-net/doc/network_topology_detection.rst @@ -0,0 +1,1376 @@ +.. _network-topology-detection: + +Network topology detection +========================== + +A Profinet device of class B has the same features as a device class A, plus +the support for network topology detection. That allows tools to ask the +devices how they are connected, so it is possible to automatically draw a +network map. See for example the illustration in the section on the PRONETA +tool below. + +LLDP +---- +Link Layer Discovery Protocol (LLDP) is used to find information on the closest +neighbours on an Ethernet network. Each IO-device, IO-controller and (managed) +switch send out frames containing its name and the port name, for each of their +Ethernet ports. + +LLDP packets should be sent every 5 seconds, and have a time-to-live value of 20 seconds. + +Managed switches filter LLDP frames, and send its own LLDP frames. +That way everyone can know what is their closest neighbour. Only LLDP frames from +the closest neighbour is received on each port, thanks to the filtering done by +the switch. + + +SNMP +---- +It is possible to ask an IO-Controller or IO-device (conformance class B or +higher) about its neighbours, using the Simple Network Management Protocol (SNMP). + +SNMP agents collect information on individual devices, and the SNMP manager +retrieves information from them. + +Some of the information can also be queried via the Profinet DCE/RPC protocol. + +SNMP versions: + +* v1 Mandatory for Profinet IO-devices and IO-Controllers +* v2c Supports 64-bit statistical counters +* v3 Encrypted + + +Monitor incoming LLDP frames on Linux +------------------------------------- +This is useful for experimenting with LLDP, but should not be running on the +same device as p-net. This is because p-net implements LLDP transmission and +reception itself. + +Install the ``lldpd`` daemon:: + + $ sudo apt install lldpd + +It will start to collect information automatically, but will also start sending +LLDP packets on each interface every 30 seconds. + +Show neighbour information:: + + $ lldpcli show neighbors + ------------------------------------------------------------------------------- + LLDP neighbors: + ------------------------------------------------------------------------------- + Interface: enp0s31f6, via: LLDP, RID: 1, Time: 0 day, 00:00:26 + Chassis: + ChassisID: local b + SysName: sysName Not Set + SysDescr: Siemens, SIMATIC NET, SCALANCE X204IRT, 6GK5 204-0BA00-2BA3, HW: Version 9, FW: Version V05.04.02, SVPL6147920 + MgmtIP: 192.168.0.99 + Capability: Station, on + Port: + PortID: local port-003 + PortDescr: Siemens, SIMATIC NET, Ethernet Port, X1 P3 + TTL: 20 + Unknown TLVs: + TLV: OUI: 00,0E,CF, SubType: 1, Len: 20 00,00,01,BC,00,00,00,00,00,00,04,63,00,00,00,00,00,00,00,00 + TLV: OUI: 00,0E,CF, SubType: 2, Len: 4 00,00,00,00 + TLV: OUI: 00,0E,CF, SubType: 5, Len: 6 20,87,56,FF,AA,83 + ------------------------------------------------------------------------------- + +To stop the daemon (to avoid sending additional LLDP packets):: + + sudo systemctl stop lldpd.service + sudo systemctl disable lldpd.service + + +SNMPwalk tool for querying SNMP agents +-------------------------------------- +To install ``snmpwalk`` and standard description files on Ubuntu:: + + sudo apt install snmp snmp-mibs-downloader + +Use ``snmpwalk`` to read all info from a device. This is the result when +querying a Siemens Profinet-enabled switch:: + + $ snmpwalk -v2c -c public 192.168.0.99 + iso.3.6.1.2.1.1.1.0 = STRING: "Siemens, SIMATIC NET, SCALANCE X204IRT, 6GK5 204-0BA00-2BA3, HW: Version 9, FW: Version V05.04.02, SVPL6147920" + iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.4196.1.1.5.2.5 + iso.3.6.1.2.1.1.3.0 = Timeticks: (602966) 1:40:29.66 + iso.3.6.1.2.1.1.4.0 = STRING: "sysContact Not Set" + iso.3.6.1.2.1.1.5.0 = STRING: "sysName Not Set" + iso.3.6.1.2.1.1.6.0 = STRING: "sysLocation Not Set" + iso.3.6.1.2.1.1.7.0 = INTEGER: 67 + iso.3.6.1.2.1.2.1.0 = INTEGER: 5 + iso.3.6.1.2.1.2.2.1.1.1 = INTEGER: 1 + iso.3.6.1.2.1.2.2.1.1.2 = INTEGER: 2 + iso.3.6.1.2.1.2.2.1.1.3 = INTEGER: 3 + iso.3.6.1.2.1.2.2.1.1.4 = INTEGER: 4 + iso.3.6.1.2.1.2.2.1.1.1000 = INTEGER: 1000 + iso.3.6.1.2.1.2.2.1.2.1 = STRING: "Siemens, SIMATIC NET, Ethernet Port, X1 P1" + iso.3.6.1.2.1.2.2.1.2.2 = STRING: "Siemens, SIMATIC NET, Ethernet Port, X1 P2" + iso.3.6.1.2.1.2.2.1.2.3 = STRING: "Siemens, SIMATIC NET, Ethernet Port, X1 P3" + iso.3.6.1.2.1.2.2.1.2.4 = STRING: "Siemens, SIMATIC NET, Ethernet Port, X1 P4" + iso.3.6.1.2.1.2.2.1.2.1000 = STRING: "Siemens, SIMATIC NET, internal, X1" + iso.3.6.1.2.1.2.2.1.3.1 = INTEGER: 6 + iso.3.6.1.2.1.2.2.1.3.2 = INTEGER: 6 + iso.3.6.1.2.1.2.2.1.3.3 = INTEGER: 6 + iso.3.6.1.2.1.2.2.1.3.4 = INTEGER: 6 + iso.3.6.1.2.1.2.2.1.3.1000 = INTEGER: 6 + iso.3.6.1.2.1.2.2.1.4.1 = INTEGER: 1500 + iso.3.6.1.2.1.2.2.1.4.2 = INTEGER: 1500 + iso.3.6.1.2.1.2.2.1.4.3 = INTEGER: 1500 + iso.3.6.1.2.1.2.2.1.4.4 = INTEGER: 1500 + iso.3.6.1.2.1.2.2.1.4.1000 = INTEGER: 1500 + iso.3.6.1.2.1.2.2.1.5.1 = Gauge32: 100000000 + iso.3.6.1.2.1.2.2.1.5.2 = Gauge32: 100000000 + iso.3.6.1.2.1.2.2.1.5.3 = Gauge32: 100000000 + iso.3.6.1.2.1.2.2.1.5.4 = Gauge32: 10000000 + iso.3.6.1.2.1.2.2.1.5.1000 = Gauge32: 0 + iso.3.6.1.2.1.2.2.1.6.1 = Hex-STRING: 20 87 56 FF AA 84 + iso.3.6.1.2.1.2.2.1.6.2 = Hex-STRING: 20 87 56 FF AA 85 + iso.3.6.1.2.1.2.2.1.6.3 = Hex-STRING: 20 87 56 FF AA 86 + iso.3.6.1.2.1.2.2.1.6.4 = Hex-STRING: 20 87 56 FF AA 87 + iso.3.6.1.2.1.2.2.1.6.1000 = Hex-STRING: 20 87 56 FF AA 83 + iso.3.6.1.2.1.2.2.1.7.1 = INTEGER: 1 + iso.3.6.1.2.1.2.2.1.7.2 = INTEGER: 1 + (etc) + +Use the command line argument ``-v`` for the SNMP version to use, and ``-c`` +for the "community string" which is a password. +The default community string for devices is often "public". + +The values in the left column are the OID (Object Identifier) values. +For example ``1.3.6.1.2.1.2.2.1.6.3`` is used for the MAC address of the third +interface of the device. It can be interpreted as: + +* 1 = iso +* 3 = identified-organization +* 6 = dod (US department of defence) +* 1 = internet +* 2 = mgmt +* 1 = mib-2 +* 2 = interfaces +* 2 = ifTable +* 1 = ifEntry +* 6 = ifPhysAddress +* 3 = Third interface + +To convert the digits to human readable text, a MIB (Management Information +Base) text file is used. + +The example OID is defined in the ``IF-MIB``, which describes interface +information. The text is from :rfc:`2863`, +and typically installed in ``/var/lib/snmp/mibs/ietf/IF-MIB`` on Linux:: + + ifPhysAddress OBJECT-TYPE + SYNTAX PhysAddress + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "The interface's address at its protocol sub-layer. For + example, for an 802.x interface, this object normally + contains a MAC address. The interface's media-specific MIB + must define the bit and byte ordering and the format of the + value of this object. For interfaces which do not have such + an address (e.g., a serial line), this object should contain + an octet string of zero length." + ::= { ifEntry 6 } + +Add the ``-m ALL`` flag to ``snmpwalk`` to use the installed MIB files. The +example line will then display as:: + + IF-MIB::ifPhysAddress.3 = STRING: 20:87:56:ff:aa:86 + +instead of the previous:: + + iso.3.6.1.2.1.2.2.1.6.3 = Hex-STRING: 20 87 56 FF AA 86 + +To always load specific MIB files, you can add them to a ``.snmp/snmp.conf`` +configuration file in your home directory. For example:: + + mibs +LLDP-MIB + + +Read and write a single OID +--------------------------- +Read only the ``SNMPv2-MIB::sysLocation.0`` OID:: + + $ snmpget -v1 -c public 192.168.0.99 1.3.6.1.2.1.1.6.0 + iso.3.6.1.2.1.1.6.0 = STRING: "sysLocation Not Set" + +Write a new string describing the device location:: + + $ snmpset -v1 -c private 192.168.0.99 1.3.6.1.2.1.1.6.0 s "My new location" + + +Using more MIB files for snmpwalk +--------------------------------- +Add any additional MIB files in the directory ``.snmp/mibs/`` in your home +directory. For Profinet you need at least these files: + +* IEC-62439-2-MGMT-MIB-20140509.mib +* IEC-62439-2-MON-MIB-20140509.mib +* LLDP-EXT-IEC61158-TYPE10-MIB.mib +* LLDP-MIB.my + +The first three files are available in the test bundle (in subdirectory +``PN-Spec``) from the Profinet organization. + +The last file is available for download on several locations. + + +Important SNMP subtrees and MIB files +------------------------------------- +The OID values are hierarchical (arranged in a tree). + ++------------------------------+-------------------------------------------------------------+ +| OID subtree | Name | ++==============================+=============================================================+ +| 1.0.8802.1.1.2 | LLDP-MIB::lldpMIB | ++------------------------------+-------------------------------------------------------------+ +| 1.0.62439.1 | IEC-62439-2-MIB::mrp | ++------------------------------+-------------------------------------------------------------+ +| 1.3.6.1.2.1 | SNMPv2-SMI::mib-2 (default for snmpwalk) | ++------------------------------+-------------------------------------------------------------+ +| 1.3.6.1.4.1.4196.1.1.5.2.100 | SNMPv2-SMI::enterprises.4196.1.1.5.2.100 (Siemens Scalance) | ++------------------------------+-------------------------------------------------------------+ +| 1.3.6.1.4.1.4329.6 | SNMPv2-SMI::enterprises.4329.6 | ++------------------------------+-------------------------------------------------------------+ +| 1.3.6.1.6 | SNMPv2-SMI::snmpV2 | ++------------------------------+-------------------------------------------------------------+ + +To convert a numerical OID to a human readable string:: + + $ snmptranslate -m ALL 1.0.8802.1.1.2.1 + LLDP-MIB::lldpObjects + +Note that a MIB file can insert entries inside existing subtrees. + +Important Profinet-related MIB files: + ++---------------------------+-----------------------------------------------------+ +| MIB file | Description | ++===========================+=====================================================+ +| BRIDGE-MIB | For MAC-layer bridges | ++---------------------------+-----------------------------------------------------+ +| DISMAN-EXPRESSION-MIB | Distributed Management | ++---------------------------+-----------------------------------------------------+ +| IEC-62439-2-MIB | Media Redundancy Protocol | ++---------------------------+-----------------------------------------------------+ +| IF-MIB | Interfaces MIB (rfc 2863) | ++---------------------------+-----------------------------------------------------+ +| LLDP-EXT-PNO-MIB | Profinet extension of LLDP-MIB | ++---------------------------+-----------------------------------------------------+ +| LLDP-MIB | LLDP MIB | ++---------------------------+-----------------------------------------------------+ +| RFC1213-MIB | MIB-2 | ++---------------------------+-----------------------------------------------------+ +| SNMP-USER-BASED-SM-MIB | SNMP User-based Security Model (RFC 3414) | ++---------------------------+-----------------------------------------------------+ +| SNMP-VIEW-BASED-ACM-MIB | View-based Access Control Model for SNMP | ++---------------------------+-----------------------------------------------------+ +| SNMP-COMMUNITY-MIB | Coexistence between SNMP v1, v2c, v3 (RFC 2576) | ++---------------------------+-----------------------------------------------------+ +| SNMP-PROXY-MIB | Parameter config of proxy forwarding (RFC 3413) | ++---------------------------+-----------------------------------------------------+ +| SNMP-NOTIFICATION-MIB | Logging SNMP Notifications (RFC 3014) | ++---------------------------+-----------------------------------------------------+ +| SNMP-TARGET-MIB | ? (RFC 3413) | ++---------------------------+-----------------------------------------------------+ +| SNMP-FRAMEWORK-MIB | ? (RFC 3411) | ++---------------------------+-----------------------------------------------------+ +| SNMPv2-MIB | | ++---------------------------+-----------------------------------------------------+ +| SNMPv2-SMI | | ++---------------------------+-----------------------------------------------------+ + +Other Profinet-relevant MIB files on Linux: + +* HOST-RESOURCES-MIB +* IP-FORWARD-MIB +* IP-MIB +* NET-SNMP-AGENT-MIB +* UDP-MIB + + +Walking a subtree using snmpwalk +-------------------------------- +By default the ``snmpwalk`` searches the ``SNMPv2-SMI::mib-2`` subtree. You can +search a smaller or larger subtree by giving the OID (or the corresponding +name) to ``snmpwalk`` as the argument after the host argument. + +As an example, here are the number of found variables for different +subtrees for a Siemens Profinet switch: + ++---------------------+------------------------------------------+-----------------+ +| OID Subtree | Subtree name | Found variables | ++=====================+==========================================+=================+ +| 1.3.6.1.2.1.2.2.1.6 | IF-MIB::ifPhysAddress | 5 | ++---------------------+------------------------------------------+-----------------+ +| 1.3.6.1.2.1.2.2.1 | IF-MIB::ifEntry | 110 | ++---------------------+------------------------------------------+-----------------+ +| 1.3.6.1.2.1.2.2 | IF-MIB::ifTable | 110 | ++---------------------+------------------------------------------+-----------------+ +| 1.3.6.1.2.1.2 | IF-MIB::interfaces | 110 | ++---------------------+------------------------------------------+-----------------+ +| 1.3.6.1.2.1 | SNMPv2-SMI::mib-2 (default for snmpwalk) | 408 | ++---------------------+------------------------------------------+-----------------+ +| 1.3.6.1.2 | SNMPv2-SMI::mgmt | 408 | ++---------------------+------------------------------------------+-----------------+ +| 1.3.6.1 | SNMPv2-SMI::internet | 2397 | ++---------------------+------------------------------------------+-----------------+ +| 1.3.6 | SNMPv2-SMI::dod | 2397 | ++---------------------+------------------------------------------+-----------------+ +| 1.3 | SNMPv2-SMI::org | 2397 | ++---------------------+------------------------------------------+-----------------+ +| 1 | iso | 2669 | ++---------------------+------------------------------------------+-----------------+ + +In order to show all available variables when running ``snmpwalk``, use the +OID ``1``. However, in this example this would result in displaying over 2600 +variables. + +To show relevant data only, you typically need to specify a subtree. +This is done by giving the OID value or corresponding name, for example +``1.0.8802.1.1.2`` or ``LLDP-MIB::lldpMIB``. + + +Showing SNMP info in tabular format +----------------------------------- +Some of the OIDs are part of tables, for example the OID ``1.3.6.1.2.1.2.2.1.2`` +which is the ``ifDescr`` (descriptive text for Ethernet interface). +It is part of the table ``IF-MIB::ifTable`` which uses the ``ifIndex`` +(interface index) as the table index, and the ``ifIndex`` is appended to the +end of the OID. Thus: + +* 1.3.6.1.2.1.2.2.1.2.1 ``ifDescr`` for first interface (actually a port in Profinet naming) +* 1.3.6.1.2.1.2.2.1.2.2 ``ifDescr`` for second interface +* 1.3.6.1.2.1.2.2.1.2.3 ``ifDescr`` for third interface + +This SNMP table can also be displayed in a tabular format on the command +line (the symbolic name must be given):: + + snmptable -v1 -c public 192.168.0.50 IF-MIB::ifTable + +Add ``-Cw 120`` to the command line to limit the ``snmptable`` output line length. + +Relevant tables: + ++-----------------------------------------+--------------------------------+ +| Table name | Top OID | ++=========================================+================================+ +| IF-MIB::ifTable | 1.3.6.1.2.1.2.2 | ++-----------------------------------------+--------------------------------+ +| LLDP-MIB::lldpConfigManAddrTable | 1.0.8802.1.1.2.1.1.7 | ++-----------------------------------------+--------------------------------+ +| LLDP-MIB::lldpLocPortTable | 1.0.8802.1.1.2.1.3.7 | ++-----------------------------------------+--------------------------------+ +| LLDP-MIB::lldpLocManAddrTable | 1.0.8802.1.1.2.1.3.8 | ++-----------------------------------------+--------------------------------+ +| LLDP-MIB::lldpRemTable | 1.0.8802.1.1.2.1.4.1 | ++-----------------------------------------+--------------------------------+ +| LLDP-MIB::lldpRemManAddrTable | 1.0.8802.1.1.2.1.4.2 | ++-----------------------------------------+--------------------------------+ +| LLDP-EXT-PNO-MIB::lldpXPnoLocTable | 1.0.8802.1.1.2.1.5.3791.1.2.1 | ++-----------------------------------------+--------------------------------+ +| LLDP-EXT-PNO-MIB::lldpXPnoRemTable | 1.0.8802.1.1.2.1.5.3791.1.3.1 | ++-----------------------------------------+--------------------------------+ + + +Supported SNMP variables for Profinet +------------------------------------- + +Device details: + ++--------------------------------+-----------------------------------------------------------------+ +| Field name | Description | ++================================+=================================================================+ +| sysDescr | String, max 255 char. Consists of product name, serial number, | +| | hardware and software versions etc. | +| | Also used as SystemIdentification = ChassisID ? | ++--------------------------------+-----------------------------------------------------------------+ +| sysObjectId | An OID with enterprise info. Use 1.3.6.1.4.1.24686 for Profinet | ++--------------------------------+-----------------------------------------------------------------+ +| sysUpTime | Uptime in 1/100 seconds | ++--------------------------------+-----------------------------------------------------------------+ +| sysContact | String, max 255 char. Writable. Contact info to a person. | ++--------------------------------+-----------------------------------------------------------------+ +| sysName | String, writable. Fully qualified domain name? | +| | Here limited to PNAL_HOSTNAME_MAX_SIZE (typically 64 char). | ++--------------------------------+-----------------------------------------------------------------+ +| sysLocation | String, writable. Here same as I&M1 location (max 22 char). | ++--------------------------------+-----------------------------------------------------------------+ +| sysServices | 78 (dec) for Profinet devices | ++--------------------------------+-----------------------------------------------------------------+ +| lldpConfigManAddrPortsTxEnable | On/off for ports. Writable? | ++--------------------------------+-----------------------------------------------------------------+ + +Interface statistics, for each port: + ++----------------+-----------------------------------------------------------------+ +| Field name | Description | ++================+=================================================================+ +| ifIndex | | ++----------------+-----------------------------------------------------------------+ +| ifDescr | String, max 255 char. Unique within device. See lldpLocPortDesc | ++----------------+-----------------------------------------------------------------+ +| ifType | Typically 6 = Ethernet | ++----------------+-----------------------------------------------------------------+ +| ifMtu | Max bytes per packet. Often 1500. | ++----------------+-----------------------------------------------------------------+ +| ifSpeed | Bits/s. uint32. Often 100000000. | ++----------------+-----------------------------------------------------------------+ +| ifPhysAddress | MAC address | ++----------------+-----------------------------------------------------------------+ +| ifAdminStatus | Up, down etc. Writable? | ++----------------+-----------------------------------------------------------------+ +| ifOperStatus | Up, down etc. | ++----------------+-----------------------------------------------------------------+ +| ifInOctets | Input bytes. uint32 | ++----------------+-----------------------------------------------------------------+ +| ifInDiscards | uint32 | ++----------------+-----------------------------------------------------------------+ +| ifInErrors | uint32 | ++----------------+-----------------------------------------------------------------+ +| ifOutOctets | Output bytes. uint32 | ++----------------+-----------------------------------------------------------------+ +| ifOutDiscards | uint32 | ++----------------+-----------------------------------------------------------------+ +| ifOutErrors | uint32 | ++----------------+-----------------------------------------------------------------+ + +Readable fields related to ports and interfaces: + ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| Field | Description | Local interface | Local port | Remote port | ++======================+======================================+=================+============+=============+ +| ChassisId | Chassis ID (same for all interfaces) | x | | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| TimeMark | Timestamp for latest LLDP frame | | | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| LocalPortNum | Port number | | x | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| PortId | String, max 14 char. | | x | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| PortIdSubtype | Typically 7 = Locally assigned | | x | o | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| PortDesc | String, max 255 char. See ifDescr. | | x | | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| Index | | | | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| ManAddrSubtype | Typically 1=IP | x | | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| ManAddr | Management (IP) address | x | | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| ManAddrIfId | See ifIndex | x | | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| LPDValue | Propagation delay in ns. uint32 | | x | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| PortTxDValue | Transmission delay in ns. uint32 | | x | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| PortRxDValue | Reception delay in ns. uint32 | | x | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| NoS | Station name (or MAC address as | x | | x | +| | string) String | | | | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| AutoNegSupported | Autonegotiation supported. Bool. | | x | o | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| AutoNegEnabled | Autonegotiation enabled. Bool. | | x | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| AutoNegAdvertisedCap | | | o | o | ++----------------------+--------------------------------------+-----------------+------------+-------------+ +| OperMauType | MAU type | | x | x | ++----------------------+--------------------------------------+-----------------+------------+-------------+ + +Note that some objects are listed "Not accessible" in the standard. These +are read indirectly via other objects (used as table index, and appended +to the OID of other objects), so the information must thus be available. + +See the Profinet standard for the corresponding numerical OID values. + +Enable SNMP when running p-net on Linux +--------------------------------------- +See the page "Additional Linux details" in this documentation. + + +Verification of SNMP communication to p-net +------------------------------------------- +To verify the SNMP capabilities of the p-net stack, first use ``ping`` to +make sure you have a connection to the device, and then use ``snmpwalk``:: + + ping 192.168.0.50 + snmpwalk -v1 -c public 192.168.0.50 1 + snmpget -v1 -c public 192.168.0.50 1.3.6.1.2.1.1.4.0 + snmpset -v1 -c private 192.168.0.50 1.3.6.1.2.1.1.4.0 s "My new sys contact" + +If you enable debug logging in the p-net stack, the two last commands will +cause entries in the p-net log. + +This will only be answered if the agent is Profinet-enabled:: + + snmpget -v1 -c public 192.168.0.50 1.0.8802.1.1.2.1.5.3791.1.2.1.1.1.1 + +The ``sysObjectId`` should be reported as ``iso.3.6.1.4.1.24686`` if the +agent is Profinet-enabled:: + + snmpget -v1 -c public 192.168.0.50 1.3.6.1.2.1.1.2.0 + + +Verification of multiport SNMP functionality for p-net +------------------------------------------------------ +Ask the device about its neighbours on the different ports by running:: + + snmpwalk -v1 -c public 192.168.0.50 1.0.8802.1.1.2.1.4.1 + +This will use the LldpRemTable to show peer ChassisID and peer PortId for the +different local ports. +Connect some other LLDP-enabled device (Profinet or not) to one of the ports, +and verify that it will show up in the results. + +A while after disconnecting the device (controlled by the TTL value) +the entry will be removed from the list. +Verify this behavior for all ports on your device. + + +Siemens PRONETA - Profinet Network Analysis tool +------------------------------------------------ +The Proneta tool can scan the network to discover the topology of connected +Profinet equipment. + +To install Proneta on Windows, use the "Download Proneta Basic free" link +on the Siemens web page: +https://new.siemens.com/global/en/products/automation/industrial-communication/profinet/proneta.html +The file is named ``proneta_VERSION.zip``. Registration is required. +Unzip the downloaded file, and double-click the ``PRONETA.exe`` file to start the program. + +Turn off the ART tester tool before starting Proneta, otherwise Proneta can +not scan the network. + +On the settings page, select which Ethernet network adapter to use +on your computer. +On the home screen, select "Network analysis" and use the "Online" tab. +Click the "Refresh" icon to scan the network topology. +A graphical view with all Profinet equipment will be shown, including +the connections between all ports. An example screenshot is shown below. + +.. image:: illustrations/proneta_example.png + +For the Proneta tool to be able to fully map the network, most of the nodes +must have valid IP addresses (and have SNMP enabled). + +The program will show details about interfaces and ports, and which of the slots +that are populated by modules. +It will also show the list of current diagnosis for IO-devices. +Proneta will read out the diagnostics buffer every 5 seconds, as long as the +device is selected in the graphical view. +In order to display the diagnostics buffer the IP address of the device must be +set. Otherwise the message "Diagnostics buffer not supported" will be shown. + +By right-clicking on a device in the graphical view, you can set the +station name and IP address (temporarily or permanently). +It is also possible to edit the I&M information, to flash the signal LED and +to do a factory reset. + +For PRONETA to be able to map the connection to the closest device, you need to +enable LLDP transmission on the Ethernet interface of your laptop. Note that +this interferes with the ART tester software. See the section on installing +ART tester in this documentation. + +Except from sending the "DCP identify", Proneta will scan the subnet with +PING and ARP messages if enabled in the settings. +It is possible to customise which address range to scan. +Found DCP-enabled hosts will be probed using EPM lookup and SNMP queries. + +Proneta will show details about modules in found devices, if it has access to the +corresponding GSDML file. Use the page "Settings" and the tab "GSDML Manager" to +load GSDML files. + + +Full SNMP readout example +------------------------- +Here is more data from the Siemens switch, when using the MIBs:: + + $ snmpwalk -v2c -c public -m ALL 192.168.0.99 + SNMPv2-MIB::sysDescr.0 = STRING: Siemens, SIMATIC NET, SCALANCE X204IRT, 6GK5 204-0BA00-2BA3, HW: Version 9, FW: Version V05.04.02, SVPL6147920 + SNMPv2-MIB::sysObjectID.0 = OID: SNMPv2-SMI::enterprises.4196.1.1.5.2.5 + DISMAN-EXPRESSION-MIB::sysUpTimeInstance = Timeticks: (756575) 2:06:05.75 + SNMPv2-MIB::sysContact.0 = STRING: sysContact Not Set + SNMPv2-MIB::sysName.0 = STRING: sysName Not Set + SNMPv2-MIB::sysLocation.0 = STRING: sysLocation Not Set + SNMPv2-MIB::sysServices.0 = INTEGER: 67 + IF-MIB::ifNumber.0 = INTEGER: 5 + IF-MIB::ifIndex.1 = INTEGER: 1 + IF-MIB::ifIndex.2 = INTEGER: 2 + IF-MIB::ifIndex.3 = INTEGER: 3 + IF-MIB::ifIndex.4 = INTEGER: 4 + IF-MIB::ifIndex.1000 = INTEGER: 1000 + IF-MIB::ifDescr.1 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P1 + IF-MIB::ifDescr.2 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P2 + IF-MIB::ifDescr.3 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P3 + IF-MIB::ifDescr.4 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P4 + IF-MIB::ifDescr.1000 = STRING: Siemens, SIMATIC NET, internal, X1 + IF-MIB::ifType.1 = INTEGER: ethernetCsmacd(6) + IF-MIB::ifType.2 = INTEGER: ethernetCsmacd(6) + IF-MIB::ifType.3 = INTEGER: ethernetCsmacd(6) + IF-MIB::ifType.4 = INTEGER: ethernetCsmacd(6) + IF-MIB::ifType.1000 = INTEGER: ethernetCsmacd(6) + IF-MIB::ifMtu.1 = INTEGER: 1500 + IF-MIB::ifMtu.2 = INTEGER: 1500 + IF-MIB::ifMtu.3 = INTEGER: 1500 + IF-MIB::ifMtu.4 = INTEGER: 1500 + IF-MIB::ifMtu.1000 = INTEGER: 1500 + IF-MIB::ifSpeed.1 = Gauge32: 10000000 + IF-MIB::ifSpeed.2 = Gauge32: 100000000 + IF-MIB::ifSpeed.3 = Gauge32: 100000000 + IF-MIB::ifSpeed.4 = Gauge32: 10000000 + IF-MIB::ifSpeed.1000 = Gauge32: 0 + IF-MIB::ifPhysAddress.1 = STRING: 20:87:56:ff:aa:84 + IF-MIB::ifPhysAddress.2 = STRING: 20:87:56:ff:aa:85 + IF-MIB::ifPhysAddress.3 = STRING: 20:87:56:ff:aa:86 + IF-MIB::ifPhysAddress.4 = STRING: 20:87:56:ff:aa:87 + IF-MIB::ifPhysAddress.1000 = STRING: 20:87:56:ff:aa:83 + IF-MIB::ifAdminStatus.1 = INTEGER: up(1) + IF-MIB::ifAdminStatus.2 = INTEGER: up(1) + IF-MIB::ifAdminStatus.3 = INTEGER: up(1) + IF-MIB::ifAdminStatus.4 = INTEGER: up(1) + IF-MIB::ifAdminStatus.1000 = INTEGER: up(1) + IF-MIB::ifOperStatus.1 = INTEGER: up(1) + IF-MIB::ifOperStatus.2 = INTEGER: up(1) + IF-MIB::ifOperStatus.3 = INTEGER: up(1) + IF-MIB::ifOperStatus.4 = INTEGER: down(2) + IF-MIB::ifOperStatus.1000 = INTEGER: up(1) + IF-MIB::ifLastChange.1 = Timeticks: (616788) 1:42:47.88 + IF-MIB::ifLastChange.2 = Timeticks: (436359) 1:12:43.59 + IF-MIB::ifLastChange.3 = Timeticks: (1094) 0:00:10.94 + IF-MIB::ifLastChange.4 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifLastChange.1000 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifInOctets.1 = Counter32: 908950 + IF-MIB::ifInOctets.2 = Counter32: 214915 + IF-MIB::ifInOctets.3 = Counter32: 576327 + IF-MIB::ifInOctets.4 = Counter32: 0 + IF-MIB::ifInOctets.1000 = Counter32: 0 + IF-MIB::ifInUcastPkts.1 = Counter32: 911 + IF-MIB::ifInUcastPkts.2 = Counter32: 308 + IF-MIB::ifInUcastPkts.3 = Counter32: 6185 + IF-MIB::ifInUcastPkts.4 = Counter32: 0 + IF-MIB::ifInUcastPkts.1000 = Counter32: 0 + IF-MIB::ifInNUcastPkts.1 = Counter32: 7840 + IF-MIB::ifInNUcastPkts.2 = Counter32: 1521 + IF-MIB::ifInNUcastPkts.3 = Counter32: 99 + IF-MIB::ifInNUcastPkts.4 = Counter32: 0 + IF-MIB::ifInNUcastPkts.1000 = Counter32: 0 + IF-MIB::ifInDiscards.1 = Counter32: 0 + IF-MIB::ifInDiscards.2 = Counter32: 0 + IF-MIB::ifInDiscards.3 = Counter32: 0 + IF-MIB::ifInDiscards.4 = Counter32: 0 + IF-MIB::ifInDiscards.1000 = Counter32: 0 + IF-MIB::ifInErrors.1 = Counter32: 0 + IF-MIB::ifInErrors.2 = Counter32: 0 + IF-MIB::ifInErrors.3 = Counter32: 0 + IF-MIB::ifInErrors.4 = Counter32: 0 + IF-MIB::ifInErrors.1000 = Counter32: 0 + IF-MIB::ifInUnknownProtos.1 = Counter32: 492 + IF-MIB::ifInUnknownProtos.2 = Counter32: 19 + IF-MIB::ifInUnknownProtos.3 = Counter32: 21 + IF-MIB::ifInUnknownProtos.4 = Counter32: 0 + IF-MIB::ifInUnknownProtos.1000 = Counter32: 0 + IF-MIB::ifOutOctets.1 = Counter32: 1516306 + IF-MIB::ifOutOctets.2 = Counter32: 596825 + IF-MIB::ifOutOctets.3 = Counter32: 2244836 + IF-MIB::ifOutOctets.4 = Counter32: 0 + IF-MIB::ifOutOctets.1000 = Counter32: 0 + IF-MIB::ifOutUcastPkts.1 = Counter32: 5835 + IF-MIB::ifOutUcastPkts.2 = Counter32: 385 + IF-MIB::ifOutUcastPkts.3 = Counter32: 6649 + IF-MIB::ifOutUcastPkts.4 = Counter32: 0 + IF-MIB::ifOutUcastPkts.1000 = Counter32: 0 + IF-MIB::ifOutNUcastPkts.1 = Counter32: 7470 + IF-MIB::ifOutNUcastPkts.2 = Counter32: 5352 + IF-MIB::ifOutNUcastPkts.3 = Counter32: 15626 + IF-MIB::ifOutNUcastPkts.4 = Counter32: 0 + IF-MIB::ifOutNUcastPkts.1000 = Counter32: 0 + IF-MIB::ifOutDiscards.1 = Counter32: 0 + IF-MIB::ifOutDiscards.2 = Counter32: 0 + IF-MIB::ifOutDiscards.3 = Counter32: 0 + IF-MIB::ifOutDiscards.4 = Counter32: 0 + IF-MIB::ifOutDiscards.1000 = Counter32: 0 + IF-MIB::ifOutErrors.1 = Counter32: 0 + IF-MIB::ifOutErrors.2 = Counter32: 0 + IF-MIB::ifOutErrors.3 = Counter32: 0 + IF-MIB::ifOutErrors.4 = Counter32: 0 + IF-MIB::ifOutErrors.1000 = Counter32: 0 + IF-MIB::ifOutQLen.1 = Gauge32: 0 + IF-MIB::ifOutQLen.2 = Gauge32: 0 + IF-MIB::ifOutQLen.3 = Gauge32: 0 + IF-MIB::ifOutQLen.4 = Gauge32: 0 + IF-MIB::ifOutQLen.1000 = Gauge32: 15 + IF-MIB::ifSpecific.1 = OID: SNMPv2-SMI::zeroDotZero + IF-MIB::ifSpecific.2 = OID: SNMPv2-SMI::zeroDotZero + IF-MIB::ifSpecific.3 = OID: SNMPv2-SMI::zeroDotZero + IF-MIB::ifSpecific.4 = OID: SNMPv2-SMI::zeroDotZero + IF-MIB::ifSpecific.1000 = OID: SNMPv2-SMI::zeroDotZero + RFC1213-MIB::ipForwarding.0 = INTEGER: not-forwarding(2) + RFC1213-MIB::ipDefaultTTL.0 = INTEGER: 64 + RFC1213-MIB::ipInReceives.0 = Counter32: 13017 + RFC1213-MIB::ipInHdrErrors.0 = Counter32: 0 + RFC1213-MIB::ipInAddrErrors.0 = Counter32: 0 + RFC1213-MIB::ipForwDatagrams.0 = Counter32: 0 + RFC1213-MIB::ipInUnknownProtos.0 = Counter32: 0 + RFC1213-MIB::ipInDiscards.0 = Counter32: 0 + RFC1213-MIB::ipInDelivers.0 = Counter32: 13023 + RFC1213-MIB::ipOutRequests.0 = Counter32: 13014 + RFC1213-MIB::ipOutDiscards.0 = Counter32: 0 + RFC1213-MIB::ipOutNoRoutes.0 = Counter32: 0 + RFC1213-MIB::ipReasmTimeout.0 = INTEGER: 60 + RFC1213-MIB::ipReasmReqds.0 = Counter32: 0 + RFC1213-MIB::ipReasmOKs.0 = Counter32: 0 + RFC1213-MIB::ipReasmFails.0 = Counter32: 0 + RFC1213-MIB::ipFragOKs.0 = Counter32: 0 + RFC1213-MIB::ipFragFails.0 = Counter32: 0 + RFC1213-MIB::ipFragCreates.0 = Counter32: 0 + RFC1213-MIB::ipAdEntAddr.192.168.0.99 = IpAddress: 192.168.0.99 + RFC1213-MIB::ipAdEntIfIndex.192.168.0.99 = INTEGER: 1000 + RFC1213-MIB::ipAdEntNetMask.192.168.0.99 = IpAddress: 255.255.255.0 + RFC1213-MIB::ipAdEntBcastAddr.192.168.0.99 = INTEGER: 1 + RFC1213-MIB::ipAdEntReasmMaxSize.192.168.0.99 = INTEGER: 65535 + RFC1213-MIB::ipRouteDest.127.0.0.1 = IpAddress: 127.0.0.1 + RFC1213-MIB::ipRouteDest.192.168.0.0 = IpAddress: 192.168.0.0 + RFC1213-MIB::ipRouteIfIndex.127.0.0.1 = INTEGER: 65535 + RFC1213-MIB::ipRouteIfIndex.192.168.0.0 = INTEGER: 65535 + RFC1213-MIB::ipRouteMetric1.127.0.0.1 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric1.192.168.0.0 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric2.127.0.0.1 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric2.192.168.0.0 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric3.127.0.0.1 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric3.192.168.0.0 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric4.127.0.0.1 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric4.192.168.0.0 = INTEGER: 0 + RFC1213-MIB::ipRouteNextHop.127.0.0.1 = IpAddress: 127.0.0.1 + RFC1213-MIB::ipRouteNextHop.192.168.0.0 = IpAddress: 192.168.0.99 + RFC1213-MIB::ipRouteType.127.0.0.1 = INTEGER: direct(3) + RFC1213-MIB::ipRouteType.192.168.0.0 = INTEGER: direct(3) + RFC1213-MIB::ipRouteProto.127.0.0.1 = INTEGER: local(2) + RFC1213-MIB::ipRouteProto.192.168.0.0 = INTEGER: local(2) + RFC1213-MIB::ipRouteAge.127.0.0.1 = INTEGER: 7563 + RFC1213-MIB::ipRouteAge.192.168.0.0 = INTEGER: 7562 + RFC1213-MIB::ipRouteMask.127.0.0.1 = IpAddress: 255.255.255.255 + RFC1213-MIB::ipRouteMask.192.168.0.0 = IpAddress: 255.255.255.0 + RFC1213-MIB::ipRouteMetric5.127.0.0.1 = INTEGER: 0 + RFC1213-MIB::ipRouteMetric5.192.168.0.0 = INTEGER: 0 + RFC1213-MIB::ipRouteInfo.127.0.0.1 = OID: SNMPv2-SMI::zeroDotZero + RFC1213-MIB::ipRouteInfo.192.168.0.0 = OID: SNMPv2-SMI::zeroDotZero + RFC1213-MIB::ipNetToMediaIfIndex.8.192.168.0.25 = INTEGER: 8 + RFC1213-MIB::ipNetToMediaIfIndex.8.192.168.0.50 = INTEGER: 8 + RFC1213-MIB::ipNetToMediaPhysAddress.8.192.168.0.25 = Hex-STRING: 1C 39 47 CD D4 EB + RFC1213-MIB::ipNetToMediaPhysAddress.8.192.168.0.50 = Hex-STRING: 54 EE 75 FF 95 A6 + RFC1213-MIB::ipNetToMediaNetAddress.8.192.168.0.25 = IpAddress: 192.168.0.25 + RFC1213-MIB::ipNetToMediaNetAddress.8.192.168.0.50 = IpAddress: 192.168.0.50 + RFC1213-MIB::ipNetToMediaType.8.192.168.0.25 = INTEGER: invalid(2) + RFC1213-MIB::ipNetToMediaType.8.192.168.0.50 = INTEGER: dynamic(3) + RFC1213-MIB::ipRoutingDiscards.0 = Counter32: 0 + RFC1213-MIB::icmpInMsgs.0 = Counter32: 0 + RFC1213-MIB::icmpInErrors.0 = Counter32: 0 + RFC1213-MIB::icmpInDestUnreachs.0 = Counter32: 0 + RFC1213-MIB::icmpInTimeExcds.0 = Counter32: 0 + RFC1213-MIB::icmpInParmProbs.0 = Counter32: 0 + RFC1213-MIB::icmpInSrcQuenchs.0 = Counter32: 0 + RFC1213-MIB::icmpInRedirects.0 = Counter32: 0 + RFC1213-MIB::icmpInEchos.0 = Counter32: 0 + RFC1213-MIB::icmpInEchoReps.0 = Counter32: 0 + RFC1213-MIB::icmpInTimestamps.0 = Counter32: 0 + RFC1213-MIB::icmpInTimestampReps.0 = Counter32: 0 + RFC1213-MIB::icmpInAddrMasks.0 = Counter32: 0 + RFC1213-MIB::icmpInAddrMaskReps.0 = Counter32: 0 + RFC1213-MIB::icmpOutMsgs.0 = Counter32: 0 + RFC1213-MIB::icmpOutErrors.0 = Counter32: 0 + RFC1213-MIB::icmpOutDestUnreachs.0 = Counter32: 0 + RFC1213-MIB::icmpOutTimeExcds.0 = Counter32: 0 + RFC1213-MIB::icmpOutParmProbs.0 = Counter32: 0 + RFC1213-MIB::icmpOutSrcQuenchs.0 = Counter32: 0 + RFC1213-MIB::icmpOutRedirects.0 = Counter32: 0 + RFC1213-MIB::icmpOutEchos.0 = Counter32: 0 + RFC1213-MIB::icmpOutEchoReps.0 = Counter32: 0 + RFC1213-MIB::icmpOutTimestamps.0 = Counter32: 0 + RFC1213-MIB::icmpOutTimestampReps.0 = Counter32: 0 + RFC1213-MIB::icmpOutAddrMasks.0 = Counter32: 0 + RFC1213-MIB::icmpOutAddrMaskReps.0 = Counter32: 0 + RFC1213-MIB::tcpRtoAlgorithm.0 = INTEGER: vanj(4) + RFC1213-MIB::tcpRtoMin.0 = INTEGER: 50000 + RFC1213-MIB::tcpRtoMax.0 = INTEGER: 3200000 + RFC1213-MIB::tcpMaxConn.0 = INTEGER: -1 + RFC1213-MIB::tcpActiveOpens.0 = Counter32: 0 + RFC1213-MIB::tcpPassiveOpens.0 = Counter32: 0 + RFC1213-MIB::tcpAttemptFails.0 = Counter32: 0 + RFC1213-MIB::tcpEstabResets.0 = Counter32: 0 + RFC1213-MIB::tcpCurrEstab.0 = Gauge32: 0 + RFC1213-MIB::tcpInSegs.0 = Counter32: 0 + RFC1213-MIB::tcpOutSegs.0 = Counter32: 0 + RFC1213-MIB::tcpRetransSegs.0 = Counter32: 0 + RFC1213-MIB::tcpConnState.0.0.0.0.22.0.0.0.0.0 = INTEGER: listen(2) + RFC1213-MIB::tcpConnState.0.0.0.0.23.0.0.0.0.0 = INTEGER: listen(2) + RFC1213-MIB::tcpConnState.0.0.0.0.80.0.0.0.0.0 = INTEGER: listen(2) + RFC1213-MIB::tcpConnState.0.0.0.0.84.0.0.0.0.0 = INTEGER: listen(2) + RFC1213-MIB::tcpConnState.0.0.0.0.443.0.0.0.0.0 = INTEGER: listen(2) + RFC1213-MIB::tcpConnLocalAddress.0.0.0.0.22.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnLocalAddress.0.0.0.0.23.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnLocalAddress.0.0.0.0.80.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnLocalAddress.0.0.0.0.84.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnLocalAddress.0.0.0.0.443.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnLocalPort.0.0.0.0.22.0.0.0.0.0 = INTEGER: 22 + RFC1213-MIB::tcpConnLocalPort.0.0.0.0.23.0.0.0.0.0 = INTEGER: 23 + RFC1213-MIB::tcpConnLocalPort.0.0.0.0.80.0.0.0.0.0 = INTEGER: 80 + RFC1213-MIB::tcpConnLocalPort.0.0.0.0.84.0.0.0.0.0 = INTEGER: 84 + RFC1213-MIB::tcpConnLocalPort.0.0.0.0.443.0.0.0.0.0 = INTEGER: 443 + RFC1213-MIB::tcpConnRemAddress.0.0.0.0.22.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnRemAddress.0.0.0.0.23.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnRemAddress.0.0.0.0.80.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnRemAddress.0.0.0.0.84.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnRemAddress.0.0.0.0.443.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::tcpConnRemPort.0.0.0.0.22.0.0.0.0.0 = INTEGER: 0 + RFC1213-MIB::tcpConnRemPort.0.0.0.0.23.0.0.0.0.0 = INTEGER: 0 + RFC1213-MIB::tcpConnRemPort.0.0.0.0.80.0.0.0.0.0 = INTEGER: 0 + RFC1213-MIB::tcpConnRemPort.0.0.0.0.84.0.0.0.0.0 = INTEGER: 0 + RFC1213-MIB::tcpConnRemPort.0.0.0.0.443.0.0.0.0.0 = INTEGER: 0 + RFC1213-MIB::tcpInErrs.0 = Counter32: 0 + RFC1213-MIB::tcpOutRsts.0 = Counter32: 0 + RFC1213-MIB::udpInDatagrams.0 = Counter32: 13139 + RFC1213-MIB::udpNoPorts.0 = Counter32: 0 + RFC1213-MIB::udpInErrors.0 = Counter32: 0 + RFC1213-MIB::udpOutDatagrams.0 = Counter32: 13132 + RFC1213-MIB::udpLocalAddress.0.0.0.0.0 = IpAddress: 0.0.0.0 + RFC1213-MIB::udpLocalAddress.0.0.0.0.68 = IpAddress: 0.0.0.0 + RFC1213-MIB::udpLocalAddress.0.0.0.0.161 = IpAddress: 0.0.0.0 + RFC1213-MIB::udpLocalAddress.0.0.0.0.34964 = IpAddress: 0.0.0.0 + RFC1213-MIB::udpLocalAddress.0.0.0.0.49152 = IpAddress: 0.0.0.0 + RFC1213-MIB::udpLocalAddress.0.0.0.0.49153 = IpAddress: 0.0.0.0 + RFC1213-MIB::udpLocalAddress.127.0.0.1.12345 = IpAddress: 127.0.0.1 + RFC1213-MIB::udpLocalAddress.127.0.0.1.12346 = IpAddress: 127.0.0.1 + RFC1213-MIB::udpLocalPort.0.0.0.0.0 = INTEGER: 0 + RFC1213-MIB::udpLocalPort.0.0.0.0.68 = INTEGER: 68 + RFC1213-MIB::udpLocalPort.0.0.0.0.161 = INTEGER: 161 + RFC1213-MIB::udpLocalPort.0.0.0.0.34964 = INTEGER: 34964 + RFC1213-MIB::udpLocalPort.0.0.0.0.49152 = INTEGER: 49152 + RFC1213-MIB::udpLocalPort.0.0.0.0.49153 = INTEGER: 49153 + RFC1213-MIB::udpLocalPort.127.0.0.1.12345 = INTEGER: 12345 + RFC1213-MIB::udpLocalPort.127.0.0.1.12346 = INTEGER: 12346 + SNMPv2-MIB::snmpInPkts.0 = Counter32: 5616 + SNMPv2-MIB::snmpOutPkts.0 = Counter32: 5607 + SNMPv2-MIB::snmpInBadVersions.0 = Counter32: 0 + SNMPv2-MIB::snmpInBadCommunityNames.0 = Counter32: 9 + SNMPv2-MIB::snmpInBadCommunityUses.0 = Counter32: 0 + SNMPv2-MIB::snmpInASNParseErrs.0 = Counter32: 0 + SNMPv2-MIB::snmpInTooBigs.0 = Counter32: 0 + SNMPv2-MIB::snmpInNoSuchNames.0 = Counter32: 0 + SNMPv2-MIB::snmpInBadValues.0 = Counter32: 0 + SNMPv2-MIB::snmpInReadOnlys.0 = Counter32: 0 + SNMPv2-MIB::snmpInGenErrs.0 = Counter32: 0 + SNMPv2-MIB::snmpInTotalReqVars.0 = Counter32: 5616 + SNMPv2-MIB::snmpInTotalSetVars.0 = Counter32: 0 + SNMPv2-MIB::snmpInGetRequests.0 = Counter32: 5 + SNMPv2-MIB::snmpInGetNexts.0 = Counter32: 5615 + SNMPv2-MIB::snmpInSetRequests.0 = Counter32: 0 + SNMPv2-MIB::snmpInGetResponses.0 = Counter32: 0 + SNMPv2-MIB::snmpInTraps.0 = Counter32: 0 + SNMPv2-MIB::snmpOutTooBigs.0 = Counter32: 0 + SNMPv2-MIB::snmpOutNoSuchNames.0 = Counter32: 0 + SNMPv2-MIB::snmpOutBadValues.0 = Counter32: 0 + SNMPv2-MIB::snmpOutGenErrs.0 = Counter32: 0 + SNMPv2-MIB::snmpOutGetRequests.0 = Counter32: 0 + SNMPv2-MIB::snmpOutGetNexts.0 = Counter32: 0 + SNMPv2-MIB::snmpOutSetRequests.0 = Counter32: 0 + SNMPv2-MIB::snmpOutGetResponses.0 = Counter32: 5630 + SNMPv2-MIB::snmpOutTraps.0 = Counter32: 0 + SNMPv2-MIB::snmpEnableAuthenTraps.0 = INTEGER: disabled(2) + SNMPv2-MIB::snmpSilentDrops.0 = Counter32: 0 + SNMPv2-MIB::snmpProxyDrops.0 = Counter32: 0 + RMON2-MIB::netDefaultGateway.0 = IpAddress: 0.0.0.0 + BRIDGE-MIB::dot1dBaseBridgeAddress.0 = STRING: 20:87:56:ff:aa:84 + BRIDGE-MIB::dot1dBaseNumPorts.0 = INTEGER: 4 ports + BRIDGE-MIB::dot1dBaseType.0 = INTEGER: transparent-only(2) + BRIDGE-MIB::dot1dBasePort.1 = INTEGER: 1 + BRIDGE-MIB::dot1dBasePort.2 = INTEGER: 2 + BRIDGE-MIB::dot1dBasePort.3 = INTEGER: 3 + BRIDGE-MIB::dot1dBasePort.4 = INTEGER: 4 + BRIDGE-MIB::dot1dBasePortIfIndex.1 = INTEGER: 1 + BRIDGE-MIB::dot1dBasePortIfIndex.2 = INTEGER: 2 + BRIDGE-MIB::dot1dBasePortIfIndex.3 = INTEGER: 3 + BRIDGE-MIB::dot1dBasePortIfIndex.4 = INTEGER: 4 + BRIDGE-MIB::dot1dBasePortCircuit.1 = OID: SNMPv2-SMI::zeroDotZero + BRIDGE-MIB::dot1dBasePortCircuit.2 = OID: SNMPv2-SMI::zeroDotZero + BRIDGE-MIB::dot1dBasePortCircuit.3 = OID: SNMPv2-SMI::zeroDotZero + BRIDGE-MIB::dot1dBasePortCircuit.4 = OID: SNMPv2-SMI::zeroDotZero + BRIDGE-MIB::dot1dBasePortDelayExceededDiscards.1 = Counter32: 0 + BRIDGE-MIB::dot1dBasePortDelayExceededDiscards.2 = Counter32: 0 + BRIDGE-MIB::dot1dBasePortDelayExceededDiscards.3 = Counter32: 0 + BRIDGE-MIB::dot1dBasePortDelayExceededDiscards.4 = Counter32: 0 + BRIDGE-MIB::dot1dBasePortMtuExceededDiscards.1 = Counter32: 0 + BRIDGE-MIB::dot1dBasePortMtuExceededDiscards.2 = Counter32: 0 + BRIDGE-MIB::dot1dBasePortMtuExceededDiscards.3 = Counter32: 0 + BRIDGE-MIB::dot1dBasePortMtuExceededDiscards.4 = Counter32: 0 + BRIDGE-MIB::dot1dTpLearnedEntryDiscards.0 = Counter32: 0 + BRIDGE-MIB::dot1dTpAgingTime.0 = INTEGER: 30 seconds + BRIDGE-MIB::dot1dTpFdbAddress.'T.u...' = STRING: 54:ee:75:ff:95:a6 + BRIDGE-MIB::dot1dTpFdbAddress.'.'....' = STRING: b8:27:eb:a4:b5:ee + BRIDGE-MIB::dot1dTpFdbPort.'T.u...' = INTEGER: 3 + BRIDGE-MIB::dot1dTpFdbPort.'.'....' = INTEGER: 2 + BRIDGE-MIB::dot1dTpFdbStatus.'T.u...' = INTEGER: learned(3) + BRIDGE-MIB::dot1dTpFdbStatus.'.'....' = INTEGER: learned(3) + BRIDGE-MIB::dot1dTpPort.1 = INTEGER: 1 + BRIDGE-MIB::dot1dTpPort.2 = INTEGER: 2 + BRIDGE-MIB::dot1dTpPort.3 = INTEGER: 3 + BRIDGE-MIB::dot1dTpPort.4 = INTEGER: 4 + BRIDGE-MIB::dot1dTpPortMaxInfo.1 = INTEGER: 1486 bytes + BRIDGE-MIB::dot1dTpPortMaxInfo.2 = INTEGER: 1486 bytes + BRIDGE-MIB::dot1dTpPortMaxInfo.3 = INTEGER: 1486 bytes + BRIDGE-MIB::dot1dTpPortMaxInfo.4 = INTEGER: 1486 bytes + BRIDGE-MIB::dot1dTpPortInFrames.1 = Counter32: 8751 frames + BRIDGE-MIB::dot1dTpPortInFrames.2 = Counter32: 1830 frames + BRIDGE-MIB::dot1dTpPortInFrames.3 = Counter32: 6546 frames + BRIDGE-MIB::dot1dTpPortInFrames.4 = Counter32: 0 frames + BRIDGE-MIB::dot1dTpPortOutFrames.1 = Counter32: 13306 frames + BRIDGE-MIB::dot1dTpPortOutFrames.2 = Counter32: 5738 frames + BRIDGE-MIB::dot1dTpPortOutFrames.3 = Counter32: 22510 frames + BRIDGE-MIB::dot1dTpPortOutFrames.4 = Counter32: 0 frames + BRIDGE-MIB::dot1dTpPortInDiscards.1 = Counter32: 0 frames + BRIDGE-MIB::dot1dTpPortInDiscards.2 = Counter32: 0 frames + BRIDGE-MIB::dot1dTpPortInDiscards.3 = Counter32: 0 frames + BRIDGE-MIB::dot1dTpPortInDiscards.4 = Counter32: 0 frames + IF-MIB::ifName.1 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P1 + IF-MIB::ifName.2 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P2 + IF-MIB::ifName.3 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P3 + IF-MIB::ifName.4 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P4 + IF-MIB::ifName.1000 = STRING: Siemens, SIMATIC NET, internal, X1 + IF-MIB::ifInMulticastPkts.1 = Counter32: 6744 + IF-MIB::ifInMulticastPkts.2 = Counter32: 1299 + IF-MIB::ifInMulticastPkts.3 = Counter32: 91 + IF-MIB::ifInMulticastPkts.4 = Counter32: 0 + IF-MIB::ifInMulticastPkts.1000 = Counter32: 0 + IF-MIB::ifInBroadcastPkts.1 = Counter32: 1096 + IF-MIB::ifInBroadcastPkts.2 = Counter32: 223 + IF-MIB::ifInBroadcastPkts.3 = Counter32: 8 + IF-MIB::ifInBroadcastPkts.4 = Counter32: 0 + IF-MIB::ifInBroadcastPkts.1000 = Counter32: 0 + IF-MIB::ifOutMulticastPkts.1 = Counter32: 7239 + IF-MIB::ifOutMulticastPkts.2 = Counter32: 5046 + IF-MIB::ifOutMulticastPkts.3 = Counter32: 14309 + IF-MIB::ifOutMulticastPkts.4 = Counter32: 0 + IF-MIB::ifOutMulticastPkts.1000 = Counter32: 0 + IF-MIB::ifOutBroadcastPkts.1 = Counter32: 232 + IF-MIB::ifOutBroadcastPkts.2 = Counter32: 307 + IF-MIB::ifOutBroadcastPkts.3 = Counter32: 1320 + IF-MIB::ifOutBroadcastPkts.4 = Counter32: 0 + IF-MIB::ifOutBroadcastPkts.1000 = Counter32: 0 + IF-MIB::ifHCInOctets.1 = Counter64: 908950 + IF-MIB::ifHCInOctets.2 = Counter64: 214979 + IF-MIB::ifHCInOctets.3 = Counter64: 605526 + IF-MIB::ifHCInOctets.4 = Counter64: 0 + IF-MIB::ifHCInOctets.1000 = Counter64: 0 + IF-MIB::ifHCInUcastPkts.1 = Counter64: 911 + IF-MIB::ifHCInUcastPkts.2 = Counter64: 308 + IF-MIB::ifHCInUcastPkts.3 = Counter64: 6501 + IF-MIB::ifHCInUcastPkts.4 = Counter64: 0 + IF-MIB::ifHCInUcastPkts.1000 = Counter64: 0 + IF-MIB::ifHCInMulticastPkts.1 = Counter64: 6744 + IF-MIB::ifHCInMulticastPkts.2 = Counter64: 1299 + IF-MIB::ifHCInMulticastPkts.3 = Counter64: 91 + IF-MIB::ifHCInMulticastPkts.4 = Counter64: 0 + IF-MIB::ifHCInMulticastPkts.1000 = Counter64: 0 + IF-MIB::ifHCInBroadcastPkts.1 = Counter64: 1096 + IF-MIB::ifHCInBroadcastPkts.2 = Counter64: 223 + IF-MIB::ifHCInBroadcastPkts.3 = Counter64: 8 + IF-MIB::ifHCInBroadcastPkts.4 = Counter64: 0 + IF-MIB::ifHCInBroadcastPkts.1000 = Counter64: 0 + IF-MIB::ifHCOutOctets.1 = Counter64: 1516677 + IF-MIB::ifHCOutOctets.2 = Counter64: 597260 + IF-MIB::ifHCOutOctets.3 = Counter64: 2274335 + IF-MIB::ifHCOutOctets.4 = Counter64: 0 + IF-MIB::ifHCOutOctets.1000 = Counter64: 0 + IF-MIB::ifHCOutUcastPkts.1 = Counter64: 5835 + IF-MIB::ifHCOutUcastPkts.2 = Counter64: 385 + IF-MIB::ifHCOutUcastPkts.3 = Counter64: 6955 + IF-MIB::ifHCOutUcastPkts.4 = Counter64: 0 + IF-MIB::ifHCOutUcastPkts.1000 = Counter64: 0 + IF-MIB::ifHCOutMulticastPkts.1 = Counter64: 7239 + IF-MIB::ifHCOutMulticastPkts.2 = Counter64: 5046 + IF-MIB::ifHCOutMulticastPkts.3 = Counter64: 14309 + IF-MIB::ifHCOutMulticastPkts.4 = Counter64: 0 + IF-MIB::ifHCOutMulticastPkts.1000 = Counter64: 0 + IF-MIB::ifHCOutBroadcastPkts.1 = Counter64: 232 + IF-MIB::ifHCOutBroadcastPkts.2 = Counter64: 307 + IF-MIB::ifHCOutBroadcastPkts.3 = Counter64: 1320 + IF-MIB::ifHCOutBroadcastPkts.4 = Counter64: 0 + IF-MIB::ifHCOutBroadcastPkts.1000 = Counter64: 0 + IF-MIB::ifLinkUpDownTrapEnable.1 = INTEGER: disabled(2) + IF-MIB::ifLinkUpDownTrapEnable.2 = INTEGER: disabled(2) + IF-MIB::ifLinkUpDownTrapEnable.3 = INTEGER: disabled(2) + IF-MIB::ifLinkUpDownTrapEnable.4 = INTEGER: disabled(2) + IF-MIB::ifLinkUpDownTrapEnable.1000 = INTEGER: disabled(2) + IF-MIB::ifHighSpeed.1 = Gauge32: 10000000 + IF-MIB::ifHighSpeed.2 = Gauge32: 100000000 + IF-MIB::ifHighSpeed.3 = Gauge32: 100000000 + IF-MIB::ifHighSpeed.4 = Gauge32: 10000000 + IF-MIB::ifHighSpeed.1000 = Gauge32: 0 + IF-MIB::ifPromiscuousMode.1 = INTEGER: false(2) + IF-MIB::ifPromiscuousMode.2 = INTEGER: false(2) + IF-MIB::ifPromiscuousMode.3 = INTEGER: false(2) + IF-MIB::ifPromiscuousMode.4 = INTEGER: false(2) + IF-MIB::ifPromiscuousMode.1000 = INTEGER: false(2) + IF-MIB::ifConnectorPresent.1 = INTEGER: true(1) + IF-MIB::ifConnectorPresent.2 = INTEGER: true(1) + IF-MIB::ifConnectorPresent.3 = INTEGER: true(1) + IF-MIB::ifConnectorPresent.4 = INTEGER: true(1) + IF-MIB::ifConnectorPresent.1000 = INTEGER: false(2) + IF-MIB::ifAlias.1 = STRING: + IF-MIB::ifAlias.2 = STRING: + IF-MIB::ifAlias.3 = STRING: + IF-MIB::ifAlias.4 = STRING: + IF-MIB::ifAlias.1000 = STRING: + IF-MIB::ifCounterDiscontinuityTime.1 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifCounterDiscontinuityTime.2 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifCounterDiscontinuityTime.3 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifCounterDiscontinuityTime.4 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifCounterDiscontinuityTime.1000 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifStackStatus.0.1000 = INTEGER: active(1) + IF-MIB::ifStackStatus.1.0 = INTEGER: active(1) + IF-MIB::ifStackStatus.2.0 = INTEGER: active(1) + IF-MIB::ifStackStatus.3.0 = INTEGER: active(1) + IF-MIB::ifStackStatus.4.0 = INTEGER: active(1) + IF-MIB::ifStackStatus.1000.1 = INTEGER: active(1) + IF-MIB::ifStackStatus.1000.2 = INTEGER: active(1) + IF-MIB::ifStackStatus.1000.3 = INTEGER: active(1) + IF-MIB::ifStackStatus.1000.4 = INTEGER: active(1) + IF-MIB::ifRcvAddressStatus.1000 = INTEGER: active(1) + IF-MIB::ifRcvAddressType.1000 = INTEGER: nonVolatile(3) + IF-MIB::ifTableLastChange.0 = Timeticks: (0) 0:00:00.00 + IF-MIB::ifStackLastChange.0 = Timeticks: (0) 0:00:00.00 + +and the LLDP subtree:: + + $ snmpwalk -v2c -c public -m ALL 192.168.0.99 LLDP-MIB::lldpMIB + LLDP-MIB::lldpMessageTxInterval.0 = INTEGER: 5 seconds + LLDP-MIB::lldpMessageTxHoldMultiplier.0 = INTEGER: 4 + LLDP-MIB::lldpReinitDelay.0 = INTEGER: 1 seconds + LLDP-MIB::lldpTxDelay.0 = INTEGER: 1 seconds + LLDP-MIB::lldpPortConfigAdminStatus.1 = INTEGER: txAndRx(3) + LLDP-MIB::lldpPortConfigAdminStatus.2 = INTEGER: txAndRx(3) + LLDP-MIB::lldpPortConfigAdminStatus.3 = INTEGER: txAndRx(3) + LLDP-MIB::lldpPortConfigAdminStatus.4 = INTEGER: txAndRx(3) + LLDP-MIB::lldpPortConfigNotificationEnable.1 = INTEGER: false(2) + LLDP-MIB::lldpPortConfigNotificationEnable.2 = INTEGER: false(2) + LLDP-MIB::lldpPortConfigNotificationEnable.3 = INTEGER: false(2) + LLDP-MIB::lldpPortConfigNotificationEnable.4 = INTEGER: false(2) + LLDP-MIB::lldpPortConfigTLVsTxEnable.1 = BITS: F0 portDesc(0) sysName(1) sysDesc(2) sysCap(3) + LLDP-MIB::lldpPortConfigTLVsTxEnable.2 = BITS: F0 portDesc(0) sysName(1) sysDesc(2) sysCap(3) + LLDP-MIB::lldpPortConfigTLVsTxEnable.3 = BITS: F0 portDesc(0) sysName(1) sysDesc(2) sysCap(3) + LLDP-MIB::lldpPortConfigTLVsTxEnable.4 = BITS: F0 portDesc(0) sysName(1) sysDesc(2) sysCap(3) + LLDP-MIB::lldpConfigManAddrPortsTxEnable.ipV4."...c" = Hex-STRING: F0 + LLDP-MIB::lldpStatsRemTablesLastChangeTime.0 = Timeticks: (1200) 0:00:12.00 + LLDP-MIB::lldpStatsRemTablesInserts.0 = Gauge32: 2 table entries + LLDP-MIB::lldpStatsRemTablesDeletes.0 = Gauge32: 0 table entries + LLDP-MIB::lldpStatsRemTablesDrops.0 = Gauge32: 0 table entries + LLDP-MIB::lldpStatsRemTablesAgeouts.0 = Gauge32: 0 + LLDP-MIB::lldpStatsTxPortFramesTotal.1 = Counter32: 1072 + LLDP-MIB::lldpStatsTxPortFramesTotal.2 = Counter32: 1072 + LLDP-MIB::lldpStatsTxPortFramesTotal.3 = Counter32: 1072 + LLDP-MIB::lldpStatsTxPortFramesTotal.4 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesDiscardedTotal.1 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesDiscardedTotal.2 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesDiscardedTotal.3 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesDiscardedTotal.4 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesErrors.1 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesErrors.2 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesErrors.3 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesErrors.4 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesTotal.1 = Counter32: 669 + LLDP-MIB::lldpStatsRxPortFramesTotal.2 = Counter32: 1073 + LLDP-MIB::lldpStatsRxPortFramesTotal.3 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortFramesTotal.4 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsDiscardedTotal.1 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsDiscardedTotal.2 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsDiscardedTotal.3 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsDiscardedTotal.4 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsUnrecognizedTotal.1 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsUnrecognizedTotal.2 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsUnrecognizedTotal.3 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortTLVsUnrecognizedTotal.4 = Counter32: 0 + LLDP-MIB::lldpStatsRxPortAgeoutsTotal.1 = Gauge32: 0 + LLDP-MIB::lldpStatsRxPortAgeoutsTotal.2 = Gauge32: 0 + LLDP-MIB::lldpStatsRxPortAgeoutsTotal.3 = Gauge32: 0 + LLDP-MIB::lldpStatsRxPortAgeoutsTotal.4 = Gauge32: 0 + LLDP-MIB::lldpLocChassisIdSubtype.0 = INTEGER: local(7) + LLDP-MIB::lldpLocChassisId.0 = STRING: "b" + LLDP-MIB::lldpLocSysName.0 = STRING: sysName Not Set + LLDP-MIB::lldpLocSysDesc.0 = STRING: Siemens, SIMATIC NET, SCALANCE X204IRT, 6GK5 204-0BA00-2BA3, HW: Version 9, FW: Version V05.04.02, SVPL6147920 + LLDP-MIB::lldpLocSysCapSupported.0 = BITS: 01 stationOnly(7) + LLDP-MIB::lldpLocSysCapEnabled.0 = BITS: 01 stationOnly(7) + LLDP-MIB::lldpLocPortIdSubtype.1 = INTEGER: local(7) + LLDP-MIB::lldpLocPortIdSubtype.2 = INTEGER: local(7) + LLDP-MIB::lldpLocPortIdSubtype.3 = INTEGER: local(7) + LLDP-MIB::lldpLocPortIdSubtype.4 = INTEGER: local(7) + LLDP-MIB::lldpLocPortId.1 = STRING: "port-001" + LLDP-MIB::lldpLocPortId.2 = STRING: "port-002" + LLDP-MIB::lldpLocPortId.3 = STRING: "port-003" + LLDP-MIB::lldpLocPortId.4 = STRING: "port-004" + LLDP-MIB::lldpLocPortDesc.1 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P1 + LLDP-MIB::lldpLocPortDesc.2 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P2 + LLDP-MIB::lldpLocPortDesc.3 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P3 + LLDP-MIB::lldpLocPortDesc.4 = STRING: Siemens, SIMATIC NET, Ethernet Port, X1 P4 + LLDP-MIB::lldpLocManAddrLen.ipV4."...c" = INTEGER: 5 + LLDP-MIB::lldpLocManAddrIfSubtype.ipV4."...c" = INTEGER: ifIndex(2) + LLDP-MIB::lldpLocManAddrIfId.ipV4."...c" = INTEGER: 1000 + LLDP-MIB::lldpLocManAddrOID.ipV4."...c" = OID: SNMPv2-SMI::enterprises.24686 + LLDP-MIB::lldpRemChassisIdSubtype.400.2.2 = INTEGER: local(7) + LLDP-MIB::lldpRemChassisIdSubtype.1200.1.1 = INTEGER: local(7) + LLDP-MIB::lldpRemChassisId.400.2.2 = STRING: "3S PN-Controller 1103000032 0000000010a4b5ee 0 V 1 0 0" + LLDP-MIB::lldpRemChassisId.1200.1.1 = STRING: "(REMOVED)" + LLDP-MIB::lldpRemPortIdSubtype.400.2.2 = INTEGER: local(7) + LLDP-MIB::lldpRemPortIdSubtype.1200.1.1 = INTEGER: local(7) + LLDP-MIB::lldpRemPortId.400.2.2 = STRING: "port-001.controller" + LLDP-MIB::lldpRemPortId.1200.1.1 = STRING: "port-001" + LLDP-MIB::lldpRemSysName.1200.1.1 = STRING: (REMOVED) + LLDP-MIB::lldpRemSysDesc.1200.1.1 = STRING: (REMOVED) + LLDP-MIB::lldpRemSysCapSupported.1200.1.1 = BITS: 01 stationOnly(7) + LLDP-MIB::lldpRemSysCapEnabled.1200.1.1 = BITS: 01 stationOnly(7) + LLDP-MIB::lldpRemManAddrIfSubtype.400.2.2.ipV4."...d" = INTEGER: systemPortNumber(3) + LLDP-MIB::lldpRemManAddrIfSubtype.1200.1.1.ipV4."...." = INTEGER: ifIndex(2) + LLDP-MIB::lldpRemManAddrIfId.400.2.2.ipV4."...d" = INTEGER: 1 + LLDP-MIB::lldpRemManAddrIfId.1200.1.1.ipV4."...." = INTEGER: 1 + LLDP-MIB::lldpRemManAddrOID.1200.1.1.ipV4."...." = OID: SNMPv2-SMI::enterprises.24686 + LLDP-EXT-PNO-MIB::lldpXPnoConfigSPDTxEnable.1 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigSPDTxEnable.2 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigSPDTxEnable.3 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigSPDTxEnable.4 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPortStatusTxEnable.1 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPortStatusTxEnable.2 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPortStatusTxEnable.3 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPortStatusTxEnable.4 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigAliasTxEnable.1 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigAliasTxEnable.2 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigAliasTxEnable.3 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigAliasTxEnable.4 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigMrpTxEnable.1 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigMrpTxEnable.2 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigMrpTxEnable.3 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigMrpTxEnable.4 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPtcpTxEnable.1 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPtcpTxEnable.2 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPtcpTxEnable.3 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoConfigPtcpTxEnable.4 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocLPDValue.1 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocLPDValue.2 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocLPDValue.3 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocLPDValue.4 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortTxDValue.1 = Gauge32: 1123 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortTxDValue.2 = Gauge32: 1123 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortTxDValue.3 = Gauge32: 1123 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortTxDValue.4 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRxDValue.1 = Gauge32: 444 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRxDValue.2 = Gauge32: 444 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRxDValue.3 = Gauge32: 444 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRxDValue.4 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT2.1 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT2.2 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT2.3 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT2.4 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3.1 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3.2 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3.3 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3.4 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortNoS.1 = STRING: b + LLDP-EXT-PNO-MIB::lldpXPnoLocPortNoS.2 = STRING: b + LLDP-EXT-PNO-MIB::lldpXPnoLocPortNoS.3 = STRING: b + LLDP-EXT-PNO-MIB::lldpXPnoLocPortNoS.4 = STRING: b + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrpUuId.1 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrpUuId.2 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrpUuId.3 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrpUuId.4 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrrtStatus.1 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrrtStatus.2 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrrtStatus.3 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortMrrtStatus.4 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpMaster.1 = STRING: 0:0:0:0:0:0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpMaster.2 = STRING: 0:0:0:0:0:0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpMaster.3 = STRING: 0:0:0:0:0:0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpMaster.4 = STRING: 0:0:0:0:0:0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpSubdomainUUID.1 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpSubdomainUUID.2 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpSubdomainUUID.3 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpSubdomainUUID.4 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpIRDataUUID.1 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpIRDataUUID.2 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpIRDataUUID.3 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPtcpIRDataUUID.4 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortModeRT3.1 = INTEGER: standard(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortModeRT3.2 = INTEGER: standard(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortModeRT3.3 = INTEGER: standard(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortModeRT3.4 = INTEGER: standard(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodLength.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 1000000 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodLength.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 1000000 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodLength.3 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 1000000 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodLength.4 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 1000000 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodValidity.1 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodValidity.2 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodValidity.3 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortPeriodValidity.4 = INTEGER: true(1) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedOffset.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedOffset.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedOffset.3 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedOffset.4 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedValidity.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedValidity.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedValidity.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortRedValidity.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeOffset.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeOffset.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeOffset.3 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeOffset.4 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeValidity.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeValidity.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeValidity.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortOrangeValidity.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenOffset.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenOffset.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenOffset.3 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenOffset.4 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenValidity.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenValidity.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenValidity.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortGreenValidity.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3OptimizationSupported.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3OptimizationSupported.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3OptimizationSupported.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3OptimizationSupported.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShorteningSupported.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShorteningSupported.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShorteningSupported.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShorteningSupported.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShortening.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShortening.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShortening.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3PreambleShortening.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3FragmentationSupported.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3FragmentationSupported.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3FragmentationSupported.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3FragmentationSupported.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3Fragmentation.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3Fragmentation.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3Fragmentation.3 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoLocPortStatusRT3Fragmentation.4 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemLPDValue.400.2.2 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoRemLPDValue.1200.1.1 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoRemPortTxDValue.400.2.2 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoRemPortTxDValue.1200.1.1 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoRemPortRxDValue.400.2.2 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoRemPortRxDValue.1200.1.1 = Gauge32: 0 ns + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT2.400.2.2 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT2.1200.1.1 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT3.400.2.2 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT3.1200.1.1 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortNoS.400.2.2 = STRING: controller + LLDP-EXT-PNO-MIB::lldpXPnoRemPortNoS.1200.1.1 = STRING: (REMOVED) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortMrpUuId.400.2.2 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortMrpUuId.1200.1.1 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortMrrtStatus.400.2.2 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortMrrtStatus.1200.1.1 = INTEGER: off(0) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPtcpMaster.400.2.2 = STRING: 0:0:0:0:0:0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPtcpMaster.1200.1.1 = STRING: 0:0:0:0:0:0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPtcpSubdomainUUID.400.2.2 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPtcpSubdomainUUID.1200.1.1 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPtcpIRDataUUID.400.2.2 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPtcpIRDataUUID.1200.1.1 = Hex-STRING: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortModeRT3.400.2.2 = INTEGER: standard(1) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortModeRT3.1200.1.1 = INTEGER: standard(1) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPeriodLength.400.2.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPeriodLength.1200.1.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPeriodValidity.400.2.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortPeriodValidity.1200.1.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortRedOffset.400.2.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortRedOffset.1200.1.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortRedValidity.400.2.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortRedValidity.1200.1.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortOrangeOffset.400.2.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortOrangeOffset.1200.1.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortOrangeValidity.400.2.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortOrangeValidity.1200.1.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortGreenOffset.400.2.2 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortGreenOffset.1200.1.1 = Wrong Type (should be Gauge32 or Unsigned32): INTEGER: 0 + LLDP-EXT-PNO-MIB::lldpXPnoRemPortGreenValidity.400.2.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortGreenValidity.1200.1.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT3PreambleShortening.400.2.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT3PreambleShortening.1200.1.1 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT3Fragmentation.400.2.2 = INTEGER: false(2) + LLDP-EXT-PNO-MIB::lldpXPnoRemPortStatusRT3Fragmentation.1200.1.1 = INTEGER: false(2) + LLDP-MIB::lldpExtensions.4623.1.1.1.1.1.1 = Hex-STRING: F0 + LLDP-MIB::lldpExtensions.4623.1.1.1.1.1.2 = Hex-STRING: F0 + LLDP-MIB::lldpExtensions.4623.1.1.1.1.1.3 = Hex-STRING: F0 + LLDP-MIB::lldpExtensions.4623.1.1.1.1.1.4 = Hex-STRING: F0 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.1.1 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.1.2 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.1.3 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.1.4 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.2.1 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.2.2 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.2.3 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.2.4 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.3.1 = Hex-STRING: FF 00 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.3.2 = Hex-STRING: FF 00 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.3.3 = Hex-STRING: FF 00 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.3.4 = Hex-STRING: FF 00 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.4.1 = INTEGER: 16 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.4.2 = INTEGER: 16 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.4.3 = INTEGER: 16 + LLDP-MIB::lldpExtensions.4623.1.2.1.1.4.4 = INTEGER: 0 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.1.1 = Hex-STRING: 00 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.1.2 = Hex-STRING: 00 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.1.3 = Hex-STRING: 00 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.1.4 = Hex-STRING: 00 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.2.1 = INTEGER: 0 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.2.2 = INTEGER: 0 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.2.3 = INTEGER: 0 + LLDP-MIB::lldpExtensions.4623.1.2.3.1.2.4 = INTEGER: 0 + LLDP-MIB::lldpExtensions.4623.1.2.4.1.1.1 = INTEGER: 1522 + LLDP-MIB::lldpExtensions.4623.1.2.4.1.1.2 = INTEGER: 1522 + LLDP-MIB::lldpExtensions.4623.1.2.4.1.1.3 = INTEGER: 1522 + LLDP-MIB::lldpExtensions.4623.1.2.4.1.1.4 = INTEGER: 1522 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.1.400.2.2 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.1.1200.1.1 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.2.400.2.2 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.2.1200.1.1 = INTEGER: 1 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.3.400.2.2 = Hex-STRING: 6C 00 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.3.1200.1.1 = Hex-STRING: EC 03 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.4.400.2.2 = INTEGER: 16 + LLDP-MIB::lldpExtensions.4623.1.3.1.1.4.1200.1.1 = INTEGER: 30 + LLDP-MIB::lldpExtensions.32962.1.1.1.1.1.1 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.1.1.1.2 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.1.1.1.3 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.1.1.1.4 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.2.1.1.1.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.2.1.1.2.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.2.1.1.3.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.2.1.1.4.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.3.1.1.1.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.3.1.1.2.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.3.1.1.3.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.3.1.1.4.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.4.1.1.1.1 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.4.1.1.2.1 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.4.1.1.3.1 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.1.4.1.1.4.1 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.1.1.1.1 = INTEGER: 0 + LLDP-MIB::lldpExtensions.32962.1.2.1.1.1.2 = INTEGER: 0 + LLDP-MIB::lldpExtensions.32962.1.2.1.1.1.3 = INTEGER: 0 + LLDP-MIB::lldpExtensions.32962.1.2.1.1.1.4 = INTEGER: 0 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.2.1.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.2.2.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.2.3.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.2.4.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.3.1.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.3.2.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.3.3.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.2.2.1.3.4.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.3.1.1.1.400.2.2 = INTEGER: 0 + LLDP-MIB::lldpExtensions.32962.1.3.1.1.1.1200.1.1 = INTEGER: 0 + LLDP-MIB::lldpExtensions.32962.1.3.2.1.2.400.2.2.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.3.2.1.2.1200.1.1.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.3.2.1.3.400.2.2.0 = INTEGER: 2 + LLDP-MIB::lldpExtensions.32962.1.3.2.1.3.1200.1.1.0 = INTEGER: 2 diff --git a/profinet_stack/p-net/doc/prepare_raspberrypi.rst b/profinet_stack/p-net/doc/prepare_raspberrypi.rst new file mode 100644 index 0000000..fbd8d79 --- /dev/null +++ b/profinet_stack/p-net/doc/prepare_raspberrypi.rst @@ -0,0 +1,382 @@ +.. _prepare_raspberry: + +Installation and configuration of Raspberry Pi +============================================== +This page consists an installation guide to use if you have a Linux laptop, +and a separate guide to use with a Windows laptop. + +The p-net stack and sample application has been tested with: + +* Raspberry Pi 3 Model B+ + +To avoid problems it is recommended to start with a fresh +Raspberry Pi OS image. + +When running the Raspberry Pi as a Profinet IO-device using p-net, the +network settings of the Raspberry Pi will be changed by Profinet. +Therefore it is highly recommended to use a keyboard, mouse and monitor or +a serial cable. + +If you use ssh you may end up in a situation were you have difficulties +connecting to, or recovering your device. If you currently do not have a +suitable USB-to-serial cable you can still build and run p-net with the +sample application but you might run into problem when a PLC is used for +configuration. + + +Installation using a Linux laptop +--------------------------------- +Burn a SD-card with Raspberry Pi OS, by using the "Raspberry Pi imager" +software and a SD card reader. +A card size of 16 - 32 GByte is recommended. +Follow the instructions on the page https://www.raspberrypi.com/software/ +Select the standard "Raspberry Pi OS" operating system. + +Unplug, and reinsert your SD-card to mount it. To enable SSH logging in on the +Raspberry Pi, create an empty file on the named ``ssh`` in the boot partition:: + + touch ssh + +To enable the serial port console add this line to the +file ``config.txt`` in the boot partition:: + + enable_uart=1 + +The DHCP client daemon will adjust the network interface settings automatically. +This interferes with the p-net control of the Ethernet interface. So if you +run your Raspberry Pi as a Profinet IO-Device (NOT if you use it as a PLC) +and have a serial cable, you need to add this line to ``/etc/dhcpcd.conf`` +in the root file system:: + + denyinterfaces eth* + +If you would like to change hostname from ``raspberrypi`` to ``pndevice-pi``, change +the texts in the files ``etc/hostname`` and ``etc/hosts`` in the rootfs +partition. + +To make sure that you subsequently are logging in to the correct Raspberry Pi, +you can create a file in the home directory in the rootfs partition. Change +name to something informative, for example:: + + touch home/pi/IAmAProfinetDevice + +Unmount the SD-card, and plug it in into your Raspberry Pi. Power up the +Raspberry Pi. Log in to it via a serial cable (see below). +Use the username ``pi`` and the default password ``raspberry``. + +If you do not have any serial cable (and not have disabled DHCP), connect +the Raspberry Pi to your local network. +Find the IP address of it by running this on a Linux machine on the network +(replace the hostname if you have changed it):: + + ping raspberrypi.local + +Verify that it is the correct machine by checking that is disappears when the +power is disconnected. + +Log in to it:: + + ssh pi@ + +Enter the password mentioned just above. + +If you would connect your Raspberry Pi to a WiFi network, follow the +guide in https://www.raspberrypi.com/documentation/computers/configuration.html + +You might also want to disable the splash screen and to expand the file system, +by using the ``raspi-config`` utility. + +.. note:: If you are following the tutorial and are setting up the IO-device, + you should head back now. See :ref:`tutorial`. + + +Installation using a Windows laptop +----------------------------------- +This section describes how to install the Raspberry Pi OS +and how to enable ssh and serial console so that the Raspberry Pi can be +used in headless mode without a display and keyboard connected. + +Step 1. Write Raspberry Pi OS image to SD card using Raspberry Pi Imager. + +* Download and install Raspberry Pi Imager from + https://www.raspberrypi.com/software/ +* Start Raspberry Pi Imager +* In the Select OS dialog choose full version +* Select SD-card +* Press Write + +Step 2. Initial configuration of Raspberry Pi OS. + +* Eject SD-card +* Reinsert SD-card in windows PC. The SD-card will be shown as external drive named ``boot``. +* Enable ssh by creating an empty file named ``ssh`` in the root folder of ``boot``. + The windows file explorer can be used for this. + Note that the file ``ssh`` shall not have a txt file extension. +* Enable serial port console. + Open ``config.txt`` in root folder of ``boot`` using Notepad. + Add the line ``enable_uart=1`` to the end of the file. + Save file and close Notepad. +* Eject SD-card + +Step 3. Start Raspberry Pi + +* Insert SD-card and power on Raspberry Pi. +* Login (preferably using serial console) with default user ``pi`` and password ``raspberry``. + +Step 4. Network configuration. + +Use the nano editor to edit the configuration files as described below. +For example to edit the ``/etc/dhcpcd.conf``:: + + sudo nano /etc/dhcpcd.conf + +Save the file in nano by pressing ``CTRL-X``, then ``Y`` and ``Enter``. + +The DHCP client daemon will adjust the network interface settings automatically. +This interferes with the p-net control of the Ethernet interface. So if you +run your Raspberry Pi as a Profinet IO-Device (NOT if you use it as a PLC) +and have a serial cable, you should add the line below to ``/etc/dhcpcd.conf``:: + + denyinterfaces eth* + +Optionally, to change hostname from ``raspberrypi`` to ``pndevice-pi``, change +the configuration in the files ``/etc/hostname`` and ``/etc/hosts``. + +To make sure that you subsequently are logging in to the correct Raspberry Pi, +you can create a file in the home directory in the rootfs partition. Change +name to something informative, for example:: + + touch /home/pi/IAmAProfinetDevice + +Reboot and the Raspberry Pi is now ready to run the p-net sample application:: + + sudo reboot + +If you would connect your Raspberry Pi to a WiFi network, follow the +guide in https://www.raspberrypi.com/documentation/computers/configuration.html + +You might also want to disable the splash screen and to expand the file system, +by using the ``raspi-config`` utility. + +.. note:: If you are following the tutorial and are setting up the IO-device, + you should head back now. See :ref:`tutorial`. + + +Optionally connect a serial cable to Raspberry Pi +------------------------------------------------- +The p-net Profinet stack will change the IP-address of the Raspberry Pi when +running it as an IO-Device (as requested by the PLC), why it can be +inconvenient to connect to it via ssh. You can use a keyboard, mouse and a +monitor to connect to the Raspberry Pi. Using a serial cable to connect it to +your laptop can then be helpful if a keyboard etc not is available. + +Use a USB-to-serial adapter cable with 3.3 V logic levels. For example +Adafruit sells a popular version of those cables. Connect the USB end to your +laptop and the other end to the header connector on the Raspberry Pi. + +If not already done, enable the serial port console by writing the line +``enable_uart=1`` in the file ``/boot/config.txt``. + +The serial port within the Raspberry Pi will be named ``/dev/ttyS0``. + ++-----+-----------+---------------------+-----------------------+ +| Pin | Name | Terminal on cable | Adafruit cable color | ++=====+===========+=====================+=======================+ +| 6 | GND | GND | Black | ++-----+-----------+---------------------+-----------------------+ +| 8 | UART0_TXD | RX | White | ++-----+-----------+---------------------+-----------------------+ +| 10 | UART0_RXD | TX | Green | ++-----+-----------+---------------------+-----------------------+ + +Use a communication program with a baud rate of 115200. + +Before connecting the serial cable to your Raspberry Pi you can verify the +functionality of the cable by connecting the USB connector to your Laptop, +and connect the RX-terminal to the TX terminal of the cable. Use a communication +program to verify that text that you enter is echoed back. When removing +the RX-to-TX connection the echo should stop. + + +Optionally connect LEDs and buttons to Raspberry Pi +--------------------------------------------------- +You need these components: + ++-----------------------+-----------------+ +| Component | Number required | ++=======================+=================+ +| LED | 2 | ++-----------------------+-----------------+ +| Button switch | 2 | ++-----------------------+-----------------+ +| Resistor 220 Ohm | 4 | ++-----------------------+-----------------+ + +Connect them like: + ++------+---------+-----------------------------------------------------+ +| Pin | Name | Description | ++======+=========+=====================================================+ +| 9 | GND | | ++------+---------+-----------------------------------------------------+ +| 11 | GPIO17 | Connect LED1 (application data) to GND via 220 Ohm | ++------+---------+-----------------------------------------------------+ +| 13 | GPIO27 | Connect Button1 to 3.3V via 220 Ohm | ++------+---------+-----------------------------------------------------+ +| 15 | GPIO22 | Connect Button2 to 3.3V via 220 Ohm | ++------+---------+-----------------------------------------------------+ +| 16 | GPIO23 | Connect LED2 (Profinet signal) to GND via 220 Ohm | ++------+---------+-----------------------------------------------------+ +| 17 | 3.3V | | ++------+---------+-----------------------------------------------------+ + +The resistors for the buttons are to limit the consequences of connecting the +wires to wrong pins. + +Set up the GPIO pins for the buttons:: + + echo 22 > /sys/class/gpio/export + echo 27 > /sys/class/gpio/export + +and for the LEDs:: + + echo 17 > /sys/class/gpio/export + echo 23 > /sys/class/gpio/export + echo out > /sys/class/gpio/gpio17/direction + echo out > /sys/class/gpio/gpio23/direction + +Turn on and off a LED:: + + echo 1 > /sys/class/gpio/gpio17/value + echo 0 > /sys/class/gpio/gpio17/value + +Show state of buttons:: + + cat /sys/class/gpio/gpio22/value + cat /sys/class/gpio/gpio27/value + +.. image:: illustrations/RaspberryPiLedButtons.jpg + + +Adjust IP address if using the Raspberry Pi as a PLC +---------------------------------------------------- +If running your Raspberry Pi as a PLC (Profinet IO-Controller). you would like +to have a static IP address (it will not work if running as a Profinet IO-Device). +Instead modify the file ``/etc/dhcpcd.conf`` to include these lines:: + + interface eth0 + static ip_address=192.168.0.100/24 + +You can still ping the .local address to find it on the network. +To re-enable DHCP, remove the lines again from ``/etc/dhcpcd.conf``. + +Once you have prepared the IP address etc on the Raspberry Pi intended for +use as a PLC, it is time to install the Codesys runtime on it. See +:ref:`using-codesys`. + + +Advanced users only: Automatic start of sample application +---------------------------------------------------------- +Use systemd to automatically start the p-net sample application at boot on a +Raspberry Pi. +Place a systemd unit file here: ``/lib/systemd/system/pnet-sampleapp.service`` + +An example file is available in the ``samples/pn_dev/`` directory of this +repository. It assumes that the code is checked out into +``/home/pi/profinet/p-net/`` on your Raspberry Pi. +Install the files:: + + sudo cp /home/pi/profinet/p-net/src/ports/linux/pnet-sampleapp.service /lib/systemd/system/ + sudo cp /home/pi/profinet/p-net/src/ports/linux/enable-rpi-gpio-pins.sh /usr/bin/ + sudo chmod +x /usr/bin/enable-rpi-gpio-pins.sh + +Adapt the contents to your paths and hardware. + +Enable automatic startup:: + + sudo systemctl daemon-reload + sudo systemctl enable pnet-sampleapp.service + +Start service:: + + sudo systemctl start pnet-sampleapp.service + +To see the status of the process, and the log output:: + + systemctl status pnet-sampleapp.service + + journalctl -u pnet-sampleapp -f + +If using a serial cable, you might need to adjust the number of visible columns:: + + stty cols 150 rows 40 + +You can for example add it to your ``.bashrc`` file on the Raspberry Pi. + +In order to speed up the boot time, you might want to disable some functionality +not necessary for Profinet applications. For example:: + + sudo systemctl disable cups-browsed.service + sudo systemctl disable cups.service + +See the section "Boot time optimization" elsewhere in this documentation. + + +Advanced users only: Control of built-in LEDs +--------------------------------------------- +The Raspberry Pi board has LEDs on the board, typically a red PWR LED and a +green ACT (activity) LED. + +Manually control the green LED (ACT = ``led0``) on Raspberry Pi 3:: + + echo none > /sys/class/leds/led0/trigger + echo 1 > /sys/class/leds/led0/brightness + +And to turn it off:: + + echo 0 > /sys/class/leds/led0/brightness + +Note that you need root privileges to control the LEDs. + +Similarly for the red (power) LED, which is called ``led1``. + + +Advanced users only: Control Linux real-time properties +------------------------------------------------------- +See the page on Linux timing in this documentation for an introduction to +the subject. + +Add this to the first (and only) line in ``/boot/cmdline.txt``:: + + isolcpus=2 + +Run the sample application on a specific CPU core, by modifying the +autostart file ``/lib/systemd/system/pnet-sampleapp.service`` (if installed):: + + ExecStart=taskset -c 2 /home/pi/profinet/build/pn_dev -v -b /sys/class/gpio/gpio27/value -d /sys/class/gpio/gpio22/value + + +SD-card problems +---------------- +If you have problems with the CPU freezing for a few seconds now and then, +the SD-card might be damaged. There is a SD-card test program available +for Raspberry Pi:: + + sudo apt update + sudo apt install agnostics + +Start the test program:: + + pi@raspberrypi:~$ sh /usr/share/agnostics/sdtest.sh + +Example output for a damaged SD-card:: + + Run 1 + prepare-file;0;0;2029;3 + seq-write;0;0;2944;5 + rand-4k-write;0;0;705;176 + rand-4k-read;7444;1861;0;0 + Sequential write speed 2944 KB/sec (target 10000) - FAIL + Note that sequential write speed declines over time as a card is used - your card may require reformatting + Random write speed 176 IOPS (target 500) - FAIL + Random read speed 1861 IOPS (target 1500) - PASS diff --git a/profinet_stack/p-net/doc/profinet_basics.rst b/profinet_stack/p-net/doc/profinet_basics.rst new file mode 100644 index 0000000..ad13df5 --- /dev/null +++ b/profinet_stack/p-net/doc/profinet_basics.rst @@ -0,0 +1,376 @@ +Profinet basics +=============== +Profinet is a field bus that communicates over Ethernet, typically at a speed +of 100 Mbit/s. For details on Profinet, see +https://en.wikipedia.org/wiki/PROFINET + +A detailed introduction to Profinet is given in the book "Industrial +communication with PROFINET" by Manfred Popp. +It is available to Profinet members. + +Profinet devices (IO-devices) are controlled by an PLC (Programmable Logic +Controller), also known as an IO-controller. + + +GSD files +--------- +A GSD (General Station Description) file is an XML file describing a Profinet +IO-Device. The XML-based language is called GSDML (GSD Markup Language). + +Note that the GSD file is not used by the p-net stack or application. It is +a machine readable file describing the capabilities, hardware- and software +versions etc, and is used by the engineering tool to adjust the PLC settings. + +The GSD file describes for example which types of pluggable hardware modules +that can be used in the IO-device. The GSD file is loaded into the +engineering tool (typically running on a personal computer), +and a user can then in the tool +describe which modules that actually should be plugged into the IO-device. +This is called configuration. Later this information is downloaded to the PLC +(IO-controller), in a process called commissioning. At startup the PLC will +tell the IO-device what type of modules it expects to be plugged in. +If the correct modules not are plugged into the IO-device, the IO-device will +send an error message back to the PLC. + + +Startup description +------------------- +In the startup of an Profinet IO device (before the IP address has been set) the +DCP protocol is used. It is like DHCP, but without using a central server. + +The PLC sends a DCP broadcast message, and all IO Devices on the subnet answer +with their MAC addresses. The PLC sends a DCP message to the IO Device with +a specific MAC address, containing which IP address and station name that the +IO Device should use. The IO Device sets its IP address and station name +accordingly. + +Then the PLC starts the actual configuration of the IO device, using the +DCE/RPC protocol that runs on UDP over IP. + +After the configuration is done, cyclic data is continuously exchanged between +the IO-device and the PLC. This communication runs on Ethernet layer 2, i.e. +the MAC addresses are used for routing frames (the IP protocol is not used in +those frames). + + +Nodes classes and device details +-------------------------------- +Profinet defines three node classes: + +.. table:: + :widths: 25 50 25 + + +---------------+------------------------------------------------------------------+----------------------------+ + | Node class | Description | | Supported by | + | | | | this software | + +===============+==================================================================+============================+ + | IO-Device | "Slave" | Yes | + +---------------+------------------------------------------------------------------+----------------------------+ + | IO-Controller | "Master", often a PLC. | No | + +---------------+------------------------------------------------------------------+----------------------------+ + | IO-Supervisor | For commissioning and diagnostics, typically a personal computer | No | + +---------------+------------------------------------------------------------------+----------------------------+ + + +Depending on the capabilities, different conformance classes are assigned. + ++------------------------+---------------------------------------------+----------------------------+----------------------------+ +| | Conformance | Description | | Supported by | | Communication | +| | class (CC) | | | this software | | profile (CP) | ++========================+=============================================+============================+============================+ +| A | Wireless is only allowed for class A | Yes | CP 3/4 | ++------------------------+---------------------------------------------+----------------------------+----------------------------+ +| B | Class A + SNMP (network topology detection) | Yes | CP 3/5 | ++------------------------+---------------------------------------------+----------------------------+----------------------------+ +| B (PA) | Class B + System redundancy | No | | ++------------------------+---------------------------------------------+----------------------------+----------------------------+ +| C | Class B + IRT communication | No | CP 3/6 | ++------------------------+---------------------------------------------+----------------------------+----------------------------+ +| D | Uses time sensitive networking Ethernet | No | CP 3/7 | ++------------------------+---------------------------------------------+----------------------------+----------------------------+ + +The first digit in the Communication Profile (CP) is the Communication Profile Family (CPF). Profinet and Profibus are CPF 3, +while for example Ethercat is CPF 12. + +Communication profiles CP 3/1 and CP 3/2 are for Profibus. The CP 3/3 was used for the legacy Profinet CBA. + +Real Time Class: + +* Real Time Class 1 Mandatory for conformance class A, B, C +* Real Time Class 2 (legacy) Only for legacy startup mode for conformance class C. +* Real Time Class 3 = IRT (Isochronous Real Time) Mandatory for conformance class C +* Real Time Class UDP For cross-network real time communication. Optional for conformance class A, B, C +* Real Time Class STREAM for conformance class D. + +This software supports Real Time Class 1. + + +MAC addresses +------------- +If your device has two Ethernet connectors (ports), then it typically uses an Ethernet +switch chip with three data connections:: + + Port X1 P1 Port X1 P2 + | | + | | + | | + +--+-----------------+--+ + | | + | Ethernet Switch | + | | + +----------+------------+ + | + | + | + Profinet interface X1 + +The device has one interface and two ports. +For Profinet conformance class B and higher, there are three assigned MAC addresses: + +* Profinet interface X1 (main MAC address or device MAC address) +* Port X1 P1 +* Port X1 P2 + +The MAC addresses for X1 P1 and X1 P2 are used by the LLDP protocol for neighbor detection. + +There are examples of Profinet controllers with more than one interface (having more than one port each). +Each interface has its own IP address. Each port has its own MAC address. + + +Communication +------------- + +Profinet uses three protocol levels: + ++-----------------------------+--------------------+-------------+---------------+----------------------------+ +| Protocol | | Typical | Description | Ethertype | | Supported by | +| | | cycle time | | | | this software | ++=============================+====================+=============+===============+============================+ +| TCP/IP (UDP actually) | 100 ms | | 0x0800 = IPv4 | Yes | ++-----------------------------+--------------------+-------------+---------------+----------------------------+ +| RT (Real Time) | 10 ms | | 0x8892 | Yes | ++-----------------------------+--------------------+-------------+---------------+----------------------------+ +| IRT (Isochronous Real-Time) | 1 ms | | 0x8892? | No | ++-----------------------------+--------------------+-------------+---------------+----------------------------+ + +Profinet uses IPv4 only (not IPv6). + + +Overview of all protocols used in a Profinet Application +-------------------------------------------------------- + +A typical Profinet application needs to handle a multitude of protocols. +The p-net stack implements some of them, while others are optional. +For example HTTP is used by some IO-devices for additional configuration via +a web page, but should be implemented outside the Profinet stack itself. + ++-------+----------+-------------+-----------------+-----------------------+---------------------------------+ +| Layer | Protocol | Addressing | | Content | Protocol | Notes | +| | | | | description | | | ++=======+==========+=============+=================+=======================+=================================+ +| 4 | TCP | Port | SSH = 22 | Remote login on Linux | +| | | +-----------------------+---------------------------------+ +| | | | HTTP = 80 + 443 | Web server for configuration | ++-------+----------+-------------------------------+-----------------------+---------------------------------+ +| 4 | UDP | Port | DHCP = 67 + 68 | IP address assignment | +| | | +-----------------------+---------------------------------+ +| | | | SNMP = 161 + 162 | Network topology query | +| | | +-----------------------+---------------------------------+ +| | | | PNIO-CM = 34964 | Profinet start-up, uses DCE/RPC | ++-------+----------+-------------+-----------------+-----------------------+---------------------------------+ +| 3 | IPv4 | IP address | Protocol number | ICMP = 1 | Used by ping | +| | | | +-----------------------+---------------------------------+ +| | | | | TCP = 6 | Transmission control protocol | +| | | | +-----------------------+---------------------------------+ +| | | | | UDP = 17 | User Datagram Protocol | ++-------+----------+-------------+-----------------+-----------------------+---------------------------------+ +| 2 | Ethernet | MAC address | Ethertype | LLDP = 0x88CC | Link layer discovery | +| | | | +-----------------------+---------------------------------+ +| | | | | ARP = 0x0806 | IP address lookup | +| | | | +-----------------------+---------------------------------+ +| | | | | IPv4 = 0x0800 | Internet protocol | +| | | | +-----------------------+---------------------------------+ +| | | | | PN-DCP = 0x8892 | Profinet start-up | +| | | | +-----------------------+---------------------------------+ +| | | | | Profinet RT = 0x8892 | PNIO_PS = Cyclic IO data | +| | | | +-----------------------+---------------------------------+ +| | | | | (VLAN = 0x8100) | Not really a protocol | +| | | | +-----------------------+---------------------------------+ +| | | | | (VLAN = 0x9100) | For double tagged frames | ++-------+----------+-------------+-----------------+-----------------------+---------------------------------+ + +Profinet Profiles +----------------- +There are a number of Profinet application profiles defined, for example +PROFIenergy and PROFIdrive. These defines for example the cyclic data should +be interpreted. + +Profiles use the API (Application Program Identifier) concept for telling +that profile-specific data is transferred. + + +Slots and modules +----------------- +A Profinet IO-device has typically a number of slots where (hardware) modules +can be placed. A module can have subslots where submodules are placed. +Each submodule have a number of channels (for example digital inputs). + +Each API has its own collection of slots. + +TODO Example + +* Module +* Submodule +* Channels + +Channels are always connected to submodules (rather than to modules). + +Addressing a channel + +* Slot +* Subslot +* Index + +First usable slot is slot number 1. Slot 0 is used for the IO-device itself, +and does not have any input/output data. Instead it has diagnostic information +for the IO-device. + +If compile time setting PNET_MAX_SLOTS is 5, then the slots are numbered 0-4. +The setting PNET_MAX_SUBSLOTS controls the number of subslots per slot, +but there is no fixed relation to which subslot numbers will be used. +Subslot numbers in the range 0-0x9FFF might be used. + +The GSD file pretty much describes the hardware: slots (and subslots), and +the modules (and submodules) that can be placed in the slots. The file does +not describe which modules that actually have been placed in each slot for +each device. That is done during the setup (configuration) in the engineering +tool during PLC programming. + +Also in the GSD file is description on the data exchange? + ++----------------------+-------------------------------------+ +| Type | Description | ++======================+=====================================+ +| Compact field device | Not possible to change modules etc? | ++----------------------+-------------------------------------+ +| Modular field device | Change modules at configuration? | ++----------------------+-------------------------------------+ + + +Payload +------- +The Profinet payload is sent as big endian (network endian) on the wire. + +The maximum data (and status) size in total in each direction for a device is 1440 bytes. +If this is consumed by a single subslot the maximum data size is 1438 byte (plus one or two bytes for IOPS and IOCS). +See the IOData entry in the GSDML specification. + +If the IOPS (producer status) for a subslot goes to BAD, the PLC will indicate an error +"User data failure of hardware component". +It will persist until the communication is restarted to the IO-device. + + +I&M data records +---------------- +This is Identification & Maintenance records, which are human-readable +information about the model type, software version etc. +Some fields are writable, for example where the device is located. + +Up to I&M15 is described in the standard. The p-net stack supports I&M0 - I&M4. +You must support writing to I&M1-3 for at least one of the DAP submodules. + ++-------------+-----------+-----------------------+--------------------------------------------------------------+ +| Data record | Mandatory | Controller can write? | Description | ++=============+===========+=======================+==============================================================+ +| I&M0 | Yes | No | Vendor ID, serial number. Hardware and software version etc. | ++-------------+-----------+-----------------------+--------------------------------------------------------------+ +| I&M1 | No | Yes | Tag function and location | ++-------------+-----------+-----------------------+--------------------------------------------------------------+ +| I&M2 | No | Yes | Date. Format "1995-02-04 16:23" | ++-------------+-----------+-----------------------+--------------------------------------------------------------+ +| I&M3 | No | Yes | Descriptor | ++-------------+-----------+-----------------------+--------------------------------------------------------------+ +| I&M4 | No | Yes | Signature. Only for functional safety. | ++-------------+-----------+-----------------------+--------------------------------------------------------------+ + +There is also I&M0 Filterdata, which is read only. + + +Startup modes +------------- +The startup mode was changed in Profinet 2.3, to "Advanced". The previous +startup mode is now called "Legacy". + + +Net load class +-------------- + +* I +* II +* III + + +Alarm types +----------- +A process alarm describes conditions in the monitored process, for example +too high temperature. + +A diagnostic alarm describes conditions in the IO Device itself, for example +a faulty channel or short circuit. Diagnostic alarms are also stored in the IO-Device. + +Description of supported alarm types: + ++------------------------------------------+--------+-----------------------------------------------------------+ +| Name | Hex | Description | ++==========================================+========+===========================================================+ +| Diagnosis | 0x0001 | There is something wrong with the IO device itself | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Process | 0x0002 | There is something wrong with the process, for | +| | | example too high temperature. High priority. | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Pull | 0x0003 | Submodule pulled from subslot. | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Plug | 0x0004 | Module/submodule plugged into slot/subslot. | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Controlled by supervisor | 0x0008 | | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Released | 0x0009 | | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Plug wrong submodule | 0x000A | Wrong module/submodule plugged into slot/subslot. | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Return of submodule | 0x000B | | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Diagnosis disappears | 0x000C | A kind of diagnosis alarm | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Port data change notification | 0x000E | Port up, or peer changes name. A kind of diagnosis alarm. | ++------------------------------------------+--------+-----------------------------------------------------------+ +| Pull module | 0x001F | Module pulled from slot. | ++------------------------------------------+--------+-----------------------------------------------------------+ + +Only process alarms are sent with high prio, all other alarms use low prio. + + +Diagnosis +--------- +Diagnosis alarms are sent to indicate for example short-circuit on an output. + +To localize the diagnosis source, these values are required: + +* API +* Slot +* Subslot +* Channel number (Use 0x8000 for "whole submodule") +* Accumulative (true when describing a channel group) +* Direction (in or out. Use "manufacturer specific" for "whole submodule") + +Do not update diagnosis information at a higher frequency than 1 Hz. + + +Logbook +------- +A logbook is a circular buffer with at least 16 entries. The IO-controller can +read out entire logbook. Each entry contains: + +* A timestamp +* Error codes (4 bytes) +* A manufacturer specific entry detail (uint32_t) diff --git a/profinet_stack/p-net/doc/profinet_details.rst b/profinet_stack/p-net/doc/profinet_details.rst new file mode 100644 index 0000000..5a527dc --- /dev/null +++ b/profinet_stack/p-net/doc/profinet_details.rst @@ -0,0 +1,643 @@ +Profinet details +================ + +Technical details on some of the protocols used in a Profinet Application +-------------------------------------------------------------------------- + ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ +| Layer | | Header | | Footer | Header contents | Footer contents | +| | | size | | size | | | ++===========================+==============+================+======================================+====================================================+ +| Ethernet (layer 2) | 14 or 18 | | MAC 6+6, Ethertype 2 (VLAN 4) | | ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ +| IPv4 | 20 | | IP addr 4+4, len 2, protocol 1, etc | | ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ +| UDP | 8 | | Port 2+2, len 2, checksum 2 | | ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ +| DCE/RPC | 80 | | UUID 16+16+16, etc | | ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ +| Profinet cyclic realtime | 2 | 4 | FrameId 2 | Cycle counter 2, data status 1, transfer status 1 | ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ +| Profinet acyclic realtime | 2 | | FrameId 2 | | ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ +| Profinet DCP | 10 | | ServiceID 1, ServiceType 1, Xid, Len | | ++---------------------------+--------------+----------------+--------------------------------------+----------------------------------------------------+ + +Sizes are given in bytes. +Note that "Profinet cyclic realtime" and "Profinet acyclic realtime" run +directly on Ethernet layer 2 (they do not use IP or UDP). + +Profinet DCP runs via "Profinet acyclic realtime". + +Note that the length field in the UDP header includes the size of the header itself. + + +Profinet cyclic and acyclic realtime protocol via Ethernet layer 2 +------------------------------------------------------------------ + ++----------+-----------------------+-------------------+ +| Frame ID | Protocol | Description | ++==========+=======================+===================+ +| 0x8000 | Profinet cyclic | Output CR | ++----------+-----------------------+-------------------+ +| 0x8001 | Profinet cyclic | Input CR | ++----------+-----------------------+-------------------+ +| 0xFC01 | | ALARM_HIGH | ++----------+-----------------------+-------------------+ +| 0xFE01 | | ALARM_LOW | ++----------+-----------------------+-------------------+ +| 0xFEFC | Profinet acyclic, DCP | HELLO | ++----------+-----------------------+-------------------+ +| 0xFEFD | Profinet acyclic, DCP | GET_SET | ++----------+-----------------------+-------------------+ +| 0xFEFE | Profinet acyclic, DCP | Identify request | ++----------+-----------------------+-------------------+ +| 0xFEFE | Profinet acyclic, DCP | Identify response | ++----------+-----------------------+-------------------+ + +PNIO status (4 bytes): + +* Error code +* Error decode +* Error code 1 +* Error code 2 + + +DCP protocol via Ethernet layer 2 +--------------------------------- +Uses Profinet cyclic realtime protocol. +This is used for example for assigning station name and IP address to devices. + ++--------------+------------------+ +| Service Type | Description | ++==============+==================+ +| 0 | Request | ++--------------+------------------+ +| 1 | Response Success | ++--------------+------------------+ + ++------------+-------------+ +| Service ID | Description | ++============+=============+ +| 3 | Get | ++------------+-------------+ +| 4 | Set | ++------------+-------------+ +| 5 | Identify | ++------------+-------------+ +| 6 | Hello | ++------------+-------------+ + ++-------------+--------+-----------+------------------------+------------------------------------+ +| Service IDs | Option | Suboption | Description | Contains | ++=============+========+===========+========================+====================================+ +| 3 | 1 | 1 | MAC address | | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 3, 4, 5, 6 | 1 | 2 | IP parameter | IP address, netmask, gateway | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 3, 4, 5, 6 | 1 | 3 | Full IP suite | IP address, netmask, gateway, DNS | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 3, 5 | 2 | 1 | Type of station | Device vendor | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 3, 4, 5, 6 | 2 | 2 | Name of station | Also permanent/temporary | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 3, 5, 6 | 2 | 3 | Device ID | VendorID, DeviceID | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 3, 5 | 2 | 4 | Device role | ? | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 3, 5 | 2 | 5 | Device options | Which options are available | ++-------------+--------+-----------+------------------------+------------------------------------+ +| Filter only | 2 | 6 | Alias name | | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 6 | 2 | 8 | OEM device ID | | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 4 | 5 | 1 | Start transaction | | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 4 | 5 | 2 | End transaction | | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 4 | 5 | 3 | Signal (Flash LED) | Flash once | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 4 | 5 | 4 | Response | | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 4 | 5 | 6 | Reset to factory | Type of reset | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 5 | 255 | 255 | All | | ++-------------+--------+-----------+------------------------+------------------------------------+ +| 6 | 6 | 1 | Device initiative | Issues Hello at power on | ++-------------+--------+-----------+------------------------+------------------------------------+ + +Setting the station name and IP address etc: + +* Permanent: The values should be used after power cycling +* Temporary: After power cycling the station name should be ``""`` and the IP address ``0.0.0.0`` + + +DCE/RPC protocol via UDP +------------------------ +In the connect request, the IO-controller (PLC) tells the IO-device how it +believes that the IO-device hardware is set up. If that not is correct, the +IO-device will complain. + +Message types: + +* "Request" sent from system A +* "Indication" when it is received in system B +* "Response" sent back from system B +* "Confirmation" when received in system A + +The "Response" and "Confirmation" can contain a positive value (+, ACK) or negative +value (-, NACK, indicating an error). + +Most often (DCE/RPC) requests are sent from the IO-controller, but CControl +request and a few alarm requests are sent from the IO-device. + +The section 5.2.40 "PDU checking rules" in the standard describes what to check in +incoming DCE/RPC messages via UDP. + +Messages from controller to device: + +* Connect request +* Parameter end request ? +* Application ready response +* Read IM0 request +* Release request +* DControl request +* CControl confirmation +* IODRead request +* IODWrite request + +Where: + +* DControl: Request to IO-device (End of parameterization) +* CControl: Request to IO-controller (Application ready) + +Operations: + +* 0: Connect +* 1: Release +* 2: Read +* 3: Write +* 4: Control +* 5: Read Implicit +* 6: Reject +* 9: Fragment acknowledge + +UDP ports: + +* 0x8892 = 34962 Port for RT_CLASS_UDP +* 0x8894 = 34964 Listening port for incoming requests, both on IO-device and IO-controller. +* 0xC000 = 49152 and up Ephemeral port range +* 0xC001 = 49153 Ephemeral port for CControl sending??? + +UDP port numbers are described in Profinet 2.4 section 4.13.3.1.2.4 + + +NDR header in DCE/RPC payload +----------------------------- +The first part of the DCE/RPC payload is the NDR (Network Data Representation) header. For requests, it contains five uint32 values: + +* Args Maximum: Buffer size available for the response +* Args Length: Number of bytes payload after the NDR header +* Maximum Count: In requests this it the same values as the Args Maximum. For responses this is the Args Maximum from the request. +* Offset: Always 0. +* Actual Count: Same as Args Length + +The Maximum Count, Offset and Actual Count are known as the "Array" block. + +In responses there is no Args Maximum field. Instead there is a status field, with these subfields: + +* code +* decode +* code1 +* code2 + + +DCE/RPC payload +--------------- +Examples of block identifiers: + +* 0x0001 AlarmNotificationHigh +* 0x0002 AlarmNotificationLow +* 0x0008 IODWriteReqHeader +* 0x0009 IODReadReqHeader +* 0x0010 DiagnosisData +* 0x0019 LogBookData +* 0x0020 I&M0 +* 0x0021 I&M1 +* 0x0101 ARBlockReq +* 0x0102 IOCRBlockReq +* 0x0103 AlarmCRBlockReq +* 0x0104 ExpectedSubmoduleBlockReq +* 0x0110 IODControlReq +* 0x8001 AlarmAckHigh +* 0x8002 AlarmAckLow +* 0x8008 IODWriteResHeader +* 0x8009 IODReadResHeader + + +DCE/RPC UUID entities +--------------------- + ++-----------------------+-------------+-------------------------------------------------------------------------------+ +| UUID variant | Section | Description | ++=======================+=============+===============================================================================+ +| Object UUID | 4.10.3.2.8 | ``dea00000-6c97-11d1-8271-`` then vendorID, device ID etc | ++-----------------------+-------------+-------------------------------------------------------------------------------+ +| PNIO Interface UUID | 4.10.3.2.9 | ``dea00001-6c97-11d1-8271-00a02442df7d`` for IO devices | ++-----------------------+-------------+-------------------------------------------------------------------------------+ +| Activity UUID | 4.10.3.2.10 | Same in request and response for one group of Connect-Write-DControl-Ccontrol | ++-----------------------+-------------+-------------------------------------------------------------------------------+ +| AR UUID | 5.2.5.47 | Can be the same between several connect attempts. | ++-----------------------+-------------+-------------------------------------------------------------------------------+ +| CMInitiatorObjectUUID | 5.1.2 | ``dea00000-6c97-11d1-8271-`` then InstanceID and deviceIdentNumber. | +| | | Only available in Connect requests. | ++-----------------------+-------------+-------------------------------------------------------------------------------+ + +The section column refers to the Profinet 2.4 Protocol document. + + +UDP message fragmentation +------------------------- +Profinet has a mechanism (part of DCE/RPC via UDP) to split large frames +(for start-up messages) into smaller fragments. Operating systems, for example +Linux, have a competing mechanism to split frames into fragments. + +If sending a large chunk of data via UDP in Linux, it is automatically split +into fragments. The maximum transfer unit (MTU) is often 1500 bytes, +including the IP header (but not the Ethernet header). An IP header is +typically 20 bytes, but some rarely used options would make it larger. +Without any IP header options, the largest IP payload would then be 1480 bytes +and the largest UDP payload would be 1472 bytes. It seems that for Linux, the +largest UDP payload is 1464 bytes before the kernel fragments the message. + + +Communication relations +----------------------- + ++-----------------------------+----------------------------------------------------------------------------------------------+ +| Communication Relation (CR) | Description | ++=============================+==============================================================================================+ +| IO data CR | Real-time cyclic data. Unacknowledged. | ++-----------------------------+----------------------------------------------------------------------------------------------+ +| Record data CR | Non-real time configuration data, for example parameter assignment and device identification | ++-----------------------------+----------------------------------------------------------------------------------------------+ +| Alarm CR | Real-time alarms | ++-----------------------------+----------------------------------------------------------------------------------------------+ + + +Details on slots and modules +---------------------------- +Subslots 0x8000-0xFFFF are reserved by the Profinet standard. + +Subslots in the DAP module: + +* 0x1 The device itself +* 0x8000 (32768) Interface 1 (typically named X1) +* 0x8001 (32769) Port 1 of interface 1 (typically named X1 P1) +* 0x8002 (32770) Port 2 of interface 1 (typically named X1 P2) +* 0x8100 (33024) Interface 2 +* 0x8101 (33025) Port 1 of interface 2 +* 0x8102 (33026) Port 2 of interface 2 + + +Read and write indexes +---------------------- +User defined indexes are in the range 0x?? to 0x?? + +Examples of pre-defined indexes: + +* 0x802B PDPortDataCheck for one subslot +* 0xAFF0 I&M0 +* 0xAFF1 I&M1 +* 0xAFF2 I&M2 +* 0xAFF3 I&M3 +* 0xF830 LogBookData +* 0xF840 I&M0FilterData +* 0xF841 PDRealData + ++-----------------------------+--------------------------------------------------------+ +| Data record | Description | ++=============================+========================================================+ +| APIData | | ++-----------------------------+--------------------------------------------------------+ +| ARData | | ++-----------------------------+--------------------------------------------------------+ +| ARFSUDataAdjust | | ++-----------------------------+--------------------------------------------------------+ +| AutoConfiguration | | ++-----------------------------+--------------------------------------------------------+ +| CombinedObjectContainer | | ++-----------------------------+--------------------------------------------------------+ +| ExpectedIdentificationData | | ++-----------------------------+--------------------------------------------------------+ +| I&M0FilterData | | ++-----------------------------+--------------------------------------------------------+ +| LogBookData | | ++-----------------------------+--------------------------------------------------------+ +| ModuleDiffBlock | | ++-----------------------------+--------------------------------------------------------+ +| PDExpectedData | | ++-----------------------------+--------------------------------------------------------+ +| PDInterfaceAdjust | Use standard LLDP mode or legacy LLDP mode. | ++-----------------------------+--------------------------------------------------------+ +| PDInterfaceDataReal | Actual station name (not ChassisID), interface MAC | +| | address, IP address etc. | ++-----------------------------+--------------------------------------------------------+ +| PDInterfaceFSUDataAdjust | | ++-----------------------------+--------------------------------------------------------+ +| PDInterfaceSecurityAdjust | | ++-----------------------------+--------------------------------------------------------+ +| PDIRData | | ++-----------------------------+--------------------------------------------------------+ +| PDIRSubframeData | | ++-----------------------------+--------------------------------------------------------+ +| PDNCDataCheck | | ++-----------------------------+--------------------------------------------------------+ +| PDPortDataAdjust | Turn on and off LLDP transmission. | ++-----------------------------+--------------------------------------------------------+ +| PDPortDataCheck | Wanted peer chassisID, port ID etc. | ++-----------------------------+--------------------------------------------------------+ +| PDPortDataReal | Actual PortID, peer PortID, peer ChassisID, peer MAC | +| | address, MAU type, link state etc. | ++-----------------------------+--------------------------------------------------------+ +| PDPortDataRealExtended | | ++-----------------------------+--------------------------------------------------------+ +| PDPortStatistic | Sent and received bytes. Errors and discards. | ++-----------------------------+--------------------------------------------------------+ +| PDRealData | Actual values and statistics for interface and port. | ++-----------------------------+--------------------------------------------------------+ +| PDSyncData | | ++-----------------------------+--------------------------------------------------------+ +| PDTimeData | | ++-----------------------------+--------------------------------------------------------+ +| PE_EntityFilterData | | ++-----------------------------+--------------------------------------------------------+ +| PE_EntityStatusData | | ++-----------------------------+--------------------------------------------------------+ +| PE_EntityStatusData | | ++-----------------------------+--------------------------------------------------------+ +| RealIdentificationData | Which modules (and submodules) are plugged into which | +| | slots (and subslots). | ++-----------------------------+--------------------------------------------------------+ +| SubstituteValue | | ++-----------------------------+--------------------------------------------------------+ + + +Allowed station name +-------------------- +The specification is found i Profinet 2.4 section 4.3.1.4.16 + + +Diagnosis details +----------------- +There are several formats in which the diagnosis information can be sent to +the PLC. The formats are described by the USI value. + +Channel diagnosis (USI 0x8000, one of the standard formats): + + * Channel number + * Channel properties (direction) + * Channel error type + +Extended channel diagnosis (USI 0x8002, one of the standard formats): + + * Same as for "Channel diagnosis", and also + * ExtChannelErrorType + * ExtChannelErrorAddValue + +Qualified channel diagnosis (USI 0x8003, one of the standard formats): + + * Same as for "Extended channel diagnosis", and also + * Qualified sub severities + +USI format (USI 0x0000 to 0x7FFF): + + * Manufacturer data + +Storage of diagnosis info +^^^^^^^^^^^^^^^^^^^^^^^^^ +There is at most one diagnosis +entry stored for each ChannelErrortype, extChannelErrorType combination. + + +LLDP +---- +A protocol for neighbourhood detection. LLDP frames are not forwarded by managed +switches, so the frames are useful to detect which neighbour the device is +connected to. + +An LLDP frame is sent by a Profinet device every 5 seconds, to indicate +the IP address etc. + +A Profinet device also receives LLDP frames. It uses the chassis ID and the +frame ID of the frame from its neighbour, to set the alias name. + +The LLDP frame is a layer 2 Ethernet frame with the payload consisting of a +number of Type-Length-Value (TLV) blocks. The first 16 bits of each block +contains info on the block type and block payload length. +It is followed by the block payload data. +Different TLV block types may have subtypes defined (within the payload). + +The frame is broadcast to MAC address ``01:80:c2:00:00:0e`` and +has an Ethertype of ``0x88cc``. + +TLV types: + +* 0: (End of LLDP frame indicator) +* 1: Chassis ID. Subtypes 4: MAC address. 7: Locally assigned name +* 2: Port ID. Subtypes 3: MAC address. 7: Locally assigned name +* 3: Time to live in seconds +* 4: Port description +* 5: System name +* 7: Capabilities (Router, Bridge, Telephone etc) +* 8: Management address (optional for LLDP, mandatory in Profinet). Includes IP + address and interface number. Address subtype 1: IPv4 2: IPv6 +* 127: Organisation specific (optional for LLDP. See below.). Has an + organisation unique code, and a subtype. + +Organisation unique code ``00:0e:cf`` belongs to Profibus Nutzerorganisation, +and supports these subtypes: + +* 1: Measured delay values +* 2: Port status. Contains RTClass2 and RTClass3 port status. +* 5: Chassis MAC address + +Organisation unique code ``00:12:0f`` belongs to the IEEE 802.3 organisation, +and supports these subtypes: + +* 1: MAC/PHY configuration status. Shows autonegotiation support, and which + speeds are supported. Also MAU type. +* 4: Maximum frame size + +Autonegotiation: + +* Bit 0: Supported +* Bit 1: Enabled + +Speed: + +* Bit 0: 1000BASE-T Full duplex +* Bit 1: 1000BASE-T Half duplex +* Bit 10: 100BASE-TX Full duplex +* Bit 11: 100BASE-TX Half duplex +* Bit 13: 10BASE-T Full duplex +* Bit 14: 10BASE-T Half duplex +* Bit 15: Unknown speed + +MAU types: + +* 0x00 Radio +* 0x10 Copper 100BaseTX Full duplex +* 0x1E Copper 1000BaseT Full duplex + + +Standard and legacy LLDP name format +------------------------------------ +The contents of the LLDP fields PortID and ChassisID were changed in +Profinet v 2.3. An example will show the differences: + ++-------------------------------+------------+ +| NameOfStation (for interface) | "dut" | ++-------------------------------+------------+ +| NameOfPort | "port-001" | ++-------------------------------+------------+ +| NameOfStation of neighbour | "b" | ++-------------------------------+------------+ +| NameOfPort of neighbour | "port-003" | ++-------------------------------+------------+ + +We have an alias name for each of our ports, and it is constructed from +the information about the connected neighbour. In this example, our +alias name for our port would be "port-003.b". + +Legacy LLDP mode (up to and including Profinet v 2.2): + ++-----------------+------------+ +| LLDP_PortID | "port-001" | ++-----------------+------------+ +| LLDP_ChassisID | "dut" | ++-----------------+------------+ + +Standard LLDP mode (Profinet v 2.3 and later): + ++-----------------+----------------------------------------+ +| LLDP_PortID | "port-001.dut" | ++-----------------+----------------------------------------+ +| LLDP_ChassisID | devicetype+orderID+serialNumber etc | ++-----------------+----------------------------------------+ + +If NameOfStation not yet is set, we instead use our device MAC address +(as a string) when constructing the LLDP strings. + +The legacy LLDP mode is used by devices conformant to Profinet v2.2 and earlier. +Later Profinet versions uses the standard LLDP mode, but an IO-device can +be ordered by the PLC at startup to use the legacy LLDP mode. +By setting the ``MultipleInterfaceMode`` to ``1`` (instead of ``0``) the PLC +will ask the IO-device to use the standard LLDP mode. This is done by writing +to the PDInterfaceAdjust index. + + +Media redundancy +---------------- +With media redundancy an Ethernet ring topology is used, so that Ethernet frames +can reach all devices in the case of a link failure. To prevent frames to +circulate in the ring forever, a manager will keep one of its links blocked +for normal packets. +If the manager detects a failure in the ring, it will open both its links. + +The detection of the ring integrity is done using the Media Redundancy Protocol +(MRP). The manager sends MRP_Test frames in both direction, and verifies that +it self can receive them. In case of ring failure, it will send a +MRP_TopologyChange frame to all clients to inform them that they should clean +their routing tables. Clients can also tell the manager that a link has failed, +by sending a MRP_LinkChange frame. + +The MRP implementation will open and close network ports, but will not +affect the Profinet class A and B operation. + +Devices supporting media redundancy needs to have at least two Ethernet ports +and an integrated switch. + +For Profinet class C there is also Media redundancy with duplicated frames +(MRPD), also known as seamless or bumpless redundancy. It sends two similar +frames with cyclic data in the different ring directions. The frames have +identical FrameID but use different VLAN IDs. In case of a link failure there +is cyclic data available from the other direction. + +Profinet also has the concept of system redundancy, where two PLCs with +automatic switchover are used. There are different levels of system +redundancy, named R1, R2, S1 and S2. + +====================== ================= =============== +Media redundancy class Conformance class Comments +====================== ================= =============== +RED_CLASS_1 A and B Uses MRP +RED_CLASS_3 C Uses MRPD +RED_CLASS_STREAM D +====================== ================= =============== + +An "interconnect ring" connects two MRP rings together, and has four nodes (two +from each normal MRP ring). This requires 3 Ethernet ports on each device. +The interconnect functionality have two modes: + +* LC: Link Check (used by Profinet). The status of each link is monitored by the + Ethernet hardware +* RC: Ring Check. Test frames are sent in the interconnect ring. + +In summary, these are the available MRP roles: + +* MRP client (MRC). Mandatory if MRP is used for Profinet. +* MRP manager (MRM) +* MRP manager with auto negotiation (MRA) +* MRP interconnection client (MIC) +* MRP interconnection manager (MIM) + +The nodes participating in the interconnection ring will also have a role in +the normal ring. Thus a node can be for example bot a MRC and a MIM +simultaneusly. + +Profinet defines alarms for different events in the MRP implementation, for +example if multiple MRP managers exist in the same ring or if the neighbour +belongs to wrong MRP ring. + +It is possible to to read out the MRP status via the SNMP protocol. This is +done using the MRP-MIB. It is optional for Profinet devices to implement this. + +See IEC 62439-2 for MRP protocol details. + + +Relevant standards +------------------ + +* IEC IEEE 60802 TSN Profile for Industrial Automation +* IEC 61158-5-10 PROFINET IO: Application Layer services for decentralized periphery (Also known as PNO-2.712) +* IEC 61158-6-10 PROFINET IO: Application Layer protocol for decentralized periphery (Also known as PNO-2.722) +* IEC 61784 Describes several fieldbuses, for example Foundation Fieldbus, Profibus and Profinet. +* IEC 61784-2 Profiles for decentralized periphery (Also known as PNO-2.742) +* IEC 62439-2 Media Redundancy Protocol +* IEEE 802 LANs +* IEEE 802.1 Higher Layer LAN Protocols +* IEEE 802.1AB LLDP (A topology detection protocol) +* IEEE 802.1AS Time synchronization +* IEEE 802.1Q Virtual LANs (VLAN) +* IEEE 802.3 Ethernet +* IEEE 802.11 WiFi +* IETF :rfc:`768` UDP +* IETF :rfc:`791` IP +* IETF :rfc:`792` ICMP +* IETF :rfc:`826` ARP +* IETF :rfc:`1034` DNS +* IETF :rfc:`1157` SNMP +* IETF :rfc:`1213` Management Information Base v 2 (MIB-II) +* IETF :rfc:`2131` DHCP +* IETF :rfc:`2132` DHCP Options +* IETF :rfc:`2741` AgentX protocol for SNMP subagents +* IETF :rfc:`2863` The Interfaces Group MIB +* IETF :rfc:`3414` SNMPv3 +* IETF :rfc:`3418` Management Information Base (MIB) for SNMP +* IETF :rfc:`3635` Definitions of Managed Objects for the Ethernet-like Interface Types +* IETF :rfc:`5890` Internationalized Domain Names for Applications (IDNA) +* ISO/IEC 7498-1 ? +* ISO 8859-1 ? +* ISO 15745 ? +* Open Group C706 Remote procedure calls (RPC) DCE 1.1 https://publications.opengroup.org/c706 diff --git a/profinet_stack/p-net/doc/sampleapp_details.rst b/profinet_stack/p-net/doc/sampleapp_details.rst new file mode 100644 index 0000000..5d9bc08 --- /dev/null +++ b/profinet_stack/p-net/doc/sampleapp_details.rst @@ -0,0 +1,237 @@ +Sample app details +================== + +Cyclic data timing +------------------ +The sample application GSDML file has the element ````, which +has an attribute ``SendClock="32"`` in units of 1/32 milliseconds. +It corresponds to 1 ms, and thus the IO-device will send cyclic +data every millisecond. Note that the sample application does not read +the GSDML file, the file just describes the hard coded value used +in the sample application. + +The sample application timer uses ``APP_TICK_INTERVAL_US`` which is +1000 microseconds = 1 millisecond. To be able to tell this information to +others, the value ``32`` is given in the ``min_device_interval`` field of the +device configuration. + +In the PLC you can set the reduction ratio to slow down the data exchange, +and this information is sent to the IO-device on startup. +For example a reduction ratio of 4 will cause the cyclic data to +be sent with 4 times longer interval (i.e. every 4 ms) from the IO-device. + + +Using more modules +------------------ +If necessary, increase the values for ``PNET_MAX_SLOTS`` and +``PNET_MAX_SUBSLOTS`` in ``include/pnet_api.h``. + +In the sample app, the input data is written to all input modules ("8 bit in + +8 bit out" and "8 bit in"). The LED is controlled by the output module ("8 bit +in + 8 bit out" or "8 bit out") with lowest slot number. + +The alarm triggered by button 2 is sent from the input module with lowest slot +number (if any). + +In order to handle larger incoming DCE/RPC messages (split in several frames), +you might need to increase PNET_MAX_SESSION_BUFFER_SIZE. Note that this will +increase the memory consumption. + + +Sample app data payload +----------------------- +This example is for when using the “8 bit in + 8 bit out” module in slot 1. +The periodic data sent from the sample application IO-device to IO-controller +is one byte: + +* Lowest 7 bits: A counter which has its value updated every 10 ms +* Most significant bit: Button1 + +When looking in Wireshark, look at the "Profinet IO Cyclic Service Data Unit", +which is 40 bytes. The relevant byte it the fourth byte from left in this +block. + +Details of the 40 bytes from IO-device to IO-controller: + +* IOPS from slot 0, subslot 1 +* IOPS from slot 0, subslot 0x8000 +* IOPS from slot 0, subslot 0x8001 +* IO data from slot 1, subslot 1 (input part of the module) +* IOPS from slot 1, subslot 1 (input part of the module) +* IOCS from slot 1, subslot 1 (output part of the module) +* (Then 34 bytes of padding, which is 0) + +From the IO-controller to IO-device is one data byte sent: + +* Most significant bit: LED + +When looking in Wireshark, look at the "Profinet IO Cyclic Service Data Unit", +which is 40 bytes. The relevant byte is the fifth byte from left in this +block. The value toggles between 0x80 and 0x00. + +Details of the 40 bytes from IO-controller to IO-device: + +* IOCS to slot 0, subslot 1 +* IOCS to slot 0, subslot 0x8000 +* IOCS to slot 0, subslot 0x8001 +* IOCS to slot 1, subslot 1 (input part of the module) +* IO data to slot 1, subslot 1 (output part of the module) +* IOPS to slot 1, subslot 1 (output part of the module) +* (Then 34 bytes of padding, which is 0) + +Note that Wireshark can help parsing the cyclic data by using the GSDML file. +See the Wireshark page in this document. + + +Ethernet frames sent during start-up +------------------------------------ +For this example, the IO-controller is started first, and then the IO-device. + ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| Sender | Protocol | Content | ++===========================+==========+======================================================================================================================+ +| IO-controller (broadcast) | LLDP | Name, MAC, IP address, port name (sent every 5 seconds) | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller (broadcast) | PN-DCP | "Ident req". Looking for "rt-labs-dev" (sent every 2.5 seconds) | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller (broadcast) | ARP | Is someone else using my IP? | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device (broadcast) | LLDP | Name, MAC, IP address, port name (sent every 5 seconds??) | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device (broadcast) | PN-DCP | "Hello req". Station name "rt-labs-dev", IP address, gateway, vendor, device (sent every 3 seconds) | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PN-DCP | "Ident OK" Identify response. Station name "rt-labs-dev", IP address, netmask, gateway, VendorID, DeviceID, options | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller | PN-DCP | "Set Req" Set IP request. Use IP address and gateway. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PN-DCP | "Set OK" Status. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller (broadcast) | ARP | Who has ? | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | ARP | IP is at | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller | PNIO-CM | "Connect request" Controller MAC, timeout, input + output data (CR), modules + submodules in slots | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PNIO-CM | "Connect response" MAC address, UDP port, input + output + alarm CR, station name | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PNIO-PS | FrameID 0x8001. Cycle counter, provider stopped. 40 bytes data. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller | PNIO-PS | FrameID 0x8000. Cycle counter, provider running. 40 bytes data. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller | PNIO-CM | "Write request" API, slot, subslot, data. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PNIO-CM | "Write response" API, slot, subslot, status. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller | PNIO-CM | "Control request" (DControl). Command: ParameterEnd. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PNIO-CM | "Control response" Command: Done | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PNIO-PS | FrameID 0x8001. Cycle counter, provider running. 40 bytes data. | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-device | PNIO-CM | "Control request" (CControl). Command: ApplicationReady | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ +| IO-controller | PNIO-CM | "Control response" Command: ApplicationReadyDone | ++---------------------------+----------+----------------------------------------------------------------------------------------------------------------------+ + +The order of the PNIO-PS frames is somewhat random in relation to PNIO-CM frames. + ++------------+---------------------------------+ +| Protocol | Description | ++============+=================================+ +| LLDP | | ++------------+---------------------------------+ +| ARP | | ++------------+---------------------------------+ +| PN-DCP | Acyclic real-time data | ++------------+---------------------------------+ +| PNIO-PS | Cyclic real-time data | ++------------+---------------------------------+ +| PNIO-AL | Acyclic real-time alarm | ++------------+---------------------------------+ +| PNIO-CM | UDP, port 34964 = 0x8892 | ++------------+---------------------------------+ + + +Ethernet frames sent at alarm +----------------------------- +Frames sent when pressing button 2. + ++---------------+----------+----------------------------------------------------------------------------------------+ +| Sender | Protocol | Content | ++===============+==========+========================================================================================+ +| IO-device | PN-AL | Alarm notification high, slot, subslot, module, submodule, sequence, 1 byte user data | ++---------------+----------+----------------------------------------------------------------------------------------+ +| IO-controller | PN-AL | ACK-RTA-PDU | ++---------------+----------+----------------------------------------------------------------------------------------+ +| IO-controller | PN-AL | Alarm ack high, alarm type Process, slot, subslot, Status OK | ++---------------+----------+----------------------------------------------------------------------------------------+ +| IO-device | PN-AL | ACK-RTA-PDU | ++---------------+----------+----------------------------------------------------------------------------------------+ + + +Cyclic data for the different slots +----------------------------------- +This is an example if you populate slot 1 to 3 with different modules. + ++------+---------+--------------------------------------------+-----------------------------------------+--------------------------------------------+ +| Slot | Subslot | Description | | Contents of Input CR | | Contents of Output CR | +| | | | | (to IO-controller) | | (from IO-controller) | ++======+=========+============================================+=========================================+============================================+ +| 0 | 1 | The IO-Device itself | (data) + IOPS | IOCS | ++------+---------+--------------------------------------------+-----------------------------------------+--------------------------------------------+ +| 0 | 0x8000 | Interface 1 | (data) + IOPS | IOCS | ++------+---------+--------------------------------------------+-----------------------------------------+--------------------------------------------+ +| 0 | 0x8001 | Port 0 of interface 1 | (data) + IOPS | IOCS | ++------+---------+--------------------------------------------+-----------------------------------------+--------------------------------------------+ +| 1 | 1 | Input part of "8-bit in 8-bit out" module | data (1 byte) + IOPS | IOCS | +| | +--------------------------------------------+-----------------------------------------+--------------------------------------------+ +| | | Output part of "8-bit in 8-bit out" module | IOCS | data (1 byte) + IOPS | ++------+---------+--------------------------------------------+-----------------------------------------+--------------------------------------------+ +| 2 | 1 | "8-bit in" module | data (1 byte) + IOPS | IOCS | ++------+---------+--------------------------------------------+-----------------------------------------+--------------------------------------------+ +| 3 | 1 | "8-bit out" module | IOCS | data (1 byte) + IOPS | ++------+---------+--------------------------------------------+-----------------------------------------+--------------------------------------------+ + +Note that the submodules (in subslots) in slot 0 do not send any cyclic data, +but they behave as inputs (as they send cyclic IOPS). + +Output (from PLC):: + + +----------------+ +-----------+ + | IO-device | | PLC | + | +----------+ | DATA | | + | | | | <----------- | | + | | CPM | | | | + | | | | IOPS | | + | | | | <----------- | | + | +----------+ | | | + | | | | + | +----------+ | | | + | | | | | | + | | PPM | | | | + | | | | IOCS | | + | | | | -----------> | | + | +----------+ | | | + | | | | + +----------------+ +-----------+ + + +Input (to PLC):: + + +----------------+ +-----------+ + | IO-device | | PLC | + | +----------+ | IOCS | | + | | | | <----------- | | + | | CPM | | | | + | | | | | | + | | | | | | + | +----------+ | | | + | | | | + | +----------+ | DATA | | + | | | | -----------> | | + | | PPM | | | | + | | | | IOPS | | + | | | | -----------> | | + | +----------+ | | | + | | | | + +----------------+ +-----------+ diff --git a/profinet_stack/p-net/doc/spelling_wordlist.txt b/profinet_stack/p-net/doc/spelling_wordlist.txt new file mode 100644 index 0000000..914bb9f --- /dev/null +++ b/profinet_stack/p-net/doc/spelling_wordlist.txt @@ -0,0 +1,223 @@ +ack +acyclic +Adafruit +AgentX +Anel +Args +autonegotiation +Avahi +BaseT +BaseTX +BaseTXFD +baudrate +Bittwist +Bonjour +Bool +boolean +booleans +breakpoint +bsp +bss +Buildroot +bumpless +ccmake +CControl +ccontrol +Changelog +chassisID +CiR +CMake +cmake +cnf +Codesys +collab +Commonmark +comparator +conformant +COntroller +customise +Datagram +DControl +dcontrol +DCP +defence +Democontroller +dev +DEvelopment +DInt +Doxygen +dropdown +dut +DWord +Einstellung +endian +Endian +endianness +enums +Ethercat +Ethertype +exe +eXtended +Fieldbus +fieldbuses +FilterData +Gbit +GByte +gcc +gdb +Github +gtest +hostname +Icinga +Ident +ident +IEC +ifAdminStatus +ifDescr +ifDescr +ifEntry +ifIndex +ifIndex +ifInDiscards +ifInErrors +ifInOctets +ifMtu +ifOperStatus +ifOutDiscards +ifOutErrors +ifOutOctets +ifPhysAddress +ifPhysAddress +ifSpeed +ifTable +ifType +imager +implementers +Infineon +init +Interoperability +IOxS +Ip +iPar +IPv +IPv4 +iso +Isochronous +J-link +jitter +journalctl +kB +libprofinet +libreNMS +lldpConfigManAddrPortsTxEnable +lldpd +lldpLocPortDesc +lwip +lwIP +makefiles +Mbit +MByte +MIB +mib +microcontroller +multicast +multicore +nano +neighbour +neighbourhood +neighbours +netload +netmask +nmap +ns +Nutzerorganisation +orderID +organisation +osal +Parameterization +parameterization +persistency +pcap +picocom +PLC +pluggable +pnet +Popp +prio +PRMEND +Profibus +Profinet +Proneta +PwrCtrl +qemu +Raspbian +realtime +remanence +repo +req +resends +Responder +reST +reStructuredText +rfc +rootfs +rtlabs +runtime +Scalance +Segger +severities +Simatic +snmp +snmpd +snmpwalk +softplc +stationname +Steuerung +structs +subagent +subdirectory +subelement +subelements +subfields +subframes +subitem +submodule +submodules +subnet +suboption +suboptions +subparts +subslot +subslots +subtab +subtree +subtrees +subtype +subtypes +sudo +sysContact +sysDescr +sysLocation +sysName +sysObjectId +sysServices +sysUpTime +tcpdump +toolchain +txt +uint +UInt +unicast +Unmount +uptime +vendorID +Virtualbox +WiFi +Wireshark +xBusy +xError +xmc +xml +xRunning +Yocto +Zabbix diff --git a/profinet_stack/p-net/doc/static/css/change_header_size.css b/profinet_stack/p-net/doc/static/css/change_header_size.css new file mode 100644 index 0000000..56e6635 --- /dev/null +++ b/profinet_stack/p-net/doc/static/css/change_header_size.css @@ -0,0 +1,3 @@ +/* Reduce subheading size */ + +h3{font-size: 110%;} diff --git a/profinet_stack/p-net/doc/static/css/fix_table_width.css b/profinet_stack/p-net/doc/static/css/fix_table_width.css new file mode 100644 index 0000000..76e9a3e --- /dev/null +++ b/profinet_stack/p-net/doc/static/css/fix_table_width.css @@ -0,0 +1,15 @@ +/* From https://rackerlabs.github.io/docs-rackspace/tools/rtd-tables.html */ + +/* override table width restrictions */ +@media screen and (min-width: 500px) { + + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from overriding + this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } + + .wy-table-responsive { + overflow: visible !important; + } +} diff --git a/profinet_stack/p-net/doc/tutorial.rst b/profinet_stack/p-net/doc/tutorial.rst new file mode 100644 index 0000000..b65eaf9 --- /dev/null +++ b/profinet_stack/p-net/doc/tutorial.rst @@ -0,0 +1,565 @@ +.. _tutorial: + +Tutorial +======== +In this tutorial we will run the p-net Profinet device stack and its +sample application on a Raspberry Pi, which is an embedded Linux board. +You can optionally connect two LEDs and two buttons to the Raspberry Pi +for easier interaction with the sample application. + +We will use a second Raspberry Pi as a PLC (Programmable Logic Controller = +IO controller) running Codesys soft PLC. + +Necessary hardware to complete the tutorial: + +* 1 Raspberry Pi as IO-device +* 1 Raspberry Pi as IO-controller (alternatively a Siemens PLC). +* 1 Ethernet switch +* 3 Ethernet cables + +Optional hardware: + +* Keyboard, mouse and monitor for the Raspberry Pi running as IO-device. If + this not is available it is helpful with a USB-to-serial cable for talking + to the Raspberry Pi (as it will automatically change IP address now and then). +* LEDs and buttons for connecting to the Raspberry Pi running as IO-device. + +It will take approximately 30 minutes to get p-net running on a Raspberry Pi. +After this the sample application will wait for incoming connects. +An additional hour is required to get another Raspberry Pi up and running +as an IO-controller (PLC), and to study the sample application data. + + +Sample app description +---------------------- +The sample application implements a Profinet IO-device, having an +IO-module with 8 digital inputs and 8 digital outputs. The example uses +two buttons connected to inputs and one LED connected to one of the outputs +(we call this the "data LED"). + +A second LED is connected as the Profinet "Signal LED", which can be flashed in +order to identify a particular IO-device if you have many devices. + +.. image:: illustrations/TutorialOverview.png + +The LED1 ("data LED") on the IO-device is controlled by the IO-controller +(PLC), and is normally flashing. With Button1 on the IO-device it is +possible to tell the PLC to turn on and off the flashing of the LED. + +Button2 triggers sending an alarm from the IO-device to the PLC. + +* Button1: Setting cyclic data value +* Button2: Trigger alarm, setting diagnosis etc +* LED1: Data LED +* LED2: Signal LED + +The resulting Ethernet traffic can be studied (see below). + + +Notes to advanced users +----------------------- +The IO-device sample application can be running on: + +* Raspberry Pi (as described in this tutorial) +* Some other embedded Linux board +* A Linux laptop (or a Linux guest in Virtualbox) +* An embedded board running an RTOS, such as RT-kernel + +Instead of a Codesys soft PLC running on a second Raspberry Pi, you can +use a Siemens Simatic PLC. See another page of this documentation. + + +Available files +--------------- +The ``samples/pn_dev`` directory in the p-net repository contains the source code +for this example. It also contains a GSD file (written in GSDML), which tells +the IO-controller how to communicate with the IO-device. + +Those parts of the sample application that are dependent on whether you run +Linux or an RTOS are located in ``src/ports``. + + +Modules and slots +----------------- +Slots are locations where you can put modules. + +The GSDML file for the sample app defines these modules: + ++----------------------+-------------------------------+----------------------------------+ +| Module | Input data (to IO-controller) | Output data (from IO-controller) | ++======================+===============================+==================================+ +| 8 bit in + 8 bit out | 1 byte | 1 byte | ++----------------------+-------------------------------+----------------------------------+ +| 8 bit in | 1 byte | | ++----------------------+-------------------------------+----------------------------------+ +| 8 bit out | | 1 byte | ++----------------------+-------------------------------+----------------------------------+ + +There are 4 slots for the sample app (in addition to slot 0 which is used by the +DAP module), and for this sample app any of the modules fit in any of +the slots 1 to 4. + +In this example we will use the "8 bit in + 8 bit out" module in slot 1. + + +Set up the IO-device Raspberry Pi for running p-net +--------------------------------------------------- +As the PLC typically will change the IP address of the IO-device, +we recommend that you connect a keyboard, mouse and monitor to the Raspberry +Pi running the p-net sample application. Alternatively you can use a +USB-to-serial cable to communicate with the Raspberry Pi from your laptop. + +To setup Raspbian on a Raspberry Pi, and optionally connect buttons and LEDs, +see :ref:`prepare_raspberry`. + +A LED is controlled by the Linux sample app by writing to a file, for example +``/sys/class/gpio/gpio17/value``. A ``0`` or ``1`` will be written to the +file upon LED state changes. This is done by a script, for easy adaptation to +your hardware. + +If you do not have a physical LED, you can use an alternate script that +writes to plain text files instead. Usage is described below. + + +Install dependencies +-------------------- +Your Raspberry Pi needs to be connected to Internet via LAN or WiFi to be +able to download software. + +In order to compile p-net on Raspberry Pi, you need a recent version of +``cmake``. Install it:: + + sudo apt update + sudo apt install snapd + sudo reboot + sudo snap install cmake --classic + +Verify the installed version:: + + cmake --version + +Compare the installed version with the minimum version required for p-net +(see first page). + +You also need ``git`` to download p-net. Install it using:: + + sudo apt install git + + +Download and compile p-net +-------------------------- +Create a directory:: + + mkdir /home/pi/profinet/ + cd /home/pi/profinet/ + +Clone the source:: + + git clone --recurse-submodules https://github.com/rtlabs-com/p-net.git + +This will clone the repository with submodules. + +Then create and configure the build:: + + cmake -B build -S p-net + +Build the code:: + + cmake --build build --target install + +We used the ``install`` target to install scripts for manipulating IP +settings, control LEDs etc. + +Instead of controlling real LEDs, the default behavior is to write LED output +to regular files. If you have connected real LEDs to your Raspberry Pi, +enable the LED control script:: + + mv build/set_profinet_leds build/set_profinet_leds.disabled + mv build/set_profinet_leds.raspberrypi build/set_profinet_leds + + +Notes to advanced users +----------------------- +If you already cloned the repository without the ``--recurse-submodules`` +flag then run this in the ``p-net`` folder:: + + git submodule update --init --recursive + +Alternate cmake command to also adjust some settings:: + + cmake -B build -S p-net -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=OFF -DBUILD_SHARED_LIBS=ON -DUSE_SCHED_FIFO=ON + +You can choose any name for the build folder, for instance if you want +to build different configurations. + +You can use the ``-j`` flag to ``make`` if you like to enable parallel build. + +Depending on how you installed cmake, you might need to run ``snap run cmake`` +instead of ``cmake``. + +It is possible to specify the location of the submodule repositories. +See the end of this page for details. + + +Run the sample application +------------------------------ +Run the sample app in the build directory:: + + cd build + +Usage of the IO-device sample application: + +.. code-block:: none + + pi@pndevice-pi:~/profinet/build$ ./pn_dev -h + + Sample application for p-net Profinet device stack. + + Wait for connection from IO-controller. + Then read buttons (input) and send to controller. + Listen for application LED output (from controller) and set application LED state. + It will also send a counter value (useful also without buttons and LED). + Button1 value is sent in the periodic data. + Button2 cycles through triggering an alarm, setting diagnosis and creating logbook entries. + + Also the mandatory Profinet signal LED is controlled by this application. + + The LEDs are controlled by the script set_profinet_leds + located in the same directory as the application binary. + A version for Raspberry Pi is available, and also a version writing + to plain text files (useful for demo if no LEDs are available). + + Assumes the default gateway is found on .1 on same subnet as the IP address. + + Optional arguments: + --help Show this help text and exit + -h Show this help text and exit + -v Incresase verbosity. Can be repeated. + -f Reset to factory settings, and store to file. Exit. + -r Remove stored files and exit. + -g Show stack details and exit. Repeat for more details. + -i INTERF Name of Ethernet interface to use. Defaults to eth0 + -s NAME Set station name. Defaults to rt-labs-dev Only used + if not already available in storage file. + -b FILE Path (absolute or relative) to read Button1. Defaults to not read Button1. + -d FILE Path (absolute or relative) to read Button2. Defaults to not read Button2. + -p PATH Absolute path to storage directory. Defaults to use current directory. + + p-net revision: 0.1.0+bb4177a + +Enable the Ethernet interface and set the initial IP address:: + + sudo ifconfig eth0 192.168.0.50 netmask 255.255.255.0 up + +Run the sample application:: + + sudo ./pn_dev -v + +Example output: + +.. code-block:: none + + pi@pndevice-pi:~/profinet/build$ sudo ./pn_dev -v + + ** Starting Profinet sample application 0.1.0+bb4177a ** + Number of slots: 5 (incl slot for DAP module) + P-net log level: 3 (DEBUG=0, FATAL=4) + App verbosity level: 1 + Number of ports: 1 + Network interfaces: eth0 + Button1 file: + Button2 file: + Station name: rt-labs-dev + Management port: eth0 + Physical port [1]: eth0 + Current hostname: pndevice-pi + Current IP address: 192.168.0.50 + Current Netmask: 255.255.255.0 + Current Gateway: 192.168.0.1 + Storage directory: /home/pi/profinet/build + + Profinet signal LED call-back. New state: 0 + Network script for eth0: Set IP 192.168.0.50 Netmask 255.255.255.0 Gateway 192.168.0.1 Permanent: 1 Hostname: rt-labs-dev Skip setting hostname: true + Module plug call-back + Pull old module. API: 0 Slot: 0 Slot was empty. + Plug module. API: 0 Slot: 0 Module ID: 0x1 + Submodule plug call-back. + Pull old submodule. API: 0 Slot: 0 Subslot: 1 Subslot was empty. + Plug submodule. API: 0 Slot: 0 Module ID: 0x1 Subslot: 1 Submodule ID: 0x1 "DAP Identity 1" + Data Dir: NO_IO In: 0 Out: 0 (Exp Data Dir: NO_IO In: 0 Out: 0) + Submodule plug call-back. + Pull old submodule. API: 0 Slot: 0 Subslot: 32768 Subslot was empty. + Plug submodule. API: 0 Slot: 0 Module ID: 0x1 Subslot: 32768 Submodule ID: 0x8000 "DAP Interface 1" + Data Dir: NO_IO In: 0 Out: 0 (Exp Data Dir: NO_IO In: 0 Out: 0) + Submodule plug call-back. + Pull old submodule. API: 0 Slot: 0 Subslot: 32769 Subslot was empty. + Plug submodule. API: 0 Slot: 0 Module ID: 0x1 Subslot: 32769 Submodule ID: 0x8001 "DAP Port 1" + Data Dir: NO_IO In: 0 Out: 0 (Exp Data Dir: NO_IO In: 0 Out: 0) + Waiting for connect request from IO-controller + +The IP settings are stored to file. If you accidentally have run the application +when IP settings were wrong, use this command to remove the stored settings:: + + sudo ./pn_dev -r + +Now you have installed the sample app on the Raspberry Pi, congratulations! +In order to see it in action, you need to connect it to a PLC. + + +Set up the PLC +-------------- +We suggest that you use Codesys soft PLC. +Install Raspberry Pi OS on the second Raspberry Pi. No serial cable or LEDs are +required. + +See :ref:`prepare_raspberry` and :ref:`using-codesys` for how to set it up as an IO-controller (PLC). + +Connect the two Raspberry Pi boards and your laptop via an Ethernet switch. + + +Print-out from Linux sample application when connecting to PLC +-------------------------------------------------------------- + +.. highlight:: none + +This is the typical output from the Linux sample application at startup if +you enable verbose output:: + + pi@pndevice-pi:~/profinet/build$ sudo ./pn_dev -v -b /sys/class/gpio/gpio22/value -d /sys/class/gpio/gpio27/value + + ** Starting Profinet sample application 0.1.0+bb4177a ** + Number of slots: 5 (incl slot for DAP module) + P-net log level: 3 (DEBUG=0, FATAL=4) + App verbosity level: 1 + Number of ports: 1 + Network interfaces: eth0 + Button1 file: /sys/class/gpio/gpio22/value + Button2 file: /sys/class/gpio/gpio27/value + Station name: rt-labs-dev + Management port: eth0 + Physical port [1]: eth0 + Current hostname: pndevice-pi + Current IP address: 192.168.0.50 + Current Netmask: 255.255.255.0 + Current Gateway: 192.168.0.1 + Storage directory: /home/pi/profinet/build + + Profinet signal LED call-back. New state: 0 + Network script for eth0: Set IP 0.0.0.0 Netmask 0.0.0.0 Gateway 0.0.0.0 Permanent: 1 Hostname: rt-labs-dev Skip setting hostname: true + No valid default gateway given. Skipping setting default gateway. + Module plug call-back + Pull old module. API: 0 Slot: 0 Slot was empty. + Plug module. API: 0 Slot: 0 Module ID: 0x1 + Submodule plug call-back. + Pull old submodule. API: 0 Slot: 0 Subslot: 1 Subslot was empty. + Plug submodule. API: 0 Slot: 0 Module ID: 0x1 Subslot: 1 Submodule ID: 0x1 "DAP Identity 1" + Data Dir: NO_IO In: 0 Out: 0 (Exp Data Dir: NO_IO In: 0 Out: 0) + Submodule plug call-back. + Pull old submodule. API: 0 Slot: 0 Subslot: 32768 Subslot was empty. + Plug submodule. API: 0 Slot: 0 Module ID: 0x1 Subslot: 32768 Submodule ID: 0x8000 "DAP Interface 1" + Data Dir: NO_IO In: 0 Out: 0 (Exp Data Dir: NO_IO In: 0 Out: 0) + Submodule plug call-back. + Pull old submodule. API: 0 Slot: 0 Subslot: 32769 Subslot was empty. + Plug submodule. API: 0 Slot: 0 Module ID: 0x1 Subslot: 32769 Submodule ID: 0x8001 "DAP Port 1" + Data Dir: NO_IO In: 0 Out: 0 (Exp Data Dir: NO_IO In: 0 Out: 0) + Waiting for connect request from IO-controller + + Network script for eth0: Set IP 192.168.0.50 Netmask 255.255.255.0 Gateway 0.0.0.0 Permanent: 0 Hostname: rt-labs-dev Skip setting hostname: true + No valid default gateway given. Skipping setting default gateway. + Module plug call-back + Pull old module. API: 0 Slot: 1 Slot was empty. + Plug module. API: 0 Slot: 1 Module ID: 0x32 + Submodule plug call-back. + Pull old submodule. API: 0 Slot: 1 Subslot: 1 Subslot was empty. + Plug submodule. API: 0 Slot: 1 Module ID: 0x32 Subslot: 1 Submodule ID: 0x1 "Input 8 bits output 8 bits" + Data Dir: INPUT_OUTPUT In: 1 Out: 1 (Exp Data Dir: INPUT_OUTPUT In: 1 Out: 1) + Connect call-back. AREP: 1 Status codes: 0 0 0 0 + Callback on event PNET_EVENT_STARTUP AREP: 1 + New data status callback. AREP: 1 Data status changes: 0x35 Data status: 0x35 + Run, Valid, Primary, Normal operation, Evaluate data status + Parameter write call-back. AREP: 1 API: 0 Slot: 1 Subslot: 1 Index: 123 Sequence: 2 Length: 4 + Bytes: 00 00 00 05 + Parameter write call-back. AREP: 1 API: 0 Slot: 1 Subslot: 1 Index: 124 Sequence: 3 Length: 4 + Bytes: 00 00 00 06 + Dcontrol call-back. AREP: 1 Command: PRM_END + Callback on event PNET_EVENT_PRMEND AREP: 1 + Set input data and IOPS for slot 0 subslot 1 "DAP Identity 1" size 0 IOXS_GOOD + Set input data and IOPS for slot 0 subslot 32768 "DAP Interface 1" size 0 IOXS_GOOD + Set input data and IOPS for slot 0 subslot 32769 "DAP Port 1" size 0 IOXS_GOOD + Set input data and IOPS for slot 1 subslot 1 "Input 8 bits output 8 bits" size 1 IOXS_GOOD + Set output IOCS for slot 1 subslot 1 "Input 8 bits output 8 bits" + Application will signal that it is ready for data. + Callback on event PNET_EVENT_APPLRDY AREP: 1 + Setting outputs to default values. + Ccontrol confirmation call-back. AREP: 1 Status codes: 0 0 0 0 + Callback on event PNET_EVENT_DATA AREP: 1 + Setting outputs to default values. + +The exact output will depend on for example which modules you use when setting +up the PLC. + + +Input buttons and LEDs, or files for simulation +----------------------------------------------- +If you use plain files as output instead of LEDs, use this to study the file +for the "Data LED":: + + watch -n 0.1 cat /home/pi/profinet/build/pnet_led_1.txt + +If you would like to use physical input buttons you must set up the +GPIO files for buttons properly first:: + + echo 22 > /sys/class/gpio/export + echo 27 > /sys/class/gpio/export + +Then:: + + sudo ./pn_dev -v -b /sys/class/gpio/gpio27/value -d /sys/class/gpio/gpio22/value + +It is possible to use plain files as inputs instead of physical buttons:: + + touch /home/pi/profinet/build/button1.txt + touch /home/pi/profinet/build/button2.txt + sudo ./pn_dev -v -b /home/pi/profinet/build/button1.txt -d /home/pi/profinet/build/button2.txt + +Manually write ``1`` or ``0`` to a file to simulate the button press and +release:: + + echo 1 > /home/pi/profinet/build/button1.txt + echo 0 > /home/pi/profinet/build/button1.txt + +If you only have one terminal, you need to run ``pn_dev`` in the background +to be able to run these commands. +That is done by adding a ``&`` at the end of the command to start ``pn_dev`` . +Later on kill the ``pn_dev`` process by using ``sudo pkill pn_dev``. + + +Study the resulting communication +--------------------------------- +LED1 should be flashing by default. Press Button1 to toggle LED1 +flashing. + +By pressing Button2 you can trigger alarms, add diagnosis etc. See the +printout in the console. + +On the page "Capturing and analyzing Ethernet packets" is a description given +on how to study the network traffic. If you are interested in the different +packets sent during startup or the cyclic data payload, see the page +"Sample app details". + + +Adjust log level +---------------- +There is logging available in the p-net stack describing the interaction +with the PLC. + +If you would like to change the p-net stack log level, run ``ccmake .`` in +the ``build`` directory. It will start a menu program. Move to the +LOG_LEVEL entry, and press Enter to change to DEBUG. Press ``c`` to save +and ``q`` to exit. + +You need to re-build the project for the changes to take effect. + + +Next steps +---------- +Great! You managed to get the sample application running. + +Try flashing the Profinet signal LED. See description on the page "Using +Codesys soft PLC". + +To enable automatic start of the sample application on power on, see the page +"Install Raspberry Pi OS on the Raspberry Pi". + +For Profinet members the "ART tester" tool is available for conformance +testing. Run the conformance tests against the sample app to verify that the +stack is compliant. See a separate page on conformance testing in this +documentation. + +To experiment with the SNMP features of conformance class B, see the page +"Network topology detection". + +Now it is time for you to start developing your own applications. You can use +the sample app as a starting template. +Experiment by modifying the available modules, and the data types they send +and receive. Modify your GSDML file accordingly to explain the IO-device +behavior to the PLC configuration tool. + +A separate page is available with a few ideas on how to write you application. +Remember to run the "ART tester" now and then to verify that you stay compliant. + + +Timing issues +------------- +If running on a Linux machine without realtime patches, you might face timeout +problems. It can look like:: + + Callback on event PNET_EVENT_ABORT. Error class: 253 Error code: 6 + +where the error code most often is 5 or 6. +See the "Real-time properties of Linux" page in this document for solutions, and +the "Using Codesys soft PLC" page for workarounds. + + +Debug compilation issues +------------------------ +To show more details on the compilation, use:: + + cmake --build build -v + + +Troubleshooting +--------------- +If you have network problems on your IO-device Raspberry Pi, re-run the +``ifconfig`` command given above. + +If you have problems establishing a connection to your PLC, connect it +directly to your laptop and run the program Wireshark on the corresponding +Ethernet interface. Study the DCP and LLDP frames to see the current PLC +settings. See another page in this documentation for details on Wireshark usage. +The "Management Address" block in a LLDP frame shows the IP address of the PLC. +There are also other blocks describing the MAC address and the port ID. +You can find the expected IO-device station name in some DCP frames. + + +Advanced users: OSAL +-------------------- +OSAL is a generic OS abstraction library that may be used by multiple +projects in a system. To avoid issues with multiple copies of the +library, possibly of conflicting versions, it has been moved to its +own repository. + +``cmake-tools`` is a repository that contains common CMake utilities for +RT-Labs projects. It contains a CMake script ``AddOsal.cmake`` that +simplifies use of OSAL. It supports two different use-cases: + + +1) Automatic download and build of OSAL +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +During CMake configuration, if OSAL is not found in the system it will +be downloaded and built automatically. For most users this will be the +default. +Run CMake configuration by issuing e.g.:: + + cmake -B build -S p-net + + +2) External OSAL +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +During CMake configuration, if OSAL is found then p-net will just link +against the external library. +CMake will find the external OSAL library if it is installed in a +default location such as ``/usr/include`` or ``/usr/local/include``. This +could be the case for a native build or a cross-compiled Linux system +with a staging folder. + +CMake can also be told of the path to an installed version of OSAL by +setting ``Osal_DIR`` during configuration, like so:: + + cmake -B build -S p-net -DOsal_DIR=/path/to/osal/install/cmake + +The install folder is produced when running:: + + make install + +or similar in the OSAL build directory. diff --git a/profinet_stack/p-net/doc/use_with_siematic.rst b/profinet_stack/p-net/doc/use_with_siematic.rst new file mode 100644 index 0000000..24e5142 --- /dev/null +++ b/profinet_stack/p-net/doc/use_with_siematic.rst @@ -0,0 +1,699 @@ +Using a Simatic PLC as IO controller +==================================== +This have been tested with: + +* Simatic S7-1200 CPU1215C DC/DC/DC 6ES7 215-1AG40-0xXB0 Firmware version 4.2 + +It uses a 24 V DC supply voltage. + + +Install Siemens TIA on a Windows PC +----------------------------------- +Install TIA (SIMATIC STEP 7 and WinCC) V15.1 + +Download the files from +`https://support.industry.siemens.com/cs/document/109752566/simatic-step-7-and-wincc-v15-trial-download-?dti=0&lc=en-US `_ + +* "SIMATIC STEP 7 Basic V15.1 is the easy-to-use engineering system for the + small modular SIMATIC S7-1200 controller and the associated I/Os." +* "SIMATIC STEP 7 Professional V15.1 is the high-performance, integrated + engineering system for the latest SIMATIC controllers S7-1500, S7-1200, + S7-300, S7-400, WinAC and ET 200 CPU." + +"SIMATIC WinCC is a supervisory control and data acquisition (SCADA) and +human-machine interface (HMI) system from Siemens." +See https://en.wikipedia.org/wiki/WinCC + +Download "WinCC Professional". Use all files from "DVD 1 Setup". The "DVD 2" +files are not necessary. + +The installer is split in a number of files, each 2 GByte. +Put the files in a directory on your Windows PC. +To start the installation, double-click the exe file. Restart of your computer +is required. + +To show installed software version, use the "Portal view" and the "Start" tab +to the left of the screen. Click "Installed software". + +After installation, make sure "PROFINET IO protocol (DCP/LLDP)" is enabled +for your Ethernet interface on your laptop. Otherwise you will have problems +communicating to Profinet devices. Remember however that this setting is +interfering with the "ART Tester" compliance verification tool, so disable +it before starting the "ART Tester" on your laptop. + + +Using Siemens TIA on a Windows PC: Add devices +---------------------------------------------- +Start the application by searching for "TIA portal" in the Windows start menu. + +In the start screen, select "Create new project" and enter the details. +Also on the start screen, select "Devices and networks", "Add new device" and +select your PLC model (a CPU that will act as IO-controller). +If you only know the series of your PLC, but not the exact model, then select +for example "Controller" > "SIMATIC S7-1200" > "CPU" > "Unspecified CPU 1200" > +"6ES7 2XX-". Click Add. In the "Device view" you will be notified that the +device not is specified. Click "detect". Make sure your PLC is powered on. +Select "PN/IE" and your Ethernet interface and "Start search". Mark the found +PLC and click "Detect". + +Import a GSD file by using the menu "Options > Manage GSD files". Browse to +the directory with your GSD file. Mark the line with the file, and click +"Install". + +In the project view, in the left menu select the PLC and the subitem "Device +configuration". Click the "Network view" tab. At the right edge open the +"Hardware catalog". Select "Other field devices" > Profinet IO > I/O > +RT-Labs > "P-Net Samples" > "P-Net Sample App". +Double-click it, and it will appear in the main window. + +Use the "Network view" tab, and right-click "Not assigned" on the rt-labs-dev +icon. Select "Assign to new IO-controller" and "PLC_1.PROFINETinterface_1". + + +Set IP addresses and connect physically +--------------------------------------- +Connect to the Siemens PLC directly via an Ethernet cable to your laptop. For +the Simatic S7-1200 PLC you can use any of the two X1 RJ45 connectors. Your +laptop should have a manual IP address in the same subnet range as the PLC +(first three groups in IP address should be the same). Changing the subnet +mask size does not seem to have an impact. + +In the project view, in the left menu select the PLC and the subitem "Device +configuration". In the "Properties" tab, use the "General" sub-tab. Select +"PROFINET interface [X1]" and "Ethernet addresses". Enter the IP address +``192.168.0.100`` for the PLC. The subnet mask should be ``255.255.255.0`` +and subnet "PN/IE_1". +Right-click the icon of the PLC, and select "Go online". Use type of +interface "PN/IE", your Ethernet network card and "Direct at slot 1 X1". +Select "Show accessible devices" in the dropdown. Click "Start search". +The table should be updated with "Device type" = "CPU 1215C..." +if the connection is OK. + +To enter the IP-address of the IO-device, go to "Device view" for the IO-device +and click the IO-device icon. in the "Properties" tab, select the "General" +sub-tab. Select +"PROFINET interface [X1]" and "Ethernet addresses". Enter the IP address +``192.168.0.50`` for the IO-device. +Also in the “PROFINET interface [X1]” section, set the IO cycle update time +to "Set update time manually" and select 16 ms. (Depending on your hardware +you can try faster updates later on). + + +Add modules to IO-device +------------------------ +In the device view, select the "rt-labs-dev" device (by using the dropdown). +Add a module to the "Device overview" tab. That is done by dragging a module +from the "Hardware catalog", which is located to the right of screen. +The modules are found in "Other field devices" > Profinet IO > I/O > rt-labs > +rt-labs dev > Module. Drop the module in the correct slot row in the "Device +overview table". For the tutorial use a "DIO 8xLogicLevel" module in slot 1. +Only modules that fit in the respective slot seems to stick. + + +Run the application +------------------- +Connect one Ethernet cable between the PLC and the Windows laptop, and one +Ethernet cable between the PLC and the IO-device. + +In the network view, right-click the PLC icon and select Compile > "Hardware +(rebuild all)". Repeat for Compile > "Software (rebuild all)". Then right-click +and select "Download to device" > "Hardware configuration". +Click Load in the pop-up window, and then Finish. +Repeat for "Download to device" > "Software (all)". + +In the main menu, use Online > "Go online". In the right part of the screen, +use "Online tools" to see the PLC LED states and to go to RUN and STOP modes. + +There will be cyclic communication regardless whether the PLC is in RUN or +STOP mode, and the run state information is available in the cyclic data. + +No PLC program is necessary for the cyclic communication to take place, but +the payload from the PLC is probably zeros. + + +Change IO-device station name +----------------------------- +To change which IO-device name the PLC will connect to, use the network view +and click the icon of the IO-device. +Select and change the +name (on top row) in the icon. Do a download (hardware and software) to the +PLC. You can verify the result by looking at the PN-DCP frames in Wireshark. + + +Connect inputs and outputs +-------------------------- +First find the address of the IO-device input byte and output byte. In the +“Device view” for the IO-device, look in the “Device overview” table. +The module "8 bits I +8 bits O" should appear (if previously inserted). Look for the I (input) +address and Q (output) address. The value can be for example 2. +Note that the IO-device must be connected to the PLC (as seen for example in +the network view) for the IO-device to have a PLC address assigned. + +In the Project tree, select PLC_1 > PLC tags > Show all tags. +Create a new tag by clicking the first line and enter the name ``ButtonIn``. +Use "Default tag table" and DataType Bool. The address should be of operand +identifier "I" (input). Use address from the "Device view" as described above. +The bit number should be 7. This is written as ``%I2.7``. + +Create a new tag ``LEDout``, also of Bool type. The bit number should be 7, so +the address should be for example ``%Q2.7``. + +In order to study the values while running, you need to create an watch table. +In the Project tree, select PLC_1 > "Watch and force table" > "Add new watch +table". In the first empty line, double-click on the small icon on the Name field. +Select "ButtonIn". Repeat on next line with "LEDout". + +When running, in order to study the values, connect to the PLC ("Online"). On +the "Watch table_1" page, click the small "Monitor all" icon. The values on the +page will be continuously updated. + + +Enter PLC program +----------------- +In the project tree, under PLC1 > Program blocks > Add new block. In the pop-up +window, select "Function block" and language SCL. Give it the name ``Flasher``. +Click "OK". + +Note that Structured Control Language (SCL) is Siemens name for Structured Text +(ST) programming language. + +In the "Flasher [FB1]" window upper part, add an input pin in the "Input" +section. Name it ``in_pin_button_LED``, and give it the data type Bool. In the +"Output" section, add a ``out_pin_LED`` which also should be Bool. +In the "Static" section, add: + +* ``in_pin_button_LED_previous``: BOOL +* ``flashing``: BOOL +* ``oscillator_state``: BOOL +* ``oscillator_cycles``: Int + +.. highlight:: none + +In the program part of the window, insert this:: + + #oscillator_cycles := #oscillator_cycles + 1; + IF #oscillator_cycles > 400 THEN + #oscillator_cycles := 0; + #oscillator_state := NOT #oscillator_state; + END_IF; + + IF #in_pin_button_LED = TRUE THEN + IF #in_pin_button_LED_previous = FALSE THEN + #flashing := NOT #flashing; + END_IF; + #out_pin_LED := TRUE; + ELSIF #flashing = TRUE THEN + #out_pin_LED := #oscillator_state; + ELSE + #out_pin_LED := FALSE; + END_IF; + #in_pin_button_LED_previous := #in_pin_button_LED; + +In the "Main [OB1]" block, drag the "Flasher" icon to the "Network 1" line. +The icon is found in the left-side menu "PLC_1" > "Program blocks". +In the pop up asking for data block, select "Flasher_DB". + +Connect the input on the "Flasher" block by double-clicking it. Select "ButtonIn". +Similarly connect the output to "LEDout". + +.. image:: illustrations/FlasherFunctionBlock.png + +The block "Flasher_DB [DB1]" has been created automatically. All the input-, +output- and static variables should appear inside it. + +Compile the program and download it to the PLC. Verify that it works as +intended. + + +Adjust data cycle time and watchdog +----------------------------------- +In the device view, select the IO-device in the dropdown. Use the "Properties" +tag and "General" subtab. Select "PROFINET interface [X1]" > Advanced options +> Real time settings. Adjust "Update time" in ms and the watchdog time (which +is the allowed number of cycles without data before an alarm is triggered). + + +Adjust parameter values for a slot +---------------------------------- +Use the "Device view" for the IO-device while you are offline from the PLC. +In the "Device overview", select the line for the relevant slot. +In the "Properties" tab and the "General" subtab, select "Module parameters". +A list of parameters is seen. Set the value in the text box. + + +Forcing output signals +---------------------- +In the Project tree, select PLC_1 > "Watch and force table" > "Force table". +In the first empty line, double-click on the small icon on the Name field. +Select "LEDout". In the "Force value" column enter `TRUE`, and enable +the checkbox in the "F" column. +Go online, and then click the small "Start or replace forcing ..." icon. +Confirm in the pop-up window. To stop forcing the signal, press the small +"Stops forcing of ..." icon. Note that it not possible to download software +to the PLC while it is forcing output signals. + + +Opening an archived project +---------------------------- +Open an archived project by using the project view menu Project > Retrieve +and select the ``.zap15_1`` file. Create a new empty folder when asked for +target directory. + + +Setting date and time on PLC +---------------------------- +To set the time zone, in the "Device view" select Properties > General > +"Time of day". Use appropriate time zone. + +To set the date and time, go online to the PLC. In the Project tree on +the "PLC_1", right-click and select "Online & diagnostics". Use Functions > +"Set time". + + +Alarm when IO-device is terminated +---------------------------------- +Approximately 7-8 ms after the last cyclic data frame is received from the +IO-device, the Simatic PLC will send an alarm frame about missing data (if +using default values). + +The Wireshark tool will display:: + + Status: Error: "RTA error", "PNIO", "RTA_ERR_CLS_PROTOCOL", "AR consumer DHT/WDT expired (RTA_ERR_ABORT)" + +Display alarms in the PLC +------------------------- +While online, right-click on the PLC and enable "Receive alarms". Alarms are +then visible in "Device view" > Diagnostics > "Alarm display". + + +Show connection errors to IO-device +----------------------------------- +If you are connected to the PLC ("online"), then it is possible to see if +there are communication problems to the IO-device. In the project tree > PLC_1 +> Distributed I/O > Profinet IO-System > rt-labs-dev. In case of communication +errors, the hover text on the small icon is showing "Not reachable". + + +Display diagnosis in the PLC +---------------------------- +To see diagnosis in standard format for an IO-device, use the "Device view" for +the IO-device while you are "Online" with the PLC. +Press the "Go online" in the menu bar. +In the "Device overview" +there is a table of slots and subslot. A red icon will be shown for the slot +with the diagnosis. Double-click the icon. In the new window select +"Diagnostics" > "Channel diagnostics". The error will be described and it is +possible to see for which channel it is reported. + +To see a list of previous communication failures for the PLC, make sure you +are online with the PLC. In the Project tree on the "PLC_1", right-click and +select "Online & diagnostics". +Select Diagnostics -> "Diagnostics buffer" in the left part of the new window. +A list of previous problems is seen. Click on relevant row to see details. + +There is also a "Diagnosis status" page that shows a summary of the current +status. + + +Scan for devices from TIA portal +-------------------------------- +In the "Portal view", click "Online & Diagnostics" and then "Accessible devices". +In the pop-up window, select "PN/IE" and the name of the Ethernet interface +of your laptop. Click "Start search". +You can flash a LED on your device by marking the relevant line in the +result table, and mark the "Flash LED" check-box. + +To change network settings for a device, mark the relevant line in the table +of found devices, and click "Show". The "Project tree" will open, and under +"Online access" in the left menu, select the relevant Ethernet interface. +If necessary click "Update accessible devices". Select relevant found device, +and use the small triangle to access its "Online & diagnostics" button. +Double-click it. A new window will open, and in its "Functions" sections it is +possible to assign IP address and to assign Profinet device name. + +It is also possible to reach the tool to scan for devices via the main top menu +Online > "Accessible devices". + + +Troubleshooting +--------------- +Note that the PLC might complain if there is a non-Profinet switch between the +IO-device and the IO-controller. This is probably due to wrong port names in LLDP messages. + + +Replace a device or a PLC +------------------------- +To replace an IO-device or an IO-controller, right-click on it in the left +side menu and select "Change device". Follow the wizard. + + +Using the Echo module +--------------------- +The echo module will receive an integer and a float from the PLC, and multiply them with a constant +value before sending them back to the PLC. The multiplier is module parameter, and can be adjusted +at startup. The integer is an unsigned 32 bit integer, and the float is a single precision float +(32 bits). + +To test it, unplug any existing modules, and plug one Echo module into slot 1. + +Check the resulting addresses for the inputs and outputs of the module (by looking in the +"Device overview"). Typically the addresses are "0..7" for both the input (I) and output (Q) addresses. + +Assuming these addresses, add these tags: + +============== =========== ======= +Name Data type Address +============== =========== ======= +EchoFloatIn LReal %ID0 +EchoIntIn UDInt %ID4 +EchoFloatOut LReal %QD0 +EchoIntOut UDInt %QD4 +============== =========== ======= + +In a program block, define these values: + +======== =========== ============ +Section Name Data type +======== =========== ============ +Input in_float Real +Input in_int UDInt +Output out_float Real +Output out_int UDInt +Temp temp_float Real +Temp temp_int UDInt +======== =========== ============ + +and enter this program:: + + #out_float := 1001.2345; + #out_int := 16; + + #temp_float := #in_float; + #temp_int := #in_int; + +It is not clear why the IO data signals not are shown in the user interface. + +Add the program block to the "Main" program block. Connect "in_float" to "EchoFloatIn" etc. + +Add "EchoFloatIn" and the three other signals to the watch table. +Add "EchoFloatOut" and "EchoIntOut" to the Force table. See description elsewhere on how to force the values. + +Start the PLC, and go online. Study the values sent to and from the IO-device. + + +Reload an GSDML file +-------------------- +When the GSDML file is updated and needs to be reloaded in the Siemens environment: + +1. Delete all devices in your project that are based on the GSDML that shall be changed. +2. Check the "Force" table. + + * Stop forcing all values. + * Delete entries in table. + +3. Open "Options/Manage general station description files" + + * In "Installed GSDs" tab, delete GSDML file + * in "GSDs in the project", "Find unused GSDs" and delete + +4. Save project +5. Restart TIA and add your updated GSDML file and recreate your device. + If you you are still facing problems you can try repeat the described + sequence and also completely remove all tags. + + +Factory reset of Simatic ET200SP CPU +------------------------------------ +Use the mode switch on the front panel to do a factory reset. See the user +manual for details. This will reset also the IP address. + +Connect the PLC to your laptop, and run Wireshark to figure out the IP address. +It is given inside the LLDP frame. Also the detailed model name, firmware +version etc are given in the LLDP frame. + + +Upgrade firmware on a Siemens PLC +--------------------------------- +Select the proper firmware to use for your PLC from the Siemens downloads +page. With the latest firmware you need a recent version of the STEP7 software. +Download the file, which is in ``.zip`` format. Unzip the file. + +In TIA Portal, right-click the PLC and select "Online and Diagnostics". In +the "Functions" sections, use "Firmware update". Browse to the downloaded file +(in ``.upd`` format) and start the update. + + +Using a Simatic ET200SP IO-device for conformance test +------------------------------------------------------ +See the page on conformance testing in this documentation for hardware details. + +This type of hardware is used when testing multiple-port IO-devices, to verify +that it is possible to communicate with other IO-devices via the ports of the +device-under-test. + +You can also use this hardware for communication reference instead of the +p-net sample app. For that use case, configure the Simatic IO-device similarly +as described for the p-net sample application above. Select the proper device +from the hardware catalog in the STEP7 software. + +In STEP7, add the digital output module (DQ) in slot 1 and the digital input +module (DI) in slot 2. The server module should be inserted into slot 3. + + +Step7 naming +------------ + +* DB - Data block for storage +* FB - Function block, that uses data blocks. +* FC - Function without any storage +* LGF - Library of general functions +* OB - Organisation block. A callback called by the PLC's operating system in different situations. + + +Data types in Step7 +------------------- +A few of the available data types: + +* Bool +* Int - 16 bit +* UInt - 16 bit unsigned +* DInt - 32 bit +* UDInt - 32 bit unsigned +* Word - 16 bits +* DWord - 32 bits +* Real - 32 bit floating point number + + +SCL programming basics +----------------------- +Line comments are written like this:: + + // Line comment + +Hexadecimal literals start with ``16#``. + +Assignment:: + + temporary_value := 0; + + +Running the sample application via a ladder logic PLC program +------------------------------------------------------------- +It is possible to program the PLC in the programming language "Ladder logic" +instead of in SCL. + +In order to flash the LED we use an available clock bit. In the "Device view" +for the PLC, use the "Properties" tab and "General" sub-tab. Select "System +and clock memory", and enable the checkbox "Enable the use of clock memory +byte". Enter the value 100 in the "Address of clock memory byte" text box. +This results in the "Clock_1.25Hz" having the address ``%M100.4``. +For this change to take effect in the PLC, you need to compile the hardware +configuration and to download the hardware configuration to the PLC. + +First create two internal (memory) tags via the left menu PLC_1 > "PLC tags" > +"Show all tags". Add a new tag "CounterValue" with data type "Int" and address +``%MW200``. Similarly a new tag "Flashing" with data type "Int" and address +``%MW201``. + +.. image:: illustrations/PlcProgramLadderLogic.png + +With your program by using the left side menu "PLC_1" > "Program blocks" > +"Main [OB1]". In order to make the button toggle the state between off and +flashing, we will use a counter and the modulo operator. +From the right-side menu "Instructions" > "Basic instructions" > "Bit logic +operations" drag the "Normally open contact" icon to the "Network 1" line. +Double-click the question marks on top of the icon, and select "Button In". + +Drag a "CTU" counter from Instructions" > "Basic instructions" > "Counter +operations". Accept the data block name in the pop-up window. Double-click the +question marks on the PV input and enter ``0``. Double-click the value connected +to the CV output, and select "CounterValue". + +The last item on this network line is a modulo operator. You find it in +"Instructions" > "Basic instructions" > "Math functions" > "MOD". +For "IN1" use "CounterValue", and for "IN2" use ``2``. Connect the output to +"Flashing". + +Insert a new network by using the small "Insert network" icon in the top of the +"Main [OB1]" window. Add a "Normally open contact" which you use with +"Clock_1.25Hz", and then add a "Instructions" > "Basic instructions" > +"Comparator operations" > "CMP>". For the top row of question marks, use +"Flashing" and use ``0`` for the bottom row of question marks. Finally add a +"Instructions" > "Basic instructions" > "Bit logic operations" > "Assignment" +and connect it to "LEDout". + +Compile and download the program to your PLC. Button1 on the sample app will +turn on and off the flashing of the LED1. + + +Reading parameter data from the sample app using ladder logic +------------------------------------------------------------- +Reading parameter data from the IO-device is done with the RDREC command. +It is a asynchronous PLC command, meaning that the command is started in +one PLC execution cycle and the result is available in some later PLC execution +cycle. + +Find the ID of the relevant "DIO 8xLogicLevel" by using the left side menu +"PLC_1" > "PLC tags" > "Show all tags" and use the "System constants" tab. +The value for "rt-labs-dev~DIO 8xLogicLevel" can be for example 264. + +Create a data block using the left side menu "PLC_1" > "Program blocks" > +"Add new block". Use a data block of type "Global DB" and name it "data". +In the block create these a tag "param_value" of type "UDInt. + +In the "Main [OB1]" block, insert a RDREC block, and keep the default name. The +block is found in the right-side menu "Instructions" > "Extended instructions" +> "Distributed I/O". + +The value at the REQ input should be ``true`` and MLEN (number of bytes to read) +should be ``4``. The INDEX input should be ``123`` as given in the sample app +GSDML file. For the ID input, use the value you did find out above. +Connect the outputs RECORD to ``"data".param_value``. + +To study the result, compile and download the program to the PLC. Go online, +and enable monitoring by clicking the small glasses-icon. The parameter value +will be seen in the ladder logic diagram. The PLC will read out the parameter +value many times per second. The VALID and BUSY outputs are switching on +and off rapidly. + +.. image:: illustrations/rdrecLadderlogic.png + + +Reading parameter data from the sample app using the SCL language +----------------------------------------------------------------- +Create a new function block "RecReader" with language SCL. +In the "Static" section, add these tags: + +* valid - Bool +* busy - Bool +* error - Bool +* status - DWord +* len - UInt + +In the code part:: + + "RDREC_DB_1"(REQ := TRUE, + ID := 264, + INDEX := 123, + MLEN := 4, + VALID => #valid, + BUSY => #busy, + ERROR => #error, + STATUS => #status, + LEN => #len, + RECORD := "data".param_value); + +In the "Main [OB1]" window, drag the "RecReader" block to the Network1 line. +Accept the name of the datablock. + +The result is the same as above. + + +Using Siemens SinecPni (Primary Network Initialization) +------------------------------------------------------- +This is a tool for configuration of Profinet network equipment. Use it +to adjust the IP address of your PLC. + +Download the program from the Siemens homepage, and unzip the file. +Start the program by double clicking the SinecPni executable. + +To be able to change IP address on a Siemens PLC, make sure that the +"mode switch" on the front panel is in the "STOP" position. + +Click the “Settings” tab, and enable “PROFINET devices” for “Scan Protocol”, and +click “Save”. On the “Device list” tab, click “Start network scan”. +Select the “S7-1500” line, and click “Configure Device”. Adjust the IP address +to ``192.168.0.100`` and netmask to ``255.255.255.0``. +Click “Load” to store the settings. + + +Setting up a Simatic HMI +------------------------ +This example uses a "HMI KTP400 Basic", which is a 4 inch touch screen. +The sample application LED state will be shown on the screen. A +touch-screen button will start and stop the LED blinking on the IO-device (and +on the screen). + +Make sure you have added your PLC to the project first. + +Add a new tag to your PLC. In the left menu go to "PLC_1" > "PLC tags" > +"Show all tags". On an empty line add the name "ButtonHmiIn", and give it the +address ``%M1.1`` (which indicates that it is in-memory only). + +In the "Portal view" select "Devices and networks" and "Add new device". +Click "HMI" and select the correct model in the tree view. Click "Add". +Follow the device wizard. When you reach the "Screens" setting use only the +default "Root" screen. For "System screens" enable these: + +* Project information +* PLC system diagnostics +* System information + +For "Buttons" select to use button area to the left. +Click "Finish". + +In the "Device view" select the "HMI_1", and click on the image of the HMI. +In the "Properties" tab and "General" sub-tab select "PROFINET Interface [X1]" > +"Ethernet addresses". Set the IP address to ``192.168.0.51`` and subnet mask to +``255.255.255.0``. + +In the left menu select "HMI_1" > "Screens" and double-click "Root screen". +The screen editor will be seen. Drag the "Welcome to HMI_1 ..." text box to +make place for other elements. From the Toolbox menu on the right side of the +screen drag a "Button" icon, found in the "Elements" section. +Double-click the button to modify its text to "Press Me!". + +Select the button icon in the graphical editor, and select the "Properties" tab +and "Events" sub-tab. Select "Press" and on the "" select +"System functions" > "Edit bits" > "Set bit". In the resulting line press the +icon with three dots, and select "PLC_1" > "PLC tags" > "Default tag table" > +"ButtonHmiIn". +Similarly for the "Release" use "Reset bit" for "ButtonHmiIn". + +To modify the button color when pressed, use the "Animations" sub-tab. Use +"Display" and double-click "Add new animation". Select "Appearance", and in the +Tag field select "PLC_1" > "PLC tags" > "Default tag table" > "ButtonHmiIn". +The line "0" is already available, so add a line for the value "1". Modify the +background color to something different. + +Add a LED to the screen by dragging a circle from the right side menu "Toolbox" > +"Basic objects". Select the circle and use the "Properties" "Animations" sub-tab. +Use the method described above to set the background color to black when the PLC +tag "LEDout" has the value 0, and red when it has the value 1. + +Update the sample app PLC program to also take the button on the HMI screen +into account. + +Compile the software for the HMI screen by right-clicking it in the "Device view", +and select Compile > "Hardware (rebuild all)" and then Compile > "Software +(rebuild all)". Then right-click on it and select "Download to device" > +"Software (all)". In the new window select "PN/IE" and your Ethernet interface. +Click "Start search" and select the device when it appears in the list. Click +"load". + +Also compile and download hardware settings and software to the PLC. + +Run the sample application. The state of the physical LED connected to your +Raspberry Pi will be reflected on the LED (drawn circle) on the HMI. Use +the touch screen button on the HMI to start and stop flashing of the LED. diff --git a/profinet_stack/p-net/doc/using_codesys.rst b/profinet_stack/p-net/doc/using_codesys.rst new file mode 100644 index 0000000..cb5a2ae --- /dev/null +++ b/profinet_stack/p-net/doc/using_codesys.rst @@ -0,0 +1,178 @@ +.. _using-codesys: + +Using Codesys soft PLC +====================== +We run the Codesys ("COntroller DEvelopment SYStem") runtime on a Raspberry Pi, +and the setup is done using a Windows-based software (Codesys Development +System). Your Windows laptop is used to make changes to the PLC (IO-controller) +settings, but it does not act as an IO-controller. + + +Download and installation of Codesys Development System on a Windows PC +----------------------------------------------------------------------- +The program can be downloaded from https://store.codesys.com/codesys.html +A trial version is available. Registration is required. + +Download “CODESYS Development System V3", in the latest available version. +The file is named :file:`CODESYS 64 .exe`. Install it on a Windows +machine by double clicking the icon. + +From within Codesys, install the "Codesys control for Raspberry Pi" +by using the menu Tools/"Codesys installer". In the "AddOns" section +use "Browse" and search for "raspberry". Select the relevant row in the +results, and click "Install". When completed, there should be an entry +"Update Raspberry Pi" available in the Tools menu. + +Restart the program after the installation. + + +Scan network to find your PLC Raspberry Pi, and install the Codesys runtime onto it +----------------------------------------------------------------------------------- +Make sure your Windows machine and the Raspberry Pi are connected to the +same local network. + +In Codesys on Windows, use the menu Tools -> "Update Raspberry Pi". +Click "Scan" to find the IP address. +In this tutorial the IP address of the PLC Raspberry Pi is ``192.168.0.100``. + +It is possible to install the runtime on the PLC Raspberry Pi as long as you +know its IP address, even if it doesn't show up during the scan. + +Click "Install" for the Codesys Runtime package. Use "Standard" runtime +in the pop-up window. + +The runtime will be installed in the :file:`/opt/codesys` directory on the Raspberry Pi. + + +Create a project in Codesys +--------------------------- +On the Windows machine, first create a suitable project area on your hard +drive. For example Documents/Codesys/Democontroller. + +From the Codesys menu, create a new project. Use the recently created +directory, and "Standard Project". Name it "Democontroller". +Select device "Codesys Control for Raspberry Pi SL", and select to program in +"Structured Text (ST)" + +It is important that you select the same version of the runtime (single core = +"SL" or multicore = "SL MC") both in the runtime and in the project, otherwise the +controller will not be found when you try to use it. + +Verify that the "Device" in the left hand menu shows as "Codesys Control for +Raspberry Pi SL". Double-click the "Device". Click "Scan network" tab, +and select the Raspberry Pi. The marker on the image should turn green. Use +tab "Device" and "Send Echo Service" to verify the communication. + +In the Codesys menu "Tools", select "Device Repository". Click "Install" and +select the GSDML file from your hard drive. +For the sample application the GSDML file is available in the +:file:`samples/pn_dev` folder in the repository that you have cloned, or +from https://github.com/rtlabs-com/p-net/tree/master/samples/pn_dev + +* On the “Device (CODESY Control for Raspberry Pi SL)” in the left hand panel, + right-click and select Add Device. Use “Ethernet adapter”, “Ethernet”. +* On the "Ethernet", right-click and select Add Device. + Use "Profinet IO master", "PN-Controller". +* On the "PN_Controller", right-click and select Add Device. Use "P-Net Sample App". +* On the "P_Net_Sample_App", right-click and select Add Device. Use "DIO 8xLogicLevel". + +Double-click the "Ethernet" node in the left menu. Select interface "eth0". +The IP address will be updated accordingly. + +Double-click the "PN_controller" node in the left menu. Adjust the IP range +using "First IP" and "Last IP" to both have the existing IP-address of your +IO-device (for example a Linux laptop or embedded Linux board running the +sample_app). For this tutorial we use the “First IP” ``192.168.0.50`` +and also the “Last IP” ``192.168.0.50``. + +Double-click the "P_Net_Sample_App" node in the left menu. Set the +IP-address to the existing address of your IO-device. +In this tutorial we use ``192.168.0.50``. + + +Structured Text programming language for PLCs +--------------------------------------------- +Structured Text (ST) is a text based programming language for PLCs. +Read about it on https://en.wikipedia.org/wiki/Structured_text + +A tutorial is found here: https://www.plcacademy.com/structured-text-tutorial/ + + +Create controller application +----------------------------- +Enter program in "PLC_PRG". + +Variables section:: + + PROGRAM PLC_PRG + VAR + in_pin_button_LED: BOOL; + out_pin_LED: BOOL; + + in_pin_button_LED_previous: BOOL; + flashing: BOOL := TRUE; + oscillator_state: BOOL := FALSE; + oscillator_cycles: UINT := 0; + END_VAR + +Program section:: + + oscillator_cycles := oscillator_cycles + 1; + IF oscillator_cycles > 200 THEN + oscillator_cycles := 0; + oscillator_state := NOT oscillator_state; + END_IF + + IF in_pin_button_LED = TRUE THEN + IF in_pin_button_LED_previous = FALSE THEN + flashing := NOT flashing; + END_IF + out_pin_LED := TRUE; + ELSIF flashing = TRUE THEN + out_pin_LED := oscillator_state; + ELSE + out_pin_LED := FALSE; + END_IF + in_pin_button_LED_previous := in_pin_button_LED; + +On the "DIO_8xLogicLevel" node in the left-side menu, +right-click and select "Edit IO mapping". +Open the "Input 8 bits" row by clicking the small ``+`` sign. +Double-click the icon on the row that you would like the edit. +Map "Input Bit 7" to "in_pin_button_LED" (found via Application/PLC_PRG), +and "Output Bit 7" to "out_pin_LED". + +In the "Application -> MainTask" select "Cyclic" with 4 ms. + +In the "Application -> Profinet_CommunicationTask" select "Cyclic" with 10 ms. +Use priority 14. + + +Transfer controller application to (controller) Raspberry Pi +------------------------------------------------------------ + +* In the top menu, use Build -> "Generate Code". +* Transfer the application to the Raspberry Pi by using the top menu + Online -> Login. Press "Yes" in the pop-up window. +* In the top menu, use Debug -> Start + +You can follow the controller log by using the top menu Tools -> "Update +Raspberry Pi". Click the "System info" button, and look in the "Runtime Info" +text box. It will show an error message if it can't find the IO-device on +the network. + +Use Wireshark to verify that the controller sends LLDP packets every 5 seconds. +Every 15 seconds it will send an ARP packet to ask for the (first?) IO-device +IP address, and a PN-DCP packet to ask for the IO-device with the name +"rt-labs-dev". + + +Running the application +----------------------- +Now it is time to run the application, so head back to the :ref:`tutorial` page. + +Once the Codesys softplc running on the Raspberry Pi has been configured, +you can turn off the personal computer (running the Codesys desktop application) +used to configure it. +Remember that you need to power cycle the Raspberry Pi running the softplc every +two hours, if using the trial version. diff --git a/profinet_stack/p-net/doc/writing_documentation.rst b/profinet_stack/p-net/doc/writing_documentation.rst new file mode 100644 index 0000000..63eff76 --- /dev/null +++ b/profinet_stack/p-net/doc/writing_documentation.rst @@ -0,0 +1,43 @@ +Writing documentation +===================== +Documentation can be written in Markdown or reStructuredText (reST). + +Use restructured text when more advanced featured are required, for example +inserting a table of contents. + +Add new documentation files to ``doc/index.rst``. + + +Markdown +-------- +Use ``.md`` file name extension. +Parsing is done with the Commonmark specification, +see https://commonmark.org/ + + +Restructured text +----------------- +Use ``.rst`` file name extension. +For details on the syntax, see +https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html + + +Installing documentation toolchain +---------------------------------- + +Install Sphinx by running this in the ``doc`` subdirectory:: + + doc$ sudo apt install python3-pip + doc$ pip3 install -r doc_requirements.txt + +Build documentation (in the ``doc`` subdirectory):: + + doc$ cmake --build ../build --target docs + doc$ make html + doc$ make pdf + +Run checks:: + + doc$ make spelling + doc$ make linkcheck + $ codespell -I doc/codespell_ignore.txt -S "*.patch" include/ samples/pn_dev/ src/ test/ diff --git a/profinet_stack/p-net/doc/yoctobuild.rst b/profinet_stack/p-net/doc/yoctobuild.rst new file mode 100644 index 0000000..d398634 --- /dev/null +++ b/profinet_stack/p-net/doc/yoctobuild.rst @@ -0,0 +1,21 @@ +Build p-net for Linux using Yocto or Buildroot +============================================== + +Yocto +----- +A Yocto layer for building p-net on embedded Linux is available +on https://github.com/rtlabs-com/meta-rtlabs. Usage details are given there. + +Add ``p-net-demo`` to your image to include the Profinet IO-device sample +application. + + +Buildroot +--------- +It is possible to use Buildroot for including p-net in your embedded Linux +product. + +A Buildroot recipe for p-net is available as one of the packages in +https://github.com/rtlabs-com/br2-rtlabs See the ``README.md`` file for usage +instructions. You might need to adjust ``P_NET_VERSION`` in the ``p-net.mk`` +file to have the latest version. diff --git a/profinet_stack/p-net/include/pnet_api.h b/profinet_stack/p-net/include/pnet_api.h new file mode 100644 index 0000000..f8e92cf --- /dev/null +++ b/profinet_stack/p-net/include/pnet_api.h @@ -0,0 +1,2257 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Public API for Profinet stack. + * + * # HW- and OS-specifics + * + * The Profinet stack depends on some OS and device-specific functions, which + * must be implemented by the application. + * + * The API for these functions is defined in osal.h. + */ + +#ifndef PNET_API_H +#define PNET_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pnet_export.h" +#include "pnet_options.h" +#include "pnet_version.h" +#include "pnal_config.h" +#if PNET_OPTION_DRIVER_ENABLE +#include "driver_config.h" /* Configuration options for enabled driver */ +#endif + +#include +#include +#include +#include + +#define PNET_PRODUCT_NAME_MAX_LEN 25 /* Not including termination */ +#define PNET_ORDER_ID_MAX_LEN 20 /* Not including termination */ +#define PNET_SERIAL_NUMBER_MAX_LEN 16 /* Not including termination */ + +/* Including termination. Standard says 22 (without termination) */ +#define PNET_LOCATION_MAX_SIZE 23 + +/** Including separator and one termination */ +#define PNET_MAX_FILE_FULLPATH_SIZE \ + (PNET_MAX_DIRECTORYPATH_SIZE + PNET_MAX_FILENAME_SIZE) + +/** Including termination. Based on Linux IFNAMSIZ */ +#define PNET_INTERFACE_NAME_MAX_SIZE 16 + +/** Supported block version by this implementation */ +#define PNET_BLOCK_VERSION_HIGH 1 +#define PNET_BLOCK_VERSION_LOW 0 + +/** Some blocks (e.g. logbook) uses the following lower version number. */ +#define PNET_BLOCK_VERSION_LOW_1 1 + +/* + * Module and submodule ident number for the DAP module. + * The DAP module and submodules must be plugged by the application after the + * call to pnet_init. + */ +#define PNET_SLOT_DAP_IDENT 0x00000000 +#define PNET_SUBSLOT_DAP_WHOLE_MODULE 0x00000000 +#define PNET_SUBSLOT_DAP_IDENT 0x00000001 +#define PNET_SUBSLOT_DAP_INTERFACE_1_IDENT 0x00008000 +#define PNET_SUBSLOT_DAP_INTERFACE_1_PORT_1_IDENT 0x00008001 +#define PNET_SUBSLOT_DAP_INTERFACE_1_PORT_2_IDENT 0x00008002 +#define PNET_SUBSLOT_DAP_INTERFACE_1_PORT_3_IDENT 0x00008003 +#define PNET_SUBSLOT_DAP_INTERFACE_1_PORT_4_IDENT 0x00008004 + +#define PNET_MOD_DAP_IDENT 0x00000001 /* For use in PNET_SLOT_DAP_IDENT */ +#define PNET_SUBMOD_DAP_IDENT 0x00000001 +#define PNET_SUBMOD_DAP_INTERFACE_1_IDENT 0x00008000 +#define PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT 0x00008001 +#define PNET_SUBMOD_DAP_INTERFACE_1_PORT_2_IDENT 0x00008002 +#define PNET_SUBMOD_DAP_INTERFACE_1_PORT_3_IDENT 0x00008003 +#define PNET_SUBMOD_DAP_INTERFACE_1_PORT_4_IDENT 0x00008004 + +#define PNET_API_NO_APPLICATION_PROFILE 0 + +#define PNET_PORT_1 1 +#define PNET_PORT_2 2 +#define PNET_PORT_3 3 +#define PNET_PORT_4 4 + +/**************************** Error codes ***********************************/ + +/** + * Error Codes + * + * The Profinet error code consists of 4 values. The data structure for this is + * defined in the typedef pnet_pnio_status_t. + * + * The values here do not constitute an exhaustive list. + */ + +/** + * # Values used in error_code + * + * Reserved 0x00 (No error) + * Reserved 0x01-0x1F + * Manufacturer specific 0x20-0x3F (LogBookData) Reserved 0x40-0x80 + */ +#define PNET_ERROR_CODE_NOERROR 0x00 +#define PNET_ERROR_CODE_PNIO 0x81 /** All other errors */ +/** Reserved 0x82-0xDE */ +#define PNET_ERROR_CODE_RTA_ERROR 0xCF /** In ERR-RTA-PDU and ERR-UDP-PDU */ +/** Reserved 0xD0-0xD9 */ +#define PNET_ERROR_CODE_ALARM_ACK 0xDA /** In DATA-RTA-PDU and DATA-UDP-PDU */ +#define PNET_ERROR_CODE_CONNECT 0xDB /** CL-RPC-PDU */ +#define PNET_ERROR_CODE_RELEASE 0xDC /** CL-RPC-PDU */ +#define PNET_ERROR_CODE_CONTROL 0xDD /** CL-RPC-PDU */ +#define PNET_ERROR_CODE_READ 0xDE /** Only with PNIORW */ +#define PNET_ERROR_CODE_WRITE 0xDF /** Only with PNIORW */ +/** Reserved 0xE0-0xFF */ + +/** + * # Values used in error_decode. + * + * Reserved 0x00 (No error) + * + * Reserved 0x01-0x7F + */ +#define PNET_ERROR_DECODE_NOERROR 0x00 +#define PNET_ERROR_DECODE_PNIORW 0x80 /** Only Read/Write */ +#define PNET_ERROR_DECODE_PNIO 0x81 +#define PNET_ERROR_DECODE_MANUFACTURER_SPECIFIC 0x82 +/** Reserved 0x83-0xFF */ + +/** + * # List of error_code_1 values, for PNET_ERROR_DECODE_PNIORW + * + * APP = application + * ACC = access + * RES = resource + */ +#define PNET_ERROR_CODE_1_APP_READ_ERROR 0xA0 +#define PNET_ERROR_CODE_1_APP_WRITE_ERROR 0xA1 +#define PNET_ERROR_CODE_1_APP_MODULE_FAILURE 0xA2 +#define PNET_ERROR_CODE_1_APP_BUSY 0xA7 +#define PNET_ERROR_CODE_1_APP_VERSION_CONFLICT 0xA8 +#define PNET_ERROR_CODE_1_APP_NOT_SUPPORTED 0xA9 +#define PNET_ERROR_CODE_1_ACC_INVALID_INDEX 0xB0 +#define PNET_ERROR_CODE_1_ACC_WRITE_LENGTH_ERROR 0xB1 +#define PNET_ERROR_CODE_1_ACC_INVALID_SLOT_SUBSLOT 0xB2 +#define PNET_ERROR_CODE_1_ACC_TYPE_CONFLICT 0xB3 +#define PNET_ERROR_CODE_1_ACC_INVALID_AREA_API 0xB4 +#define PNET_ERROR_CODE_1_ACC_STATE_CONFLICT 0xB5 +#define PNET_ERROR_CODE_1_ACC_ACCESS_DENIED 0xB6 +#define PNET_ERROR_CODE_1_ACC_INVALID_RANGE 0xB7 +#define PNET_ERROR_CODE_1_ACC_INVALID_PARAMETER 0xB8 +#define PNET_ERROR_CODE_1_ACC_INVALID_TYPE 0xB9 +#define PNET_ERROR_CODE_1_ACC_BACKUP 0xBA +#define PNET_ERROR_CODE_1_RES_READ_CONFLICT 0xC0 +#define PNET_ERROR_CODE_1_RES_WRITE_CONFLICT 0xC1 +#define PNET_ERROR_CODE_1_RES_RESOURCE_BUSY 0xC2 +#define PNET_ERROR_CODE_1_RES_RESOURCE_UNAVAILABLE 0xC3 + +/** + * # List of error_code_1 values, for PNET_ERROR_DECODE_PNIO + */ +#define PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ 0x01 +#define PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ 0x02 +#define PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ 0x03 +#define PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ 0x04 +#define PNET_ERROR_CODE_1_CONN_FAULTY_PRM_SERVER_BLOCK_REQ 0x05 +#define PNET_ERROR_CODE_1_CONN_FAULTY_MCR_BLOCK_REQ 0x06 +#define PNET_ERROR_CODE_1_CONN_FAULTY_AR_RPC_BLOCK_REQ 0x07 +#define PNET_ERROR_CODE_1_CONN_FAULTY_FAULTY_RECORD 0x08 +#define PNET_ERROR_CODE_1_CONN_FAULTY_IR_INFO 0x09 +#define PNET_ERROR_CODE_1_CONN_FAULTY_SR_INFO 0x0A +#define PNET_ERROR_CODE_1_CONN_FAULTY_AR_FSU 0x0B +#define PNET_ERROR_CODE_1_CONN_FAULTY_VENDOR 0x0C +#define PNET_ERROR_CODE_1_CONN_FAULTY_RSINFO 0x0D +/** Reserved 0x0E-0x13 */ +#define PNET_ERROR_CODE_1_DCTRL_FAULTY_CONNECT 0x14 +#define PNET_ERROR_CODE_1_DCTRL_FAULTY_CONNECT_PLUG 0x15 +#define PNET_ERROR_CODE_1_XCTRL_FAULTY_CONNECT 0x16 +#define PNET_ERROR_CODE_1_XCTRL_FAULTY_CONNECT_PLUG 0x17 +#define PNET_ERROR_CODE_1_DCTRL_FAULTY_CONNECT_PRMBEG 0x18 +#define PNET_ERROR_CODE_1_DCTRL_FAULTY_CONNECT_SUBMODLIST 0x19 +/** Reserved 0x1A-0x27 */ +#define PNET_ERROR_CODE_1_RELS_FAULTY_BLOCK 0x28 +/** Reserved 0x29-0x31 */ +/** Reserved 0x39-0x3B */ +#define PNET_ERROR_CODE_1_ALARM_ACK 0x3C +#define PNET_ERROR_CODE_1_CMDEV 0x3D +#define PNET_ERROR_CODE_1_CMCTL 0x3E +#define PNET_ERROR_CODE_1_CTLDINA 0x3F +#define PNET_ERROR_CODE_1_CMRPC 0x40 +#define PNET_ERROR_CODE_1_ALPMI 0x41 +#define PNET_ERROR_CODE_1_ALPMR 0x42 +#define PNET_ERROR_CODE_1_LMPM 0x43 +#define PNET_ERROR_CODE_1_MAC 0x44 +#define PNET_ERROR_CODE_1_RPC 0x45 +#define PNET_ERROR_CODE_1_APMR 0x46 +#define PNET_ERROR_CODE_1_APMS 0x47 +#define PNET_ERROR_CODE_1_CPM 0x48 +#define PNET_ERROR_CODE_1_PPM 0x49 +#define PNET_ERROR_CODE_1_DCPUCS 0x4A +#define PNET_ERROR_CODE_1_DCPUCR 0x4B +#define PNET_ERROR_CODE_1_DCPMCS 0x4C +#define PNET_ERROR_CODE_1_DCPMCR 0x4D +#define PNET_ERROR_CODE_1_FSPM 0x4E +/** Reserved 0x4F-0x63 */ +/** CTRLxxx 0x64-0xC7 */ +#define PNET_ERROR_CODE_1_CMSM 0xC8 +#define PNET_ERROR_CODE_1_CMRDR 0xCA +#define PNET_ERROR_CODE_1_CMWRR 0xCC +#define PNET_ERROR_CODE_1_CMIO 0xCD +#define PNET_ERROR_CODE_1_CMSU 0xCE +#define PNET_ERROR_CODE_1_CMINA 0xD0 +#define PNET_ERROR_CODE_1_CMPBE 0xD1 +#define PNET_ERROR_CODE_1_CMSRL 0xD2 +#define PNET_ERROR_CODE_1_CMDMC 0xD3 +/** CTRLxxx 0xD4-0xFC */ +#define PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL 0xFD +/** Reserved 0xFE */ +#define PNET_ERROR_CODE_1_USER_SPECIFIC 0xFF + +/** + * # List of error_code_2 values (not exhaustive) + */ +#define PNET_ERROR_CODE_2_CMDEV_STATE_CONFLICT 0x00 +#define PNET_ERROR_CODE_2_CMDEV_RESOURCE 0x01 + +#define PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID 0x00 +#define PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS 0x01 +#define PNET_ERROR_CODE_2_CMRPC_IOCR_MISSING 0x02 +#define PNET_ERROR_CODE_2_CMRPC_WRONG_BLOCK_COUNT 0x03 +#define PNET_ERROR_CODE_2_CMRPC_NO_AR_RESOURCES 0x04 +#define PNET_ERROR_CODE_2_CMRPC_AR_UUID_UNKNOWN 0x05 +#define PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT 0x06 +#define PNET_ERROR_CODE_2_CMRPC_OUT_OF_PCA_RESOURCES \ + 0x07 /** PCA = Provider, Consumer or Alarm */ +#define PNET_ERROR_CODE_2_CMRPC_OUT_OF_MEMORY 0x08 +#define PNET_ERROR_CODE_2_CMRPC_PDEV_ALREADY_OWNED 0x09 +#define PNET_ERROR_CODE_2_CMRPC_ARSET_STATE_CONFLICT 0x0A +#define PNET_ERROR_CODE_2_CMRPC_ARSET_PARAM_CONFLICT 0x0B +#define PNET_ERROR_CODE_2_CMRPC_PDEV_PORTS_WO_INTERFACE 0x0C + +#define PNET_ERROR_CODE_2_CTLDINA_ARP_MULTIPLE_IP_ADDRESSES 0x07 + +#define PNET_ERROR_CODE_2_CMSM_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMSM_SIGNALED_ERROR 0x01 + +#define PNET_ERROR_CODE_2_CMRDR_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMRDR_SIGNALED_ERROR 0x01 + +#define PNET_ERROR_CODE_2_CMWRR_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMWRR_AR_STATE_NOT_PRIMARY 0x01 +#define PNET_ERROR_CODE_2_CMWRR_SIGNALED_ERROR 0x02 + +#define PNET_ERROR_CODE_2_CMIO_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMIO_SIGNALED_ERROR 0x01 + +#define PNET_ERROR_CODE_2_CMSU_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMSU_AR_ADD_PROV_CONS_FAILED 0x01 +#define PNET_ERROR_CODE_2_CMSU_AR_ALARM_OPEN_FAILED 0x02 +#define PNET_ERROR_CODE_2_CMSU_AR_ALARM_SEND 0x03 +#define PNET_ERROR_CODE_2_CMSU_AR_ALARM_ACK_SEND 0x04 +#define PNET_ERROR_CODE_2_CMSU_AR_ALARM_IND 0x05 + +#define PNET_ERROR_CODE_2_CMINA_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMINA_SIGNALED_ERROR 0x01 + +#define PNET_ERROR_CODE_2_CMPBE_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMPBE_SIGNALED_ERROR 0x01 + +#define PNET_ERROR_CODE_2_CMSRL_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMSRL_SIGNALED_ERROR 0x01 + +#define PNET_ERROR_CODE_2_CMDMC_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CMDMC_SIGNALED_ERROR 0x01 + +#define PNET_ERROR_CODE_2_CPM_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_CPM_INVALID 0x01 + +#define PNET_ERROR_CODE_2_PPM_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_PPM_INVALID 0x01 + +#define PNET_ERROR_CODE_2_APMS_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_APMS_LMPM_ERROR 0x01 +#define PNET_ERROR_CODE_2_APMS_TIMEOUT 0x02 +/* Reserved 0x03..0xff */ + +#define PNET_ERROR_CODE_2_APMR_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_APMR_LMPM_ERROR 0x01 +/* Reserved 0x02..0xff */ + +#define PNET_ERROR_CODE_2_ALPMI_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_ALPMI_WRONG_ACK_PDU 0x01 +#define PNET_ERROR_CODE_2_ALPMI_INVALID 0x02 +#define PNET_ERROR_CODE_2_ALPMI_WRONG_STATE 0x03 +/* Reserved 0x04..0xff */ + +#define PNET_ERROR_CODE_2_ALPMR_INVALID_STATE 0x00 +#define PNET_ERROR_CODE_2_ALPMR_WRONG_ALARM_PDU 0x01 +#define PNET_ERROR_CODE_2_ALPMR_INVALID 0x02 +#define PNET_ERROR_CODE_2_ALPMR_WRONG_STATE 0x03 +/* Reserved 0x04..0xff */ + +#define PNET_ERROR_CODE_2_INVALID_BLOCK_LEN 0x01 +#define PNET_ERROR_CODE_2_INVALID_BLOCK_VERSION_HIGH 0x02 +#define PNET_ERROR_CODE_2_INVALID_BLOCK_VERSION_LOW 0x03 + +/** + * # List of error_code_2 values, for + * PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL (not exhaustive). + */ +#define PNET_ERROR_CODE_2_ABORT_CODE_SEQ 0x01 +#define PNET_ERROR_CODE_2_ABORT_INSTANCE_CLOSED 0x02 +#define PNET_ERROR_CODE_2_ABORT_AR_OUT_OF_MEMORY 0x03 +#define PNET_ERROR_CODE_2_ABORT_AR_ADD_CPM_PPM_FAILED 0x04 +#define PNET_ERROR_CODE_2_ABORT_AR_CONSUMER_DHT_EXPIRED 0x05 +#define PNET_ERROR_CODE_2_ABORT_AR_CMI_TIMEOUT 0x06 +#define PNET_ERROR_CODE_2_ABORT_AR_ALARM_OPEN_FAILED 0x07 +#define PNET_ERROR_CODE_2_ABORT_AR_ALARM_SEND_CNF_NEG 0x08 +#define PNET_ERROR_CODE_2_ABORT_AR_ALARM_ACK_SEND_CNF_NEG 0x09 +#define PNET_ERROR_CODE_2_ABORT_AR_ALARM_DATA_TOO_LONG 0x0a +#define PNET_ERROR_CODE_2_ABORT_AR_ALARM_IND_ERROR 0x0b +#define PNET_ERROR_CODE_2_ABORT_AR_RPC_CLIENT_CALL_CNF_NEG 0x0c +#define PNET_ERROR_CODE_2_ABORT_AR_ABORT_REQ 0x0d +#define PNET_ERROR_CODE_2_ABORT_AR_RE_RUN_ABORTS_EXISTING 0x0e +#define PNET_ERROR_CODE_2_ABORT_AR_RELEASE_IND_RECEIVED 0x0f +#define PNET_ERROR_CODE_2_ABORT_AR_DEVICE_DEACTIVATED 0x10 +#define PNET_ERROR_CODE_2_ABORT_AR_REMOVED 0x11 +#define PNET_ERROR_CODE_2_ABORT_AR_PROTOCOL_VIOLATION 0x12 +#define PNET_ERROR_CODE_2_ABORT_AR_NAME_RESOLUTION_ERROR 0x13 +#define PNET_ERROR_CODE_2_ABORT_AR_RPC_BIND_ERROR 0x14 +#define PNET_ERROR_CODE_2_ABORT_AR_RPC_CONNECT_ERROR 0x15 +#define PNET_ERROR_CODE_2_ABORT_AR_RPC_READ_ERROR 0x16 +#define PNET_ERROR_CODE_2_ABORT_AR_RPC_WRITE_ERROR 0x17 +#define PNET_ERROR_CODE_2_ABORT_AR_RPC_CONTROL_ERROR 0x18 +#define PNET_ERROR_CODE_2_ABORT_AR_FORBIDDEN_PULL_OR_PLUG 0x19 +#define PNET_ERROR_CODE_2_ABORT_AR_AP_REMOVED 0x1a +#define PNET_ERROR_CODE_2_ABORT_AR_LINK_DOWN 0x1b +#define PNET_ERROR_CODE_2_ABORT_AR_MC_REGISTER_FAILED 0x1c +#define PNET_ERROR_CODE_2_ABORT_NOT_SYNCHRONIZED 0x1d +#define PNET_ERROR_CODE_2_ABORT_WRONG_TOPOLOGY 0x1e +#define PNET_ERROR_CODE_2_ABORT_DCP_STATION_NAME_CHANGED 0x1f +#define PNET_ERROR_CODE_2_ABORT_DCP_RESET_TO_FACTORY 0x20 +#define PNET_ERROR_CODE_2_ABORT_PDEV_CHECK_FAILED 0x24 + +/** + * # List of error_code_2 values, for + * PNET_ERROR_CODE_1_DCTRL_FAULTY_CONNECT (not exhaustive). + */ +#define PNET_ERROR_CODE_2_DCTRL_FAULTY_CONNECT_CONTROLCOMMAND 0x08 + +/******************************** Events ******************************/ + +/** + * The events are sent from CMDEV to the application using the + * \a pnet_state_ind() call-back function. + */ +typedef enum pnet_event_values +{ + /** The AR has been closed. */ + PNET_EVENT_ABORT, + + /** A connection has been initiated. */ + PNET_EVENT_STARTUP, + + /** Controller has sent all write records. */ + PNET_EVENT_PRMEND, + + /** Controller has acknowledged the APPL_RDY message */ + PNET_EVENT_APPLRDY, + + /** The cyclic data transmission is now properly set up */ + PNET_EVENT_DATA +} pnet_event_values_t; + +/** + * Values used for IOCS and IOPS. The actual values are important, as they are + * sent on the wire. + */ +typedef enum pnet_ioxs_values +{ + PNET_IOXS_BAD = 0x00, + PNET_IOXS_GOOD = 0x80 +} pnet_ioxs_values_t; + +/** + * Values used when plugging a sub-module using \a pnet_plug_submodule(). + * The actual values are important, as they are sent on the wire. + */ +typedef enum pnet_submodule_dir +{ + PNET_DIR_NO_IO = 0x00, + PNET_DIR_INPUT = 0x01, + PNET_DIR_OUTPUT = 0x02, + PNET_DIR_IO = 0x03 +} pnet_submodule_dir_t; + +/* + * Data configuration for one submodule. + * + * Used when indicating an expected submodule. + */ +typedef struct pnet_data_cfg +{ + pnet_submodule_dir_t data_dir; + uint16_t insize; + uint16_t outsize; +} pnet_data_cfg_t; + +/** + * CControl command codes used in the \a pnet_dcontrol_ind() call-back function. + */ +typedef enum pnet_control_command +{ + PNET_CONTROL_COMMAND_PRM_BEGIN, + PNET_CONTROL_COMMAND_PRM_END, + PNET_CONTROL_COMMAND_APP_RDY, + PNET_CONTROL_COMMAND_RELEASE, + + /** Not yet implemented */ + PNET_CONTROL_COMMAND_RDY_FOR_COMPANION, + + /** Not yet implemented */ + PNET_CONTROL_COMMAND_RDY_FOR_RTC3, +} pnet_control_command_t; + +/** + * Data status bits used in the \a pnet_new_data_status_ind() call-back + * function. + */ +typedef enum pnet_data_status_bits +{ + /** 0 => BACKUP, 1 => PRIMARY */ + PNET_DATA_STATUS_BIT_STATE = 0, + + /** Meaning depends on STATE bit */ + PNET_DATA_STATUS_BIT_REDUNDANCY, + + /** 0 => INVALID, 1 => VALID */ + PNET_DATA_STATUS_BIT_DATA_VALID, + + PNET_DATA_STATUS_BIT_RESERVED_1, + + /** 0 => STOP, 1 => RUN */ + PNET_DATA_STATUS_BIT_PROVIDER_STATE, + + /** 0 => Problem detected, 1 => Normal operation */ + PNET_DATA_STATUS_BIT_STATION_PROBLEM_INDICATOR, + + PNET_DATA_STATUS_BIT_RESERVED_2, + + /** 0 => Evaluate data status, + * 1 => Ignore the data status (typically used on a frame with subframes) */ + PNET_DATA_STATUS_BIT_IGNORE +} pnet_data_status_bits_t; + +/** Network handle */ +typedef struct pnet pnet_t; + +/** + * Profinet stack detailed error information. + */ +typedef struct pnet_pnio_status +{ + uint8_t error_code; + uint8_t error_decode; + uint8_t error_code_1; + uint8_t error_code_2; +} pnet_pnio_status_t; + +/************************ Alarm and Diagnosis ********************************/ + +/** Summary of all diagnosis items, sent in an alarm. + Only valid for alarms attached to the Diagnosis ASE + See Profinet 2.4 Services, section 7.3.1.6.5.1 Alarm Notification */ +typedef struct pnet_alarm_spec +{ + /** Diagnosis in standard format (any severity, appear) + * on this subslot */ + bool channel_diagnosis; + + /** Diagnosis in USI format (which always is FAULT) on this subslot, + * (USI format does not have appears/disappears) */ + bool manufacturer_diagnosis; + + /** Fault or Qual27-Qual31 (standard or USI format, appear) on + * this subslot */ + bool submodule_diagnosis; + + /** Fault or Qual27-Qual31 (standard or USI format, appear) on this AR */ + bool ar_diagnosis; +} pnet_alarm_spec_t; + +/** + * Shows location and summary of diagnosis items in an alarm + */ +typedef struct pnet_alarm_argument +{ + uint32_t api_id; + uint16_t slot_nbr; + uint16_t subslot_nbr; + uint16_t alarm_type; + uint16_t sequence_number; + pnet_alarm_spec_t alarm_specifier; +} pnet_alarm_argument_t; + +/** + * Sent to controller on negative result. + */ +typedef struct pnet_result +{ + /** Profinet-specific error information. */ + pnet_pnio_status_t pnio_status; + + /** Application-specific error information. */ + uint16_t add_data_1; + + /** Application-specific error information. */ + uint16_t add_data_2; +} pnet_result_t; + +/******************************* Callbacks ************************************/ + +/* + * Application call-back functions + * + * The application should define call-back functions which are called by + * the stack when specific events occurs within the stack. + * + * Note that most of these functions are mandatory in the sense that they must + * exist and return 0 (zero) for a functioning stack. Some functions are + * required to perform specific functionality. + * + * The call-back functions should return 0 (zero) for a successful call and + * -1 for an unsuccessful call. + */ + +/** + * Indication to the application that a Connect request was received from the + * controller. + * + * This application call-back function is called by the Profinet stack on every + * Connect request from the Profinet controller. + * + * The connection will be opened if this function returns 0 (zero) and the stack + * is otherwise able to establish a connection. + * + * If this function returns something other than 0 (zero) then the Connect + * request is refused by the device. In case of error the application should + * provide error information in \a p_result. + * + * It is optional to implement this callback (assumes success if not + * implemented). + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param p_result Out: Detailed error information if return != 0. + * @return 0 on success. + * -1 if an error occurred. + */ +typedef int (*pnet_connect_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result); + +/** + * Indication to the application that a Release request was received from the + * controller. + * + * This application call-back function is called by the Profinet stack on every + * Release request from the Profinet controller. + * + * The connection will be closed regardless of the return value from this + * function. In case of error the application should provide error information + * in \a p_result. + * + * It is optional to implement this callback (assumes success if not + * implemented). + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param p_result Out: Detailed error information if return != 0. + * @return 0 on success. + * -1 if an error occurred. + */ +typedef int (*pnet_release_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result); + +/** + * Indication to the application that a DControl request was received from the + * controller. Typically this means that the controller is done writing + * parameters. + * + * This application call-back function is called by the Profinet stack on every + * DControl request from the Profinet controller. + * + * The application is not required to take any action but the function must + * return 0 (zero) for proper function of the stack. If this function returns + * something other than 0 (zero) then the DControl request is refused by the + * device. In case of error the application should provide error information in + * \a p_result. + * + * It is optional to implement this callback (assumes success if not + * implemented). + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param control_command In: The DControl command code. + * @param p_result Out: Detailed error information if return != 0. + * @return 0 on success. + * -1 if an error occurred. + */ +typedef int (*pnet_dcontrol_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_control_command_t control_command, + pnet_result_t * p_result); + +/** + * Indication to the application that a CControl confirmation was received from + * the controller. Typically this means that the controller has received our + * "Application ready" message. + * + * This application call-back function is called by the Profinet stack on every + * CControl confirmation from the Profinet controller. + * + * The application is not required to take any action. + * The return value from this call-back function is ignored by the Profinet + * stack. In case of error the application should provide error information in + * \a p_result. + * + * It is optional to implement this callback (assumes success?). + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param p_result Out: Detailed error information. + * @return 0 on success. Other values are ignored. + */ +typedef int (*pnet_ccontrol_cnf) ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result); + +/** + * Indication to the application that a state transition has occurred within the + * Profinet stack. + * + * This application call-back function is called by the Profinet stack on + * specific state transitions within the Profinet stack. + * + * At the very least the application must react to the PNET_EVENT_PRMEND state + * transition. After this event the application must call \a + * pnet_application_ready(), when it has finished its setup and it is ready to + * exchange data. + * + * The return value from this call-back function is ignored by the Profinet + * stack. + * + * It is optional to implement this callback (but then it would be difficult + * to know when to call the \a pnet_application_ready() function). + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param state In: The state transition event. See + * pnet_event_values_t. + * @return 0 on success. Other values are ignored. + */ +typedef int (*pnet_state_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_event_values_t state); + +/** + * Indication to the application that an IODRead request was received from the + * controller. + * + * This application call-back function is called by the Profinet stack on every + * IODRead request from the Profinet controller which specify an + * application-specific value of \a idx (0x0000 - 0x7fff). All other values of + * \a idx are handled internally by the Profinet stack. + * + * The application must verify the value of \a idx, and that \a p_read_length is + * large enough. Further, the application must provide a + * pointer to the binary value in \a pp_read_data and the size, in bytes, of the + * binary value in \a p_read_length. + * + * The Profinet stack does not perform any endianness conversion on the binary + * value. + * + * In case of error the application should provide error information in \a + * p_result. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param api In: The AP identifier. + * @param slot In: The slot number. + * @param subslot In: The sub-slot number. + * @param idx In: The data record index. + * @param sequence_number In: The sequence number. + * @param pp_read_data Out: A pointer to the binary value. + * @param p_read_length InOut: The maximum (in) and actual (out) length in + * bytes of the binary value. + * @param p_result Out: Detailed error information if returning != 0 + * @return 0 on success. + * -1 if an error occurred. + */ +typedef int (*pnet_read_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t idx, + uint16_t sequence_number, + uint8_t ** pp_read_data, + uint16_t * p_read_length, + pnet_result_t * p_result); + +/** + * Indication to the application that an IODWrite request was received from the + * controller. + * + * This application call-back function is called by the Profinet stack on every + * IODWrite request from the Profinet controller which specify an + * application-specific value of \a idx (0x0000 - 0x7fff). All other values of + * \a idx are handled internally by the Profinet stack. + * + * The application must verify the values of \a idx and \a write_length and save + * (copy) the binary value in \a p_write_data. A future IODRead must return the + * latest written value. + * + * The Profinet stack does not perform any endianness conversion on the binary + * value. + * + * In case of error the application should provide error information in \a + * p_result. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param api In: The API identifier. + * @param slot In: The slot number. + * @param subslot In: The sub-slot number. + * @param idx In: The data record index. + * @param sequence_number In: The sequence number. + * @param write_length In: The length in bytes of the binary value. + * @param p_write_data In: A pointer to the binary value. + * @param p_result Out: Detailed error information if returning != 0 + * @return 0 on success. + * -1 if an error occurred. + */ +typedef int (*pnet_write_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t idx, + uint16_t sequence_number, + uint16_t write_length, + const uint8_t * p_write_data, + pnet_result_t * p_result); + +/** + * Indication to the application that a module is requested by the controller in + * a specific slot. + * + * This application call-back function is called by the Profinet stack to + * indicate that the controller has requested the presence of a specific module, + * ident number \a module_ident, in the slot number \a slot. + * + * The application must react to this by configuring itself accordingly (if + * possible) and call function pnet_plug_module() to configure the stack for + * this module. + * + * If the wrong module ident number is plugged then the stack will accept this, + * but signal to the controller that a substitute module is fitted. + * + * This function should return 0 (zero) if a valid module was plugged. Or return + * -1 if the application cannot handle this request. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param api In: The AP identifier. + * @param slot In: The slot number. + * @param module_ident In: The module ident number. + * @return 0 on success. + * -1 if an error occurred. + */ +typedef int (*pnet_exp_module_ind) ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint32_t module_ident); + +/** + * Indication to the application that a sub-module is requested by the + * controller in a specific sub-slot. + * + * This application call-back function is called by the Profinet stack to + * indicate that the controller has requested the presence of a specific + * sub-module with ident number \a submodule_ident, in the sub-slot number + * \a subslot, with module ident number \a module_ident in slot \a slot. + * + * If a module has not been plugged in the slot \a slot then an automatic plug + * request is issued internally by the stack. + * + * The application must react to this by configuring itself accordingly (if + * possible) and call function \a pnet_plug_submodule() to configure the stack + * with the correct input/output data sizes. + * + * If the wrong sub-module ident number is plugged then the stack will accept + * this, but signal to the controller that a substitute sub-module is fitted. + * + * This function should return 0 (zero) if a valid sub-module was plugged, + * or return -1 if the application cannot handle this request. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param api In: The AP identifier. + * @param slot In: The slot number. + * @param subslot In: The sub-slot number. + * @param module_ident In: The module ident number. + * @param submodule_ident In: The sub-module ident number. + * @param p_exp_data In: The expected data configuration (sizes and + * direction) + * @return 0 on success. + * -1 if an error occurred. + */ +typedef int (*pnet_exp_submodule_ind) ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_ident, + uint32_t submodule_ident, + const pnet_data_cfg_t * p_exp_data); + +/** + * Indication to the application that the data status received from the + * controller has changed. + * + * This application call-back function is called by the Profinet stack to + * indicate that the received data status has changed. + * + * The application is not required by the Profinet stack to take any action. It + * may use this information as it wishes. The return value from this call-back + * function is ignored by the Profinet stack. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param crep In: The CREP. + * @param changes In: The changed bits in the received data status. + * See pnet_data_status_bits_t + * @param data_status In: Current received data status (after changes). + * See pnet_data_status_bits_t + * @return 0 on success. Other values are ignored. + */ +typedef int (*pnet_new_data_status_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t crep, + uint8_t changes, + uint8_t data_status); + +/** + * The IO-controller has sent an alarm to the device. + * + * This functionality is used for alarms triggered by the IO-controller. + * + * When receiving this indication, the application shall + * respond with \a pnet_alarm_send_ack(), which + * may be called in the context of this callback. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param p_alarm_argument In: The alarm argument (with slot, subslot, + * alarm_type etc) + * @param data_len In: Data length + * @param data_usi In: Alarm USI + * @param p_data In: Alarm data + * @return 0 on success. + * Other values are ignored. + */ +typedef int (*pnet_alarm_ind) ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_argument, + uint16_t data_len, + uint16_t data_usi, + const uint8_t * p_data); + +/** + * The controller acknowledges the alarm sent previously. + * It is now possible to send another alarm. + * + * This functionality is used for alarms triggered by the IO-device. + * + * It is optional to implement this callback. + * The return value from this call-back function is ignored by the Profinet + * stack. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param p_pnio_status In: Detailed ACK information. + * @return 0 on success. Other values are ignored. + */ +typedef int (*pnet_alarm_cnf) ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status); + +/** + * The controller acknowledges the alarm ACK sent previously. + * + * This functionality is used for alarms triggered by the IO-controller. + * + * It is optional to implement this callback. + * The return value from this call-back function is ignored by the Profinet + * stack. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param arep In: The AREP. + * @param res In: 0 if ACK was received by the remote side. + * This is cnf(+). + * -1 if ACK was not received by the remote + * side. This is cnf(-). + * @return 0 on success. Other values are ignored. + */ +typedef int ( + *pnet_alarm_ack_cnf) (pnet_t * net, void * arg, uint32_t arep, int res); + +/** + * Indication to the application that a reset request was received from the + * IO-controller. + * + * The IO-controller can ask for communication parameters or application + * data to be reset, or to do a factory reset. + * + * This application call-back function is called by the Profinet stack on every + * reset request (via the DCP "Set" command) from the Profinet controller. + * + * The application should reset the application data if + * \a should_reset_application is true. For other cases this callback is + * triggered for diagnostic reasons. + * + * The return value from this call-back function is ignored by the Profinet + * stack. + * + * It is optional to implement this callback (if you do not have any application + * data that could be reset). + * + * Reset modes: + * * 0: (Power-on reset, not from IO-controller. Will not trigger this.) + * * 1: Reset application data + * * 2: Reset communication parameters (done by the stack) + * * 99: Reset all (factory reset). + * + * The reset modes 1-9 (out of which 1 and 2 are supported here) are defined + * by the Profinet standard. Value 99 is used here to indicate that the + * IO-controller has requested a factory reset via another mechanism. + * + * In order to remain responsive to DCP communication and Ethernet switching, + * the device should not do a hard or soft reset for reset mode 1 or 2. It is + * allowed for the factory reset case (but not mandatory). + * + * No \a arep information is available, as this callback typically is triggered + * when there is no active connection. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param should_reset_application In: True if the user should reset the + * application data. + * @param reset_mode In: Detailed reset information. + * @return 0 on success. Other values are ignored. + */ +typedef int (*pnet_reset_ind) ( + pnet_t * net, + void * arg, + bool should_reset_application, + uint16_t reset_mode); + +/** + * Indication to the application that the Profinet signal LED should change + * state. + * + * Use this callback to implement control of the LED. + * + * It is optional to implement this callback (but a compliant Profinet device + * must have a signal LED) + * + * No \a arep information is available, as this callback typically is triggered + * when there is no active connection. + * + * @param net InOut: The p-net stack instance + * @param arg InOut: User-defined data (not used by p-net) + * @param led_state In: True if the signal LED should be on. + * @return 0 on success. + * -1 if an error occurred. Will trigger a log message. + */ +typedef int (*pnet_signal_led_ind) (pnet_t * net, void * arg, bool led_state); + +/* + * Network and device configuration. + * + * Configuration of the stack is performed by transferring a structure + * in the call to pnet_init(). + * + * Along with the configuration the initial (default) values of the + * I&M data records are conveyed as well as the values used for + * sending LLDP frames. + * + * Configuration values are taken as is. No validation is performed. + */ + +/** + * Describe which I&M values are supported. + * + * I&M0 is always supported. + * + */ +typedef enum pnet_im_supported_values +{ + PNET_SUPPORTED_IM1 = 0x0002, + PNET_SUPPORTED_IM2 = 0x0004, + PNET_SUPPORTED_IM3 = 0x0008, + + /** Should only be used together with functional safety */ + PNET_SUPPORTED_IM4 = 0x0010, +} pnet_im_supported_values_t; + +/** + * The I&M0 data record is read-only by the controller. + * + * This data record is mandatory. + */ +typedef struct pnet_im_0 +{ + uint8_t im_vendor_id_hi; + uint8_t im_vendor_id_lo; + + /** Terminated string */ + char im_order_id[PNET_ORDER_ID_MAX_LEN + 1]; + + /** Terminated string */ + char im_serial_number[PNET_SERIAL_NUMBER_MAX_LEN + 1]; + + uint16_t im_hardware_revision; + char im_sw_revision_prefix; + uint8_t im_sw_revision_functional_enhancement; + uint8_t im_sw_revision_bug_fix; + uint8_t im_sw_revision_internal_change; + uint16_t im_revision_counter; + uint16_t im_profile_id; + uint16_t im_profile_specific_type; + + /** Always 1 */ + uint8_t im_version_major; + + /** Always 1 */ + uint8_t im_version_minor; + + /** One bit for each supported I&M1..15. I&M0 is always supported. + Use pnet_im_supported_values_t. */ + uint16_t im_supported; +} pnet_im_0_t; + +/** + * The I&M1 data record is read-write by the controller. + * + * This data record is optional. If this data record is supported + * by the application then bit 1 in the im_supported member of I&M0 shall be + * set. + */ +typedef struct pnet_im_1 +{ + /** Terminated string */ + char im_tag_function[32 + 1]; + + /** Terminated string */ + char im_tag_location[PNET_LOCATION_MAX_SIZE]; +} pnet_im_1_t; + +/** + * The I&M2 data record is read-write by the controller. + * + * This data record is optional. If this data record is supported + * by the application then bit 2 in the im_supported member of I&M0 shall be + * set. + */ +typedef struct pnet_im_2 +{ + /** Terminated string in the format "YYYY-MM-DD HH:MM" */ + char im_date[16 + 1]; +} pnet_im_2_t; + +/** + * The I&M3 data record is read-write by the controller. + * + * This data record is optional. If this data record is supported + * by the application then bit 3 in the im_supported member of I&M0 shall be + * set. + */ +typedef struct pnet_im_3 +{ + /** Terminated string padded with spaces */ + char im_descriptor[54 + 1]; +} pnet_im_3_t; + +/** + * The I&M4 data record is read-write by the controller. + * + * Used for functional safety. + * + * This data record is optional. If this data record is supported + * by the application then bit 4 in the im_supported member of I&M0 shall be + * set. + */ +typedef struct pnet_im_4 +{ + /** Non-terminated binary string, padded with zeroes */ + char im_signature[54]; +} pnet_im_4_t; + +/** + * The device-specific vendor and device identities. + * + * The vendor id is obtained from Profibus International. + * + * The device id is assigned by the vendor. + */ +typedef struct pnet_cfg_device_id +{ + uint8_t vendor_id_hi; + uint8_t vendor_id_lo; + uint8_t device_id_hi; + uint8_t device_id_lo; +} pnet_cfg_device_id_t; + +/** + * Used for assigning a static IPv4 address to the device. + * + * The Profinet stack also supports assigning an IP address, mask and gateway + * address via DCP Set commands based on the Ethernet MAC address. + * + * An IP address of 0.0.0.1 has the member d=1, and the rest of the members + * set to 0. + * + */ +typedef struct pnet_ip_addr_t +{ + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; +} pnet_cfg_ip_addr_t; + +/** + * The Ethernet MAC address. + */ +typedef struct pnet_ethaddr +{ + uint8_t addr[6]; +} pnet_ethaddr_t; + +/* Including termination. Standard says 240 (without termination) */ +#define PNET_CHASSIS_ID_MAX_SIZE (241) + +/* Including termination. Standard says 240 (without termination) */ +#define PNET_STATION_NAME_MAX_SIZE (241) + +/* Including termination. Standard says 14 (without termination) */ +#define PNET_PORT_NAME_MAX_SIZE (15) + +/** Including termination */ +#define PNET_LLDP_CHASSIS_ID_MAX_SIZE (PNET_CHASSIS_ID_MAX_SIZE) + +/** Including termination */ +#define PNET_LLDP_PORT_ID_MAX_SIZE \ + (PNET_STATION_NAME_MAX_SIZE + PNET_PORT_NAME_MAX_SIZE) + +/** + * Physical Port Configuration + */ +typedef struct pnet_port_cfg +{ + const char * netif_name; + + /** Ethernet MAU type to use when it not can be read from hardware. + * For example + * 0x0010 = Copper 100 Mbit/s Full duplex. + * 0x001E = Copper 1000 Mbit/s Full duplex. + * See \a pnal_eth_mau_t for more examples. */ + uint16_t default_mau_type; +} pnet_port_cfg_t; + +/** + * IP Configuration + */ +typedef struct pnet_ip_cfg +{ + /** Not yet supported by stack. */ + bool dhcp_enable; + + pnet_cfg_ip_addr_t ip_addr; + pnet_cfg_ip_addr_t ip_mask; + pnet_cfg_ip_addr_t ip_gateway; +} pnet_ip_cfg_t; + +/** + * Interface Configuration + * + * Configuration of network interfaces used by the stack. + * The \a main_netif_name defines the network interface used by a controller/PLC + * to access the device (called Management Port in Profinet). + * The \a physical_ports array defines the physical ports connected to the + * main_port. + * + * In the case one network interface is used, \a main_netif_name and + * \a physical_ports[0].netif_name will refer to the same network interface. + */ +typedef struct pnet_if_cfg +{ + /** Main (DAP) network interface */ + const char * main_netif_name; + + /** IP Settings for main network interface */ + pnet_ip_cfg_t ip_cfg; + + pnet_port_cfg_t physical_ports[PNET_MAX_PHYSICAL_PORTS]; +} pnet_if_cfg_t; + +/** + * This is all the configuration needed to use the Profinet stack. + * + * The application must supply the values in the call to + * function \a pnet_init(). + */ +typedef struct pnet_cfg +{ + /** Tick interval in us. + * Specifies the time between calls to \a pnet_handle_periodic(). + */ + uint32_t tick_us; + + pnet_state_ind state_cb; + pnet_connect_ind connect_cb; + pnet_release_ind release_cb; + pnet_dcontrol_ind dcontrol_cb; + pnet_ccontrol_cnf ccontrol_cb; + pnet_write_ind write_cb; + pnet_read_ind read_cb; + pnet_exp_module_ind exp_module_cb; + pnet_exp_submodule_ind exp_submodule_cb; + pnet_new_data_status_ind new_data_status_cb; + pnet_alarm_ind alarm_ind_cb; + pnet_alarm_cnf alarm_cnf_cb; + pnet_alarm_ack_cnf alarm_ack_cnf_cb; + pnet_reset_ind reset_cb; + pnet_signal_led_ind signal_led_cb; + + /** Userdata passed to callbacks, not used by stack */ + void * cb_arg; + + pnet_im_0_t im_0_data; + pnet_im_1_t im_1_data; + pnet_im_2_t im_2_data; + pnet_im_3_t im_3_data; + pnet_im_4_t im_4_data; + + pnet_cfg_device_id_t device_id; + pnet_cfg_device_id_t oem_device_id; + + /** Default station name. Terminated string */ + char station_name[PNET_STATION_NAME_MAX_SIZE]; + + /** + * Product name + * + * This is known as DeviceVendorValue and DeviceType in the Profinet + * specification. It constitutes the first part of SystemIdentification + * (sysDescr in SNMP). It may also be used to construct the Chassis ID. + * See IEC CDV 61158-6-10 ch. 4.10.3.3.1. + * + * Terminated string. + */ + char product_name[PNET_PRODUCT_NAME_MAX_LEN + 1]; + + /** Smallest allowed data exchange interval, in units of 31.25 us. + * Used for triggering error messages to the PLC. Should match GSDML file. + * Typically 32, which corresponds to 1 ms. Max 0x1000 (128 ms) */ + uint16_t min_device_interval; + + /** Operating system dependent settings */ + pnal_cfg_t pnal_cfg; + + /** Send DCP HELLO message on startup if true. */ + bool send_hello; + + /** Number of physical ports. Should respect PNET_MAX_PHYSICAL_PORTS. + This parameter is useful when shipping a single compiled version of the + library, but there are several applications with different number of + ports. */ + uint8_t num_physical_ports; // TODO int or uint16_t ? + + /** Send diagnosis in the qualified format (otherwise extended format) */ + bool use_qualified_diagnosis; + + pnet_if_cfg_t if_cfg; + +#if PNET_OPTION_DRIVER_ENABLE + bool driver_enable; + pnet_driver_config_t driver_config; +#endif + + /** Storage between runs + * Terminated string with absolute path. + * Use NULL or empty string for current directory. */ + char file_directory[PNET_MAX_DIRECTORYPATH_SIZE]; + +} pnet_cfg_t; + +/* + * API function return values + * + * All functions return either (zero) 0 for a successful call or + * -1 for an unsuccessful call. + */ + +/** + * Initialize the Profinet stack. + * + * This function must be called to initialize the Profinet stack. + * @param p_cfg In: Profinet configuration. These values are used + * at first startup and at factory reset. + * @return a handle to the stack instance, or NULL if an error occurred. + */ +PNET_EXPORT pnet_t * pnet_init (const pnet_cfg_t * p_cfg); + +/** + * Execute all periodic functions within the ProfiNet stack. + * + * This function shall be called periodically by the application. + * The period is specified by the tick_us parameter, part of pnet_cfg_t + * configuration. + * The period should match the expected I/O data rate to and from the device. + * @param net InOut: The p-net stack instance + */ +PNET_EXPORT void pnet_handle_periodic (pnet_t * net); + +/** + * Application signals ready to exchange data. + * + * Sends a CControl request to the IO-controller. + * + * This function must be called _after_ the application has received + * the \a pnet_state_ind() user callback with PNET_EVENT_PRMEND, + * in order for a connection to be established. + * + * If this CControl request ("Application ready") is sent (to the PLC) before + * the DControl response ("Param end") is sent (to the PLC) there will + * be problems at startup. + * + * If this function does not see all PPM data and IOPS areas are set (by the + * app) then it returns error and the application must try again - otherwise the + * startup will hang. + * + * Triggers the \a pnet_state_ind() user callback with PNET_EVENT_APPLRDY. + * + * @param net InOut: The p-net stack instance + * @param arep In: The AREP + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_application_ready (pnet_t * net, uint32_t arep); + +/** + * Plug a module into a slot. + * + * This function is used to plug a specific module into a specific slot. + * + * This function may be called from the \a pnet_exp_module_ind() call-back + * function of the application. + * + * @param net InOut: The p-net stack instance + * @param api In: The API identifier. + * @param slot In: The slot number. + * @param module_ident In: The module ident number. + * @return 0 if a module could be plugged into the slot. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_plug_module ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint32_t module_ident); + +/** + * Plug a sub-module into a sub-slot. + * + * This function is used to plug a specific sub-module into a specific sub-slot. + * + * This function may be called from the \a pnet_exp_submodule_ind call-back + * function of the application. + * + * If a module has not been plugged into the designated slot then it will be + * plugged automatically by this function. + * + * @param net InOut: The p-net stack instance + * @param api In: The API identifier. + * @param slot In: The slot number. + * @param subslot In: The sub-slot number. + * @param module_ident In: The module ident number. + * @param submodule_ident In: The sub-module ident number. + * @param direction In: The direction of data. + * @param length_input In: The size in bytes of the input data. + * @param length_output In: The size in bytes of the output data. + * @return 0 if the sub-module could be plugged into the sub-slot. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_plug_submodule ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_ident, + uint32_t submodule_ident, + pnet_submodule_dir_t direction, + uint16_t length_input, + uint16_t length_output); + +/** + * Pull a module from a slot. + * + * This function may be used to unplug a module from a specific slot. + * + * This function internally calls pnet_pull_submodule() on any plugged + * sub-modules before pulling the module itself. + * + * @param net InOut: The p-net stack instance + * @param api In: The API identifier. + * @param slot In: The slot number. + * @return 0 if a module could be pulled from the slot. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_pull_module (pnet_t * net, uint32_t api, uint16_t slot); + +/** + * Pull a sub-module from a sub-slot. + * + * This function may be used to unplug a sub-module from a specific sub-slot. + * + * @param net InOut: The p-net stack instance + * @param api In: The API identifier. + * @param slot In: The slot number. + * @param subslot In: The sub-slot number. + * @return 0 if the sub-module could be pulled from the sub-slot. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_pull_submodule ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot); + +/** + * Updates the IOPS and data of one sub-slot to send to the controller. + * + * This function may be called to set new data and IOPS values of a sub-slot to + * send to the controller. + * + * @param net InOut: The p-net stack instance + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param p_data In: Data buffer. If NULL the data will not + * be updated. + * @param data_len In: Bytes in data buffer. + * @param iops In: The device provider status. + * See pnet_ioxs_values_t + * @return 0 if a sub-module data and IOPS was set. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_input_set_data_and_iops ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + const uint8_t * p_data, + uint16_t data_len, + uint8_t iops); + +/** + * Fetch the controller consumer status of one sub-slot. + * + * This function is used to retrieve the consumer status (IOCS) value of + * an input sub-slot (data sent to the controller) back from the controller. + * + * Note that this function will reset the \a p_new_flag flag that is available + * in the \a pnet_output_get_data_and_iops() function. See that documentation + * entry for details. + * + * @param net InOut: The p-net stack instance + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param p_iocs Out: The controller consumer status. + * See pnet_ioxs_values_t + * @return 0 if a sub-module IOCS was set. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_input_get_iocs ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint8_t * p_iocs); + +/** + * Retrieve latest sub-slot data and IOPS received from the controller. + * + * This function may be called to retrieve the latest data and IOPS values + * of a sub-slot sent from the controller. + * + * Note that this function retrieves output data from the controller. The + * function \a pnet_input_set_data_and_iops() sends input data to the + * controller. Those are different data streams, so you can not expect to read + * back data that has been set by \a pnet_input_set_data_and_iops() using this + * function. + * + * If a valid new data (and IOPS) frame has arrived from the IO-controller since + * your last call to this function (regardless of the slot/subslot arguments) + * then the flag \a p_new_flag is set to true, else it is set to false. + * Note that this does not check whether the data content has changed from + * any previous frame. This flag will be reset also by using + * \a pnet_input_get_iocs(), so it is recommended to execute + * \a pnet_output_get_data_and_iops() first if you are to execute both + * (and the value of the flag is important to your application). + * + * Note that the latest data and IOPS values are copied to the application + * buffers regardless of the value of \a p_new_flag. + * + * @param net InOut: The p-net stack instance + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param p_new_flag Out: true if new data. + * @param p_data Out: The received data. + * @param p_data_len In: Size of receive buffer. + * Out: Received number of data bytes. + * @param p_iops Out: The controller provider status (IOPS). + * See pnet_ioxs_values_t + * @return 0 if a sub-module data and IOPS is retrieved. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_output_get_data_and_iops ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + bool * p_new_flag, + uint8_t * p_data, + uint16_t * p_data_len, + uint8_t * p_iops); + +/** + * Set the device consumer status for one sub-slot. + * + * This function is used to set the device consumer status (IOCS) + * of a specific sub-slot. It is used for output sub-slots (data from the + * controller) to send consumer status back to the controller. + * + * @param net InOut: The p-net stack instance + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param iocs In: The device consumer status. + * See pnet_ioxs_values_t + * @return 0 if a sub-module IOCS was set. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_output_set_iocs ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint8_t iocs); + +/** + * Set the state to "Primary" or "Backup" in the cyclic data sent to the + * IO-Controller. + * + * Implements the "Local Set State" primitive. + * + * By default the cyclic communication is "Primary". + * + * @param net InOut: The p-net stack instance + * @param primary In: true to set state to "Primary". + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_set_primary_state (pnet_t * net, bool primary); + +/** + * Set the state to redundant in the cyclic data sent to the IO-Controller. + * + * Implements the "Local Set Redundancy State" primitive. + * + * By default the cyclic communication is not in redundant state. + * + * @param net InOut: The p-net stack instance + * @param redundant In: true to set state to "redundant". + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_set_redundancy_state (pnet_t * net, bool redundant); + +/** + * Implements the "Local set Provider State" primitive. + * + * The application should call this function at least once during startup to set + * the provider status of frames sent to the controller. + * + * The application may call this function at any time, e.g., to signal a + * temporary inability to produce new application data to the controller. + * + * Its effect is similar to setting IOPS to PNET_IOXS_BAD for all sub-slots. + * + * @param net InOut: The p-net stack instance + * @param run In: true to set state to "run". + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_set_provider_state (pnet_t * net, bool run); + +/** + * Application requests abortion of the connection. + * + * @param net InOut: The p-net stack instance + * @param arep In: The AREP + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_ar_abort (pnet_t * net, uint32_t arep); + +/** + * Application requests factory reset of the device. + * + * Use this for example when a local hardware switch is used to do a + * factory reset. + * + * Also closes any open connections. + * + * @param net InOut: The p-net stack instance + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_factory_reset (pnet_t * net); + +/** + * Delete data files. + * + * Mainly intended for device development and testing. + * Applications should typically not use this function. + * + * Use \a pnet_factory_reset() instead, that uses this functionality internally. + * + * @param file_directory In: File directory + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_remove_data_files (const char * file_directory); + +/** + * Fetch error information from the AREP. + * + * @param net InOut: The p-net stack instance + * @param arep In: The AREP. + * @param p_err_cls Out: The error class. See PNET_ERROR_CODE_1_* + * @param p_err_code Out: The error code. See PNET_ERROR_CODE_2_* + * @return 0 If the AREP is valid. + * -1 if the AREP is not valid. + */ +PNET_EXPORT int pnet_get_ar_error_codes ( + pnet_t * net, + uint32_t arep, + uint16_t * p_err_cls, + uint16_t * p_err_code); + +/** + * Application creates an entry in the log book. + * + * This function is used to insert an entry into the Profinet log-book. + * Controllers may read the entire log-book using IODRead requests. + * + * @param net InOut: The p-net stack instance + * @param arep In: The AREP. + * @param p_pnio_status In: The PNIO status. + * @param entry_detail In: Additional application information. + * Manufacturer specific. + */ +PNET_EXPORT void pnet_create_log_book_entry ( + pnet_t * net, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status, + uint32_t entry_detail); + +/** + * Application issues a process alarm. + * + * This function may be used by the application to issue a process alarm. + * Such alarms are sent to the controller using high-priority alarm messages. + * + * Optional payload may be attached to the alarm. If \a payload_usi is != 0 + * and \a payload_len > 0 then + * the \a payload_usi and the payload at \a p_payload is attached to the alarm. + * + * After calling this function the application must wait for + * the \a pnet_alarm_cnf() callback before sending another alarm. + * This function fails if the application does not wait for the + * \a pnet_alarm_cnf() between sending two alarms. + * + * Note that the PLC can set the max alarm payload length at startup. This + * limit can be 200 to 1432 bytes. + * + * This functionality is used for alarms triggered by the IO-device. + * + * @param net InOut: The p-net stack instance + * @param arep In: The AREP. + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param payload_usi In: The USI for the payload. Max 0x7fff + * @param payload_len In: Length in bytes of the payload. May be 0. + * Max PNET_MAX_ALARM_PAYLOAD_DATA_SIZE or + * value from PLC. + * @param p_payload In: The alarm payload (USI specific format). + * @return 0 if the operation succeeded. + * -1 if an error occurred (or waiting for ACK from controller: re-try + * later). + */ +PNET_EXPORT int pnet_alarm_send_process_alarm ( + pnet_t * net, + uint32_t arep, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t payload_usi, + uint16_t payload_len, + const uint8_t * p_payload); + +/** + * Application acknowledges the reception of an alarm from the controller. + * + * This function sends an ACK to the controller. + * This function must be called by the application after receiving an alarm + * in the \a pnet_alarm_ind() call-back. Failure to do so within the timeout + * specified in the connect of the controller will make the controller + * re-send the alarm. + * + * This functionality is used for alarms triggered by the IO-controller. + * + * @param net InOut: The p-net stack instance + * @param arep In: The AREP. + * @param p_alarm_argument In: The alarm argument (with slot, subslot, + * alarm_type etc) + * @param p_pnio_status In: Detailed ACK status. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_alarm_send_ack ( + pnet_t * net, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_argument, + const pnet_pnio_status_t * p_pnio_status); + +/* ****************************** Diagnosis ****************************** */ + +#define PNET_CHANNEL_WHOLE_SUBMODULE 0x8000 + +/** + * Channel size in bits. Values 8..255 are reserved. + */ +typedef enum pnet_diag_ch_prop_type_values +{ + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED = 0, + PNET_DIAG_CH_PROP_TYPE_1_BIT = 1, + PNET_DIAG_CH_PROP_TYPE_2_BIT = 2, + PNET_DIAG_CH_PROP_TYPE_4_BIT = 3, + PNET_DIAG_CH_PROP_TYPE_8_BIT = 4, + PNET_DIAG_CH_PROP_TYPE_16_BIT = 5, + PNET_DIAG_CH_PROP_TYPE_32_BIT = 6, + PNET_DIAG_CH_PROP_TYPE_64_BIT = 7, +} pnet_diag_ch_prop_type_values_t; + +/** Channel group or individual channel. Also known as "Accumulative" */ +typedef enum pnet_diag_ch_group_values +{ + PNET_DIAG_CH_INDIVIDUAL_CHANNEL = 0, + PNET_DIAG_CH_CHANNEL_GROUP = 1 +} pnet_diag_ch_group_values_t; + +/** + * Channel direction. + */ +typedef enum pnet_diag_ch_prop_dir_values +{ + PNET_DIAG_CH_PROP_DIR_MANUF_SPEC = 0, + PNET_DIAG_CH_PROP_DIR_INPUT = 1, + PNET_DIAG_CH_PROP_DIR_OUTPUT = 2, + PNET_DIAG_CH_PROP_DIR_INOUT = 3, +} pnet_diag_ch_prop_dir_values_t; + +/** + * Diagnosis severity. + */ +typedef enum pnet_diag_ch_prop_maint_values +{ + PNET_DIAG_CH_PROP_MAINT_FAULT = 0, + PNET_DIAG_CH_PROP_MAINT_REQUIRED = 1, + PNET_DIAG_CH_PROP_MAINT_DEMANDED = 2, + PNET_DIAG_CH_PROP_MAINT_QUALIFIED = 3, +} pnet_diag_ch_prop_maint_values_t; + +/** + * Diagnosis source. + */ +typedef struct pnet_diag_source +{ + uint32_t api; + uint16_t slot; + uint16_t subslot; + + /** 0 - 0x7FFF manufacturer specific, 0x8000 whole submodule */ + uint16_t ch; + + pnet_diag_ch_group_values_t ch_grouping; + pnet_diag_ch_prop_dir_values_t ch_direction; +} pnet_diag_source_t; + +/** + * Add a diagnosis entry, in the standard format. + * + * The channel error types and the extended channel error types are + * defined in Profinet 2.4 Services Annex F. + * + * This sends a diagnosis alarm. + * + * Uses the "Qualified channel diagnosis" format on the wire if the + * configuration parameter use_qualified_diagnosis is true, otherwise + * "Extended channel diagnosis". + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_bits In: Number of bits in the channel. + * @param severity In: Diagnosis severity. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type + * (more details on the channel error type). + * Use 0 if not given. + * @param ext_ch_add_value In: The extended channel error additional + * value. Typically a measurement value (for + * example number of dropped packets) related + * to the error. + * @param qual_ch_qualifier In: More detailed severity information. Used + * when severity = + * PNET_DIAG_CH_PROP_MAINT_QUALIFIED and the + * use_qualified_diagnosis is enabled. + * Max one bit should be set. + * Use 0 otherwise. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_std_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier); + +/** + * Update the "extended channel error additional value", for a diagnosis entry + * in the standard format. + * + * The Profinet standard says that diagnosis updates should not be done at a + * higher frequency than 1 Hz. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type + * (more details on the channel error type). + * Use 0 if not given. + * @param ext_ch_add_value In: New extended channel error additional value. + * Typically a measurement value (for + * example number of dropped packets) related + * to the error. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_std_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value); + +/** + * Remove a diagnosis entry in the standard format. + * + * An error is returned if the diagnosis doesn't exist. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type + * (more details on the channel error type). + * Use 0 if not given. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_std_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type); + +/** + * Add a diagnosis entry in manufacturer-specified ("USI") format. + * + * If the diagnosis already exists, it is updated. + * Use \a pnet_diag_usi_update() instead if you would like to have an error + * if the diagnosis is missing when trying to update it. + * + * A diagnosis in USI format is always assigned to the channel “whole submodule” + * (not individual channels). The severity is always “Fault”, the accumulative + * flag is false and the direction is "Manufacturer specific". + * + * It is recommended to use the standard diagnosis format in most cases. + * Use the manufacturer specific format ("USI format") only if it's not + * possible to use the standard format. + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * This payload limit can be 200 to 1432 bytes. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param usi In: The USI. Range 0..0x7fff + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * A value 0 is allowed. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or value + * from PLC. + * @param p_manuf_data In: The manufacturer specific diagnosis data. + * Mandatory if manuf_data_len > 0, otherwise + * NULL. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_usi_add ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Update the manufacturer specific data, for a diagnosis entry in + * manufacturer-specified ("USI") format. + * + * An error is returned if the diagnosis doesn't exist. + * Use \a pnet_diag_usi_add() instead if you would like to create the + * missing diagnosis when trying to update it. + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * This payload limit can be 200 to 1432 bytes. + * + * The Profinet standard says that diagnosis updates should not be done at a + * higher frequency than 1 Hz. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param usi In: The USI. Range 0..0x7fff + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * A value 0 is allowed. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or + * value from PLC. + * @param p_manuf_data In: New manufacturer specific diagnosis data. + * Mandatory if manuf_data_len > 0, otherwise + * NULL. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_usi_update ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Remove a diagnosis entry in manufacturer-specified ("USI") format. + * + * An error is returned if the diagnosis doesn't exist. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param usi In: The USI. Range 0..0x7fff + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_usi_remove ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t usi); + +/** + * Add a diagnosis entry, in standard or USI format. + * + * This is a low-level function. Typically you should use one of these instead + * (which uses this function internally): + * - pnet_diag_std_add() + * - pnet_diag_usi_add() + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * This payload limit can be 200 to 1432 bytes. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_bits In: Number of bits in the channel. + * @param severity In: Diagnosis severity. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type. + * @param ext_ch_add_value In: The extended channel error additional + * value. + * @param qual_ch_qualifier In: The qualified channel qualifier. + * @param usi In: The USI. + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or + * value from PLC. + * (Only needed if USI <= 0x7fff, + * and may still be 0). + * @param p_manuf_data In: The manufacturer specific diagnosis data. + * (Only needed if USI <= 0x7fff). + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Update a diagnosis entry, in standard or USI format. + * + * This is a low-level function. Typically you should use one of these instead + * (which uses this function internally): + * - pnet_diag_std_update() + * - pnet_diag_usi_update() + * + * If the USI of the item is in the manufacturer-specified range then + * the USI is used to identify the item. + * Otherwise the other parameters are used (all must match): + * - p_diag_source (all fields) + * - Channel error type. + * - Extended error type. + * + * When the item is found either the manufacturer data is updated (for + * USI in manufacturer-specific range) or the extended channel additional + * value is updated. + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * This payload limit can be 200 to 1432 bytes. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type, or 0. + * @param ext_ch_add_value In: New extended channel error additional value. + * @param usi In: The USI. + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or + * value from PLC. + * (Only needed if USI <= 0x7fff, + * and may still be 0). + * @param p_manuf_data In: New manufacturer specific diagnosis data. + * (Only needed if USI <= 0x7fff). + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Remove a diagnosis entry, in standard or USI format. + * + * This is a low-level function. Typically you should use one of these instead + * (which uses this function internally): + * - pnet_diag_std_remove() + * - pnet_diag_usi_remove() + * + * If the USI of the item is in the manufacturer-specified range then + * the USI is used to identify the item. + * Otherwise the other parameters are used (all must match): + * - p_diag_source (all fields) + * - Channel error type. + * - Extended error type. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type, or 0. + * @param usi In: The USI. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +PNET_EXPORT int pnet_diag_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint16_t usi); + +/******************** Show Profinet stack info ********************************/ + +/** + * Show information from the Profinet stack. + * + * @param net InOut: The p-net stack instance + * @param level In: The amount of detail to show. + * + * 0x0010 | Show compile time options + * 0x0020 | Show CMDEV + * 0x0080 | Show SNMP + * 0x0100 | Show Ports + * 0x0200 | Show diagnosis + * 0x0400 | Show logbook + * 0x0800 | Show all sessions. + * 0x1000 | Show all ARs. + * 0x1001 | include IOCR. + * 0x1002 | include data_descriptors. + * 0x1003 | include IOCR and data_descriptors. + * 0x2000 | Show config/CMINA information. + * 0x4000 | Show scheduler information. + * 0x8000 | Show I&M data. + * + * Bit in the level parameter: + * 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 + * 1 I&M + * 1 Scheduler + * 1 CMINA + * 1 AR + * 1 Sessions + * 1 Logbook + * 1 Diagnosis + * 1 Ports + * 1 SNMP + * 1 CMDEV + * 1 Options + * 1 More IOCR info on AR + * 1 More IOCR info on AR + */ +PNET_EXPORT void pnet_show (pnet_t * net, unsigned level); + +#ifdef __cplusplus +} +#endif + +#endif /* PNET_API_H */ diff --git a/profinet_stack/p-net/options.h.in b/profinet_stack/p-net/options.h.in new file mode 100644 index 0000000..ca4abc2 --- /dev/null +++ b/profinet_stack/p-net/options.h.in @@ -0,0 +1,246 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef OPTIONS_H +#define OPTIONS_H + +/** + * # Profinet Stack Options + * + * The defines named `PNET_OPTION_*` may be used to extend or reduce + * the functionality of the stack. Setting these values to 0 excludes + * the specific function and may also reduce the memory usage of the + * Profinet stack. + * + * Note that none of these options are currently supported (even if + * enabled by setting the value to 1), except for parsing the RPC + * Connect request message and generating the connect RPC Connect + * response message. + */ + +#if !defined (PNET_OPTION_FAST_STARTUP) +#cmakedefine01 PNET_OPTION_FAST_STARTUP +#endif + +#if !defined (PNET_OPTION_PARAMETER_SERVER) +#cmakedefine01 PNET_OPTION_PARAMETER_SERVER +#endif + +#if !defined (PNET_OPTION_IR) +#cmakedefine01 PNET_OPTION_IR +#endif + +#if !defined (PNET_OPTION_SR) +#cmakedefine01 PNET_OPTION_SR +#endif + +#if !defined (PNET_OPTION_REDUNDANCY) +#cmakedefine01 PNET_OPTION_REDUNDANCY +#endif + +#if !defined (PNET_OPTION_AR_VENDOR_BLOCKS) +#cmakedefine01 PNET_OPTION_AR_VENDOR_BLOCKS +#endif + +#if !defined (PNET_OPTION_RS) +#cmakedefine01 PNET_OPTION_RS +#endif + +#if !defined (PNET_OPTION_MC_CR) +#cmakedefine01 PNET_OPTION_MC_CR +#endif + +#if !defined (PNET_OPTION_SRL) +#cmakedefine01 PNET_OPTION_SRL +#endif + +#if !defined (PNET_OPTION_SNMP) +#cmakedefine01 PNET_OPTION_SNMP +#endif + +/** + * Disable use of atomic operations (stdatomic.h). + * If the compiler supports it then set this define to 1. + */ +/* TODO: compiler abstraction should be handled by cc.h */ +#if !defined (PNET_USE_ATOMICS) +#cmakedefine01 PNET_USE_ATOMICS +#endif + +/** + * # Memory Usage + * + * Memory usage is controlled by the `PNET_MAX_*` defines. Define the + * required number of supported items. + * + * These values directly affect the memory usage of the + * implementation. Sometimes in complicated ways. + * + * Please note that some defines have minimum requirements. + * Only a few of the defines are validated. + */ + +#if !defined (PNET_MAX_AR) +/** Number of connections. Must be > 0. "Automated RT Tester" uses 2 */ +#define PNET_MAX_AR @PNET_MAX_AR@ +#endif + +#if !defined (PNET_MAX_API) +/** Number of Application Processes. Must be > 0. */ +#define PNET_MAX_API @PNET_MAX_API@ +#endif + +#if !defined (PNET_MAX_CR) +/** Per AR. 1 input and 1 output. */ +#define PNET_MAX_CR @PNET_MAX_CR@ +#endif + +#if !defined (PNET_MAX_SLOTS) +/** Per API. Should be > 1 to allow at least one I/O module. */ +#define PNET_MAX_SLOTS @PNET_MAX_SLOTS@ +#endif + +#if !defined (PNET_MAX_SUBSLOTS) +/** Per slot (3 needed for DAP). */ +#define PNET_MAX_SUBSLOTS @PNET_MAX_SUBSLOTS@ +#endif + +#if !defined (PNET_MAX_DFP_IOCR) +/** Allowed values are 0 (zero) or 2. */ +#define PNET_MAX_DFP_IOCR @PNET_MAX_DFP_IOCR@ +#endif + +#if !defined (PNET_MAX_LOG_BOOK_ENTRIES) +#define PNET_MAX_LOG_BOOK_ENTRIES @PNET_MAX_LOG_BOOK_ENTRIES@ +#endif + +#if !defined (PNET_MAX_ALARMS) +/** Per AR and queue. One queue for hi and one for lo alarms. */ +#define PNET_MAX_ALARMS @PNET_MAX_ALARMS@ +#endif + +#if !defined (PNET_MAX_ALARM_PAYLOAD_DATA_SIZE) +/** Max size of alarm payload */ +#define PNET_MAX_ALARM_PAYLOAD_DATA_SIZE @PNET_MAX_ALARM_PAYLOAD_DATA_SIZE@ +#endif + +#if !defined (PNET_MAX_DIAG_ITEMS) +/** Total, per device. Max is 65534 items. */ +#define PNET_MAX_DIAG_ITEMS @PNET_MAX_DIAG_ITEMS@ +#endif + +#if !defined (PNET_MAX_DIAG_MANUF_DATA_SIZE) +/** Max size of manufacturer specific diagnosis data */ +#define PNET_MAX_DIAG_MANUF_DATA_SIZE @PNET_MAX_DIAG_MANUF_DATA_SIZE@ +#endif + +#if PNET_OPTION_MC_CR + +#if !defined (PNET_MAX_MC_CR) +/**< Per AR. */ +#define PNET_MAX_MC_CR @PNET_MAX_MC_CR@ +#endif + +#endif /* PNET_OPTION_MC_CR */ + +#if PNET_OPTION_AR_VENDOR_BLOCKS + +#if !defined (PNET_MAX_AR_VENDOR_BLOCKS) +/**< Must be > 0 */ +#define PNET_MAX_AR_VENDOR_BLOCKS @PNET_MAX_AR_VENDOR_BLOCKS@ +#endif + +#if !defined (PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH) +#define PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH @PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH@ +#endif + +#endif /* PNET_OPTION_AR_VENDOR_BLOCKS */ + +#if !defined (PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH) +/** or 512 (bytes) */ +#define PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH @PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH@ +#endif + +#if !defined (PNET_MAX_SESSION_BUFFER_SIZE) +/** Max fragmented RPC request/response length */ +#define PNET_MAX_SESSION_BUFFER_SIZE @PNET_MAX_SESSION_BUFFER_SIZE@ +#endif + +#if !defined (PNET_MAX_FILENAME_SIZE) +/** Max filename size, including termination */ +#define PNET_MAX_FILENAME_SIZE @PNET_MAX_FILENAME_SIZE@ +#endif + +#if !defined (PNET_MAX_PORT_DESCRIPTION_SIZE) +/** Max port description size, including termination */ +#define PNET_MAX_PORT_DESCRIPTION_SIZE @PNET_MAX_PORT_DESCRIPTION_SIZE@ +#endif + + +/** + * # Logging + * + * The following options controlling logging. + */ + +#ifndef LOG_LEVEL +#define LOG_LEVEL (LOG_LEVEL_@LOG_LEVEL@) +#endif + +#ifndef PF_ETH_LOG +#define PF_ETH_LOG (LOG_STATE_@PF_ETH_LOG@) +#endif + +#ifndef PF_LLDP_LOG +#define PF_LLDP_LOG (LOG_STATE_@PF_LLDP_LOG@) +#endif + +#ifndef PF_SNMP_LOG +#define PF_SNMP_LOG (LOG_STATE_@PF_SNMP_LOG@) +#endif + +#ifndef PF_CPM_LOG +#define PF_CPM_LOG (LOG_STATE_@PF_CPM_LOG@) +#endif + +#ifndef PF_PPM_LOG +#define PF_PPM_LOG (LOG_STATE_@PF_PPM_LOG@) +#endif + +#ifndef PF_DCP_LOG +#define PF_DCP_LOG (LOG_STATE_@PF_DCP_LOG@) +#endif + +#ifndef PF_RPC_LOG +#define PF_RPC_LOG (LOG_STATE_@PF_RPC_LOG@) +#endif + +#ifndef PF_ALARM_LOG +#define PF_ALARM_LOG (LOG_STATE_@PF_ALARM_LOG@) +#endif + +#ifndef PF_AL_BUF_LOG +#define PF_AL_BUF_LOG (LOG_STATE_@PF_AL_BUF_LOG@) +#endif + +#ifndef PF_PNAL_LOG +#define PF_PNAL_LOG (LOG_STATE_@PF_PNAL_LOG@) +#endif + +#ifndef PNET_LOG +#define PNET_LOG (LOG_STATE_@PNET_LOG@) +#endif + +#endif /* OPTIONS_H */ diff --git a/profinet_stack/p-net/pnet_options.h.in b/profinet_stack/p-net/pnet_options.h.in new file mode 100644 index 0000000..f4c9816 --- /dev/null +++ b/profinet_stack/p-net/pnet_options.h.in @@ -0,0 +1,33 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PNET_OPTIONS_H +#define PNET_OPTIONS_H + +#if !defined (PNET_MAX_PHYSICAL_PORTS) +/** Max number of physical ports */ +#define PNET_MAX_PHYSICAL_PORTS @PNET_MAX_PHYSICAL_PORTS@ +#endif + +#if !defined (PNET_MAX_DIRECTORYPATH_SIZE) +/** Max directory path size, including termination */ +#define PNET_MAX_DIRECTORYPATH_SIZE @PNET_MAX_DIRECTORYPATH_SIZE@ +#endif + +#if !defined (PNET_OPTION_DRIVER_ENABLE) +#cmakedefine01 PNET_OPTION_DRIVER_ENABLE +#endif + +#endif /* PNET_OPTIONS_H */ diff --git a/profinet_stack/p-net/pnet_version.h.in b/profinet_stack/p-net/pnet_version.h.in new file mode 100644 index 0000000..d04b7ad --- /dev/null +++ b/profinet_stack/p-net/pnet_version.h.in @@ -0,0 +1,41 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PNET_VERSION_H +#define PNET_VERSION_H + +#cmakedefine PROFINET_GIT_REVISION "@PROFINET_GIT_REVISION@" + +#if !defined(PNET_VERSION_BUILD) && defined(PROFINET_GIT_REVISION) +#define PNET_VERSION_BUILD PROFINET_GIT_REVISION +#endif + +/* clang-format-off */ + +#define PNET_VERSION_MAJOR @PROFINET_VERSION_MAJOR@ +#define PNET_VERSION_MINOR @PROFINET_VERSION_MINOR@ +#define PNET_VERSION_PATCH @PROFINET_VERSION_PATCH@ + +#if defined(PNET_VERSION_BUILD) +#define PNET_VERSION \ + "@PROFINET_VERSION_MAJOR@.@PROFINET_VERSION_MINOR@.@PROFINET_VERSION_PATCH@+" PNET_VERSION_BUILD +#else +#define PNET_VERSION \ + "@PROFINET_VERSION_MAJOR@.@PROFINET_VERSION_MINOR@.@PROFINET_VERSION_PATCH@" +#endif + +/* clang-format-on */ + +#endif /* PNET_VERSION_H */ diff --git a/profinet_stack/p-net/samples/pn_dev/CMakeLists.txt b/profinet_stack/p-net/samples/pn_dev/CMakeLists.txt new file mode 100644 index 0000000..dcdd713 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/CMakeLists.txt @@ -0,0 +1,21 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# http://www.rt-labs.com +# Copyright 2017 rt-labs AB, Sweden. +# See LICENSE file in the project root for full license information. +#*******************************************************************/ + +target_include_directories(pn_dev + PRIVATE + ${PROFINET_SOURCE_DIR}/src + ${PROFINET_BINARY_DIR}/src + ) + +target_link_libraries (pn_dev PUBLIC profinet) + +install (TARGETS pn_dev DESTINATION bin) diff --git a/profinet_stack/p-net/samples/pn_dev/GSDML-RT-LABS-STACK.bmp b/profinet_stack/p-net/samples/pn_dev/GSDML-RT-LABS-STACK.bmp new file mode 100644 index 0000000000000000000000000000000000000000..02189afeac67c4757b312bccc80975eb1c71cd21 GIT binary patch literal 47670 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 literal 0 HcmV?d00001 diff --git a/profinet_stack/p-net/samples/pn_dev/GSDML-V2.4-RT-Labs-P-Net-Sample-App-20220324.xml b/profinet_stack/p-net/samples/pn_dev/GSDML-V2.4-RT-Labs-P-Net-Sample-App-20220324.xml new file mode 100644 index 0000000..f565d10 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/GSDML-V2.4-RT-Labs-P-Net-Sample-App-20220324.xml @@ -0,0 +1,301 @@ + + + + + PROFINET Device Profile + 1.00 + Device Profile for PROFINET Devices + PROFIBUS Nutzerorganisation e. V. (PNO) + Device + + 4 + 1 + GSDML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/profinet_stack/p-net/samples/pn_dev/app_data.c b/profinet_stack/p-net/samples/pn_dev/app_data.c new file mode 100644 index 0000000..0f63a60 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_data.c @@ -0,0 +1,315 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "app_data.h" +#include "app_utils.h" +#include "app_gsdml.h" +#include "app_log.h" +#include "sampleapp_common.h" +#include "osal.h" +#include "pnal.h" +#include + +#include +#include +#include + +#define APP_DATA_DEFAULT_OUTPUT_DATA 0 + +/* Parameter data for digital submodules + * The stored value is shared between all digital submodules in this example. + * + * Todo: Data is always in pnio data format. Add conversion to uint32_t. + */ +static uint32_t app_param_1 = 0; /* Network endianness */ +static uint32_t app_param_2 = 0; /* Network endianness */ + +/* Parameter data for echo submodules + * The stored value is shared between all echo submodules in this example. + * + * Todo: Data is always in pnio data format. Add conversion to uint32_t. + */ +static uint32_t app_param_echo_gain = 1; /* Network endianness */ + +/* Digital submodule process data + * The stored value is shared between all digital submodules in this example. */ +static uint8_t inputdata[APP_GSDML_INPUT_DATA_DIGITAL_SIZE] = {0}; +static uint8_t outputdata[APP_GSDML_OUTPUT_DATA_DIGITAL_SIZE] = {0}; +static uint8_t counter = 0; + +/* Network endianness */ +static uint8_t echo_inputdata[APP_GSDML_INPUT_DATA_ECHO_SIZE] = {0}; +static uint8_t echo_outputdata[APP_GSDML_OUTPUT_DATA_ECHO_SIZE] = {0}; + +CC_PACKED_BEGIN +typedef struct CC_PACKED app_echo_data +{ + /* Network endianness. + Used as a float, but we model it as a 4-byte integer to easily + do endianness conversion */ + uint32_t echo_float_bytes; + + /* Network endianness */ + uint32_t echo_int; +} app_echo_data_t; +CC_PACKED_END +CC_STATIC_ASSERT (sizeof (app_echo_data_t) == APP_GSDML_INPUT_DATA_ECHO_SIZE); +CC_STATIC_ASSERT (sizeof (app_echo_data_t) == APP_GSDML_OUTPUT_DATA_ECHO_SIZE); + +/** + * Set LED state. + * + * Compares new state with previous state, to minimize system calls. + * + * Uses the hardware specific app_set_led() function. + * + * @param led_state In: New LED state + */ +static void app_handle_data_led_state (bool led_state) +{ + static bool previous_led_state = false; + + if (led_state != previous_led_state) + { + app_set_led (APP_DATA_LED_ID, led_state); + } + previous_led_state = led_state; +} + +uint8_t * app_data_get_input_data ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + bool button_pressed, + uint16_t * size, + uint8_t * iops) +{ + float inputfloat; + float outputfloat; + uint32_t hostorder_inputfloat_bytes; + uint32_t hostorder_outputfloat_bytes; + app_echo_data_t * p_echo_inputdata = (app_echo_data_t *)&echo_inputdata; + app_echo_data_t * p_echo_outputdata = (app_echo_data_t *)&echo_outputdata; + + if (size == NULL || iops == NULL) + { + return NULL; + } + + if ( + submodule_id == APP_GSDML_SUBMOD_ID_DIGITAL_IN || + submodule_id == APP_GSDML_SUBMOD_ID_DIGITAL_IN_OUT) + { + /* Prepare digital input data + * Lowest 7 bits: Counter Most significant bit: Button + */ + inputdata[0] = counter++; + if (button_pressed) + { + inputdata[0] |= 0x80; + } + else + { + inputdata[0] &= 0x7F; + } + + *size = APP_GSDML_INPUT_DATA_DIGITAL_SIZE; + *iops = PNET_IOXS_GOOD; + return inputdata; + } + + if (submodule_id == APP_GSDML_SUBMOD_ID_ECHO) + { + /* Calculate echodata input (to the PLC) + * by multiplying the output (from the PLC) with a gain factor + */ + + /* Integer */ + p_echo_inputdata->echo_int = CC_TO_BE32 ( + CC_FROM_BE32 (p_echo_outputdata->echo_int) * + CC_FROM_BE32 (app_param_echo_gain)); + + /* Float */ + /* Use memcopy to avoid strict-aliasing rule warnings */ + hostorder_outputfloat_bytes = + CC_FROM_BE32 (p_echo_outputdata->echo_float_bytes); + memcpy (&outputfloat, &hostorder_outputfloat_bytes, sizeof (outputfloat)); + inputfloat = outputfloat * CC_FROM_BE32 (app_param_echo_gain); + memcpy (&hostorder_inputfloat_bytes, &inputfloat, sizeof (outputfloat)); + p_echo_inputdata->echo_float_bytes = + CC_TO_BE32 (hostorder_inputfloat_bytes); + + *size = APP_GSDML_INPUT_DATA_ECHO_SIZE; + *iops = PNET_IOXS_GOOD; + return echo_inputdata; + } + + /* Automated RT Tester scenario 2 - unsupported (sub)module */ + return NULL; +} + +int app_data_set_output_data ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + uint8_t * data, + uint16_t size) +{ + bool led_state; + + if (data == NULL) + { + return -1; + } + + if ( + submodule_id == APP_GSDML_SUBMOD_ID_DIGITAL_OUT || + submodule_id == APP_GSDML_SUBMOD_ID_DIGITAL_IN_OUT) + { + if (size == APP_GSDML_OUTPUT_DATA_DIGITAL_SIZE) + { + memcpy (outputdata, data, size); + + /* Most significant bit: LED */ + led_state = (outputdata[0] & 0x80) > 0; + app_handle_data_led_state (led_state); + + return 0; + } + } + else if (submodule_id == APP_GSDML_SUBMOD_ID_ECHO) + { + if (size == APP_GSDML_OUTPUT_DATA_ECHO_SIZE) + { + memcpy (echo_outputdata, data, size); + + return 0; + } + } + + return -1; +} + +int app_data_set_default_outputs (void) +{ + outputdata[0] = APP_DATA_DEFAULT_OUTPUT_DATA; + app_handle_data_led_state (false); + return 0; +} + +int app_data_write_parameter ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + uint32_t index, + const uint8_t * data, + uint16_t length) +{ + const app_gsdml_param_t * par_cfg; + + par_cfg = app_gsdml_get_parameter_cfg (submodule_id, index); + if (par_cfg == NULL) + { + APP_LOG_WARNING ( + "PLC write request unsupported submodule/parameter. " + "Submodule id: %u Index: %u\n", + (unsigned)submodule_id, + (unsigned)index); + return -1; + } + + if (length != par_cfg->length) + { + APP_LOG_WARNING ( + "PLC write request unsupported length. " + "Index: %u Length: %u Expected length: %u\n", + (unsigned)index, + (unsigned)length, + par_cfg->length); + return -1; + } + + if (index == APP_GSDML_PARAMETER_1_IDX) + { + memcpy (&app_param_1, data, length); + } + else if (index == APP_GSDML_PARAMETER_2_IDX) + { + memcpy (&app_param_2, data, length); + } + else if (index == APP_GSDML_PARAMETER_ECHO_IDX) + { + memcpy (&app_param_echo_gain, data, length); + } + + APP_LOG_DEBUG (" Writing parameter \"%s\"\n", par_cfg->name); + app_log_print_bytes (APP_LOG_LEVEL_DEBUG, data, length); + + return 0; +} + +int app_data_read_parameter ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + uint32_t index, + uint8_t ** data, + uint16_t * length) +{ + const app_gsdml_param_t * par_cfg; + + par_cfg = app_gsdml_get_parameter_cfg (submodule_id, index); + if (par_cfg == NULL) + { + APP_LOG_WARNING ( + "PLC read request unsupported submodule/parameter. " + "Submodule id: %u Index: %u\n", + (unsigned)submodule_id, + (unsigned)index); + return -1; + } + + if (*length < par_cfg->length) + { + APP_LOG_WARNING ( + "PLC read request unsupported length. " + "Index: %u Length: %u Expected length: %u\n", + (unsigned)index, + (unsigned)*length, + par_cfg->length); + return -1; + } + + APP_LOG_DEBUG (" Reading \"%s\"\n", par_cfg->name); + if (index == APP_GSDML_PARAMETER_1_IDX) + { + *data = (uint8_t *)&app_param_1; + *length = sizeof (app_param_1); + } + else if (index == APP_GSDML_PARAMETER_2_IDX) + { + *data = (uint8_t *)&app_param_2; + *length = sizeof (app_param_2); + } + else if (index == APP_GSDML_PARAMETER_ECHO_IDX) + { + *data = (uint8_t *)&app_param_echo_gain; + *length = sizeof (app_param_echo_gain); + } + + app_log_print_bytes (APP_LOG_LEVEL_DEBUG, *data, *length); + + return 0; +} diff --git a/profinet_stack/p-net/samples/pn_dev/app_data.h b/profinet_stack/p-net/samples/pn_dev/app_data.h new file mode 100644 index 0000000..83e7c4b --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_data.h @@ -0,0 +1,140 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_DATA_H +#define APP_DATA_H + +/** + * @file + * @brief Sample application data interface + * + * Functions for: + * - Getting input data (Button 1 and counter value) + * - Setting output data (LED 1) + * - Setting default output state. This should be + * part of all device implementations for setting + * defined state when device is not connected to PLC + * - Reading and writing parameters + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * Get application specific PNIO input data (for sending to PLC) + * + * The main sample application keeps track + * of button so it is a parameter to this function. + * + * This function is not called for the DAP submodules (slot_nbr==0). + * + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @param submodule_id In: Submodule id + * @param button_state In: State of button 1 + * @param size Out: Size of pnio data. + * Not modified on error. + * @param iops Out: Provider status. If for example + * a sensor is failing or a short + * circuit is detected on digital + * input this shall be set to BAD. + * Not modified on error. + * @return Reference to PNIO data, NULL on error + */ +uint8_t * app_data_get_input_data ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + bool button_state, + uint16_t * size, + uint8_t * iops); + +/** + * Set application specific PNIO output data (received from PLC) + * + * This function is not called for the DAP submodules (slot_nbr==0). + * + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @param submodule_id In: Submodule id + * @param data In: Reference to output data + * @param size In: Length of output data + * @return 0 on success, -1 on error + */ +int app_data_set_output_data ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + uint8_t * data, + uint16_t size); + +/** + * Set default outputs for all subslots. + * + * For the sample application this means that + * LED 1 is turned off. + * + * @return 0 on success, -1 on error + */ +int app_data_set_default_outputs (void); + +/** + * Write parameter index for a subslot + * + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @param submodule_id In: Submodule id + * @param index In: Parameter index + * @param data In: New parameter value + * @param write_length In: Length of parameter data + * @return 0 on success, -1 on error + */ +int app_data_write_parameter ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + uint32_t index, + const uint8_t * data, + uint16_t write_length); + +/** + * Read parameter index from a subslot + * + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @param submodule_id In: Submodule id + * @param index In: Parameter index + * @param data In: Reference to parameter data + * @param length InOut: The maximum (in) and actual (out) length in + * bytes of the data. + * @return 0 on success, -1 on error + */ +int app_data_read_parameter ( + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + uint32_t index, + uint8_t ** data, + uint16_t * length); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_DATA_H */ diff --git a/profinet_stack/p-net/samples/pn_dev/app_gsdml.c b/profinet_stack/p-net/samples/pn_dev/app_gsdml.c new file mode 100644 index 0000000..e0ac13e --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_gsdml.c @@ -0,0 +1,260 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" +#include "app_utils.h" +#include "app_gsdml.h" +#include "app_log.h" +#include "osal.h" +#include "pnal.h" +#include + +#include +#include +#include + +/******************* Supported modules ***************************/ + +static const app_gsdml_module_t dap_1 = { + .id = PNET_MOD_DAP_IDENT, + .name = "DAP 1", + .submodules = { + PNET_SUBMOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_2_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_3_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_4_IDENT, + 0}}; + +static const app_gsdml_module_t module_digital_in = { + .id = APP_GSDML_MOD_ID_8_0_DIGITAL_IN, + .name = "DI 8xLogicLevel", + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN, 0}, +}; + +static const app_gsdml_module_t module_digital_out = { + .id = APP_GSDML_MOD_ID_0_8_DIGITAL_OUT, + .name = "DO 8xLogicLevel", + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_OUT, 0}}; + +static const app_gsdml_module_t module_digital_in_out = { + .id = APP_GSDML_MOD_ID_8_8_DIGITAL_IN_OUT, + .name = "DIO 8xLogicLevel", + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN_OUT, 0}}; + +static const app_gsdml_module_t module_echo = { + .id = APP_GSDML_MOD_ID_ECHO, + .name = "Echo module", + .submodules = {APP_GSDML_SUBMOD_ID_ECHO, 0}}; + +/******************* Supported submodules ************************/ + +static const app_gsdml_submodule_t dap_indentity_1 = { + .name = "DAP Identity 1", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_interface_1 = { + .name = "DAP Interface 1", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_1 = { + .name = "DAP Port 1", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_2 = { + .name = "DAP Port 2", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_2_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_3 = { + .name = "DAP Port 3", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_3_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_4 = { + .name = "DAP Port 4", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_4_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submod_digital_in = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN, + .name = "Digital Input", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_INPUT, + .insize = APP_GSDML_INPUT_DATA_DIGITAL_SIZE, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submod_digital_out = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT, + .name = "Digital Output", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_OUTPUT, + .insize = 0, + .outsize = APP_GSDML_OUTPUT_DATA_DIGITAL_SIZE, + .parameters = {0}}; + +static const app_gsdml_submodule_t submod_digital_inout = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_OUT, + .name = "Digital Input/Output", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_IO, + .insize = APP_GSDML_INPUT_DATA_DIGITAL_SIZE, + .outsize = APP_GSDML_OUTPUT_DATA_DIGITAL_SIZE, + .parameters = {APP_GSDML_PARAMETER_1_IDX, APP_GSDML_PARAMETER_2_IDX, 0}}; + +static const app_gsdml_submodule_t submod_echo = { + .id = APP_GSDML_SUBMOD_ID_ECHO, + .name = "Echo submodule", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_IO, + .insize = APP_GSDML_INPUT_DATA_ECHO_SIZE, + .outsize = APP_GSDML_OUTPUT_DATA_ECHO_SIZE, + .parameters = {APP_GSDML_PARAMETER_ECHO_IDX, 0}}; + +/** List of supported modules */ +static const app_gsdml_module_t * app_gsdml_modules[] = { + &dap_1, + &module_digital_in, + &module_digital_out, + &module_digital_in_out, + &module_echo}; + +/** List of supported submodules */ +static const app_gsdml_submodule_t * app_gsdml_submodules[] = { + &dap_indentity_1, + &dap_interface_1, + &dap_port_1, + &dap_port_2, + &dap_port_3, + &dap_port_4, + + &submod_digital_in, + &submod_digital_out, + &submod_digital_inout, + + &submod_echo, +}; + +/* List of supported parameters. + * Note that parameters are submodule attributes. + * This list contain all parameters while each + * submodule list its supported parameters using + * their indexes. + */ +static app_gsdml_param_t app_gsdml_parameters[] = { + { + .index = APP_GSDML_PARAMETER_1_IDX, + .name = "Demo 1", + .length = APP_GSDML_PARAMETER_LENGTH, + }, + { + .index = APP_GSDML_PARAMETER_2_IDX, + .name = "Demo 2", + .length = APP_GSDML_PARAMETER_LENGTH, + }, + { + .index = APP_GSDML_PARAMETER_ECHO_IDX, + .name = "Echo gain setting", + .length = APP_GSDML_PARAMETER_LENGTH, + }}; + +const app_gsdml_module_t * app_gsdml_get_module_cfg (uint32_t id) +{ + uint32_t i; + for (i = 0; i < NELEMENTS (app_gsdml_modules); i++) + { + if (app_gsdml_modules[i]->id == id) + { + return app_gsdml_modules[i]; + } + } + return NULL; +} + +const app_gsdml_submodule_t * app_gsdml_get_submodule_cfg (uint32_t id) +{ + uint32_t i; + for (i = 0; i < NELEMENTS (app_gsdml_submodules); i++) + { + if (app_gsdml_submodules[i]->id == id) + { + return app_gsdml_submodules[i]; + } + } + return NULL; +} + +const app_gsdml_param_t * app_gsdml_get_parameter_cfg ( + uint32_t submodule_id, + uint32_t index) +{ + uint16_t i; + uint16_t j; + + const app_gsdml_submodule_t * submodule_cfg = + app_gsdml_get_submodule_cfg (submodule_id); + + if (submodule_cfg == NULL) + { + /* Unsupported submodule id */ + return NULL; + } + + /* Search for parameter index in submodule configuration */ + for (i = 0; submodule_cfg->parameters[i] != 0; i++) + { + if (submodule_cfg->parameters[i] == index) + { + /* Find parameter configuration */ + for (j = 0; j < NELEMENTS (app_gsdml_parameters); j++) + { + if (app_gsdml_parameters[j].index == index) + { + return &app_gsdml_parameters[j]; + } + } + } + } + + return NULL; +} diff --git a/profinet_stack/p-net/samples/pn_dev/app_gsdml.h b/profinet_stack/p-net/samples/pn_dev/app_gsdml.h new file mode 100644 index 0000000..648bf88 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_gsdml.h @@ -0,0 +1,184 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_GSDML_H +#define APP_GSDML_H + +/** + * @file + * @brief Device properties defined by the GSDML device definition + * + * Functions for getting module, submodule and parameter + * configurations using their ids. + * + * Important: + * Any change in this file may require an update of the GSDML file. + * Note that when the GSDML file is updated it has to be reloaded + * in your Profinet engineering tool. PLC applications may be affected. + * + * Design requires unique submodule IDs and unique parameter indexes. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define APP_GSDML_API 0 + +#define APP_GSDML_DEFAULT_STATION_NAME "rt-labs-dev" + +/* GSDML tag: VendorID */ +#define APP_GSDML_VENDOR_ID 0x0493 + +/* GSDML tag: DeviceID */ +#define APP_GSDML_DEVICE_ID 0x0002 + +/* Used in DCP communication */ +#define APP_GSDML_OEM_VENDOR_ID 0xcafe +#define APP_GSDML_OEM_DEVICE_ID 0xee02 + +/* Used in I&M0 */ +#define APP_GSDML_IM_HARDWARE_REVISION 3 +#define APP_GSDML_IM_VERSION_MAJOR 1 +#define APP_GSDML_IM_VERSION_MINOR 2 + +/* Allowed: 'V', 'R', 'P', 'U', 'T' */ +#define APP_GSDML_SW_REV_PREFIX 'V' +#define APP_GSDML_PROFILE_ID 0x1234 +#define APP_GSDML_PROFILE_SPEC_TYPE 0x5678 +#define APP_GSDML_IM_REVISION_COUNTER 0 /* Typically 0 */ + +/* Note: You need to read out the actual hardware serial number instead */ +#define APP_GSDML_EXAMPLE_SERIAL_NUMBER "007" + +/* Initial values. Can be overwritten by PLC */ +#define APP_GSDML_TAG_FUNCTION "my function" +#define APP_GSDML_TAG_LOCATION "my location" +#define APP_GSDML_IM_DATE "2022-03-01 10:03" +#define APP_GSDML_DESCRIPTOR "my descriptor" +#define APP_GSDML_SIGNATURE "" + +/* GSDML tag: Writeable_IM_Records */ +#define APP_GSDML_IM_SUPPORTED \ + (PNET_SUPPORTED_IM1 | PNET_SUPPORTED_IM2 | PNET_SUPPORTED_IM3) + +/* GSDML tag: OrderNumber */ +#define APP_GSDML_ORDER_ID "12345 Abcdefghijk" + +/* GSDML tag: ModuleInfo / Name */ +#define APP_GSDML_PRODUCT_NAME "P-Net Sample Application" + +/* GSDML tag: MinDeviceInterval */ +#define APP_GSDML_MIN_DEVICE_INTERVAL 32 /* 1 ms */ + +#define APP_GSDML_DIAG_CUSTOM_USI 0x1234 + +/* See "Specification for GSDML" 8.26 LogBookEntryItem for allowed values */ +#define APP_GSDML_LOGBOOK_ERROR_CODE 0x20 /* Manufacturer specific */ +#define APP_GSDML_LOGBOOK_ERROR_DECODE 0x82 /* Manufacturer specific */ +#define APP_GSDML_LOGBOOK_ERROR_CODE_1 PNET_ERROR_CODE_1_FSPM +#define APP_GSDML_LOGBOOK_ERROR_CODE_2 0x00 /* Manufacturer specific */ +#define APP_GSDML_LOGBOOK_ENTRY_DETAIL 0xFEE1DEAD /* Manufacturer specific */ + +#define APP_GSDML_PARAMETER_1_IDX 123 +#define APP_GSDML_PARAMETER_2_IDX 124 +#define APP_GSDML_PARAMETER_ECHO_IDX 125 + +/* Use same size for all parameters in example */ +#define APP_GSDML_PARAMETER_LENGTH 4 + +#define APP_GSDML_DEFAULT_MAUTYPE 0x10 /* Copper 100 Mbit/s Full duplex */ + +typedef struct app_gsdml_module +{ + uint32_t id; + + /** Module name */ + const char * name; + + /** Submodule IDs. Variable length, ends with 0. */ + uint32_t submodules[]; +} app_gsdml_module_t; + +typedef struct app_gsdml_submodule +{ + uint32_t id; + + /** Submodule name */ + const char * name; + + uint32_t api; + pnet_submodule_dir_t data_dir; + uint16_t insize; + uint16_t outsize; + + /** Parameter indexes. See app_gsdml_parameters. + * Variable length, ends with 0. */ + uint16_t parameters[]; +} app_gsdml_submodule_t; + +typedef struct +{ + uint32_t index; + const char * name; + uint16_t length; +} app_gsdml_param_t; + +#define APP_GSDML_MOD_ID_8_0_DIGITAL_IN 0x00000030 +#define APP_GSDML_MOD_ID_0_8_DIGITAL_OUT 0x00000031 +#define APP_GSDML_MOD_ID_8_8_DIGITAL_IN_OUT 0x00000032 +#define APP_GSDML_MOD_ID_ECHO 0x00000040 +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN 0x00000130 +#define APP_GSDML_SUBMOD_ID_DIGITAL_OUT 0x00000131 +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN_OUT 0x00000132 +#define APP_GSDML_SUBMOD_ID_ECHO 0x00000140 +#define APP_GSDML_INPUT_DATA_DIGITAL_SIZE 1 /* bytes */ +#define APP_GSDML_OUTPUT_DATA_DIGITAL_SIZE 1 /* bytes */ +#define APP_GSDML_INPUT_DATA_ECHO_SIZE 8 /* bytes */ +#define APP_GSDML_OUTPUT_DATA_ECHO_SIZE APP_GSDML_INPUT_DATA_ECHO_SIZE +#define APP_GSDML_ALARM_PAYLOAD_SIZE 1 /* bytes */ + +/** + * Get module configuration from module ID + * @param module_id In: Module ID + * @return Module configuration, NULL if not found + */ +const app_gsdml_module_t * app_gsdml_get_module_cfg (uint32_t module_id); + +/** + * Get submodule module configuration from submodule ID + * @param submodule_id In: Submodule ID + * @return Submodule configuration, NULL if not found + */ +const app_gsdml_submodule_t * app_gsdml_get_submodule_cfg ( + uint32_t submodule_id); + +/** + * Get parameter configuration from parameter index + * @param submodule_id In: Submodule ID + * @param index In: Parameters index + * @return Parameter configuration, NULL if not found + */ +const app_gsdml_param_t * app_gsdml_get_parameter_cfg ( + uint32_t submodule_id, + uint32_t index); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_GSDML_H */ diff --git a/profinet_stack/p-net/samples/pn_dev/app_log.c b/profinet_stack/p-net/samples/pn_dev/app_log.c new file mode 100644 index 0000000..9b9d782 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_log.c @@ -0,0 +1,52 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "app_log.h" + +#include +#include + +static int32_t log_level = APP_DEFAULT_LOG_LEVEL; + +void app_log_set_log_level (int32_t level) +{ + log_level = level; +} + +void app_log (int32_t level, const char * fmt, ...) +{ + va_list list; + + if (level >= log_level) + { + va_start (list, fmt); + vprintf (fmt, list); + va_end (list); + fflush (stdout); + } +} + +void app_log_print_bytes (int32_t level, const uint8_t * bytes, uint32_t len) +{ + if (level >= log_level) + { + printf (" Bytes: "); + for (uint32_t i = 0; i < len; i++) + { + printf ("%02X ", bytes[i]); + } + printf ("\n"); + } +} diff --git a/profinet_stack/p-net/samples/pn_dev/app_log.h b/profinet_stack/p-net/samples/pn_dev/app_log.h new file mode 100644 index 0000000..259b070 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_log.h @@ -0,0 +1,75 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_LOG_H +#define APP_LOG_H + +/** + * @file + * @brief Application debug log utility + * + * Runtime configurable debug log using printf() + * Levels matches levels used in P-Net. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define APP_LOG_LEVEL_DEBUG 0x00 +#define APP_LOG_LEVEL_INFO 0x01 +#define APP_LOG_LEVEL_WARNING 0x02 +#define APP_LOG_LEVEL_ERROR 0x03 +#define APP_LOG_LEVEL_FATAL 0x04 + +#define APP_DEFAULT_LOG_LEVEL APP_LOG_LEVEL_FATAL + +#define APP_LOG(level, ...) app_log (level, __VA_ARGS__) + +#define APP_LOG_DEBUG(...) APP_LOG (APP_LOG_LEVEL_DEBUG, __VA_ARGS__) +#define APP_LOG_INFO(...) APP_LOG (APP_LOG_LEVEL_INFO, __VA_ARGS__) +#define APP_LOG_WARNING(...) APP_LOG (APP_LOG_LEVEL_WARNING, __VA_ARGS__) +#define APP_LOG_ERROR(...) APP_LOG (APP_LOG_LEVEL_ERROR, __VA_ARGS__) +#define APP_LOG_FATAL(...) APP_LOG (APP_LOG_LEVEL_FATAL, __VA_ARGS__) + +/** + * Print log message depending on level + * Use the APP_LOG_xxxxx macros instead of this function. + * @param level In: Message log level + * @param fmt In: Log message format string + */ +void app_log (int32_t level, const char * fmt, ...); + +/** + * Log an array of bytes + * @param level In: Log level + * @param bytes In: Array of bytes + * @param length In: Length of array + */ +void app_log_print_bytes (int32_t level, const uint8_t * bytes, uint32_t length); + +/** + * Set log level + * @param level In: Log level + */ +void app_log_set_log_level (int32_t level); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_LOG_H */ diff --git a/profinet_stack/p-net/samples/pn_dev/app_utils.c b/profinet_stack/p-net/samples/pn_dev/app_utils.c new file mode 100644 index 0000000..e9b2477 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_utils.c @@ -0,0 +1,741 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#define _GNU_SOURCE /* For asprintf() */ + +#include "app_utils.h" +#include "app_log.h" +#include "app_gsdml.h" +#include "sampleapp_common.h" +#include "osal.h" +#include "osal_log.h" /* For LOG_LEVEL */ +#include "pnal.h" +#include + +#include +#include +#include +#include + +#define GET_HIGH_BYTE(id) ((id >> 8) & 0xFF) +#define GET_LOW_BYTE(id) (id & 0xFF) + +void app_utils_ip_to_string (pnal_ipaddr_t ip, char * outputstring) +{ + snprintf ( + outputstring, + PNAL_INET_ADDRSTR_SIZE, + "%u.%u.%u.%u", + (uint8_t)((ip >> 24) & 0xFF), + (uint8_t)((ip >> 16) & 0xFF), + (uint8_t)((ip >> 8) & 0xFF), + (uint8_t)(ip & 0xFF)); +} + +void app_utils_mac_to_string (pnet_ethaddr_t mac, char * outputstring) +{ + snprintf ( + outputstring, + PNAL_ETH_ADDRSTR_SIZE, + "%02X:%02X:%02X:%02X:%02X:%02X", + mac.addr[0], + mac.addr[1], + mac.addr[2], + mac.addr[3], + mac.addr[4], + mac.addr[5]); +} + +const char * app_utils_submod_dir_to_string (pnet_submodule_dir_t direction) +{ + const char * s = ""; + + switch (direction) + { + case PNET_DIR_NO_IO: + s = "NO_IO"; + break; + case PNET_DIR_INPUT: + s = "INPUT"; + break; + case PNET_DIR_OUTPUT: + s = "OUTPUT"; + break; + case PNET_DIR_IO: + s = "INPUT_OUTPUT"; + break; + } + + return s; +} + +const char * app_utils_ioxs_to_string (pnet_ioxs_values_t ioxs) +{ + const char * s = ""; + switch (ioxs) + { + case PNET_IOXS_BAD: + s = "IOXS_BAD"; + break; + case PNET_IOXS_GOOD: + s = "IOXS_GOOD"; + break; + } + + return s; +} + +void app_utils_get_error_code_strings ( + uint16_t err_cls, + uint16_t err_code, + const char ** err_cls_str, + const char ** err_code_str) +{ + if (err_cls_str == NULL || err_cls_str == NULL) + { + return; + } + *err_cls_str = "Not decoded"; + *err_code_str = "Not decoded"; + + switch (err_cls) + { + case PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL: + *err_cls_str = "Real-Time Acyclic Protocol"; + switch (err_code) + { + case PNET_ERROR_CODE_2_ABORT_AR_CONSUMER_DHT_EXPIRED: + *err_code_str = "Device missed cyclic data " + "deadline, device terminated AR"; + break; + case PNET_ERROR_CODE_2_ABORT_AR_CMI_TIMEOUT: + *err_code_str = "Communication initialization " + "timeout, device terminated AR"; + break; + case PNET_ERROR_CODE_2_ABORT_AR_RELEASE_IND_RECEIVED: + *err_code_str = "AR release indication received"; + break; + case PNET_ERROR_CODE_2_ABORT_DCP_STATION_NAME_CHANGED: + *err_code_str = "DCP station name changed, " + "device terminated AR"; + break; + case PNET_ERROR_CODE_2_ABORT_DCP_RESET_TO_FACTORY: + *err_code_str = "DCP reset to factory or factory " + "reset, device terminated AR"; + break; + } + break; + + case PNET_ERROR_CODE_1_CTLDINA: + *err_cls_str = "CTLDINA = Name and IP assignment from controller"; + switch (err_code) + { + case PNET_ERROR_CODE_2_CTLDINA_ARP_MULTIPLE_IP_ADDRESSES: + *err_code_str = "Multiple users of same IP address"; + break; + } + break; + } +} + +void app_utils_copy_ip_to_struct ( + pnet_cfg_ip_addr_t * destination_struct, + pnal_ipaddr_t ip) +{ + destination_struct->a = ((ip >> 24) & 0xFF); + destination_struct->b = ((ip >> 16) & 0xFF); + destination_struct->c = ((ip >> 8) & 0xFF); + destination_struct->d = (ip & 0xFF); +} + +const char * app_utils_dcontrol_cmd_to_string ( + pnet_control_command_t control_command) +{ + const char * s = NULL; + + switch (control_command) + { + case PNET_CONTROL_COMMAND_PRM_BEGIN: + s = "PRM_BEGIN"; + break; + case PNET_CONTROL_COMMAND_PRM_END: + s = "PRM_END"; + break; + case PNET_CONTROL_COMMAND_APP_RDY: + s = "APP_RDY"; + break; + case PNET_CONTROL_COMMAND_RELEASE: + s = "RELEASE"; + break; + default: + s = ""; + break; + } + + return s; +} + +const char * app_utils_event_to_string (pnet_event_values_t event) +{ + const char * s = ""; + + switch (event) + { + case PNET_EVENT_ABORT: + s = "PNET_EVENT_ABORT"; + break; + case PNET_EVENT_STARTUP: + s = "PNET_EVENT_STARTUP"; + break; + case PNET_EVENT_PRMEND: + s = "PNET_EVENT_PRMEND"; + break; + case PNET_EVENT_APPLRDY: + s = "PNET_EVENT_APPLRDY"; + break; + case PNET_EVENT_DATA: + s = "PNET_EVENT_DATA"; + break; + } + + return s; +} + +int app_utils_pnet_cfg_init_default (pnet_cfg_t * cfg) +{ + memset (cfg, 0, sizeof (pnet_cfg_t)); + + cfg->tick_us = APP_TICK_INTERVAL_US; + + /* Identification & Maintenance */ + + cfg->im_0_data.im_vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_VENDOR_ID); + cfg->im_0_data.im_vendor_id_lo = GET_LOW_BYTE (APP_GSDML_VENDOR_ID); + + cfg->im_0_data.im_hardware_revision = APP_GSDML_IM_HARDWARE_REVISION; + cfg->im_0_data.im_sw_revision_prefix = APP_GSDML_SW_REV_PREFIX; + cfg->im_0_data.im_sw_revision_functional_enhancement = PNET_VERSION_MAJOR; + cfg->im_0_data.im_sw_revision_bug_fix = PNET_VERSION_MINOR; + cfg->im_0_data.im_sw_revision_internal_change = PNET_VERSION_PATCH; + cfg->im_0_data.im_revision_counter = APP_GSDML_IM_REVISION_COUNTER; + cfg->im_0_data.im_profile_id = APP_GSDML_PROFILE_ID; + cfg->im_0_data.im_profile_specific_type = APP_GSDML_PROFILE_SPEC_TYPE; + cfg->im_0_data.im_version_major = 1; /** Always 1 */ + cfg->im_0_data.im_version_minor = 1; /** Always 1 */ + cfg->im_0_data.im_supported = APP_GSDML_IM_SUPPORTED; + + snprintf ( + cfg->im_0_data.im_order_id, + sizeof (cfg->im_0_data.im_order_id), + "%s", + APP_GSDML_ORDER_ID); + snprintf ( + cfg->im_0_data.im_serial_number, + sizeof (cfg->im_0_data.im_serial_number), + "%s", + APP_GSDML_EXAMPLE_SERIAL_NUMBER); + snprintf ( + cfg->im_1_data.im_tag_function, + sizeof (cfg->im_1_data.im_tag_function), + "%s", + APP_GSDML_TAG_FUNCTION); + snprintf ( + cfg->im_1_data.im_tag_location, + sizeof (cfg->im_1_data.im_tag_location), + "%s", + APP_GSDML_TAG_LOCATION); + snprintf ( + cfg->im_2_data.im_date, + sizeof (cfg->im_2_data.im_date), + "%s", + APP_GSDML_IM_DATE); + snprintf ( + cfg->im_3_data.im_descriptor, + sizeof (cfg->im_3_data.im_descriptor), + "%s", + APP_GSDML_DESCRIPTOR); + snprintf ( + cfg->im_4_data.im_signature, + sizeof (cfg->im_4_data.im_signature), + "%s", + APP_GSDML_SIGNATURE); + + /* Device configuration */ + cfg->device_id.vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_VENDOR_ID); + cfg->device_id.vendor_id_lo = GET_LOW_BYTE (APP_GSDML_VENDOR_ID); + cfg->device_id.device_id_hi = GET_HIGH_BYTE (APP_GSDML_DEVICE_ID); + cfg->device_id.device_id_lo = GET_LOW_BYTE (APP_GSDML_DEVICE_ID); + cfg->oem_device_id.vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_OEM_VENDOR_ID); + cfg->oem_device_id.vendor_id_lo = GET_LOW_BYTE (APP_GSDML_OEM_VENDOR_ID); + cfg->oem_device_id.device_id_hi = GET_HIGH_BYTE (APP_GSDML_OEM_DEVICE_ID); + cfg->oem_device_id.device_id_lo = GET_LOW_BYTE (APP_GSDML_OEM_DEVICE_ID); + + snprintf ( + cfg->product_name, + sizeof (cfg->product_name), + "%s", + APP_GSDML_PRODUCT_NAME); + + cfg->send_hello = true; + + /* Timing */ + cfg->min_device_interval = APP_GSDML_MIN_DEVICE_INTERVAL; + + /* Should be set by application as part of network configuration. */ + cfg->num_physical_ports = 1; + + snprintf ( + cfg->station_name, + sizeof (cfg->station_name), + "%s", + APP_GSDML_DEFAULT_STATION_NAME); + + /* Diagnosis mechanism */ + /* We prefer using "Extended channel diagnosis" instead of + * "Qualified channel diagnosis" format on the wire, + * as this is better supported by Wireshark. + */ + cfg->use_qualified_diagnosis = false; + + return 0; +} + +int app_utils_get_netif_namelist ( + const char * arg_str, + uint16_t max_port, + app_utils_netif_namelist_t * p_if_list, + uint16_t * p_num_ports) +{ + int ret = 0; + uint16_t i = 0; + uint16_t j = 0; + uint16_t if_index = 0; + uint16_t number_of_given_names = 1; + uint16_t if_list_size = max_port + 1; + char c; + + if (max_port == 0) + { + printf ("Error: max_port is 0.\n"); + return -1; + } + + memset (p_if_list, 0, sizeof (*p_if_list)); + c = arg_str[i++]; + while (c != '\0') + { + if (c != ',') + { + if (if_index < if_list_size) + { + p_if_list->netif[if_index].name[j++] = c; + } + } + else + { + if (if_index < if_list_size) + { + p_if_list->netif[if_index].name[j++] = '\0'; + j = 0; + if_index++; + } + number_of_given_names++; + } + + c = arg_str[i++]; + } + + if (max_port == 1 && number_of_given_names > 1) + { + printf ("Error: Only 1 network interface expected as max_port is 1.\n"); + return -1; + } + if (number_of_given_names == 2) + { + printf ("Error: It is illegal to give 2 interface names. Use 1, or one " + "more than the number of physical interfaces.\n"); + return -1; + } + if (number_of_given_names > max_port + 1) + { + printf ( + "Error: You have given %u interface names, but max is %u as " + "PNET_MAX_PHYSICAL_PORTS is %u.\n", + number_of_given_names, + max_port + 1, + max_port); + return -1; + } + + if (number_of_given_names == 1) + { + if (strlen (p_if_list->netif[0].name) == 0) + { + printf ("Error: Zero length network interface name.\n"); + return -1; + } + else + { + p_if_list->netif[1] = p_if_list->netif[0]; + *p_num_ports = 1; + } + } + else + { + for (i = 0; i < number_of_given_names; i++) + { + if (strlen (p_if_list->netif[i].name) == 0) + { + printf ("Error: Zero length network interface name (%d).\n", i); + return -1; + } + } + + *p_num_ports = number_of_given_names - 1; + } + + return ret; +} + +int app_utils_pnet_cfg_init_netifs ( + const char * netif_list_str, + app_utils_netif_namelist_t * if_list, + uint16_t * number_of_ports, + pnet_if_cfg_t * if_cfg) +{ + int ret = 0; + int i = 0; + pnal_ipaddr_t ip; + pnal_ipaddr_t netmask; + pnal_ipaddr_t gateway; + + ret = app_utils_get_netif_namelist ( + netif_list_str, + PNET_MAX_PHYSICAL_PORTS, + if_list, + number_of_ports); + if (ret != 0) + { + return ret; + } + if_cfg->main_netif_name = if_list->netif[0].name; + + for (i = 1; i <= *number_of_ports; i++) + { + if_cfg->physical_ports[i - 1].netif_name = if_list->netif[i].name; + if_cfg->physical_ports[i - 1].default_mau_type = + APP_GSDML_DEFAULT_MAUTYPE; + } + + /* Read IP, netmask, gateway from operating system */ + ip = pnal_get_ip_address (if_cfg->main_netif_name); + netmask = pnal_get_netmask (if_cfg->main_netif_name); + gateway = pnal_get_gateway (if_cfg->main_netif_name); + + app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_addr, ip); + app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_gateway, gateway); + app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_mask, netmask); + + return ret; +} + +static void app_utils_print_mac_address (const char * netif_name) +{ + pnal_ethaddr_t pnal_mac_addr; + if (pnal_get_macaddress (netif_name, &pnal_mac_addr) == 0) + { + APP_LOG_INFO ( + "%02X:%02X:%02X:%02X:%02X:%02X\n", + pnal_mac_addr.addr[0], + pnal_mac_addr.addr[1], + pnal_mac_addr.addr[2], + pnal_mac_addr.addr[3], + pnal_mac_addr.addr[4], + pnal_mac_addr.addr[5]); + } + else + { + APP_LOG_ERROR ("Failed read mac address\n"); + } +} + +void app_utils_print_network_config ( + pnet_if_cfg_t * if_cfg, + uint16_t number_of_ports) +{ + uint16_t i; + char hostname_string[PNAL_HOSTNAME_MAX_SIZE]; /* Terminated string */ + + APP_LOG_INFO ("Management port: %s ", if_cfg->main_netif_name); + app_utils_print_mac_address (if_cfg->main_netif_name); + for (i = 1; i <= number_of_ports; i++) + { + APP_LOG_INFO ( + "Physical port [%u]: %s ", + i, + if_cfg->physical_ports[i - 1].netif_name); + + app_utils_print_mac_address (if_cfg->physical_ports[i - 1].netif_name); + } + + if (pnal_get_hostname (hostname_string) != 0) + { + hostname_string[0] = '\0'; + } + + APP_LOG_INFO ("Hostname: %s\n", hostname_string); + APP_LOG_INFO ( + "IP address: %u.%u.%u.%u\n", + if_cfg->ip_cfg.ip_addr.a, + if_cfg->ip_cfg.ip_addr.b, + if_cfg->ip_cfg.ip_addr.c, + if_cfg->ip_cfg.ip_addr.d); + APP_LOG_INFO ( + "Netmask: %u.%u.%u.%u\n", + if_cfg->ip_cfg.ip_mask.a, + if_cfg->ip_cfg.ip_mask.b, + if_cfg->ip_cfg.ip_mask.c, + if_cfg->ip_cfg.ip_mask.d); + APP_LOG_INFO ( + "Gateway: %u.%u.%u.%u\n", + if_cfg->ip_cfg.ip_gateway.a, + if_cfg->ip_cfg.ip_gateway.b, + if_cfg->ip_cfg.ip_gateway.c, + if_cfg->ip_cfg.ip_gateway.d); +} + +void app_utils_print_ioxs_change ( + const app_subslot_t * subslot, + const char * ioxs_str, + uint8_t iocs_current, + uint8_t iocs_new) +{ + if (iocs_current != iocs_new) + { + if (iocs_new == PNET_IOXS_BAD) + { + APP_LOG_DEBUG ( + "PLC reports %s BAD for slot %u subslot %u \"%s\"\n", + ioxs_str, + subslot->slot_nbr, + subslot->subslot_nbr, + subslot->submodule_name); + } + else if (iocs_new == PNET_IOXS_GOOD) + { + APP_LOG_DEBUG ( + "PLC reports %s GOOD for slot %u subslot %u \"%s\".\n", + ioxs_str, + subslot->slot_nbr, + subslot->subslot_nbr, + subslot->submodule_name); + } + else if (iocs_new != PNET_IOXS_GOOD) + { + APP_LOG_DEBUG ( + "PLC reports %s %u for input slot %u subslot %u \"%s\".\n" + " Is the PLC in STOP mode?\n", + ioxs_str, + iocs_new, + subslot->slot_nbr, + subslot->subslot_nbr, + subslot->submodule_name); + } + } +} + +int app_utils_plug_module ( + app_api_t * p_api, + uint16_t slot_nbr, + uint32_t id, + const char * name) +{ + if (slot_nbr >= PNET_MAX_SLOTS) + { + return -1; + } + + p_api->slots[slot_nbr].module_id = id; + p_api->slots[slot_nbr].plugged = true; + p_api->slots[slot_nbr].name = name; + + return 0; +} + +int app_utils_pull_module (app_api_t * p_api, uint16_t slot_nbr) +{ + if (slot_nbr >= PNET_MAX_SLOTS) + { + return -1; + } + + p_api->slots[slot_nbr].plugged = false; + + return 0; +} + +app_subslot_t * app_utils_plug_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_ident, + const pnet_data_cfg_t * p_data_cfg, + const char * submodule_name, + app_utils_cyclic_callback cyclic_callback, + void * tag) +{ + uint16_t subslot_ix; + + if (slot_nbr >= PNET_MAX_SLOTS || p_api == NULL || p_data_cfg == NULL) + { + return NULL; + } + + /** Find a free subslot */ + for (subslot_ix = 0; subslot_ix < PNET_MAX_SUBSLOTS; subslot_ix++) + { + if (p_api->slots[slot_nbr].subslots[subslot_ix].used == false) + { + app_subslot_t * p_subslot = + &p_api->slots[slot_nbr].subslots[subslot_ix]; + + p_subslot->used = true; + p_subslot->plugged = true; + p_subslot->slot_nbr = slot_nbr; + p_subslot->subslot_nbr = subslot_nbr; + p_subslot->submodule_name = submodule_name; + p_subslot->submodule_id = submodule_ident; + p_subslot->data_cfg = *p_data_cfg; + p_subslot->cyclic_callback = cyclic_callback; + p_subslot->tag = tag; + p_subslot->indata_iocs = PNET_IOXS_BAD; + p_subslot->outdata_iops = PNET_IOXS_BAD; + return p_subslot; + } + } + + return NULL; +} + +int app_utils_pull_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr) +{ + app_subslot_t * p_subslot = NULL; + + if (slot_nbr >= PNET_MAX_SUBSLOTS || p_api == NULL) + { + return -1; + } + + p_subslot = app_utils_subslot_get (p_api, slot_nbr, subslot_nbr); + if (p_subslot == NULL) + { + return -1; + } + + memset (p_subslot, 0, sizeof (app_subslot_t)); + p_subslot->used = false; + + return 0; +} + +app_subslot_t * app_utils_subslot_get ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr) +{ + uint16_t subslot_ix; + + if (slot_nbr >= PNET_MAX_SLOTS || p_api == NULL) + { + return NULL; + } + + for (subslot_ix = 0; subslot_ix < PNET_MAX_SUBSLOTS; subslot_ix++) + { + if (p_api->slots[slot_nbr].subslots[subslot_ix].subslot_nbr == subslot_nbr) + { + return &p_api->slots[slot_nbr].subslots[subslot_ix]; + } + } + + return NULL; +} + +bool app_utils_subslot_is_input (const app_subslot_t * p_subslot) +{ + if (p_subslot == NULL || p_subslot->used == false) + { + return false; + } + + if ( + p_subslot->data_cfg.data_dir == PNET_DIR_INPUT || + p_subslot->data_cfg.data_dir == PNET_DIR_IO) + { + return true; + } + + return false; +} + +bool app_utils_subslot_is_no_io (const app_subslot_t * p_subslot) +{ + if (p_subslot == NULL || p_subslot->used == false) + { + return false; + } + + return p_subslot->data_cfg.data_dir == PNET_DIR_NO_IO; +} + +bool app_utils_subslot_is_output (const app_subslot_t * p_subslot) +{ + if (p_subslot == NULL || p_subslot->used == false) + { + return false; + } + + if ( + p_subslot->data_cfg.data_dir == PNET_DIR_OUTPUT || + p_subslot->data_cfg.data_dir == PNET_DIR_IO) + { + return true; + } + + return false; +} + +void app_utils_cyclic_data_poll (app_api_t * p_api) +{ + uint16_t slot_nbr; + uint16_t subslot_index; + app_subslot_t * p_subslot; + + for (slot_nbr = 0; slot_nbr < PNET_MAX_SLOTS; slot_nbr++) + { + for (subslot_index = 0; subslot_index < PNET_MAX_SUBSLOTS; + subslot_index++) + { + p_subslot = &p_api->slots[slot_nbr].subslots[subslot_index]; + if (p_subslot->plugged && p_subslot->cyclic_callback != NULL) + { + p_subslot->cyclic_callback (p_subslot, p_subslot->tag); + } + } + } +} diff --git a/profinet_stack/p-net/samples/pn_dev/app_utils.h b/profinet_stack/p-net/samples/pn_dev/app_utils.h new file mode 100644 index 0000000..64894f0 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/app_utils.h @@ -0,0 +1,428 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_UTILS_H +#define APP_UTILS_H + +/** + * @file + * @brief Application utilities and helper functions + * + * Functions for getting string representation of + * P-Net events, error codes and more. + * + * API, slot and subslot administration. + * + * Initialization of P-Net configuration from app_gsdml.h. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "osal.h" +#include "pnal.h" +#include + +typedef struct app_utils_netif_name +{ + char name[PNET_INTERFACE_NAME_MAX_SIZE]; +} app_utils_netif_name_t; + +typedef struct app_utils_netif_namelist +{ + app_utils_netif_name_t netif[PNET_MAX_PHYSICAL_PORTS + 1]; +} app_utils_netif_namelist_t; + +/* Forward declaration */ +typedef struct app_subslot app_subslot_t; + +/** + * Callback for updated cyclic data + * + * @param subslot InOut: Subslot structure + * @param tag InOut: Typically a handle to a submodule + */ +typedef void (*app_utils_cyclic_callback) (app_subslot_t * subslot, void * tag); + +/** + * Information of submodule plugged into a subslot. + * + * Note that submodule data is not stored here but must + * be handled by the submodule implementation. + * + * All parameters are initialized by the app_utils_plug_submodule() + * function. + * + * The cyclic_callback is used when app_utils_cyclic_data_poll() + * is called. Typically on the tick event in the main task. + * The \a tag parameter is passed with the cyclic_callback and + * is typically a handle to a submodule on application. + */ +typedef struct app_subslot +{ + /** True when the position in the subslot array is occupied */ + bool used; + + /** True when the subslot is plugged */ + bool plugged; + + uint16_t slot_nbr; + uint16_t subslot_nbr; + uint32_t submodule_id; + const char * submodule_name; + pnet_data_cfg_t data_cfg; + + /** Status indicator from PLC */ + uint8_t indata_iocs; + + /** Status indicator from PLC */ + uint8_t outdata_iops; + + /** Callback for cyclic input- or output data, or NULL if not implemented */ + app_utils_cyclic_callback cyclic_callback; + void * tag; +} app_subslot_t; + +/** + * Information of module plugged into a slot, + * and array of subslots for admin of submodules. + */ +typedef struct app_slot +{ + bool plugged; + uint32_t module_id; + const char * name; /** Module name */ + + /** Subslots. Use a separate index, as the subslot number might be large. + * For example the subslot for DAP port 1 has number 0x8001 */ + app_subslot_t subslots[PNET_MAX_SUBSLOTS]; +} app_slot_t; + +/** + * Profinet API state for application + * + * Used to manage plugged modules into slots (and submodules into subslots). + */ +typedef struct app_api_t +{ + uint32_t api_id; + uint32_t arep; + + /** Slots. Use slot number as index */ + app_slot_t slots[PNET_MAX_SLOTS]; +} app_api_t; + +/** + * Convert IP address to string + * @param ip In: IP address + * @param outputstring Out: Resulting string buffer. Should have size + * PNAL_INET_ADDRSTR_SIZE. + */ +void app_utils_ip_to_string (pnal_ipaddr_t ip, char * outputstring); + +/** + * Get string description of data direction + * @param direction In: Submodule data direction + * @return String represention of data direction + */ +const char * app_utils_submod_dir_to_string (pnet_submodule_dir_t direction); + +/** + * Get string description of PNIO producer or consumer status + * @param ioxs In: Producer or consumer status (IOPS/IOCS) + * @return String represention of ioxs (IOPS/IOCS) + */ +const char * app_utils_ioxs_to_string (pnet_ioxs_values_t ioxs); + +/** + * Convert MAC address to string + * @param mac In: MAC address + * @param outputstring Out: Resulting string buffer. Should have size + * PNAL_ETH_ADDRSTR_SIZE. + */ +void app_utils_mac_to_string (pnet_ethaddr_t mac, char * outputstring); + +/** + * Convert error code to string format + * Only common error codes supported. + * Todo: Add rest of error codes. + * + * @param err_cls In: The error class. See PNET_ERROR_CODE_1_* + * @param err_code In: The error code. See PNET_ERROR_CODE_2_* + * @param err_cls_str Out: The error class string + * @param err_code_str Out: The error code string + */ +void app_utils_get_error_code_strings ( + uint16_t err_cls, + uint16_t err_code, + const char ** err_cls_str, + const char ** err_code_str); + +/** + * Copy an IP address (as an integer) to a struct + * @param destination_struct Out: Destination + * @param ip In: IP address + */ +void app_utils_copy_ip_to_struct ( + pnet_cfg_ip_addr_t * destination_struct, + pnal_ipaddr_t ip); + +/** + * Return a string representation of + * the given dcontrol command. + * @param event In: control_command + * @return A string representing the command + */ +const char * app_utils_dcontrol_cmd_to_string ( + pnet_control_command_t control_command); + +/** + * Return a string representation of the given event. + * @param event In: event + * @return A string representing the event + */ +const char * app_utils_event_to_string (pnet_event_values_t event); + +/** + * Update network configuration from a string + * defining a list of network interfaces examples: + * "eth0" or "br0,eth0,eth1" + * + * Read IP, netmask etc from operating system. + * + * @param netif_list_str In: Comma separated string of network ifs + * @param if_list Out: Array of network ifs + * @param number_of_ports Out: Number of ports + * @param if_cfg Out: P-Net network configuration to be updated + * @return 0 on success, -1 on error + */ +int app_utils_pnet_cfg_init_netifs ( + const char * netif_list_str, + app_utils_netif_namelist_t * if_list, + uint16_t * number_of_ports, + pnet_if_cfg_t * if_cfg); + +/** + * Parse a comma separated list of network interfaces and check + * that the number of interfaces match the PNET_MAX_PHYSICAL_PORTS + * configuration. + * + * For a single Ethernet interface, the \a arg_str should consist of + * one name. For two Ethernet interfaces, the \a arg_str should consist of + * three names, as we also need a bridge interface. + * + * Does only consider the number of comma separated names. No check of the + * names themselves are done. + * + * Examples: + * arg_str num_ports + * "eth0" 1 + * "eth0,eth1" error (We need a bridge as well) + * "br0,eth0,eth1" 2 + * + * @param arg_str In: Network interface list as comma separated, + * terminated string. For example "eth0" or + * "br0,eth0,eth1". + * @param max_port In: PNET_MAX_PHYSICAL_PORTS, passed as argument to + * allow test. + * @param p_if_list Out: List of network interfaces + * @param p_num_ports Out: Resulting number of physical ports + * @return 0 on success + * -1 on error + */ +int app_utils_get_netif_namelist ( + const char * arg_str, + uint16_t max_port, + app_utils_netif_namelist_t * p_if_list, + uint16_t * p_num_ports); + +/** + * Print network configuration using APP_LOG_INFO(). + * + * @param if_cfg In: Network configuration + * @param number_of_ports In: Number of used ports + */ +void app_utils_print_network_config ( + pnet_if_cfg_t * if_cfg, + uint16_t number_of_ports); + +/** + * Print message if IOXS has changed. + * + * Uses APP_LOG_INFO() + * + * @param subslot In: Subslot + * @param ioxs_str In: String description Producer or Consumer + * @param ioxs_current In: Current status + * @param ioxs_new In: New status + */ +void app_utils_print_ioxs_change ( + const app_subslot_t * subslot, + const char * ioxs_str, + uint8_t ioxs_current, + uint8_t ioxs_new); + +/** + * Init the p-net configuration to default values. + * + * Most values are picked from app_gsdml.h + * + * Network configuration not initialized. + * This means that \a '.if_cfg' must be set by application. + * + * Use this function to init P-Net configuration before + * before passing config to app_init(). + * + * @param pnet_cfg Out: Configuration for use by p-net + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int app_utils_pnet_cfg_init_default (pnet_cfg_t * pnet_cfg); + +/** + * Plug application module + * + * This is for the application to remember which slots are + * populated in the p-net stack. + * + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @param id In: Module identity + * @param name In: Module name + * @return 0 on success, -1 on error + */ +int app_utils_plug_module ( + app_api_t * p_api, + uint16_t slot_nbr, + uint32_t id, + const char * name); + +/** + * Pull any application module in given slot. + * + * This is for the application to remember which slots are + * populated in the p-net stack. + * + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @return 0 on success, -1 on error + */ +int app_utils_pull_module (app_api_t * p_api, uint16_t slot_nbr); + +/** + * Plug application submodule. + * + * This is for the application to remember which subslots are + * populated in the p-net stack. + * + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @param submodule_id In: Submodule identity + * @param p_data_cfg In: Data configuration, + * direction, in and out sizes + * @param submodule_name In: Submodule name + * @param cyclic_callback In: Submodule data callback + * @param tag In: Tag passed in cyclic callback + * Typically application or + * submodule handle + * @return Reference to allocated subslot, + * NULL if no free subslot is available. This should + * never happen if application is aligned with p-net state. + */ +app_subslot_t * app_utils_plug_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + const pnet_data_cfg_t * p_data_cfg, + const char * submodule_name, + app_utils_cyclic_callback cyclic_callback, + void * tag); + +/** + * Unplug any application submodule from given subslot. + * + * This is for the application to remember which subslots are + * populated in the p-net stack. + * + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @return 0 on success, -1 on error. + */ +int app_utils_pull_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr); + +/** + * Trigger data callback for all plugged submodules in all slots. + * + * The callbacks given in \a app_utils_plug_submodule() are used. + * + * @param p_api In: API + */ +void app_utils_cyclic_data_poll (app_api_t * p_api); + +/** + * Get subslot application information. + * + * @param p_appdata InOut: Application state. + * @param slot_nbr In: Slot number. + * @param subslot_nbr In: Subslot number. Range 0 - 0x9FFF. + * @return Reference to application subslot, + * NULL if subslot is not found/plugged. + */ +app_subslot_t * app_utils_subslot_get ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr); + +/** + * Return true if subslot is input. + * + * @param p_subslot In: Reference to subslot. + * @return true if subslot is input or input/output. + * false if not. + */ +bool app_utils_subslot_is_input (const app_subslot_t * p_subslot); + +/** + * Return true if subslot is neither input or output. + * + * This is applies for DAP submodules/slots + * + * @param p_subslot In: Reference to subslot. + * @return true if subslot is input or input/output. + * false if not. + */ +bool app_utils_subslot_is_no_io (const app_subslot_t * p_subslot); + +/** + * Return true if subslot is output. + * + * @param p_subslot In: Reference to subslot. + * @return true if subslot is output or input/output, + * false if not. + */ +bool app_utils_subslot_is_output (const app_subslot_t * p_subslot); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_UTILS_H */ diff --git a/profinet_stack/p-net/samples/pn_dev/sampleapp_common.c b/profinet_stack/p-net/samples/pn_dev/sampleapp_common.c new file mode 100644 index 0000000..ef45a8d --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/sampleapp_common.c @@ -0,0 +1,1622 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" + +#include "app_utils.h" +#include "app_gsdml.h" +#include "app_data.h" +#include "app_log.h" +#include "osal.h" +#include "pnal.h" +#include + +#include +#include +#include + +/* Events handled by main task */ +#define APP_EVENT_READY_FOR_DATA BIT (0) +#define APP_EVENT_TIMER BIT (1) +#define APP_EVENT_ALARM BIT (2) +#define APP_EVENT_ABORT BIT (15) + +/* Defines used for alarm demo functionality */ +#define CHANNEL_ERRORTYPE_SHORT_CIRCUIT 0x0001 +#define CHANNEL_ERRORTYPE_LINE_BREAK 0x0006 +#define CHANNEL_ERRORTYPE_DATA_TRANSMISSION_IMPOSSIBLE 0x8000 +#define CHANNEL_ERRORTYPE_NETWORK_COMPONENT_FUNCTION_MISMATCH 0x8008 +#define EXTENDED_CHANNEL_ERRORTYPE_FRAME_DROPPED 0x8000 +#define EXTENDED_CHANNEL_ERRORTYPE_MAUTYPE_MISMATCH 0x8001 +#define EXTENDED_CHANNEL_ERRORTYPE_LINE_DELAY_MISMATCH 0x8002 + +#define APP_ALARM_USI 0x0010 +#define APP_DIAG_CHANNEL_NUMBER 4 +#define APP_DIAG_CHANNEL_DIRECTION PNET_DIAG_CH_PROP_DIR_INPUT +#define APP_DIAG_CHANNEL_NUMBER_OF_BITS PNET_DIAG_CH_PROP_TYPE_1_BIT +#define APP_DIAG_CHANNEL_SEVERITY PNET_DIAG_CH_PROP_MAINT_FAULT +#define APP_DIAG_CHANNEL_ERRORTYPE CHANNEL_ERRORTYPE_SHORT_CIRCUIT +#define APP_DIAG_CHANNEL_ADDVALUE_A 0 +#define APP_DIAG_CHANNEL_ADDVALUE_B 1234 +#define APP_DIAG_CHANNEL_EXTENDED_ERRORTYPE 0 +#define APP_DIAG_CHANNEL_QUAL_SEVERITY 0 /* Not used (Max one bit set) */ + +typedef enum app_demo_state +{ + APP_DEMO_STATE_ALARM_SEND = 0, + APP_DEMO_STATE_LOGBOOK_ENTRY, + APP_DEMO_STATE_ABORT_AR, + APP_DEMO_STATE_CYCLIC_REDUNDANT, + APP_DEMO_STATE_CYCLIC_NORMAL, + APP_DEMO_STATE_DIAG_STD_ADD, + APP_DEMO_STATE_DIAG_STD_UPDATE, + APP_DEMO_STATE_DIAG_STD_REMOVE, + APP_DEMO_STATE_DIAG_USI_ADD, + APP_DEMO_STATE_DIAG_USI_UPDATE, + APP_DEMO_STATE_DIAG_USI_REMOVE, +} app_demo_state_t; + +typedef struct app_data_t +{ + pnet_t * net; + + /* P-Net configuration passed in app_init(). */ + const pnet_cfg_t * pnet_cfg; + + /* Application API for administration of plugged + * (sub)modules and connection state. */ + app_api_t main_api; + + os_timer_t * main_timer; + os_event_t * main_events; + + bool alarm_allowed; + pnet_alarm_argument_t alarm_arg; + app_demo_state_t alarm_demo_state; + uint8_t alarm_payload[APP_GSDML_ALARM_PAYLOAD_SIZE]; + + uint32_t arep_for_appl_ready; + + bool button1_pressed; + bool button2_pressed; + bool button2_pressed_previous; + + /* Counters used to control when buttons are checked + * and process data is updated */ + uint32_t buttons_tick_counter; + uint32_t process_data_tick_counter; + +} app_data_t; + +/* Forward declarations */ +static void app_plug_dap (app_data_t * app, uint16_t number_of_ports); +static int app_set_initial_data_and_ioxs (app_data_t * app); +static void app_cyclic_data_callback (app_subslot_t * subslot, void * tag); + +/** Static app data */ +static app_data_t app_state; + +pnet_t * app_get_pnet_instance (app_data_t * app) +{ + if (app == NULL) + { + return NULL; + } + + return app->net; +} + +/** Check if we are connected to the controller + * + * @param app InOut: Application handle + * @return true if we are connected to the IO-controller + */ +static bool app_is_connected_to_controller (app_data_t * app) +{ + return app->main_api.arep != UINT32_MAX; +} + +app_data_t * app_init (const pnet_cfg_t * pnet_cfg, const app_args_t * app_args) +{ + APP_LOG_INFO ("Init P-Net stack and sample application\n"); + + app_data_t * app = &app_state; + + app->alarm_allowed = true; + app->main_api.arep = UINT32_MAX; + app->pnet_cfg = pnet_cfg; + + app->net = pnet_init (app->pnet_cfg); + + if (app->net == NULL) + { + return NULL; + } + + return app; +} + +/** + * Callback for timer tick. + * + * This is a callback for the timer defined in OSAL. + * See \a os_timer_create() for details. + * + * @param timer InOut: Timer instance + * @param arg InOut: User defined argument, app_data_t pointer + */ +static void main_timer_tick (os_timer_t * timer, void * arg) +{ + app_data_t * app = (app_data_t *)arg; + + os_event_set (app->main_events, APP_EVENT_TIMER); +} + +int app_start (app_data_t * app, app_run_in_separate_task_t task_config) +{ + APP_LOG_INFO ("Start sample application main loop\n"); + if (app == NULL) + { + return -1; + } + + app->main_events = os_event_create(); + if (app->main_events == NULL) + { + return -1; + } + + app->main_timer = os_timer_create ( + APP_TICK_INTERVAL_US, + main_timer_tick, + (void *)app, + false); + + if (app->main_timer == NULL) + { + os_event_destroy (app->main_events); + return -1; + } + + if (task_config == RUN_IN_SEPARATE_THREAD) + { + os_thread_create ( + "p-net_sample_app", + APP_MAIN_THREAD_PRIORITY, + APP_MAIN_THREAD_STACKSIZE, + app_loop_forever, + (void *)app); + } + + os_timer_start (app->main_timer); + + return 0; +} + +/** + * Set outputs to default value + */ +static void app_set_outputs_default_value (void) +{ + APP_LOG_DEBUG ("Setting outputs to default values.\n"); + app_data_set_default_outputs(); +} + +/*********************************** Callbacks ********************************/ + +static int app_connect_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ("PLC connect indication. AREP: %u\n", arep); + /* + * Handle the request on an application level. + * This is a very simple application which does not need to handle anything. + * All the needed information is in the AR data structure. + */ + + return 0; +} + +static int app_release_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ("PLC release (disconnect) indication. AREP: %u\n", arep); + + app_set_outputs_default_value(); + + return 0; +} + +static int app_dcontrol_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_control_command_t control_command, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ( + "PLC dcontrol message (The PLC is done with parameter writing). " + "AREP: %u Command: %s\n", + arep, + app_utils_dcontrol_cmd_to_string (control_command)); + + return 0; +} + +static int app_ccontrol_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ( + "PLC ccontrol message confirmation (The PLC has received our Application " + "Ready message). AREP: %u Status codes: %d %d %d %d\n", + arep, + p_result->pnio_status.error_code, + p_result->pnio_status.error_decode, + p_result->pnio_status.error_code_1, + p_result->pnio_status.error_code_2); + + return 0; +} + +static int app_write_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t idx, + uint16_t sequence_number, + uint16_t write_length, + const uint8_t * p_write_data, + pnet_result_t * p_result) +{ + int result = 0; + app_data_t * app = (app_data_t *)arg; + app_subslot_t * subslot; + APP_LOG_DEBUG ( + "PLC write record indication.\n" + " AREP: %u API: %u Slot: %2u Subslot: %u Index: %u Sequence: %2u " + "Length: %u\n", + arep, + api, + slot_nbr, + subslot_nbr, + (unsigned)idx, + sequence_number, + write_length); + + subslot = app_utils_subslot_get (&app->main_api, slot_nbr, subslot_nbr); + if (subslot == NULL) + { + APP_LOG_WARNING ( + "No submodule plugged in AREP: %u API: %u Slot: %2u Subslot: %u " + "Index will not be written.\n", + arep, + api, + slot_nbr, + subslot_nbr); + p_result->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_APP_WRITE_ERROR; + p_result->pnio_status.error_code_2 = 0; /* User specific */ + + return -1; + } + + result = app_data_write_parameter ( + slot_nbr, + subslot_nbr, + subslot->submodule_id, + idx, + p_write_data, + write_length); + if (result != 0) + { + APP_LOG_WARNING ( + "Failed to write index for AREP: %u API: %u Slot: %2u Subslot: %u " + "index %u.\n", + arep, + api, + slot_nbr, + subslot_nbr, + idx); + p_result->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_APP_WRITE_ERROR; + p_result->pnio_status.error_code_2 = 0; /* User specific */ + } + + return result; +} + +static int app_read_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t idx, + uint16_t sequence_number, + uint8_t ** pp_read_data, + uint16_t * p_read_length, + pnet_result_t * p_result) +{ + int result = 0; + app_data_t * app = (app_data_t *)arg; + app_subslot_t * subslot; + + APP_LOG_DEBUG ( + "PLC read record indication.\n" + " AREP: %u API: %u Slot: %2u Subslot: %u Index: %u Sequence: %2u Max " + "length: %u\n", + arep, + api, + slot_nbr, + subslot_nbr, + (unsigned)idx, + sequence_number, + (unsigned)*p_read_length); + + subslot = app_utils_subslot_get (&app->main_api, slot_nbr, subslot_nbr); + if (subslot == NULL) + { + APP_LOG_WARNING ( + "No submodule plugged in AREP: %u API: %u Slot: %2u Subslot: %u " + "Index will not be read.\n", + arep, + api, + slot_nbr, + subslot_nbr); + p_result->pnio_status.error_code = PNET_ERROR_CODE_READ; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_APP_READ_ERROR; + p_result->pnio_status.error_code_2 = 0; /* User specific */ + return -1; + } + + result = app_data_read_parameter ( + slot_nbr, + subslot_nbr, + subslot->submodule_id, + idx, + pp_read_data, + p_read_length); + + if (result != 0) + { + APP_LOG_WARNING ( + "Failed to read index for AREP: %u API: %u Slot: %2u Subslot: %u " + "index %u.\n", + arep, + api, + slot_nbr, + subslot_nbr, + idx); + p_result->pnio_status.error_code = PNET_ERROR_CODE_READ; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_APP_READ_ERROR; + p_result->pnio_status.error_code_2 = 0; /* User specific */ + } + + return result; +} + +static int app_state_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_event_values_t event) +{ + uint16_t err_cls = 0; /* Error code 1 */ + uint16_t err_code = 0; /* Error code 2 */ + const char * error_class_description = ""; + const char * error_code_description = ""; + + app_data_t * app = (app_data_t *)arg; + + APP_LOG_DEBUG ( + "Event indication %s AREP: %u\n", + app_utils_event_to_string (event), + arep); + + if (event == PNET_EVENT_ABORT) + { + if (pnet_get_ar_error_codes (net, arep, &err_cls, &err_code) == 0) + { + app_utils_get_error_code_strings ( + err_cls, + err_code, + &error_class_description, + &error_code_description); + APP_LOG_DEBUG ( + " Error class: 0x%02x %s \n" + " Error code: 0x%02x %s \n", + (unsigned)err_cls, + error_class_description, + (unsigned)err_code, + error_code_description); + } + else + { + APP_LOG_DEBUG (" No error status available\n"); + } + /* Set output values */ + app_set_outputs_default_value(); + + /* Only abort AR with correct session key */ + os_event_set (app->main_events, APP_EVENT_ABORT); + } + else if (event == PNET_EVENT_PRMEND) + { + if (app_is_connected_to_controller (app)) + { + APP_LOG_WARNING ("Warning - AREP out of sync\n"); + } + app->main_api.arep = arep; + app_set_initial_data_and_ioxs (app); + + (void)pnet_set_provider_state (net, true); + + /* Send application ready at next tick + Do not call pnet_application_ready() here as it will affect + the internal stack states */ + app->arep_for_appl_ready = arep; + os_event_set (app->main_events, APP_EVENT_READY_FOR_DATA); + } + else if (event == PNET_EVENT_DATA) + { + APP_LOG_DEBUG ("Cyclic data transmission started\n\n"); + } + + return 0; +} + +static int app_reset_ind ( + pnet_t * net, + void * arg, + bool should_reset_application, + uint16_t reset_mode) +{ + APP_LOG_DEBUG ( + "PLC reset indication. Application reset mandatory: %u Reset mode: %d\n", + should_reset_application, + reset_mode); + + return 0; +} + +static int app_signal_led_ind (pnet_t * net, void * arg, bool led_state) +{ + APP_LOG_INFO ("Profinet signal LED indication. New state: %u\n", led_state); + + app_set_led (APP_PROFINET_SIGNAL_LED_ID, led_state); + return 0; +} + +static int app_exp_module_ind ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint32_t module_ident) +{ + int ret = -1; + int result = 0; + app_data_t * app = (app_data_t *)arg; + const char * module_name = "unknown"; + const app_gsdml_module_t * module_config; + + APP_LOG_DEBUG ("Module plug indication\n"); + + if (slot >= PNET_MAX_SLOTS) + { + APP_LOG_ERROR ( + "Wrong slot number received: %u It should be less than %u\n", + slot, + PNET_MAX_SLOTS); + return -1; + } + + module_config = app_gsdml_get_module_cfg (module_ident); + if (module_config == NULL) + { + APP_LOG_ERROR (" Module ID %08x not found.\n", (unsigned)module_ident); + /* + * Needed to pass Behavior scenario 2 + */ + APP_LOG_DEBUG (" Plug expected module anyway\n"); + } + else + { + module_name = module_config->name; + } + + APP_LOG_DEBUG (" Pull old module. API: %u Slot: %2u\n", api, slot); + result = pnet_pull_module (net, api, slot); + + if (result == 0) + { + (void)app_utils_pull_module (&app->main_api, slot); + } + + APP_LOG_DEBUG ( + " Plug module. API: %u Slot: %2u Module ID: 0x%x \"%s\"\n", + api, + slot, + (unsigned)module_ident, + module_name); + + ret = pnet_plug_module (net, api, slot, module_ident); + if (ret == 0) + { + (void)app_utils_plug_module ( + &app->main_api, + slot, + module_ident, + module_name); + } + else + { + APP_LOG_ERROR ( + "Plug module failed. Ret: %u API: %u Slot: %2u Module ID: 0x%x\n", + ret, + api, + slot, + (unsigned)module_ident); + } + + return ret; +} + +static int app_exp_submodule_ind ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_id, + uint32_t submodule_id, + const pnet_data_cfg_t * p_exp_data) +{ + int ret = -1; + int result = 0; + pnet_data_cfg_t data_cfg = {0}; + app_data_t * app = (app_data_t *)arg; + const app_gsdml_submodule_t * submod_cfg; + const char * name = "Unsupported"; + app_utils_cyclic_callback cyclic_data_callback = NULL; + + APP_LOG_DEBUG ("Submodule plug indication.\n"); + + submod_cfg = app_gsdml_get_submodule_cfg (submodule_id); + if (submod_cfg != NULL) + { + data_cfg.data_dir = submod_cfg->data_dir; + data_cfg.insize = submod_cfg->insize; + data_cfg.outsize = submod_cfg->outsize; + name = submod_cfg->name; + + if (data_cfg.insize > 0 || data_cfg.outsize > 0) + { + cyclic_data_callback = app_cyclic_data_callback; + } + } + else + { + APP_LOG_WARNING ( + " Submodule ID 0x%x in module ID 0x%x not found. API: %u Slot: %2u " + "Subslot %u \n", + (unsigned)submodule_id, + (unsigned)module_id, + api, + slot, + subslot); + + /* + * Needed for behavior scenario 2 to pass. + * Iops will be set to bad for this subslot + */ + APP_LOG_WARNING (" Plug expected submodule anyway \n"); + + data_cfg.data_dir = p_exp_data->data_dir; + data_cfg.insize = p_exp_data->insize; + data_cfg.outsize = p_exp_data->outsize; + } + + APP_LOG_DEBUG ( + " Pull old submodule. API: %u Slot: %2u Subslot: %u\n", + api, + slot, + subslot); + + result = pnet_pull_submodule (net, api, slot, subslot); + if (result == 0) + { + (void)app_utils_pull_submodule (&app->main_api, slot, subslot); + } + + APP_LOG_DEBUG ( + " Plug submodule. API: %u Slot: %2u Module ID: 0x%-4x\n" + " Subslot: %u Submodule ID: 0x%x \"%s\"\n", + api, + slot, + (unsigned)module_id, + subslot, + (unsigned)submodule_id, + name); + + APP_LOG_DEBUG ( + " Data Dir: %s In: %u bytes Out: %u bytes\n", + app_utils_submod_dir_to_string (data_cfg.data_dir), + data_cfg.insize, + data_cfg.outsize); + + if ( + data_cfg.data_dir != p_exp_data->data_dir || + data_cfg.insize != p_exp_data->insize || + data_cfg.outsize != p_exp_data->outsize) + { + APP_LOG_WARNING ( + " Warning expected Data Dir: %s In: %u bytes Out: %u bytes\n", + app_utils_submod_dir_to_string (p_exp_data->data_dir), + p_exp_data->insize, + p_exp_data->outsize); + } + ret = pnet_plug_submodule ( + net, + api, + slot, + subslot, + module_id, + submodule_id, + data_cfg.data_dir, + data_cfg.insize, + data_cfg.outsize); + + if (ret == 0) + { + (void)app_utils_plug_submodule ( + &app->main_api, + slot, + subslot, + submodule_id, + &data_cfg, + name, + cyclic_data_callback, + app); + } + else + { + APP_LOG_ERROR ( + " Plug submodule failed. Ret: %u API: %u Slot: %2u Subslot %u " + "Module ID: 0x%x Submodule ID: 0x%x \n", + ret, + api, + slot, + subslot, + (unsigned)module_id, + (unsigned)submodule_id); + } + + return ret; +} + +static int app_new_data_status_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t crep, + uint8_t changes, + uint8_t data_status) +{ + bool is_running = data_status & BIT (PNET_DATA_STATUS_BIT_PROVIDER_STATE); + bool is_valid = data_status & BIT (PNET_DATA_STATUS_BIT_DATA_VALID); + + APP_LOG_DEBUG ( + "Data status indication. AREP: %u Data status changes: 0x%02x " + "Data status: 0x%02x\n", + arep, + changes, + data_status); + APP_LOG_DEBUG ( + " %s, %s, %s, %s, %s\n", + is_running ? "Run" : "Stop", + is_valid ? "Valid" : "Invalid", + (data_status & BIT (PNET_DATA_STATUS_BIT_STATE)) ? "Primary" : "Backup", + (data_status & BIT (PNET_DATA_STATUS_BIT_STATION_PROBLEM_INDICATOR)) + ? "Normal operation" + : "Problem", + (data_status & BIT (PNET_DATA_STATUS_BIT_IGNORE)) + ? "Ignore data status" + : "Evaluate data status"); + + if (is_running == false || is_valid == false) + { + app_set_outputs_default_value(); + } + + return 0; +} + +static int app_alarm_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_arg, + uint16_t data_len, + uint16_t data_usi, + const uint8_t * p_data) +{ + app_data_t * app = (app_data_t *)arg; + + APP_LOG_DEBUG ( + "Alarm indication. AREP: %u API: %d Slot: %d Subslot: %d " + "Type: %d Seq: %d Length: %d USI: %d\n", + arep, + p_alarm_arg->api_id, + p_alarm_arg->slot_nbr, + p_alarm_arg->subslot_nbr, + p_alarm_arg->alarm_type, + p_alarm_arg->sequence_number, + data_len, + data_usi); + + app->alarm_arg = *p_alarm_arg; + os_event_set (app->main_events, APP_EVENT_ALARM); + + return 0; +} + +static int app_alarm_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status) +{ + app_data_t * app = (app_data_t *)arg; + + APP_LOG_DEBUG ( + "PLC alarm confirmation. AREP: %u Status code %u, " + "%u, %u, %u\n", + arep, + p_pnio_status->error_code, + p_pnio_status->error_decode, + p_pnio_status->error_code_1, + p_pnio_status->error_code_2); + + app->alarm_allowed = true; + + return 0; +} + +static int app_alarm_ack_cnf (pnet_t * net, void * arg, uint32_t arep, int res) +{ + APP_LOG_DEBUG ( + "PLC alarm ACK confirmation. AREP: %u Result: " + "%d\n", + arep, + res); + + return 0; +} + +/******************************************************************************/ + +/** + * Plug all DAP (sub)modules + * Use existing callback functions to plug the (sub-)modules + * @param app InOut: Application handle + * @param number_of_ports In: Number of active ports + */ +static void app_plug_dap (app_data_t * app, uint16_t number_of_ports) +{ + const pnet_data_cfg_t cfg_dap_data = { + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + }; + + APP_LOG_DEBUG ("\nPlug DAP module and its submodules\n"); + + app_exp_module_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_MOD_DAP_IDENT); + + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_IDENT, + &cfg_dap_data); + + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_IDENT, + &cfg_dap_data); + + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_1_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT, + &cfg_dap_data); + + if (number_of_ports >= 2) + { + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_2_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_2_IDENT, + &cfg_dap_data); + } + + if (number_of_ports >= 3) + { + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_3_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_3_IDENT, + &cfg_dap_data); + } + + if (number_of_ports >= 4) + { + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_4_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_4_IDENT, + &cfg_dap_data); + } + + APP_LOG_DEBUG ("Done plugging DAP\n\n"); +} + +/** + * Send application ready to the PLC + * @param net InOut: p-net stack instance + * @param arep In: Arep + */ +static void app_handle_send_application_ready (pnet_t * net, uint32_t arep) +{ + int ret = -1; + + APP_LOG_DEBUG ( + "Application will signal that it is ready for data, for " + "AREP %u.\n", + arep); + + ret = pnet_application_ready (net, arep); + if (ret != 0) + { + APP_LOG_ERROR ( + "Error returned when application telling that it is ready for " + "data. Have you set IOCS or IOPS for all subslots?\n"); + } + + /* When the PLC sends a confirmation to this message, the + pnet_ccontrol_cnf() callback will be triggered. */ +} + +/** + * Send alarm ACK to the PLC + * + * @param net InOut: p-net stack instance + * @param arep In: Arep + * @param p_alarm_arg In: Alarm argument (slot, subslot etc) + */ +static void app_handle_send_alarm_ack ( + pnet_t * net, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_arg) +{ + pnet_pnio_status_t pnio_status = {0, 0, 0, 0}; + int ret; + + ret = pnet_alarm_send_ack (net, arep, p_alarm_arg, &pnio_status); + if (ret != 0) + { + APP_LOG_DEBUG ("Error when sending alarm ACK. Error: %d\n", ret); + } + else + { + APP_LOG_DEBUG ("Alarm ACK sent\n"); + } +} + +/** + * Handle cyclic input- and output data for a subslot. + * + * Data is read and written using functions in the .c file, + * which handles the data and update the physical input and outputs. + * + * @param subslot InOut: Subslot reference + * @param tag In: Application handle, here \a app_data_t pointer + */ +static void app_cyclic_data_callback (app_subslot_t * subslot, void * tag) +{ + app_data_t * app = (app_data_t *)tag; + uint8_t indata_iops = PNET_IOXS_BAD; + uint8_t indata_iocs = PNET_IOXS_BAD; + uint8_t * indata; + uint16_t indata_size = 0; + bool outdata_updated; + uint16_t outdata_length; + uint8_t outdata_iops; + uint8_t outdata_buf[20]; /* Todo: Remove temporary buffer */ + + if (app == NULL) + { + APP_LOG_ERROR ("Application tag not set in subslot?\n"); + return; + } + + if (subslot->slot_nbr != PNET_SLOT_DAP_IDENT && subslot->data_cfg.outsize > 0) + { + outdata_length = subslot->data_cfg.outsize; + CC_ASSERT (outdata_length < sizeof (outdata_buf)); + + /* Get output data from the PLC */ + (void)pnet_output_get_data_and_iops ( + app->net, + APP_GSDML_API, + subslot->slot_nbr, + subslot->subslot_nbr, + &outdata_updated, + outdata_buf, + &outdata_length, + &outdata_iops); + + app_utils_print_ioxs_change ( + subslot, + "Provider Status (IOPS)", + subslot->outdata_iops, + outdata_iops); + subslot->outdata_iops = outdata_iops; + + if (outdata_length != subslot->data_cfg.outsize) + { + APP_LOG_ERROR ("Wrong outputdata length: %u\n", outdata_length); + app_set_outputs_default_value(); + } + else if (outdata_iops == PNET_IOXS_GOOD) + { + /* Application specific handling of the output data to a submodule. + For the sample application, the data sets a LED. */ + (void)app_data_set_output_data ( + subslot->slot_nbr, + subslot->subslot_nbr, + subslot->submodule_id, + outdata_buf, + outdata_length); + } + else + { + app_set_outputs_default_value(); + } + } + + if (subslot->slot_nbr != PNET_SLOT_DAP_IDENT && subslot->data_cfg.insize > 0) + { + /* Get application specific input data from a submodule (not DAP) + * + * For the sample application, the data includes a button + * state and a counter value. */ + indata = app_data_get_input_data ( + subslot->slot_nbr, + subslot->subslot_nbr, + subslot->submodule_id, + app->button1_pressed, + &indata_size, + &indata_iops); + + /* Send input data to the PLC */ + (void)pnet_input_set_data_and_iops ( + app->net, + APP_GSDML_API, + subslot->slot_nbr, + subslot->subslot_nbr, + indata, + indata_size, + indata_iops); + + (void)pnet_input_get_iocs ( + app->net, + APP_GSDML_API, + subslot->slot_nbr, + subslot->subslot_nbr, + &indata_iocs); + + app_utils_print_ioxs_change ( + subslot, + "Consumer Status (IOCS)", + subslot->indata_iocs, + indata_iocs); + subslot->indata_iocs = indata_iocs; + } +} + +/** + * Set initial input data, provider and consumer status for a subslot. + * + * @param app In: Application handle + */ +static int app_set_initial_data_and_ioxs (app_data_t * app) +{ + int ret; + uint16_t slot; + uint16_t subslot_index; + const app_subslot_t * p_subslot; + uint8_t * indata; + uint16_t indata_size; + uint8_t indata_iops; + + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + for (subslot_index = 0; subslot_index < PNET_MAX_SUBSLOTS; + subslot_index++) + { + p_subslot = &app->main_api.slots[slot].subslots[subslot_index]; + if (p_subslot->plugged) + { + indata = NULL; + indata_size = 0; + indata_iops = PNET_IOXS_BAD; + + if ( + p_subslot->data_cfg.insize > 0 || + p_subslot->data_cfg.data_dir == PNET_DIR_NO_IO) + { + + /* Get input data for submodule + * + * For the sample application data includes + * includes button state and a counter value + */ + if ( + p_subslot->slot_nbr != PNET_SLOT_DAP_IDENT && + p_subslot->data_cfg.insize > 0) + { + indata = app_data_get_input_data ( + p_subslot->slot_nbr, + p_subslot->subslot_nbr, + p_subslot->submodule_id, + app->button1_pressed, + &indata_size, + &indata_iops); + } + else if (p_subslot->slot_nbr == PNET_SLOT_DAP_IDENT) + { + indata_iops = PNET_IOXS_GOOD; + } + + ret = pnet_input_set_data_and_iops ( + app->net, + app->main_api.api_id, + p_subslot->slot_nbr, + p_subslot->subslot_nbr, + indata, + indata_size, + indata_iops); + + /* + * If a submodule is still plugged but not used in current AR, + * setting the data and IOPS will fail. + * This is not a problem. + * Log message below will only be printed for active submodules. + */ + if (ret == 0) + { + APP_LOG_DEBUG ( + " Set initial input data and IOPS for slot %2u subslot " + "%5u %9s size %3d \"%s\" \n", + p_subslot->slot_nbr, + p_subslot->subslot_nbr, + app_utils_ioxs_to_string (indata_iops), + p_subslot->data_cfg.insize, + p_subslot->submodule_name); + } + } + + if (p_subslot->data_cfg.outsize > 0) + { + ret = pnet_output_set_iocs ( + app->net, + app->main_api.api_id, + p_subslot->slot_nbr, + p_subslot->subslot_nbr, + PNET_IOXS_GOOD); + + if (ret == 0) + { + APP_LOG_DEBUG ( + " Set initial output IOCS for slot %2u subslot " + "%5u %9s \"%s\"\n", + p_subslot->slot_nbr, + p_subslot->subslot_nbr, + app_utils_ioxs_to_string (PNET_IOXS_GOOD), + p_subslot->submodule_name); + } + } + } + } + } + return 0; +} + +/** + * Send and receive cyclic/process data for all subslots. + * + * Updates the data only on every APP_TICKS_UPDATE_DATA invocation + * + * @param app In: Application handle + */ +static void app_handle_cyclic_data (app_data_t * app) +{ + /* For the sample application cyclic data is updated + * with a period defined by APP_TICKS_UPDATE_DATA + */ + app->process_data_tick_counter++; + if (app->process_data_tick_counter < APP_TICKS_UPDATE_DATA) + { + return; + } + app->process_data_tick_counter = 0; + + app_utils_cyclic_data_poll (&app->main_api); +} + +/** + * Set alarm, diagnostics and logbook entries. + * + * Alternates between these functions each time the button2 is pressed: + * - pnet_alarm_send_process_alarm() + * - pnet_diag_std_add() + * - pnet_set_redundancy_state() + * - pnet_set_state() + * - pnet_diag_std_update() + * - pnet_diag_usi_add() + * - pnet_diag_usi_update() + * - pnet_diag_usi_remove() + * - pnet_diag_std_remove() + * - pnet_create_log_book_entry() + * - pnet_ar_abort() + * + * Uses first 8-bit digital input module, if available. + * + * @param app InOut: Application handle + */ +static void app_handle_demo_pnet_api (app_data_t * app) +{ + uint16_t slot = 0; + bool found_inputsubslot = false; + uint16_t subslot_ix = 0; + const app_subslot_t * p_subslot = NULL; + pnet_pnio_status_t pnio_status = {0}; + pnet_diag_source_t diag_source = { + .api = APP_GSDML_API, + .slot = 0, + .subslot = 0, + .ch = APP_DIAG_CHANNEL_NUMBER, + .ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL, + .ch_direction = APP_DIAG_CHANNEL_DIRECTION}; + + /* Loop though all subslots to find first digital 8-bit input subslot */ + while (!found_inputsubslot && (slot < PNET_MAX_SLOTS)) + { + for (subslot_ix = 0; + !found_inputsubslot && (subslot_ix < PNET_MAX_SUBSLOTS); + subslot_ix++) + { + p_subslot = &app->main_api.slots[slot].subslots[subslot_ix]; + if ( + app_utils_subslot_is_input (p_subslot) && + (p_subslot->submodule_id == APP_GSDML_SUBMOD_ID_DIGITAL_IN || + p_subslot->submodule_id == APP_GSDML_SUBMOD_ID_DIGITAL_IN_OUT)) + { + found_inputsubslot = true; + break; + } + } + if (!found_inputsubslot) + { + slot++; + } + } + if (!found_inputsubslot) + { + APP_LOG_DEBUG ("Did not find any input module in the slots. Skipping.\n"); + return; + } + + diag_source.slot = slot; + diag_source.subslot = p_subslot->subslot_nbr; + + switch (app->alarm_demo_state) + { + case APP_DEMO_STATE_ALARM_SEND: + if (app->alarm_allowed == true && app_is_connected_to_controller (app)) + { + app->alarm_payload[0]++; + APP_LOG_INFO ( + "Sending process alarm from slot %u subslot %u USI %u to " + "PLC. Payload: 0x%x\n", + slot, + p_subslot->subslot_nbr, + APP_ALARM_USI, + app->alarm_payload[0]); + pnet_alarm_send_process_alarm ( + app->net, + app->main_api.arep, + APP_GSDML_API, + slot, + p_subslot->subslot_nbr, + APP_ALARM_USI, + APP_GSDML_ALARM_PAYLOAD_SIZE, + app->alarm_payload); + app->alarm_allowed = false; /* Not allowed until ACK received */ + + /* todo handle return code on pnet_alarm_send_process_alarm */ + } + else + { + APP_LOG_WARNING ( + "Could not send process alarm, as alarm_allowed == false or " + "no connection available\n"); + } + break; + + case APP_DEMO_STATE_CYCLIC_REDUNDANT: + APP_LOG_INFO ( + "Setting cyclic data to backup and to redundant. See Wireshark.\n"); + if (pnet_set_primary_state (app->net, false) != 0) + { + APP_LOG_WARNING (" Could not set cyclic data state to backup.\n"); + } + if (pnet_set_redundancy_state (app->net, true) != 0) + { + APP_LOG_WARNING (" Could not set cyclic data state to reundant.\n"); + } + break; + + case APP_DEMO_STATE_CYCLIC_NORMAL: + APP_LOG_INFO ( + "Setting cyclic data back to primary and non-redundant. See " + "Wireshark.\n"); + if (pnet_set_primary_state (app->net, true) != 0) + { + APP_LOG_ERROR (" Could not set cyclic data state to primary.\n"); + } + if (pnet_set_redundancy_state (app->net, false) != 0) + { + APP_LOG_ERROR ( + " Could not set cyclic data state to non-reundant.\n"); + } + break; + + case APP_DEMO_STATE_DIAG_STD_ADD: + APP_LOG_INFO ( + "Adding standard diagnosis. Slot %u subslot %u channel %u Errortype " + "%u\n", + diag_source.slot, + diag_source.subslot, + diag_source.ch, + APP_DIAG_CHANNEL_ERRORTYPE); + (void)pnet_diag_std_add ( + app->net, + &diag_source, + APP_DIAG_CHANNEL_NUMBER_OF_BITS, + APP_DIAG_CHANNEL_SEVERITY, + APP_DIAG_CHANNEL_ERRORTYPE, + APP_DIAG_CHANNEL_EXTENDED_ERRORTYPE, + APP_DIAG_CHANNEL_ADDVALUE_A, + APP_DIAG_CHANNEL_QUAL_SEVERITY); + break; + + case APP_DEMO_STATE_DIAG_STD_UPDATE: + APP_LOG_INFO ( + "Updating standard diagnosis. Slot %u subslot %u channel %u\n", + diag_source.slot, + diag_source.subslot, + diag_source.ch); + pnet_diag_std_update ( + app->net, + &diag_source, + APP_DIAG_CHANNEL_ERRORTYPE, + APP_DIAG_CHANNEL_EXTENDED_ERRORTYPE, + APP_DIAG_CHANNEL_ADDVALUE_B); + break; + + case APP_DEMO_STATE_DIAG_STD_REMOVE: + APP_LOG_INFO ( + "Removing standard diagnosis. Slot %u subslot %u channel %u\n", + diag_source.slot, + diag_source.subslot, + diag_source.ch); + pnet_diag_std_remove ( + app->net, + &diag_source, + APP_DIAG_CHANNEL_ERRORTYPE, + APP_DIAG_CHANNEL_EXTENDED_ERRORTYPE); + break; + + case APP_DEMO_STATE_DIAG_USI_ADD: + APP_LOG_INFO ( + "Adding USI diagnosis. Slot %u subslot %u\n", + slot, + p_subslot->subslot_nbr); + pnet_diag_usi_add ( + app->net, + APP_GSDML_API, + slot, + p_subslot->subslot_nbr, + APP_GSDML_DIAG_CUSTOM_USI, + 11, + (uint8_t *)"diagdata_1"); + break; + + case APP_DEMO_STATE_DIAG_USI_UPDATE: + APP_LOG_INFO ( + "Updating USI diagnosis. Slot %u subslot %u\n", + slot, + p_subslot->subslot_nbr); + pnet_diag_usi_update ( + app->net, + APP_GSDML_API, + slot, + p_subslot->subslot_nbr, + APP_GSDML_DIAG_CUSTOM_USI, + 13, + (uint8_t *)"diagdata_123"); + break; + + case APP_DEMO_STATE_DIAG_USI_REMOVE: + APP_LOG_INFO ( + "Removing USI diagnosis. Slot %u subslot %u\n", + slot, + p_subslot->subslot_nbr); + pnet_diag_usi_remove ( + app->net, + APP_GSDML_API, + slot, + p_subslot->subslot_nbr, + APP_GSDML_DIAG_CUSTOM_USI); + break; + + case APP_DEMO_STATE_LOGBOOK_ENTRY: + if (app_is_connected_to_controller (app)) + { + APP_LOG_INFO ( + "Writing to logbook. Error_code1: %02X Error_code2: %02X Entry " + "detail: 0x%08X\n", + APP_GSDML_LOGBOOK_ERROR_CODE_1, + APP_GSDML_LOGBOOK_ERROR_CODE_2, + APP_GSDML_LOGBOOK_ENTRY_DETAIL); + pnio_status.error_code = APP_GSDML_LOGBOOK_ERROR_CODE; + pnio_status.error_decode = APP_GSDML_LOGBOOK_ERROR_DECODE; + pnio_status.error_code_1 = APP_GSDML_LOGBOOK_ERROR_CODE_1; + pnio_status.error_code_2 = APP_GSDML_LOGBOOK_ERROR_CODE_2; + pnet_create_log_book_entry ( + app->net, + app->main_api.arep, + &pnio_status, + APP_GSDML_LOGBOOK_ENTRY_DETAIL); + } + else + { + APP_LOG_WARNING ( + "Could not add logbook entry as no connection is available\n"); + } + break; + + case APP_DEMO_STATE_ABORT_AR: + if (app_is_connected_to_controller (app)) + { + APP_LOG_INFO ( + "Sample app will disconnect and reconnect. Executing " + "pnet_ar_abort() AREP: %u\n", + app->main_api.arep); + (void)pnet_ar_abort (app->net, app->main_api.arep); + } + else + { + APP_LOG_WARNING ( + "Could not execute pnet_ar_abort(), as no connection is " + "available\n"); + } + break; + } + + switch (app->alarm_demo_state) + { + case APP_DEMO_STATE_ALARM_SEND: + app->alarm_demo_state = APP_DEMO_STATE_CYCLIC_REDUNDANT; + break; + case APP_DEMO_STATE_CYCLIC_REDUNDANT: + app->alarm_demo_state = APP_DEMO_STATE_CYCLIC_NORMAL; + break; + case APP_DEMO_STATE_CYCLIC_NORMAL: + app->alarm_demo_state = APP_DEMO_STATE_DIAG_STD_ADD; + break; + case APP_DEMO_STATE_DIAG_STD_ADD: + app->alarm_demo_state = APP_DEMO_STATE_DIAG_STD_UPDATE; + break; + case APP_DEMO_STATE_DIAG_STD_UPDATE: + app->alarm_demo_state = APP_DEMO_STATE_DIAG_USI_ADD; + break; + case APP_DEMO_STATE_DIAG_USI_ADD: + app->alarm_demo_state = APP_DEMO_STATE_DIAG_USI_UPDATE; + break; + case APP_DEMO_STATE_DIAG_USI_UPDATE: + app->alarm_demo_state = APP_DEMO_STATE_DIAG_USI_REMOVE; + break; + case APP_DEMO_STATE_DIAG_USI_REMOVE: + app->alarm_demo_state = APP_DEMO_STATE_DIAG_STD_REMOVE; + break; + case APP_DEMO_STATE_DIAG_STD_REMOVE: + app->alarm_demo_state = APP_DEMO_STATE_LOGBOOK_ENTRY; + break; + case APP_DEMO_STATE_LOGBOOK_ENTRY: + app->alarm_demo_state = APP_DEMO_STATE_ABORT_AR; + break; + default: + case APP_DEMO_STATE_ABORT_AR: + app->alarm_demo_state = APP_DEMO_STATE_ALARM_SEND; + break; + } +} + +void app_pnet_cfg_init_default (pnet_cfg_t * pnet_cfg) +{ + app_utils_pnet_cfg_init_default (pnet_cfg); + + pnet_cfg->state_cb = app_state_ind; + pnet_cfg->connect_cb = app_connect_ind; + pnet_cfg->release_cb = app_release_ind; + pnet_cfg->dcontrol_cb = app_dcontrol_ind; + pnet_cfg->ccontrol_cb = app_ccontrol_cnf; + pnet_cfg->read_cb = app_read_ind; + pnet_cfg->write_cb = app_write_ind; + pnet_cfg->exp_module_cb = app_exp_module_ind; + pnet_cfg->exp_submodule_cb = app_exp_submodule_ind; + pnet_cfg->new_data_status_cb = app_new_data_status_ind; + pnet_cfg->alarm_ind_cb = app_alarm_ind; + pnet_cfg->alarm_cnf_cb = app_alarm_cnf; + pnet_cfg->alarm_ack_cnf_cb = app_alarm_ack_cnf; + pnet_cfg->reset_cb = app_reset_ind; + pnet_cfg->signal_led_cb = app_signal_led_ind; + + pnet_cfg->cb_arg = (void *)&app_state; +} + +/** + * Read button states from operating system + * + * Actual reading is done every APP_TICKS_READ_BUTTONS invocation + * + * @param app InOut: Application handle + */ +static void update_button_states (app_data_t * app) +{ + app->buttons_tick_counter++; + if (app->buttons_tick_counter > APP_TICKS_READ_BUTTONS) + { + app->button1_pressed = app_get_button (0); + app->button2_pressed = app_get_button (1); + app->buttons_tick_counter = 0; + } +} + +void app_loop_forever (void * arg) +{ + app_data_t * app = (app_data_t *)arg; + uint32_t mask = APP_EVENT_READY_FOR_DATA | APP_EVENT_TIMER | + APP_EVENT_ALARM | APP_EVENT_ABORT; + uint32_t flags = 0; + + app->main_api.arep = UINT32_MAX; + + app_set_led (APP_DATA_LED_ID, false); + app_plug_dap (app, app->pnet_cfg->num_physical_ports); + APP_LOG_INFO ("Waiting for PLC connect request\n\n"); + + /* Main event loop */ + for (;;) + { + os_event_wait (app->main_events, mask, &flags, OS_WAIT_FOREVER); + if (flags & APP_EVENT_READY_FOR_DATA) + { + os_event_clr (app->main_events, APP_EVENT_READY_FOR_DATA); + + app_handle_send_application_ready (app->net, app->arep_for_appl_ready); + } + else if (flags & APP_EVENT_ALARM) + { + os_event_clr (app->main_events, APP_EVENT_ALARM); + + app_handle_send_alarm_ack ( + app->net, + app->main_api.arep, + &app->alarm_arg); + } + else if (flags & APP_EVENT_TIMER) + { + os_event_clr (app->main_events, APP_EVENT_TIMER); + + update_button_states (app); + if (app_is_connected_to_controller (app)) + { + app_handle_cyclic_data (app); + } + + /* Run alarm demo function if button2 is pressed */ + if ( + (app->button2_pressed == true) && + (app->button2_pressed_previous == false)) + { + app_handle_demo_pnet_api (app); + } + app->button2_pressed_previous = app->button2_pressed; + + /* Run p-net stack */ + pnet_handle_periodic (app->net); + } + else if (flags & APP_EVENT_ABORT) + { + os_event_clr (app->main_events, APP_EVENT_ABORT); + + app->main_api.arep = UINT32_MAX; + app->alarm_allowed = true; + APP_LOG_DEBUG ("Connection closed\n"); + APP_LOG_DEBUG ("Waiting for PLC connect request\n\n"); + } + } +} diff --git a/profinet_stack/p-net/samples/pn_dev/sampleapp_common.h b/profinet_stack/p-net/samples/pn_dev/sampleapp_common.h new file mode 100644 index 0000000..a763e42 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev/sampleapp_common.h @@ -0,0 +1,156 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef SAMPLEAPP_COMMON_H +#define SAMPLEAPP_COMMON_H + +#include "osal.h" +#include "pnal.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define APP_TICK_INTERVAL_US 1000 /* 1 ms */ + +/* Thread configuration for targets where sample + * event loop is run in a separate thread (not main). + * This applies for linux sample app implementation. + */ +#define APP_MAIN_THREAD_PRIORITY 15 +#define APP_MAIN_THREAD_STACKSIZE 4096 /* bytes */ + +#define APP_DATA_LED_ID 1 +#define APP_PROFINET_SIGNAL_LED_ID 2 + +#define APP_TICKS_READ_BUTTONS 10 +#define APP_TICKS_UPDATE_DATA 100 + +/** HW Offload configuration. */ +typedef enum +{ + MODE_HW_OFFLOAD_NONE = 0, + MODE_HW_OFFLOAD_CPU, + MODE_HW_OFFLOAD_FULL, +} app_mode_t; + +/** Command line arguments for sample application */ +typedef struct app_args +{ + char path_button1[PNET_MAX_FILE_FULLPATH_SIZE]; /** Terminated string */ + char path_button2[PNET_MAX_FILE_FULLPATH_SIZE]; /** Terminated string */ + char path_storage_directory[PNET_MAX_DIRECTORYPATH_SIZE]; /** Terminated */ + char station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated string */ + char eth_interfaces + [PNET_INTERFACE_NAME_MAX_SIZE * (PNET_MAX_PHYSICAL_PORTS + 1) + + PNET_MAX_PHYSICAL_PORTS]; /** Terminated string */ + int verbosity; + int show; + bool factory_reset; + bool remove_files; + app_mode_t mode; +} app_args_t; + +typedef enum +{ + RUN_IN_SEPARATE_THREAD, + RUN_IN_MAIN_THREAD +} app_run_in_separate_task_t; + +typedef struct app_data_t app_data_t; + +/** Partially initialise config values, and use proper callbacks + * + * @param pnet_cfg Out: Configuration to be updated + */ +void app_pnet_cfg_init_default (pnet_cfg_t * pnet_cfg); + +/** + * Initialize P-Net stack and application. + * + * The \a pnet_cfg argument shall have been initialized using + * \a app_pnet_cfg_init_default() before this function is + * called. + * + * @param pnet_cfg In: P-Net configuration + * @param app_args In: Application arguments + * @return Application handle, NULL on error + */ +app_data_t * app_init (const pnet_cfg_t * pnet_cfg, const app_args_t * app_args); + +/** + * Start application main loop + * + * Application must have been initialized using \a app_init() before + * this function is called. + * + * If \a task_config parameters is set to RUN_IN_SEPARATE_THREAD a + * thread execution the \a app_loop_forever() function is started. + * If task_config is set to RUN_IN_MAIN_THREAD no such thread is + * started and the caller must call the \a app_loop_forever() after + * calling this function. + * + * RUN_IN_MAIN_THREAD is intended for rt-kernel targets. + * RUN_IN_SEPARATE_THREAD is intended for linux targets. + * + * @param app In: Application handle + * @param task_config In: Defines if stack and application + * is run in main or separate task. + * @return 0 on success, -1 on error + */ +int app_start (app_data_t * app, app_run_in_separate_task_t task_config); + +/** + * Application task definition. Handles events in eternal loop. + * + * @param arg In: Application handle + */ +void app_loop_forever (void * arg); + +/** + * Get P-Net instance from application + * + * @param app In: Application handle + * @return P-Net instance, NULL on failure + */ +pnet_t * app_get_pnet_instance (app_data_t * app); + +/** + * Set LED state + * Hardware specific. Implemented in sample app main file for + * each supported platform. + * + * @param id In: LED number, starting from 0. + * @param led_state In: LED state. Use true for on and false for off. + */ +void app_set_led (uint16_t id, bool led_state); + +/** + * Read button state + * + * Hardware specific. Implemented in sample app main file for + * each supported platform. + * + * @param id In: Button number, starting from 0. + * @return true if button is pressed, false if not + */ +bool app_get_button (uint16_t id); + +#ifdef __cplusplus +} +#endif + +#endif /* SAMPLEAPP_COMMON_H */ diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/CMakeLists.txt b/profinet_stack/p-net/samples/pn_dev_lan9662/CMakeLists.txt new file mode 100644 index 0000000..6b3120e --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/CMakeLists.txt @@ -0,0 +1,23 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# http://www.rt-labs.com +# Copyright 2017 rt-labs AB, Sweden. +# See LICENSE file in the project root for full license information. +#*******************************************************************/ + +target_include_directories(pn_lan9662 + PRIVATE + ${PROFINET_SOURCE_DIR}/src + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662 + ${PROFINET_BINARY_DIR}/src + ) + + +target_link_libraries (pn_lan9662 PUBLIC profinet mera) + +install (TARGETS pn_lan9662 DESTINATION bin) diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/GSDML-V2.4-RT-Labs-P-Net-LAN9662-20220511.xml b/profinet_stack/p-net/samples/pn_dev_lan9662/GSDML-V2.4-RT-Labs-P-Net-LAN9662-20220511.xml new file mode 100644 index 0000000..d4135ca --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/GSDML-V2.4-RT-Labs-P-Net-LAN9662-20220511.xml @@ -0,0 +1,417 @@ + + + + + PROFINET Device Profile + 1.00 + Device Profile for PROFINET Devices + PROFIBUS Nutzerorganisation e. V. (PNO) + Device + + 4 + 1 + GSDML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/S90_start_profinet.sh b/profinet_stack/p-net/samples/pn_dev_lan9662/S90_start_profinet.sh new file mode 100644 index 0000000..2ddade0 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/S90_start_profinet.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# +# Initialization script for automatic +# start of the pn_lan9662 sample application. +# Copy this script to overlay/etc/init.d/ +# to start the application automatically. +# + +start() { + # Assume a valid partion on /dev/mmcblk0p2 + mkdir /tmp/pn_data + mount -o sync /dev/mmcblk0p2 /tmp/pn_data + + echo "Configure network settings for pn_lan9662 profinet sample application" + # Create bridge + ip link add name br0 type bridge + symreg ANA_PGID[61] 0x1ff + ip link set br0 address 12:A9:2D:16:93:89 + ip link set br0 up + sysctl -w net.ipv6.conf.br0.disable_ipv6=1 + + ip link set eth0 master br0 + ip link set eth1 master br0 + + ip link set eth0 up + ip link set eth1 up + + # Enable forwarding to chip port 4 (RTE) + symreg qsys_sw_port_mode[4] 0x45000 + + # Start Profinet application + # Supported modes: none, cpu, full + # Read mode from file + mode="full" + if test -f /tmp/pn_data/mode.txt; then + mode=$(cat /tmp/pn_data/mode.txt) + fi + + echo "Starting LAN9662 Profinet sample application" + echo "RTE mode: $mode" + + /usr/bin/pn_lan9662 -m $mode -vvvv -p /tmp/pn_data & + + # Enable for interleaved ART-Tester logs + #tcpdump -i br0 udp port 514 -v -a & +} + +stop() { + printf "Todo - Stop Profinet application" +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart|reload) + stop + start + ;; + *) + echo "Usage: $0 {start|stop|restart}" + exit 1 +esac + +exit $? diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_data.c b/profinet_stack/p-net/samples/pn_dev_lan9662/app_data.c new file mode 100644 index 0000000..a3bba1c --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_data.c @@ -0,0 +1,442 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "app_data.h" +#include "app_shm.h" +#include "app_utils.h" +#include "app_gsdml.h" +#include "app_log.h" +#include "sampleapp_common.h" +#include "osal.h" +#include "pnal.h" +#include + +#include +#include +#include + +/** + * IO FPGA address mapping. + */ +#define APP_DATA_FPGA_START_ADDRESS 0x100 +#define ALIGN32(x) ((((x)) + 3) & ~3) + +#define APP_FPGA_ADDRESS_DIGITAL_IN_1x8 (APP_DATA_FPGA_START_ADDRESS) +#define APP_FPGA_ADDRESS_DIGITAL_OUT_1x8 \ + (APP_FPGA_ADDRESS_DIGITAL_IN_1x8 + ALIGN32 (APP_GSDML_SIZE_DIGITAL_IN_1x8)) +#define APP_FPGA_ADDRESS_DIGITAL_IN_1x64 \ + (APP_FPGA_ADDRESS_DIGITAL_OUT_1x8 + ALIGN32 (APP_GSDML_SIZE_DIGITAL_OUT_1x8)) +#define APP_FPGA_ADDRESS_DIGITAL_IN_2x32a \ + (APP_FPGA_ADDRESS_DIGITAL_IN_1x64 + ALIGN32 (APP_GSDML_SIZE_DIGITAL_IN_1x64)) +#define APP_FPGA_ADDRESS_DIGITAL_IN_2x32b \ + (APP_FPGA_ADDRESS_DIGITAL_IN_2x32a + \ + ALIGN32 (APP_GSDML_SIZE_DIGITAL_IN_2x32a)) +#define APP_FPGA_ADDRESS_DIGITAL_IN_1x800 \ + (APP_FPGA_ADDRESS_DIGITAL_IN_2x32b + \ + ALIGN32 (APP_GSDML_SIZE_DIGITAL_IN_2x32b)) +#define APP_FPGA_ADDRESS_DIGITAL_OUT_1x64 \ + (APP_FPGA_ADDRESS_DIGITAL_IN_1x800 + \ + ALIGN32 (APP_GSDML_SIZE_DIGITAL_IN_1x800)) +#define APP_FPGA_ADDRESS_DIGITAL_OUT_2x32a \ + (APP_FPGA_ADDRESS_DIGITAL_OUT_1x64 + \ + ALIGN32 (APP_GSDML_SIZE_DIGITAL_OUT_1x64)) +#define APP_FPGA_ADDRESS_DIGITAL_OUT_2x32b \ + (APP_FPGA_ADDRESS_DIGITAL_OUT_2x32a + \ + ALIGN32 (APP_GSDML_SIZE_DIGITAL_OUT_2x32a)) +#define APP_FPGA_ADDRESS_DIGITAL_OUT_1x800 \ + (APP_FPGA_ADDRESS_DIGITAL_OUT_2x32b + \ + ALIGN32 (APP_GSDML_SIZE_DIGITAL_OUT_2x32b)) + +#define APP_FPGA_ADDRESS_DIGITAL_IN_PORT_A 0x200 +#define APP_FPGA_ADDRESS_DIGITAL_OUT_PORT_A 0x10 + +typedef struct submodule_fpga_config +{ + uint16_t address; + const uint8_t * default_data; +} submodule_fpga_config_t; + +typedef struct submodule +{ + const app_gsdml_submodule_t * info; + + /* shm -> pnet*/ + app_shm_t * indata_shm; + uint8_t * in_data; + + /* pnet -> shm */ + app_shm_t * outdata_shm; + uint8_t * out_data; + + /* FPGA address for full offload mode */ + const submodule_fpga_config_t * fpga_config; + +} submodule_t; + +typedef struct fpga_map +{ + uint32_t submodule_id; + submodule_fpga_config_t fpga_config; +} fpga_map_t; + +static const uint8_t app_data_default[APP_GSDML_MAX_SUBMODULE_DATA_SIZE] = {0}; + +/** + * Configuration of FPGA addresses for the different + * submodules when running full offload. + */ +static const fpga_map_t fpga[] = { + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x8, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_IN_1x8, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x8, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_OUT_1x8, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x64, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_IN_1x64, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32a, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_IN_2x32a, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32b, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_IN_2x32b, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x800, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_IN_1x800, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x64, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_OUT_1x64, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32a, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_OUT_2x32a, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32b, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_OUT_2x32b, + .default_data = app_data_default}}, + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x800, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_OUT_1x800, + .default_data = app_data_default}}, + + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_PORT_A, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_IN_PORT_A, + .default_data = app_data_default}}, + + {.submodule_id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_PORT_A, + .fpga_config = + {.address = APP_FPGA_ADDRESS_DIGITAL_OUT_PORT_A, + .default_data = app_data_default}}, +}; + +submodule_t submodules[APP_GSDML_MAX_SUBMODULES] = {0}; + +/** + * Get a submodule using its submodule id + * @param id In: Submodule id + * @return Reference to submodule. NULL if id is not found. + **/ +static submodule_t * get_submodule_by_id (uint32_t id) +{ + uint16_t i; + + for (i = 0; i < NELEMENTS (submodules); i++) + { + if (submodules[i].info != NULL && submodules[i].info->id == id) + { + return &submodules[i]; + } + } + + return NULL; +} + +/** + * Get submodule fpga config using its id. + * @param id In: Submodule id + * @return Reference to fgpa mapping. NULL if id is not found. + **/ +static const submodule_fpga_config_t * get_fpga_config_by_id (uint32_t id) +{ + uint32_t i = 0; + + while (i < NELEMENTS (fpga)) + { + if (fpga[i].submodule_id == id) + { + return &fpga[i].fpga_config; + } + i++; + } + return NULL; +} + +int app_data_get_fpga_info_by_id ( + uint32_t id, + uint16_t * address, + const uint8_t ** default_data) +{ + const submodule_fpga_config_t * config = get_fpga_config_by_id (id); + + if (config != NULL) + { + *address = config->address; + *default_data = config->default_data; + return 0; + } + return -1; +} + +/** + * Allocate a submodule from submodules array. + * The returned submodule is uninitialized. + * @return Reference to submodule. NULL if allocation fails. + **/ +static submodule_t * alloc_submodule (void) +{ + uint32_t i = 0; + + while (i < NELEMENTS (submodules)) + { + if (submodules[i].info == NULL) + { + return &submodules[i]; + } + i++; + } + return NULL; +} + +/** + * Initialize submodule + * Set module and submodule configurations. + * Configure shared memory regions. + * Allocate buffers. + * Will generate assert on failure. + * @param module_cfg In: Module configuration + * @param submodule_cfg In: Submodule configuration + * @param app_mode In: Application mode + **/ +static void app_data_init_submodule ( + const app_gsdml_module_t * module_cfg, + const app_gsdml_submodule_t * submodule_cfg, + app_mode_t app_mode) +{ + submodule_t * submodule; + + CC_ASSERT (module_cfg != NULL); + CC_ASSERT (submodule_cfg != NULL); + + submodule = alloc_submodule(); + CC_ASSERT (submodule != NULL); + + submodule->info = submodule_cfg; + if (app_mode == MODE_HW_OFFLOAD_FULL) + { + submodule->fpga_config = get_fpga_config_by_id (submodule_cfg->id); + CC_ASSERT (submodule->fpga_config != NULL); + } + else + { + submodule->fpga_config = NULL; + } + + if (submodule->info->insize > 0) + { + submodule->in_data = malloc (submodule->info->insize); + CC_ASSERT (submodule->in_data != NULL); + + if (app_mode != MODE_HW_OFFLOAD_FULL) + { + submodule->indata_shm = app_shm_create_input ( + submodule->info->name, + module_cfg->fixed_slot, + submodule_cfg->fixed_subslot, + submodule->info->insize); + CC_ASSERT (submodule->indata_shm != NULL); + } + else + { + APP_LOG_INFO ( + "[%u,%u,\"%s\"] mapped to FPGA address 0x%x\n", + module_cfg->fixed_slot, + submodule_cfg->fixed_subslot, + submodule->info->name, + submodule->fpga_config->address); + } + } + + if (submodule->info->outsize > 0) + { + submodule->out_data = malloc (submodule->info->outsize); + CC_ASSERT (submodule->out_data != NULL); + + if (app_mode != MODE_HW_OFFLOAD_FULL) + { + submodule->outdata_shm = app_shm_create_output ( + submodule->info->name, + module_cfg->fixed_slot, + submodule_cfg->fixed_subslot, + submodule->info->outsize); + CC_ASSERT (submodule->outdata_shm != NULL); + } + else + { + APP_LOG_INFO ( + "[%u,%u,\"%s\"] mapped to FPGA address 0x%x\n", + module_cfg->fixed_slot, + submodule_cfg->fixed_subslot, + submodule->info->name, + submodule->fpga_config->address); + } + } +} + +int app_data_init (app_mode_t app_mode) +{ + uint16_t i, j; + const app_gsdml_module_t * module_cfg; + const app_gsdml_submodule_t * submodule_cfg; + uint32_t modules_array_len; + const app_gsdml_module_t ** app_gsdml_modules; + + app_gsdml_modules = app_gsdml_get_modules (&modules_array_len); + + for (i = 0; i < modules_array_len; i++) + { + module_cfg = app_gsdml_modules[i]; + + j = 0; + while (module_cfg->submodules[j] != 0) + { + submodule_cfg = + app_gsdml_get_submodule_cfg (module_cfg->submodules[j]); + app_data_init_submodule (module_cfg, submodule_cfg, app_mode); + j++; + } + } + return 0; +} + +uint8_t * app_data_get_input_data ( + uint32_t submodule_id, + uint16_t * size, + uint8_t * iops) +{ + int error = -1; + submodule_t * submodule; + + if (size == NULL || iops == NULL) + { + return NULL; + } + + submodule = get_submodule_by_id (submodule_id); + + if (submodule == NULL) + { + /* Handle unsupported submodules part of process data */ + *iops = PNET_IOXS_BAD; + return NULL; + } + + *size = submodule->info->insize; + if (submodule->fpga_config == NULL) + { + error = app_shm_read (submodule->indata_shm, submodule->in_data, *size); + CC_ASSERT (error == 0); + } + else + { + /* Input is mapped to FPGA. Data is not available to + * application. Use default value set status. + * P-Net needs an initial value during initialization. + */ + memcpy (submodule->in_data, submodule->fpga_config->default_data, *size); + } + *iops = PNET_IOXS_GOOD; + + return submodule->in_data; +} + +uint8_t * app_data_get_output_data_buffer ( + uint32_t submodule_id, + uint16_t * size) +{ + submodule_t * submodule = get_submodule_by_id (submodule_id); + + CC_ASSERT (size != NULL); + if (submodule == NULL) + { + *size = 0; + return NULL; + } + + *size = submodule->info->outsize; + return submodule->out_data; +} + +int app_data_set_output_data ( + uint32_t submodule_id, + uint8_t * data, + uint16_t size) +{ + int error = 0; + submodule_t * submodule = get_submodule_by_id (submodule_id); + + if (data == NULL || submodule == NULL || size == 0) + { + return -1; + } + + if (submodule->fpga_config == NULL) + { + /* Only write shared memory if output is not mapped + * to FPGA. + */ + error = app_shm_write (submodule->outdata_shm, data, size); + } + + return error; +} + +int app_data_set_default_outputs (void) +{ + uint16_t i; + + for (i = 0; i < NELEMENTS (submodules); i++) + { + if (submodules[i].outdata_shm != NULL) + { + if (submodules[i].fpga_config == NULL) + { + /* Only write shared memory if output is not mapped + * to FPGA. + */ + app_shm_reset (submodules[i].outdata_shm); + } + } + } + return 0; +} diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_data.h b/profinet_stack/p-net/samples/pn_dev_lan9662/app_data.h new file mode 100644 index 0000000..58626b8 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_data.h @@ -0,0 +1,121 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_DATA_H +#define APP_DATA_H + +#include "sampleapp_common.h" + +/** + * @file + * @brief Sample application data interface + * + * Functions for: + * - Getting input data + * - Setting ouput data + * - Setting default output state + * + * In this LAN9662 sample application the submodule data + * is mapped to shared memory found in /dev/shm on Linux. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * Initialize data submodules. + * During the initialization the shared memory areas + * supported by the application are created and the + * mapping between the shared memory and the submodules + * is configured. + * @param app_mode In: Application mode + * @return 0 on success, -1 on error + */ +int app_data_init (app_mode_t app_mode); + +/** + * Get PNIO input data using submodule id. + * When this function is called the mapped shared + * memory is copied to the submodule input buffer. + * A reference to the the input buffer is returned. + * @param submodule_id In: Submodule id + * @param size Out: Size of pnio data + * @param iops Out: Provider status. If for example + * a sensor is failing or a short + * circuit detected on digital + * input this shall be set to BAD. + * Set to BAD for unsupported submodules + * or data can not be provided. + * @return Reference to PNIO data, NULL on error + */ +uint8_t * app_data_get_input_data ( + uint32_t submodule_id, + uint16_t * size, + uint8_t * iops); + +/** + * Get FPGA data start address for submodule data + * @param id In: Submodule id + * @param address Out: FPGA start address + * @param default_data Out: Submodule default data + * @return 0 on success, -1 on error + */ +int app_data_get_fpga_info_by_id ( + uint32_t id, + uint16_t * address, + const uint8_t ** default_data); + +/** + * Get PNIO output data buffer using submodule id. + * @param submodule_id In: Submodule id + * @param size Out: Size of pnio data. If submodule + * is not supported size is set to 0. + * @return Reference to PNIO data, NULL on error + */ +uint8_t * app_data_get_output_data_buffer ( + uint32_t submodule_id, + uint16_t * size); + +/** + * Set PNIO output data using module id. + * When this operation is called the data is + * written the mapped shared memory. + * @param submodule_id In: Submodule id + * @param data In: Reference to output data + * @param size In: Length of output data + * @return 0 on success, -1 on error + */ +int app_data_set_output_data ( + uint32_t submodule_id, + uint8_t * data, + uint16_t size); + +/** + * Set default values for all outputs. + * When this operation is called the mapped + * shared memory is set to 0. + * @return 0 on success, -1 on error + */ +int app_data_set_default_outputs (void); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_DATA_H */ diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.c b/profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.c new file mode 100644 index 0000000..30b4acc --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.c @@ -0,0 +1,394 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Device properties defined by the GSDML device definition + * + * Functions for getting module, submodule and parameter + * configurations using their IDs. + * + * Important: + * Any change in this file may require an update of the GSDML file. + * Note that when the GSDML file is updated it has to be reloaded + * in your Profinet engineering tool. PLC applications may be affected. + * + * Design requires unique submodule IDs. + */ + +#include "sampleapp_common.h" +#include "app_utils.h" +#include "app_gsdml.h" +#include "app_log.h" +#include "osal.h" +#include "pnal.h" +#include + +#include +#include +#include + +/************************* Modules ***************************/ + +static const app_gsdml_module_t dap_1 = { + .id = PNET_MOD_DAP_IDENT, + .name = "DAP 1", + .fixed_slot = 0, + .submodules = { + PNET_SUBMOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_2_IDENT, + 0}}; + +static const app_gsdml_module_t module_di_1x8 = { + .id = APP_GSDML_MOD_ID_DIGITAL_IN_1x8, + .name = "DI 1x8", + .fixed_slot = 1, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x8, 0}, +}; + +static const app_gsdml_module_t module_do_1x8 = { + .id = APP_GSDML_MOD_ID_DIGITAL_OUT_1x8, + .name = "DO 1x8", + .fixed_slot = 2, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x8, 0}}; + +static const app_gsdml_module_t module_di_1x64 = { + .id = APP_GSDML_MOD_ID_DIGITAL_IN_1x64, + .name = "DI 1x64", + .fixed_slot = 3, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x64, 0}, +}; + +static const app_gsdml_module_t module_di_2x32a = { + .id = APP_GSDML_MOD_ID_DIGITAL_IN_2x32a, + .name = "DI 2x32a", + .fixed_slot = 4, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32a, 0}, +}; + +static const app_gsdml_module_t module_di_2x32b = { + .id = APP_GSDML_MOD_ID_DIGITAL_IN_2x32b, + .name = "DI 2x32b", + .fixed_slot = 5, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32b, 0}, +}; + +static const app_gsdml_module_t module_di_1x800 = { + .id = APP_GSDML_MOD_ID_DIGITAL_IN_1x800, + .name = "DI 1x800", + .fixed_slot = 6, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x800, 0}, +}; + +static const app_gsdml_module_t module_do_1x64 = { + .id = APP_GSDML_MOD_ID_DIGITAL_OUT_1x64, + .name = "DO 1x64", + .fixed_slot = 7, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x64, 0}, +}; + +static const app_gsdml_module_t module_do_2x32a = { + .id = APP_GSDML_MOD_ID_DIGITAL_OUT_2x32a, + .name = "DO 2x32a", + .fixed_slot = 8, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32a, 0}, +}; + +static const app_gsdml_module_t module_do_2x32b = { + .id = APP_GSDML_MOD_ID_DIGITAL_OUT_2x32b, + .name = "DO 2x32b", + .fixed_slot = 9, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32b, 0}, +}; + +static const app_gsdml_module_t module_do_1x800 = { + .id = APP_GSDML_MOD_ID_DIGITAL_OUT_1x800, + .name = "DO 1x800", + .fixed_slot = 10, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x800, 0}, +}; + +static const app_gsdml_module_t module_di_port_a = { + .id = APP_GSDML_MOD_ID_DIGITAL_IN_PORT_A, + .name = "DI Port A", + .fixed_slot = 11, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_IN_PORT_A, 0}, +}; + +static const app_gsdml_module_t module_do_port_a = { + .id = APP_GSDML_MOD_ID_DIGITAL_OUT_PORT_A, + .name = "DO Port A", + .fixed_slot = 12, + .submodules = {APP_GSDML_SUBMOD_ID_DIGITAL_OUT_PORT_A, 0}, +}; +/************************* Submodules ***************************/ + +static const app_gsdml_submodule_t dap_identity_1 = { + .name = "DAP Identity 1", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_IDENT, + .fixed_subslot = PNET_SUBSLOT_DAP_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_interface_1 = { + .name = "DAP Interface 1", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_IDENT, + .fixed_subslot = PNET_SUBSLOT_DAP_INTERFACE_1_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_1 = { + .name = "DAP Port 1", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT, + .fixed_subslot = PNET_SUBSLOT_DAP_INTERFACE_1_PORT_1_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_2 = { + .name = "DAP Port 2", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_2_IDENT, + .fixed_subslot = PNET_SUBSLOT_DAP_INTERFACE_1_PORT_2_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_3 = { + .name = "DAP Port 3", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_3_IDENT, + .fixed_subslot = PNET_SUBSLOT_DAP_INTERFACE_1_PORT_3_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t dap_port_4 = { + .name = "DAP Port 4", + .api = APP_GSDML_API, + .id = PNET_SUBMOD_DAP_INTERFACE_1_PORT_4_IDENT, + .fixed_subslot = PNET_SUBSLOT_DAP_INTERFACE_1_PORT_4_IDENT, + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_di_1x8 = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x8, + .fixed_subslot = 1, + .name = "Digital Input 1x8", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_INPUT, + .insize = APP_GSDML_SIZE_DIGITAL_IN_1x8, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_do_1x8 = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x8, + .fixed_subslot = 1, + .name = "Digital Output 1x8", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_OUTPUT, + .insize = 0, + .outsize = APP_GSDML_SIZE_DIGITAL_OUT_1x8, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_di_1x64 = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x64, + .fixed_subslot = 1, + .name = "Digital Input 1x64", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_INPUT, + .insize = APP_GSDML_SIZE_DIGITAL_IN_1x64, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_di_2x32a = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32a, + .fixed_subslot = 1, + .name = "Digital Input 2x32 a", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_INPUT, + .insize = 8, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_di_2x32b = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32b, + .fixed_subslot = 1, + .name = "Digital Input 2x32 b", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_INPUT, + .insize = APP_GSDML_SIZE_DIGITAL_IN_2x32b, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_di_1x800 = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x800, + .fixed_subslot = 1, + .name = "Digital Input 1x800", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_INPUT, + .insize = APP_GSDML_SIZE_DIGITAL_IN_1x800, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_do_1x64 = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x64, + .fixed_subslot = 1, + .name = "Digital Output 1x64", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_OUTPUT, + .insize = 0, + .outsize = APP_GSDML_SIZE_DIGITAL_OUT_1x64, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_do_2x32a = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32a, + .fixed_subslot = 1, + .name = "Digital Output 2x32 a", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_OUTPUT, + .insize = 0, + .outsize = APP_GSDML_SIZE_DIGITAL_OUT_2x32a, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_do_2x32b = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32b, + .fixed_subslot = 1, + .name = "Digital Output 2x32 b", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_OUTPUT, + .insize = 0, + .outsize = APP_GSDML_SIZE_DIGITAL_OUT_2x32b, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_do_1x800 = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x800, + .fixed_subslot = 1, + .name = "Digital Output 1x800", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_OUTPUT, + .insize = 0, + .outsize = APP_GSDML_SIZE_DIGITAL_OUT_1x800, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_di_port_a = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_IN_PORT_A, + .fixed_subslot = 1, + .name = "Digital Input Port A", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_INPUT, + .insize = APP_GSDML_SIZE_DIGITAL_IN_PORT_A, + .outsize = 0, + .parameters = {0}}; + +static const app_gsdml_submodule_t submodule_do_port_a = { + .id = APP_GSDML_SUBMOD_ID_DIGITAL_OUT_PORT_A, + .fixed_subslot = 1, + .name = "Digital Output Port A", + .api = APP_GSDML_API, + .data_dir = PNET_DIR_OUTPUT, + .insize = 0, + .outsize = APP_GSDML_SIZE_DIGITAL_OUT_PORT_A, + .parameters = {0}}; + +/* List of supported modules */ +static const app_gsdml_module_t * app_gsdml_modules[] = { + &dap_1, + &module_di_1x8, + &module_do_1x8, + &module_di_1x64, + &module_di_2x32a, + &module_di_2x32b, + &module_di_1x800, + &module_do_1x64, + &module_do_2x32a, + &module_do_2x32b, + &module_do_1x800, + &module_di_port_a, + &module_do_port_a}; + +/* List of supported submodules */ +static const app_gsdml_submodule_t * app_gsdml_submodules[] = { + &dap_identity_1, + &dap_interface_1, + &dap_port_1, + &dap_port_2, + &dap_port_3, + &dap_port_4, + + &submodule_di_1x8, + &submodule_do_1x8, + &submodule_di_1x64, + &submodule_di_2x32a, + &submodule_di_2x32b, + &submodule_di_1x800, + &submodule_do_1x64, + &submodule_do_2x32a, + &submodule_do_2x32b, + &submodule_do_1x800, + &submodule_di_port_a, + &submodule_do_port_a}; + +const app_gsdml_module_t * app_gsdml_get_module_cfg (uint32_t id) +{ + uint32_t i; + for (i = 0; i < NELEMENTS (app_gsdml_modules); i++) + { + if (app_gsdml_modules[i]->id == id) + { + return app_gsdml_modules[i]; + } + } + return NULL; +} + +const app_gsdml_module_t ** app_gsdml_get_modules (uint32_t * array_len) +{ + *array_len = NELEMENTS (app_gsdml_modules); + return app_gsdml_modules; +} + +const app_gsdml_submodule_t ** app_gsdml_get_submodules (uint32_t * array_len) +{ + *array_len = NELEMENTS (app_gsdml_submodules); + return app_gsdml_submodules; +} + +const app_gsdml_submodule_t * app_gsdml_get_submodule_cfg (uint32_t id) +{ + uint32_t i; + for (i = 0; i < NELEMENTS (app_gsdml_submodules); i++) + { + if (app_gsdml_submodules[i]->id == id) + { + return app_gsdml_submodules[i]; + } + } + return NULL; +} diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.h b/profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.h new file mode 100644 index 0000000..87dbfb5 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_gsdml.h @@ -0,0 +1,199 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_GSDML_H +#define APP_GSDML_H + +/** + * @file + * @brief Device properties defined by the GSDML device definition + * + * Functions for getting module, submodule and parameter + * configurations using their IDs. + * + * Important: + * Any change in this file may require an update of the GSDML file. + * Note that when the GSDML file is updated it has to be reloaded + * in your Profinet engineering tool. PLC applications may be affected. + * + * Design requires unique submodule IDs. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define APP_GSDML_API 0 + +#define APP_GSDML_DEFAULT_STATION_NAME "lan9662-dev" + +/* GSDML tag: VendorID */ +#define APP_GSDML_VENDOR_ID 0x0493 + +/* GSDML tag: DeviceID */ +#define APP_GSDML_DEVICE_ID 0x9662 + +/* Allowed: 'V', 'R', 'P', 'U', 'T' */ +#define APP_GSDML_SW_REV_PREFIX 'V' +#define APP_GSDML_PROFILE_ID 0x1234 +#define APP_GSDML_PROFILE_SPEC_TYPE 0x5678 + +/* GSDML tag: Writeable_IM_Records */ +#define APP_GSDML_IM_SUPPORTED \ + (PNET_SUPPORTED_IM1 | PNET_SUPPORTED_IM2 | PNET_SUPPORTED_IM3) + +/* GSDML tag: OrderNumber */ +#define APP_GSDML_ORDER_ID "12345 Abcdefghijk" +/* GSDML tag: ModuleInfo / Name */ +#define APP_GSDML_PRODUCT_NAME "P-Net LAN9662 Sample" + +/* GSDML tag: MinDeviceInterval */ +#define APP_GSDML_MIN_DEVICE_INTERVAL 32 /* 1ms */ + +#define APP_GSDML_DIAG_CUSTOM_USI 0x1234 + +/* See "Specification for GSDML" 8.26 LogBookEntryItem for allowed values */ +#define APP_GSDML_LOGBOOK_ERROR_CODE 0x20 /* Manufacturer specific */ +#define APP_GSDML_LOGBOOK_ERROR_DECODE 0x82 /* Manufacturer specific */ +#define APP_GSDML_LOGBOOK_ERROR_CODE_1 PNET_ERROR_CODE_1_FSPM +#define APP_GSDML_LOGBOOK_ERROR_CODE_2 0x00 /* Manufacturer specific */ +#define APP_GSDML_LOGBOOK_ENTRY_DETAIL 0xFEE1DEAD /* Manufacturer specific */ + +#define APP_GSDML_MAX_SUBMODULES 20 + +#define APP_GSDML_ALARM_PAYLOAD_SIZE 1 /* bytes */ + +#define APP_GSDML_MOD_ID_DIGITAL_IN_1x8 0x00001001 +#define APP_GSDML_MOD_ID_DIGITAL_OUT_1x8 0x00001002 +#define APP_GSDML_MOD_ID_DIGITAL_IN_1x64 0x00001003 +#define APP_GSDML_MOD_ID_DIGITAL_IN_2x32a 0x00001004 +#define APP_GSDML_MOD_ID_DIGITAL_IN_2x32b 0x00001005 +#define APP_GSDML_MOD_ID_DIGITAL_IN_1x800 0x00001006 +#define APP_GSDML_MOD_ID_DIGITAL_OUT_1x64 0x00001007 +#define APP_GSDML_MOD_ID_DIGITAL_OUT_2x32a 0x00001008 +#define APP_GSDML_MOD_ID_DIGITAL_OUT_2x32b 0x00001009 +#define APP_GSDML_MOD_ID_DIGITAL_OUT_1x800 0x0000100A +#define APP_GSDML_MOD_ID_DIGITAL_IN_PORT_A 0x0000100B +#define APP_GSDML_MOD_ID_DIGITAL_OUT_PORT_A 0x0000100C + +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x8 0x00002001 +#define APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x8 0x00002002 +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x64 0x00002003 +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32a 0x00002004 +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN_2x32b 0x00002005 +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN_1x800 0x00002006 +#define APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x64 0x00002007 +#define APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32a 0x00002008 +#define APP_GSDML_SUBMOD_ID_DIGITAL_OUT_2x32b 0x00002009 +#define APP_GSDML_SUBMOD_ID_DIGITAL_OUT_1x800 0x0000200A +#define APP_GSDML_SUBMOD_ID_DIGITAL_IN_PORT_A 0x0000200B +#define APP_GSDML_SUBMOD_ID_DIGITAL_OUT_PORT_A 0x0000200C + +#define APP_GSDML_SIZE_DIGITAL_IN_1x8 1 +#define APP_GSDML_SIZE_DIGITAL_OUT_1x8 1 +#define APP_GSDML_SIZE_DIGITAL_IN_1x64 8 +#define APP_GSDML_SIZE_DIGITAL_IN_2x32a 8 +#define APP_GSDML_SIZE_DIGITAL_IN_2x32b 8 +#define APP_GSDML_SIZE_DIGITAL_IN_1x800 100 +#define APP_GSDML_SIZE_DIGITAL_OUT_1x64 8 +#define APP_GSDML_SIZE_DIGITAL_OUT_2x32a 8 +#define APP_GSDML_SIZE_DIGITAL_OUT_2x32b 8 +#define APP_GSDML_SIZE_DIGITAL_OUT_1x800 100 +#define APP_GSDML_SIZE_DIGITAL_IN_PORT_A 4 +#define APP_GSDML_SIZE_DIGITAL_OUT_PORT_A 4 + +#define APP_GSDML_MAX_SUBMODULE_DATA_SIZE 100 + +/* TODO support virtual and pluggable submodules. */ +typedef struct cfg_module +{ + uint32_t id; + const char * name; + uint32_t fixed_slot; /* Set to UINT32_MAX if not fixed */ + uint32_t submodules[]; /* Variable length, ends with 0*/ +} app_gsdml_module_t; + +typedef struct app_gsdml_submodule +{ + uint32_t id; + const char * name; + uint32_t api; + uint32_t fixed_subslot; /* Set to UINT32_MAX if not fixed */ + pnet_submodule_dir_t data_dir; + uint16_t insize; + uint16_t outsize; + uint16_t parameters[]; /* Variable length, ends with 0 */ +} app_gsdml_submodule_t; + +typedef struct app_gsdml_param +{ + uint32_t index; + const char * name; + uint16_t length; +} app_gsdml_param_t; + +/** + * Get array of supported modules + * @param array_len Out: Length of array + * @return Modules array + */ +const app_gsdml_module_t ** app_gsdml_get_modules (uint32_t * array_len); + +/** + * Get array of supported submodules + * @param array_len Out: Length of array + * @return Submodules array + */ +const app_gsdml_submodule_t ** app_gsdml_get_submodules (uint32_t * array_len); + +/** + * Get array of supported parameters + * @param array_len Out: Length of array + * @return Parameters array + */ +const app_gsdml_param_t ** app_gsdml_get_parameters (uint32_t * array_len); + +/** + * Get module configuration from module id + * @param module_id In: Module id + * @return Module configuration, NULL if not found + */ +const app_gsdml_module_t * app_gsdml_get_module_cfg (uint32_t module_id); + +/** + * Get submodule module configuration from submodule id + * @param submodule_id In: Submodule id + * @return Submodule configuration, NULL if not found + */ +const app_gsdml_submodule_t * app_gsdml_get_submodule_cfg ( + uint32_t submodule_id); + +/** + * Get parameter configuration from parameter index + * @param submodule_id In: Submodule id + * @param index In: Parameters index + * @return Parameter configuration, NULL if not found + */ +const app_gsdml_param_t * app_gsdml_get_parameter_cfg ( + uint32_t submodule_id, + uint32_t index); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_GSDML_H */ diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_log.c b/profinet_stack/p-net/samples/pn_dev_lan9662/app_log.c new file mode 100644 index 0000000..9b9d782 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_log.c @@ -0,0 +1,52 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "app_log.h" + +#include +#include + +static int32_t log_level = APP_DEFAULT_LOG_LEVEL; + +void app_log_set_log_level (int32_t level) +{ + log_level = level; +} + +void app_log (int32_t level, const char * fmt, ...) +{ + va_list list; + + if (level >= log_level) + { + va_start (list, fmt); + vprintf (fmt, list); + va_end (list); + fflush (stdout); + } +} + +void app_log_print_bytes (int32_t level, const uint8_t * bytes, uint32_t len) +{ + if (level >= log_level) + { + printf (" Bytes: "); + for (uint32_t i = 0; i < len; i++) + { + printf ("%02X ", bytes[i]); + } + printf ("\n"); + } +} diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_log.h b/profinet_stack/p-net/samples/pn_dev_lan9662/app_log.h new file mode 100644 index 0000000..4f90aae --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_log.h @@ -0,0 +1,75 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_LOG_H +#define APP_LOG_H + +/** + * @file + * @brief Application debug log utility + * + * Runtime configurable debug log using printf() + * Levels matches levels used in P-Net. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define APP_LOG_LEVEL_DEBUG 0x00 +#define APP_LOG_LEVEL_INFO 0x01 +#define APP_LOG_LEVEL_WARNING 0x02 +#define APP_LOG_LEVEL_ERROR 0x03 +#define APP_LOG_LEVEL_FATAL 0x04 + +#define APP_DEFAULT_LOG_LEVEL APP_LOG_LEVEL_FATAL + +#define APP_LOG(level, ...) app_log (level, __VA_ARGS__) + +#define APP_LOG_DEBUG(...) APP_LOG (APP_LOG_LEVEL_DEBUG, __VA_ARGS__) +#define APP_LOG_INFO(...) APP_LOG (APP_LOG_LEVEL_INFO, __VA_ARGS__) +#define APP_LOG_WARNING(...) APP_LOG (APP_LOG_LEVEL_WARNING, __VA_ARGS__) +#define APP_LOG_ERROR(...) APP_LOG (APP_LOG_LEVEL_ERROR, __VA_ARGS__) +#define APP_LOG_FATAL(...) APP_LOG (APP_LOG_LEVEL_FATAL, __VA_ARGS__) + +/** + * Print log message depending on level + * Use the APP_LOG_xxxxx macros instead of this function. + * @param level In: Message log level + * @param fmt In: Log message format string + */ +void app_log (int32_t level, const char * fmt, ...); + +/** + * Log an array of bytes + * @param level In: Log level + * @param bytes In: Array of bytes + * @param length In: Length of array + */ +void app_log_print_bytes (int32_t level, const uint8_t * bytes, uint32_t length); + +/** + * Set log level + * @param level In: Log level + */ +void app_log_set_log_level (int32_t level); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_LOG_H */ diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.c b/profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.c new file mode 100644 index 0000000..4fbb072 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.c @@ -0,0 +1,318 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "app_shm.h" +#include "app_log.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define APP_SHM_MAX_FILE_NAME_LEN 40 + +typedef struct app_shm_t +{ + char name[APP_SHM_MAX_FILE_NAME_LEN]; + int fd; + caddr_t mem_address; + sem_t * sem; + size_t size; +} app_shm_t; + +/** + * Format name string + * + * Replace space " " with "-" + * Convert uppercase characters to lowercase. + * @param name In: NULL terminated string + * @return 0 on success, -1 on error + **/ +static int format_name (char * name) +{ + uint32_t i; + + if (name == NULL) + { + return -1; + } + + for (i = 0; name[i] != '\0'; i++) + { + name[i] = tolower (name[i]); + if (name[i] == ' ') + { + name[i] = '_'; + } + } + + return 0; +} + +/** + * Initialise an existing shared memory area handle + * + * @param handle In: Handle + * @param name In: Name + * @param slot In: Slot number + * @param subslot In: Subslot number + * @param size In: Size in bytes + * @param is_output In: True if the shared memory area is an output + * @return 0 on success, -1 on error + **/ +static int app_shm_init ( + app_shm_t * handle, + const char * name, + uint32_t slot, + uint32_t subslot, + uint32_t size, + bool is_output) +{ + const int shm_flags = O_RDWR | O_CREAT; + + if (handle == NULL || name == NULL || size == 0) + { + return -1; + } + memset (handle, 0, sizeof (app_shm_t)); + + /* Only include slot and subslot numbers if both module and + submodules are fixed in slot and subslot */ + if (slot != UINT32_MAX && subslot != UINT32_MAX) + { + snprintf ( + handle->name, + APP_SHM_MAX_FILE_NAME_LEN - 1, + "pnet-%s-%d-%d-%s", + is_output ? "out" : "in", + slot, + subslot, + name); + } + else + { + snprintf ( + handle->name, + APP_SHM_MAX_FILE_NAME_LEN - 1, + "pnet-%s-%s", + is_output ? "out" : "in", + name); + } + + format_name (handle->name); + + handle->size = size; + + handle->fd = shm_open (handle->name, shm_flags, 0666); + + if (handle->fd < 0) + { + APP_LOG_ERROR ( + "Failed to open shared memory\n" + " file name: %s\n" + " Input files is expected to be created by data generating " + " application\n", + handle->name); + + return -1; + } + + if (ftruncate (handle->fd, handle->size) != 0) + { + return -1; + } + + handle->mem_address = mmap ( + NULL, + handle->size, + PROT_READ | PROT_WRITE, + MAP_SHARED, + handle->fd, + 0); + + if (handle->mem_address == MAP_FAILED) + { + return -1; + } + + handle->sem = sem_open (handle->name, O_CREAT, 0666, 1); + + if (handle->sem == NULL) + { + return -1; + } + + /* sem_unlink (handle->name); TODO */ + APP_LOG_DEBUG ("shared memory area created\n"); + APP_LOG_DEBUG (" name=%s\n", handle->name); + APP_LOG_DEBUG (" size=%lu\n", handle->size); + APP_LOG_DEBUG (" backing file = /dev/shm/%s\n", handle->name); + + return 0; +} + +app_shm_t * app_shm_create_input ( + const char * name, + int slot, + int subslot, + int size) +{ + app_shm_t * handle = (app_shm_t *)malloc (sizeof (app_shm_t)); + + if (handle != NULL) + { + if (app_shm_init (handle, name, slot, subslot, size, false) != 0) + { + app_shm_destroy (handle); + handle = NULL; + } + } + + return handle; +} + +app_shm_t * app_shm_create_output ( + const char * name, + int slot, + int subslot, + int size) +{ + app_shm_t * handle = (app_shm_t *)malloc (sizeof (app_shm_t)); + + if (handle != NULL) + { + if (app_shm_init (handle, name, slot, subslot, size, true) != 0) + { + app_shm_destroy (handle); + handle = NULL; + } + } + + return handle; +} + +/** + * Free resources related to a shared memory area. + * + * Does not free the memory allocated for the handle itself. + * + * @param handle In: Handle + **/ +static void app_shm_free_resources (app_shm_t * handle) +{ + if (handle == NULL) + { + return; + } + + if (handle->mem_address != 0) + { + munmap (handle->mem_address, handle->size); + } + + if (handle->fd >= 0) + { + close (handle->fd); + } + + if (handle->sem != NULL) + { + sem_close (handle->sem); + /* sem_unlink TODO */ + } + + unlink (handle->name); +} + +void app_shm_destroy (app_shm_t * handle) +{ + if (handle != NULL) + { + app_shm_free_resources (handle); + free (handle); + } +} + +int app_shm_read (app_shm_t * handle, void * data, size_t size) +{ + if (handle == NULL || size == 0) + { + return -1; + } + + if (sem_wait (handle->sem) != 0) + { + return -1; + } + + memcpy (data, handle->mem_address, size); + + if (sem_post (handle->sem) != 0) + { + return -1; + } + + return 0; +} + +int app_shm_write (app_shm_t * handle, void * data, size_t size) +{ + if (handle == NULL || data == NULL || size > handle->size) + { + return -1; + } + + if (sem_wait (handle->sem) != 0) + { + return -1; + } + + memcpy (handle->mem_address, data, size); + + if (sem_post (handle->sem) != 0) + { + return -1; + } + + return 0; +} + +int app_shm_reset (app_shm_t * handle) +{ + if (handle == NULL) + { + return -1; + } + + if (sem_wait (handle->sem) != 0) + { + return -1; + } + + memset (handle->mem_address, 0, handle->size); + + if (sem_post (handle->sem) != 0) + { + return -1; + } + + return 0; +} diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.h b/profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.h new file mode 100644 index 0000000..f9c7777 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_shm.h @@ -0,0 +1,104 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_SHM_H +#define APP_SHM_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct app_shm_t app_shm_t; + +/** + * Create a shared memory area used as input. + * + * Shared memory is read by this application. + * Intended for submodule input data. + * The slot and and subslot info is used to + * generate the name of the shared memory area. + * + * @param name In: Submodule id + * @param slot In: Slot number + * @param subslot In: Subslot number + * @param size In: Size of area + * @return Handle to created shared memory area. NULL on error. + */ +app_shm_t * app_shm_create_input ( + const char * name, + int slot, + int subslot, + int size); + +/** + * Create a shared memory area used as output. + * + * Shared memory is written by this application. + * Intended for submodule output data. + * The slot and and subslot info is used to + * generate the name of the shared memory area. + * + * @param name In: Submodule id + * @param slot In: Slot number + * @param subslot In: Subslot number + * @param size In: Size of area + * @return Handle to created shared memory area. NULL on error. + */ +app_shm_t * app_shm_create_output ( + const char * name, + int slot, + int subslot, + int size); + +/** + * Free the resources of a shared memory area + * + * @param handle In: Shared memory object handle + */ +void app_shm_destroy (app_shm_t * handle); + +/** + * Read shared memory area into a buffer + * + * @param handle In: Shared memory object handle + * @param data In: Start of buffer + * @param size In: Number of bytes to read + * @return 0 on success, -1 on error + */ +int app_shm_read (app_shm_t * handle, void * data, size_t size); + +/** + * Write to shared memory + * + * @param handle In: Shared memory object handle + * @param data In: Start of buffer + * @param size In: Number of bytes to write + * @return 0 on success, -1 on error + */ +int app_shm_write (app_shm_t * handle, void * data, size_t size); + +/** + * Set shared memory to zero + * + * @param handle In: Shared memory object handle + * @return 0 on success, -1 on error + */ +int app_shm_reset (app_shm_t * handle); + +#endif /* APP_SHM_H */ diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.c b/profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.c new file mode 100644 index 0000000..b85c376 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.c @@ -0,0 +1,722 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#define _GNU_SOURCE /* For asprintf() */ + +#include "app_utils.h" +#include "app_log.h" +#include "app_gsdml.h" +#include "sampleapp_common.h" +#include "osal.h" +#include "osal_log.h" /* For LOG_LEVEL */ +#include "pnal.h" +#include + +#include +#include +#include +#include + +#define GET_HIGH_BYTE(id) ((id >> 8) & 0xFF) +#define GET_LOW_BYTE(id) (id & 0xFF) + +void app_utils_ip_to_string (pnal_ipaddr_t ip, char * outputstring) +{ + snprintf ( + outputstring, + PNAL_INET_ADDRSTR_SIZE, + "%u.%u.%u.%u", + (uint8_t) ((ip >> 24) & 0xFF), + (uint8_t) ((ip >> 16) & 0xFF), + (uint8_t) ((ip >> 8) & 0xFF), + (uint8_t) (ip & 0xFF)); +} + +void app_utils_mac_to_string (pnet_ethaddr_t mac, char * outputstring) +{ + snprintf ( + outputstring, + PNAL_ETH_ADDRSTR_SIZE, + "%02X:%02X:%02X:%02X:%02X:%02X", + mac.addr[0], + mac.addr[1], + mac.addr[2], + mac.addr[3], + mac.addr[4], + mac.addr[5]); +} + +const char * app_utils_submod_dir_to_string (pnet_submodule_dir_t direction) +{ + const char * s = ""; + + switch (direction) + { + case PNET_DIR_NO_IO: + s = "NO_IO"; + break; + case PNET_DIR_INPUT: + s = "INPUT"; + break; + case PNET_DIR_OUTPUT: + s = "OUTPUT"; + break; + case PNET_DIR_IO: + s = "INPUT_OUTPUT"; + break; + } + + return s; +} + +const char * app_utils_ioxs_to_string (pnet_ioxs_values_t ioxs) +{ + const char * s = ""; + switch (ioxs) + { + case PNET_IOXS_BAD: + s = "BAD"; + break; + case PNET_IOXS_GOOD: + s = "GOOD"; + break; + } + + return s; +} + +char * app_utils_get_subslot_string (const app_subslot_t * subslot) +{ + static char buf[40]; + snprintf ( + buf, + sizeof (buf), + "[%u,%u,\"%s\"]", + subslot->slot_nbr, + subslot->subslot_nbr, + subslot->submodule_name); + return buf; +} + +void app_utils_get_error_code_strings ( + uint16_t err_cls, + uint16_t err_code, + const char ** err_cls_str, + const char ** err_code_str) +{ + if (err_cls_str == NULL || err_cls_str == NULL) + { + return; + } + *err_cls_str = "Not decoded"; + *err_code_str = "Not decoded"; + + switch (err_cls) + { + case PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL: + *err_cls_str = "Real-Time Acyclic Protocol"; + switch (err_code) + { + case PNET_ERROR_CODE_2_ABORT_AR_CONSUMER_DHT_EXPIRED: + *err_code_str = "Device missed cyclic data " + "deadline, device terminated AR"; + break; + case PNET_ERROR_CODE_2_ABORT_AR_CMI_TIMEOUT: + *err_code_str = "Communication initialization " + "timeout, device terminated AR"; + break; + case PNET_ERROR_CODE_2_ABORT_AR_RELEASE_IND_RECEIVED: + *err_code_str = "AR release indication received"; + break; + case PNET_ERROR_CODE_2_ABORT_DCP_STATION_NAME_CHANGED: + *err_code_str = "DCP station name changed, " + "device terminated AR"; + break; + case PNET_ERROR_CODE_2_ABORT_DCP_RESET_TO_FACTORY: + *err_code_str = "DCP reset to factory or factory " + "reset, device terminated AR"; + break; + } + break; + + case PNET_ERROR_CODE_1_CTLDINA: + *err_cls_str = "CTLDINA = Name and IP assignment from controller"; + switch (err_code) + { + case PNET_ERROR_CODE_2_CTLDINA_ARP_MULTIPLE_IP_ADDRESSES: + *err_code_str = "Multiple users of same IP address"; + break; + } + break; + } +} +void app_utils_copy_ip_to_struct ( + pnet_cfg_ip_addr_t * destination_struct, + pnal_ipaddr_t ip) +{ + destination_struct->a = ((ip >> 24) & 0xFF); + destination_struct->b = ((ip >> 16) & 0xFF); + destination_struct->c = ((ip >> 8) & 0xFF); + destination_struct->d = (ip & 0xFF); +} + +const char * app_utils_dcontrol_cmd_to_string ( + pnet_control_command_t control_command) +{ + const char * s = NULL; + + switch (control_command) + { + case PNET_CONTROL_COMMAND_PRM_BEGIN: + s = "PRM_BEGIN"; + break; + case PNET_CONTROL_COMMAND_PRM_END: + s = "PRM_END"; + break; + case PNET_CONTROL_COMMAND_APP_RDY: + s = "APP_RDY"; + break; + case PNET_CONTROL_COMMAND_RELEASE: + s = "RELEASE"; + break; + default: + s = ""; + break; + } + + return s; +} + +const char * app_utils_event_to_string (pnet_event_values_t event) +{ + const char * s = ""; + + switch (event) + { + case PNET_EVENT_ABORT: + s = "PNET_EVENT_ABORT"; + break; + case PNET_EVENT_STARTUP: + s = "PNET_EVENT_STARTUP"; + break; + case PNET_EVENT_PRMEND: + s = "PNET_EVENT_PRMEND"; + break; + case PNET_EVENT_APPLRDY: + s = "PNET_EVENT_APPLRDY"; + break; + case PNET_EVENT_DATA: + s = "PNET_EVENT_DATA"; + break; + } + + return s; +} + +int app_utils_pnet_cfg_init_default (pnet_cfg_t * cfg) +{ + memset (cfg, 0, sizeof (pnet_cfg_t)); + + cfg->tick_us = APP_TICK_INTERVAL_US; + + /* Identification & Maintenance */ + + cfg->im_0_data.im_vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_VENDOR_ID); + cfg->im_0_data.im_vendor_id_lo = GET_LOW_BYTE (APP_GSDML_VENDOR_ID); + + cfg->im_0_data.im_hardware_revision = 1; + cfg->im_0_data.im_sw_revision_prefix = APP_GSDML_SW_REV_PREFIX; + cfg->im_0_data.im_sw_revision_functional_enhancement = PNET_VERSION_MAJOR; + cfg->im_0_data.im_sw_revision_bug_fix = PNET_VERSION_MINOR; + cfg->im_0_data.im_sw_revision_internal_change = PNET_VERSION_PATCH; + cfg->im_0_data.im_revision_counter = 0; /* Only 0 allowed according + to standard */ + cfg->im_0_data.im_profile_id = APP_GSDML_PROFILE_ID; + cfg->im_0_data.im_profile_specific_type = APP_GSDML_PROFILE_SPEC_TYPE; + cfg->im_0_data.im_version_major = 1; + cfg->im_0_data.im_version_minor = 1; + cfg->im_0_data.im_supported = APP_GSDML_IM_SUPPORTED; + strcpy (cfg->im_0_data.im_order_id, APP_GSDML_ORDER_ID); + strcpy (cfg->im_0_data.im_serial_number, "00001"); + strcpy (cfg->im_1_data.im_tag_function, "my function"); + strcpy (cfg->im_1_data.im_tag_location, "my location"); + strcpy (cfg->im_2_data.im_date, "2020-09-03 13:53"); + strcpy (cfg->im_3_data.im_descriptor, "my descriptor"); + strcpy (cfg->im_4_data.im_signature, ""); /* Functional safety */ + + /* Device configuration */ + cfg->device_id.vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_VENDOR_ID); + cfg->device_id.vendor_id_lo = GET_LOW_BYTE (APP_GSDML_VENDOR_ID); + cfg->device_id.device_id_hi = GET_HIGH_BYTE (APP_GSDML_DEVICE_ID); + cfg->device_id.device_id_lo = GET_LOW_BYTE (APP_GSDML_DEVICE_ID); + + cfg->oem_device_id.vendor_id_hi = 0xc0; + cfg->oem_device_id.vendor_id_lo = 0xff; + cfg->oem_device_id.device_id_hi = 0xee; + cfg->oem_device_id.device_id_lo = 0x01; + strcpy (cfg->product_name, APP_GSDML_PRODUCT_NAME); + + cfg->send_hello = true; + + /* Timing */ + cfg->min_device_interval = APP_GSDML_MIN_DEVICE_INTERVAL; + + /* Should be set by application as part of network configuration. */ + cfg->num_physical_ports = 1; + + snprintf ( + cfg->station_name, + sizeof (cfg->station_name), + APP_GSDML_DEFAULT_STATION_NAME); + + /* Diagnosis mechanism */ + /* Use "Extended channel diagnosis" instead of + * "Qualified channel diagnosis" format on the wire, + * as this is better supported by Wireshark. + */ + cfg->use_qualified_diagnosis = false; + +#if PNET_OPTION_DRIVER_ENABLE + cfg->driver_enable = false; + cfg->driver_config.mera.vcam_base_id = PNET_LAN9662_VCAM_BASE; + cfg->driver_config.mera.rtp_base_id = PNET_LAN9662_RTP_BASE; + cfg->driver_config.mera.wal_base_id = PNET_LAN9662_WAL_BASE; + cfg->driver_config.mera.ral_base_id = PNET_LAN9662_RAL_BASE; +#endif + + return 0; +} + +int app_utils_get_netif_namelist ( + const char * arg_str, + uint16_t max_port, + app_utils_netif_namelist_t * p_if_list, + uint16_t * p_num_ports) +{ + int ret = 0; + uint16_t i = 0; + uint16_t j = 0; + uint16_t if_index = 0; + uint16_t number_of_given_names = 1; + uint16_t if_list_size = max_port + 1; + char c; + + if (max_port == 0) + { + printf ("Error: max_port is 0.\n"); + return -1; + } + + memset (p_if_list, 0, sizeof (*p_if_list)); + c = arg_str[i++]; + while (c != '\0') + { + if (c != ',') + { + if (if_index < if_list_size) + { + p_if_list->netif[if_index].name[j++] = c; + } + } + else + { + if (if_index < if_list_size) + { + p_if_list->netif[if_index].name[j++] = '\0'; + j = 0; + if_index++; + } + number_of_given_names++; + } + + c = arg_str[i++]; + } + + if (max_port == 1 && number_of_given_names > 1) + { + printf ("Error: Only 1 network interface expected as max_port is 1.\n"); + return -1; + } + if (number_of_given_names == 2) + { + printf ("Error: It is illegal to give 2 interface names. Use 1, or one " + "more than the number of physical interfaces.\n"); + return -1; + } + if (number_of_given_names > max_port + 1) + { + printf ( + "Error: You have given %u interface names, but max is %u as " + "PNET_MAX_PHYSICAL_PORTS is %u.\n", + number_of_given_names, + max_port + 1, + max_port); + return -1; + } + + if (number_of_given_names == 1) + { + if (strlen (p_if_list->netif[0].name) == 0) + { + printf ("Error: Zero length network interface name.\n"); + return -1; + } + else + { + p_if_list->netif[1] = p_if_list->netif[0]; + *p_num_ports = 1; + } + } + else + { + for (i = 0; i < number_of_given_names; i++) + { + if (strlen (p_if_list->netif[i].name) == 0) + { + printf ("Error: Zero length network interface name (%d).\n", i); + return -1; + } + } + + *p_num_ports = number_of_given_names - 1; + } + + return ret; +} + +int app_utils_pnet_cfg_init_netifs ( + const char * netif_list_str, + app_utils_netif_namelist_t * if_list, + uint16_t * number_of_ports, + pnet_if_cfg_t * if_cfg) +{ + int ret = 0; + int i = 0; + pnal_ipaddr_t ip; + pnal_ipaddr_t netmask; + pnal_ipaddr_t gateway; + + ret = app_utils_get_netif_namelist ( + netif_list_str, + PNET_MAX_PHYSICAL_PORTS, + if_list, + number_of_ports); + if (ret != 0) + { + return ret; + } + if_cfg->main_netif_name = if_list->netif[0].name; + + for (i = 1; i <= *number_of_ports; i++) + { + if_cfg->physical_ports[i - 1].netif_name = if_list->netif[i].name; + } + + /* Read IP, netmask, gateway from operating system */ + ip = pnal_get_ip_address (if_cfg->main_netif_name); + netmask = pnal_get_netmask (if_cfg->main_netif_name); + gateway = pnal_get_gateway (if_cfg->main_netif_name); + + app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_addr, ip); + app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_gateway, gateway); + app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_mask, netmask); + + return ret; +} + +static void app_utils_print_mac_address (const char * netif_name) +{ + pnal_ethaddr_t pnal_mac_addr; + if (pnal_get_macaddress (netif_name, &pnal_mac_addr) == 0) + { + APP_LOG_INFO ( + "%02X:%02X:%02X:%02X:%02X:%02X\n", + pnal_mac_addr.addr[0], + pnal_mac_addr.addr[1], + pnal_mac_addr.addr[2], + pnal_mac_addr.addr[3], + pnal_mac_addr.addr[4], + pnal_mac_addr.addr[5]); + } + else + { + APP_LOG_ERROR ("Failed read mac address\n"); + } +} + +void app_utils_print_network_config ( + pnet_if_cfg_t * if_cfg, + uint16_t number_of_ports) +{ + uint16_t i; + char hostname_string[PNAL_HOSTNAME_MAX_SIZE]; /* Terminated string */ + + APP_LOG_INFO ("Management port: %s ", if_cfg->main_netif_name); + app_utils_print_mac_address (if_cfg->main_netif_name); + for (i = 1; i <= number_of_ports; i++) + { + APP_LOG_INFO ( + "Physical port [%u]: %s ", + i, + if_cfg->physical_ports[i - 1].netif_name); + + app_utils_print_mac_address (if_cfg->physical_ports[i - 1].netif_name); + } + + if (pnal_get_hostname (hostname_string) != 0) + { + hostname_string[0] = '\0'; + } + + APP_LOG_INFO ("Hostname: %s\n", hostname_string); + APP_LOG_INFO ( + "IP address: %u.%u.%u.%u\n", + if_cfg->ip_cfg.ip_addr.a, + if_cfg->ip_cfg.ip_addr.b, + if_cfg->ip_cfg.ip_addr.c, + if_cfg->ip_cfg.ip_addr.d); + APP_LOG_INFO ( + "Netmask: %u.%u.%u.%u\n", + if_cfg->ip_cfg.ip_mask.a, + if_cfg->ip_cfg.ip_mask.b, + if_cfg->ip_cfg.ip_mask.c, + if_cfg->ip_cfg.ip_mask.d); + APP_LOG_INFO ( + "Gateway: %u.%u.%u.%u\n", + if_cfg->ip_cfg.ip_gateway.a, + if_cfg->ip_cfg.ip_gateway.b, + if_cfg->ip_cfg.ip_gateway.c, + if_cfg->ip_cfg.ip_gateway.d); +} + +void app_utils_print_ioxs_change ( + app_subslot_t * subslot, + const char * ioxs_str, + uint8_t ioxs_current, + uint8_t ioxs_new) +{ + if (ioxs_current != ioxs_new) + { + if (ioxs_new == PNET_IOXS_BAD || ioxs_new == PNET_IOXS_GOOD) + { + APP_LOG_DEBUG ( + " %-40s PLC reports %s %s \n", + app_utils_get_subslot_string (subslot), + ioxs_str, + app_utils_ioxs_to_string (ioxs_new)); + } + else if (ioxs_new != PNET_IOXS_GOOD) + { + APP_LOG_WARNING ( + " %-40s PLC reports %s 0x%x Is it in STOP mode?\n", + app_utils_get_subslot_string (subslot), + ioxs_str, + ioxs_new); + } + } +} + +int app_utils_plug_module ( + app_api_t * p_api, + uint16_t slot_nbr, + uint32_t id, + const char * name) +{ + if (slot_nbr >= PNET_MAX_SLOTS) + { + return -1; + } + + p_api->slots[slot_nbr].module_id = id; + p_api->slots[slot_nbr].plugged = true; + p_api->slots[slot_nbr].name = name; + + return 0; +} + +int app_utils_pull_module (app_api_t * p_api, uint16_t slot_nbr) +{ + if (slot_nbr >= PNET_MAX_SLOTS) + { + return -1; + } + + p_api->slots[slot_nbr].plugged = false; + + return 0; +} + +app_subslot_t * app_utils_plug_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_ident, + const pnet_data_cfg_t * p_data_cfg, + const char * submodule_name, + app_utils_cyclic_callback cyclic_callback, + void * tag) +{ + uint16_t subslot_ix; + + if (slot_nbr >= PNET_MAX_SLOTS || p_api == NULL || p_data_cfg == NULL) + { + return NULL; + } + + for (subslot_ix = 0; subslot_ix < PNET_MAX_SUBSLOTS; subslot_ix++) + { + if (p_api->slots[slot_nbr].subslots[subslot_ix].used == false) + { + app_subslot_t * p_subslot = + &p_api->slots[slot_nbr].subslots[subslot_ix]; + + p_subslot->used = true; + p_subslot->plugged = true; + p_subslot->slot_nbr = slot_nbr; + p_subslot->subslot_nbr = subslot_nbr; + p_subslot->submodule_name = submodule_name; + p_subslot->submodule_id = submodule_ident; + p_subslot->data_cfg = *p_data_cfg; + p_subslot->cyclic_callback = cyclic_callback; + p_subslot->tag = tag; + p_subslot->iocs = 0; + return p_subslot; + } + } + + return NULL; +} + +int app_utils_pull_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr) +{ + app_subslot_t * p_subslot = NULL; + + if (slot_nbr >= PNET_MAX_SUBSLOTS || p_api == NULL) + { + return -1; + } + + p_subslot = app_utils_subslot_get (p_api, slot_nbr, subslot_nbr); + if (p_subslot != NULL) + { + memset (p_subslot, 0, sizeof (app_subslot_t)); + p_subslot->used = false; + return 0; + } + + return -1; +} + +/** + * Get subslot application information. + * @param p_appdata InOut: Application state. + * @param slot_nbr In: Slot number. + * @param subslot_nbr In: Subslot number. Range 0 - 0x9FFF. + * @return Reference to application subslot, + * NULL if subslot is not found/plugged. + */ +app_subslot_t * app_utils_subslot_get ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr) +{ + uint16_t subslot_ix; + + if (slot_nbr >= PNET_MAX_SLOTS || p_api == NULL) + { + return NULL; + } + + for (subslot_ix = 0; subslot_ix < PNET_MAX_SUBSLOTS; subslot_ix++) + { + if (p_api->slots[slot_nbr].subslots[subslot_ix].subslot_nbr == subslot_nbr) + { + return &p_api->slots[slot_nbr].subslots[subslot_ix]; + } + } + + return NULL; +} + +bool app_utils_subslot_is_input (const app_subslot_t * p_subslot) +{ + if ( + p_subslot != NULL && (p_subslot->data_cfg.data_dir == PNET_DIR_INPUT || + p_subslot->data_cfg.data_dir == PNET_DIR_IO)) + { + return true; + } + + return false; +} + +/** + * Return true if subslot is neither input or output. + * + * This is applies for DAP submodules/slots + * @param p_subslot In: Reference to subslot. + * @return true if subslot is input or input/output. + * false if not. + */ +bool app_utils_subslot_is_no_io (const app_subslot_t * p_subslot) +{ + if (p_subslot != NULL && p_subslot->data_cfg.data_dir == PNET_DIR_NO_IO) + { + return true; + } + + return false; +} + +/** + * Return true if subslot is output. + * + * @param p_subslot In: Reference to subslot. + * @return true if subslot is output or input/output, + * false if not. + */ +bool app_utils_subslot_is_output (const app_subslot_t * p_subslot) +{ + if ( + p_subslot != NULL && (p_subslot->data_cfg.data_dir == PNET_DIR_OUTPUT || + p_subslot->data_cfg.data_dir == PNET_DIR_IO)) + { + return true; + } + + return false; +} + +void app_utils_cyclic_data_poll (app_api_t * p_api) +{ + uint16_t slot; + uint16_t subslot; + app_subslot_t * p_subslot; + + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + for (subslot = 0; subslot < PNET_MAX_SUBSLOTS; subslot++) + { + p_subslot = &p_api->slots[slot].subslots[subslot]; + if (p_subslot->plugged && p_subslot->cyclic_callback != NULL) + { + p_subslot->cyclic_callback (p_subslot, p_subslot->tag); + } + } + } +} diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.h b/profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.h new file mode 100644 index 0000000..32b116c --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/app_utils.h @@ -0,0 +1,382 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef APP_UTILS_H +#define APP_UTILS_H + +/** + * @file + * @brief Application utilities and helper functions + * + * Functions for getting string representation of + * P-Net events, error codes and more. + * + * API, slot and subslot administration. + * + * Initialization of P-Net configuration from app_gsdml.h. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "osal.h" +#include "pnal.h" +#include + +typedef struct app_utils_netif_name +{ + char name[PNET_INTERFACE_NAME_MAX_SIZE]; +} app_utils_netif_name_t; + +typedef struct app_utils_netif_namelist +{ + app_utils_netif_name_t netif[PNET_MAX_PHYSICAL_PORTS + 1]; +} app_utils_netif_namelist_t; + +typedef struct app_subslot app_subslot_t; +typedef void (*app_utils_cyclic_callback) (app_subslot_t * subslot, void * tag); + +/** + * Information of submodule plugged into a subslot. + * Note that submodule data is not stored here but must + * be handled by the submodule implementation. + * All parameters are initialized by the app_utils_plug_submodule() + * function. + * The cyclic_callback is generated when app_utils_cyclic_data_poll() + * is called. Typically on the tick event in the main task. + * tag parameter is passed with the cyclic_callback and + * is typically a handle to a submodule on application. + */ +typedef struct app_subslot +{ + bool used; + bool plugged; + uint16_t slot_nbr; + uint16_t subslot_nbr; + uint32_t submodule_id; + const char * submodule_name; + pnet_data_cfg_t data_cfg; + uint8_t iocs; + uint8_t iops; + + app_utils_cyclic_callback cyclic_callback; + void * tag; +} app_subslot_t; + +/** + * Information of module plugged into + * slot and array of subslots for admin + * of submodules. + */ +typedef struct app_slot +{ + bool plugged; + uint32_t module_id; + const char * name; + app_subslot_t subslots[PNET_MAX_SUBSLOTS]; +} app_slot_t; + +/** + * Profinet API state + * Used to admin modules is plugged into + * slots and submodules into subslots. + */ +typedef struct app_api_t +{ + uint32_t api_id; + uint32_t arep; + app_slot_t slots[PNET_MAX_SLOTS]; +} app_api_t; + +/** + * Convert IP address to string + * @param ip In: IP address + * @param outputstring Out: Resulting string buffer. Should have size + * PNAL_INET_ADDRSTR_SIZE. + */ +void app_utils_ip_to_string (pnal_ipaddr_t ip, char * outputstring); + +/** + * Get string description of data direction + * @param direction In: Submodule data direction + * @return String representation of data direction + */ +const char * app_utils_submod_dir_to_string (pnet_submodule_dir_t direction); + +/** + * Get string description of pnio produced or consumer status + * @param ioxs In: Producer or consumer status (iops/iocs) + * @return String representation of ioxs (iops/iocs) + */ +const char * app_utils_ioxs_to_string (pnet_ioxs_values_t ioxs); + +/** + * Get string representation of application subslot in format + * [slot,subslot,module name] + * This function has an internal buffer for temp storage + * of the generated string. + * @param subslot In: Subslot + * @return String representation of subslot. + */ +char * app_utils_get_subslot_string (const app_subslot_t * subslot); + +/** + * Convert MAC address to string + * @param mac In: MAC address + * @param outputstring Out: Resulting string buffer. Should have size + * PNAL_ETH_ADDRSTR_SIZE. + */ +void app_utils_mac_to_string (pnet_ethaddr_t mac, char * outputstring); + +/** + * Convert error code to string format + * Only common error codes supported. + * Todo: Add rest of error codes. + * + * @param err_cls In: The error class. See PNET_ERROR_CODE_1_* + * @param err_code In: The error code. See PNET_ERROR_CODE_2_* + * @param err_cls_str Out: The error class string + * @param err_code_str Out: The error code string + */ +void app_utils_get_error_code_strings ( + uint16_t err_cls, + uint16_t err_code, + const char ** err_cls_str, + const char ** err_code_str); + +/** + * Copy an IP address (as an integer) to a struct + * @param destination_struct Out: Destination + * @param ip In: IP address + */ +void app_utils_copy_ip_to_struct ( + pnet_cfg_ip_addr_t * destination_struct, + pnal_ipaddr_t ip); + +/** + * Return a string representation of + * the given dcontrol command. + * @param event In: control_command + * @return A string representing the command + */ +const char * app_utils_dcontrol_cmd_to_string ( + pnet_control_command_t control_command); + +/** + * Return a string representation of the given event. + * @param event In: event + * @return A string representing the event + */ +const char * app_utils_event_to_string (pnet_event_values_t event); + +/** + * Get network configuration from a string + * defining a list of network interfaces examples: + * "eth0" or "br0,eth0,eth1" + * @param netif_list_str In: list of network ifs + * @param if_list Out: Array of network ifs + * @param number_of_ports Out: Number of ports + * @param if_cfg Out: P-Net network configuration + * @return 0 on success, -1 on error + */ +int app_utils_pnet_cfg_init_netifs ( + const char * netif_list_str, + app_utils_netif_namelist_t * if_list, + uint16_t * number_of_ports, + pnet_if_cfg_t * if_cfg); + +/** + * Parse a comma separated list of network interfaces and check + * that the number of interfaces match the PNET_MAX_PHYSICAL_PORTS + * configuration. + * + * For a single Ethernet interface, the \a arg_str should consist of + * one name. For two Ethernet interfaces, the \a arg_str should consist of + * three names, as we also need a bridge interface. + * + * Does only consider the number of comma separated names. No check of the + * names themselves are done. + * + * Examples: + * arg_str num_ports + * "eth0" 1 + * "eth0,eth1" error (We need a bridge as well) + * "br0,eth0,eth1" 2 + * + * @param arg_str In: Network interface list as comma separated, + * terminated string. For example "eth0" or + * "br0,eth0,eth1". + * @param max_port In: PNET_MAX_PHYSICAL_PORTS, passed as argument to + * allow test. + * @param p_if_list Out: List of network interfaces + * @param p_num_ports Out: Resulting number of physical ports + * @return 0 on success + * -1 on error + */ +int app_utils_get_netif_namelist ( + const char * arg_str, + uint16_t max_port, + app_utils_netif_namelist_t * p_if_list, + uint16_t * p_num_ports); + +/** + * Print network configuration using APP_LOG_INFO(). + * @param if_cfg In: Network configuration + * @param number_of_ports In: Number of used ports + */ +void app_utils_print_network_config ( + pnet_if_cfg_t * if_cfg, + uint16_t number_of_ports); + +/** + * Print iocs warning message if ioxs is changed. + * @param subslot In: Subslot + * @param ioxs_str In: String description Producer or Consumer + * @param ioxs_current In: Current status + * @param ioxs_new In: New status + */ +void app_utils_print_ioxs_change ( + app_subslot_t * subslot, + const char * ioxs_str, + uint8_t ioxs_current, + uint8_t ioxs_new); + +/** + * Init the p-net configuration to default values. + * Some values hardcoded, some picked from app_gsdml.h + * Network configuration not initialized. + * This means'.if_cfg' must be set by application. + * Use this function to init P-Net configuration before + * before passing config to app_init(). + * @param pnet_cfg Out: Configuration for use by p-net + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int app_utils_pnet_cfg_init_default (pnet_cfg_t * pnet_cfg); + +/** + * Plug application module. + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @param module_id In: Module identity + * @return 0 on success, -1 on error + */ +int app_utils_plug_module ( + app_api_t * p_api, + uint16_t slot_nbr, + uint32_t id, + const char * name); + +/** + * Pull any application module in given slot. + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @return 0 on success, -1 on error + */ +int app_utils_pull_module (app_api_t * p_api, uint16_t slot_nbr); + +/** + * Plug application submodule. + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @param submodule_id In: Submodule identity + * @param p_data_cfg In: Data configuration, + * direction, in and out sizes + * @param submodule_name In: Submodule name + * @param cyclic_callback In: Submodule data callback + * @param tag In: Tag passed in cyclic callback + * Typically application or + * submodule handle + * + * @return Reference to allocated subslot, + * NULL if no free subslot is available. This should + * never happen if application is aligned with p-net state. + */ +app_subslot_t * app_utils_plug_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t submodule_id, + const pnet_data_cfg_t * p_data_cfg, + const char * submodule_name, + app_utils_cyclic_callback cyclic_callback, + void * tag); + +/** + * Unplug any application submodule from given subslot. + * @param p_api InOut: API + * @param slot_nbr In: Slot number + * @param subslot_nbr In: Subslot number + * @return 0 on success, -1 on error. + */ +int app_utils_pull_submodule ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr); + +/** + * Generate data callback for all plugged + * submodules. The callback passed in + * app_utils_plug_submodule is used. + * @param p_api In: API + */ +void app_utils_cyclic_data_poll (app_api_t * p_api); + +/** + * Get subslot application information. + * + * @param p_appdata InOut: Application state. + * @param slot_nbr In: Slot number. + * @param subslot_nbr In: Subslot number. Range 0 - 0x9FFF. + * @return Reference to application subslot, + * NULL if subslot is not found/plugged. + */ +app_subslot_t * app_utils_subslot_get ( + app_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr); + +/** + * Return true if subslot is input. + * @param p_subslot In: Reference to subslot. + * @return true if subslot is input or input/output. + * false if not. + */ +bool app_utils_subslot_is_input (const app_subslot_t * p_subslot); + +/** + * Return true if subslot is neither input or output. + * This is applies for DAP submodules/slots + * @param p_subslot In: Reference to subslot. + * @return true if subslot is input or input/output. + * false if not. + */ +bool app_utils_subslot_is_no_io (const app_subslot_t * p_subslot); + +/** + * Return true if subslot is output. + * @param p_subslot In: Reference to subslot. + * @return true if subslot is output or input/output, + * false if not. + */ +bool app_utils_subslot_is_output (const app_subslot_t * p_subslot); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_UTILS_H */ diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.c b/profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.c new file mode 100644 index 0000000..80e58c6 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.c @@ -0,0 +1,1235 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" +#include "app_utils.h" +#include "app_gsdml.h" +#include "app_data.h" +#include "app_log.h" +#include "osal.h" +#include "pnal.h" +#include +#include "pnet_lan9662_api.h" + +#include +#include +#include + +/* Events handled by main task */ +#define APP_EVENT_READY_FOR_DATA BIT (0) +#define APP_EVENT_TIMER BIT (1) +#define APP_EVENT_ALARM BIT (2) +#define APP_EVENT_ABORT BIT (15) + +/* Defines used for alarm demo functionality */ +#define CHANNEL_ERRORTYPE_SHORT_CIRCUIT 0x0001 +#define CHANNEL_ERRORTYPE_LINE_BREAK 0x0006 +#define CHANNEL_ERRORTYPE_DATA_TRANSMISSION_IMPOSSIBLE 0x8000 +#define CHANNEL_ERRORTYPE_NETWORK_COMPONENT_FUNCTION_MISMATCH 0x8008 +#define EXTENDED_CHANNEL_ERRORTYPE_FRAME_DROPPED 0x8000 +#define EXTENDED_CHANNEL_ERRORTYPE_MAUTYPE_MISMATCH 0x8001 +#define EXTENDED_CHANNEL_ERRORTYPE_LINE_DELAY_MISMATCH 0x8002 + +#define APP_ALARM_USI 0x0010 +#define APP_DIAG_CHANNEL_NUMBER 4 +#define APP_DIAG_CHANNEL_DIRECTION PNET_DIAG_CH_PROP_DIR_INPUT +#define APP_DIAG_CHANNEL_NUMBER_OF_BITS PNET_DIAG_CH_PROP_TYPE_1_BIT +#define APP_DIAG_CHANNEL_SEVERITY PNET_DIAG_CH_PROP_MAINT_FAULT +#define APP_DIAG_CHANNEL_ERRORTYPE CHANNEL_ERRORTYPE_SHORT_CIRCUIT +#define APP_DIAG_CHANNEL_ADDVALUE_A 0 +#define APP_DIAG_CHANNEL_ADDVALUE_B 1234 +#define APP_DIAG_CHANNEL_EXTENDED_ERRORTYPE 0 +#define APP_DIAG_CHANNEL_QUAL_SEVERITY 0 /* Not used (Max one bit set) */ + +typedef struct app_data_t +{ + pnet_t * net; + + /* P-Net configuration passed in app_init(). */ + pnet_cfg_t * pnet_cfg; + + /* Application api for administration of plugged + *(sub)modules and connection state. + */ + app_api_t main_api; + + os_timer_t * main_timer; + os_event_t * main_events; + + bool alarm_allowed; + pnet_alarm_argument_t alarm_arg; + uint8_t alarm_payload[APP_GSDML_ALARM_PAYLOAD_SIZE]; + + uint32_t arep_for_appl_ready; + uint32_t appl_ready_delay_count; + bool appl_ready_wait; + + uint32_t process_data_tick_counter; + + app_mode_t mode; + +} app_data_t; + +static void app_plug_dap (app_data_t * app, uint16_t number_of_ports); +static int app_set_initial_data_and_ioxs (app_data_t * app); +static void app_cyclic_data_callback (app_subslot_t * subslot, void * tag); + +app_data_t app_state; + +pnet_t * app_get_pnet_instance (app_data_t * app) +{ + if (app != NULL) + { + return app->net; + } + return NULL; +} + +app_data_t * app_init (pnet_cfg_t * pnet_cfg, const app_args_t * app_args) +{ + app_data_t * app = &app_state; + + app->alarm_allowed = true; + app->main_api.arep = UINT32_MAX; + app->pnet_cfg = pnet_cfg; + +#if PNET_OPTION_DRIVER_LAN9662 + app->mode = app_args->mode; + switch (app->mode) + { + case MODE_HW_OFFLOAD_CPU: + APP_LOG_INFO ("Application RTE mode \"cpu\"\n"); + pnet_cfg->driver_enable = true; + break; + case MODE_HW_OFFLOAD_FULL: + APP_LOG_INFO ("Application RTE mode \"full\"\n"); + pnet_cfg->driver_enable = true; + break; + case MODE_HW_OFFLOAD_NONE: + APP_LOG_INFO ("Application RTE mode \"none\"\n"); + pnet_cfg->driver_enable = false; + break; + default: + APP_LOG_ERROR ("Application mode undefined\n"); + pnet_cfg->driver_enable = false; + break; + } +#else + app->mode = MODE_HW_OFFLOAD_NONE; +#endif + + app_data_init (app->mode); + + app->net = pnet_init (app->pnet_cfg); + + if (app->net == NULL) + { + return NULL; + } + + return app; +} + +static void main_timer_tick (os_timer_t * timer, void * arg) +{ + app_data_t * app = (app_data_t *)arg; + + os_event_set (app->main_events, APP_EVENT_TIMER); +} + +int app_start (app_data_t * app, app_run_in_separate_task_t task_config) +{ + if (app == NULL) + { + return -1; + } + + app->main_events = os_event_create(); + if (app->main_events == NULL) + { + return -1; + } + + app->main_timer = os_timer_create ( + APP_TICK_INTERVAL_US, + main_timer_tick, + (void *)app, + false); + + if (app->main_timer == NULL) + { + os_event_destroy (app->main_events); + return -1; + } + + if (task_config == RUN_IN_SEPARATE_THREAD) + { + os_thread_create ( + "p-net_sample_app", + APP_MAIN_THREAD_PRIORITY, + APP_MAIN_THREAD_STACKSIZE, + app_loop_forever, + (void *)app); + } + + os_timer_start (app->main_timer); + + return 0; +} + +static void app_set_outputs_default_value (void) +{ + // APP_LOG_DEBUG ("Setting outputs to default values.\n"); + app_data_set_default_outputs(); +} + +/*********************************** Callbacks ********************************/ + +static int app_connect_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ("PLC connect indication. AREP: %u\n", arep); + /* + * Handle the request on an application level. + * This is a very simple application which does not need to handle anything. + * All the needed information is in the AR data structure. + */ + + return 0; +} + +static int app_release_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ("PLC release (disconnect) indication. AREP: %u\n", arep); + + app_set_outputs_default_value(); + + return 0; +} + +static int app_dcontrol_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_control_command_t control_command, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ( + "PLC dcontrol message. AREP: %u Command: %s\n", + arep, + app_utils_dcontrol_cmd_to_string (control_command)); + + return 0; +} + +static int app_ccontrol_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ( + "PLC ccontrol message confirmation. AREP: %u Status codes: %d " + "%d %d %d\n", + arep, + p_result->pnio_status.error_code, + p_result->pnio_status.error_decode, + p_result->pnio_status.error_code_1, + p_result->pnio_status.error_code_2); + + return 0; +} + +static int app_write_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t idx, + uint16_t sequence_number, + uint16_t write_length, + const uint8_t * p_write_data, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ( + "PLC write record indication.\n" + " AREP: %u API: %u Slot: %2u Subslot: %u Index: %u Sequence: %2u " + "Length: %u\n", + arep, + api, + slot_nbr, + subslot_nbr, + (unsigned)idx, + sequence_number, + write_length); + + return 0; +} + +static int app_read_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t idx, + uint16_t sequence_number, + uint8_t ** pp_read_data, + uint16_t * p_read_length, + pnet_result_t * p_result) +{ + APP_LOG_DEBUG ( + "PLC read record indication.\n" + " AREP: %u API: %u Slot: %2u Subslot: %u Index: %u Sequence: %2u Max " + "length: %u\n", + arep, + api, + slot_nbr, + subslot_nbr, + (unsigned)idx, + sequence_number, + (unsigned)*p_read_length); + + return 0; +} + +static int app_state_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_event_values_t state) +{ + uint16_t err_cls = 0; /* Error code 1 */ + uint16_t err_code = 0; /* Error code 2 */ + const char * error_class_description = ""; + const char * error_code_description = ""; + + app_data_t * app = (app_data_t *)arg; + + APP_LOG_DEBUG ( + "Event indication %s AREP: %u\n", + app_utils_event_to_string (state), + arep); + + if (state == PNET_EVENT_ABORT) + { + if (pnet_get_ar_error_codes (net, arep, &err_cls, &err_code) == 0) + { + app_utils_get_error_code_strings ( + err_cls, + err_code, + &error_class_description, + &error_code_description); + APP_LOG_DEBUG ( + " Error class: 0x%02x %s \n" + " Error code: 0x%02x %s \n", + (unsigned)err_cls, + error_class_description, + (unsigned)err_code, + error_code_description); + } + else + { + APP_LOG_DEBUG (" No error status available\n"); + } + /* Set output values */ + app_set_outputs_default_value(); + + /* Only abort AR with correct session key */ + os_event_set (app->main_events, APP_EVENT_ABORT); + } + else if (state == PNET_EVENT_PRMEND) + { + if (app->main_api.arep != UINT32_MAX) + { + APP_LOG_WARNING ("Warning - AREP out of sync\n"); + } + app->main_api.arep = arep; + + app_set_initial_data_and_ioxs (app); + + (void)pnet_set_provider_state (net, true); + + /* Send application ready at next tick + Do not call pnet_application_ready() here as it will affect + the internal stack states */ + app->arep_for_appl_ready = arep; + os_event_set (app->main_events, APP_EVENT_READY_FOR_DATA); + } + else if (state == PNET_EVENT_DATA) + { + APP_LOG_DEBUG ("Cyclic data transmission started\n\n"); + } + + return 0; +} + +static int app_reset_ind ( + pnet_t * net, + void * arg, + bool should_reset_application, + uint16_t reset_mode) +{ + APP_LOG_DEBUG ( + "PLC reset indication. Application reset mandatory: %u Reset mode: %d\n", + should_reset_application, + reset_mode); + + return 0; +} + +static int app_signal_led_ind (pnet_t * net, void * arg, bool led_state) +{ + APP_LOG_INFO ("Profinet signal LED indication. New state: %u\n", led_state); + + app_set_led (APP_PROFINET_SIGNAL_LED_ID, led_state); + return 0; +} + +static int app_exp_module_ind ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint32_t module_ident) +{ + int ret = -1; + int result = 0; + app_data_t * app = (app_data_t *)arg; + const char * module_name = "unknown"; + const app_gsdml_module_t * module_config; + + APP_LOG_DEBUG ("Module plug indication API %d\n"); + + if (slot >= PNET_MAX_SLOTS) + { + APP_LOG_ERROR ( + "Unsupported slot number: %u It should be less than %u\n", + slot, + PNET_MAX_SLOTS); + return -1; + } + + module_config = app_gsdml_get_module_cfg (module_ident); + if (module_config == NULL) + { + APP_LOG_ERROR (" Module ID %08x not found.\n", (unsigned)module_ident); + /* + * Needed to pass Behavior scenario 2 + */ + APP_LOG_DEBUG (" Plug expected module anyway\n"); + } + else + { + module_name = module_config->name; + } + + APP_LOG_DEBUG (" [%u] Pull old module\n", slot); + result = pnet_pull_module (net, api, slot); + + if (result == 0) + { + app_utils_pull_module (&app->main_api, slot); + } + + APP_LOG_DEBUG ( + " [%u] Plug module. Module ID: 0x%x \"%s\"\n", + slot, + (unsigned)module_ident, + module_name); + + ret = pnet_plug_module (net, api, slot, module_ident); + if (ret == 0) + { + app_utils_plug_module (&app->main_api, slot, module_ident, module_name); + } + else + { + APP_LOG_ERROR ( + " [%u] Plug module failed. Ret: %u Module ID: 0x%x \" \"\n", + slot, + ret, + (unsigned)module_ident, + module_name); + } + + return ret; +} + +static int app_exp_submodule_ind ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_id, + uint32_t submodule_id, + const pnet_data_cfg_t * p_exp_data) +{ + int ret = -1; + int result = 0; + pnet_data_cfg_t data_cfg = {0}; + app_data_t * app = (app_data_t *)arg; + const app_gsdml_submodule_t * submod_cfg; + const char * name = "Unsupported"; + app_utils_cyclic_callback cyclic_data_callback = NULL; + app_subslot_t * p_subslot; + + APP_LOG_DEBUG ("Submodule plug indication API %u\n", api); + + submod_cfg = app_gsdml_get_submodule_cfg (submodule_id); + if (submod_cfg != NULL) + { + data_cfg.data_dir = submod_cfg->data_dir; + data_cfg.insize = submod_cfg->insize; + data_cfg.outsize = submod_cfg->outsize; + name = submod_cfg->name; + + if (data_cfg.insize > 0 || data_cfg.outsize > 0) + { + cyclic_data_callback = app_cyclic_data_callback; + } + } + else + { + APP_LOG_WARNING ( + " [%u,%u] Submodule ID 0x%x in Module ID 0x%x not found \n", + slot, + subslot, + (unsigned)submodule_id, + (unsigned)module_id); + + /* + * Needed for behavior scenario 2 to pass. + * Iops will be set to bad for this subslot + */ + APP_LOG_WARNING ( + " [%u,%u] Plug expected submodule anyway\n", + slot, + subslot); + + data_cfg.data_dir = p_exp_data->data_dir; + data_cfg.insize = p_exp_data->insize; + data_cfg.outsize = p_exp_data->outsize; + } + + APP_LOG_DEBUG (" [%u,%u] Pull old submodule.\n", slot, subslot); + + result = pnet_pull_submodule (net, api, slot, subslot); + if (result == 0) + { + app_utils_pull_submodule (&app->main_api, slot, subslot); + } + + APP_LOG_DEBUG ( + " [%u,%u] Plug submodule. Submodule ID: 0x%x Data Dir: %s In: %u Out: " + "%u \"%s\"\n", + slot, + subslot, + (unsigned)submodule_id, + app_utils_submod_dir_to_string (data_cfg.data_dir), + data_cfg.insize, + data_cfg.outsize, + name); + + if ( + data_cfg.data_dir != p_exp_data->data_dir || + data_cfg.insize != p_exp_data->insize || + data_cfg.outsize != p_exp_data->outsize) + { + APP_LOG_WARNING ( + " [%u,%u] Warning expected Data Dir: %s In: %u Out: %u\n", + slot, + subslot, + app_utils_submod_dir_to_string (p_exp_data->data_dir), + p_exp_data->insize, + p_exp_data->outsize); + } + ret = pnet_plug_submodule ( + net, + api, + slot, + subslot, + module_id, + submodule_id, + data_cfg.data_dir, + data_cfg.insize, + data_cfg.outsize); + + if (ret == 0) + { + p_subslot = app_utils_plug_submodule ( + &app->main_api, + slot, + subslot, + submodule_id, + &data_cfg, + name, + cyclic_data_callback, + app); + + /** + * Setup the RTE configuration for submodules data provided by the FPGA. + */ + + if (app->mode == MODE_HW_OFFLOAD_FULL) + { + const uint8_t * default_data; + uint16_t fpga_address; + + if ( + app_data_get_fpga_info_by_id ( + submodule_id, + &fpga_address, + &default_data) == 0) + { + pnet_mera_rte_data_cfg_t rte_cfg = { + .type = PNET_MERA_DATA_TYPE_QSPI, + .address = fpga_address, + .default_data = default_data}; + + APP_LOG_INFO ( + " %-40s Set RTE QSPI address 0x%x\n", + app_utils_get_subslot_string (p_subslot), + rte_cfg.address); + + if (data_cfg.insize > 0) + { + if ( + pnet_mera_input_set_rte_config ( + app->net, + APP_GSDML_API, + slot, + subslot, + &rte_cfg) != 0) + { + APP_LOG_ERROR ( + " %-40s RTE configuration failed\n", + app_utils_get_subslot_string (p_subslot)); + } + } + else + { + if ( + pnet_mera_output_set_rte_config ( + app->net, + APP_GSDML_API, + slot, + subslot, + &rte_cfg) != 0) + { + APP_LOG_ERROR ( + " %-40s RTE configuration failed\n", + app_utils_get_subslot_string (p_subslot)); + } + } + } + } + } + else + { + APP_LOG_ERROR ( + " [%u,%u] Plug submodule failed. Ret: %u\n", + slot, + subslot, + ret); + } + + return ret; +} + +static int app_new_data_status_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t crep, + uint8_t changes, + uint8_t data_status) +{ + bool is_running = data_status & BIT (PNET_DATA_STATUS_BIT_PROVIDER_STATE); + bool is_valid = data_status & BIT (PNET_DATA_STATUS_BIT_DATA_VALID); + + APP_LOG_DEBUG ( + "Data status indication. AREP: %u Data status changes: 0x%02x " + "Data status: 0x%02x\n", + arep, + changes, + data_status); + APP_LOG_DEBUG ( + " %s, %s, %s, %s, %s\n", + is_running ? "Run" : "Stop", + is_valid ? "Valid" : "Invalid", + (data_status & BIT (PNET_DATA_STATUS_BIT_STATE)) ? "Primary" : "Backup", + (data_status & BIT (PNET_DATA_STATUS_BIT_STATION_PROBLEM_INDICATOR)) + ? "Normal operation" + : "Problem", + (data_status & BIT (PNET_DATA_STATUS_BIT_IGNORE)) + ? "Ignore data status" + : "Evaluate data status"); + + if (is_running == false || is_valid == false) + { + app_set_outputs_default_value(); + } + + return 0; +} + +static int app_alarm_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_arg, + uint16_t data_len, + uint16_t data_usi, + const uint8_t * p_data) +{ + app_data_t * app = (app_data_t *)arg; + + APP_LOG_DEBUG ( + "Alarm indication. AREP: %u API: %d Slot: %d Subslot: %d " + "Type: %d Seq: %d Length: %d USI: %d\n", + arep, + p_alarm_arg->api_id, + p_alarm_arg->slot_nbr, + p_alarm_arg->subslot_nbr, + p_alarm_arg->alarm_type, + p_alarm_arg->sequence_number, + data_len, + data_usi); + + app->alarm_arg = *p_alarm_arg; + os_event_set (app->main_events, APP_EVENT_ALARM); + + return 0; +} + +static int app_alarm_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status) +{ + app_data_t * app = (app_data_t *)arg; + + APP_LOG_DEBUG ( + "PLC alarm confirmation. AREP: %u Status code %u, " + "%u, %u, %u\n", + arep, + p_pnio_status->error_code, + p_pnio_status->error_decode, + p_pnio_status->error_code_1, + p_pnio_status->error_code_2); + + app->alarm_allowed = true; + + return 0; +} + +static int app_alarm_ack_cnf (pnet_t * net, void * arg, uint32_t arep, int res) +{ + APP_LOG_DEBUG ( + "PLC alarm ACK confirmation. AREP: %u Result: " + "%d\n", + arep, + res); + + return 0; +} + +/** + * Plug all DAP (sub)modules + * Use existing callback functions to plug the (sub-)modules + * @param app InOut: Application handle + * @param number_of_ports In: Number of active ports + */ +static void app_plug_dap (app_data_t * app, uint16_t number_of_ports) +{ + const pnet_data_cfg_t cfg_dap_data = { + .data_dir = PNET_DIR_NO_IO, + .insize = 0, + .outsize = 0, + }; + + APP_LOG_DEBUG ("Plug DAP module and its submodules\n"); + + app_exp_module_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_MOD_DAP_IDENT); + + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_IDENT, + &cfg_dap_data); + + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_IDENT, + &cfg_dap_data); + + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_1_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT, + &cfg_dap_data); + + if (number_of_ports >= 2) + { + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_2_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_2_IDENT, + &cfg_dap_data); + } + + if (number_of_ports >= 3) + { + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_3_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_3_IDENT, + &cfg_dap_data); + } + + if (number_of_ports >= 4) + { + app_exp_submodule_ind ( + app->net, + app, + APP_GSDML_API, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_4_IDENT, + PNET_MOD_DAP_IDENT, + PNET_SUBMOD_DAP_INTERFACE_1_PORT_4_IDENT, + &cfg_dap_data); + } +} + +/** + * Send application ready to the PLC + * @param net InOut: p-net stack instance + * @param arep In: Arep + */ +static void app_handle_send_application_ready (pnet_t * net, uint32_t arep) +{ + int ret = -1; + + APP_LOG_DEBUG ( + "Application will signal that it is ready for data, for " + "AREP %u.\n", + arep); + + ret = pnet_application_ready (net, arep); + if (ret != 0) + { + APP_LOG_ERROR ( + "Error returned when application telling that it is ready for " + "data. Have you set IOCS or IOPS for all subslots?\n"); + } + + /* When the PLC sends a confirmation to this message, the + pnet_ccontrol_cnf() callback will be triggered. */ +} + +/** + * Send alarm ACK to the PLC + * + * @param net InOut: p-net stack instance + * @param arep In: Arep + * @param p_alarm_arg In: Alarm argument (slot, subslot etc) + */ +static void app_handle_send_alarm_ack ( + pnet_t * net, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_arg) +{ + pnet_pnio_status_t pnio_status = {0, 0, 0, 0}; + int ret; + + ret = pnet_alarm_send_ack (net, arep, p_alarm_arg, &pnio_status); + if (ret != 0) + { + APP_LOG_DEBUG ("Error when sending alarm ACK. Error: %d\n", ret); + } + else + { + APP_LOG_DEBUG ("Alarm ACK sent\n"); + } +} + +/** + * Handle cyclic data for all plugged submodules + * Data is read from / written to the app_data file + * which handles the data and update the physical + * input /outputs. + * + * For subslots with mapped shared memory inputs, the + * shared memory is read and the data is passed to the + * stack. + * + * For subslots with mapped shared memory outputs, the + * current output data is fetched from the stack and written to + * mapped shared memory. + * + * @param subslot In: Subslot reference + * @param tag In: Application handle + */ +static void app_cyclic_data_callback (app_subslot_t * subslot, void * tag) +{ + app_data_t * app = (app_data_t *)tag; + uint8_t iops = PNET_IOXS_BAD; + uint8_t iocs = PNET_IOXS_BAD; + uint8_t * indata; + uint16_t indata_size; + bool outdata_updated; + uint16_t outdata_length; + uint8_t outdata_iops; + uint8_t * outdata; + + if (app == NULL) + { + APP_LOG_ERROR ("Application tag not set in subslot?\n"); + return; + } + + if (subslot->data_cfg.insize > 0) + { + indata = + app_data_get_input_data (subslot->submodule_id, &indata_size, &iops); + + if (indata != NULL) + { + iops = PNET_IOXS_GOOD; + } + + (void)pnet_input_set_data_and_iops ( + app->net, + APP_GSDML_API, + subslot->slot_nbr, + subslot->subslot_nbr, + indata, + indata_size, + iops); + + (void)pnet_input_get_iocs ( + app->net, + APP_GSDML_API, + subslot->slot_nbr, + subslot->subslot_nbr, + &iocs); + + app_utils_print_ioxs_change ( + subslot, + "Consumer Status (IOCS)", + subslot->iocs, + iocs); + subslot->iocs = iocs; + } + + if (subslot->data_cfg.outsize > 0) + { + outdata = app_data_get_output_data_buffer ( + subslot->submodule_id, + &outdata_length); + + pnet_output_get_data_and_iops ( + app->net, + APP_GSDML_API, + subslot->slot_nbr, + subslot->subslot_nbr, + &outdata_updated, + outdata, + &outdata_length, + &outdata_iops); + + app_utils_print_ioxs_change ( + subslot, + "Provider Status (IOPS)", + subslot->iops, + outdata_iops); + subslot->iops = outdata_iops; + + if (outdata_length != subslot->data_cfg.outsize) + { + APP_LOG_ERROR ( + " %-40s Wrong outputdata length %u (%u)\n", + app_utils_get_subslot_string (subslot), + outdata_length, + subslot->data_cfg.outsize); + app_set_outputs_default_value(); + } + else if (outdata_iops == PNET_IOXS_GOOD) + { + /* Set output data for submodule */ + app_data_set_output_data ( + subslot->submodule_id, + outdata, + outdata_length); + } + else + { + app_set_outputs_default_value(); + } + } +} + +/** + * Set initial input data, provider and consumer status for a subslot. + * + * @param app In: Application handle + */ +static int app_set_initial_data_and_ioxs (app_data_t * app) +{ + int ret; + uint8_t iops; + uint16_t slot; + uint16_t subslot; + const app_subslot_t * p_subslot; + uint8_t * indata; + uint16_t indata_size; + + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + for (subslot = 0; subslot < PNET_MAX_SUBSLOTS; subslot++) + { + p_subslot = &app->main_api.slots[slot].subslots[subslot]; + if (p_subslot->plugged) + { + iops = PNET_IOXS_GOOD; + if ( + p_subslot->data_cfg.insize > 0 || + p_subslot->data_cfg.data_dir == PNET_DIR_NO_IO) + { + indata = NULL; + indata_size = p_subslot->data_cfg.insize; + + if (p_subslot->data_cfg.insize > 0) + { + indata = app_data_get_input_data ( + p_subslot->submodule_id, + &indata_size, + &iops); + } + + ret = pnet_input_set_data_and_iops ( + app->net, + APP_GSDML_API, + p_subslot->slot_nbr, + p_subslot->subslot_nbr, + indata, + indata_size, + iops); + + /* + * If a submodule is still plugged but not used in current + * AR setting the data and iops will fail. This is not a + * problem. Log message below will only be printed for + * active submodules. + */ + if (ret == 0) + { + APP_LOG_DEBUG ( + " %-40s Set input data and IOPS. Size: %d " + "IOPS: %s\n", + app_utils_get_subslot_string (p_subslot), + p_subslot->data_cfg.insize, + app_utils_ioxs_to_string (iops)); + } + } + + if (p_subslot->data_cfg.outsize > 0) + { + ret = pnet_output_set_iocs ( + app->net, + APP_GSDML_API, + p_subslot->slot_nbr, + p_subslot->subslot_nbr, + PNET_IOXS_GOOD); + + if (ret == 0) + { + APP_LOG_DEBUG ( + " %-40s Set output IOCS: %s\n", + app_utils_get_subslot_string (p_subslot), + app_utils_ioxs_to_string (PNET_IOXS_GOOD)); + } + } + } + } + } + return 0; +} + +/** + * Send and receive cyclic / process data + * + * Generate callback for all plugged submodules + * the sample application handles all submodules + * in the app_cyclic_data_callback() function. + * A complex device may register separate callbacks + * for each submodule. + * + * @param app In: Application handle + */ +static void app_handle_cyclic_data (app_data_t * app) +{ + /* For the sample application cyclic data is updated + * with a period defined by APP_TICKS_UPDATE_DATA + */ + app->process_data_tick_counter++; + if (app->process_data_tick_counter < APP_TICKS_UPDATE_DATA) + { + return; + } + app->process_data_tick_counter = 0; + + app_utils_cyclic_data_poll (&app->main_api); +} + +void app_pnet_cfg_init_default (pnet_cfg_t * pnet_cfg) +{ + app_utils_pnet_cfg_init_default (pnet_cfg); + + pnet_cfg->state_cb = app_state_ind; + pnet_cfg->connect_cb = app_connect_ind; + pnet_cfg->release_cb = app_release_ind; + pnet_cfg->dcontrol_cb = app_dcontrol_ind; + pnet_cfg->ccontrol_cb = app_ccontrol_cnf; + pnet_cfg->read_cb = app_read_ind; + pnet_cfg->write_cb = app_write_ind; + pnet_cfg->exp_module_cb = app_exp_module_ind; + pnet_cfg->exp_submodule_cb = app_exp_submodule_ind; + pnet_cfg->new_data_status_cb = app_new_data_status_ind; + pnet_cfg->alarm_ind_cb = app_alarm_ind; + pnet_cfg->alarm_cnf_cb = app_alarm_cnf; + pnet_cfg->alarm_ack_cnf_cb = app_alarm_ack_cnf; + pnet_cfg->reset_cb = app_reset_ind; + pnet_cfg->signal_led_cb = app_signal_led_ind; + + pnet_cfg->cb_arg = (void *)&app_state; +} + +void app_loop_forever (void * arg) +{ + app_data_t * app = (app_data_t *)arg; + uint32_t mask = APP_EVENT_READY_FOR_DATA | APP_EVENT_TIMER | + APP_EVENT_ALARM | APP_EVENT_ABORT; + uint32_t flags = 0; + + app->main_api.arep = UINT32_MAX; + + app_set_led (APP_DATA_LED_ID, false); + app_plug_dap (app, app->pnet_cfg->num_physical_ports); + APP_LOG_INFO ("Waiting for PLC connect request\n\n"); + + /* Main event loop */ + for (;;) + { + os_event_wait (app->main_events, mask, &flags, OS_WAIT_FOREVER); + if (flags & APP_EVENT_READY_FOR_DATA) + { + /* Delay the application ready event to + * allow the RTE to start sending frames. + */ + app->appl_ready_delay_count = 256; + app->appl_ready_wait = true; + os_event_clr (app->main_events, APP_EVENT_READY_FOR_DATA); + } + else if (flags & APP_EVENT_ALARM) + { + os_event_clr (app->main_events, APP_EVENT_ALARM); + + app_handle_send_alarm_ack ( + app->net, + app->main_api.arep, + &app->alarm_arg); + } + else if (flags & APP_EVENT_TIMER) + { + os_event_clr (app->main_events, APP_EVENT_TIMER); + + if (app->appl_ready_wait) + { + /* Delayed application ready event */ + if (app->appl_ready_delay_count == 0) + { + app->appl_ready_wait = false; + app_handle_send_application_ready ( + app->net, + app->arep_for_appl_ready); + } + else + { + app->appl_ready_delay_count--; + } + } + + if (app->main_api.arep != UINT32_MAX) + { + /* Todo this operation should be split + * and inputs handled before pnet_handle_periodic is + * called and outputs after.*/ + app_handle_cyclic_data (app); + } + + /* Run p-net stack */ + pnet_handle_periodic (app->net); + } + else if (flags & APP_EVENT_ABORT) + { + os_event_clr (app->main_events, APP_EVENT_ABORT); + + app->main_api.arep = UINT32_MAX; + app->alarm_allowed = true; + APP_LOG_DEBUG ("Connection closed\n"); + APP_LOG_DEBUG ("Waiting for PLC connect request\n\n"); + } + } +} diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.h b/profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.h new file mode 100644 index 0000000..decdf76 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/sampleapp_common.h @@ -0,0 +1,148 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef SAMPLEAPP_COMMON_H +#define SAMPLEAPP_COMMON_H + +#include "osal.h" +#include "pnal.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define APP_TICK_INTERVAL_US 1000 /* 1 ms */ + +/* Thread configuration for targets where sample + * event loop is run in a separate thread (not main). + * This applies for linux sample app implementation. + */ +#define APP_MAIN_THREAD_PRIORITY 15 +#define APP_MAIN_THREAD_STACKSIZE 4096 /* bytes */ + +#define APP_DATA_LED_ID 1 +#define APP_PROFINET_SIGNAL_LED_ID 2 + +#define APP_TICKS_UPDATE_DATA 100 + +typedef enum +{ + MODE_HW_OFFLOAD_NONE = 0, + MODE_HW_OFFLOAD_CPU, + MODE_HW_OFFLOAD_FULL, +} app_mode_t; + +typedef struct app_args +{ + char path_button1[PNET_MAX_FILE_FULLPATH_SIZE]; /** Terminated string */ + char path_button2[PNET_MAX_FILE_FULLPATH_SIZE]; /** Terminated string */ + char path_storage_directory[PNET_MAX_DIRECTORYPATH_SIZE]; /** Terminated */ + char station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated string */ + char eth_interfaces + [PNET_INTERFACE_NAME_MAX_SIZE * (PNET_MAX_PHYSICAL_PORTS + 1) + + PNET_MAX_PHYSICAL_PORTS]; /** Terminated string */ + int verbosity; + int show; + bool factory_reset; + bool remove_files; + app_mode_t mode; +} app_args_t; + +typedef enum +{ + RUN_IN_SEPARATE_THREAD, + RUN_IN_MAIN_THREAD +} app_run_in_separate_task_t; + +typedef struct app_data_t app_data_t; + +void app_pnet_cfg_init_default (pnet_cfg_t * pnet_cfg); + +/** + * Initialize application + * + * Initialize P-Net stack and application. + * The pnet_cfg argument shall have been initialized using + * app_pnet_cfg_init_default() before this function is + * called. + * @param pnet_cfg In: P-Net start configuration + * @return Application handle, NULL on error + */ +app_data_t * app_init (pnet_cfg_t * pnet_cfg, const app_args_t * app_args); + +/** + * Start application + * + * Application must have been initialized using app_init() before + * app_start() is called. + * If task_config parameters is set to RUN_IN_SEPARATE_THREAD a + * thread execution the app_loop_forever() function is started. + * If task_config is set to RUN_IN_MAIN_THREAD no such thread is + * started and the caller must call the app_loop_forever() after + * calling this function. + * RUN_IN_MAIN_THREAD is intended for rt-kernel targets. + * RUN_IN_SEPARATE_THREAD is intended for linux targets. + * @param app In: Application handle + * @param task_config In: Defines if stack and application + * is run in main or separate task. + * + * @return 0 on success, -1 on error + */ +int app_start (app_data_t * app, app_run_in_separate_task_t task_config); + +/** + * Application task definition. Handles events + * in eternal loop. + * @param arg In: Application handle + * return Should not + */ +void app_loop_forever (void * arg); + +/** + * Get P-Net instance from application + * + * @param app In: Application handle + * + * @return P-Net instance + */ +pnet_t * app_get_pnet_instance (app_data_t * app); + +/** + * Set LED state + * Hardware specific. Implemented in sample app main file for + * each supported platform. + * + * @param id In: LED number, starting from 0. + * @param led_state In: LED state. Use true for on and false for off. + */ +void app_set_led (uint16_t id, bool led_state); + +/** + * Read button state + * + * Hardware specific. Implemented in sample app main file for + * each supported platform. + * + * @param id In: Button number, starting from 0. + * @return true if button is pressed, false if not + */ +bool app_get_button (uint16_t id); + +#ifdef __cplusplus +} +#endif + +#endif /* SAMPLEAPP_COMMON_H */ diff --git a/profinet_stack/p-net/samples/pn_dev_lan9662/switchdev-profinet-example.sh b/profinet_stack/p-net/samples/pn_dev_lan9662/switchdev-profinet-example.sh new file mode 100644 index 0000000..9228883 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_dev_lan9662/switchdev-profinet-example.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# +# Start Profinet sample application +# By default RTE mode is set to full / RTE-FGPA +# The RTE mode can be selected by passing +# "none", "cpu" or "full" to this script. +# +# Application RTE modes +# none +# - LAN9662 RTE: Disabled +# - I/O Data mapping: Shared Memory +# - P-Net data path: P-Net default +# cpu +# - LAN9662 RTE: Enabled +# - I/O Data mapping: Shared Memory +# - P-Net data path: RTE - SRAM +# full +# - LAN9662 RTE: Enabled +# - I/O Data mapping: iofpga +# - P-Net data path: RTE - QSPI +# +# P-Net profinet requires a file system for +# configuration storage. This example assumes +# a rw file system mounted on /tmp/pn_data +# + +echo "Starting switchdev-profinet-example" + +# Assume a valid partion on /dev/mmcblk0p2 +mkdir /tmp/pn_data +mount -o sync /dev/mmcblk0p2 /tmp/pn_data + +# Configure bridge +ip link add name br0 type bridge + +# Needed for bridge to work +symreg ANA_PGID[61] 0x1ff + +ip link set br0 address 12:A9:2D:16:93:89 +ip link set br0 up +sysctl -w net.ipv6.conf.br0.disable_ipv6=1 + +ip link set eth0 up +ip link set eth1 up + +ip link set eth0 master br0 +ip link set eth1 master br0 + +# Enable forwarding to chip port 4 (RTE) +symreg qsys_sw_port_mode[4] 0x45000 + +# Start Profinet application +if [[ $# -eq 0 ]] ; then + mode=full +else + mode=$1 +fi + +echo "Starting LAN9662 Profinet sample application" +echo "RTE mode: $mode" + +/usr/bin/pn_lan9662 -m "$mode" -vvvv -p /tmp/pn_data & + +# When enabling log level "DEBUG" for the P-Net stack +# it may be useful to write log to file. The log file +# can be uploaded from target using scp: +# scp root@192.168.0.50:/tmp/pn_dev_log.txt pn_dev_log.txt + +#echo "Setup dropbear to support scp" +#mkdir -p /var/run/dropbear/ +#dropbear -R -B + +# Enable to to write p-net log to file. +#touch /tmp/pn_dev_log.txt +#chmod a+w /tmp/pn_dev_log.txt +#echo " Application log will be stored to /tmp/pn_dev_log.txt" +#/usr/bin/pn_lan9662 -m "$mode" -vvvv -p /tmp/pn_data > /tmp/pn_dev_log.txt + + diff --git a/profinet_stack/p-net/samples/pn_shm_tool/CMakeLists.txt b/profinet_stack/p-net/samples/pn_shm_tool/CMakeLists.txt new file mode 100644 index 0000000..27647e4 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/CMakeLists.txt @@ -0,0 +1,17 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# http://www.rt-labs.com +# Copyright 2017 rt-labs AB, Sweden. +# See LICENSE file in the project root for full license information. +#*******************************************************************/ + +target_include_directories(pn_shm_tool + PRIVATE + ) + +install (TARGETS pn_shm_tool DESTINATION bin) diff --git a/profinet_stack/p-net/samples/pn_shm_tool/pn_shm_tool.c b/profinet_stack/p-net/samples/pn_shm_tool/pn_shm_tool.c new file mode 100644 index 0000000..3f4633f --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/pn_shm_tool.c @@ -0,0 +1,509 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* Linux application intended to be used + * to read and write shared memory areas supported + * by the lan9662 sample application. + * See show_usage() for a functional description. + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#define MAX_FILE_NAME 200 +#define MAX_STDIO_BUFFER 10000 + +#define DEBUG_LOG (1) +#if DEBUG_LOG == 1 +#define LOG_ERR(...) \ + fprintf (stderr, "Line:%d ", __LINE__); \ + fprintf (stderr, __VA_ARGS__); +#else +#define LOG_ERR(...) +#endif + +typedef struct app +{ + enum + { + UNDEFINED = 0, + READ, + WRITE, + CREATE, + READ_BIT + } mode; + + char name[MAX_FILE_NAME]; /* Shared memory file name*/ + int fd; /* Shared memory file descriptor */ + caddr_t mem_address; /* Address of mapped shared memory */ + sem_t * sem; /* Named semaphore */ + size_t shm_size; /* The size of an existing shared memory area */ + size_t create_size; /* Size argument used when creating a shared memory + area*/ + uint32_t bitnumber; /* Bit number to read */ + uint8_t buffer[MAX_STDIO_BUFFER]; +} app_t; + +static void show_usage (void) +{ + printf ("\nTool for accessing shared memory files\n"); + printf ("Intended to be used in combination with the \n" + "pn_lan9662 sample application. \n"); + printf ("Arguments:\n"); + printf (" -h Show help and exit\n"); + printf ( + " -r Read shared memory and write content to stdout \n"); + printf (" -w Write content on stdin to shared memory \n"); + printf (" -c Create shared memory area\n"); + printf (" -s Size of shared memory, only used with create\n"); + printf (" -b Read bit from shared memory. Write '1' or '0' " + "to stdout\n"); + printf ( + " -n Bit number, only used with read bit. Defaults to\n"); + printf ( + " bit number 0 (least significant bit in first byte)\n"); + printf ( + "Partial write is allowed. Offsets are not supported. If write data\n"); + printf ("is larger than shared memory, data is truncated.\n"); + + printf ("\nExample usage:\n"); + printf ("Read slot and passing data to hexdump:\n"); + printf ("pn_shm_tool -r pnet-in-1-1-digital_input_1x8 | hexdump -C\n"); + printf ("Write slot:\n"); + printf ("echo -n -e \"\\x02\" | pn_shm_tool -w " + "pnet-in-1-1-digital_input_1x8\n"); + printf ("Create 4 bytes shared memory area (for testing this tool):\n"); + printf ("pn_shm_tool -c test_shm -s 4\n"); +} + +/** + * Open an existing shared memory area + * figure out its size and map it. + * @param app InOut: Application context + * @return 0 on success, -1 on error + */ +int open_shm (app_t * app) +{ + struct stat st; + + if (app->mode == CREATE) + { + return -1; + } + + app->fd = shm_open (app->name, O_RDWR, 0666); + + if (app->fd < 0) + { + LOG_ERR ("\nFailed to open %s %s\n", app->name, strerror (errno)); + return -1; + } + + if (fstat (app->fd, &st)) + { + LOG_ERR ("\nfstat error: [%s]\n", strerror (errno)); + close (app->fd); + return -1; + } + + app->shm_size = st.st_size; + + if (ftruncate (app->fd, app->shm_size) != 0) + { + return -1; + } + + app->mem_address = + mmap (NULL, app->shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, app->fd, 0); + + if (app->mem_address == MAP_FAILED) + { + LOG_ERR ("\nmmap error: [%s]\n", strerror (errno)); + return -1; + } + + app->sem = sem_open (app->name, 0, 0644, 1); + + if (app->sem == NULL) + { + LOG_ERR ("\nFailed to open semaphore: [%s]\n", strerror (errno)); + return -1; + } + + return 0; +} + +/** + * Create a new shared memory area + * @param app InOut: Application context + * @return 0 on success, -1 on error + */ +int create_shm (app_t * app) +{ + if (app->mode != CREATE) + { + return -1; + } + + app->fd = shm_open (app->name, O_RDWR | O_CREAT, 0666); + + if (app->fd < 0) + { + LOG_ERR ("\nFailed to create %s %s\n", app->name, strerror (errno)); + return -1; + } + + if (ftruncate (app->fd, app->create_size) != 0) + { + return -1; + } + + app->mem_address = mmap ( + NULL, + app->create_size, + PROT_READ | PROT_WRITE, + MAP_SHARED, + app->fd, + 0); + + if (app->mem_address == MAP_FAILED) + { + LOG_ERR ("\nmmap error: [%s]\n", strerror (errno)); + return -1; + } + + app->sem = sem_open (app->name, O_CREAT, 0644, 1); + + if (app->sem == NULL) + { + LOG_ERR ("\nFailed to open semaphore: [%s]\n", strerror (errno)); + return -1; + } + + return 0; +} + +/** + * Close a shared memory area and its + * open resources. + * @param app InOut: Application context + * @return 0 always + */ +int close_shm (app_t * app) +{ + if (app->mem_address != 0) + { + munmap (app->mem_address, app->shm_size); + app->mem_address = 0; + } + + if (app->fd >= 0) + { + close (app->fd); + app->fd = -1; + } + + if (app->sem != NULL) + { + sem_close (app->sem); + app->sem = NULL; + } + + return 0; +} + +/** + * Read a shared memory area and print its content + * to STDOUT. + * @param app InOut: Application context + * @return 0 on success, -1 on error + */ +int read_shm (app_t * app) +{ + int write_len; + + if (sem_wait (app->sem) != 0) + { + LOG_ERR ("\nFailed to take semaphore: [%s]\n", strerror (errno)); + return -1; + } + + memcpy (app->buffer, app->mem_address, app->shm_size); + + if (sem_post (app->sem) != 0) + { + LOG_ERR ("\nFailed to release semaphore: [%s]\n", strerror (errno)); + return -1; + } + + write_len = write (STDOUT_FILENO, app->buffer, app->shm_size); + if (write_len != (int)app->shm_size) + { + LOG_ERR ("\nFailed to write stdout\n"); + return -1; + } + + return 0; +} + +/** + * Read a bit from shared memory area and print "0" or "1" to STDOUT. + * @param app InOut: Application context + * @return 0 on success, -1 on error + */ +int read_bit_shm (app_t * app) +{ + int write_len = 0; + uint32_t byte_number = 0; + uint8_t bitnumber_in_byte = 0; + uint8_t byte_value = 0; + uint8_t bit_value = 0; + + if (app->shm_size == 0) + { + LOG_ERR ("\nShared memory size is zero\n"); + return -1; + } + + if (app->bitnumber >= MAX_STDIO_BUFFER * 8) + { + LOG_ERR ( + "\nToo large bitnumber. Given: %u (Buffer size %u bytes)\n", + app->bitnumber, + MAX_STDIO_BUFFER); + return -1; + } + + if (app->bitnumber >= app->shm_size * 8) + { + LOG_ERR ( + "\nToo large bitnumber. Given: %u (Memory size %zu bytes)\n", + app->bitnumber, + app->shm_size); + return -1; + } + + if (sem_wait (app->sem) != 0) + { + LOG_ERR ("\nFailed to take semaphore: [%s]\n", strerror (errno)); + return -1; + } + + memcpy (app->buffer, app->mem_address, app->shm_size); + + if (sem_post (app->sem) != 0) + { + LOG_ERR ("\nFailed to release semaphore: [%s]\n", strerror (errno)); + return -1; + } + + byte_number = app->bitnumber / 8; /* byte_number 0 is first */ + byte_value = app->buffer[byte_number]; + bitnumber_in_byte = app->bitnumber % 8; + bit_value = (byte_value >> bitnumber_in_byte) & 0x01; + + write_len = write (STDOUT_FILENO, bit_value ? "1\n" : "0\n", 2); + if (write_len != 2) + { + LOG_ERR ("\nFailed to write stdout\n"); + return -1; + } + + return 0; +} + +/** + * Read STDIN and copy the data to shared memory area. + * @param app InOut: Application context + * @return 0 on success, -1 on error + */ +int write_shm (app_t * app) +{ + int len = read (STDIN_FILENO, app->buffer, app->shm_size); + if (len > (int)app->shm_size) + { + /* Truncate len of data to match shared memory */ + len = app->shm_size; + } + + if (len <= 0) + { + LOG_ERR ("\nFailed to read stdin: [%s]\n", strerror (errno)); + return -1; + } + + if (sem_wait (app->sem) != 0) + { + LOG_ERR ("\nFailed to take semaphore: [%s]\n", strerror (errno)); + return -1; + } + + memcpy (app->mem_address, app->buffer, len); + + if (sem_post (app->sem) != 0) + { + LOG_ERR ("\nFailed to release semaphore: [%s]\n", strerror (errno)); + return -1; + } + + return 0; +} + +/** + * Execute a command given the configuration + * set in the app variable. + * Creation, read and write of shared memory regions + * are supported. + * @param app InOut: Application context + * @return 0 on success, -1 on error + */ +static int run (app_t * app) +{ + int error = 0; + + if (app->mode == CREATE) + { + error = create_shm (app); + close_shm (app); + return error; + } + + error = open_shm (app); + if (!error) + { + if (app->mode == READ) + { + error = read_shm (app); + } + else if (app->mode == READ_BIT) + { + error = read_bit_shm (app); + } + else if (app->mode == WRITE) + { + error = write_shm (app); + } + error |= close_shm (app); + } + return error; +} + +/** + * Parse command line arguments and execute selected command. + * @param argc In: Number of args + * @param argv In: Arguments + * @return 0 on success, 1 on error + */ +int main (int argc, char * argv[]) +{ + int error = 0; + int option = 0; + app_t app = {0}; + + if (argc < 2) + { + show_usage(); + exit (EXIT_FAILURE); + } + + while ((option = getopt (argc, argv, "hw:r:c:s:b:n:")) != -1) + { + switch (option) + { + case 'r': + if (app.mode != UNDEFINED) + { + show_usage(); + printf ("\nOnly one operation allowed\n"); + exit (EXIT_FAILURE); + } + strncpy (app.name, optarg, MAX_FILE_NAME - 1); + app.mode = READ; + break; + case 'b': + if (app.mode != UNDEFINED) + { + show_usage(); + printf ("\nOnly one operation allowed\n"); + exit (EXIT_FAILURE); + } + strncpy (app.name, optarg, MAX_FILE_NAME - 1); + app.mode = READ_BIT; + break; + case 'w': + if (app.mode != UNDEFINED) + { + show_usage(); + printf ("\nOnly one operation allowed\n"); + exit (EXIT_FAILURE); + } + strncpy (app.name, optarg, MAX_FILE_NAME - 1); + app.mode = WRITE; + break; + case 'c': + if (app.mode != UNDEFINED) + { + show_usage(); + printf ("\nOnly one operation allowed\n"); + exit (EXIT_FAILURE); + } + strncpy (app.name, optarg, MAX_FILE_NAME - 1); + app.mode = CREATE; + break; + case 's': + app.create_size = atoi (optarg); + break; + case 'n': + app.bitnumber = atoi (optarg); + break; + case 'h': + show_usage(); + exit (EXIT_SUCCESS); + default: + show_usage(); + exit (EXIT_FAILURE); + } + } + if (app.mode == CREATE) + { + if (app.create_size <= 0 || app.create_size > MAX_STDIO_BUFFER) + { + show_usage(); + printf ("\nSize must be set when creating shared mem area\n"); + printf ("Min size 1, Max size %d\n", MAX_STDIO_BUFFER); + exit (EXIT_FAILURE); + } + } + + error = run (&app); + if (error != 0) + { + fprintf (stderr, "Operation failed.\n"); + exit (EXIT_FAILURE); + } + + exit (EXIT_SUCCESS); +} diff --git a/profinet_stack/p-net/samples/pn_shm_tool/shm_LED_handler.sh b/profinet_stack/p-net/samples/pn_shm_tool/shm_LED_handler.sh new file mode 100644 index 0000000..1e117ab --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/shm_LED_handler.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# +# Read shared memory and write to EVB-LAN9662 LED +# Utility script using the pn_shm_tool. Intended to be +# used for testing the pn_lan9662 application. +# + + +MEMORY_NAME="pnet-out-2-1-digital_output_1x8" +BIT_NUMBER=7 +LED_NAME="twr1:yellow" + +echo "Starting LED handler for Profinet data LED" + +sleep 1 + +while true +do + sleep 0.1 + + led_state=$(pn_shm_tool -b "$MEMORY_NAME" -n "$BIT_NUMBER") + exitcode=$? + + if [ $exitcode -ne 0 ]; then + echo "Failed to get LED state from shared memory" + exit 1 + fi + + echo ${led_state} > "/sys/class/leds/${LED_NAME}/brightness" + +done diff --git a/profinet_stack/p-net/samples/pn_shm_tool/shm_button_handler.sh b/profinet_stack/p-net/samples/pn_shm_tool/shm_button_handler.sh new file mode 100644 index 0000000..cff375f --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/shm_button_handler.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# +# Read button on GPIO pin and write to shared memory +# Utility script using the pn_shm_tool. Intended to be +# used for testing the pn_lan9662 application. +# + +MEMORY_NAME="pnet-in-1-1-digital_input_1x8" + +# Set up digital input on "Raspberry Pi"-type header +# Button1, pin 7, use GND at pin 9 +GPIO_NUMBER=39 + +echo "Starting button state monitor for GPIO $GPIO_NUMBER" + +echo "$GPIO_NUMBER" > /sys/class/gpio/export +echo "1" > "/sys/class/gpio/gpio$GPIO_NUMBER/active_low" + +sleep 1 + +while true +do + sleep 0.1 + + button_state=$(cat "/sys/class/gpio/gpio$GPIO_NUMBER/value") + exitcode=$? + + if [ $exitcode -ne 0 ]; then + echo "Failed to read button state" + exit 1 + fi + + if [ "$button_state" = "1" ]; then + echo -n -e '\x80' | pn_shm_tool -w "$MEMORY_NAME" + else + echo -n -e '\x00' | pn_shm_tool -w "$MEMORY_NAME" + fi + +done diff --git a/profinet_stack/p-net/samples/pn_shm_tool/shm_echo_all.sh b/profinet_stack/p-net/samples/pn_shm_tool/shm_echo_all.sh new file mode 100644 index 0000000..e3dd9dc --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/shm_echo_all.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Write shm outputs to inputs +# For test running this command forever +# may be handy: +# while true; do /usr/bin/shm_echo_all.sh; done +# Utility script using the pn_shm_tool. Intended to be +# used for testing the pn_lan9662 application. +# + + +pn_shm_tool -r pnet-out-2-1-digital_output_1x8 | pn_shm_tool -w pnet-in-1-1-digital_input_1x8 +pn_shm_tool -r pnet-out-7-1-digital_out_1x64 | pn_shm_tool -w pnet-in-3-1-digital_input_1x64 +pn_shm_tool -r pnet-out-8-1-digital_output_2x32_a | pn_shm_tool -w pnet-in-4-1-digital_input_2x32_a +pn_shm_tool -r pnet-out-9-1-digital_output_2x32_b | pn_shm_tool -w pnet-in-5-1-digital_input_2x32_b +pn_shm_tool -r pnet-out-10-1-digital_output_1x8000 | pn_shm_tool -w pnet-in-6-1-digital_input_1x8000 diff --git a/profinet_stack/p-net/samples/pn_shm_tool/shm_read_all.sh b/profinet_stack/p-net/samples/pn_shm_tool/shm_read_all.sh new file mode 100644 index 0000000..573514b --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/shm_read_all.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Utility script using the pn_shm_tool. Intended to be +# used for testing the pn_lan9662 application. +# +# Dump the contents of all shared memory areas +# supported by the lan9662 profinet sample application + +echo "Inputs" +echo "------------------------------------------------------------------" +echo "pnet-in-1-1-digital_input_1x8" +pn_shm_tool -r pnet-in-1-1-digital_input_1x8 | hexdump -C +echo "pnet-in-3-1-digital_input_1x64" +pn_shm_tool -r pnet-in-3-1-digital_input_1x64 | hexdump -C +echo "pnet-in-4-1-digital_input_2x32_a" +pn_shm_tool -r pnet-in-4-1-digital_input_2x32_a | hexdump -C +echo "pnet-in-5-1-digital_input_2x32_b" +pn_shm_tool -r pnet-in-5-1-digital_input_2x32_b | hexdump -C +echo "pnet-in-6-1-digital_input_1x8000" +pn_shm_tool -r pnet-in-6-1-digital_input_1x8000 | hexdump -C + +echo "\nOutputs" +echo "------------------------------------------------------------------" +echo "pnet-out-2-1-digital_output_1x8" +pn_shm_tool -r pnet-out-2-1-digital_output_1x8 | hexdump -C +echo "pnet-out-7-1-digital_out_1x64" +pn_shm_tool -r pnet-out-7-1-digital_out_1x64 | hexdump -C +echo "pnet-out-8-1-digital_output_2x32_a" +pn_shm_tool -r pnet-out-8-1-digital_output_2x32_a | hexdump -C +echo "pnet-out-9-1-digital_output_2x32_b" +pn_shm_tool -r pnet-out-9-1-digital_output_2x32_b | hexdump -C +echo "pnet-out-10-1-digital_output_1x8000" +pn_shm_tool -r pnet-out-10-1-digital_output_1x8000 | hexdump -C diff --git a/profinet_stack/p-net/samples/pn_shm_tool/shm_write_all_inputs.sh b/profinet_stack/p-net/samples/pn_shm_tool/shm_write_all_inputs.sh new file mode 100644 index 0000000..cfed9ae --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/shm_write_all_inputs.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Utility script using the pn_shm_tool. Intended to be +# used for testing the pn_lan9662 application. +# +# Write a character / byte to all shared memory inputs +# supported by the lan9662 profinet sample application +# Usage: +# shm_write_all_inputs.sh a +# shm_write_all_inputs.sh "\x02" + +echo -n -e "$1" | \ +pn_shm_tool -w pnet-in-1-1-digital_input_1x8 + +echo -n -e "$1$1$1$1$1$1$1$1" | \ +pn_shm_tool -w pnet-in-3-1-digital_input_1x64 + +echo -n -e "$1$1$1$1$1$1$1$1" | \ +pn_shm_tool -w pnet-in-4-1-digital_input_2x32_a + +echo -n -e "$1$1$1$1$1$1$1$1" | \ +pn_shm_tool -w pnet-in-5-1-digital_input_2x32_b + +data='' +for i in `seq 1 100`; +do +data="${data}$1" +done +echo -n -e "$data" | \ +pn_shm_tool -w pnet-in-6-1-digital_input_1x8000 diff --git a/profinet_stack/p-net/samples/pn_shm_tool/test.sh b/profinet_stack/p-net/samples/pn_shm_tool/test.sh new file mode 100644 index 0000000..50fed65 --- /dev/null +++ b/profinet_stack/p-net/samples/pn_shm_tool/test.sh @@ -0,0 +1,122 @@ +#!/bin/sh +echo "Sanity test of pn_shm_tool." + +# Create a shared memory area for test +f="shm_test_2" +echo "Creating /dev/shm/$f for test" + +$(./pn_shm_tool -c "$f" -s 4) + + +# 1 Verify correct size of data + +data_in="abcd" +echo "Test data: $data_in" + +$(echo -n -e "$data_in" | ./pn_shm_tool -w "$f") +data_out=$(./pn_shm_tool -r "$f") + +echo "Data out: $data_out" + +if [ "$data_out" != "$data_in" ]; then + echo "Error" + exit 1 +fi + + +# 2 Verify correct too small input data + +data_in="ABC" +echo "Test data: $data_in" + +$(echo -n -e "$data_in" | ./pn_shm_tool -w "$f") +data_out=$(./pn_shm_tool -r "$f") + +echo "Data out: $data_out" + +# First three bytes changed. Last byte keeps value +if [ "$data_out" != "ABCd" ]; then + echo "Error" + exit 1 +fi + + +# 3 Verify correct too large input data + +data_in="123456" +echo "Test data: $data_in" + +$(echo -n -e "$data_in" | ./pn_shm_tool -w "$f") +data_out=$(./pn_shm_tool -r "$f") + +echo "Data out: $data_out" + +# All bytes changed. Size unchanged +if [ "$data_out" != "1234" ]; then + echo "Error" + exit 1 +fi + + +# 4 faulty parameter + +echo "Testing invalid parameter:" +show_cmd=$(./pn_shm_tool -d "$f") +ret_val=$? +echo " " + +if [ $ret_val -ne 1 ]; then + echo "Error" + exit 1 +fi + +# 5 Verify reading bit + +data_in="1234" +echo "Test data: $data_in" +echo "The stored data as ASCII: 0x31 0x32 0x33 0x34" + +$(echo -n -e "$data_in" | ./pn_shm_tool -w "$f") + +data_out=$(./pn_shm_tool -b "$f" -n 0) +ret_val=$? +if [ $ret_val -ne 0 ]; then + echo "Error when reading 0: Wrong exit code" + exit 1 +fi +if [ "$data_out" != "1" ]; then + echo "Error reading bit 0: Wrong value" + exit 1 +fi + +data_out=$(./pn_shm_tool -b "$f" -n 1) +if [ "$data_out" != "0" ]; then + echo "Error reading bit 1" + exit 1 +fi + +data_out=$(./pn_shm_tool -b "$f" -n 2) +if [ "$data_out" != "0" ]; then + echo "Error reading bit 2" + exit 1 +fi + +data_out=$(./pn_shm_tool -b "$f" -n 3) +if [ "$data_out" != "0" ]; then + echo "Error reading bit 3" + exit 1 +fi + + +# 6 faulty parameter to read bit +echo "Testing invalid parameter:" +data_out=$(./pn_shm_tool -b "$f" -n 32) +ret_val=$? +echo " " + +if [ $ret_val -ne 1 ]; then + echo "Error" + exit 1 +fi + +echo "OK, test passed" diff --git a/profinet_stack/p-net/src/CMakeLists.txt b/profinet_stack/p-net/src/CMakeLists.txt new file mode 100644 index 0000000..1d4639e --- /dev/null +++ b/profinet_stack/p-net/src/CMakeLists.txt @@ -0,0 +1,90 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2018 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +# NOTE: add headers to make them show up in an IDE +# NOTE: Use full path for the <$ expression +# to work with certain cmake versions +target_sources (profinet PRIVATE + ${PROFINET_SOURCE_DIR}/include/pnet_api.h + pf_includes.h + pf_types.h + device/pnet_api.c + device/pf_block_reader.c + device/pf_block_writer.c + device/pf_fspm.c + device/pf_diag.c + device/pf_cmdev.c + device/pf_cmdmc.c + device/pf_cmina.c + device/pf_cmio.c + device/pf_cmpbe.c + device/pf_cmrdr.c + device/pf_cmrpc.c + device/pf_cmrpc_epm.c + device/pf_cmrpc_helpers.c + device/pf_cmrs.c + device/pf_cmsm.c + device/pf_cmsu.c + device/pf_cmwrr.c + device/pf_pdport.c + device/pf_port.c + device/pf_block_reader.h + device/pf_block_writer.h + device/pf_fspm.h + device/pf_diag.h + device/pf_cmdev.h + device/pf_cmdmc.h + device/pf_cmina.h + device/pf_cmio.h + device/pf_cmpbe.h + device/pf_cmrdr.h + device/pf_cmrpc.h + device/pf_cmrpc_epm.h + device/pf_cmrpc_helpers.h + device/pf_cmrs.h + device/pf_cmsm.h + device/pf_cmsu.h + device/pf_cmwrr.h + device/pf_pdport.h + device/pf_port.h + common/pf_alarm.c + common/pf_bg_worker.c + common/pf_cpm.c + common/pf_cpm_driver_sw.c + common/pf_dcp.c + common/pf_ppm.c + common/pf_ppm_driver_sw.c + common/pf_ptcp.c + common/pf_scheduler.c + common/pf_eth.c + common/pf_file.c + common/pf_lldp.c + $<$:${PROFINET_SOURCE_DIR}/src/common/pf_snmp.c> + common/pf_udp.c + common/pf_alarm.h + common/pf_bg_worker.h + common/pf_cpm.h + common/pf_cpm_driver_sw.h + common/pf_dcp.h + common/pf_file.h + common/pf_ppm.h + common/pf_ppm_driver_sw.h + common/pf_ptcp.h + common/pf_scheduler.h + common/pf_eth.h + common/pf_lldp.h + common/pf_snmp.h + common/pf_udp.h + ) diff --git a/profinet_stack/p-net/src/common/pf_alarm.c b/profinet_stack/p-net/src/common/pf_alarm.c new file mode 100644 index 0000000..649ced7 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_alarm.c @@ -0,0 +1,3485 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Alarm implementation + * + * Sends and receives low- and high prio alarm Ethernet frames. + * + * A timer resends alarms if not acknowledged. + * + * - ALPMI Sends alarm (initiator) + * - ALPMR Receives alarm (responder) + * - APMR Receives high- and low priority alarm Ethernet frames from the + * IO-controller. + * - APMS Sends alarm Ethernet frames to IO-controller + * + * See the documentation of this project for an overview of alarm implementation + * details. + * + * Note that ALPMX is a general term for ALPMI and ALPMR, and APMX is a + * general term for APMS and APMR. + * + * There is one APMS+APMR pair for low prio messages and one pair for high prio + * messages. Similarly there is one ALPMI+ALPMR pair for low prio and one pair + * for high prio. + * + * A frame handler is registered for each of the high prio and low prio + * alarm frame IDs. + * + * Each AR has two alarm receive queues, one for low prio and one for high + * prio alarm frames. The thread responsible for processing incoming frames + * puts frames into the queues, and another thread (driven by the + * periodic task) retrieves messages from the receive queues. + * A message in an input queue contains a pointer to an incoming frame buffer. + * + * Each AR has its own alarm send queue. As alarms can only be processed + * one-by-one, a send queue is necessary. The same thread puts messages + * into the send queue and retrieves messages from the send queue. + * A message in the alarm send queue contains the full alarm, including payload. + * + * There are convenience functions to send different types of alarms, for + * example process alarms. + * + * a_data means acyclic data. + * + */ + +#ifdef UNIT_TEST +#endif + +/* + * ToDo: + * 1) Send UDP frames. + */ + +#include + +#include "pf_includes.h" +#include "pf_block_reader.h" +#include "pf_block_writer.h" + +#define PF_FRAME_ID_ALARM_HIGH 0xfc01 +#define PF_FRAME_ID_ALARM_LOW 0xfe01 + +#define ALARM_VLAN_PRIO_LOW 5 +#define ALARM_VLAN_PRIO_HIGH 6 + +#if PNET_MAX_ALARMS < 1 +#error "PNET_MAX_ALARMS needs to be at least 1" +#endif + +/* TODO Add reference to Profinet specification section */ +#if PNET_MAX_ALARM_PAYLOAD_DATA_SIZE > 1408 +#error "PNET_MAX_ALARM_PAYLOAD_DATA_SIZE is too large" +#endif + +#if PNET_MAX_DIAG_MANUF_DATA_SIZE > 1396 +#error "PNET_MAX_DIAG_MANUF_DATA_SIZE is too large" +#endif + +CC_STATIC_ASSERT (PNET_MAX_ALARM_PAYLOAD_DATA_SIZE >= sizeof (pf_diag_item_t)); + +/*************** Diagnostic strings *****************************************/ + +/** + * @internal + * Return a string representation of alarm PDU type (message type). + * @param pdu_type In: alarm PDU type + * See pf_alarm_pdu_type_values_t + * @return A string representing the alarm PDU type. + */ +static const char * pf_alarm_pdu_type_to_string (uint8_t pdu_type) +{ + const char * s = NULL; + + switch (pdu_type) + { + case PF_RTA_PDU_TYPE_DATA: + s = "PF_RTA_PDU_TYPE_DATA"; + break; + case PF_RTA_PDU_TYPE_NACK: + s = "PF_RTA_PDU_TYPE_NACK"; + break; + case PF_RTA_PDU_TYPE_ACK: + s = "PF_RTA_PDU_TYPE_ACK"; + break; + case PF_RTA_PDU_TYPE_ERR: + s = "PF_RTA_PDU_TYPE_ERR"; + break; + default: + s = ""; + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the given ALPMI state. + * @param state In: The ALPMI state. + * @return A string representing the ALPMI state. + */ +static const char * pf_alarm_alpmi_state_to_string ( + pf_alpmi_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_ALPMI_STATE_W_START: + s = "PF_ALPMI_STATE_W_START"; + break; + case PF_ALPMI_STATE_W_ALARM: + s = "PF_ALPMI_STATE_W_ALARM"; + break; + case PF_ALPMI_STATE_W_ACK: + s = "PF_ALPMI_STATE_W_ACK"; + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the given ALPMR state. + * @param state In: The ALPMR state. + * @return A string representing the ALPMR state. + */ +static const char * pf_alarm_alpmr_state_to_string ( + pf_alpmr_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_ALPMR_STATE_W_START: + s = "PF_ALPMR_STATE_W_START"; + break; + case PF_ALPMR_STATE_W_NOTIFY: + s = "PF_ALPMR_STATE_W_NOTIFY"; + break; + case PF_ALPMR_STATE_W_USER_ACK: + s = "PF_ALPMR_STATE_W_USER_ACK"; + break; + case PF_ALPMR_STATE_W_TACK: + s = "PF_ALPMR_STATE_W_TACK"; + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the given APMS state. + * @param state In: The APMS state. + * @return A string representing the APMS state. + */ +static const char * pf_alarm_apms_state_to_string (pf_apms_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_APMS_STATE_CLOSED: + s = "PF_APMS_STATE_CLOSED"; + break; + case PF_APMS_STATE_OPEN: + s = "PF_APMS_STATE_OPEN"; + break; + case PF_APMS_STATE_WTACK: + s = "PF_APMS_STATE_WTACK"; + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the given APMR state. + * @param state In: The APMR state. + * @return A string representing the APMR state. + */ +static const char * pf_alarm_apmr_state_to_string (pf_apmr_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_APMR_STATE_CLOSED: + s = "PF_APMR_STATE_CLOSED"; + break; + case PF_APMR_STATE_OPEN: + s = "PF_APMR_STATE_OPEN"; + break; + case PF_APMR_STATE_WCNF: + s = "PF_APMR_STATE_WCNF"; + break; + } + + return s; +} + +void pf_alarm_show (const pf_ar_t * p_ar) +{ + printf ("Alarms\n"); + printf (" PNET_MAX_ALARMS = %d\n", PNET_MAX_ALARMS); + printf ("Alarms (low prio)\n"); + printf ( + " alpmi_state = %s\n", + pf_alarm_alpmi_state_to_string (p_ar->alpmx[0].alpmi_state)); + printf ( + " alpmr_state = %s\n", + pf_alarm_alpmr_state_to_string (p_ar->alpmx[0].alpmr_state)); + printf ( + " apms_state = %s\n", + pf_alarm_apms_state_to_string (p_ar->apmx[0].apms_state)); + printf ( + " apmr_state = %s\n", + pf_alarm_apmr_state_to_string (p_ar->apmx[0].apmr_state)); + printf ( + " exp_seq_count = 0x%x\n", + (unsigned)p_ar->apmx[0].exp_seq_count); + printf ( + " exp_seq_count_o = 0x%x\n", + (unsigned)p_ar->apmx[0].exp_seq_count_o); + printf ( + " send_seq_count = 0x%x\n", + (unsigned)p_ar->apmx[0].send_seq_count); + printf ( + " send_seq_count_o = 0x%x\n", + (unsigned)p_ar->apmx[0].send_seq_count_o); + printf ( + " sequence_number = %u\n", + (unsigned)p_ar->alpmx[0].sequence_number); + printf ( + " Number of frames in incoming queue = %u\n", + p_ar->apmx[0].alarm_receive_q.accountant.count); + printf ("Alarms (high prio)\n"); + printf ( + " alpmi_state = %s\n", + pf_alarm_alpmi_state_to_string (p_ar->alpmx[1].alpmi_state)); + printf ( + " alpmr_state = %s\n", + pf_alarm_alpmr_state_to_string (p_ar->alpmx[1].alpmr_state)); + printf ( + " apms_state = %s\n", + pf_alarm_apms_state_to_string (p_ar->apmx[1].apms_state)); + printf ( + " apmr_state = %s\n", + pf_alarm_apmr_state_to_string (p_ar->apmx[1].apmr_state)); + printf ( + " exp_seq_count = 0x%x\n", + (unsigned)p_ar->apmx[1].exp_seq_count); + printf ( + " exp_seq_count_o = 0x%x\n", + (unsigned)p_ar->apmx[1].exp_seq_count_o); + printf ( + " send_seq_count = 0x%x\n", + (unsigned)p_ar->apmx[1].send_seq_count); + printf ( + " send_seq_count_o = 0x%x\n", + (unsigned)p_ar->apmx[1].send_seq_count_o); + printf ( + " sequence_number = %u\n", + (unsigned)p_ar->alpmx[1].sequence_number); + printf ( + " Number of frames in incoming queue = %u\n", + p_ar->apmx[1].alarm_receive_q.accountant.count); +} + +/*****************************************************************************/ + +void pf_alarm_init (pnet_t * net) +{ + /* Enable alarms from the start */ + net->global_alarm_enable = true; +} + +/** + * @internal + * Send an ALARM error indication to CMSU. + * + * ALPMI: ALPMI_Error_ind + * ALPMR: ALPMR_Error_ind + * APMS: APMS_Error_ind + * APMR: APMR_Error_ind + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMX instance. + * @param err_cls In: The ERRCLS variable. ErrorCode1. + * @param err_code In: The ERRCODE variable. ErrorCode2. + */ +static void pf_alarm_error_ind ( + pnet_t * net, + pf_apmx_t * p_apmx, + uint8_t err_cls, + uint8_t err_code) +{ + /* + * ALPMI: PNET_ERROR_CODE_2_CMSU_AR_ALARM_SEND; + * ALPMI: PNET_ERROR_CODE_2_CMSU_AR_ALARM_ACK_SEND; + * + * ALPMR: PNET_ERROR_CODE_2_CMSU_AR_ALARM_IND + * + * APMR: PNET_ERROR_CODE_2_CMSU_AR_ALARM_IND + */ + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Setting err_cls 0x%02x err_code 0x%02x\n", + __LINE__, + err_cls, + err_code); + (void)pf_cmsu_alarm_error_ind (net, p_apmx->p_ar, err_cls, err_code); +} + +/** + * @internal + * Activate the ALPMI and ALPMR instances. + * + * ALPMI: ALPMI_Activate_req + * ALPMI: ALPMI_Activate_cnf(+/-) via return value and err_cls/err_code + * ALPMR: ALPMR_Activate_req + * ALPMR: ALPMR_Activate_cnf(+/-) via return value and err_cls/err_code + * + * @param p_ar InOut: The AR instance. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_alpmx_activate (pf_ar_t * p_ar) +{ + int ret = 0; /* Assume all goes well */ + uint16_t ix; + + for (ix = 0; ix < NELEMENTS (p_ar->alpmx); ix++) + { + if ( + p_ar->alpmx[ix].alpmi_state == PF_ALPMI_STATE_W_START && + p_ar->alpmx[ix].alpmr_state == PF_ALPMR_STATE_W_START) + { + /* Save the remote side MAC address for easy retrieval. */ + memcpy ( + &p_ar->alpmx[ix].da, + &p_ar->ar_param.cm_initiator_mac_add, + sizeof (p_ar->alpmx[ix].da)); + + p_ar->alpmx[ix].prev_sequence_number = 0xffff; + p_ar->alpmx[ix].sequence_number = 0; + + p_ar->alpmx[ix].alpmr_state = PF_ALPMR_STATE_W_NOTIFY; + p_ar->alpmx[ix].alpmi_state = PF_ALPMI_STATE_W_ALARM; + } + else if (p_ar->alpmx[ix].alpmi_state != PF_ALPMI_STATE_W_START) + { + /* This is ALPMI_Activate_cnf(-) */ + p_ar->err_cls = PNET_ERROR_CODE_1_ALPMI; + p_ar->err_code = PNET_ERROR_CODE_2_ALPMI_WRONG_STATE; + + ret = -1; + } + else + { + /* This is ALPMR_Activate_cnf(-) */ + p_ar->err_cls = PNET_ERROR_CODE_1_ALPMR; + p_ar->err_code = PNET_ERROR_CODE_2_ALPMR_WRONG_STATE; + + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Close all the ALPMI and ALPMR instances. + * + * ALPMI: ALPMI_Close_req + * ALPMI: ALPMI_Close_cnf(+) + * ALPMR: ALPMR_Close_req + * ALPMR: ALPMR_Close_cnf via return value and err_cls/err_code + * + * @param p_ar InOut: The AR instance. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_alpmx_close (pf_ar_t * p_ar) +{ + int ret = 0; /* Assume all goes well */ + uint16_t ix; + + /* This is ALPMI_Close_req */ + for (ix = 0; ix < NELEMENTS (p_ar->alpmx); ix++) + { + p_ar->alpmx[ix].alpmi_state = PF_ALPMI_STATE_W_START; + } + + /* This is ALPMR_Close_req */ + for (ix = 0; ix < NELEMENTS (p_ar->alpmx); ix++) + { + switch (p_ar->alpmx[ix].alpmr_state) + { + case PF_ALPMR_STATE_W_START: + /* Nothing special */ + break; + case PF_ALPMR_STATE_W_NOTIFY: + case PF_ALPMR_STATE_W_USER_ACK: + case PF_ALPMR_STATE_W_TACK: + /* + * According to the spec we should call pf_alarm_error_ind here with + * ERRCLS and ERRCODE. Since this avalanches into CMDEV which calls + * pf_cmsu_cmdev_state_ind which calls this function we would create an + * eternal loop. So dont call here. + * + * CMSU is the only caller of pf_alarm_close - why would we need to + * call CMSU with an error indicating that we are closing?? + */ + + p_ar->alpmx[ix].alpmr_state = PF_ALPMR_STATE_W_START; + /* pf_alarm_error_ind(net, &p_ar->apmx[ix], p_ar->err_cls, + * PNET_ERROR_CODE_2_CMSU_AR_ALARM_IND); */ + break; + } + } + + return ret; +} + +/** + * @internal + * Confirmation back to ALPMI on message sent via APMS. + * + * ALPMI: APMS_A_Data.cnf(+/-) + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMS instance sending the confirmation. + * @param res In: 0 positive confirmation. This is cnf(+) + * -1 negative confirmation. This is cnf(-) + */ +static void pf_alarm_alpmi_apms_a_data_cnf ( + pnet_t * net, + pf_apmx_t * p_apmx, + int res) +{ + switch (p_apmx->p_alpmx->alpmi_state) + { + case PF_ALPMI_STATE_W_START: + case PF_ALPMI_STATE_W_ALARM: + case PF_ALPMI_STATE_W_ACK: + if (res != 0) + { + /* Handle neg cnf */ + p_apmx->p_ar->err_cls = PNET_ERROR_CODE_1_ALPMI; + p_apmx->p_ar->err_code = PNET_ERROR_CODE_2_ALPMI_INVALID; + pf_alarm_error_ind ( + net, + p_apmx, + p_apmx->p_ar->err_cls, + p_apmx->p_ar->err_code); + } + break; + } +} + +/** + * @internal + * The APMR has received an Alarm ACK DATA message over wire, and tells ALPMI + * about the result. + * + * ALPMI: APMR_A_Data.ind (Implements part of it) + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMS instance sending the confirmation. + * @param p_pnio_status In: Detailed ACK information. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_alpmi_apmr_a_data_ind ( + pnet_t * net, + pf_apmx_t * p_apmx, + const pnet_pnio_status_t * p_pnio_status) +{ + int ret = -1; + + switch (p_apmx->p_alpmx->alpmi_state) + { + case PF_ALPMI_STATE_W_START: + case PF_ALPMI_STATE_W_ALARM: + /* Ignore */ + ret = 0; + break; + case PF_ALPMI_STATE_W_ACK: + /* This function is only called for DATA = ACK */ + p_apmx->p_alpmx->alpmi_state = PF_ALPMI_STATE_W_ALARM; + pf_fspm_alpmi_alarm_cnf (net, p_apmx->p_ar, p_pnio_status); + ret = 0; + break; + } + + return ret; +} + +/** + * @internal + * Confirmation back to ALPMR from APMS that a Transport ACK (TACK) has been + * received. + * + * Calls user call-back \a pnet_alarm_ack_cnf() for some cases. + * + * ALPMR: APMS_A_Data.cnf(+/-) + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMS instance sending the confirmation. + * @param res In: 0 positive confirmation. This is conf(+) + * -1 negative confirmation. This is conf(-) + */ +static void pf_alarm_alpmr_apms_a_data_cnf ( + pnet_t * net, + pf_apmx_t * p_apmx, + int res) +{ + switch (p_apmx->p_alpmx->alpmr_state) + { + case PF_ALPMR_STATE_W_START: + /* Ignore */ + break; + case PF_ALPMR_STATE_W_NOTIFY: + case PF_ALPMR_STATE_W_USER_ACK: + if (res != 0) + { + /* Handle neg cnf */ + p_apmx->p_ar->err_cls = PNET_ERROR_CODE_1_ALPMR; + p_apmx->p_ar->err_code = PNET_ERROR_CODE_2_ALPMR_INVALID; + pf_alarm_error_ind ( + net, + p_apmx, + p_apmx->p_ar->err_cls, + p_apmx->p_ar->err_code); + } + else + { + /* Ignore pos cnf */ + } + break; + case PF_ALPMR_STATE_W_TACK: + if (res != 0) + { + /* Handle neg cnf */ + p_apmx->p_ar->err_cls = PNET_ERROR_CODE_1_ALPMR; + p_apmx->p_ar->err_code = PNET_ERROR_CODE_2_ALPMR_INVALID; + pf_alarm_error_ind ( + net, + p_apmx, + p_apmx->p_ar->err_cls, + p_apmx->p_ar->err_code); + } + else + { + /* Handle pos cnf */ + /* ALPMR: ALPMR_Alarm_Ack.cnf(+) */ + pf_fspm_alpmr_alarm_ack_cnf (net, p_apmx->p_ar, res); + p_apmx->p_alpmx->alpmr_state = PF_ALPMR_STATE_W_NOTIFY; + } + break; + } +} + +/** + * @internal + * The APMR has received an Alarm Notification DATA message over wire, and tells + * ALPMR about the result. + * + * We have already sent the transport acknowledge ("TACK") frame. + * + * Will trigger the user callback \a pnet_alarm_ind(). + * + * ALPMR: APMR_A_Data.ind + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMR instance that received the message. + * @param p_fixed In: The Fixed part of the RTA-PDU. Not used. + * @param p_alarm_data In: The AlarmData from the RTA-PDU. (Slot, subslot + * etc) + * @param data_len In: VarDataLen from the RTA-PDU. + * @param data_usi In: The USI from the RTA-PDU. + * @param p_data In: The variable part of the RTA-PDU. + * @return + */ +static int pf_alarm_alpmr_apmr_a_data_ind ( + pnet_t * net, + pf_apmx_t * p_apmx, + const pf_alarm_fixed_t * p_fixed, + const pf_alarm_data_t * p_alarm_data, + uint16_t data_len, + uint16_t data_usi, + const uint8_t * p_data) +{ + pnet_alarm_argument_t alarm_arg = {0}; + int ret = -1; + + switch (p_apmx->p_alpmx->alpmr_state) + { + case PF_ALPMR_STATE_W_START: + /* Ignore */ + ret = 0; + break; + case PF_ALPMR_STATE_W_NOTIFY: + /* Only DATA: AlarmNotifications are sent to this function */ + alarm_arg.api_id = p_alarm_data->api_id; + alarm_arg.slot_nbr = p_alarm_data->slot_nbr; + alarm_arg.subslot_nbr = p_alarm_data->subslot_nbr; + alarm_arg.alarm_type = p_alarm_data->alarm_type; + alarm_arg.alarm_specifier = p_alarm_data->alarm_specifier; + alarm_arg.sequence_number = p_alarm_data->sequence_number; + + p_apmx->p_alpmx->alpmr_state = PF_ALPMR_STATE_W_USER_ACK; + + /* + * Indicate alarm to app. App must respond to callback + * using pnet_alarm_send_ack() + */ + (void)pf_fspm_alpmr_alarm_ind ( + net, + p_apmx->p_ar, + &alarm_arg, + data_len, + data_usi, + p_data); + ret = 0; + break; + case PF_ALPMR_STATE_W_USER_ACK: + /* ToDo: "Combine data for the RTA error request */ + /* FALL-THRU */ + case PF_ALPMR_STATE_W_TACK: + p_apmx->p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_apmx->p_ar->err_code = PNET_ERROR_CODE_2_ABORT_AR_PROTOCOL_VIOLATION; + pf_alarm_error_ind ( + net, + p_apmx, + p_apmx->p_ar->err_cls, + p_apmx->p_ar->err_code); + break; + } + + return ret; +} + +/*********************** Frame handler callback ******************************/ + +/** + * @internal + * Handle an alarm frame from the IO-controller, and put it into the + * correct alarm frame input queue. + * + * There are two instances of this handler, one for low prio alarm frames + * and one for high prio alarm frames (delivering to separare queues). + * + * All incoming Profinet frames with ID = PF_FRAME_ID_ALARM_LOW and + * ID = PF_FRAME_ID_ALARM_HIGH end up here. + * + * This is a callback for the frame handler. Arguments should fulfill + * pf_eth_frame_handler_t + * + * @param net InOut: The p-net stack instance + * @param frame_id In: The Ethernet frame id. + * @param p_buf In: The Ethernet frame buffer. + * @param frame_id_pos In: Position in the buffer of the frame id. + * Depends on whether VLAN tagging is used. + * @param p_arg In: The APMX instance. Should be pf_apmx_t + * @return 0 if the frame was NOT handled by this function. + * 1 if the frame was handled and the buffer was freed. + */ +static int pf_alarm_apmr_frame_handler ( + pnet_t * net, + uint16_t frame_id, + pnal_buf_t * p_buf, + uint16_t frame_id_pos, + void * p_arg) +{ + pf_apmx_t * p_apmx = (pf_apmx_t *)p_arg; + pf_apmr_msg_t tempmsg; + int ret = 0; /* Failed to handle frame. The frame handler needs to free + the buffer. */ + + if (p_buf != NULL) + { + tempmsg.p_buf = p_buf; + tempmsg.frame_id_pos = frame_id_pos; + + if (pf_alarm_receive_queue_post (&p_apmx->alarm_receive_q, &tempmsg) == 0) + { + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Received %s prio alarm frame %p. Put in queue. \n", + __LINE__, + p_apmx->high_priority ? "high" : "low", + p_buf); + + ret = 1; /* Means that calling function should not free buffer, + as that will be done when reading the queue */ + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to put incoming %s prio alarm frame in queue. " + "Framehandler will free %p\n", + __LINE__, + p_apmx->high_priority ? "high" : "low", + p_buf); + } + } + else + { + /* No need for the frame handler to free p_buf as it is NULL */ + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Did not put incoming %s prio alarm frame in queue, " + "as p_buf is NULL.\n", + __LINE__, + p_apmx->high_priority ? "high" : "low"); + ret = 1; + } + + return ret; +} + +/********************* Scheduler callback ************************************/ + +/** + * @internal + * Timeout while waiting for an alarm TACK from the controller. + * + * Re-send the alarm frame unless the number of re-transmits have been reached. + * If an ACK is received then the APMS state is no longer WTACK and the + * frame is not re-sent (and the timer stops). + * + * Might free the frame buffer. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * APMS: TimerExpired + * APMS: A_Data_req retransmission (This is LMPM_A_Data.req via macro) + * + * @param net InOut: The p-net stack instance + * @param arg + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +static void pf_alarm_apms_timeout ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_apmx_t * p_apmx = (pf_apmx_t *)arg; + pnal_buf_t * p_rta; + + pf_scheduler_reset_handle (&p_apmx->resend_timeout); + + if (p_apmx->apms_state == PF_APMS_STATE_WTACK) + { + if (p_apmx->p_rta == NULL) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): No alarm frame available for resending.\n", + __LINE__); + } + else if (p_apmx->resend_counter > 0) + { + p_apmx->resend_counter--; + + /* Retransmit */ + if (p_apmx->p_ar->alarm_cr_request.alarm_cr_properties.transport_udp == true) + { + /* ToDo: Send as UDP frame */ + } + else + { + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Re-sending %s prio alarm frame in %p\n", + __LINE__, + p_apmx->high_priority ? "high" : "low", + p_apmx->p_rta); + (void)pf_eth_send_on_management_port (net, p_apmx->p_rta); + } + + if ( + pf_scheduler_add ( + net, + p_apmx->timeout_us, + pf_alarm_apms_timeout, + p_apmx, + &p_apmx->resend_timeout) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to schedule resending of alarm frame\n", + __LINE__); + } + } + else + { +#if 0 + /* + * After the call to pf_alarm_error_ind() above CMDEV closes the connection. + * CMDEV also calls CMSU which calls pf_alarm_close. + * + * There is no need to do anything more here!!! + */ + LOG_DEBUG(PF_AL_BUF_LOG, "Alarm(%d): Free saved RTA buffer %p\n", __LINE__, p_apmx->p_rta); + if (p_apmx->p_rta != NULL) + { + p_rta = p_apmx->p_rta; + p_apmx->p_rta = NULL; + pnal_buf_free(p_rta); + } + + pf_alarm_alpmi_apms_a_data_cnf(net, p_apmx, -1); + pf_alarm_alpmr_apms_a_data_cnf(net, p_apmx, -1); + +#endif + + /* Timeout */ + p_apmx->apms_state = PF_APMS_STATE_OPEN; + + p_apmx->p_ar->err_cls = PNET_ERROR_CODE_1_APMS; + p_apmx->p_ar->err_code = PNET_ERROR_CODE_2_ABORT_AR_ALARM_SEND_CNF_NEG; + pf_alarm_error_ind ( + net, + p_apmx, + p_apmx->p_ar->err_cls, + p_apmx->p_ar->err_code); + /* Standard also says APMS_A_Data.cnf(-) */ + } + } + else + { + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Skip scheduled alarm retransmission as we are in in state " + "%s. Free saved %s prio alarm output buffer %p\n", + __LINE__, + pf_alarm_apms_state_to_string (p_apmx->apms_state), + p_apmx->high_priority ? "high" : "low", + p_apmx->p_rta); + if (p_apmx->p_rta != NULL) + { + p_rta = p_apmx->p_rta; + p_apmx->p_rta = NULL; + pnal_buf_free (p_rta); + } + } +} + +/****************************************************************************/ + +/** + * @internal + * Initialize and start an AMPX instance (an APMS/APMR pair). + * + * It registers handlers for incoming alarm frames. + * + * APMS: APMS_Activate_req + * APMS: APMS_Activate_cnf(+/-) via return value and err_cls/err_code + * APMR: APMR_Activate_req + * APMR: APMR_Activate_cnf(+/-) via return value and err_cls/err_code + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apmx_activate (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = 0; /* Assume all goes well */ + uint16_t ix; + + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Activating ALPMX for AREP %u\n", + __LINE__, + p_ar->arep); + + for (ix = 0; ix < NELEMENTS (p_ar->apmx); ix++) + { + if (p_ar->apmx[ix].apms_state != PF_APMS_STATE_CLOSED) + { + p_ar->err_cls = PNET_ERROR_CODE_1_APMS; + p_ar->err_code = PNET_ERROR_CODE_2_APMS_INVALID_STATE; + + ret = -1; + } + else if (p_ar->apmx[ix].apmr_state != PF_APMR_STATE_CLOSED) + { + p_ar->err_cls = PNET_ERROR_CODE_1_APMR; + p_ar->err_code = PNET_ERROR_CODE_2_APMR_INVALID_STATE; + + ret = -1; + } + else + { + memcpy ( + &p_ar->apmx[ix].da, + &p_ar->ar_param.cm_initiator_mac_add, + sizeof (p_ar->apmx[ix].da)); + + p_ar->apmx[ix].src_ref = p_ar->alarm_cr_result.remote_alarm_reference; + p_ar->apmx[ix].dst_ref = p_ar->alarm_cr_request.local_alarm_reference; + + /* APMS counters */ + p_ar->apmx[ix].send_seq_count = 0xffff; + p_ar->apmx[ix].send_seq_count_o = 0xfffe; + + /* APMR counters */ + p_ar->apmx[ix].exp_seq_count = 0xffff; + p_ar->apmx[ix].exp_seq_count_o = 0xfffe; + + if (ix == 0) + { + p_ar->apmx[ix].high_priority = false; + p_ar->apmx[ix].vlan_prio = ALARM_VLAN_PRIO_LOW; + p_ar->apmx[ix].block_type_alarm_notify = + PF_BT_ALARM_NOTIFICATION_LOW; + p_ar->apmx[ix].block_type_alarm_ack = PF_BT_ALARM_ACK_LOW; + p_ar->apmx[ix].frame_id = PF_FRAME_ID_ALARM_LOW; + } + else + { + p_ar->apmx[ix].high_priority = true; + p_ar->apmx[ix].vlan_prio = ALARM_VLAN_PRIO_HIGH; + p_ar->apmx[ix].block_type_alarm_notify = + PF_BT_ALARM_NOTIFICATION_HIGH; + p_ar->apmx[ix].block_type_alarm_ack = PF_BT_ALARM_ACK_HIGH; + p_ar->apmx[ix].frame_id = PF_FRAME_ID_ALARM_HIGH; + } + + /* Input alarm frame queue */ + pf_alarm_queue_mutex_create ( + &p_ar->apmx[ix].alarm_receive_q.accountant); + pf_alarm_receive_queue_reset (&p_ar->apmx[ix].alarm_receive_q); + + p_ar->apmx[ix].timeout_us = + 100 * 1000 * p_ar->alarm_cr_request.rta_timeout_factor; + p_ar->apmx[ix].resend_counter = 0; /* Updated in + * pf_alarm_apms_apms_a_data_req() + */ + + p_ar->apmx[ix].p_ar = p_ar; + p_ar->apmx[ix].p_alpmx = &p_ar->alpmx[ix]; + + pf_scheduler_init_handle (&p_ar->apmx[ix].resend_timeout, "apmx"); + + p_ar->apmx[ix].apms_state = PF_APMS_STATE_OPEN; + p_ar->apmx[ix].apmr_state = PF_APMR_STATE_OPEN; + } + } + + pf_eth_frame_id_map_add ( + net, + p_ar->apmx[0].frame_id, + pf_alarm_apmr_frame_handler, + &p_ar->apmx[0]); + pf_eth_frame_id_map_add ( + net, + p_ar->apmx[1].frame_id, + pf_alarm_apmr_frame_handler, + &p_ar->apmx[1]); + + return ret; +} + +/** + * @internal + * APMS is checking incoming TACK (the ack_seq_num) + * + * APMS: A_Data_ind (Implements parts of it. This ia LMPM_A_Data.ind via macro) + * + * Does free the outgoing alarm frame buffer. + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMX instance. + * @param p_fixed In: The fixed part of the ALARM message. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apms_a_data_ind ( + pnet_t * net, + pf_apmx_t * p_apmx, + const pf_alarm_fixed_t * p_fixed) +{ + int ret = -1; + pnal_buf_t * p_rta; + + if ( + (p_fixed->pdu_type.type == PF_RTA_PDU_TYPE_ACK) || + (p_fixed->pdu_type.type == PF_RTA_PDU_TYPE_DATA)) + { + if (p_apmx->apms_state == PF_APMS_STATE_WTACK) + { + if (p_fixed->ack_seq_nbr == p_apmx->send_seq_count) + { + p_apmx->send_seq_count_o = p_apmx->send_seq_count; + p_apmx->send_seq_count = (p_apmx->send_seq_count + 1) & 0x7fff; + + LOG_DEBUG ( + PF_AL_BUF_LOG, + "Alarm(%d): Valid TACK received via DATA or ACK frame. Free " + "saved outgoing %s prio alarm buffer if necessary %p\n", + __LINE__, + p_apmx->high_priority ? "high" : "low", + p_apmx->p_rta); + if (p_apmx->p_rta != NULL) + { + p_rta = p_apmx->p_rta; + p_apmx->p_rta = NULL; + pnal_buf_free (p_rta); + } + + /* This is APMS_A_Data.cnf(+) */ + pf_alarm_alpmi_apms_a_data_cnf (net, p_apmx, 0); + + pf_alarm_alpmr_apms_a_data_cnf (net, p_apmx, 0); + + ret = 0; + + p_apmx->apms_state = PF_APMS_STATE_OPEN; + } + else + { + /* + * Ignore! + * Wrong sequence number. + * A timeout occurs and the frame will be re-sent. + */ + LOG_WARNING ( + PF_ALARM_LOG, + "Alarm(%d): Wrong ACK sequence number. Received %u but it " + "should be %u. Ignore.\n", + __LINE__, + p_fixed->ack_seq_nbr, + p_apmx->send_seq_count); + ret = 0; + } + } + else + { + /* + * Ignore! + * A timeout occurred and the frame has already been freed. + */ + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): We are not waiting for transport ack, so do not clean " + "alarm frame output buffer.\n", + __LINE__); + ret = 0; + } + } + else + { + /* + * Ignore! + * This frame is not interesting. + */ + ret = 0; + } + + return ret; +} + +/** + * @internal + * Create and send a Profinet alarm frame to the IO-controller. + * + * Different frame variants: + * DATA with alarm notification + * DATA with ACK + * ERR + * NACK + * ACK (used as transport ACK = TACK) + * + * Messages with TACK == true are saved in the APMX instance in order + * to handle re-transmissions. + * + * APMS: A_Data_req (This is LMPM_A_Data.req via macro) + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMX instance. + * @param p_fixed In: The fixed part of the RTA-PDU. PDU type, + * TACK flag, sequence counters etc. + * @param p_alarm_data In: Alarm type, slot, subslot etc. + * Mandatory for DATA messages, otherwise NULL. + * The payload part of it (if any) is not used. + * @param maint_status In: The Maintenance status used for specific USI + * values (inserted only if not zero). + * @param payload_usi In: The payload USI. Only used for + * block type = alarm notify. + * Use 0 for no payload. + * @param payload_len In: Number of bytes of manufaturer data. + * Only used for manufacturer data ("USI format", + * payload USI <= 0x7FFF). + * May be 0 for manufacturer data. + * @param p_payload In: pf_diag_item_t, manufacturer data or NULL. + * Mandatory if payload_len > 0 or for USI + * values for diagnosis in standard format + * (which expect a pf_diag_item_t) + * @param p_pnio_status In: Mandatory for ERROR messages. + * For DATA messages a NULL value gives an Alarm + * Notification DATA message, otherwise an + * Alarm Ack DATA message. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apms_a_data_req ( + pnet_t * net, + pf_apmx_t * p_apmx, + const pf_alarm_fixed_t * p_fixed, + const pf_alarm_data_t * p_alarm_data, + uint32_t maint_status, + uint16_t payload_usi, + uint16_t payload_len, + const uint8_t * p_payload, + const pnet_pnio_status_t * p_pnio_status) +{ + int ret = -1; + uint16_t var_part_len_pos; + uint16_t var_part_len; + pnal_buf_t * p_rta; + uint8_t * p_buf = NULL; + uint16_t pos = 0; + const pnet_ethaddr_t * mac_address = pf_cmina_get_device_macaddr (net); + uint16_t u16 = 0; + + if (p_apmx->p_ar->alarm_cr_request.alarm_cr_properties.transport_udp == true) + { + /* ToDo: Create UDP frames */ + } + else + { + p_rta = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + if (p_rta == NULL) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to allocate output buffer for alarm", + __LINE__); + } + else + { + p_buf = p_rta->payload; + if (p_buf == NULL) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to allocate payload output buffer for " + "alarm.\n", + __LINE__); + } + else + { + LOG_DEBUG ( + PF_AL_BUF_LOG, + "Alarm(%d): Allocated alarm output buffer for %s %p\n", + __LINE__, + pf_alarm_pdu_type_to_string (p_fixed->pdu_type.type), + p_rta); + + /* Insert destination MAC address */ + pf_put_mem ( + &p_apmx->da, + sizeof (p_apmx->da), + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + + /* Insert source MAC address (our interface MAC address) */ + memcpy (&p_buf[pos], mac_address->addr, sizeof (pnet_ethaddr_t)); + pos += sizeof (pnet_ethaddr_t); + + /* Insert VLAN Tag protocol identifier (TPID) */ + pf_put_uint16 ( + true, + PNAL_ETHTYPE_VLAN, + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + + /* Insert VLAN prio (and VLAN ID=0) */ + u16 = (p_apmx->vlan_prio & 0x0007) << 13; /* Three leftmost bits */ + pf_put_uint16 (true, u16, PF_FRAME_BUFFER_SIZE, p_buf, &pos); + + /* Insert EtherType */ + pf_put_uint16 ( + true, + PNAL_ETHTYPE_PROFINET, + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + + /* Insert Profinet frame ID (first part of Ethernet frame payload) + */ + pf_put_uint16 ( + true, + p_apmx->frame_id, + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + + /* Insert alarm specific data */ + pf_put_alarm_fixed ( + true, + p_fixed, + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + + var_part_len_pos = pos; + /* var_part_len is not yet known */ + pf_put_uint16 (true, 0, PF_FRAME_BUFFER_SIZE, p_buf, &pos); + + /* Insert variable part of alarm payload for DATA and ERR */ + if (p_fixed->pdu_type.type == PF_RTA_PDU_TYPE_DATA) + { + if (p_pnio_status != NULL) + { + /* Build an Alarm Ack DATA message */ + pf_put_alarm_block ( + true, + p_apmx->block_type_alarm_ack, + p_alarm_data, + maint_status, + 0, /* No payload */ + 0, /* No payload */ + NULL, /* No payload */ + p_pnio_status, + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + } + else + { + /* Build an Alarm Notification DATA message */ + pf_put_alarm_block ( + true, + p_apmx->block_type_alarm_notify, + p_alarm_data, + maint_status, + payload_usi, + payload_len, + p_payload, + NULL, /* No PNIO status */ + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + } + } + else if (p_fixed->pdu_type.type == PF_RTA_PDU_TYPE_ERR) + { + CC_ASSERT (p_pnio_status != NULL); + pf_put_pnet_status ( + true, + p_pnio_status, + PF_FRAME_BUFFER_SIZE, + p_buf, + &pos); + } + else + { + /* NACK or ACK does not take any var part. */ + } + + /* Finally insert the correct VarPartLen */ + var_part_len = pos - (var_part_len_pos + sizeof (var_part_len)); + pf_put_uint16 ( + true, + var_part_len, + PF_FRAME_BUFFER_SIZE, + p_buf, + &var_part_len_pos); + + p_rta->len = pos; + + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Send an alarm frame %s %p Frame ID: 0x%04X " + "Alarmtype: %u Tack: %u USI: 0x%04X Payload len: %u " + "Send seq: %u ACK seq: %u\n", + __LINE__, + pf_alarm_pdu_type_to_string (p_fixed->pdu_type.type), + p_rta, + p_apmx->frame_id, + (p_alarm_data != NULL) ? p_alarm_data->alarm_type : 0, + p_fixed->add_flags.tack, + payload_usi, + payload_len, + p_fixed->send_seq_num, + p_fixed->ack_seq_nbr); + + if (pf_eth_send_on_management_port (net, p_rta) <= 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Error from pnal_eth_send(rta)\n", + __LINE__); + } + else + { + ret = 0; + } + } + + if (p_fixed->add_flags.tack == true) + { + if (p_apmx->p_rta == NULL) + { + LOG_DEBUG ( + PF_AL_BUF_LOG, + "Alarm(%d): Save alarm output buffer for later use. %p\n", + __LINE__, + p_rta); + p_apmx->p_rta = p_rta; + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): RTA alarm output buffer with TACK lost! " + "Free %p Existing buffer %p\n", + __LINE__, + p_rta, + p_apmx->p_rta); + pnal_buf_free (p_rta); + } + } + else + { + /* ACK, NAK and ERR buffers are not saved for retransmission. */ + LOG_DEBUG ( + PF_AL_BUF_LOG, + "Alarm(%d): Free unsaved alarm output buffer %p\n", + __LINE__, + p_rta); + pnal_buf_free (p_rta); + } + } + } + + return ret; +} + +/** + * @internal + * Create and send an DATA RTA-PDU + * + * This is done by APMS, and is triggered by ALPMI or ALPMR. + * + * The message will have TACK == true, and will be scheduled for retransmission. + * It uses pf_alarm_apms_a_data_req() for the sending. + * + * APMS: APMS_A_Data.req + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMS instance. Select high or low prio by + * using the correct instance. + * @param p_alarm_data In: Alarm details (Alarm type, slot, subslot, + * possibly payload etc) + * @param maint_status In: The Maintenance status used for specific USI + * values (inserted only if not zero). + * @param p_pnio_status In: Optional. Detailed error information (or NULL) + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apms_apms_a_data_req ( + pnet_t * net, + pf_apmx_t * p_apmx, + const pf_alarm_data_t * p_alarm_data, + uint32_t maint_status, + const pnet_pnio_status_t * p_pnio_status) +{ + int ret = -1; + pf_alarm_fixed_t fixed; + + if (p_apmx->apms_state != PF_APMS_STATE_OPEN) + { + /* TODO: The standard says err_cls = PNET_ERROR_CODE_1_APMS + err_code = PNET_ERROR_CODE_2_APMS_WRONG_STATE + */ + pf_alarm_alpmi_apms_a_data_cnf (net, p_apmx, -1); + pf_alarm_alpmr_apms_a_data_cnf (net, p_apmx, -1); + } + else + { + memset (&fixed, 0, sizeof (fixed)); + fixed.src_ref = p_apmx->src_ref; + fixed.dst_ref = p_apmx->dst_ref; + fixed.pdu_type.type = PF_RTA_PDU_TYPE_DATA; + fixed.pdu_type.version = PF_ALARM_PDU_TYPE_VERSION_1; + fixed.add_flags.window_size = 1; + fixed.add_flags.tack = true; + fixed.send_seq_num = p_apmx->send_seq_count; + fixed.ack_seq_nbr = p_apmx->exp_seq_count_o; + + p_apmx->resend_counter = p_apmx->p_ar->alarm_cr_request.rta_retries; + + ret = pf_alarm_apms_a_data_req ( + net, + p_apmx, + &fixed, + p_alarm_data, + maint_status, + p_alarm_data->payload.usi, + p_alarm_data->payload.len, + p_alarm_data->payload.data, + p_pnio_status); + + p_apmx->apms_state = PF_APMS_STATE_WTACK; + + /* APMS: A_Data_cnf (tells that incoming message is acyclic data) + In state PF_APMS_STATE_WTACK with A_Data_cnf we should start + retransmission timer */ + if ( + pf_scheduler_add ( + net, + p_apmx->timeout_us, + pf_alarm_apms_timeout, + p_apmx, + &p_apmx->resend_timeout) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to schedule sending of alarm frame\\n", + __LINE__); + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Close APMX (i.e. close APMS and APMR, for both high and low prio alarms) + * + * Sends a low prio alarm. + * Unregisters frame handlers. + * + * APMS: APMS_Close_req + * APMS: APMS_Close_cnf via return value and err_cls/err_code + * APMR: APMR_Close_req + * APMR: APMR_Close_cnf(+) + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param err_code In: Error code. See PNET_ERROR_CODE_2_* + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apmx_close (pnet_t * net, pf_ar_t * p_ar, uint8_t err_code) +{ + uint16_t ix; + pnet_pnio_status_t pnio_status; + pnal_buf_t * p_rta; + pf_alarm_fixed_t fixed; + + /* Send CLOSE alarm first, with low prio by using p_ar->apmx[0] */ + if (p_ar->apmx[0].apms_state != PF_APMS_STATE_CLOSED) + { + pnio_status.error_code = PNET_ERROR_CODE_RTA_ERROR; + pnio_status.error_decode = PNET_ERROR_DECODE_PNIO; + pnio_status.error_code_1 = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + pnio_status.error_code_2 = err_code; + + memset (&fixed, 0, sizeof (fixed)); + fixed.src_ref = p_ar->apmx[0].src_ref; + fixed.dst_ref = p_ar->apmx[0].dst_ref; + fixed.pdu_type.type = PF_RTA_PDU_TYPE_ERR; + fixed.pdu_type.version = PF_ALARM_PDU_TYPE_VERSION_1; + fixed.add_flags.window_size = 1; + fixed.add_flags.tack = false; + fixed.send_seq_num = p_ar->apmx[0].send_seq_count; + fixed.ack_seq_nbr = p_ar->apmx[0].exp_seq_count_o; + + if ( + pf_alarm_apms_a_data_req ( + net, + &p_ar->apmx[0], + &fixed, + NULL, /* No alarm_data */ + 0, + 0, + 0, + NULL, /* No payload */ + &pnio_status) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "alarm(%d): Could not send close alarm\n", + __LINE__); + } + } + + pf_eth_frame_id_map_remove (net, PF_FRAME_ID_ALARM_HIGH); + pf_eth_frame_id_map_remove (net, PF_FRAME_ID_ALARM_LOW); + + for (ix = 0; ix < NELEMENTS (p_ar->apmx); ix++) + { + /* Close APMS */ + if (p_ar->apmx[ix].apms_state != PF_APMS_STATE_CLOSED) + { + /* Free resources */ + /* StopTimer */ + pf_scheduler_remove_if_running (net, &p_ar->apmx[ix].resend_timeout); + + p_ar->apmx[ix].p_ar = NULL; + p_ar->apmx[ix].apms_state = PF_APMS_STATE_CLOSED; + } + + LOG_DEBUG ( + PF_AL_BUF_LOG, + "Alarm(%d): Free saved %s prio alarm output buffer %p for AREP %u if " + "necessary\n", + __LINE__, + p_ar->apmx[ix].high_priority ? "high" : "low", + p_ar->apmx[ix].p_rta, + p_ar->arep); + if (p_ar->apmx[ix].p_rta != NULL) + { + p_rta = p_ar->apmx[ix].p_rta; + p_ar->apmx[ix].p_rta = NULL; + pnal_buf_free (p_rta); + } + + /* Close APMR */ + if (p_ar->apmx[ix].apmr_state != PF_APMR_STATE_CLOSED) + { + p_ar->apmx[ix].apmr_state = PF_APMR_STATE_CLOSED; + + pf_alarm_receive_queue_reset (&p_ar->apmx[ix].alarm_receive_q); + pf_alarm_queue_mutex_destroy ( + &p_ar->apmx[ix].alarm_receive_q.accountant); + } + } + + return 0; +} + +/** + * @internal + * Send alarm APMR ACK (a TACK = transport acknowledge) + * + * It uses APMS pf_alarm_apms_a_data_req() for the sending. + * + * APMR: A_Data_req (Implements parts of it. This ia LMPM_A_Data.req via macro) + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMX instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apmr_send_ack (pnet_t * net, pf_apmx_t * p_apmx) +{ + int ret = -1; + pf_alarm_fixed_t fixed; + + memset (&fixed, 0, sizeof (fixed)); + fixed.src_ref = p_apmx->src_ref; + fixed.dst_ref = p_apmx->dst_ref; + fixed.pdu_type.type = PF_RTA_PDU_TYPE_ACK; + fixed.pdu_type.version = PF_ALARM_PDU_TYPE_VERSION_1; + fixed.add_flags.window_size = 1; + fixed.add_flags.tack = false; + fixed.send_seq_num = p_apmx->send_seq_count_o; + fixed.ack_seq_nbr = p_apmx->exp_seq_count_o; + + ret = pf_alarm_apms_a_data_req ( + net, + p_apmx, + &fixed, + NULL, /* No additional data for ACK message */ + 0, + 0, + 0, + NULL, /* No payload for ACK message */ + NULL); + + return ret; +} + +/** + * @internal + * Send alarm APMR NACK + * + * It uses APMS pf_alarm_apms_a_data_req() for the sending. + * + * APMR: A_Data_req (Implements parts of it. This ia LMPM_A_Data.req via macro) + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMX instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apmr_send_nack (pnet_t * net, pf_apmx_t * p_apmx) +{ + int ret = -1; + pf_alarm_fixed_t fixed; + + memset (&fixed, 0, sizeof (fixed)); + fixed.src_ref = p_apmx->src_ref; + fixed.dst_ref = p_apmx->dst_ref; + fixed.pdu_type.type = PF_RTA_PDU_TYPE_NACK; + fixed.pdu_type.version = PF_ALARM_PDU_TYPE_VERSION_1; + fixed.add_flags.window_size = 1; + fixed.add_flags.tack = false; + fixed.send_seq_num = p_apmx->send_seq_count_o; + fixed.ack_seq_nbr = p_apmx->exp_seq_count_o; + + ret = pf_alarm_apms_a_data_req ( + net, + p_apmx, + &fixed, + NULL, /* No additional data for NACK message */ + 0, + 0, + 0, + NULL, /* No payload for NACK message */ + NULL); + + return ret; +} + +/** + * @internal + * Handle reception of an RTA DATA PDU in APMR. + * + * Sends back ACK or NACK. + * Uses APMS to handle ack_seq_num in incoming messages. + * + * APMR: A_Data_ind (Implements part of it. This ia LMPM_A_Data.ind via macro) + * + * @param net InOut: The p-net stack instance + * @param p_apmx InOut: The APMX instance. + * @param p_fixed In: The fixed part of the RTA-DPU. + * @param data_len In: Length of the variable part of the RTA-PDU. + * @param p_get_info InOut: ? + * @param pos In: Position of ? + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apmr_a_data_ind ( + pnet_t * net, + pf_apmx_t * p_apmx, + const pf_alarm_fixed_t * p_fixed, + uint16_t data_len, + pf_get_info_t * p_get_info, + uint16_t pos) +{ + int ret = -1; + uint16_t start_pos = pos; + pf_block_header_t block_header; + pf_alarm_data_t alarm_data; + pnet_pnio_status_t pnio_status; + uint16_t data_usi; + const uint8_t * p_data; + + switch (p_apmx->apmr_state) + { + case PF_APMR_STATE_CLOSED: + /* Ignore */ + ret = 0; + break; + case PF_APMR_STATE_OPEN: + if (p_fixed->add_flags.tack == false) + { + /* Ignore */ + ret = 0; + } + else + { + if (p_fixed->send_seq_num == p_apmx->exp_seq_count) + { + pf_get_block_header (p_get_info, &pos, &block_header); + if (block_header.block_type == p_apmx->block_type_alarm_ack) + { + /* This message is an Alarm ACK PDU. Parse it and deliver to + * ALPMI. */ + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): The incoming Alarm DATA message is an ACK PDU. " + "Send seq: %u Trigger a TACK response.\n", + __LINE__, + p_fixed->send_seq_num); + + /* Tell APMS to handle ack_seq_num in message */ + (void)pf_alarm_apms_a_data_ind (net, p_apmx, p_fixed); + + p_apmx->exp_seq_count_o = p_apmx->exp_seq_count; + p_apmx->exp_seq_count = (p_apmx->exp_seq_count + 1) & 0x7fff; + + ret = pf_alarm_apmr_send_ack (net, p_apmx); + if (ret == 0) + { + p_apmx->apmr_state = PF_APMR_STATE_WCNF; + + /* APMR: A_Data_cnf is triggered here*/ + + pf_get_pnio_status (p_get_info, &pos, &pnio_status); + ret = pf_alarm_alpmi_apmr_a_data_ind ( + net, + p_apmx, + &pnio_status); /* APMR: APMR_A_Data.ind */ + if (ret == 0) + { + p_apmx->apmr_state = PF_APMR_STATE_OPEN; + } + } + } + else if (block_header.block_type == p_apmx->block_type_alarm_notify) + { + /* This message is an Alarm Notification PDU. Parse it and + * deliver to ALPMR. */ + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): The incoming Alarm DATA message is an alarm " + "notification PDU. Send seq: %u Trigger a TACK response.\n", + __LINE__, + p_fixed->send_seq_num); + + /* Tell APMS to handle ack_seq_num in message */ + (void)pf_alarm_apms_a_data_ind (net, p_apmx, p_fixed); + + p_apmx->exp_seq_count_o = p_apmx->exp_seq_count; + p_apmx->exp_seq_count = (p_apmx->exp_seq_count + 1) & 0x7fff; + + ret = pf_alarm_apmr_send_ack (net, p_apmx); + if (ret == 0) + { + p_apmx->apmr_state = PF_APMR_STATE_WCNF; + + /* APMR: A_Data_cnf is triggered here*/ + + pf_get_alarm_data (p_get_info, &pos, &alarm_data); + data_usi = pf_get_uint16 (p_get_info, &pos); + if (p_get_info->result == PF_PARSE_OK) + { + data_len -= (pos - start_pos); + p_data = &p_get_info->p_buf[pos]; + ret = pf_alarm_alpmr_apmr_a_data_ind ( + net, + p_apmx, + p_fixed, + &alarm_data, + data_len, + data_usi, + p_data); /* APMR: APMR_A_Data.ind */ + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Error parsing Alarm notification\n", + __LINE__); + ret = -1; + } + if (ret == 0) + { + p_apmx->apmr_state = PF_APMR_STATE_OPEN; + } + } + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Unhandled incoming alarm block type 0x%04x\n", + __LINE__, + block_header.block_type); + /* The standard says PNET_ERROR_CODE_1_ALPMR, + * PNET_ERROR_CODE_2_ALPMR_WRONG_ALARM_PDU internally */ + p_apmx->p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_apmx->p_ar->err_code = + PNET_ERROR_CODE_2_ABORT_AR_ALARM_IND_ERROR; + pf_alarm_error_ind ( + net, + p_apmx, + p_apmx->p_ar->err_cls, + p_apmx->p_ar->err_code); + ret = -1; + } + } + else if (p_fixed->send_seq_num == p_apmx->exp_seq_count_o) + { + /* Tell APMS to handle ack_seq_num in message */ + (void)pf_alarm_apms_a_data_ind (net, p_apmx, p_fixed); + + /* Re-send the ACK */ + (void)pf_alarm_apmr_send_ack (net, p_apmx); + ret = 0; + } + else + { + p_apmx->p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_apmx->p_ar->err_code = PNET_ERROR_CODE_2_ABORT_CODE_SEQ; + pnio_status.error_code = PNET_ERROR_CODE_PNIO; + pnio_status.error_decode = PNET_ERROR_DECODE_PNIO; + pnio_status.error_code_1 = p_apmx->p_ar->err_cls; + pnio_status.error_code_2 = p_apmx->p_ar->err_code; + pf_fspm_create_log_book_entry ( + net, + p_apmx->p_ar->arep, + &pnio_status, + __LINE__); + + (void)pf_alarm_apmr_send_nack (net, p_apmx); + ret = 0; + } + } + break; + case PF_APMR_STATE_WCNF: + /* WCNF is a virtual state in the case above */ + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): This should never happen!\n", + __LINE__); + break; + default: + /* Ignore */ + ret = 0; + break; + } + + return ret; +} + +/** + * @internal + * Return the alarm specifier and maintenance status for a specific sub-slot, + * by looking at all diagnosis items found for that subslot. + * + * Also includes the "current diag item" in the analysis. + * + * Describes diagnosis alarms. + * + * This corresponds to the "BuildSummarizedAlarmSpecifier" and + * "BuildSummarizedMaintenanceStatus" functions mentioned in the standard. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_diag_item In: The current diag item (may be NULL). + * @param p_alarm_spec Out: The computed alarm specifier. + * @param p_maint_status Out: The computed maintenance status. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_get_diag_summary ( + pnet_t * net, + const pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const pf_diag_item_t * p_diag_item, + pnet_alarm_spec_t * p_alarm_spec, + uint32_t * p_maint_status) +{ + pf_device_t * p_dev = NULL; + pf_subslot_t * p_subslot = NULL; + uint16_t item_ix; + pf_diag_item_t * p_item = NULL; + + if (pf_cmdev_get_device (net, &p_dev) == 0) + { + memset (p_alarm_spec, 0, sizeof (*p_alarm_spec)); + *p_maint_status = 0; + + /* Consider only alarms on this sub-slot. */ + if ( + pf_cmdev_get_subslot_full ( + net, + api_id, + slot_nbr, + subslot_nbr, + &p_subslot) == 0) + { + /* Collect all diags into maintenanceStatus*/ + + /* First handle the "current" (unreported) item. */ + if (p_diag_item != NULL) + { + pf_alarm_add_diag_item_to_summary ( + p_ar, + p_subslot, + p_diag_item, + p_alarm_spec, + p_maint_status); + } + + /* Now handle all the already reported diag items. */ + item_ix = p_subslot->diag_list; + pf_cmdev_get_diag_item (net, item_ix, &p_item); + while (p_item != NULL) + { + /* Collect all diags into maintenanceStatus*/ + pf_alarm_add_diag_item_to_summary ( + p_ar, + p_subslot, + p_item, + p_alarm_spec, + p_maint_status); + + item_ix = p_item->next; + pf_cmdev_get_diag_item (net, item_ix, &p_item); + } + } + } + + return 0; +} + +/** + * @internal + * Handle the reception of Alarm messages for one AR instance. + * + * This function reads alarm frames from both input queues of the AR. + * + * The message is normally processed in APMR/ALPMR, and indications may be sent + * to the application. Repeated for both low and high prio queue. + * + * Incoming ACK is sent to APMS. + * + * APMR + APMS: Implementation detail + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_apmr_periodic (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = 0; + pf_apmx_t * p_apmx; + uint16_t ix; + pnal_buf_t * p_buf; + pf_alarm_fixed_t fixed; + uint16_t var_part_len; + pnet_pnio_status_t pnio_status; + pf_get_info_t get_info; + uint16_t pos; + pf_apmr_msg_t inputmsg; + + /* + * Periodic is run cyclically. It may run for some time. + * If anything happens to the APMR (e.g. it is closed) during this time + * then we need to stop handling this APMR! + * Otherwise there will be lots of NULL pointers. + */ + for (ix = 0; ((ret == 0) && (ix < NELEMENTS (p_ar->apmx))); ix++) + { + p_apmx = &p_ar->apmx[ix]; + p_buf = NULL; + while ( + (ret == 0) && (p_apmx->apmr_state != PF_APMR_STATE_CLOSED) && + (pf_alarm_receive_queue_fetch (&p_apmx->alarm_receive_q, &inputmsg) == + 0)) + { + /* Got something - extract! */ + p_buf = inputmsg.p_buf; + + /* Skip frame_id */ + pos = inputmsg.frame_id_pos + sizeof (uint16_t); + + get_info.result = PF_PARSE_OK; + get_info.is_big_endian = true; + get_info.p_buf = (uint8_t *)p_buf->payload; + get_info.len = p_buf->len; + + /* Parse fixed part of incoming alarm frame */ + memset (&fixed, 0, sizeof (fixed)); + pf_get_alarm_fixed (&get_info, &pos, &fixed); + var_part_len = pf_get_uint16 (&get_info, &pos); + + if (fixed.pdu_type.version == 1) + { + switch (fixed.pdu_type.type) + { + case PF_RTA_PDU_TYPE_ACK: + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Got %s prio alarm ACK frame from queue. %p\n", + __LINE__, + p_apmx->high_priority ? "high" : "low", + p_buf); + if (var_part_len == 0) + { + /* Tell APMS to check for and handle ACK */ + (void)pf_alarm_apms_a_data_ind (net, p_apmx, &fixed); + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Wrong var_part_len %u for incoming ACK " + "frame\n", + __LINE__, + (unsigned)var_part_len); + ret = 0; /* Just ignore */ + } + break; + case PF_RTA_PDU_TYPE_NACK: + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Got %s prio alarm NACK frame from queue. %p\n", + __LINE__, + p_apmx->high_priority ? "high" : "low", + p_buf); + /* APMS: A_Data_ind (Implements parts of it, for NACK) */ + if (var_part_len == 0) + { + /* Ignore */ + ret = 0; + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Wrong var_part_len %u for incoming " + "NACK frame\n", + __LINE__, + (unsigned)var_part_len); + ret = 0; /* Just ignore */ + } + break; + case PF_RTA_PDU_TYPE_DATA: + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Got incoming %s prio alarm DATA frame from " + "queue. %p. " + "Length on wire: %d Var part len: %d\n", + __LINE__, + p_apmx->high_priority ? "high" : "low", + p_buf, + p_buf->len, + var_part_len); + ret = pf_alarm_apmr_a_data_ind ( + net, + p_apmx, + &fixed, + var_part_len, + &get_info, + pos); + break; + case PF_RTA_PDU_TYPE_ERR: + if (var_part_len == 4) /* sizeof(pf_pnio_status_t) */ + { + /* APMR: A_Data_ind (Implements parts of it, for ERR) */ + /* APMS: A_Data_ind (Implements parts of it, for ERR) */ + pf_get_pnio_status (&get_info, &pos, &pnio_status); + + /* Should we also be able to receive ERR frames to APMS? + * Should its state have an effect of the result? */ + switch (p_apmx->apmr_state) + { + case PF_APMR_STATE_OPEN: + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Got incoming alarm ERROR frame from " + "queue. %p. Error code 1 (ERRCLS): %0x02x " + "Error code 2 (ERRCODE): %0x02x\n", + __LINE__, + p_buf, + pnio_status.error_code_1, + pnio_status.error_code_2); + pf_alarm_error_ind ( + net, + p_apmx, + pnio_status.error_code_1, + pnio_status.error_code_2); + break; + default: + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Alarm received from IO-controller, but " + "the APMR state is %s\n", + __LINE__, + pf_alarm_apmr_state_to_string (p_apmx->apmr_state)); + pf_alarm_error_ind ( + net, + p_apmx, + PNET_ERROR_CODE_1_APMR, + PNET_ERROR_CODE_2_APMR_INVALID_STATE); + break; + } + + ret = 0; + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Alarm ERROR frame received from " + "IO-controller, but it has wrong var_part_len %u\n", + __LINE__, + (unsigned)var_part_len); + /* Ignore */ + ret = 0; + } + break; + default: + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Alarm received from IO-controller, but it " + "has wrong PDU-Type.type %u\n", + __LINE__, + (unsigned)fixed.pdu_type.type); + /* Ignore */ + ret = 0; + break; + } + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Alarm received from IO-controller, but it " + "has wrong PDU-Type.version %u\n", + __LINE__, + (unsigned)fixed.pdu_type.version); + /* Ignore */ + ret = 0; + } + + LOG_DEBUG ( + PF_AL_BUF_LOG, + "Alarm(%d): Free received buffer %p\n", + __LINE__, + p_buf); + pnal_buf_free (p_buf); + p_buf = NULL; + } + } + + return ret; +} + +/** + * Send alarm + * - Update diag summary + * - Update sequence numbers + * - Hand over to alarm to APMS instance + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_apmx InOut: The APMX instance. + * @param alarm_data In: Alarm details (Alarm type, slot, subslot, + * possibly payload etc) + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_alarm_send_internal ( + pnet_t * net, + pf_ar_t * p_ar, + pf_apmx_t * p_apmx, + pf_alarm_data_t * alarm_data) +{ + int ret = -1; + uint32_t maint_status = 0; + + CC_ASSERT (net->global_alarm_enable == true); + CC_ASSERT (p_ar->alarm_enable == true); + CC_ASSERT (p_apmx->p_alpmx->alpmi_state == PF_ALPMI_STATE_W_ALARM); + + (void)pf_alarm_get_diag_summary ( + net, + p_ar, + alarm_data->api_id, + alarm_data->slot_nbr, + alarm_data->subslot_nbr, + NULL, + &alarm_data->alarm_specifier, + &maint_status); + + /* Calculate sequence numbers */ + alarm_data->sequence_number = p_apmx->p_alpmx->sequence_number; + p_apmx->p_alpmx->prev_sequence_number = p_apmx->p_alpmx->sequence_number; + p_apmx->p_alpmx->sequence_number++; + p_apmx->p_alpmx->sequence_number %= 0x0800; + + /* Create and send an DATA RTA-PDU */ + ret = pf_alarm_apms_apms_a_data_req ( + net, + p_apmx, + alarm_data, + maint_status, + NULL); + + if (ret == 0) + { + p_apmx->p_alpmx->alpmi_state = PF_ALPMI_STATE_W_ACK; + } + else + { + LOG_ERROR (PF_ALARM_LOG, "Alarm(%d): Failed to send alarm\n", __LINE__); + } + + return ret; +} + +/************************ Queue handling ************************************/ + +/** + * @internal + * Create a mutex for a queue. + * + * Note that on first usage, the pf_queue_accountant_t must be fully cleared. + * + * @param p_accountant InOut: Queue accountant + */ +void pf_alarm_queue_mutex_create (pf_queue_accountant_t * p_accountant) +{ + if (p_accountant->mutex == NULL) + { + p_accountant->mutex = os_mutex_create(); + } +} + +/** + * @internal + * Destroy a mutex for a queue. + * + * @param p_accountant InOut: Queue accountant + */ +void pf_alarm_queue_mutex_destroy (pf_queue_accountant_t * p_accountant) +{ + if (p_accountant->mutex != NULL) + { + os_mutex_destroy (p_accountant->mutex); + p_accountant->mutex = NULL; + } +} + +/** + * @internal + * Check if the queue is available (initialized). + * + * @param p_accountant InOut: Queue accountant + * @return true if the queue is available + * false if the queue not is initialized + */ +bool pf_alarm_queue_is_available (pf_queue_accountant_t * p_accountant) +{ + return p_accountant->mutex != NULL; +} + +/** + * @internal + * Lock a queue. + * + * @param p_accountant InOut: Queue accountant + */ +static void pf_alarm_queue_lock (pf_queue_accountant_t * p_accountant) +{ + CC_ASSERT (p_accountant->mutex != NULL); + os_mutex_lock (p_accountant->mutex); +} + +/** + * @internal + * Unlock a queue. + * + * @param p_accountant InOut: Queue accountant + */ +static void pf_alarm_queue_unlock (pf_queue_accountant_t * p_accountant) +{ + CC_ASSERT (p_accountant->mutex != NULL); + os_mutex_unlock (p_accountant->mutex); +} + +/** + * @internal + * Reset the queue indexes. + * + * NOTE: Remember to lock/unlock the queue before and after this operation. + * + * @param p_accountant InOut: Queue accountant + */ +static void pf_alarm_queue_accountant_reset ( + pf_queue_accountant_t * p_accountant) +{ + p_accountant->count = 0; + p_accountant->read_index = 0; + p_accountant->write_index = 0; +} + +/** + * @internal + * Get next write index for a queue. + * + * NOTE: Remember to lock/unlock the queue before and after this operation. + * + * @param p_accountant InOut: Queue accountant + * @param p_write_index Out: Write index to use + * @return 0 if there is room in the queue + * -1 if the queue is full + */ +static int pf_alarm_queue_get_writeindex ( + pf_queue_accountant_t * p_accountant, + uint16_t * p_write_index) +{ + if (p_accountant->count >= PNET_MAX_ALARMS) + { + return -1; + } + + *p_write_index = p_accountant->write_index; + + p_accountant->write_index++; + if (p_accountant->write_index >= PNET_MAX_ALARMS) + { + p_accountant->write_index = 0; + } + p_accountant->count++; + + return 0; +} + +/** + * @internal + * Get next read index for a queue. + * + * NOTE: Remember to lock/unlock the queue before and after this operation. + * + * @param p_accountant InOut: Queue accountant + * @param p_read_index Out: Read index to use + * @return 0 if the queue has an item to read + * -1 if the queue is empty + */ +static int pf_alarm_queue_get_readindex ( + pf_queue_accountant_t * p_accountant, + uint16_t * p_read_index) +{ + if (p_accountant->count == 0) + { + return -1; + } + + *p_read_index = p_accountant->read_index; + + p_accountant->read_index++; + if (p_accountant->read_index >= PNET_MAX_ALARMS) + { + p_accountant->read_index = 0; + } + p_accountant->count--; + + return 0; +} + +/** + * Reset queue for incoming alarm frames. Will free corresponding buffers. + * + * Note: The mutex must have been created before. + * First time the pf_alarm_receive_queue_t is used it should be fully + * cleared. So if this function is used immediately thereafter on the + * queue it is a no-op. + * + * @param q InOut: Alarm receive queue + */ +void pf_alarm_receive_queue_reset (pf_alarm_receive_queue_t * q) +{ + uint16_t ix; + + if (pf_alarm_queue_is_available (&q->accountant) == false) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Resetting non-initialized queue.\n", + __LINE__); + + return; + } + + pf_alarm_queue_lock (&q->accountant); + pf_alarm_queue_accountant_reset (&q->accountant); + for (ix = 0; ix < PNET_MAX_ALARMS; ix++) + { + if (q->items[ix].p_buf != NULL) + { + pnal_buf_free (q->items[ix].p_buf); + q->items[ix].p_buf = NULL; + } + q->items[ix].frame_id_pos = 0; + } + pf_alarm_queue_unlock (&q->accountant); +} + +/** + * Post an alarm frame to the receive queue + * @param q InOut: Alarm receive queue + * @param p_alarm_frame In: Alarm frame + * @return 0 is returned if an alarm frame is posted onto the queue + * -1 is returned if queue is non-available or full (Logs an error) + */ +int pf_alarm_receive_queue_post ( + pf_alarm_receive_queue_t * q, + const pf_apmr_msg_t * p_alarm_frame) +{ + uint16_t write_index; + int ret = -1; + + if (pf_alarm_queue_is_available (&q->accountant) == false) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Receive queue is not available, alarm frame dropped.\n", + __LINE__); + + return ret; + } + + pf_alarm_queue_lock (&q->accountant); + if (pf_alarm_queue_get_writeindex (&q->accountant, &write_index) == 0) + { + q->items[write_index].frame_id_pos = p_alarm_frame->frame_id_pos; + q->items[write_index].p_buf = p_alarm_frame->p_buf; + ret = 0; + } + pf_alarm_queue_unlock (&q->accountant); + + if (ret != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Receive queue is full, alarm frame dropped.\n", + __LINE__); + } + + return ret; +} + +/** + * Fetch an alarm frame from the receive queue + * @param q InOut: Alarm receive queue + * @param p_alarm_frame Out: Alarm frame + * @return 0 is returned if an alarm frame is fetched from the queue + * -1 is returned if queue is non-available or empty + */ +int pf_alarm_receive_queue_fetch ( + pf_alarm_receive_queue_t * q, + pf_apmr_msg_t * p_alarm_frame) +{ + uint16_t read_index; + int ret = -1; + + if (pf_alarm_queue_is_available (&q->accountant) == false) + { + return ret; + } + + pf_alarm_queue_lock (&q->accountant); + if (pf_alarm_queue_get_readindex (&q->accountant, &read_index) == 0) + { + p_alarm_frame->frame_id_pos = q->items[read_index].frame_id_pos; + q->items[read_index].frame_id_pos = 0; + + p_alarm_frame->p_buf = q->items[read_index].p_buf; + q->items[read_index].p_buf = NULL; + + ret = 0; + } + pf_alarm_queue_unlock (&q->accountant); + + return ret; +} + +/** + * Reset queue for outgoing alarms + * @param q InOut: Alarm send queue (High or low prio) + */ +void pf_alarm_send_queue_reset (pf_alarm_send_queue_t * q) +{ + if (pf_alarm_queue_is_available (&q->accountant) == false) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Resetting non-initialized queue.\n", + __LINE__); + + return; + } + + pf_alarm_queue_lock (&q->accountant); + pf_alarm_queue_accountant_reset (&q->accountant); + memset (q->items, 0, sizeof (q->items)); + pf_alarm_queue_unlock (&q->accountant); +} + +/** + * Post an alarm to the send queue + * @param q InOut: Alarm send queue (High or low prio) + * @param p_alarm_data In: Alarm details (Alarm type, slot, subslot, + * possibly payload etc) + * @return 0 is returned if an alarm is posted onto the queue + * -1 is returned if queue is non-available or full (Logs an error) + */ +int pf_alarm_send_queue_post ( + pf_alarm_send_queue_t * q, + const pf_alarm_data_t * p_alarm_data) +{ + uint16_t write_index; + int ret = -1; + if (pf_alarm_queue_is_available (&q->accountant) == false) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Send queue is not available, alarm message dropped.\n", + __LINE__); + + return ret; + } + + pf_alarm_queue_lock (&q->accountant); + if (pf_alarm_queue_get_writeindex (&q->accountant, &write_index) == 0) + { + memcpy (&q->items[write_index], p_alarm_data, sizeof (*p_alarm_data)); + ret = 0; + } + pf_alarm_queue_unlock (&q->accountant); + + if (ret != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Send queue is full, alarm message dropped.\n", + __LINE__); + } + + return ret; +} + +/** + * Fetch an alarm from the send queue + * @param q InOut: Alarm send queue (High or low prio) + * @param p_alarm_data Out: Alarm details (Alarm type, slot, subslot, + * possibly payload etc) + * @return 0 is returned if an alarm is fetched from the queue + * -1 is returned if queue is empty or non-available + */ +int pf_alarm_send_queue_fetch ( + pf_alarm_send_queue_t * q, + pf_alarm_data_t * p_alarm_data) +{ + uint16_t read_index; + int ret = -1; + + if (pf_alarm_queue_is_available (&q->accountant) == false) + { + return ret; + } + + pf_alarm_queue_lock (&q->accountant); + if (pf_alarm_queue_get_readindex (&q->accountant, &read_index) == 0) + { + memcpy (p_alarm_data, &q->items[read_index], sizeof (*p_alarm_data)); + ret = 0; + } + pf_alarm_queue_unlock (&q->accountant); + + return ret; +} + +/****************************************************************************/ + +/** + * @internal + * Handle outgoing alarm messages for one AR instance. + * + * When called this function checks the ALMPI state for high and + * low priority alarms. If state is PF_ALPMI_STATE_W_ALARM and a alarm + * is found in the send queue this alarm is sent. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static void pf_alarm_almpi_periodic (pnet_t * net, pf_ar_t * p_ar) +{ + int res = 0; + int16_t prio; + pf_apmx_t * p_apmx; + pf_alarm_send_queue_t * q; + pf_alarm_data_t alarm_data; + + if (p_ar->alarm_enable == true) + { + for (prio = 1; prio >= 0; prio--) + { + p_apmx = &p_ar->apmx[prio]; + q = &p_ar->alarm_send_q[prio]; + if (p_apmx->p_alpmx->alpmi_state == PF_ALPMI_STATE_W_ALARM) + { + res = pf_alarm_send_queue_fetch (q, &alarm_data); + if (res == 0) + { + (void)pf_alarm_send_internal (net, p_ar, p_apmx, &alarm_data); + /* + * If a high prio alarm is found don't check low prio + * alarms until next call of this operation. + */ + break; + } + } + } + } +} + +/************************ Common alarm functions *****************************/ + +void pf_alarm_enable (pf_ar_t * p_ar) +{ + p_ar->alarm_enable = true; +} + +void pf_alarm_disable (pf_ar_t * p_ar) +{ + p_ar->alarm_enable = false; +} + +bool pf_alarm_pending (const pf_ar_t * p_ar) +{ + /* ToDo: Handle this call from cmpbe. */ + return false; +} + +int pf_alarm_activate (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = 0; /* Assume all goes well */ + int16_t i; + pf_alarm_send_queue_t * q; + + for (i = 0; i < PF_ALARM_NUMBER_OF_PRIORITY_LEVELS; i++) + { + q = &p_ar->alarm_send_q[i]; + pf_alarm_queue_mutex_create (&q->accountant); + pf_alarm_send_queue_reset (q); + } + + if (pf_alarm_alpmx_activate (p_ar) != 0) + { + ret = -1; + } + + if (pf_alarm_apmx_activate (net, p_ar) != 0) + { + ret = -1; + } + + return ret; +} + +int pf_alarm_close (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = 0; + int16_t i; + pf_alarm_send_queue_t * q; + + for (i = 0; i < PF_ALARM_NUMBER_OF_PRIORITY_LEVELS; i++) + { + q = &p_ar->alarm_send_q[i]; + pf_alarm_send_queue_reset (q); + pf_alarm_queue_mutex_destroy (&q->accountant); + } + + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Closing alarm instance for AREP %u. Sending alarm with " + "error_code2 = 0x%X\n", + __LINE__, + p_ar->arep, + p_ar->err_code); + if (pf_alarm_alpmx_close (p_ar) != 0) + { + ret = -1; + } + + if (pf_alarm_apmx_close (net, p_ar, p_ar->err_code) != 0) + { + ret = -1; + } + + return ret; +} + +/** + * @internal + * Update the alarm specifier and maintenance status from a diagnosis item. + * + * Add one diag item to the alarm summary (this function is typically executed + * several times on different diagnosis items to update the same alarm + * specifier and maintenance status values). + * + * Looks at: + * p_diag_item->usi + * p_diag_item->fmt.std.ch_properties + * p_diag_item->fmt.std.qual_ch_qualifier + * + * @param p_ar In: The AR instance. + * @param p_subslot In: The subslot instance. + * @param p_diag_item In: The diag item. + * @param p_alarm_spec InOut: The updated alarm specifier. + * Describes diagnosis alarms. + * @param p_maint_status InOut: The updated maintenance status. + * Describes diagnosis alarms. + */ +void pf_alarm_add_diag_item_to_summary ( + const pf_ar_t * p_ar, + const pf_subslot_t * p_subslot, + const pf_diag_item_t * p_diag_item, + pnet_alarm_spec_t * p_alarm_spec, + uint32_t * p_maint_status) +{ + bool is_fault = false; + bool is_maintenance_demanded = false; + bool is_maintenance_required = false; + bool is_appears = false; + bool is_same_ar = false; + uint32_t severity_qualifier = 0; + + /* Is the diagnosis on the same AR? */ + if (p_subslot->p_ar == p_ar) + { + is_same_ar = true; + } + + if (p_diag_item->usi < PF_USI_CHANNEL_DIAGNOSIS) + { + /* Diagnosis in manufacturer specific format (always FAULT) */ + + p_alarm_spec->manufacturer_diagnosis = true; + p_alarm_spec->submodule_diagnosis = true; + if (is_same_ar == true) + { + p_alarm_spec->ar_diagnosis = true; + } + } + else + { + /* Diagnosis in standard format */ + + severity_qualifier = + (p_diag_item->fmt.std.qual_ch_qualifier & + PF_DIAG_QUALIFIED_SEVERITY_MASK); + + /* Severity "Maintenance required" */ + if ( + PF_DIAG_CH_PROP_MAINT_GET (p_diag_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_REQUIRED) + { + is_maintenance_required = true; + } + else if ( + PF_DIAG_CH_PROP_MAINT_GET (p_diag_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_QUALIFIED) + { + if ((severity_qualifier & PF_DIAG_QUALIFIER_MASK_REQUIRED) > 0) + { + is_maintenance_required = true; + } + } + + /* Severity "Maintenance demanded" */ + if ( + PF_DIAG_CH_PROP_MAINT_GET (p_diag_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_DEMANDED) + { + is_maintenance_demanded = true; + } + else if ( + PF_DIAG_CH_PROP_MAINT_GET (p_diag_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_QUALIFIED) + { + if ((severity_qualifier & PF_DIAG_QUALIFIER_MASK_DEMANDED) > 0) + { + is_maintenance_demanded = true; + } + } + + /* Severity "Fault" */ + if ( + PF_DIAG_CH_PROP_MAINT_GET (p_diag_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_FAULT) + { + is_fault = true; + } + else if ( + PF_DIAG_CH_PROP_MAINT_GET (p_diag_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_QUALIFIED) + { + if ((severity_qualifier & PF_DIAG_QUALIFIER_MASK_FAULT) > 0) + { + is_fault = true; + } + } + + /* Is the diagnosis appearing */ + if ( + PF_DIAG_CH_PROP_SPEC_GET (p_diag_item->fmt.std.ch_properties) == + PF_DIAG_CH_PROP_SPEC_APPEARS) + { + is_appears = true; + } + + /* Maintenance qualifier bits */ + *p_maint_status |= severity_qualifier; + + /* MaintenanceRequired and MaintenanceDemanded bits */ + if (is_maintenance_required == true) + { + *p_maint_status |= PF_DIAG_BIT_MAINTENANCE_REQUIRED; + } + if (is_maintenance_demanded == true) + { + *p_maint_status |= PF_DIAG_BIT_MAINTENANCE_DEMANDED; + } + + /* Alarm specifiers for diagnosis in standard format */ + if (is_appears == true) + { + p_alarm_spec->channel_diagnosis = true; + } + if (is_appears == true && is_fault == true) + { + p_alarm_spec->submodule_diagnosis = true; + } + if (is_appears == true && is_fault == true && is_same_ar == true) + { + p_alarm_spec->ar_diagnosis = true; + } + } +} + +int pf_alarm_periodic (pnet_t * net) +{ + uint16_t ix; + pf_ar_t * p_ar; + + if (net->global_alarm_enable == true) + { + for (ix = 0; ix < PNET_MAX_AR; ix++) + { + p_ar = pf_ar_find_by_index (net, ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + /* Handle incoming alarm frames */ + (void)pf_alarm_apmr_periodic (net, p_ar); + + /* Handle outgoing alarm messages */ + (void)pf_alarm_almpi_periodic (net, p_ar); + } + } + } + + return 0; +} + +int pf_alarm_alpmr_alarm_ack ( + pnet_t * net, + pf_ar_t * p_ar, + const pnet_alarm_argument_t * p_alarm_argument, + const pnet_pnio_status_t * p_pnio_status) +{ + int ret = -1; + pf_alarm_data_t alarm_data; + pf_apmx_t * p_apmx = &p_ar->apmx[1]; /* Always use high prio. */ + pf_alpmx_t * p_alpmx = &p_ar->alpmx[1]; + + CC_ASSERT (p_alarm_argument != NULL); /* From user code */ + CC_ASSERT (p_pnio_status != NULL); + + switch (p_alpmx->alpmr_state) + { + case PF_ALPMR_STATE_W_USER_ACK: + memset (&alarm_data, 0, sizeof (alarm_data)); + alarm_data.alarm_type = p_alarm_argument->alarm_type; + alarm_data.api_id = p_alarm_argument->api_id; + alarm_data.slot_nbr = p_alarm_argument->slot_nbr; + alarm_data.subslot_nbr = p_alarm_argument->subslot_nbr; + alarm_data.alarm_specifier = p_alarm_argument->alarm_specifier; + alarm_data.sequence_number = p_alarm_argument->sequence_number; + + p_alpmx->prev_sequence_number = p_alpmx->sequence_number; + p_alpmx->sequence_number++; + p_alpmx->sequence_number %= 0x0800; + + ret = pf_alarm_apms_apms_a_data_req ( + net, + p_apmx, + &alarm_data, + 0, + p_pnio_status); + + p_alpmx->alpmr_state = PF_ALPMR_STATE_W_TACK; + break; + case PF_ALPMR_STATE_W_START: + case PF_ALPMR_STATE_W_NOTIFY: + case PF_ALPMR_STATE_W_TACK: + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): You tried to send an alarm ACK, but the ALPMR state is " + "%s.\n", + __LINE__, + pf_alarm_alpmr_state_to_string (p_alpmx->alpmr_state)); + p_ar->err_cls = PNET_ERROR_CODE_1_ALPMI; + p_ar->err_code = PNET_ERROR_CODE_2_ALPMI_WRONG_STATE; + break; + } + + return ret; +} + +/** + * @internal + * Send an alarm to the controller using an RTA DATA PDU. + * + * It will always require TACK back. + * + * It uses pf_alarm_apms_apms_a_data_req() for sending (and retransmission). + * + * Also look in the diagnostics database to attach information whether + * there are any relevant diagnosis items. + * + * Note that the PLC can set the max alarm payload length at startup. This + * limit can be 200 to 1432 bytes. + * + * ALPMI: ALPMI_Alarm_Notification.req + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param alarm_type In: The alarm type. + * @param high_prio In: true => Use high prio alarm. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_diag_item In: Additional diagnosis item (may be NULL). + * Used when calculating alarm specifier and + * maintenance status. + * @param module_ident In: The module ident number. + * @param submodule_ident In: The sub-module ident number. + * @param payload_usi In: The payload USI. Only used for + * block type = alarm notify. + * Use 0 for no payload (not even the USI value). + * @param payload_len In: Number of bytes of manufaturer data, + * or sizeof(pf_diag_item_t) for diagnosis in + * standard format. + * May be 0 also for manufacturer data. + * Max PNET_MAX_ALARM_PAYLOAD_DATA_SIZE or + * value from PLC. + * @param p_payload In: pf_diag_item_t, manufacturer data or NULL. + * Mandatory if payload_len > 0 or for USI + * values for diagnosis in standard format + * (which expect a pf_diag_item_t) + * @return 0 if operation succeeded. + * -1 if an error occurred. Errors include: + * Payload oversize (process alarms only) + * Alarms not enabled for AR + * Queue for outgoing alarms is non-available or full + */ +static int pf_alarm_send_alarm ( + pnet_t * net, + pf_ar_t * p_ar, + pf_alarm_type_values_t alarm_type, + bool high_prio, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const pf_diag_item_t * p_diag_item, + uint32_t module_ident, + uint32_t submodule_ident, + uint16_t payload_usi, + uint16_t payload_len, + const uint8_t * p_payload) +{ + pf_alarm_data_t alarm_data; + + if (net->global_alarm_enable == false || p_ar->alarm_enable == false) + { + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Alarm sending is disabled. Global enable: %u, AR enable: " + "%u.\n", + __LINE__, + net->global_alarm_enable, + p_ar->alarm_enable); + + return -1; + } + + if (payload_len > sizeof (alarm_data.payload.data)) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): You provided too long alarm payload (%u bytes) but max " + "is %zu. Increase PNET_MAX_ALARM_PAYLOAD_DATA_SIZE.\n", + __LINE__, + payload_len, + sizeof (alarm_data.payload.data)); + + return -1; + } + + if (payload_len > p_ar->alarm_cr_request.max_alarm_data_length) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): You provided too long alarm payload (%u bytes) but PLC " + "said max %u bytes.\n", + __LINE__, + payload_len, + p_ar->alarm_cr_request.max_alarm_data_length); + + return -1; + } + + if (payload_len > 0 && p_payload == NULL) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): payload_len is %u but no payload is given.\n", + __LINE__, + payload_len); + + return -1; + } + + /* Prepare data */ + memset (&alarm_data, 0, sizeof (alarm_data)); + alarm_data.alarm_type = alarm_type; + alarm_data.api_id = api_id; + alarm_data.slot_nbr = slot_nbr; + alarm_data.subslot_nbr = subslot_nbr; + alarm_data.module_ident = module_ident; + alarm_data.submodule_ident = submodule_ident; + + alarm_data.payload.usi = payload_usi; + alarm_data.payload.len = payload_len; + + if (payload_len > 0) + { + memcpy (alarm_data.payload.data, p_payload, payload_len); + } + + LOG_DEBUG ( + PF_ALARM_LOG, + "Alarm(%d): Putting alarm data in %s prio output queue. Alarm type %u " + "Payload %u bytes.\n", + __LINE__, + high_prio ? "high" : "low", + alarm_type, + payload_len); + + return pf_alarm_send_queue_post ( + &p_ar->alarm_send_q[high_prio ? 1 : 0], + &alarm_data); +} + +/************************ Send specific alarm types **************************/ + +int pf_alarm_send_process ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t payload_usi, + uint16_t payload_len, + const uint8_t * p_payload) +{ + uint32_t module_ident = 0; + uint32_t submodule_ident = 0; + + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Sending process alarm. Slot %u, subslot %u, %u bytes, USI " + "%u\n", + __LINE__, + slot_nbr, + subslot_nbr, + payload_len, + payload_usi); + + if (payload_usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + LOG_ERROR ( + PNET_LOG, + "Alarm(%d): Wrong USI given for process alarm: %u Slot: %u " + "Subslot %u\n", + __LINE__, + payload_usi, + slot_nbr, + subslot_nbr); + return -1; + } + + /* Find module and submodule identities */ + if (pf_cmdev_get_module_ident (net, api_id, slot_nbr, &module_ident) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to get module ident for slot: %u\n", + __LINE__, + slot_nbr); + + return -1; + } + if ( + pf_cmdev_get_submodule_ident ( + net, + api_id, + slot_nbr, + subslot_nbr, + &submodule_ident) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to get submodule ident for slot: %u, " + "subslot: %u\n", + __LINE__, + slot_nbr, + subslot_nbr); + + return -1; + } + + return pf_alarm_send_alarm ( + net, + p_ar, + PF_ALARM_TYPE_PROCESS, + true, /* High prio */ + api_id, + slot_nbr, + subslot_nbr, + NULL, /* No p_diag_item */ + module_ident, + submodule_ident, + payload_usi, + payload_len, + p_payload); +} + +int pf_alarm_send_diagnosis ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const pf_diag_item_t * p_diag_item) +{ + pf_alarm_type_values_t alarm_type = PF_ALARM_TYPE_DIAGNOSIS; + uint32_t module_ident = 0; + uint32_t submodule_ident = 0; + + if (p_diag_item == NULL) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): The diagnosis item is NULL\n", + __LINE__); + + return -1; + } + + /* Calculate alarm type */ + if (p_diag_item->usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + switch (p_diag_item->fmt.std.ch_error_type) + { + case PF_WRT_ERROR_REMOTE_MISMATCH: + alarm_type = PF_ALARM_TYPE_PORT_DATA_CHANGE; + break; + default: + break; + } + } + + /* Find module and submodule identities */ + if (pf_cmdev_get_module_ident (net, api_id, slot_nbr, &module_ident) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to get module ident for slot: %u\n", + __LINE__, + slot_nbr); + + return -1; + } + if ( + pf_cmdev_get_submodule_ident ( + net, + api_id, + slot_nbr, + subslot_nbr, + &submodule_ident) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to get submodule ident for slot: %u, " + "subslot: %u\n", + __LINE__, + slot_nbr, + subslot_nbr); + + return -1; + } + + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Sending diagnosis alarm " + "(type 0x%04X) Slot: %u Subslot: 0x%04X " + "Module ident: %u Submodule ident: %u " + "USI: 0x%04X\n", + __LINE__, + alarm_type, + slot_nbr, + subslot_nbr, + (unsigned)module_ident, + (unsigned)submodule_ident, + p_diag_item->usi); + + if (p_diag_item->usi < PF_USI_CHANNEL_DIAGNOSIS) + { + /* USI format */ + return pf_alarm_send_alarm ( + net, + p_ar, + alarm_type, + false, /* Low prio */ + api_id, + slot_nbr, + subslot_nbr, + p_diag_item, + module_ident, + submodule_ident, + p_diag_item->usi, + p_diag_item->fmt.usi.len, + (uint8_t *)&p_diag_item->fmt.usi.manuf_data); + } + + /* Standard format */ + return pf_alarm_send_alarm ( + net, + p_ar, + alarm_type, + false, /* Low prio */ + api_id, + slot_nbr, + subslot_nbr, + p_diag_item, + module_ident, + submodule_ident, + p_diag_item->usi, + sizeof (*p_diag_item), + (uint8_t *)p_diag_item); +} + +int pf_alarm_send_usi_diagnosis_disappears ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi) +{ + uint32_t module_ident = 0; + uint32_t submodule_ident = 0; + + CC_ASSERT (usi < PF_USI_CHANNEL_DIAGNOSIS); + + if (pf_cmdev_get_module_ident (net, api_id, slot_nbr, &module_ident) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to get module ident for slot: %u\n", + __LINE__, + slot_nbr); + + return -1; + } + + if ( + pf_cmdev_get_submodule_ident ( + net, + api_id, + slot_nbr, + subslot_nbr, + &submodule_ident) != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "Alarm(%d): Failed to get submodule ident for slot: %u, " + "subslot: %u\n", + __LINE__, + slot_nbr, + subslot_nbr); + + return -1; + } + + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Sending diagnosis alarm disappears " + "Slot: %u Subslot: 0x%04X " + "Module ident: %u Submodule ident: %u " + "USI: 0x%04X\n", + __LINE__, + slot_nbr, + subslot_nbr, + (unsigned)module_ident, + (unsigned)submodule_ident, + usi); + + return pf_alarm_send_alarm ( + net, + p_ar, + PF_ALARM_TYPE_DIAGNOSIS_DISAPPEARS, + false, /* Low prio */ + api_id, + slot_nbr, + subslot_nbr, + NULL, + module_ident, + submodule_ident, + usi, + 0, + NULL); +} + +int pf_alarm_send_pull ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr) +{ + uint16_t alarm_type = PF_ALARM_TYPE_PULL; + + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Sending pull alarm. Slot: %u Subslot: 0x%04X\n", + __LINE__, + slot_nbr, + subslot_nbr); + if (subslot_nbr == 0) + { + if (p_ar->ar_param.ar_properties.pull_module_alarm_allowed == true) + { + alarm_type = PF_ALARM_TYPE_PULL_MODULE; + } + } + + (void)pf_alarm_send_alarm ( + net, + p_ar, + alarm_type, + false, /* Low prio */ + api_id, + slot_nbr, + subslot_nbr, + NULL, /* No p_diag_item */ + 0, /* module_ident */ + 0, /* submodule_ident */ + 0, /* No payload */ + 0, /* No payload */ + NULL); /* No payload */ + + return 0; +} + +int pf_alarm_send_plug ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t module_ident, + uint32_t submodule_ident) +{ + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Sending plug alarm. Slot: %u Subslot: 0x%04X\n", + __LINE__, + slot_nbr, + subslot_nbr); + + (void)pf_alarm_send_alarm ( + net, + p_ar, + PF_ALARM_TYPE_PLUG, + false, /* Low prio */ + api_id, + slot_nbr, + subslot_nbr, + NULL, /* No p_diag_item */ + module_ident, + submodule_ident, + 0, /* No payload */ + 0, /* No payload */ + NULL); /* No payload */ + + return 0; +} + +int pf_alarm_send_plug_wrong ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t module_ident, + uint32_t submodule_ident) +{ + LOG_INFO ( + PF_ALARM_LOG, + "Alarm(%d): Sending plug wrong alarm. Slot: %u Subslot: 0x%04X\n", + __LINE__, + slot_nbr, + subslot_nbr); + + (void)pf_alarm_send_alarm ( + net, + p_ar, + PF_ALARM_TYPE_PLUG_WRONG_MODULE, + false, /* Low prio */ + api_id, + slot_nbr, + subslot_nbr, + NULL, /* No p_diag_item */ + module_ident, + submodule_ident, + 0, /* No payload */ + 0, /* No payload */ + NULL); /* No payload */ + + return 0; +} diff --git a/profinet_stack/p-net/src/common/pf_alarm.h b/profinet_stack/p-net/src/common/pf_alarm.h new file mode 100644 index 0000000..ea6c64f --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_alarm.h @@ -0,0 +1,326 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_ALARM_H +#define PF_ALARM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the alarm component. + * + * @param net InOut: The p-net stack instance + */ +void pf_alarm_init (pnet_t * net); + +/** + * Create and activate an alarm instance for the specified AR. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_alarm_activate (pnet_t * net, pf_ar_t * p_ar); + +/** + * Close an alarm instance for the specified AR. + * + * Sends a low prio alarm. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_alarm_close (pnet_t * net, pf_ar_t * p_ar); + +/** + * Enable sending of all alarms. + * + * This is the default setting. + * + * @param p_ar InOut: The AR instance. + */ +void pf_alarm_enable (pf_ar_t * p_ar); + +/** + * Disable sending of all alarms. + * + * @param p_ar InOut: The AR instance. + */ +void pf_alarm_disable (pf_ar_t * p_ar); + +/** + * Check if an alarm is pending. + * + * @param p_ar In: The AR instance. + * @return true if an alarm is pending. + * false if no alarms are pending. + */ +bool pf_alarm_pending (const pf_ar_t * p_ar); + +/** + * Perform periodic tasks in the alarm component. + * + * Check for controller inputs. + * + * @param net InOut: The p-net stack instance + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_alarm_periodic (pnet_t * net); + +/** + * Send a PULL alarm. + * + * This functions sends a PULL alarm. + * If subslot_nbr == 0 and PULL_MODULE alarms are allowed by the controller (in + * the CONNECT) then a PULL_MODULE alarm is send. Otherwise a PULL alarm is + * sent. + * + * Uses no alarm payload (not attached to diagnosis ASE. + * Related to whole submodule, not related to channels). + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @return 0 if operation succeeded. + * -1 if an error occurred (or waiting for ACK from controller: re-try + * later). + */ +int pf_alarm_send_pull ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr); + +/** + * Send a PLUG OK alarm. + * + * Uses no alarm payload (not attached to diagnosis ASE. + * Related to whole submodule, not related to channels). + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param module_ident In: The module ident number. + * @param submodule_ident In: The sub-module ident number. + * @return 0 if operation succeeded. + * -1 if an error occurred (or waiting for ACK from controller: re-try + * later). + */ +int pf_alarm_send_plug ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t module_ident, + uint32_t submodule_ident); + +/** + * Send a PLUG_WRONG alarm. + * + * Uses no alarm payload (not attached to diagnosis ASE. + * Related to whole submodule, not related to channels). + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param module_ident In: The module ident number. + * @param submodule_ident In: The sub-module ident number. + * @return 0 if operation succeeded. + * -1 if an error occurred (or waiting for ACK from controller: re-try + * later). + */ +int pf_alarm_send_plug_wrong ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t module_ident, + uint32_t submodule_ident); + +/** + * Send process alarm. + * + * This function sends a process alarm to a controller. + * These alarms are always sent as high priority alarms. + * + * User-supplied payload. Not attached to diagnosis ASE, but might have a + * relation to channels. + * + * Note that the PLC can set the max alarm payload length at startup. This + * limit can be 200 to 1432 bytes. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param payload_usi In: The USI for the alarm. Use 0 if no payload. + * Max 0x7fff + * @param payload_len In: Length of diagnostics data (may be 0). + * Max PNET_MAX_ALARM_PAYLOAD_DATA_SIZE or + * value from PLC. + * @param p_payload In: User supplied payload (may be NULL if + * payload_usi == 0). + * @return 0 if operation succeeded. + * -1 if an error occurred (or waiting for ACK from controller: re-try + * later). + */ +int pf_alarm_send_process ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t payload_usi, + uint16_t payload_len, + const uint8_t * p_payload); + +/** + * Send diagnosis alarm. + * + * This function sends a diagnosis alarm to a controller. + * These alarms are always sent as low priority alarms. + * + * The alarm type is typically "Diagnosis alarm", but might be for example + * "Port data change notification" (based on the channel error type in the + * diag item). + * + * This is attached to diagnosis ASE (has a diagnosis object as payload). + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_diag_item In: The diag item. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_alarm_send_diagnosis ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const pf_diag_item_t * p_diag_item); + +/** + * Send "diagnosis disappears" alarm for diagnosis in USI format. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param usi In: USI value. Should be <= 0x7FFF + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_alarm_send_usi_diagnosis_disappears ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi); + +/** + * Send Alarm ACK (from the application) + * + * Always uses high prio, has no payload. + * + * This is the "Alarm notification" service. + * + * ALPMR: ALPMR_Alarm_Ack.req + * ALPMR: ALPMR_Alarm_Ack.cnf (Implements part of that signal via the return + * value) + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_alarm_argument In: The alarm argument (with slot, subslot, + * alarm_type etc) + * @param p_pnio_status In: Detailed ACK information. + * @return 0 if operation succeeded. + * -1 if an error occurred (or waiting for ACK from controller: re-try + * later). + */ +int pf_alarm_alpmr_alarm_ack ( + pnet_t * net, + pf_ar_t * p_ar, + const pnet_alarm_argument_t * p_alarm_argument, + const pnet_pnio_status_t * p_pnio_status); + +/** + * Show all alarm information of the specified AR. + * + * @param p_ar In: The AR instance. + */ +void pf_alarm_show (const pf_ar_t * p_ar); + +/************ Internal functions, made available for unit testing ************/ + +void pf_alarm_queue_mutex_create (pf_queue_accountant_t * p_accountant); + +void pf_alarm_queue_mutex_destroy (pf_queue_accountant_t * p_accountant); + +bool pf_alarm_queue_is_available (pf_queue_accountant_t * p_accountant); + +void pf_alarm_add_diag_item_to_summary ( + const pf_ar_t * p_ar, + const pf_subslot_t * p_subslot, + const pf_diag_item_t * p_diag_item, + pnet_alarm_spec_t * p_alarm_spec, + uint32_t * p_maint_status); + +void pf_alarm_receive_queue_reset (pf_alarm_receive_queue_t * q); + +int pf_alarm_receive_queue_post ( + pf_alarm_receive_queue_t * q, + const pf_apmr_msg_t * p_alarm_frame); + +int pf_alarm_receive_queue_fetch ( + pf_alarm_receive_queue_t * q, + pf_apmr_msg_t * p_alarm_frame); + +void pf_alarm_send_queue_reset (pf_alarm_send_queue_t * q); + +int pf_alarm_send_queue_post ( + pf_alarm_send_queue_t * q, + const pf_alarm_data_t * p_alarm_data); + +int pf_alarm_send_queue_fetch ( + pf_alarm_send_queue_t * q, + pf_alarm_data_t * p_alarm_data); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_ALARM_H */ diff --git a/profinet_stack/p-net/src/common/pf_bg_worker.c b/profinet_stack/p-net/src/common/pf_bg_worker.c new file mode 100644 index 0000000..7bccf8c --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_bg_worker.c @@ -0,0 +1,132 @@ + +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pf_includes.h" + +#include + +#ifdef UNIT_TEST +/* Background worker is disabled during unit tests. + * Stack reinitialization during test recreates + * the background task which cause problems. + * Disabling is implemented using mock functions + * - mock_pf_bg_worker_init + * - mock_pf_bg_worker_start_job + */ +#endif + +/* Events handled by bg worker task */ + +#define BG_JOB_EVENT_UPDATE_PORTS_STATUS BIT (0) +#define BG_JOB_EVENT_SAVE_ASE_NVM_DATA BIT (1) +#define BG_JOB_EVENT_SAVE_IM_NVM_DATA BIT (2) +#define BG_JOB_EVENT_SAVE_PDPORT_NVM_DATA BIT (3) + +static void bg_worker_task (void * arg); + +void pf_bg_worker_init (pnet_t * net) +{ + net->pf_bg_worker.events = os_event_create(); + CC_ASSERT (net->pf_bg_worker.events != NULL); + + os_thread_create ( + "p-net_bg_worker", + net->fspm_cfg.pnal_cfg.bg_worker_thread.prio, + net->fspm_cfg.pnal_cfg.bg_worker_thread.stack_size, + bg_worker_task, + (void *)net); +} + +int pf_bg_worker_start_job (pnet_t * net, pf_bg_job_t job_id) +{ + if (net == NULL) + { + return -1; + } + + switch (job_id) + { + case PF_BGJOB_UPDATE_PORTS_STATUS: + os_event_set (net->pf_bg_worker.events, BG_JOB_EVENT_UPDATE_PORTS_STATUS); + break; + case PF_BGJOB_SAVE_ASE_NVM_DATA: + os_event_set (net->pf_bg_worker.events, BG_JOB_EVENT_SAVE_ASE_NVM_DATA); + break; + case PF_BGJOB_SAVE_IM_NVM_DATA: + os_event_set (net->pf_bg_worker.events, BG_JOB_EVENT_SAVE_IM_NVM_DATA); + break; + case PF_BGJOB_SAVE_PDPORT_NVM_DATA: + os_event_set (net->pf_bg_worker.events, BG_JOB_EVENT_SAVE_PDPORT_NVM_DATA); + break; + default: + LOG_ERROR ( + PNET_LOG, + "BGW(%d): Unsupported job %d\n", + __LINE__, + (int)job_id); + return -1; + } + + return 0; +} + +/** + * Event handling loop for background thread + * + * @param arg InOut: Thread argument, must be of type pnet_t * + */ +static void bg_worker_task (void * arg) +{ + pnet_t * net = (pnet_t *)arg; + uint32_t mask = + BG_JOB_EVENT_UPDATE_PORTS_STATUS | BG_JOB_EVENT_SAVE_ASE_NVM_DATA | + BG_JOB_EVENT_SAVE_IM_NVM_DATA | BG_JOB_EVENT_SAVE_PDPORT_NVM_DATA; + uint32_t flags = 0; + + for (;;) + { + os_event_wait (net->pf_bg_worker.events, mask, &flags, OS_WAIT_FOREVER); + + if (flags & BG_JOB_EVENT_SAVE_ASE_NVM_DATA) + { + os_event_clr (net->pf_bg_worker.events, BG_JOB_EVENT_SAVE_ASE_NVM_DATA); + + pf_cmina_save_ase (net, &net->cmina_nonvolatile_dcp_ase); + } + if (flags & BG_JOB_EVENT_SAVE_IM_NVM_DATA) + { + os_event_clr (net->pf_bg_worker.events, BG_JOB_EVENT_SAVE_IM_NVM_DATA); + + pf_fspm_save_im (net); + } + if (flags & BG_JOB_EVENT_SAVE_PDPORT_NVM_DATA) + { + os_event_clr ( + net->pf_bg_worker.events, + BG_JOB_EVENT_SAVE_PDPORT_NVM_DATA); + + (void)pf_pdport_save_all (net); + } + if (flags & BG_JOB_EVENT_UPDATE_PORTS_STATUS) + { + os_event_clr ( + net->pf_bg_worker.events, + BG_JOB_EVENT_UPDATE_PORTS_STATUS); + + pf_pdport_update_eth_status (net); + } + } +} diff --git a/profinet_stack/p-net/src/common/pf_bg_worker.h b/profinet_stack/p-net/src/common/pf_bg_worker.h new file mode 100644 index 0000000..8f4a201 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_bg_worker.h @@ -0,0 +1,63 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_BG_WORKER_H +#define PF_BG_WORKER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Background worker jobs + */ +typedef enum pf_bg_job +{ + /** Update status for all ports */ + PF_BGJOB_UPDATE_PORTS_STATUS = 0, + + /** Save non volatile ASE data to file */ + PF_BGJOB_SAVE_ASE_NVM_DATA, + + /** Save non volatile IM data to file */ + PF_BGJOB_SAVE_IM_NVM_DATA, + + /** Save non volatile PDPort data to file */ + PF_BGJOB_SAVE_PDPORT_NVM_DATA, +} pf_bg_job_t; + +/** + * Initialize the background worker. + * + * @param net InOut: The p-net stack instance + */ +void pf_bg_worker_init (pnet_t * net); + +/** + * Start a background job. + * This function is non-blocking and sends the job request + * to the background worker task. + * @param net InOut: The p-net stack instance + * @param job_id In: Job to run + * @return 0 if operation is successfully initiated + * -1 if an error occurred. + */ +int pf_bg_worker_start_job (pnet_t * net, pf_bg_job_t job_id); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_BG_WORKER_H */ diff --git a/profinet_stack/p-net/src/common/pf_cpm.c b/profinet_stack/p-net/src/common/pf_cpm.c new file mode 100644 index 0000000..17178d8 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_cpm.c @@ -0,0 +1,618 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Cyclic Consumer Provider Protocol Machine (CPM) + * + * This handles receiving cyclic data. Registers a handler for incoming + * real time data frames. Maintains a timer to monitor incoming frames. + * + * States are W_START, FRUN and RUN. + * + * A global mutex is used instead of a per-instance mutex. + * The locking time is very low so it should not be very congested. + * The mutex is created on the first call to pf_cpm_create and deleted on + * the last call to pf_cpm_close. + * Keep track of how many instances exist and delete the mutex when the + * number reaches 0 (zero). + * + * The the received frames are handled by a CPM driver. The driver + * configures the reception for the specific frame IDs, checks incoming + * frames and implements timeouts. The driver also provides functions + * for accessing cyclic data and data status. + * The driver is installed in the CPM init function. + * + */ + +#ifdef UNIT_TEST +#endif + +#include "pf_includes.h" + +#include +#include + +#include "pf_includes.h" +#include "pf_block_reader.h" + +/** + * @internal + * Return a string representation of the CPM state. + * @param state In: The CPM state. + * @return A string representing the CPM state. + */ +static const char * pf_cpm_state_to_string (pf_cpm_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_CPM_STATE_W_START: + s = "PF_CPM_STATE_W_START"; + break; + case PF_CPM_STATE_FRUN: + s = "PF_CPM_STATE_FRUN"; + break; + case PF_CPM_STATE_RUN: + s = "PF_CPM_STATE_RUN"; + break; + } + + return s; +} + +void pf_cpm_init (pnet_t * net) +{ + net->cpm_instance_cnt = ATOMIC_VAR_INIT (0); + + LOG_DEBUG (PF_CPM_LOG, "CPM(%d): Init driver\n", __LINE__); + +#if PNET_OPTION_DRIVER_ENABLE + if (net->fspm_cfg.driver_enable) + { + pf_driver_cpm_init (net); + } + else + { + pf_cpm_driver_sw_init (net); + } +#else + pf_cpm_driver_sw_init (net); +#endif +} + +void pf_cpm_set_state (pf_cpm_t * p_cpm, pf_cpm_state_values_t state) +{ + if (state != p_cpm->state) + { + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): New state %s (was %s)\n", + __LINE__, + pf_cpm_state_to_string (state), + pf_cpm_state_to_string (p_cpm->state)); + p_cpm->state = state; + } +} + +void pf_cpm_state_ind (pnet_t * net, pf_ar_t * p_ar, uint32_t crep, bool start) +{ + pf_cmio_cpm_state_ind (net, p_ar, crep, start); + pf_cmdmc_cpm_state_ind (p_ar, crep, start); +} + +int pf_cpm_create (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + int ret; + uint32_t cnt; + + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): Create CPM instance. AREP %u " + "CREP %" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + cnt = atomic_fetch_add (&net->cpm_instance_cnt, 1); + if (cnt == 0) + { + net->cpm_buf_lock = os_mutex_create(); + } + + pf_cpm_set_state (&p_ar->iocrs[crep].cpm, PF_CPM_STATE_W_START); + ret = net->cpm_drv->create (net, p_ar, crep); + if (ret != 0) + { + LOG_FATAL ( + PF_CPM_LOG, + "CPM(%d): Driver create request failed\n", + __LINE__); + } + + return ret; +} + +int pf_cpm_close_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + int ret; + uint32_t cnt; + + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): Closing CPM for AREP %u CREP %" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + pf_cpm_set_state (&p_ar->iocrs[crep].cpm, PF_CPM_STATE_W_START); + + ret = net->cpm_drv->close_req (net, p_ar, crep); + if (ret == 0) + { + cnt = atomic_fetch_sub (&net->cpm_instance_cnt, 1); + if (cnt == 1) + { + os_mutex_destroy (net->cpm_buf_lock); + net->cpm_buf_lock = NULL; + } + } + else + { + LOG_FATAL (PF_CPM_LOG, "CPM(%d): Driver close request failed\n", __LINE__); + } + + return ret; +} + +int pf_cpm_check_cycle (int32_t prev, uint16_t now) +{ + int ret = -1; + uint16_t diff; + + diff = (int32_t)now - (uint16_t)prev; + if ((diff >= 1) && (diff <= 61440)) + { + ret = 0; + } + + return ret; +} + +int pf_cpm_check_src_addr (const pf_cpm_t * p_cpm, const pnal_buf_t * p_buf) +{ + int ret = -1; + + /* Payload contains [da], [sa], */ + if ( + memcmp ( + &p_cpm->sa, + &((uint8_t *)p_buf->payload)[sizeof (pnet_ethaddr_t)], + sizeof (p_cpm->sa)) == 0) + { + ret = 0; + } + + return ret; +} + +int pf_cpm_activate_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + int ret = -1; + uint16_t ix; + pf_cpm_t * p_cpm; + pf_iocr_t * p_iocr; + + p_iocr = &p_ar->iocrs[crep]; + p_cpm = &p_iocr->cpm; + + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): Activating CPM for output data reception. " + "AREP %u CREP %" PRIu32 " FrameID: 0x%04x\n", + __LINE__, + p_ar->arep, + crep, + p_iocr->param.frame_id); + + switch (p_cpm->state) + { + case PF_CPM_STATE_W_START: + p_cpm->data_hold_factor = p_iocr->param.data_hold_factor; + + p_cpm->control_interval = + ((uint32_t)p_iocr->param.send_clock_factor * + (uint32_t)p_iocr->param.reduction_ratio * 1000U) / + 32U; /* us */ + + for (ix = 0; ix < pf_port_get_number_of_ports (net); ix++) + { + p_cpm->rxa[ix][0] = -1; /* "invalid" cycle counter */ + p_cpm->rxa[ix][1] = -1; + } + p_cpm->cycle = -1; /* "invalid" */ + p_cpm->new_data = false; + + p_cpm->dht = 0; + p_cpm->recv_cnt = 0; + + memcpy ( + &p_cpm->sa, + &p_ar->ar_param.cm_initiator_mac_add, + sizeof (p_cpm->sa)); + + p_cpm->buffer_pos = 2 * sizeof (pnet_ethaddr_t) + /* ETH src and dest addr + */ + sizeof (uint16_t) + /* LT */ + sizeof (uint16_t); /* FrameId */ + p_cpm->data_status = 0; + p_cpm->buffer_length = p_cpm->buffer_pos + /* ETH frame header */ + p_iocr->param.c_sdu_length + /* Profinet data + length */ + sizeof (uint16_t) + /* cycle counter */ + 1 + /* data status */ + 1; /* transfer status */ + + p_cpm->nbr_frame_id = 1; /* ToDo: For now. More for RTC_3 */ + p_cpm->frame_id[0] = p_iocr->param.frame_id; + + ret = net->cpm_drv->activate_req (net, p_ar, crep); + if (ret == 0) + { + pf_cpm_set_state (p_cpm, PF_CPM_STATE_FRUN); + } + else + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM(%d): Driver activate request failed\n", + __LINE__); + + p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID; + pf_cmsu_cpm_error_ind (net, p_ar, p_ar->err_cls, p_ar->err_code); + } + /* pf_cpm_activate_cnf */ + break; + case PF_CPM_STATE_FRUN: + case PF_CPM_STATE_RUN: + p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID_STATE; + break; + default: + LOG_ERROR ( + PF_CPM_LOG, + "CPM(%d): Illegal state in cpm[%d] %d\n", + __LINE__, + (int)crep, + p_cpm->state); + p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID_STATE; + break; + } + + return ret; +} + +/** + * @internal + * Find the AR, output IOCR and IODATA object instances for the specified + * sub-slot. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param pp_ar Out: The AR instance. + * @param pp_iocr Out: The IOCR instance. + * @param pp_iodata Out: The IODATA object instance. + * @return 0 If the information has been found. + * -1 If the information was not found. + */ +static int pf_cpm_get_ar_iocr_desc ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_ar_t ** pp_ar, + pf_iocr_t ** pp_iocr, + pf_iodata_object_t ** pp_iodata) +{ + int ret = -1; + uint32_t crep; + uint16_t iodata_ix; + pf_subslot_t * p_subslot = NULL; + pf_ar_t * p_ar = NULL; + pf_iocr_t * p_iocr = NULL; + bool found = false; + + if ( + pf_cmdev_get_subslot_full (net, api_id, slot_nbr, subslot_nbr, &p_subslot) == + 0) + { + p_ar = p_subslot->p_ar; + } + + if (p_ar == NULL) + { + LOG_DEBUG (PF_CPM_LOG, "CPM(%d): No AR set in sub-slot\n", __LINE__); + } + else + { + /* + * Search the AR for an OUTPUT CR or an MC consumer CR containing the + * sub-slot. + */ + for (crep = 0; ((found == false) && (crep < p_ar->nbr_iocrs)); crep++) + { + if ( + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_OUTPUT) || + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_MC_CONSUMER)) + { + p_iocr = &p_ar->iocrs[crep]; + for (iodata_ix = 0; + ((found == false) && (iodata_ix < p_iocr->nbr_data_desc)); + iodata_ix++) + { + if ( + (p_iocr->data_desc[iodata_ix].in_use == true) && + (p_iocr->data_desc[iodata_ix].api_id == api_id) && + (p_iocr->data_desc[iodata_ix].slot_nbr == slot_nbr) && + (p_iocr->data_desc[iodata_ix].subslot_nbr == subslot_nbr)) + { + *pp_iodata = &p_iocr->data_desc[iodata_ix]; + *pp_iocr = p_iocr; + *pp_ar = p_ar; + found = true; + } + } + } + } + } + + if (found == true) + { + ret = 0; + } + + return ret; +} + +int pf_cpm_get_data_and_iops ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + bool * p_new_flag, + uint8_t * p_data, + uint16_t * p_data_len, + uint8_t * p_iops, + uint8_t * p_iops_len) +{ + int ret = -1; + pf_iocr_t * p_iocr = NULL; + pf_iodata_object_t * p_iodata = NULL; + pf_ar_t * p_ar = NULL; + + if ( + pf_cpm_get_ar_iocr_desc ( + net, + api_id, + slot_nbr, + subslot_nbr, + &p_ar, + &p_iocr, + &p_iodata) == 0) + { + switch (p_iocr->cpm.state) + { + case PF_CPM_STATE_W_START: + p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID_STATE; + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): Get data in wrong state: %u for AREP %u\n", + __LINE__, + p_iocr->cpm.state, + p_ar->arep); + break; + case PF_CPM_STATE_FRUN: + case PF_CPM_STATE_RUN: + if ( + (*p_data_len < p_iodata->data_length) || + (*p_iops_len < p_iodata->iops_length)) + { + *p_data_len = 0; + *p_new_flag = false; + LOG_ERROR ( + PF_CPM_LOG, + "CPM(%d): Given data buffer size %u and IOPS buffer size " + "%u, but minimum sizes are %u and %u for slot %u subslot " + "0x%04x\n", + __LINE__, + (unsigned)*p_data_len, + (unsigned)*p_iops_len, + (unsigned)p_iodata->data_length, + (unsigned)p_iodata->iops_length, + slot_nbr, + subslot_nbr); + } + else + { + *p_data_len = p_iodata->data_length; + *p_iops_len = p_iodata->iops_length; + + ret = net->cpm_drv->get_data_and_iops ( + net, + p_iocr, + p_iodata, + p_new_flag, + p_data, + *p_data_len, + p_iops, + *p_iops_len); + + if (ret != 0) + { + *p_data_len = 0; + *p_iops_len = 0; + } + } + break; + default: + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): Set data in wrong state: %u for AREP %u\n", + __LINE__, + p_iocr->cpm.state, + p_ar->arep); + break; + } + } + else + { + /* May happen after an ABORT */ + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): No data descriptor found in set data\n", + __LINE__); + } + + return ret; +} + +int pf_cpm_get_iocs ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_iocs, + uint8_t * p_iocs_len) +{ + int ret = -1; + pf_iocr_t * p_iocr = NULL; + pf_iodata_object_t * p_iodata = NULL; + pf_ar_t * p_ar = NULL; + + if ( + pf_cpm_get_ar_iocr_desc ( + net, + api_id, + slot_nbr, + subslot_nbr, + &p_ar, + &p_iocr, + &p_iodata) == 0) + { + switch (p_iocr->cpm.state) + { + case PF_CPM_STATE_W_START: + p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID_STATE; + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): Get iocs in wrong state: %u for AREP %u\n", + __LINE__, + p_iocr->cpm.state, + p_ar->arep); + break; + case PF_CPM_STATE_FRUN: + case PF_CPM_STATE_RUN: + if (*p_iocs_len < p_iodata->iocs_length) + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM(%d): Given IOCS buffer size %u, but minimum size is %u " + "for slot %u subslot 0x%04x\n", + __LINE__, + (unsigned)*p_iocs_len, + (unsigned)p_iodata->iocs_length, + slot_nbr, + subslot_nbr); + } + else if (p_iodata->iocs_length == 0) + { + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): iocs_length is zero in get iocs\n", + __LINE__); + } + else + { + *p_iocs_len = p_iodata->iocs_length; + ret = net->cpm_drv + ->get_iocs (net, p_iocr, p_iodata, p_iocs, *p_iocs_len); + } + break; + default: + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): Get iocs in wrong state: %u for AREP %u\n", + __LINE__, + (unsigned)p_iocr->cpm.state, + p_ar->arep); + break; + } + } + else + { + /* May happen after an ABORT */ + LOG_DEBUG ( + PF_CPM_LOG, + "CPM(%d): No data descriptor found in get iocs\n", + __LINE__); + } + + return ret; +} + +int pf_cpm_get_data_status (const pf_cpm_t * p_cpm, uint8_t * p_data_status) +{ + *p_data_status = p_cpm->data_status; + + return 0; +} + +void pf_cpm_show (const pnet_t * net, const pf_cpm_t * p_cpm) +{ + printf ("cpm:\n"); + printf (" instance_cnt = %u\n", (unsigned)net->cpm_instance_cnt); + printf ( + " state = %s\n", + pf_cpm_state_to_string (p_cpm->state)); + printf (" max_exec = %u\n", (unsigned)p_cpm->max_exec); + printf (" errline = %u\n", (unsigned)p_cpm->errline); + printf (" errcnt = %u\n", (unsigned)p_cpm->errcnt); + printf (" frame_id = %u\n", (unsigned)p_cpm->frame_id[0]); + printf (" data_hold_factor = %u\n", (unsigned)p_cpm->data_hold_factor); + printf (" dHT = %u\n", (unsigned)p_cpm->dht); + printf (" control_interval = %i\n", (int)p_cpm->control_interval); + printf (" cycle = %i\n", (int)p_cpm->cycle); + printf (" recv_cnt = %u\n", (unsigned)p_cpm->recv_cnt); + printf (" free_cnt = %u\n", (unsigned)p_cpm->free_cnt); + printf (" p_buffer_app = %p\n", p_cpm->p_buffer_app); + printf (" p_buffer_cpm = %p\n", p_cpm->p_buffer_cpm); + printf ( + " p_buffer->len = %u\n", + p_cpm->p_buffer_cpm ? ((pnal_buf_t *)(p_cpm->p_buffer_cpm))->len : 0); + printf (" new_buf = %u\n", (unsigned)p_cpm->new_buf); + printf (" ci_running = %u\n", (unsigned)p_cpm->ci_running); + printf ( + " ci_timer = %u\n", + (unsigned)pf_scheduler_get_value (&p_cpm->ci_timeout)); + printf (" buffer_status = %x\n", (unsigned)p_cpm->data_status); + printf (" buffer_length = %u\n", (unsigned)p_cpm->buffer_length); + printf (" buffer_pos = %u\n", (unsigned)p_cpm->buffer_pos); +} diff --git a/profinet_stack/p-net/src/common/pf_cpm.h b/profinet_stack/p-net/src/common/pf_cpm.h new file mode 100644 index 0000000..ce1d00e --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_cpm.h @@ -0,0 +1,192 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CPM_H +#define PF_CPM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the CPM component. + * @param net InOut: The p-net stack instance + */ +void pf_cpm_init (pnet_t * net); + +/** + * Create a CPM for a specific IOCR instance. + * + * This function creates a CPM instance for the specified IOCR instance. + * Set the CPM state to W_START. + * Allocate the buffer mutex on first call. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the CPM instance was created. + * -1 if an error occurred. + */ +int pf_cpm_create (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + +/** + * Close a CPM instance. + * + * This function terminates the specified CPM instance. + * De-registers a frame handler for incoming Ethernet frames. + * The buffer mutex is destroyed on the last call. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the CPM instance was closed. + * -1 if an error occurred. + */ +int pf_cpm_close_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + +/** + * Activate a CPM instance and of the specified CR instance. + * + * Registers a frame handler for incoming Ethernet frames. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the CPM instance could be activated. + * -1 if an error occurred. + */ +int pf_cpm_activate_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + +/** + * Retrieve the specified sub-slot IOCS sent from the controller. + * User must supply a buffer large enough to hold the received IOCS. + * Maximum buffer size is 256 bytes. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_iocs Out: Copy of the received IOCS. + * @param p_iocs_len In: Size of buffer at p_iocs. + * Out: The length of the received IOCS. + * @return + */ +int pf_cpm_get_iocs ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_iocs, + uint8_t * p_iocs_len); + +/** + * Retrieve the specified sub-slot data and IOPS received from the controller. + * User must supply a buffer large enough to hold the received data. + * Maximum buffer size is PF_FRAME_BUFFER_SIZE (1500) bytes. + * User must supply a buffer large enough to hold the received IOPS. + * Maximum buffer size is 256 bytes. + * + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_new_flag Out: true means new valid data (and IOPS) frame + * available since last call. + * @param p_data Out: Copy of the received data. + * @param p_data_len In: Buffer size. + * Out: Length of received data. + * @param p_iops Out: The received IOPS. + * @param p_iops_len In: Size of buffer at p_iops. + * Out: The length of the received IOPS. + * @return 0 if the data and IOPS could be retrieved. + * -1 if an error occurred. + */ +int pf_cpm_get_data_and_iops ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + bool * p_new_flag, + uint8_t * p_data, + uint16_t * p_data_len, + uint8_t * p_iops, + uint8_t * p_iops_len); + +/** + * Get the data status of the CPM connection. + * @param p_cpm In: The CPM instance. + * @param p_data_status Out: The CPM data status. + * @return + */ +int pf_cpm_get_data_status (const pf_cpm_t * p_cpm, uint8_t * p_data_status); + +/** + * Show information about a CPM instance. + * @param net In: The p-net stack instance + * @param p_cpm In: The CPM instance. + */ +void pf_cpm_show (const pnet_t * net, const pf_cpm_t * p_cpm); + +/* Functions used by cpm driver */ + +/** + * Change the CPM state. + * @param p_spm InOut: The CPM instance. + * @param state In: The new CPM state. + */ +void pf_cpm_set_state (pf_cpm_t * p_cpm, pf_cpm_state_values_t state); + +/** + * Notify other components about CPM events. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index + * @param start In: Start/Stop indicator. True if CPM is starting. + */ +void pf_cpm_state_ind (pnet_t * net, pf_ar_t * p_ar, uint32_t crep, bool start); + +/** + * Perform a check of the source address of the received frame. + * @param p_cpm In: The CPM instance. + * @param p_buf In: The frame buffer. + * @return 0 if the source address is OK. + * -1 if the source address is wrong. + */ +int pf_cpm_check_src_addr (const pf_cpm_t * p_cpm, const pnal_buf_t * p_buf); + +/************ Internal functions, made available for unit testing ************/ + +/** + * Perform the cycle counter check of the received frame. + * + * Profinet 2.4, section 4.7.2.1.2 + * + * In general, the current counter value should be larger than the + * previous counter value. + * + * However as we use cyclic counters, also allow it to be a lot less (not the + * same or slightly less). + * + * @param prev In: The previous cycle counter. + * @param now In: The current cycle counter. + * @return 0 If the cycle check is OK. + * -1 if the cycle check fails. + */ +int pf_cpm_check_cycle (int32_t prev, uint16_t now); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CPM_H */ diff --git a/profinet_stack/p-net/src/common/pf_cpm_driver_sw.c b/profinet_stack/p-net/src/common/pf_cpm_driver_sw.c new file mode 100644 index 0000000..a44df40 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_cpm_driver_sw.c @@ -0,0 +1,547 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the CPM default software (SW) driver + * The driver handles receiving cyclic data. Registers a handler for incoming + * real time data frames. Maintains a timer to monitor incoming frames. + * + */ + +#ifdef UNIT_TEST +#define os_get_current_time_us mock_os_get_current_time_us +#endif + +#include "pf_includes.h" + +#include +#include + +/** + * @internal + * The control_interval timer has expired. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * @param net InOut: The p-net stack instance + * @param arg In: The IOCR instance. pf_iocr_t + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +static void pf_cpm_control_interval_expired ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_iocr_t * p_iocr = (pf_iocr_t *)arg; + uint32_t start = os_get_current_time_us(); + uint32_t exec; + + pf_scheduler_reset_handle (&p_iocr->cpm.ci_timeout); + + if (p_iocr->cpm.ci_running == true) /* Timer running */ + { + switch (p_iocr->cpm.state) + { + case PF_CPM_STATE_W_START: + case PF_CPM_STATE_FRUN: + break; + case PF_CPM_STATE_RUN: + if (p_iocr->cpm.dht >= p_iocr->cpm.data_hold_factor) + { + /* dht expired */ + p_iocr->p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_iocr->p_ar->err_code = + PNET_ERROR_CODE_2_ABORT_AR_CONSUMER_DHT_EXPIRED; + + p_iocr->cpm.dht = 0; + p_iocr->cpm.ci_running = false; /* Stop timer */ + pf_cpm_state_ind (net, p_iocr->p_ar, p_iocr->crep, false); /* stop + */ + + pf_cpm_set_state (&p_iocr->cpm, PF_CPM_STATE_W_START); + } + else + { + p_iocr->cpm.dht++; + } + break; + } + + if (p_iocr->cpm.ci_running == true) + { + /* Timer auto-reload */ + if ( + pf_scheduler_add ( + net, + p_iocr->cpm.control_interval, + pf_cpm_control_interval_expired, + arg, + &p_iocr->cpm.ci_timeout) != 0) + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_SW(%d): Timeout not started\n", + __LINE__); + p_iocr->p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_iocr->p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID; + pf_cmsu_cpm_error_ind ( + net, + p_iocr->p_ar, + p_iocr->p_ar->err_cls, + p_iocr->p_ar->err_code); + } + } + } + exec = os_get_current_time_us() - start; + if (exec > p_iocr->cpm.max_exec) + { + p_iocr->cpm.max_exec = exec; + } +} + +#if PNET_OPTION_REDUNDANCY +/** + * @internal + * Handle that the MRWDT timer expires. + * @param timer In: The timer instance. + * @param arg In: The IOCR instance. + */ +void pf_cpm_mrwdt_expired (/* ToDo: Make static later */ + os_timer_t * timer, + void * arg) +{ + pf_iocr_t * p_iocr = (pf_iocr_t *)arg; + + switch (p_iocr->cpm.state) + { + case PF_CPM_STATE_W_START: + case PF_CPM_STATE_FRUN: + /* Ignore */ + break; + case PF_CPM_STATE_RUN: + /* ToDo: Handle */ + break; + } +} +#endif + +static int pf_cpm_driver_sw_create (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + return 0; +} + +static int pf_cpm_driver_sw_close_req ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep) +{ + pf_cpm_t * p_cpm = &p_ar->iocrs[crep].cpm; + + p_cpm->ci_running = false; /* StopTimer */ + pf_scheduler_remove_if_running (net, &p_cpm->ci_timeout); + + pf_eth_frame_id_map_remove (net, p_cpm->frame_id[0]); + if (p_cpm->nbr_frame_id == 2) + { + pf_eth_frame_id_map_remove (net, p_cpm->frame_id[1]); + } + if (p_cpm->p_buffer_cpm != NULL) + { + pnal_buf_free (p_cpm->p_buffer_cpm); + } + if (p_cpm->p_buffer_app != NULL) + { + pnal_buf_free (p_cpm->p_buffer_app); + } + + return 0; +} + +/** + * @internal + * Replace the current buffer with a newer one and set the new_buf flag. + * @param net InOut: The p-net stack instance + * @param p_cpm InOut: The CPM instance. + * @param pp_buf In: The new buffer. + * Out: The previous buffer. + */ +static void pf_cpm_put_buf (pnet_t * net, pf_cpm_t * p_cpm, pnal_buf_t ** pp_buf) +{ + void * p; + + os_mutex_lock (net->cpm_buf_lock); + p = p_cpm->p_buffer_cpm; + p_cpm->p_buffer_cpm = *pp_buf; + *pp_buf = p; + p_cpm->new_buf = true; + os_mutex_unlock (net->cpm_buf_lock); +} + +/** + * @internal + * Make sure that p_buffer_app points to the newest received buffer. + * @param net InOut: The p-net stack instance + * @param p_cpm InOut: The CPM instance. + * @param p_new_flag Out: true if a new valid data frame has been + * received. + * @param pp_buffer Out: A pointer to the latest received data (or NULL). + */ +static void pf_cpm_get_buf ( + pnet_t * net, + pf_cpm_t * p_cpm, + bool * p_new_flag, + uint8_t ** pp_buffer) +{ + void * p; + + os_mutex_lock (net->cpm_buf_lock); + if (p_cpm->new_buf == true) + { + *p_new_flag = true; + p = p_cpm->p_buffer_app; + p_cpm->p_buffer_app = p_cpm->p_buffer_cpm; + p_cpm->p_buffer_cpm = p; + p_cpm->new_buf = false; + } + else + { + *p_new_flag = false; + } + os_mutex_unlock (net->cpm_buf_lock); + + if (p_cpm->p_buffer_app != NULL) + { + *pp_buffer = &((uint8_t *)((pnal_buf_t *)p_cpm->p_buffer_app) + ->payload)[p_cpm->buffer_pos]; + } + else + { + *pp_buffer = NULL; + } +} + +/** + * @internal + * Handle new incoming cyclic data frames on Ethernet. + * + * This is a callback for the frame handler, and is intended to be registered by + * \a pf_eth_frame_id_map_add(). Arguments should fulfill pf_eth_frame_handler_t + * + * Triggers the \a pnet_new_data_status_ind() user callback on data + * status changes. + * + * @param net InOut: The p-net stack instance + * @param frame_id In: The frame id of the frame. + * @param p_buf In: The received data. + * @param frame_id_pos In: Position of the frame id in the buffer. + * @param p_arg In: The IOCR instance. pf_iocr_t + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +static int pf_cpm_c_data_ind ( + pnet_t * net, + uint16_t frame_id, + pnal_buf_t * p_buf, + uint16_t frame_id_pos, + void * p_arg) +{ + int ret = 0; /* Means "Not handled" */ + pf_iocr_t * p_iocr = p_arg; + pf_cpm_t * p_cpm = &p_iocr->cpm; + uint16_t pos; + uint16_t len; + uint16_t cycle; + uint8_t transfer_status; + uint8_t data_status; + uint8_t changes; + uint8_t * p_ind_buf = (uint8_t *)p_buf->payload; + bool frame_structure; + bool c_sdu_structure; + bool dht_reload; + bool data_valid; + bool primary; + bool backup; + bool update_data; + + p_cpm->recv_cnt++; + + switch (p_cpm->state) + { + case PF_CPM_STATE_W_START: + p_iocr->p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_iocr->p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID_STATE; + p_cpm->errline = __LINE__; + p_cpm->errcnt++; + ret = 1; /* Means "handled" */ + break; + case PF_CPM_STATE_FRUN: + /* FALL-THRU */ + case PF_CPM_STATE_RUN: + pos = p_buf->len - 4; /* cycle counter is at the end of the data */ + + /* Data from the APDU */ + cycle = (uint16_t)p_ind_buf[pos] * 0x100 + p_ind_buf[pos + 1]; + data_status = p_ind_buf[pos + sizeof (uint16_t)]; + transfer_status = p_ind_buf[pos + sizeof (uint16_t) + 1]; + + /* ToDo: ReceivedInRED + * ToDo: LocalDataTransferControl + */ + data_valid = (data_status & 0x04) != 0; + backup = (data_status & 0x01) == 0; + primary = (data_status & 0x01) != 0; + + /* Compute length of p_buf without VLAN tags */ + len = p_buf->len - frame_id_pos; + len += 2 * sizeof (pnet_ethaddr_t) + sizeof (uint16_t); + + frame_structure = + ((transfer_status == 0) && + (pf_cpm_check_src_addr (p_cpm, p_buf) == 0) && + /* Frame_id is OK - or we would not be here. */ + (len == p_cpm->buffer_length)); + c_sdu_structure = (pf_cpm_check_cycle (p_cpm->cycle, cycle) == 0); + + dht_reload = + (frame_structure && c_sdu_structure && (data_valid || backup)); + update_data = + (frame_structure && c_sdu_structure && data_valid && + (primary || backup)); + + if (data_valid == false) + { + /* 19 */ + /* Ignore */ + LOG_DEBUG ( + PF_CPM_LOG, + "CPM_DRV_SW(%d): data_valid == false\n", + __LINE__); + } + else if (dht_reload) + { + if (p_cpm->state == PF_CPM_STATE_FRUN) + { + pf_cpm_state_ind (net, p_iocr->p_ar, p_iocr->crep, true); /* start + */ + } + + if (update_data) + { + /* 20 */ + pf_cpm_put_buf (net, p_cpm, &p_buf); + p_cpm->frame_id_pos = frame_id_pos; /* Save for consumer */ + p_cpm->buffer_pos = p_cpm->frame_id_pos + sizeof (uint16_t); + (void)pf_cmio_cpm_new_data_ind (p_iocr->p_ar, p_iocr->crep, true); + } + else + { + /* 21 */ + (void)pf_cmio_cpm_new_data_ind (p_iocr->p_ar, p_iocr->crep, false); + } + + /* 20, 21 */ + p_cpm->dht = 0; + + p_cpm->cycle = (int32_t)cycle; + changes = p_cpm->data_status ^ data_status; + p_cpm->data_status = data_status; + + if (changes != 0) + { + /* Notify the application about changes in the data_status */ + pf_fspm_data_status_changed ( + net, + p_iocr->p_ar, + p_iocr, + changes, + data_status); + } + pf_cpm_set_state (p_cpm, PF_CPM_STATE_RUN); + } + else + { + /* Ignore */ + LOG_DEBUG ( + PF_CPM_LOG, + "CPM_DRV_SW(%d): data_valid != false && dht_reload == 0\n", + __LINE__); + } + + ret = 1; /* Means "handled" */ + break; + } + + if (ret != 0) + { + if (p_buf != NULL) + { + p_cpm->free_cnt++; + pnal_buf_free (p_buf); + } + } + + return ret; +} + +static int pf_cpm_driver_sw_activate_req ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep) +{ + int ret = -1; + pf_cpm_t * p_cpm; + pf_iocr_t * p_iocr; + + p_iocr = &p_ar->iocrs[crep]; + p_cpm = &p_iocr->cpm; + + pf_eth_frame_id_map_add (net, p_cpm->frame_id[0], pf_cpm_c_data_ind, p_iocr); + + if (p_cpm->nbr_frame_id == 2) + { + p_cpm->frame_id[1] = p_cpm->frame_id[0] + 1; + pf_eth_frame_id_map_add ( + net, + p_cpm->frame_id[1], + pf_cpm_c_data_ind, + p_iocr); + } + pf_scheduler_init_handle (&p_cpm->ci_timeout, "cpm"); + ret = pf_scheduler_add ( + net, + p_cpm->control_interval, + pf_cpm_control_interval_expired, + p_iocr, + &p_cpm->ci_timeout); + if (ret != 0) + { + LOG_ERROR (PF_CPM_LOG, "CPM_DRV_SW(%d): Timeout not started\n", __LINE__); + } + else + { + p_cpm->ci_running = true; + } + + return ret; +} + +static int pf_cpm_driver_sw_get_data_and_iops ( + pnet_t * net, + pf_iocr_t * p_iocr, + const pf_iodata_object_t * p_iodata, + bool * p_new_flag, + uint8_t * p_data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len) +{ + int ret = -1; + + uint8_t * p_buffer = NULL; + + /* Get the latest frame buffer */ + pf_cpm_get_buf (net, &p_iocr->cpm, p_new_flag, &p_buffer); + + if (p_buffer != NULL) + { + os_mutex_lock (net->cpm_buf_lock); + if (p_iodata->data_length > 0) + { + memcpy ( + p_data, + &p_buffer[p_iodata->data_offset], + p_iodata->data_length); + } + if (p_iodata->iops_length > 0) + { + memcpy ( + p_iops, + &p_buffer[p_iodata->iops_offset], + p_iodata->iops_length); + } + os_mutex_unlock (net->cpm_buf_lock); + ret = 0; + } + else + { + *p_new_flag = false; + LOG_DEBUG ( + PF_CPM_LOG, + "CPM_DRV_SW(%d): No data received in get data\n", + __LINE__); + } + + return ret; +} + +static int pf_cpm_driver_sw_get_iocs ( + pnet_t * net, + pf_iocr_t * p_iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * p_iocs, + uint8_t iocs_len) +{ + int ret = -1; + uint8_t * p_buffer = NULL; + bool new_flag; + + pf_cpm_get_buf (net, &p_iocr->cpm, &new_flag, &p_buffer); + + if (p_buffer != NULL) + { + os_mutex_lock (net->cpm_buf_lock); + memcpy (p_iocs, &p_buffer[p_iodata->iocs_offset], p_iodata->iocs_length); + os_mutex_unlock (net->cpm_buf_lock); + ret = 0; + } + + return ret; +} + +static int pf_cpm_driver_sw_get_data_status ( + const pf_cpm_t * p_cpm, + uint8_t * p_data_status) +{ + *p_data_status = p_cpm->data_status; + return 0; +} + +static void pf_cpm_driver_sw_show (const pnet_t * net, const pf_cpm_t * p_cpm) +{ + printf ("pf_cpm_driver_sw_show not implemented\n"); +} + +void pf_cpm_driver_sw_init (pnet_t * net) +{ + static const pf_cpm_driver_t drv = { + .create = pf_cpm_driver_sw_create, + .activate_req = pf_cpm_driver_sw_activate_req, + .close_req = pf_cpm_driver_sw_close_req, + .get_data_and_iops = pf_cpm_driver_sw_get_data_and_iops, + .get_iocs = pf_cpm_driver_sw_get_iocs, + .get_data_status = pf_cpm_driver_sw_get_data_status, + .show = pf_cpm_driver_sw_show}; + + net->cpm_drv = &drv; + + LOG_INFO ( + PF_CPM_LOG, + "CPM_DRIVER_SW(%d): Default CPM driver installed\n", + __LINE__); +} diff --git a/profinet_stack/p-net/src/common/pf_cpm_driver_sw.h b/profinet_stack/p-net/src/common/pf_cpm_driver_sw.h new file mode 100644 index 0000000..dafb3f9 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_cpm_driver_sw.h @@ -0,0 +1,33 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CPM_DRIVER_SW_H +#define PF_CPM_DRIVER_SW_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize p-net default cpm driver. + * @param net InOut: The p-net stack instance + */ +void pf_cpm_driver_sw_init (pnet_t * net); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CPM_DRIVER_SW_H */ diff --git a/profinet_stack/p-net/src/common/pf_dcp.c b/profinet_stack/p-net/src/common/pf_dcp.c new file mode 100644 index 0000000..aebf1a0 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_dcp.c @@ -0,0 +1,2010 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Discovery and basic Configuration Protocol (DCP). + * + * DCP runs over Ethernet layer 2 (not IP or UDP). + * + * The main DCP messages are: + * - GET + * - SET + * - IDENTIFY (asking for a particular device) + * - HELLO (broadcast to indicate the presence of a device) + * + * Registers the different frame IDs for these messages with the frame handler. + * + * Responses to IDENTIFY are delayed (by an amount calculated from the + * MAC address), as many devices might respond to the same request. + * + * The SAM (Source Address ?) makes sure that that just a single remote + * MAC address is used for the communication. + * A timer of 3 seconds is used for the SAM. + */ + +#ifdef UNIT_TEST +#endif + +#include +#include + +#include "pf_includes.h" +#include "pf_block_writer.h" + +/* + * Various constants from the standard document. + */ +#define PF_DCP_SERVICE_GET 0x03 +#define PF_DCP_SERVICE_SET 0x04 +#define PF_DCP_SERVICE_IDENTIFY 0x05 +#define PF_DCP_SERVICE_HELLO 0x06 + +#define PF_DCP_BLOCK_PADDING 0x00 + +#define PF_DCP_SERVICE_TYPE_REQUEST 0x00 +#define PF_DCP_SERVICE_TYPE_SUCCESS 0x01 +#define PF_DCP_SERVICE_TYPE_NOT_SUPPORTED 0x05 + +#define PF_DCP_HEADER_SIZE 10 +#define PF_DCP_BLOCK_HDR_SIZE 4 + +#define PF_DCP_HELLO_FRAME_ID 0xfefc +#define PF_DCP_GET_SET_FRAME_ID 0xfefd +#define PF_DCP_ID_REQ_FRAME_ID 0xfefe +#define PF_DCP_ID_RES_FRAME_ID 0xfeff + +#define PF_DCP_SIGNAL_LED_HALF_INTERVAL 500000 /* 0.5 seconds */ +#define PF_DCP_SIGNAL_LED_NUMBER_OF_FLASHES 3 + +/* Client hold time, 3 seconds. + See Profinet 2.4 Services, section 6.3.11.2.2 */ +#define PF_DCP_SAM_TIMEOUT 3000000 /* microseconds */ + +CC_PACKED_BEGIN +typedef struct CC_PACKED pf_ethhdr +{ + pnet_ethaddr_t dest; + pnet_ethaddr_t src; + uint16_t type; /* Note: network endianness */ +} pf_ethhdr_t; +CC_PACKED_END + +CC_PACKED_BEGIN +typedef struct CC_PACKED pf_dcp_header +{ + uint8_t service_id; + uint8_t service_type; + uint32_t xid; /* Note: network endianness */ + uint16_t response_delay_factor; /* Note: network endianness */ + uint16_t data_length; /* Note: network endianness */ +} pf_dcp_header_t; +CC_PACKED_END +CC_STATIC_ASSERT (PF_DCP_HEADER_SIZE == sizeof (pf_dcp_header_t)); + +CC_PACKED_BEGIN +typedef struct CC_PACKED pf_dcp_block_hdr +{ + uint8_t option; + uint8_t sub_option; + uint16_t block_length; /* Note: network endianness */ +} pf_dcp_block_hdr_t; +CC_PACKED_END +CC_STATIC_ASSERT (PF_DCP_BLOCK_HDR_SIZE == sizeof (pf_dcp_block_hdr_t)); + +/* + * This is the standard DCP HELLO broadcast address (MAC). + */ +static const pnet_ethaddr_t dcp_mc_addr_hello = { + {0x01, 0x0e, 0xcf, 0x00, 0x00, 0x01}}; + +#define PF_MAC_NIL \ + { \ + { \ + 0, 0, 0, 0, 0, 0 \ + } \ + } + +static const pnet_ethaddr_t mac_nil = PF_MAC_NIL; + +/* + * A list of options that we can get/set/filter. + * Use in identify filter and identify response. + */ +typedef struct pf_dcp_opt_sub +{ + uint8_t opt; + uint8_t sub; +} pf_dcp_opt_sub_t; + +/* Device options to be included for example in DCP IDENTIFY responses */ +static const pf_dcp_opt_sub_t device_options[] = { + {PF_DCP_OPT_IP, PF_DCP_SUB_IP_PAR}, + {PF_DCP_OPT_IP, PF_DCP_SUB_IP_MAC}, + {PF_DCP_OPT_DEVICE_PROPERTIES, PF_DCP_SUB_DEV_PROP_VENDOR}, + {PF_DCP_OPT_DEVICE_PROPERTIES, PF_DCP_SUB_DEV_PROP_NAME}, + {PF_DCP_OPT_DEVICE_PROPERTIES, PF_DCP_SUB_DEV_PROP_ID}, + {PF_DCP_OPT_DEVICE_PROPERTIES, PF_DCP_SUB_DEV_PROP_ROLE}, + {PF_DCP_OPT_DEVICE_PROPERTIES, PF_DCP_SUB_DEV_PROP_OPTIONS}, + {PF_DCP_OPT_DEVICE_PROPERTIES, PF_DCP_SUB_DEV_PROP_ALIAS}, + {PF_DCP_OPT_CONTROL, PF_DCP_SUB_CONTROL_START}, + {PF_DCP_OPT_CONTROL, PF_DCP_SUB_CONTROL_STOP}, + {PF_DCP_OPT_CONTROL, PF_DCP_SUB_CONTROL_SIGNAL}, + {PF_DCP_OPT_CONTROL, PF_DCP_SUB_CONTROL_FACTORY_RESET}, + {PF_DCP_OPT_CONTROL, PF_DCP_SUB_CONTROL_RESET_TO_FACTORY}, +#if 0 + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_HOSTNAME}, + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_VENDOR_SPEC}, + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_SERVER_ID}, + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_PAR_REQ_LIST}, + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_CLASS_ID}, + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_CLIENT_ID}, + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_FQDN}, + {PF_DCP_OPT_DHCP, PF_DCP_SUB_DHCP_UUID_CLIENT_ID}, +#endif + // {PF_DCP_OPT_DEVICE_INITIATIVE, PF_DCP_SUB_DEV_INITIATIVE_SUPPORT}, + {PF_DCP_OPT_ALL, PF_DCP_SUB_ALL}, +}; + +/** + * @internal + * Send a delayed DCP response to an IDENTIFY request. + * + * The delay is important as many devices might respond to the same request. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * @param net InOut: The p-net stack instance + * @param arg In: DCP responder data. Should be pnal_buf_t. + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. Not used here. + */ +static void pf_dcp_responder (pnet_t * net, void * arg, uint32_t current_time) +{ + pnal_buf_t * p_buf = (pnal_buf_t *)arg; + + pf_scheduler_reset_handle (&net->dcp_identresp_timeout); + + if (p_buf == NULL) + { + return; + } + + if (net->dcp_delayed_response_waiting == false) + { + return; + } + + if (pf_eth_send_on_management_port (net, p_buf) > 0) + { + LOG_DEBUG (PNET_LOG, "DCP(%d): Sent a DCP identify response.\n", __LINE__); + } + + pnal_buf_free (p_buf); + net->dcp_delayed_response_waiting = false; +} + +/** + * @internal + * Clear SAM + * SAM limits the communication to a single remote MAC address + * for 3 seconds. This operation implements the timeout callback. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * @param net InOut: The p-net stack instance + * @param arg In: Not used. + * @param current_time In: Not used. + */ +static void pf_dcp_clear_sam (pnet_t * net, void * arg, uint32_t current_time) +{ + LOG_DEBUG ( + PF_DCP_LOG, + "DCP(%d): SAM timeout. Clear stored remote MAC address.\n", + __LINE__); + net->dcp_sam = mac_nil; + pf_scheduler_reset_handle (&net->dcp_sam_timeout); +} + +/** + * @internal + * Restart SAM timeout + * SAM limits the communication to a single remote MAC address + * for 3 seconds. + * + * @param net InOut: The p-net stack instance + * @param mac In: Remote MAC address + */ +static void pf_dcp_restart_sam_timeout (pnet_t * net, const pnet_ethaddr_t * mac) +{ + /* Make sure no other MAC address is used in the DCP communication + * for 3 seconds */ + LOG_DEBUG ( + PF_DCP_LOG, + "DCP(%d): Update SAM remote MAC address, and restart timeout.\n", + __LINE__); + + memcpy (&net->dcp_sam, mac, sizeof (net->dcp_sam)); + (void)pf_scheduler_restart ( + net, + PF_DCP_SAM_TIMEOUT, + pf_dcp_clear_sam, + NULL, + &net->dcp_sam_timeout); +} + +/** + * @internal + * Check SAM + * SAM limits the communication to a single remote MAC address + * for 3 seconds. + * + * @param net InOut: The p-net stack instance + * @param mac In: Remote MAC address + * @return true if DCP communication is allowed with respect to SAM. + * false if not. + */ +static bool pf_dcp_check_sam (pnet_t * net, const pnet_ethaddr_t * mac) +{ + return ((memcmp (&net->dcp_sam, &mac_nil, sizeof (net->dcp_sam))) == 0) || + (memcmp (&net->dcp_sam, mac, sizeof (net->dcp_sam)) == 0); +} + +/** + * @internal + * Check that address is not a multi-cast address + * and also that it matches the local MAC address. + * + * This check was added to handle situation where network + * interface is in promiscuous mode and the need to filter out + * DCP requests intended for other devices. + * + * @param local_mac In: Local MAC address. Should be the + * MAC address of main (DAP) interface. + * @param destination_mac In: Destination MAC address + * @return true if destination address is ok and shall packet shall + * be accepted. false if not. + */ +bool pf_dcp_check_destination_address ( + const pnet_ethaddr_t * local_mac, + const pnet_ethaddr_t * destination_mac) +{ + return ( + ((destination_mac->addr[0] & 1) == 0) && /* Not multi-cast */ + (memcmp (destination_mac, local_mac, sizeof (*destination_mac)) == 0)); +} + +/** + * @internal + * Add a block to a buffer, possibly including a block_info. + * + * @param p_dst Out: The destination buffer. + * @param p_dst_pos InOut: Position in the destination buffer. + * @param dst_max In: Size of destination buffer. + * @param opt In: Option key. + * @param sub In: Sub-option key. + * @param with_block_info In: If true then add block_info argument. + * @param block_info In: The block info argument. + * @param value_length In: The length in bytes of the p_value data. + * @param p_value In: The source data. + * @return + */ +static int pf_dcp_put_block ( + uint8_t * p_dst, + uint16_t * p_dst_pos, + uint16_t dst_max, + uint8_t opt, + uint8_t sub, + bool with_block_info, + uint16_t block_info, + uint16_t value_length, /* Not including length of block_info */ + const void * p_value) +{ + uint16_t b_len; + + if ((*p_dst_pos + value_length) >= dst_max) + { + return 0; /* Skip excess data silently */ + } + + /* Adjust written block length if block_info is included */ + b_len = value_length; + if (with_block_info) + { + b_len += sizeof (b_len); + } + + pf_put_byte (opt, dst_max, p_dst, p_dst_pos); + pf_put_byte (sub, dst_max, p_dst, p_dst_pos); + pf_put_uint16 (true, b_len, dst_max, p_dst, p_dst_pos); + if (with_block_info == true) + { + pf_put_uint16 (true, block_info, dst_max, p_dst, p_dst_pos); + } + if ((p_value != NULL) && (value_length > 0)) + { + pf_put_mem (p_value, value_length, dst_max, p_dst, p_dst_pos); + } + + /* Add padding to align on uint16_t */ + while ((*p_dst_pos) & 1) + { + pf_put_byte (0, dst_max, p_dst, p_dst_pos); + } + + return 0; +} + +/** + * @internal + * Handle one block in a DCP get request, by inserting it into the response. + * + * @param net InOut: The p-net stack instance + * @param p_dst Out: The destination buffer. + * @param p_dst_pos InOut: Position in the destination buffer. + * @param dst_max In: Size of destination buffer. + * @param opt In: Option key. + * @param sub In: Sub-option key. + * @param request_is_identify In: Usage in response to Identify request + * (skips some blocks) + * @param alias_name In: Alias name appended if != NULL + * @return + */ +static int pf_dcp_get_req ( + pnet_t * net, + uint8_t * p_dst, + uint16_t * p_dst_pos, + uint16_t dst_max, + uint8_t opt, + uint8_t sub, + bool request_is_identify, + const char * alias_name) +{ + int ret = 0; /* Assume all OK */ + uint8_t block_error = PF_DCP_BLOCK_ERROR_NO_ERROR; + uint8_t negative_response_data[3]; /* For negative get */ + uint16_t block_info = 0; + uint16_t value_length = 0; + uint8_t * p_value = NULL; + bool skip = false; /* When true: Do not insert block in response */ + pf_full_ip_suite_t full_ip_suite; + uint16_t temp16; + uint16_t ix; + + /* Get the data */ + ret = pf_cmina_dcp_get_req ( + net, + opt, + sub, + &value_length, + &p_value, + &block_error); + + /* Some very specific options require special treatment */ + switch (opt) + { + case PF_DCP_OPT_IP: + switch (sub) + { + case PF_DCP_SUB_IP_PAR: + memcpy ( + &full_ip_suite.ip_suite, + p_value, + sizeof (full_ip_suite.ip_suite)); + full_ip_suite.ip_suite.ip_addr = + htonl (full_ip_suite.ip_suite.ip_addr); + full_ip_suite.ip_suite.ip_mask = + htonl (full_ip_suite.ip_suite.ip_mask); + full_ip_suite.ip_suite.ip_gateway = + htonl (full_ip_suite.ip_suite.ip_gateway); + p_value = (uint8_t *)&full_ip_suite.ip_suite; + + block_info = ((full_ip_suite.ip_suite.ip_addr == 0) && + (full_ip_suite.ip_suite.ip_mask == 0) && + (full_ip_suite.ip_suite.ip_gateway == 0)) + ? 0 + : BIT (0); + /* ToDo: We do not yet support DHCP (block_info, BIT(1)) */ + /* ToDo: We do not yet report on "IP address conflict" (block_info, + * BIT(7)) */ + break; + case PF_DCP_SUB_IP_SUITE: + memcpy (&full_ip_suite, p_value, sizeof (full_ip_suite)); + full_ip_suite.ip_suite.ip_addr = + htonl (full_ip_suite.ip_suite.ip_addr); + full_ip_suite.ip_suite.ip_mask = + htonl (full_ip_suite.ip_suite.ip_mask); + full_ip_suite.ip_suite.ip_gateway = + htonl (full_ip_suite.ip_suite.ip_gateway); + for (ix = 0; ix < NELEMENTS (full_ip_suite.dns_addr); ix++) + { + full_ip_suite.dns_addr[ix] = htonl (full_ip_suite.dns_addr[ix]); + } + p_value = (uint8_t *)&full_ip_suite; + + block_info = ((full_ip_suite.ip_suite.ip_addr == 0) && + (full_ip_suite.ip_suite.ip_mask == 0) && + (full_ip_suite.ip_suite.ip_gateway == 0)) + ? 0 + : BIT (0); + /* ToDo: We do not yet support DHCP (block_info, BIT(1)) */ + /* ToDo: We do not yet report on "IP address conflict" (block_info, + * BIT(7)) */ + break; + case PF_DCP_SUB_IP_MAC: + skip = request_is_identify; + break; + default: + break; + } + break; + case PF_DCP_OPT_DEVICE_PROPERTIES: + switch (sub) + { + case PF_DCP_SUB_DEV_PROP_OPTIONS: + if (sizeof (device_options) > 0) + { + p_value = (uint8_t *)device_options; + value_length = sizeof (device_options); + ret = 0; + } + else + { + skip = true; + ret = 0; /* This is OK - just omit the entire block. */ + } + break; + case PF_DCP_SUB_DEV_PROP_GATEWAY: + if (value_length == sizeof (temp16)) + { + memcpy (&temp16, p_value, sizeof (temp16)); + temp16 = htons (temp16); + p_value = (uint8_t *)&temp16; + } + else + { + ret = -1; + } + break; + case PF_DCP_SUB_DEV_PROP_NAME: + value_length = (uint16_t)strlen ((char *)p_value); + break; + case PF_DCP_SUB_DEV_PROP_ALIAS: + skip = (alias_name == NULL); + if (!skip) + { + value_length = (uint16_t)strlen (alias_name); + p_value = (uint8_t *)alias_name; + ret = 0; + } + break; + case PF_DCP_SUB_DEV_PROP_VENDOR: + value_length = (uint16_t)strlen ((char *)p_value); + break; + case PF_DCP_SUB_DEV_PROP_ROLE: + value_length += 1; + break; + case PF_DCP_SUB_DEV_PROP_ID: + case PF_DCP_SUB_DEV_PROP_INSTANCE: + case PF_DCP_SUB_DEV_PROP_OEM_ID: + default: + break; + } + break; + case PF_DCP_OPT_DEVICE_INITIATIVE: + if (value_length == sizeof (temp16)) + { + memcpy (&temp16, p_value, sizeof (temp16)); + temp16 = htons (temp16); + p_value = (uint8_t *)&temp16; + } + else + { + ret = -1; + } + break; + case PF_DCP_OPT_CONTROL: + skip = true; + break; + case PF_DCP_OPT_ALL: + skip = true; + break; + case PF_DCP_OPT_DHCP: + default: + break; + } + + if (skip == false) + { + if (ret == 0) + { + ret = pf_dcp_put_block ( + p_dst, + p_dst_pos, + dst_max, + opt, + sub, + true, + block_info, + value_length, + p_value); + } + else + { + /* GetNegResBlock consists of: + * - option = Control 1 byte + * - suboption = Response 1 byte + * - block length 2 bytes + * - type of option involved (option + suboption) 2 bytes + * - block error 1 byte + */ + negative_response_data[0] = opt; + negative_response_data[1] = sub; + negative_response_data[2] = block_error; + ret = pf_dcp_put_block ( + p_dst, + p_dst_pos, + dst_max, + PF_DCP_OPT_CONTROL, + PF_DCP_SUB_CONTROL_RESPONSE, + false, + 0, + sizeof (negative_response_data), + negative_response_data); + } + } + + return ret; +} + +/** + * @internal + * Blink Profinet signal LED. + * + * This functions blinks the Profinet signal LED 3 times at 1 Hz. + * The LED is accessed via the pnet_signal_led_ind() callback function. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * @param net InOut: The p-net stack instance + * @param arg In: The current state (number of remaining + * transitions) + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + * Not used here. + */ +static void pf_dcp_control_signal_led ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + uint32_t state = (uint32_t)(uintptr_t)arg; + + if ((state % 2) == 1) + { + /* Turn LED on */ + if (pf_fspm_signal_led_ind (net, true) != 0) + { + LOG_ERROR ( + PF_DCP_LOG, + "DCP(%d): Could not turn signal LED on\n", + __LINE__); + } + } + else + { + /* Turn LED off */ + if (pf_fspm_signal_led_ind (net, false) != 0) + { + LOG_ERROR ( + PF_DCP_LOG, + "DCP(%d): Could not turn signal LED off\n", + __LINE__); + } + } + + if ((state > 0) && (state < 200)) /* Plausibility test */ + { + state--; + + /* Schedule another round */ + (void)pf_scheduler_add ( + net, + PF_DCP_SIGNAL_LED_HALF_INTERVAL, + pf_dcp_control_signal_led, + (void *)(uintptr_t)state, + &net->dcp_led_timeout); + } + else + { + pf_scheduler_reset_handle (&net->dcp_led_timeout); + } +} + +/** + * @internal + * Trigger blinking Profinet signal LED. + * + * If the LED already is blinking, do not restart the blinking. + * + * @param net InOut: The p-net stack instance + * @return 0 on success + * -1 on error + */ +int pf_dcp_trigger_signal_led (pnet_t * net) +{ + if (pf_scheduler_is_running (&net->dcp_led_timeout) == false) + { + LOG_INFO ( + PF_DCP_LOG, + "DCP(%d): Received request to flash LED\n", + __LINE__); + (void)pf_scheduler_add ( + net, + PF_DCP_SIGNAL_LED_HALF_INTERVAL, + pf_dcp_control_signal_led, + (void *)(2 * PF_DCP_SIGNAL_LED_NUMBER_OF_FLASHES - 1), + &net->dcp_led_timeout); + } + else + { + LOG_INFO ( + PF_DCP_LOG, + "DCP(%d): Received request to flash LED, but it is already " + "flashing.\n", + __LINE__); + } + + return 0; +} + +/** + * @internal + * Handle one block in a DCP set request, by inserting it into the response. + * + * Triggers the application callback \a pnet_reset_ind() for some values. + * + * @param net InOut: The p-net stack instance + * @param p_dst Out: The destination buffer. + * @param p_dst_pos InOut: Position in the destination buffer. + * @param dst_max In: Size of destination buffer. + * @param opt In: Option key. + * @param sub In: Sub-option key. + * @param block_info In: The block info argument. + * @param value_length In: The length in bytes of the p_value data. + * @param p_value In: The source data. + * @return + */ +static int pf_dcp_set_req ( + pnet_t * net, + uint8_t * p_dst, + uint16_t * p_dst_pos, + uint16_t dst_max, + uint8_t opt, + uint8_t sub, + uint16_t block_qualifier, + uint16_t value_length, + const uint8_t * p_value) +{ + int ret = -1; + uint8_t response_data[3]; + pf_full_ip_suite_t full_ip_suite; + uint16_t ix; + + response_data[0] = opt; + response_data[1] = sub; + /* Assume negative response initially */ + response_data[2] = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + + switch (opt) + { + case PF_DCP_OPT_IP: + switch (sub) + { + case PF_DCP_SUB_IP_MAC: + /* Can't set MAC address */ + break; + case PF_DCP_SUB_IP_PAR: + if (value_length == sizeof (full_ip_suite.ip_suite)) + { + memcpy ( + &full_ip_suite.ip_suite, + p_value, + sizeof (full_ip_suite.ip_suite)); + full_ip_suite.ip_suite.ip_addr = + ntohl (full_ip_suite.ip_suite.ip_addr); + full_ip_suite.ip_suite.ip_mask = + ntohl (full_ip_suite.ip_suite.ip_mask); + full_ip_suite.ip_suite.ip_gateway = + ntohl (full_ip_suite.ip_suite.ip_gateway); + + ret = pf_cmina_dcp_set_ind ( + net, + opt, + sub, + block_qualifier, + value_length, + (uint8_t *)&full_ip_suite.ip_suite, + &response_data[2]); + } + break; + case PF_DCP_SUB_IP_SUITE: + if (value_length == sizeof (full_ip_suite.ip_suite)) + { + memcpy (&full_ip_suite, p_value, sizeof (full_ip_suite)); + full_ip_suite.ip_suite.ip_addr = + ntohl (full_ip_suite.ip_suite.ip_addr); + full_ip_suite.ip_suite.ip_mask = + ntohl (full_ip_suite.ip_suite.ip_mask); + full_ip_suite.ip_suite.ip_gateway = + ntohl (full_ip_suite.ip_suite.ip_gateway); + for (ix = 0; ix < NELEMENTS (full_ip_suite.dns_addr); ix++) + { + full_ip_suite.dns_addr[ix] = ntohl (full_ip_suite.dns_addr[ix]); + } + + ret = pf_cmina_dcp_set_ind ( + net, + opt, + sub, + block_qualifier, + value_length, + (uint8_t *)&full_ip_suite.ip_suite, + &response_data[2]); + } + break; + default: + break; + } + break; + case PF_DCP_OPT_DEVICE_PROPERTIES: + switch (sub) + { + case PF_DCP_SUB_DEV_PROP_NAME: + ret = pf_cmina_dcp_set_ind ( + net, + opt, + sub, + block_qualifier, + value_length, + p_value, + &response_data[2]); + break; + case PF_DCP_SUB_DEV_PROP_VENDOR: + case PF_DCP_SUB_DEV_PROP_ID: + case PF_DCP_SUB_DEV_PROP_ROLE: + case PF_DCP_SUB_DEV_PROP_OPTIONS: + case PF_DCP_SUB_DEV_PROP_ALIAS: + case PF_DCP_SUB_DEV_PROP_INSTANCE: + case PF_DCP_SUB_DEV_PROP_OEM_ID: + case PF_DCP_SUB_DEV_PROP_GATEWAY: + /* Can't set these */ + break; + default: + break; + } + break; + case PF_DCP_OPT_DHCP: + switch (sub) + { + case PF_DCP_SUB_DHCP_HOSTNAME: + case PF_DCP_SUB_DHCP_VENDOR_SPEC: + case PF_DCP_SUB_DHCP_SERVER_ID: + case PF_DCP_SUB_DHCP_PAR_REQ_LIST: + case PF_DCP_SUB_DHCP_CLASS_ID: + case PF_DCP_SUB_DHCP_CLIENT_ID: + case PF_DCP_SUB_DHCP_FQDN: + case PF_DCP_SUB_DHCP_UUID_CLIENT_ID: + /* ToDo: DHCP Not yet implemented */ + break; + case PF_DCP_SUB_DHCP_CONTROL: + /* Can't touch this */ + break; + default: + break; + } + break; + case PF_DCP_OPT_CONTROL: + /* Just ignore if not supported */ + switch (sub) + { + case PF_DCP_SUB_CONTROL_START: + net->dcp_global_block_qualifier = block_qualifier; + response_data[2] = PF_DCP_BLOCK_ERROR_NO_ERROR; + ret = 0; + break; + case PF_DCP_SUB_CONTROL_STOP: + if (block_qualifier == net->dcp_global_block_qualifier) + { + response_data[2] = PF_DCP_BLOCK_ERROR_NO_ERROR; + ret = 0; + } + else + { + ret = 0; + } + break; + case PF_DCP_SUB_CONTROL_SIGNAL: + /* Schedule a state-machine that flashes "the LED" for 3s, 1Hz. */ + ret = pf_dcp_trigger_signal_led (net); + break; + case PF_DCP_SUB_CONTROL_RESPONSE: + /* Can't set this */ + break; + case PF_DCP_SUB_CONTROL_FACTORY_RESET: + case PF_DCP_SUB_CONTROL_RESET_TO_FACTORY: + if ( + pf_cmina_dcp_set_ind ( + net, + opt, + sub, + block_qualifier, + value_length, + p_value, + &response_data[2]) != 0) + { + LOG_ERROR ( + PF_DCP_LOG, + "DCP(%d): Could not perform Factory Reset\n", + __LINE__); + } + else + { + ret = 0; + } + break; + default: + break; + } + break; + case PF_DCP_OPT_DEVICE_INITIATIVE: + default: + /* Can't set this */ + response_data[2] = PF_DCP_BLOCK_ERROR_OPTION_NOT_SUPPORTED; + break; + } + + if (ret == 0) + { + response_data[2] = PF_DCP_BLOCK_ERROR_NO_ERROR; + } + + /* SetResBlock and SetNegResBlock consists of: + * - option = Control 1 byte + * - suboption = Response 1 byte + * - block length 2 bytes + * - type of option involved (option + suboption) 2 bytes + * - block error 1 byte (=0 when no error) + */ + (void)pf_dcp_put_block ( + p_dst, + p_dst_pos, + dst_max, + PF_DCP_OPT_CONTROL, + PF_DCP_SUB_CONTROL_RESPONSE, + false, + 0, + sizeof (response_data), + response_data); + + return ret; +} + +/** + * @internal + * Handle a DCP Set or Get request. + * + * Triggers the application callback \a pnet_reset_ind() for some values. + * Might set the IP address. + * + * This is a callback for the frame handler. Arguments should fulfill + * pf_eth_frame_handler_t + * + * @param net InOut: The p-net stack instance + * @param frame_id In: The frame id. + * @param p_buf InOut: The Ethernet frame. Will be freed. + * @param frame_id_pos In: Position of the frame id in the buffer. + * Depends on whether VLAN tagging is used. + * @param p_arg In: Not used. + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +static int pf_dcp_get_set ( + pnet_t * net, + uint16_t frame_id, + pnal_buf_t * p_buf, + uint16_t frame_id_pos, + void * p_arg) +{ + uint8_t * p_src; + uint16_t src_pos; + uint16_t src_dcplen; + uint16_t src_block_len; + uint16_t src_block_qualifier; + pf_ethhdr_t * p_src_ethhdr; + pf_dcp_header_t * p_src_dcphdr; + pf_dcp_block_hdr_t * p_src_block_hdr; + pnal_buf_t * p_rsp = NULL; + uint8_t * p_dst; + uint16_t dst_pos; + uint16_t dst_start; + pf_ethhdr_t * p_dst_ethhdr; + pf_dcp_header_t * p_dst_dcphdr; + const pnet_ethaddr_t * mac_address = pf_cmina_get_device_macaddr (net); + + if (p_buf == NULL) + { + return 1; /* Buffer handled */ + } + + /* Extract info from the request */ + p_src = (uint8_t *)p_buf->payload; + src_pos = 0; + p_src_ethhdr = (pf_ethhdr_t *)&p_src[src_pos]; + + src_pos = frame_id_pos; + src_pos += sizeof (uint16_t); /* FrameId */ + p_src_dcphdr = (pf_dcp_header_t *)&p_src[src_pos]; + + src_pos += sizeof (pf_dcp_header_t); + src_dcplen = (src_pos + ntohs (p_src_dcphdr->data_length)); + + if (src_dcplen > p_buf->len) + { + goto out; + } + + p_rsp = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); /* Get a transmit buffer + for the response */ + if (p_rsp == NULL) + { + goto out; + } + + if (pf_dcp_check_sam (net, &p_src_ethhdr->src) == false) + { + goto out; + } + + if (pf_dcp_check_destination_address (mac_address, &p_src_ethhdr->dest) == false) + { + goto out; + } + + /* Prepare the response */ + p_dst = (uint8_t *)p_rsp->payload; + dst_pos = 0; + p_dst_ethhdr = (pf_ethhdr_t *)&p_dst[dst_pos]; + dst_pos += sizeof (pf_ethhdr_t); + + /* Insert frame ID into response */ + p_dst[dst_pos++] = ((PF_DCP_GET_SET_FRAME_ID & 0xff00) >> 8); + p_dst[dst_pos++] = PF_DCP_GET_SET_FRAME_ID & 0xff; + + p_dst_dcphdr = (pf_dcp_header_t *)&p_dst[dst_pos]; + dst_pos += sizeof (pf_dcp_header_t); + + /* Save position for later */ + dst_start = dst_pos; + + /* Set eth header in the response */ + memcpy ( + p_dst_ethhdr->dest.addr, + p_src_ethhdr->src.addr, + sizeof (pnet_ethaddr_t)); + memcpy (p_dst_ethhdr->src.addr, mac_address->addr, sizeof (pnet_ethaddr_t)); + p_dst_ethhdr->type = htons (PNAL_ETHTYPE_PROFINET); + + /* Copy DCP header from the request, and modify what is needed. */ + *p_dst_dcphdr = *p_src_dcphdr; + p_dst_dcphdr->service_type = PF_DCP_SERVICE_TYPE_SUCCESS; + p_dst_dcphdr->response_delay_factor = htons (0); + + if (p_src_dcphdr->service_id == PF_DCP_SERVICE_SET) + { + LOG_DEBUG ( + PF_DCP_LOG, + "DCP(%d): Incoming DCP Set request. Xid: %" PRIu32 "\n", + __LINE__, + ntohl (p_src_dcphdr->xid)); + p_src_block_hdr = (pf_dcp_block_hdr_t *)&p_src[src_pos]; + src_pos += sizeof (*p_src_block_hdr); /* Point to the block value */ + src_block_len = ntohs (p_src_block_hdr->block_length); + + while (src_dcplen >= (src_pos + src_block_len)) + { + /* Extract block qualifier */ + src_block_qualifier = ntohs (*(uint16_t *)&p_src[src_pos]); + src_pos += sizeof (uint16_t); + src_block_len -= sizeof (uint16_t); + + (void)pf_dcp_set_req ( + net, + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + p_src_block_hdr->option, + p_src_block_hdr->sub_option, + src_block_qualifier, + src_block_len, + &p_src[src_pos]); + + /* Point to next block */ + src_pos += src_block_len; + + /* Skip padding to align on uint16_t */ + while (src_pos & 1) + { + src_pos++; + } + p_src_block_hdr = (pf_dcp_block_hdr_t *)&p_src[src_pos]; + src_pos += sizeof (*p_src_block_hdr); /* Point to the block value + */ + src_block_len = ntohs (p_src_block_hdr->block_length); + } + + pf_dcp_restart_sam_timeout (net, &p_src_ethhdr->src); + } + else if (p_src_dcphdr->service_id == PF_DCP_SERVICE_GET) + { + LOG_DEBUG ( + PF_DCP_LOG, + "DCP(%d): Incoming DCP Get request. Xid: %" PRIu32 "\n", + __LINE__, + ntohl (p_src_dcphdr->xid)); + while (src_dcplen >= (src_pos + sizeof (uint8_t) + sizeof (uint8_t))) + { + (void)pf_dcp_get_req ( + net, + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + p_src[src_pos], + p_src[src_pos + 1], + false, + false); + + /* Point to next block */ + src_pos += sizeof (uint8_t) + sizeof (uint8_t); + } + + pf_dcp_restart_sam_timeout (net, &p_src_ethhdr->src); + } + else + { + LOG_ERROR ( + PF_DCP_LOG, + "DCP(%d): Unknown DCP service id %u. Xid: %" PRIu32 "\n", + __LINE__, + (unsigned)p_src_dcphdr->service_id, + ntohl (p_src_dcphdr->xid)); + p_dst_dcphdr->service_type = PF_DCP_SERVICE_TYPE_NOT_SUPPORTED; + } + + /* Insert final response length and ship it! */ + p_dst_dcphdr->data_length = htons (dst_pos - dst_start); + p_rsp->len = dst_pos; + + if (pf_eth_send_on_management_port (net, p_rsp) > 0) + { + LOG_DEBUG (PF_DCP_LOG, "DCP(%d): Sent DCP Get/Set response\n", __LINE__); + } + + if (p_src_dcphdr->service_id == PF_DCP_SERVICE_SET) + { + pf_cmina_dcp_set_commit (net); + } + + /* Send LLDP _after_ the response in order to pass I/O-tester tests. */ + pf_pdport_lldp_restart_transmission (net); + +out: + if (p_buf != NULL) + { + pnal_buf_free (p_buf); + } + if (p_rsp != NULL) + { + pnal_buf_free (p_rsp); + } + + return 1; /* Buffer handled */ +} + +/** + * @internal + * Handle an incoming HELLO message + * + * The HELLO indication is used by another device to find its MC peer. + * + * This is a callback for the frame handler. Arguments should fulfill + * pf_eth_frame_handler_t + * + * ToDo: Device-device communication (MC) is not yet implemented. + * @param net InOut: The p-net stack instance + * @param frame_id In: The frame id. + * @param p_buf InOut: The Ethernet frame. Will be freed. + * @param frame_id_pos In: Position of the frame id in the buffer. + * Depends on whether VLAN tagging is used. + * @param p_arg In: Not used. + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +static int pf_dcp_hello_ind ( + pnet_t * net, + uint16_t frame_id, + pnal_buf_t * p_buf, + uint16_t frame_id_pos, + void * p_arg) /* Not used */ +{ + pf_ethhdr_t * p_eth_hdr = p_buf->payload; + + if ( + memcmp ( + p_eth_hdr->dest.addr, + dcp_mc_addr_hello.addr, + sizeof (dcp_mc_addr_hello.addr)) != 0) + { + /* Wrong dest eth address - discard. */ + } + else + { + /* + * input = DCP-MC-Header, NameOfStationBlockRes, IPParameterBlockRes, + * DeviceIDBlockRes, [DeviceOptionsBlockRes ^ DeviceVendorBlockRes], + * DeviceRoleBlockRes, DeviceInitiativeBlockRes + */ + + /* + * NameOfStationBlockRes = NameOfStationType, DCPBlockLength, BlockInfo, + * NameOfStationValue NameOfStationType = DevicePropertiesOption (=0x02), + * SuboptionNameOfStation (=0x02) + */ + + /* + * IPParameterBlockRes = IPParameterType, DCPBlockLength, BlockInfo, + * IPParameterValue IPParameterType = IPOption (=0x01) + */ + + /* + * DeviceIDBlockRes = DeviceIDType, DCPBlockLength, BlockInfo, + * DeviceIDValue + */ + + /* + * DeviceOptionsBlockRes = DeviceOptionsType, DCPBlockLength, BlockInfo, + * DeviceOptionsValue + */ + + /* + * DeviceVendorBlockRes = DeviceVendorType, DCPBlockLength, BlockInfo, + * DeviceVendorValue + */ + + /* + * DeviceRoleBlockRes = DeviceRoleType, DCPBlockLength, BlockInfo, + * DeviceRoleValue + */ + + /* + * DeviceInitiativeBlockRes = DeviceInitiativeType, DCPBlockLength, + * BlockInfo, DeviceInitiativeValue + */ + } + pnal_buf_free (p_buf); + return 1; /* Means: handled */ +} + +int pf_dcp_hello_req (pnet_t * net) +{ + pnal_buf_t * p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + uint8_t * p_dst; + uint16_t dst_pos; + uint16_t dst_start_pos; + pf_ethhdr_t * p_ethhdr; + pf_dcp_header_t * p_dcphdr; + uint16_t value_length; + uint8_t * p_value; + uint8_t block_error; + uint16_t temp16; + pf_ip_suite_t ip_suite; + const pnet_ethaddr_t * mac_address = pf_cmina_get_device_macaddr (net); + + if (p_buf == NULL) + { + return -1; + } + + p_dst = (uint8_t *)p_buf->payload; + if (p_dst == NULL) + { + pnal_buf_free (p_buf); + return -1; + } + + dst_pos = 0; + p_ethhdr = (pf_ethhdr_t *)&p_dst[dst_pos]; + memcpy ( + p_ethhdr->dest.addr, + dcp_mc_addr_hello.addr, + sizeof (p_ethhdr->dest.addr)); + memcpy (p_ethhdr->src.addr, mac_address->addr, sizeof (pnet_ethaddr_t)); + + p_ethhdr->type = htons (PNAL_ETHTYPE_PROFINET); + dst_pos += sizeof (pf_ethhdr_t); + + /* Insert FrameId */ + p_dst[dst_pos++] = (PF_DCP_HELLO_FRAME_ID & 0xff00) >> 8; + p_dst[dst_pos++] = PF_DCP_HELLO_FRAME_ID & 0x00ff; + + p_dcphdr = (pf_dcp_header_t *)&p_dst[dst_pos]; + p_dcphdr->service_id = PF_DCP_SERVICE_HELLO; + p_dcphdr->service_type = PF_DCP_SERVICE_TYPE_REQUEST; + p_dcphdr->xid = htonl (1); + p_dcphdr->response_delay_factor = htons (0); + p_dcphdr->data_length = htons (0); /* At the moment */ + dst_pos += sizeof (pf_dcp_header_t); + + dst_start_pos = dst_pos; + + if ( + pf_cmina_dcp_get_req ( + net, + PF_DCP_OPT_DEVICE_PROPERTIES, + PF_DCP_SUB_DEV_PROP_NAME, + &value_length, + &p_value, + &block_error) == 0) + { + LOG_DEBUG ( + PF_DCP_LOG, + "DCP(%d): Sending DCP Hello request. Station name: %s\n", + __LINE__, + (char *)p_value); + (void)pf_dcp_put_block ( + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + PF_DCP_OPT_DEVICE_PROPERTIES, + PF_DCP_SUB_DEV_PROP_NAME, + true, + 0, + (uint16_t)strlen ((char *)p_value), + p_value); + } + + if ( + pf_cmina_dcp_get_req ( + net, + PF_DCP_OPT_IP, + PF_DCP_SUB_IP_PAR, + &value_length, + &p_value, + &block_error) == 0) + { + memcpy (&ip_suite, p_value, sizeof (ip_suite)); + ip_suite.ip_addr = htonl (ip_suite.ip_addr); + ip_suite.ip_mask = htonl (ip_suite.ip_mask); + ip_suite.ip_gateway = htonl (ip_suite.ip_gateway); + (void)pf_dcp_put_block ( + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + PF_DCP_OPT_IP, + PF_DCP_SUB_IP_PAR, + true, + 0, + value_length, + &ip_suite); + } + + if ( + pf_cmina_dcp_get_req ( + net, + PF_DCP_OPT_DEVICE_PROPERTIES, + PF_DCP_SUB_DEV_PROP_ID, + &value_length, + &p_value, + &block_error) == 0) + { + (void)pf_dcp_put_block ( + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + PF_DCP_OPT_DEVICE_PROPERTIES, + PF_DCP_SUB_DEV_PROP_ID, + true, + 0, + value_length, + p_value); + } + + if ( + pf_cmina_dcp_get_req ( + net, + PF_DCP_OPT_DEVICE_PROPERTIES, + PF_DCP_SUB_DEV_PROP_OEM_ID, + &value_length, + &p_value, + &block_error) == 0) + { + (void)pf_dcp_put_block ( + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + PF_DCP_OPT_DEVICE_PROPERTIES, + PF_DCP_SUB_DEV_PROP_OEM_ID, + true, + 0, + value_length, + p_value); + } + + if ( + pf_cmina_dcp_get_req ( + net, + PF_DCP_OPT_DEVICE_INITIATIVE, + PF_DCP_SUB_DEV_INITIATIVE_SUPPORT, + &value_length, + &p_value, + &block_error) == 0) + { + temp16 = htons (*p_value); + (void)pf_dcp_put_block ( + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + PF_DCP_OPT_DEVICE_INITIATIVE, + PF_DCP_SUB_DEV_INITIATIVE_SUPPORT, + true, + 0, + value_length, + &temp16); + } + + /* Insert final response length and ship it! */ + p_dcphdr->data_length = htons (dst_pos - dst_start_pos); + p_buf->len = dst_pos; + + (void)pf_eth_send_on_management_port (net, p_buf); + + pnal_buf_free (p_buf); + + return 0; +} + +/** + * @internal + * Calculate the delay used when sending responses to DCP identify request. + * + * Given a large number of MAC addresses, the output is approximately random. + * + * The max delay will be (response_delay_factor - 1) x 10 ms. + * + * The resulting delay is in the range 0 to 64 s (in steps of 10 ms). + * + * See Profinet 2.4 Services 6.3.11.3.4 + * Profinet 2.4 Protocol 4.3.1.3.5 (formula) + * + * @param mac_address In: MAC address + * @param response_delay_factor In: Response delay factor, from the request. + * Allowed range 1 to 6400. + * @return delay in microseconds + */ +uint32_t pf_dcp_calculate_response_delay ( + const pnet_ethaddr_t * mac_address, + uint16_t response_delay_factor) +{ + uint16_t random_number = 0; + uint32_t spread = 0; /* Naming from the standard */ + + if ((response_delay_factor == 0) || (response_delay_factor > 6400)) + { + return 0; + } + + random_number = mac_address->addr[4] * 0x100 + mac_address->addr[5]; + spread = random_number % response_delay_factor; + return spread * 10 * 1000; +} + +/** + * @internal + * Handle an incoming DCP identify request. + * + * The request may contain filter conditions. Only respond if ALL conditions + * match. + * + * The response is sent with a delay, as many devices might respond to the + * request. + * + * This is a callback for the frame handler. Arguments should fulfill + * pf_eth_frame_handler_t + * + * @param net InOut: The p-net stack instance + * @param frame_id In: The frame id. + * @param p_buf In: The Ethernet frame. + * @param frame_id_pos In: Position of the frame id in the buffer. + * Depends on whether VLAN tagging is used. + * @param p_arg In: Not used. + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +static int pf_dcp_identify_req ( + pnet_t * net, + uint16_t frame_id, + pnal_buf_t * p_buf, + uint16_t frame_id_pos, + void * p_arg) /* Not used */ +{ + int ret = 0; /* Assume all OK */ + uint16_t ix; + bool first = true; /* First of the blocks */ + bool match = false; /* Is it for us? */ + bool filter = false; /* Is it IdentifyFilter or IdentifyAll? */ + uint8_t * p_src; + uint16_t src_pos = 0; + pf_ethhdr_t * p_src_ethhdr; + pf_dcp_header_t * p_src_dcphdr; + uint16_t src_dcplen = 0; + pf_dcp_block_hdr_t * p_src_block_hdr; + uint16_t src_block_len; + + pnal_buf_t * p_rsp = NULL; + uint8_t * p_dst; + uint16_t dst_pos = 0; + uint16_t dst_start = 0; + pf_ethhdr_t * p_dst_ethhdr; + pf_dcp_header_t * p_dst_dcphdr; + + uint32_t response_delay; + uint16_t value_length; + uint8_t * p_value; + uint8_t block_error; + const pnet_ethaddr_t * mac_address = pf_cmina_get_device_macaddr (net); + char alias[PF_ALIAS_NAME_MAX_SIZE]; /** Terminated */ + char * p_req_alias_name = NULL; + + /* For diagnostic logging */ +#if LOG_INFO_ENABLED(PF_DCP_LOG) + bool identify_all = false; + uint16_t stationname_position = 0; + uint16_t stationname_len = 0; + uint16_t alias_position = 0; + uint16_t alias_len = 0; +#endif + + /* + * IdentifyReqBlock = DeviceRoleBlock ^ DeviceVendorBlock ^ DeviceIDBlock ^ + * DeviceOptionsBlock ^ OEMDeviceIDBlock ^ MACAddressBlock ^ IPParameterBlock + * ^ DHCPParameterBlock ^ ManufacturerSpecificParameterBlock + */ + + if (p_buf == NULL) + { + return 1; /* Means: handled */ + } + + /* Setup access to the request */ + p_src = (uint8_t *)p_buf->payload; + p_src_ethhdr = (pf_ethhdr_t *)p_src; + src_pos = frame_id_pos; + src_pos += sizeof (uint16_t); /* FrameId */ + p_src_dcphdr = (pf_dcp_header_t *)&p_src[src_pos]; + src_pos += sizeof (pf_dcp_header_t); + src_dcplen = (src_pos + ntohs (p_src_dcphdr->data_length)); + + if (src_dcplen > p_buf->len) + { + goto out1; + } + + p_rsp = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); /* Get a transmit buffer + for the response */ + if (p_rsp == NULL) + { + LOG_ERROR ( + PF_DCP_LOG, + "DCP(%d): Could not allocate memory for response to incoming DCP " + "identify request.\n", + __LINE__); + + goto out1; + } + + /* Prepare the response */ + p_dst = (uint8_t *)p_rsp->payload; + dst_pos = 0; + p_dst_ethhdr = (pf_ethhdr_t *)p_dst; + dst_pos += sizeof (pf_ethhdr_t); + /* frame ID */ + p_dst[dst_pos++] = ((PF_DCP_ID_RES_FRAME_ID & 0xff00) >> 8); + p_dst[dst_pos++] = PF_DCP_ID_RES_FRAME_ID & 0xff; + + p_dst_dcphdr = (pf_dcp_header_t *)&p_dst[dst_pos]; + dst_pos += sizeof (pf_dcp_header_t); + + /* Save position for later */ + dst_start = dst_pos; + + memcpy ( + p_dst_ethhdr->dest.addr, + p_src_ethhdr->src.addr, + sizeof (pnet_ethaddr_t)); + memcpy (p_dst_ethhdr->src.addr, mac_address->addr, sizeof (pnet_ethaddr_t)); + p_dst_ethhdr->type = htons (PNAL_ETHTYPE_PROFINET); + + /* Start with the request header and modify what is needed. */ + *p_dst_dcphdr = *p_src_dcphdr; + p_dst_dcphdr->service_type = PF_DCP_SERVICE_TYPE_SUCCESS; + p_dst_dcphdr->response_delay_factor = htons (0); + + /* The block header is expected to be 16-bit aligned */ + p_src_block_hdr = (pf_dcp_block_hdr_t *)&p_src[src_pos]; + src_pos += sizeof (*p_src_block_hdr); /* Point to the block value */ + + src_block_len = ntohs (p_src_block_hdr->block_length); + + match = true; /* So far so good */ + while ((ret == 0) && (first || (filter && match)) && + (src_dcplen >= (src_pos + src_block_len)) && + (dst_pos < PF_FRAME_BUFFER_SIZE)) + { + /* + * At the start of the loop p_src_block_hdr shall point to the source + * block header and src_pos shall point to the block value. Also, + * src_block_len shall be set to block_length of the current buffer. + */ + /* + * DCP-Identify-ReqPDU = DCP-IdentifyFilter-ReqPDU ^ + * DCP-IdentifyAll-ReqPDU + * + * DCP-IdentifyAll-ReqPDU = DCP-MC-Header, AllSelectorOption (=0xff), + * SuboptionAllSelector (=0xff), DCPBlockLength (=0x02) + * DCP-IdentifyFilter-ReqPDU = DCP-MC-Header, [NameOfStationBlock] ^ + * [AliasNameBlock], [IdentifyReqBlock] * a + * + * DCP-MC-Header = ServiceID, ServiceType, Xid, ResponseDelayFactor, + * DCPDataLength + * + * IdentifyReqBlock = DeviceRoleBlock ^ DeviceVendorBlock ^ + * DeviceIDBlock ^ DeviceOptionsBlock ^ OEMDeviceIDBlock ^ + * MACAddressBlock ^ IPParameterBlock ^ DHCPParameterBlock ^ + * ManufacturerSpecificParameterBlock + */ + if ( + pf_cmina_dcp_get_req ( + net, + p_src_block_hdr->option, + p_src_block_hdr->sub_option, + &value_length, + &p_value, + &block_error) == 0) + { + switch (p_src_block_hdr->option) + { + case PF_DCP_OPT_ALL: + switch (p_src_block_hdr->sub_option) + { + case PF_DCP_SUB_ALL: +#if LOG_INFO_ENABLED(PF_DCP_LOG) + identify_all = true; +#endif + /* ToDo: Is there a bug in the PNIO tester? It sends + * src_block_len == 0! */ + if ( + (first == true) && + ((src_block_len == 0x02) || (src_block_len == 0x00))) + { + filter = false; + } + else + { + ret = -1; + } + break; + default: + ret = -1; + break; + } + break; + case PF_DCP_OPT_IP: + if (filter == true) + { + /* + * MACAddressBlock = 0x01, 0x01, DCPBlockLength, + * MACAddressValue IPParameterBlock = 0x01, 0x02, + * DCPBlockLength, IPAddress, Subnetmask, StandardGateway + * Note: No query for FullSuite + */ + switch (p_src_block_hdr->sub_option) + { + case PF_DCP_SUB_IP_MAC: + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + break; + case PF_DCP_SUB_IP_PAR: + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + break; + default: + ret = -1; + break; + } + } + else + { + ret = -1; + } + break; + case PF_DCP_OPT_DEVICE_PROPERTIES: + /* + * DeviceVendorBlock = 0x02, 0x01, DCPBlockLength, + * DeviceVendorValue + * + * NameOfStationBlock = 0x02, 0x02, DCPBlockLength, + * NameOfStationValue + * + * DeviceIDBlock = 0x02, 0x03, DCPBlockLength, VendorIDHigh, + * VendorIDLow, DeviceIDHigh, DeviceIDLow + * + * DeviceRoleBlock = 0x02, 0x04, DCPBlockLength, DeviceRoleValue + * + * DeviceOptionsBlock = 0x02, 0x05, DCPBlockLength, (Option, + * Suboption) + * + * AliasNameBlock = 0x02, 0x06, DCPBlockLength, AliasNameValue + * + * OEMDeviceIDBlock = 0x02, 0x08, DCPBlockLength, VendorIDHigh, + * VendorIDLow, DeviceIDHigh, DeviceIDLow + */ + switch (p_src_block_hdr->sub_option) + { + case PF_DCP_SUB_DEV_PROP_VENDOR: + if (filter == true) + { + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + case PF_DCP_SUB_DEV_PROP_NAME: +#if LOG_INFO_ENABLED(PF_DCP_LOG) + stationname_position = src_pos; + stationname_len = src_block_len; +#endif + if ( + (memcmp (p_value, &p_src[src_pos], src_block_len) == 0) && + (p_value[src_block_len] == '\0')) + { + if (first == true) + { + filter = true; + } + } + else + { + match = false; + } + break; + case PF_DCP_SUB_DEV_PROP_ID: + if (filter == true) + { + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + case PF_DCP_SUB_DEV_PROP_ROLE: + if (filter == true) + { + p_src_block_hdr->block_length += 1; + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + case PF_DCP_SUB_DEV_PROP_OPTIONS: + if (filter == true) + { + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + case PF_DCP_SUB_DEV_PROP_OEM_ID: + if (filter == true) + { + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + case PF_DCP_SUB_DEV_PROP_INSTANCE: + if (filter == true) + { + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + case PF_DCP_SUB_DEV_PROP_GATEWAY: + if (filter == true) + { + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + default: + ret = -1; + break; + } + break; + case PF_DCP_OPT_DHCP: + if (filter == true) + { + if ( + (memcmp (p_value, &p_src[src_pos], value_length) != 0) || + (src_block_len != value_length)) + { + match = false; + } + } + else + { + ret = -1; + } + break; + default: + ret = -1; + break; + } + src_pos += src_block_len; + + /* Skip padding to align on uint16_t */ + while (src_pos & 1) + { + src_pos++; + } + /* Prepare for the next round */ + p_src_block_hdr = (pf_dcp_block_hdr_t *)&p_src[src_pos]; + src_block_len = ntohs (p_src_block_hdr->block_length); + src_pos += sizeof (*p_src_block_hdr); + } + else if ( + (p_src_block_hdr->option == PF_DCP_OPT_DEVICE_PROPERTIES) && + (p_src_block_hdr->sub_option == PF_DCP_SUB_DEV_PROP_ALIAS)) + { +#if LOG_INFO_ENABLED(PF_DCP_LOG) + alias_position = src_pos; + alias_len = src_block_len; +#endif + if (src_block_len < PF_ALIAS_NAME_MAX_SIZE) + { + memcpy (alias, &p_src[src_pos], src_block_len); + alias[src_block_len] = '\0'; + if (pf_lldp_is_alias_matching (net, alias)) + { + if (first == true) + { + p_req_alias_name = alias; + filter = true; + } + } + else + { + match = false; + } + } + else + { + match = false; + } + + src_pos += src_block_len; + + /* Skip padding to align on uint16_t */ + while (src_pos & 1) + { + src_pos++; + } + /* Prepare for the next round */ + p_src_block_hdr = (pf_dcp_block_hdr_t *)&p_src[src_pos]; + src_block_len = ntohs (p_src_block_hdr->block_length); + src_pos += sizeof (*p_src_block_hdr); + } + else + { + LOG_DEBUG ( + PF_DCP_LOG, + "DCP(%d): Unknown incoming DCP identify request.\n", + __LINE__); + ret = -1; /* Give up on bad request */ + } + + first = false; + } + + if ((ret == 0) && (match == true)) + { + + /* Build the response */ + for (ix = 0; ix < NELEMENTS (device_options); ix++) + { + pf_dcp_get_req ( + net, + p_dst, + &dst_pos, + PF_FRAME_BUFFER_SIZE, + device_options[ix].opt, + device_options[ix].sub, + true, + p_req_alias_name); + } + + /* Insert final response length and ship it! */ + p_dst_dcphdr->data_length = htons (dst_pos - dst_start); + p_rsp->len = dst_pos; + + net->dcp_delayed_response_waiting = true; + response_delay = pf_dcp_calculate_response_delay ( + mac_address, + ntohs (p_src_dcphdr->response_delay_factor)); +#if LOG_INFO_ENABLED(PF_DCP_LOG) + LOG_INFO ( + PF_DCP_LOG, + "DCP(%d): Responding to incoming DCP identify request. All: %d " + "StationName: %.*s Alias: %.*s Delay %" PRIu32 " us. Xid: %" PRIu32 + "\n", + __LINE__, + identify_all, + stationname_len, + &p_src[stationname_position], /* Not terminated */ + alias_len, + &p_src[alias_position], /* Not terminated */ + response_delay, + ntohl (p_src_dcphdr->xid)); +#endif + + (void)pf_scheduler_add ( + net, + response_delay, + pf_dcp_responder, + p_rsp, + &net->dcp_identresp_timeout); + + /* Note: Do not free p_rsp, it is used in pf_dcp_responder() */ + } + else + { +#if LOG_INFO_ENABLED(PF_DCP_LOG) + LOG_INFO ( + PF_DCP_LOG, + "DCP(%d): No match for incoming DCP identify request. All: %d " + "StationName: %.*s Alias: %.*s Xid: %" PRIu32 "\n", + __LINE__, + identify_all, + stationname_len, + &p_src[stationname_position], /* Not terminated */ + alias_len, + &p_src[alias_position], /* Not terminated */ + ntohl (p_src_dcphdr->xid)); +#endif + + pnal_buf_free (p_rsp); + } + +out1: + if (p_buf != NULL) + { + pnal_buf_free (p_buf); + } + + return 1; /* Means: handled */ +} + +void pf_dcp_exit (pnet_t * net) +{ + pf_eth_frame_id_map_remove (net, PF_DCP_HELLO_FRAME_ID); + pf_eth_frame_id_map_remove (net, PF_DCP_GET_SET_FRAME_ID); + pf_eth_frame_id_map_remove (net, PF_DCP_ID_REQ_FRAME_ID); +} + +void pf_dcp_init (pnet_t * net) +{ + net->dcp_global_block_qualifier = 0; + net->dcp_delayed_response_waiting = false; + net->dcp_sam = mac_nil; + pf_scheduler_init_handle (&net->dcp_sam_timeout, "dcp_sam"); + pf_scheduler_init_handle (&net->dcp_led_timeout, "dcp_led"); + pf_scheduler_init_handle (&net->dcp_identresp_timeout, "dcp_identresp"); + + LOG_DEBUG ( + PF_DCP_LOG, + "DCP(%d): Activate. Register frame handlers.\n", + __LINE__); + + /* Insert handlers for our specific frame_ids */ + pf_eth_frame_id_map_add (net, PF_DCP_HELLO_FRAME_ID, pf_dcp_hello_ind, NULL); + pf_eth_frame_id_map_add (net, PF_DCP_GET_SET_FRAME_ID, pf_dcp_get_set, NULL); + pf_eth_frame_id_map_add ( + net, + PF_DCP_ID_REQ_FRAME_ID, + pf_dcp_identify_req, + NULL); +} diff --git a/profinet_stack/p-net/src/common/pf_dcp.h b/profinet_stack/p-net/src/common/pf_dcp.h new file mode 100644 index 0000000..3081dab --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_dcp.h @@ -0,0 +1,145 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_DCP_H +#define PF_DCP_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * The option and sub-option values are used by DCP and also by CMINA. + */ +typedef enum pf_dcp_opt_values +{ + PF_DCP_OPT_RESERVED_0 = 0, + PF_DCP_OPT_IP, + PF_DCP_OPT_DEVICE_PROPERTIES, + PF_DCP_OPT_DHCP, + PF_DCP_OPT_RESERVED_4, + PF_DCP_OPT_CONTROL, + PF_DCP_OPT_DEVICE_INITIATIVE, + /* + * Reserved 0x07 .. 0x7f + * Manufacturer specific 0x80 .. 0xfe + */ + PF_DCP_OPT_ALL = 0xff +} pf_dcp_opt_values_t; + +typedef enum pf_dcp_sub_all_values +{ + PF_DCP_SUB_ALL = 0xff +} pf_dcp_sub_all_values_t; + +typedef enum pf_dcp_sub_ip_values +{ + PF_DCP_SUB_IP_MAC = 0x01, /* Read */ + PF_DCP_SUB_IP_PAR, /* Read/Write */ + PF_DCP_SUB_IP_SUITE /* Read/(optional Write) */ +} pf_dcp_sub_ip_values_t; + +typedef enum pf_dcp_sub_dev_prop +{ + PF_DCP_SUB_DEV_PROP_VENDOR = 0x01, /* Read */ + PF_DCP_SUB_DEV_PROP_NAME, /* Read/Write */ + PF_DCP_SUB_DEV_PROP_ID, /* Read */ + PF_DCP_SUB_DEV_PROP_ROLE, /* Read */ + PF_DCP_SUB_DEV_PROP_OPTIONS, /* Read */ + PF_DCP_SUB_DEV_PROP_ALIAS, /* Used as filter only */ + PF_DCP_SUB_DEV_PROP_INSTANCE, /* Read, optional */ + PF_DCP_SUB_DEV_PROP_OEM_ID, /* Read, optional */ + PF_DCP_SUB_DEV_PROP_GATEWAY /* Read, optional */ +} pf_dcp_sub_dev_prop_t; + +typedef enum pf_dcp_sub_dhcp +{ + PF_DCP_SUB_DHCP_HOSTNAME = 12, + PF_DCP_SUB_DHCP_VENDOR_SPEC = 43, + PF_DCP_SUB_DHCP_SERVER_ID = 54, + PF_DCP_SUB_DHCP_PAR_REQ_LIST = 55, + PF_DCP_SUB_DHCP_CLASS_ID = 60, + PF_DCP_SUB_DHCP_CLIENT_ID = 61, + PF_DCP_SUB_DHCP_FQDN = 81, + PF_DCP_SUB_DHCP_UUID_CLIENT_ID = 97, + PF_DCP_SUB_DHCP_CONTROL = 255 /* Defined as END in the DHCP spec */ +} pf_dcp_sub_dhcp_t; + +typedef enum pf_dcp_sub_control +{ + PF_DCP_SUB_CONTROL_START = 0x01, /* Write */ + PF_DCP_SUB_CONTROL_STOP, /* Write */ + PF_DCP_SUB_CONTROL_SIGNAL, /* Write */ + PF_DCP_SUB_CONTROL_RESPONSE, + PF_DCP_SUB_CONTROL_FACTORY_RESET, /* Optional, Write */ + PF_DCP_SUB_CONTROL_RESET_TO_FACTORY, /* Write */ +} pf_dcp_sub_control_t; + +typedef enum pf_dcp_sub_dev_initiative +{ + PF_DCP_SUB_DEV_INITIATIVE_SUPPORT = 0x01 /* Read */ +} pf_dcp_sub_dev_initiative_t; + +typedef enum pf_dcp_block_error_values +{ + PF_DCP_BLOCK_ERROR_NO_ERROR, + PF_DCP_BLOCK_ERROR_OPTION_NOT_SUPPORTED, + PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED, + PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SET, + PF_DCP_BLOCK_ERROR_RESOURCE_ERROR, + PF_DCP_BLOCK_ERROR_SET_NOT_POSSIBLE +} pf_dcp_block_error_values_t; + +/** + * Initialize the DCP component. + * + * Registers incoming frame IDs with the frame handler. + * + * @param net InOut: The p-net stack instance + */ +void pf_dcp_init (pnet_t * net); + +/** + * Stop the DCP component. + * + * Unregisters frame IDs from the frame handler. + * + * @param net InOut: The p-net stack instance + */ +void pf_dcp_exit (pnet_t * net); + +/** + * Send a DCP HELLO message. + * @param net InOut: The p-net stack instance + * @return 0 if a HELLO message was sent. + * -1 if an error occurred. + */ +int pf_dcp_hello_req (pnet_t * net); + +/************ Internal functions, made available for unit testing ************/ + +uint32_t pf_dcp_calculate_response_delay ( + const pnet_ethaddr_t * mac_address, + uint16_t response_delay_factor); + +bool pf_dcp_check_destination_address ( + const pnet_ethaddr_t * local_mac, + const pnet_ethaddr_t * destination_mac); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_DCP_H */ diff --git a/profinet_stack/p-net/src/common/pf_eth.c b/profinet_stack/p-net/src/common/pf_eth.c new file mode 100644 index 0000000..b9bc30b --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_eth.c @@ -0,0 +1,320 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Map incoming cyclic Profinet data Ethernet frames to frame handlers + * + * The frame id map is used to quickly find the function responsible for + * handling a frame with a specific frame id. + * Clients may add or remove entries on the fly, but there is no locking of the + * table. ToDo: This may be a problem in the future. Note that frames may arrive + * at any time. + */ + +#ifdef UNIT_TEST +#define pnal_eth_init mock_pnal_eth_init +#define pnal_eth_send mock_pnal_eth_send +#define pnal_get_macaddress mock_pnal_get_macaddress +#endif + +#include +#include "pf_includes.h" + +/** + * @internal + * Initialize one network interface + * + * @param net InOut: The p-net stack instance + * @param netif_name In: Interface name + * @param eth_receive_type In: Protocol to receive + * @param pnal_cfg In: Operating system dependent configuration + * @param netif InOut: Network interface instance + * @return 0 on success + * -1 on error + */ +static int pf_eth_init_netif ( + pnet_t * net, + const char * netif_name, + pnal_ethertype_t eth_receive_type, + const pnal_cfg_t * pnal_cfg, + pf_netif_t * netif) +{ + pnal_ethaddr_t pnal_mac_addr; + + snprintf (netif->name, sizeof (netif->name), "%s", netif_name); + + netif->handle = pnal_eth_init ( + netif->name, + eth_receive_type, + pnal_cfg, + pf_eth_recv, + (void *)net); + + if (netif->handle == NULL) + { + LOG_ERROR (PNET_LOG, "Failed to init \"%s\"\n", netif_name); + return -1; + } + + if (pnal_get_macaddress (netif_name, &pnal_mac_addr) != 0) + { + LOG_ERROR (PNET_LOG, "Failed read mac address on \"%s\"\n", netif_name); + return -1; + } + + LOG_DEBUG ( + PF_ETH_LOG, + "ETH(%d): Initialising interface \"%s\" %02X:%02X:%02X:%02X:%02X:%02X " + "Ethertype 0x%04X\n", + __LINE__, + netif_name, + pnal_mac_addr.addr[0], + pnal_mac_addr.addr[1], + pnal_mac_addr.addr[2], + pnal_mac_addr.addr[3], + pnal_mac_addr.addr[4], + pnal_mac_addr.addr[5], + eth_receive_type); + + memcpy ( + netif->mac_address.addr, + pnal_mac_addr.addr, + sizeof (netif->mac_address.addr)); + + netif->previous_is_link_up = false; + + return 0; +} + +int pf_eth_init (pnet_t * net, const pnet_cfg_t * p_cfg) +{ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data; + uint8_t number_of_ports = p_cfg->num_physical_ports; + const pnet_port_cfg_t * p_port_cfg; + pnal_ethertype_t main_port_receive_type = + (number_of_ports == 1) ? PNAL_ETHTYPE_ALL : PNAL_ETHTYPE_PROFINET; + + memset (net->eth_id_map, 0, sizeof (net->eth_id_map)); + + /* Init management port */ + if ( + pf_eth_init_netif ( + net, + p_cfg->if_cfg.main_netif_name, + main_port_receive_type, + &p_cfg->pnal_cfg, + &net->pf_interface.main_port) != 0) + { + return -1; + } + + /* Init physical ports */ + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + + if (number_of_ports > 1) + { + p_port_cfg = pf_port_get_config (net, port); + + if ( + pf_eth_init_netif ( + net, + p_port_cfg->netif_name, + PNAL_ETHTYPE_LLDP, + &p_cfg->pnal_cfg, + &p_port_data->netif) != 0) + { + return -1; + } + } + else + { + /* In single port configuration the managed port is also + physical port 1 */ + p_port_data->netif = net->pf_interface.main_port; + } + + port = pf_port_get_next (&port_iterator); + } + + return 0; +} + +int pf_eth_send_on_physical_port ( + pnet_t * net, + int loc_port_num, + pnal_buf_t * buf) +{ + int sent_len = 0; + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + sent_len = pnal_eth_send (p_port_data->netif.handle, buf); + if (sent_len <= 0) + { + LOG_ERROR ( + PF_ETH_LOG, + "ETH(%d): Error from pnal_eth_send_on_physical_port()\n", + __LINE__); + } + + return sent_len; +} + +int pf_eth_send_on_management_port (pnet_t * net, pnal_buf_t * buf) +{ + int sent_len = 0; + + sent_len = pnal_eth_send (net->pf_interface.main_port.handle, buf); + if (sent_len <= 0) + { + LOG_ERROR ( + PF_ETH_LOG, + "ETH(%d): Error from pnal_send_on_management_port()\n", + __LINE__); + } + + return sent_len; +} + +int pf_eth_recv (pnal_eth_handle_t * eth_handle, void * arg, pnal_buf_t * p_buf) +{ + int ret = 0; /* Means: "Not handled" */ + uint16_t eth_type_pos = 2 * sizeof (pnet_ethaddr_t); + uint16_t eth_type = 0; + uint16_t frame_id = 0; + uint16_t frame_pos = 0; + const uint16_t * p_data = NULL; + uint16_t ix = 0; + int loc_port_num = 0; + pnet_t * net = (pnet_t *)arg; + + /* Skip ALL VLAN tags */ + p_data = (uint16_t *)(&((uint8_t *)p_buf->payload)[eth_type_pos]); + eth_type = ntohs (p_data[0]); + while (eth_type == PNAL_ETHTYPE_VLAN) + { + eth_type_pos += 4; /* Sizeof VLAN tag */ + + p_data = (uint16_t *)(&((uint8_t *)p_buf->payload)[eth_type_pos]); + eth_type = ntohs (p_data[0]); + } + + frame_pos = eth_type_pos + sizeof (uint16_t); + + switch (eth_type) + { + case PNAL_ETHTYPE_PROFINET: + p_data = (uint16_t *)(&((uint8_t *)p_buf->payload)[frame_pos]); + frame_id = ntohs (p_data[0]); + + /* Find the associated frame handler */ + ix = 0; + while ((ix < NELEMENTS (net->eth_id_map)) && + ((net->eth_id_map[ix].in_use == false) || + (net->eth_id_map[ix].frame_id != frame_id))) + { + ix++; + } + if (ix < NELEMENTS (net->eth_id_map)) + { + /* Call the frame handler */ + ret = net->eth_id_map[ix].frame_handler ( + net, + frame_id, + p_buf, /* This cannot be NULL, as seen above */ + frame_pos, + net->eth_id_map[ix].p_arg); + } + break; + case PNAL_ETHTYPE_LLDP: + loc_port_num = pf_port_get_port_number (net, eth_handle); + ret = pf_lldp_recv (net, loc_port_num, p_buf, frame_pos); + break; + case PNAL_ETHTYPE_IP: + /* IP-packets (UDP) are also received via the UDP sockets. Do not count + * statistics here. */ + ret = 0; + break; + default: + /* Unknown packet. */ + + ret = 0; + break; + } + + return ret; +} + +void pf_eth_frame_id_map_add ( + pnet_t * net, + uint16_t frame_id, + pf_eth_frame_handler_t frame_handler, + void * p_arg) +{ + uint16_t ix = 0; + + while ((ix < NELEMENTS (net->eth_id_map)) && + (net->eth_id_map[ix].in_use == true)) + { + ix++; + } + + if (ix < NELEMENTS (net->eth_id_map)) + { + LOG_DEBUG ( + PF_ETH_LOG, + "ETH(%d): Framehandler is adding FrameId %#x at index %u.\n", + __LINE__, + (unsigned)frame_id, + (unsigned)ix); + net->eth_id_map[ix].frame_id = frame_id; + net->eth_id_map[ix].frame_handler = frame_handler; + net->eth_id_map[ix].p_arg = p_arg; + net->eth_id_map[ix].in_use = true; + } + else + { + LOG_ERROR (PF_ETH_LOG, "ETH(%d): No more room for FrameIds\n", __LINE__); + } +} + +void pf_eth_frame_id_map_remove (pnet_t * net, uint16_t frame_id) +{ + uint16_t ix = 0; + + while ((ix < NELEMENTS (net->eth_id_map)) && + ((net->eth_id_map[ix].in_use == false) || + (net->eth_id_map[ix].frame_id != frame_id))) + { + ix++; + } + + if (ix < NELEMENTS (net->eth_id_map)) + { + net->eth_id_map[ix].in_use = false; + LOG_DEBUG ( + PF_ETH_LOG, + "ETH(%d): Free room for FrameIds %#x at index %u\n", + __LINE__, + (unsigned)frame_id, + (unsigned)ix); + } +} diff --git a/profinet_stack/p-net/src/common/pf_eth.h b/profinet_stack/p-net/src/common/pf_eth.h new file mode 100644 index 0000000..bf1f4f4 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_eth.h @@ -0,0 +1,111 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_ETH_H +#define PF_ETH_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize ETH component and network interfaces according to configuration + * If device is configured with one network interface + * (num_physical_ports==1), the management port and physical port 1 + * refers to same network interface. In a multi port configuration, the + * management port and physical ports are different network interfaces. + * @param net InOut: The p-net stack instance + * @param p_cfg In: Configuration + * @return 0 on success + * -1 on error + */ +int pf_eth_init (pnet_t * net, const pnet_cfg_t * p_cfg); + +/** + * Send raw Ethernet frame on a specific physical port. + * + * Not for use with management port. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param buf In: Buffer with data to be sent + * @return The number of bytes sent, or -1 if an error occurred. + */ +int pf_eth_send_on_physical_port ( + pnet_t * net, + int loc_port_num, + pnal_buf_t * buf); + +/** + * Send raw Ethernet frame on management port. + * + * @param net InOut: The p-net stack instance + * @param buf In: Buffer with data to be sent + * @return The number of bytes sent, or -1 if an error occurred. + */ +int pf_eth_send_on_management_port (pnet_t * net, pnal_buf_t * buf); + +/** + * Add a frame_id entry to the frame id filter map. + * + * This function adds an entry to the frame id table. + * This table is used to map incoming frames to the right handler functions, + * based on the frame id. + * + * Used only for incoming frames with Ethtype = Profinet. + * + * @param net InOut: The p-net stack instance + * @param frame_id In: The frame ID to look for. + * @param frame_handler In: The handler function to call. + * @param p_arg In: Argument to handler function. + */ +void pf_eth_frame_id_map_add ( + pnet_t * net, + uint16_t frame_id, + pf_eth_frame_handler_t frame_handler, + void * p_arg); + +/** + * Remove an entry from the frame id filter map (if it exists). + * + * @param net InOut: The p-net stack instance + * @param frame_id In: The frame ID to remove. + */ +void pf_eth_frame_id_map_remove (pnet_t * net, uint16_t frame_id); + +/** + * Inspect and possibly handle a raw Ethernet frame + * + * Find the packet type. If it is for Profinet then send it to the right + * handler, depending on the frame_id within the packet. The frame_id is located + * right after the packet type. Take care of handling the VLAN tag. + * + * Note also that this function is a callback and will be passed as an argument + * to pnal_eth_init(). + * + * @param eth_handle InOut: Network interface the frame was received on. + * @param arg InOut: User argument, will be converted to pnet_t + * @param p_buf InOut: The Ethernet frame. Might be freed. + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +int pf_eth_recv (pnal_eth_handle_t * eth_handle, void * arg, pnal_buf_t * p_buf); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_DCPMCS_H */ diff --git a/profinet_stack/p-net/src/common/pf_file.c b/profinet_stack/p-net/src/common/pf_file.c new file mode 100644 index 0000000..a41519f --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_file.c @@ -0,0 +1,323 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Utility functions for reading and writing data from/to disk or flash. + * + * Adds the bytes "PNET" and a version indicator to the beginning of the file + * when writing. Checks the corresponding values when reading. + * + */ + +#ifdef UNIT_TEST +#define pnal_clear_file mock_pnal_clear_file +#define pnal_load_file mock_pnal_load_file +#define pnal_save_file mock_pnal_save_file +#endif + +#include "pf_includes.h" +#include "pf_block_reader.h" +#include "pf_block_writer.h" + +#include +#include +#include + +#define PF_FILE_MAGIC 0x504E4554U /* "PNET" */ + +/* Increase every time the saved contents have another format */ +#define PF_FILE_VERSION 0x00000001U + +/* The configurable constant PNET_MAX_FILENAME_SIZE should be at least + * as large as the longest filename used, including termination. + */ +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_IP)); +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_IM)); +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_DIAGNOSTICS)); +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_PDPORT_1)); +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_PDPORT_2)); +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_PDPORT_3)); +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_PDPORT_4)); + +/** + * @internal + * Join directory and filename into a full path. + * + * If no directory is given, use only the filename. + * + * @param directory In: Directory for files. Terminated string. + * NULL or empty string is interpreted as + * current directory. + * @param filename In: File name. Terminated string. + * @param fullpath Out: Resulting string. Terminated. + * @param size In: Size of outputbuffer. + * @return 0 if the operation succeeded. + * -1 if not found or an error occurred. + */ +int pf_file_join_directory_filename ( + const char * directory, + const char * filename, + char * fullpath, + size_t size) +{ + bool use_directory = true; + + if (filename == NULL || fullpath == NULL) + { + LOG_ERROR ( + PNET_LOG, + "FILE(%d): Filename and outputbuffer must be given!\n", + __LINE__); + return -1; + } + if (strlen (filename) < 1) + { + LOG_ERROR (PNET_LOG, "FILE(%d): Too short filename!\n", __LINE__); + return -1; + } + + if (directory == NULL) + { + use_directory = false; + } + else + { + if (strlen (directory) == 0) + { + use_directory = false; + } + } + + if (use_directory == true) + { + if (strlen (directory) + strlen (filename) + 2 > size) + { + LOG_ERROR ( + PNET_LOG, + "FILE(%d): Too long directory and filename! Max total length %zu\n", + __LINE__, + size); + return -1; + } + + strcpy (fullpath, directory); + if (fullpath[strlen (directory) - 1] != '/') + { + strcat (fullpath, "/"); + } + strcat (fullpath, filename); + } + else + { + if (strlen (filename) + 1 > size) + { + LOG_ERROR ( + PNET_LOG, + "FILE(%d): Too long filename! Max total length %zu\n", + __LINE__, + size); + return -1; + } + strcpy (fullpath, filename); + } + + return 0; +} + +int pf_file_load ( + const char * directory, + const char * filename, + void * p_object, + size_t size) +{ + uint8_t versioning_buffer[8] = {0}; /* Two uint32_t */ + pf_get_info_t bufferinfo; + uint32_t version = 0; + uint32_t magic = 0; + uint16_t pos = 0; + char path[PNET_MAX_FILE_FULLPATH_SIZE]; + uint32_t start_time_us = 0; + + bufferinfo.p_buf = versioning_buffer; + bufferinfo.len = sizeof (versioning_buffer); + bufferinfo.is_big_endian = true; + bufferinfo.result = PF_PARSE_OK; + + if ( + pf_file_join_directory_filename ( + directory, + filename, + path, + PNET_MAX_FILE_FULLPATH_SIZE) != 0) + { + return -1; + } + + /* Read file */ + start_time_us = os_get_current_time_us(); + if ( + pnal_load_file ( + path, + versioning_buffer, + sizeof (versioning_buffer), + p_object, + size) != 0) + { + return -1; + } + LOG_DEBUG ( + PNET_LOG, + "FILE(%d): Did read file %s Access time %" PRIu32 " ms.\n", + __LINE__, + path, + ((os_get_current_time_us() - start_time_us) / 1000)); + + magic = pf_get_uint32 (&bufferinfo, &pos); + version = pf_get_uint32 (&bufferinfo, &pos); + + /* Check file magic bytes */ + if (magic != PF_FILE_MAGIC) + { + LOG_ERROR ( + PNET_LOG, + "FILE(%d): Wrong file magic bytes in file %s\n", + __LINE__, + path); + return -1; + } + + /* Check file version */ + if (version != PF_FILE_VERSION) + { + LOG_WARNING ( + PNET_LOG, + "FILE(%d): Wrong file version identifier in file %s Expected %" PRIu32 + " but got %" PRIu32 ".\n", + __LINE__, + path, + (uint32_t)PF_FILE_VERSION, + version); + return -1; + } + + return 0; +} + +int pf_file_save ( + const char * directory, + const char * filename, + const void * p_object, + size_t size) +{ + char path[PNET_MAX_FILE_FULLPATH_SIZE]; /**< Terminated string */ + uint8_t versioning_buffer[8] = {0}; /**< Two uint32_t */ + uint16_t pos = 0; + int ret = 0; + uint32_t start_time_us = 0; + + if ( + pf_file_join_directory_filename ( + directory, + filename, + path, + PNET_MAX_FILE_FULLPATH_SIZE) != 0) + { + return -1; + } + + pf_put_uint32 ( + true, + PF_FILE_MAGIC, + sizeof (versioning_buffer), + versioning_buffer, + &pos); /* Big endian */ + pf_put_uint32 ( + true, + PF_FILE_VERSION, + sizeof (versioning_buffer), + versioning_buffer, + &pos); /* Big endian */ + + start_time_us = os_get_current_time_us(); + ret = pnal_save_file ( + path, + versioning_buffer, + sizeof (versioning_buffer), + p_object, + size); + LOG_DEBUG ( + PNET_LOG, + "FILE(%d): Did save file %s Access time %" PRIu32 " ms.\n", + __LINE__, + path, + ((os_get_current_time_us() - start_time_us) / 1000)); + + return ret; +} + +int pf_file_save_if_modified ( + const char * directory, + const char * filename, + const void * p_object, + void * p_tempobject, + size_t size) +{ + bool save = false; + int ret = 0; /* Assume no changes */ + + memset (p_tempobject, 0, size); + + if (pf_file_load (directory, filename, p_tempobject, size) == 0) + { + if (memcmp (p_tempobject, p_object, size) != 0) + { + ret = 1; + save = true; + } + } + else + { + ret = 2; + save = true; + } + + if (save == true) + { + if (pf_file_save (directory, filename, p_object, size) != 0) + { + ret = -1; + } + } + + return ret; +} + +void pf_file_clear (const char * directory, const char * filename) +{ + char path[PNET_MAX_FILE_FULLPATH_SIZE]; + + if ( + pf_file_join_directory_filename ( + directory, + filename, + path, + PNET_MAX_FILE_FULLPATH_SIZE) != 0) + { + return; + } + + return pnal_clear_file (path); +} diff --git a/profinet_stack/p-net/src/common/pf_file.h b/profinet_stack/p-net/src/common/pf_file.h new file mode 100644 index 0000000..1b90bac --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_file.h @@ -0,0 +1,120 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_FILE_H +#define PF_FILE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Filenames used by p-net stack. A filename may not be longer than + * PNET_MAX_FILENAME_SIZE (termination included). + */ +#define PF_FILENAME_IP "pnet_data_ip.bin" +#define PF_FILENAME_IM "pnet_data_im.bin" +#define PF_FILENAME_DIAGNOSTICS "pnet_data_diagnostics.bin" +#define PF_FILENAME_PDPORT_1 "pnet_data_pdport_1.bin" +#define PF_FILENAME_PDPORT_2 "pnet_data_pdport_2.bin" +#define PF_FILENAME_PDPORT_3 "pnet_data_pdport_3.bin" +#define PF_FILENAME_PDPORT_4 "pnet_data_pdport_4.bin" + +/** + * Load a binary file, and verify the file version. + * + * @param directory In: Directory for files. Terminated string. NULL + * or empty string is interpreted as current + * directory. + * @param filename In: File name. Terminated string. + * @param p_object Out: Struct to load + * @param size In: Size of struct to load + * @return 0 if the operation succeeded. + * -1 if not found or an error occurred (for example wrong version). + */ +int pf_file_load ( + const char * directory, + const char * filename, + void * p_object, + size_t size); + +/** + * Save a binary file, and include version information. + * + * @param directory In: Directory for files. Terminated string. NULL + * or empty string is interpreted as current + * directory. + * @param filename In: File name. Terminated string. + * @param p_object In: Struct to save + * @param size In: Size of struct to save + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_file_save ( + const char * directory, + const char * filename, + const void * p_object, + size_t size); + +/** + * Save a binary file if modified, and include version information. + * + * No saving is done if the content would be the same. This reduces the flash + * memory wear. + * + * @param directory In: Directory for files. Terminated string. NULL + * or empty string is interpreted as current + * directory. + * @param filename In: File name. Terminated string. + * @param p_object In: Struct to save + * @param p_tempobject Temp: Temporary buffer (of same size as object) for + * loading existing file. + * @param size In: Size of struct to save + * @return 2 First saving of file (no previous file with correct version found) + * 1 Updated file + * 0 No storing required (no changes) + * -1 if an error occurred. + */ +int pf_file_save_if_modified ( + const char * directory, + const char * filename, + const void * p_object, + void * p_tempobject, + size_t size); + +/** + * Clear a binary file. + * + * @param directory In: Directory for files. Terminated string. NULL + * or empty string is interpreted as current + * directory. + * @param filename In: File name. Terminated string. + */ +void pf_file_clear (const char * directory, const char * filename); + +/************ Internal functions, made available for unit testing ************/ + +int pf_file_join_directory_filename ( + const char * directory, + const char * filename, + char * fullpath, + size_t size); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_FILE_H */ \ No newline at end of file diff --git a/profinet_stack/p-net/src/common/pf_lldp.c b/profinet_stack/p-net/src/common/pf_lldp.c new file mode 100644 index 0000000..0037d01 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_lldp.c @@ -0,0 +1,2036 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST +#define pnal_get_system_uptime_10ms mock_pnal_get_system_uptime_10ms +#define pnal_get_interface_index mock_pnal_get_interface_index +#endif + +#define STRINGIFY(s) STRINGIFIED (s) +#define STRINGIFIED(s) #s + +/** + * @file + * @brief Implements Link Layer Discovery Protocol (LLDP), for neighbourhood + * detection. + * + * Builds and sends LLDP frames. + * Receives and parses LLDP frames. + * + * This file should not have any knowledge of Profinet slots, subslots etc. + * + * ToDo: Differentiate between device and port MAC addresses. + */ + +#include "pf_includes.h" +#include "pf_block_writer.h" +#include "pf_block_reader.h" +#include "pf_cmrpc.h" + +#include +#include + +#if PNET_MAX_PORT_DESCRIPTION_SIZE > 256 +#error "Port description can't be larger than 255 bytes plus termination" +#endif +#if PNET_MAX_PORT_DESCRIPTION_SIZE < PNET_INTERFACE_NAME_MAX_SIZE +#error "Port description should be at least as large as interface name" +#endif + +#define LLDP_TYPE_END 0 +#define LLDP_TYPE_CHASSIS_ID 1 +#define LLDP_TYPE_PORT_ID 2 +#define LLDP_TYPE_TTL 3 +#define LLDP_TYPE_PORT_DESCRIPTION 4 +#define LLDP_TYPE_MANAGEMENT_ADDRESS 8 +#define LLDP_TYPE_ORG_SPEC 127 + +#define LLDP_IEEE_SUBTYPE_MAC_PHY 1 + +#define LLDP_TYPE_MASK 0xFE00 +#define LLDP_TYPE_SHIFT 0x9 +#define LLDP_LENGTH_MASK 0x1FF +#define LLDP_MAX_TLV 512 + +#define LLDP_PEER_MAX_TTL_IN_SECS \ + 60 /* Profinet peers should have TTL = 20 seconds. Use some margin. */ + +#define LLDP_PNIO_SUBTYPE_MEAS_DELAY_VALUES_TLV_LEN 24 +#define LLDP_PNIO_SUBTYPE_PORT_STATUS_TLV_LEN 8 +#define LLDP_PNIO_SUBTYPE_CHASSIS_MAC_TLV_LEN 10 +#define LLDP_IEEE_SUBTYPE_MAC_PHY_TLV_LEN 9 + +/* Autonegotiation status */ +#define LLDP_AUTONEG_SUPPORTED BIT (0) +#define LLDP_AUTONEG_ENABLED BIT (1) + +typedef struct lldp_tlv +{ + uint8_t type; + uint16_t len; + const uint8_t * p_data; +} lldp_tlv_t; + +/** Header for Organizationally Specific TLV */ +typedef struct pf_lldp_org_header +{ + /** Organizationally unique identifier */ + uint8_t id[3]; + + /** Organizationally defined subtype */ + uint8_t subtype; +} pf_lldp_org_header_t; + +static const char org_id_pnio[] = {0x00, 0x0e, 0xcf}; +static const char org_id_ieee_8023[] = {0x00, 0x12, 0x0f}; + +typedef enum lldp_pnio_subtype_values +{ + LLDP_PNIO_SUBTYPE_RESERVED = 0, + LLDP_PNIO_SUBTYPE_MEAS_DELAY_VALUES = 1, + LLDP_PNIO_SUBTYPE_PORT_STATUS = 2, + LLDP_PNIO_SUBTYPE_PORT_ALIAS = 3, + LLDP_PNIO_SUBTYPE_MRP_RING_PORT_STATUS = 4, + LLDP_PNIO_SUBTYPE_CHASSIS_MAC = 5, + LLDP_PNIO_SUBTYPE_PTCP_STATUS = 6, + LLDP_PNIO_SUBTYPE_MAU_TYPE_EXTENSION = 7, + LLDP_PNIO_SUBTYPE_MRP_INTERCONNECTION_PORT_STATUS = 8, + /* 0x09..0xff reserved */ +} lldp_pnio_subtype_values_t; + +static const pnet_ethaddr_t lldp_dst_addr = { + {0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e} /* LLDP Multicast */ +}; + +/******************* Insert data into buffer ********************************/ + +/** + * @internal + * Insert header of a TLV field into a buffer. + * Each TLV is structured as follows: + * - Type = 7 bits + * - Length = 9 bits + * - data = 0-511 bytes + * + * This is for the type and the payload length. + * + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The buffer position. + * @param typ In: The TLV header type. + * @param len In: The TLV payload length. + */ +static inline void pf_lldp_add_tlv_header ( + uint8_t * p_buf, + uint16_t * p_pos, + uint8_t typ, + uint8_t len) +{ + uint16_t tlv_header = ((typ) << LLDP_TYPE_SHIFT) + ((len)&LLDP_LENGTH_MASK); + pf_put_uint16 (true, tlv_header, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); +} + +/** + * @internal + * Parse LLDP TLV header. + * Each TLV is structured as follows: + * - Type = 7 bits + * - Length = 9 bits + * - data = 0-511 bytes + * + * @param p_info InOut: Parser state + * @param p_pos InOut: Offset in parsed buffer + * @return parsed frame. If parsing fails, frame type is set to LLDP_TYPE_END + * and len field set to 0. + */ +lldp_tlv_t pf_lldp_get_tlv_from_packet (pf_get_info_t * p_info, uint16_t * p_pos) +{ + lldp_tlv_t tlv; + uint16_t tlv_head = pf_get_uint16 (p_info, p_pos); + + if (p_info->result == PF_PARSE_OK) + { + tlv.type = (tlv_head & LLDP_TYPE_MASK) >> LLDP_TYPE_SHIFT; + tlv.len = (tlv_head & LLDP_LENGTH_MASK); + tlv.p_data = &p_info->p_buf[*p_pos]; + } + else + { + tlv.type = LLDP_TYPE_END; + tlv.len = 0; + tlv.p_data = NULL; + } + + return tlv; +} + +/** + * Insert Ethernet header into a buffer. + * + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The buffer position. + * @param dst_addr In: Destination MAC address. + * @param src_addr In: Source MAC address. + */ +static void pf_lldp_add_ethernet_header ( + uint8_t * p_buf, + uint16_t * p_pos, + pnet_ethaddr_t dst_addr, + pnet_ethaddr_t src_addr) +{ + pf_put_mem ( + dst_addr.addr, + sizeof (dst_addr.addr), + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + + pf_put_mem ( + src_addr.addr, + sizeof (src_addr.addr), + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + + pf_put_uint16 (true, PNAL_ETHTYPE_LLDP, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); +} + +/** + * @internal + * Insert a Profinet-specific header for a TLV field into a buffer. + * + * This inserts a TLV header with type="organisation-specific", and + * the Profinet organisation identifier as the first part of the TLV payload. + * + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The buffer position. + * @param len In: The TLV payload length (for the part after the + * organisation identifier) + */ +static inline void pf_lldp_add_pnio_header ( + uint8_t * p_buf, + uint16_t * p_pos, + uint8_t len) +{ + pf_lldp_add_tlv_header (p_buf, p_pos, LLDP_TYPE_ORG_SPEC, (len) + 3); + pf_put_byte (0x00, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_byte (0x0e, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_byte (0xcf, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); +} + +/** + * @internal + * Insert a IEEE 802.3-specific header for a TLV field into a buffer. + * + * This inserts a TLV header with type="organisation-specific", and + * the IEEE 802.3 organisation identifier as the first part of the TLV payload. + * + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The buffer position. + * @param len In: The TLV payload length (for the part after the + * organisation identifier) + */ +static inline void pf_lldp_add_ieee_header ( + uint8_t * p_buf, + uint16_t * p_pos, + uint8_t len) +{ + pf_lldp_add_tlv_header (p_buf, p_pos, LLDP_TYPE_ORG_SPEC, (len) + 3); + pf_put_byte (0x00, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_byte (0x12, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_byte (0x0f, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); +} + +/** + * @internal + * Insert the mandatory chassis_id TLV into a buffer. + * + * @param p_chassis_id In: The Chassis ID. + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The position in the buffer. + */ +static void pf_lldp_add_chassis_id_tlv ( + const pf_lldp_chassis_id_t * p_chassis_id, + uint8_t * p_buf, + uint16_t * p_pos) +{ + pf_lldp_add_tlv_header ( + p_buf, + p_pos, + LLDP_TYPE_CHASSIS_ID, + 1 + p_chassis_id->len); + pf_put_byte (p_chassis_id->subtype, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_mem ( + p_chassis_id->string, + p_chassis_id->len, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); +} + +/** + * @internal + * Insert the mandatory port_id TLV into a buffer. + * @param p_port_id In: Port Id + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The position in the buffer. + */ +static void pf_lldp_add_port_id_tlv ( + const pf_lldp_port_id_t * p_port_id, + uint8_t * p_buf, + uint16_t * p_pos) +{ + pf_lldp_add_tlv_header (p_buf, p_pos, LLDP_TYPE_PORT_ID, 1 + p_port_id->len); + pf_put_byte (p_port_id->subtype, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_mem ( + p_port_id->string, + p_port_id->len, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); +} + +/** + * @internal + * Insert the mandatory time-to-live (TTL) TLV into a buffer. + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The position in the buffer. + */ +static void pf_lldp_add_ttl_tlv (uint8_t * p_buf, uint16_t * p_pos) +{ + pf_lldp_add_tlv_header (p_buf, p_pos, LLDP_TYPE_TTL, 2); + pf_put_uint16 (true, PF_LLDP_TTL, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); +} + +/** + * @internal + * Insert the optional Profinet port status TLV into a buffer. + * + * The port status TLV is mandatory for ProfiNet. + * @param rtclass_2_status In: Use 0. Not supported. + * @param rtclass_3_status In: Use 0. Not supported. + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The position in the buffer. + */ +static void pf_lldp_add_port_status ( + uint16_t rtclass_2_status, + uint16_t rtclass_3_status, + uint8_t * p_buf, + uint16_t * p_pos) +{ + pf_lldp_add_pnio_header (p_buf, p_pos, 5); + + pf_put_byte ( + LLDP_PNIO_SUBTYPE_PORT_STATUS, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + pf_put_uint16 (true, rtclass_2_status, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_uint16 (true, rtclass_3_status, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); +} + +/** + * @internal + * Insert the optional Profinet chassis MAC TLV into a buffer. + * + * The chassis MAC TLV is mandatory for ProfiNet. + * @param mac_address In: Device MAC address. + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The position in the buffer. + */ +static void pf_lldp_add_chassis_mac ( + const pnet_ethaddr_t * mac_address, + uint8_t * p_buf, + uint16_t * p_pos) +{ + pf_lldp_add_pnio_header (p_buf, p_pos, 1 + sizeof (pnet_ethaddr_t)); + + pf_put_byte ( + LLDP_PNIO_SUBTYPE_CHASSIS_MAC, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + memcpy (&p_buf[*p_pos], mac_address->addr, sizeof (pnet_ethaddr_t)); + (*p_pos) += sizeof (pnet_ethaddr_t); +} + +/** + * @internal + * Insert the optional IEEE 802.3 MAC TLV into a buffer. + * + * This is the autonegotiation capabilities and available speeds, and cable MAU + * type. + * + * The IEEE 802.3 MAC TLV is mandatory for ProfiNet on 803.2 interfaces. + * @param p_status In: Link status for this port. + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The position in the buffer. + */ +static void pf_lldp_add_ieee_mac_phy ( + const pf_lldp_link_status_t * p_status, + uint8_t * p_buf, + uint16_t * p_pos) +{ + uint8_t autoneg_status; + + autoneg_status = 0x00; + if (p_status->is_autonegotiation_supported) + { + autoneg_status |= LLDP_AUTONEG_SUPPORTED; + } + if (p_status->is_autonegotiation_enabled) + { + autoneg_status |= LLDP_AUTONEG_ENABLED; + } + + pf_lldp_add_ieee_header (p_buf, p_pos, 6); + + pf_put_byte (LLDP_IEEE_SUBTYPE_MAC_PHY, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_byte (autoneg_status, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_uint16 ( + true, + p_status->autonegotiation_advertised_capabilities, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + pf_put_uint16 ( + true, + p_status->operational_mau_type, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); +} + +/** + * Insert the optional management data TLV into a buffer. + * It is mandatory for ProfiNet. + * + * Contains the IP address as well as information about the + * management interface. + * + * @param p_man_address In: Management address + * @param p_buf InOut: The buffer. + * @param p_pos InOut: The position in the buffer. + */ +static void pf_lldp_add_management ( + const pf_lldp_management_address_t * p_man_address, + uint8_t * p_buf, + uint16_t * p_pos) +{ + pf_lldp_add_tlv_header (p_buf, p_pos, LLDP_TYPE_MANAGEMENT_ADDRESS, 12); + + /* Management address length, type and value */ + pf_put_byte (1 + p_man_address->len, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_byte (p_man_address->subtype, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); + pf_put_mem ( + p_man_address->value, + p_man_address->len, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + + /* Interface number for management interface */ + pf_put_byte ( + p_man_address->interface_number.subtype, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + pf_put_uint32 ( + true, + p_man_address->interface_number.value, + PF_FRAME_BUFFER_SIZE, + p_buf, + p_pos); + + /* Management OID. Not supported */ + pf_put_byte (0, PF_FRAME_BUFFER_SIZE, p_buf, p_pos); +} + +void pf_lldp_invalidate_peer_info (pnet_t * net, int loc_port_num) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + p_port_data->lldp.is_peer_info_received = false; + p_port_data->lldp.peer_info.chassis_id.is_valid = false; + p_port_data->lldp.peer_info.port_id.is_valid = false; + p_port_data->lldp.peer_info.port_description.is_valid = false; + p_port_data->lldp.peer_info.management_address.is_valid = false; + p_port_data->lldp.peer_info.port_delay.is_valid = false; + p_port_data->lldp.peer_info.phy_config.is_valid = false; + p_port_data->lldp.peer_info.ttl = 0; + os_mutex_unlock (net->lldp_mutex); +} + +/********************* Initialize and send **********************************/ + +/** + * @internal + * Handle LLDP peer timeout. Timeout occurs if no peer LLDP data is received + * on port within the LLPD TTL timeout interval. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * @param net InOut: The p-net stack instance + * @param arg In: Port instance, p_port_data + * @param current_time In: Not used + */ +static void pf_lldp_receive_timeout ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_port_t * p_port_data = (pf_port_t *)arg; + + pf_scheduler_reset_handle (&p_port_data->lldp.rx_timeout); + LOG_INFO ( + PF_LLDP_LOG, + "LLDP(%d): Port %d, receive timeout expired\n", + __LINE__, + p_port_data->port_num); + + pf_lldp_invalidate_peer_info (net, p_port_data->port_num); + pf_pdport_peer_indication (net, p_port_data->port_num); +} + +void pf_lldp_restart_peer_timeout ( + pnet_t * net, + int loc_port_num, + uint16_t timeout_in_secs) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + pf_scheduler_remove_if_running (net, &p_port_data->lldp.rx_timeout); + + /* + * Profinet states that the time to live shall be 20 seconds, + * See IEC61158-6-10 6.3.13.2.2 + * + * To handle situations where ttl is longer than 20s and timeout gets + * longer than scheduler can handle, only start receive timeout if + * ttl is within configured max ttl. + * Timeout will always be started if a profinet enabled switch + * with 20s LLDP peer ttl is used. + */ + if (timeout_in_secs > LLDP_PEER_MAX_TTL_IN_SECS) + { + return; + } + + if ( + pf_scheduler_add ( + net, + timeout_in_secs * 1000000, + pf_lldp_receive_timeout, + p_port_data, + &p_port_data->lldp.rx_timeout) != 0) + { + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Failed to add LLDP timeout\n", + __LINE__); + } +} + +void pf_lldp_stop_peer_timeout (pnet_t * net, int loc_port_num) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + pf_scheduler_remove_if_running (net, &p_port_data->lldp.rx_timeout); +} + +/** + * @internal + * Check if management address is of type IPv4 + * + * @param address In: Management address. + * @return true if address is a valid IPv4 address, + * false if not. + */ +static bool pf_lldp_management_address_is_ipv4 ( + const pf_lldp_management_address_t * address) +{ + return address->is_valid && address->subtype == 1 && address->len == 4; +} + +int pf_lldp_get_peer_timestamp ( + pnet_t * net, + int loc_port_num, + uint32_t * timestamp_10ms) +{ + bool is_received; + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + *timestamp_10ms = p_port_data->lldp.timestamp_for_last_peer_change; + is_received = p_port_data->lldp.is_peer_info_received; + os_mutex_unlock (net->lldp_mutex); + + return is_received ? 0 : -1; +} + +void pf_lldp_get_chassis_id (pnet_t * net, pf_lldp_chassis_id_t * p_chassis_id) +{ + const pnet_ethaddr_t * device_mac_address = + pf_cmina_get_device_macaddr (net); + char station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated */ + + if ( + net->pf_interface.name_of_device_mode.mode == + PF_LLDP_NAME_OF_DEVICE_MODE_LEGACY) + { + /* + * FIXME: Use of pf_cmina_get_station_name() is not thread-safe. + * Fix this, e.g. using a mutex. + */ + pf_cmina_get_station_name (net, station_name); + p_chassis_id->len = strlen (station_name); + + if ( + p_chassis_id->len == 0 || + p_chassis_id->len >= PNET_LLDP_CHASSIS_ID_MAX_SIZE) + { + /* Use the device MAC address */ + p_chassis_id->subtype = PF_LLDP_SUBTYPE_MAC; + p_chassis_id->len = sizeof (pnet_ethaddr_t); + memcpy ( + p_chassis_id->string, + device_mac_address->addr, + sizeof (pnet_ethaddr_t)); + } + else + { + /* Use the station name (NameOfStation/NameOfInterface) */ + p_chassis_id->subtype = PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED; + memcpy (p_chassis_id->string, station_name, p_chassis_id->len); + } + } + else /* PF_LLDP_NAME_OF_DEVICE_MODE_STANDARD */ + { + p_chassis_id->subtype = PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED; + p_chassis_id->len = pf_lldp_get_system_description ( + net, + p_chassis_id->string, + sizeof (p_chassis_id->string)); + } + p_chassis_id->string[p_chassis_id->len] = '\0'; + p_chassis_id->is_valid = true; +} + +int pf_lldp_get_peer_chassis_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_chassis_id_t * p_chassis_id) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + *p_chassis_id = p_port_data->lldp.peer_info.chassis_id; + os_mutex_unlock (net->lldp_mutex); + + return p_chassis_id->is_valid ? 0 : -1; +} + +void pf_lldp_get_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + char station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated */ + const pnet_ethaddr_t * device_mac_address = + pf_cmina_get_device_macaddr (net); + + if ( + net->pf_interface.name_of_device_mode.mode == + PF_LLDP_NAME_OF_DEVICE_MODE_LEGACY) + { + snprintf ( + p_port_id->string, + sizeof (p_port_id->string), + "%s", + p_port_data->port_name); + } + else /* PF_LLDP_NAME_OF_DEVICE_MODE_STANDARD */ + { + pf_cmina_get_station_name (net, station_name); + if (strlen (station_name) == 0) + { + /* Profinet 2.4 Protocol, table 335 "LLDP substitutions": + Use chassis MAC address if no NameOfStation is assigned */ + pf_lldp_mac_address_to_string ( + device_mac_address->addr, + station_name, + sizeof (station_name)); + } + + snprintf ( + p_port_id->string, + sizeof (p_port_id->string), + "%s.%s", + p_port_data->port_name, + station_name); + } + + p_port_id->subtype = PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED; + p_port_id->len = strlen (p_port_id->string); + p_port_id->is_valid = true; +} + +int pf_lldp_get_peer_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + *p_port_id = p_port_data->lldp.peer_info.port_id; + os_mutex_unlock (net->lldp_mutex); + + return p_port_id->is_valid ? 0 : -1; +} + +void pf_lldp_get_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_descr) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + snprintf ( + p_port_descr->string, + sizeof (p_port_descr->string), + "%s", + p_port_data->netif.name); + p_port_descr->len = strlen (p_port_descr->string); + p_port_descr->is_valid = true; +} + +int pf_lldp_get_peer_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_desc) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + *p_port_desc = p_port_data->lldp.peer_info.port_description; + os_mutex_unlock (net->lldp_mutex); + + return p_port_desc->is_valid ? 0 : -1; +} + +void pf_lldp_get_management_address ( + pnet_t * net, + pf_lldp_management_address_t * p_man_address) +{ + pnal_ipaddr_t ipaddr = pf_cmina_get_ipaddr (net); + + p_man_address->subtype = 1; /* IPv4 */ + ipaddr = CC_TO_BE32 (ipaddr); + memcpy (p_man_address->value, &ipaddr, sizeof (ipaddr)); + p_man_address->len = sizeof (ipaddr); + + p_man_address->interface_number.subtype = 2; /* ifIndex */ + p_man_address->interface_number.value = + pnal_get_interface_index (net->pf_interface.main_port.name); + + p_man_address->is_valid = true; +} + +int pf_lldp_get_peer_management_address ( + pnet_t * net, + int loc_port_num, + pf_lldp_management_address_t * p_man_address) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + *p_man_address = p_port_data->lldp.peer_info.management_address; + os_mutex_unlock (net->lldp_mutex); + + return p_man_address->is_valid ? 0 : -1; +} + +int pf_lldp_get_peer_station_name ( + pnet_t * net, + int loc_port_num, + pf_lldp_station_name_t * p_station_name) +{ + pf_lldp_chassis_id_t chassis_id; + pf_lldp_port_id_t port_id; + char * p; + bool found = false; + + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + memset (p_station_name, 0, sizeof (*p_station_name)); + + os_mutex_lock (net->lldp_mutex); + port_id = p_port_data->lldp.peer_info.port_id; + chassis_id = p_port_data->lldp.peer_info.chassis_id; + os_mutex_unlock (net->lldp_mutex); + + if (port_id.is_valid) + { + p = strchr (port_id.string, '.'); + if (p != NULL) + { + /* PF_LLDP_NAME_OF_DEVICE_MODE_STANDARD + * Example: port-001.dut + */ + snprintf ( + p_station_name->string, + sizeof (p_station_name->string), + "%s", + p + 1); + found = true; + } + } + + if (!found && chassis_id.is_valid) + { + /* PF_LLDP_NAME_OF_DEVICE_MODE_LEGACY */ + if (chassis_id.subtype != PF_LLDP_SUBTYPE_MAC) + { + snprintf ( + p_station_name->string, + sizeof (p_station_name->string), + "%s", + chassis_id.string); + } + else + { + pf_lldp_mac_address_to_string ( + (uint8_t *)chassis_id.string, + p_station_name->string, + sizeof (p_station_name->string)); + } + } + + /* Do not use peer mac address here since its status is unknown. */ + + p_station_name->len = strlen (p_station_name->string); + + return p_station_name->len ? 0 : -1; +} + +int pf_lldp_get_peer_port_name ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_name_t * p_port_name) +{ + pf_lldp_port_id_t port_id; + size_t i; + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + memset (p_port_name, 0, sizeof (*p_port_name)); + + os_mutex_lock (net->lldp_mutex); + port_id = p_port_data->lldp.peer_info.port_id; + os_mutex_unlock (net->lldp_mutex); + + if (!port_id.is_valid) + { + return -1; + } + + for (i = 0; (i < port_id.len) && (port_id.string[i] != '\0') && + (i < (sizeof (p_port_name->string) - 1)); + i++) + { + if (port_id.string[i] == '.') + { + /* PF_LLDP_NAME_OF_DEVICE_MODE_STANDARD */ + break; + } + p_port_name->string[i] = port_id.string[i]; + } + + p_port_name->len = strlen (p_port_name->string); + + return p_port_name->len ? 0 : -1; +} + +void pf_lldp_get_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_lldp_signal_delay_t * p_delays) +{ + /* Line delay measurement is not supported */ + p_delays->cable_delay_local = 0; /* Not measured */ + p_delays->rx_delay_local = 0; /* Not measured */ + p_delays->tx_delay_local = 0; /* Not measured */ + p_delays->rx_delay_remote = 0; /* Not measured */ + p_delays->tx_delay_remote = 0; /* Not measured */ + p_delays->is_valid = true; +} + +int pf_lldp_get_peer_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_lldp_signal_delay_t * p_delays) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + *p_delays = p_port_data->lldp.peer_info.port_delay; + os_mutex_unlock (net->lldp_mutex); + + return p_delays->is_valid ? 0 : -1; +} + +void pf_lldp_get_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status) +{ + pnal_eth_status_t status = + pf_pdport_get_eth_status_filtered_mau (net, loc_port_num); + + p_link_status->is_autonegotiation_supported = + status.is_autonegotiation_supported; + p_link_status->is_autonegotiation_enabled = + status.is_autonegotiation_enabled; + p_link_status->autonegotiation_advertised_capabilities = + status.autonegotiation_advertised_capabilities; + p_link_status->operational_mau_type = status.operational_mau_type; + p_link_status->is_valid = true; +} + +int pf_lldp_get_peer_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status) +{ + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->lldp_mutex); + *p_link_status = p_port_data->lldp.peer_info.phy_config; + os_mutex_unlock (net->lldp_mutex); + + return p_link_status->is_valid ? 0 : -1; +} + +size_t pf_lldp_get_system_description ( + const pnet_t * net, + char * system_description, + size_t system_description_max_len) +{ + return snprintf ( + system_description, + system_description_max_len, + /* clang-format off */ + "%-" STRINGIFY (PNET_PRODUCT_NAME_MAX_LEN) "s " + "%-" STRINGIFY (PNET_ORDER_ID_MAX_LEN) "s " + "%-" STRINGIFY (PNET_SERIAL_NUMBER_MAX_LEN) "s " + "%5u " + "%c%3u%3u%3u", + /* clang-format on */ + net->fspm_cfg.product_name, + net->fspm_cfg.im_0_data.im_order_id, + net->fspm_cfg.im_0_data.im_serial_number, + net->fspm_cfg.im_0_data.im_hardware_revision, + net->fspm_cfg.im_0_data.im_sw_revision_prefix, + net->fspm_cfg.im_0_data.im_sw_revision_functional_enhancement, + net->fspm_cfg.im_0_data.im_sw_revision_bug_fix, + net->fspm_cfg.im_0_data.im_sw_revision_internal_change); +} + +void pf_lldp_mac_address_to_string ( + const uint8_t * mac, + char * mac_str, + size_t mac_str_max_len) +{ + snprintf ( + mac_str, + mac_str_max_len, + "%02X-%02X-%02X-%02X-%02X-%02X", + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5]); +} + +/** + * Construct Ethernet frame containing LLDP PDU as payload + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param buf Out: Ethernet frame buffer of size + * PF_FRAME_BUFFER_SIZE bytes. + * + * @return Size of constructed frame, in bytes. + */ +size_t pf_lldp_construct_frame (pnet_t * net, int loc_port_num, uint8_t buf[]) +{ + uint16_t pos; + const pnet_ethaddr_t * device_mac_address = + pf_cmina_get_device_macaddr (net); + pf_lldp_link_status_t link_status; + pf_lldp_chassis_id_t chassis_id; + pf_lldp_port_id_t port_id; + pf_lldp_management_address_t man_address; + const pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + +#if LOG_DEBUG_ENABLED(PF_LLDP_LOG) + pnal_ipaddr_t ipaddr = pf_cmina_get_ipaddr (net); + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + const char * chassis_id_description = ""; +#endif + + pf_lldp_get_chassis_id (net, &chassis_id); + pf_lldp_get_port_id (net, loc_port_num, &port_id); + pf_lldp_get_link_status (net, loc_port_num, &link_status); + pf_lldp_get_management_address (net, &man_address); +#if LOG_DEBUG_ENABLED(PF_LLDP_LOG) + pf_cmina_ip_to_string (ipaddr, ip_string); + if (chassis_id.subtype == PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED) + { + chassis_id_description = chassis_id.string; + } + LOG_DEBUG ( + PF_LLDP_LOG, + "LLDP(%d): Send port %u MAC: " + "%02X:%02X:%02X:%02X:%02X:%02X " + "IP: %s Chassis ID: \"%s\" Port ID: \"%s\"\n", + __LINE__, + loc_port_num, + p_port_data->netif.mac_address.addr[0], + p_port_data->netif.mac_address.addr[1], + p_port_data->netif.mac_address.addr[2], + p_port_data->netif.mac_address.addr[3], + p_port_data->netif.mac_address.addr[4], + p_port_data->netif.mac_address.addr[5], + ip_string, + chassis_id_description, + port_id.string); +#endif + + pos = 0; + + pf_lldp_add_ethernet_header ( + buf, + &pos, + lldp_dst_addr, + p_port_data->netif.mac_address); + + /* Add mandatory parts */ + pf_lldp_add_chassis_id_tlv (&chassis_id, buf, &pos); + pf_lldp_add_port_id_tlv (&port_id, buf, &pos); + pf_lldp_add_ttl_tlv (buf, &pos); + + /* Add optional parts */ + pf_lldp_add_port_status (0, 0, buf, &pos); + pf_lldp_add_chassis_mac (device_mac_address, buf, &pos); + pf_lldp_add_ieee_mac_phy (&link_status, buf, &pos); + pf_lldp_add_management (&man_address, buf, &pos); + + /* Add end of LLDP-PDU marker */ + pf_lldp_add_tlv_header (buf, &pos, LLDP_TYPE_END, 0); + + return pos; +} + +/** + * Build and send a LLDP message on a specific port. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_lldp_send (pnet_t * net, int loc_port_num) +{ + /* FIXME: Buffer size should include Ethernet header (14 bytes) */ + pnal_buf_t * p_buffer = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + + if (p_buffer == NULL) + { + return; + } + + if (p_buffer->payload != NULL) + { + p_buffer->len = + pf_lldp_construct_frame (net, loc_port_num, p_buffer->payload); + (void)pf_eth_send_on_physical_port (net, loc_port_num, p_buffer); + } + + pnal_buf_free (p_buffer); +} + +/** + * @internal + * Trigger sending a LLDP frame. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * Re-schedules itself after 5 s. + * + * @param net InOut: The p-net stack instance + * @param arg In: Reference to port_data + * @param current_time In: Not used. + */ +static void pf_lldp_trigger_sending ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_port_t * p_port_data = (pf_port_t *)arg; + + pf_lldp_send (net, p_port_data->port_num); + + if ( + pf_scheduler_add ( + net, + PF_LLDP_SEND_INTERVAL * 1000, + pf_lldp_trigger_sending, + p_port_data, + &p_port_data->lldp.tx_timeout) != 0) + { + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Failed to reschedule LLDP sending\n", + __LINE__); + } +} + +/** + * @internal + * Restart LLDP transmission timer and optionally send LLDP frame. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param send In: Send LLDP message + */ +static void pf_lldp_tx_restart (pnet_t * net, int loc_port_num, bool send) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + if ( + pf_scheduler_restart ( + net, + PF_LLDP_SEND_INTERVAL * 1000, + pf_lldp_trigger_sending, + p_port_data, + &p_port_data->lldp.tx_timeout) != 0) + { + LOG_ERROR ( + PF_ETH_LOG, + "LLDP(%d): Failed to schedule LLDP sending for port %d\n", + __LINE__, + loc_port_num); + } + + if (send) + { + pf_lldp_send (net, loc_port_num); + } +} + +void pf_lldp_init (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + + memset (&p_port_data->lldp, 0, sizeof (p_port_data->lldp)); + pf_scheduler_init_handle (&p_port_data->lldp.rx_timeout, "lldp_rx"); + pf_scheduler_init_handle (&p_port_data->lldp.tx_timeout, "lldp_tx"); + + port = pf_port_get_next (&port_iterator); + } + + net->lldp_mutex = os_mutex_create(); + CC_ASSERT (net->lldp_mutex != NULL); +} + +void pf_lldp_send_enable (pnet_t * net, int loc_port_num) +{ + LOG_DEBUG ( + PF_LLDP_LOG, + "LLDP(%d): Enabling LLDP transmission for port %d\n", + __LINE__, + loc_port_num); + pf_lldp_tx_restart (net, loc_port_num, true); +} + +void pf_lldp_send_disable (pnet_t * net, int loc_port_num) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + LOG_DEBUG ( + PF_LLDP_LOG, + "LLDP(%d): Disabling LLDP transmission for port %d\n", + __LINE__, + loc_port_num); + pf_scheduler_remove_if_running (net, &p_port_data->lldp.tx_timeout); +} + +/** + * Get organizationally specific header from received packet + * + * Layout of organizationally specific header: + * 3 Organizationally unique identifier + * 1 Organizationally defined subtype + * + * @param parse_info InOut: Parser state. + * @param offset InOut: Offset in parsed buffer. + * @param header Out: Parsed organizationally specific header. + */ +void pf_lldp_get_org_header_from_packet ( + pf_get_info_t * parse_info, + uint16_t * offset, + pf_lldp_org_header_t * header) +{ + pf_get_mem (parse_info, offset, sizeof (header->id), header->id); + header->subtype = pf_get_byte (parse_info, offset); +} + +/** + * Get Chassis ID from received packet + * + * Layout of Chassis ID TLV: + * 2 TLV header (already processed) + * 1 Chassis ID subtype + * 1-255 Chassis ID + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param chassis_id Out: Parsed Chassis ID. + */ +static void pf_lldp_get_chassis_id_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + pf_lldp_chassis_id_t * chassis_id) +{ + size_t max_len = sizeof (chassis_id->string) - 1; + + if (tlv_len == 0) + { + LOG_WARNING ( + PF_LLDP_LOG, + "LLDP(%d): Ignoring Chassis Id, tlv len=%d\n", + __LINE__, + (int)tlv_len); + + return; + } + + chassis_id->len = tlv_len - 1; + if (chassis_id->len > max_len) + { + *offset += tlv_len; + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Chassis Id too long (%d)\n", + __LINE__, + (int)chassis_id->len); + /* Should not happen. Reconfigure PNET_LLDP_CHASSIS_ID_MAX_LEN + * if it does*/ + + return; + } + + chassis_id->subtype = pf_get_byte (parse_info, offset); + pf_get_mem (parse_info, offset, chassis_id->len, chassis_id->string); + chassis_id->string[chassis_id->len] = '\0'; + chassis_id->is_valid = true; +} + +/** + * Get Port ID from received packet + * + * Layout of Port ID TLV: + * 2 TLV header (already processed) + * 1 Port ID subtype + * 1-255 Port ID + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param port_id Out: Parsed Port ID. + */ +static void pf_lldp_get_port_id_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + pf_lldp_port_id_t * port_id) +{ + size_t max_len = sizeof (port_id->string) - 1; + + if (tlv_len == 0) + { + LOG_WARNING ( + PF_LLDP_LOG, + "LLDP(%d): Ignoring Port Id, tlv len=%d\n", + __LINE__, + (int)tlv_len); + + return; + } + + port_id->len = tlv_len - 1; + if (port_id->len > max_len) + { + *offset += tlv_len; + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Port Id too long (%d)\n", + __LINE__, + (int)port_id->len); + /* Should not happen. Reconfigure PNET_LLDP_PORT_ID_MAX_LEN + * if it does*/ + + return; + } + + port_id->subtype = pf_get_byte (parse_info, offset); + pf_get_mem (parse_info, offset, port_id->len, port_id->string); + port_id->string[port_id->len] = '\0'; + port_id->is_valid = true; +} + +/** + * Get TTL from received packet + * + * Layout of Time To Live TLV: + * 2 TLV header (already processed) + * 2 Time to live (TTL) + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param ttl Out: Parsed TTL. + */ +static void pf_lldp_get_ttl_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + uint16_t * ttl) +{ + if (tlv_len != 2) + { + parse_info->result = PF_PARSE_ERROR; + LOG_WARNING ( + PF_LLDP_LOG, + "LLDP(%d): Ignoring TTL, unexpected tlv len %d(2)\n", + __LINE__, + (int)tlv_len); + + return; + } + + *ttl = pf_get_uint16 (parse_info, offset); +} + +/** + * Get port description from received packet + * + * Layout of Port description TLV: + * 2 TLV header (already processed) + * 0-255 Port description + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param port_description Out: Parsed port description. + */ +static void pf_lldp_get_port_description_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + pf_lldp_port_description_t * port_description) +{ + size_t max_len = sizeof (port_description->string) - 1; + + if (tlv_len > max_len) + { + *offset += tlv_len; + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Port Description too long (%d)\n", + __LINE__, + (int)tlv_len); + + return; + } + + port_description->len = tlv_len; + pf_get_mem (parse_info, offset, tlv_len, port_description->string); + port_description->string[tlv_len] = '\0'; + + port_description->is_valid = true; +} + +/** + * Get management address from received packet + * + * Layout of Management address TLV: + * 2 TLV header (already processed) + * 1 Management address string length + * 1 Management address subtype + * 1-31 Management address + * 1 Interface numbering subtype + * 4 Interface number + * 1 OID string length + * 0-128 Object identifier + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param management_address Out: Parsed management address. + */ +static void pf_lldp_get_management_address_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + pf_lldp_management_address_t * management_address) +{ + size_t oid_len; + + memset (management_address, 0, sizeof (*management_address)); + + management_address->len = pf_get_byte (parse_info, offset) - 1; + if (management_address->len < 1 || management_address->len > 31) + { + *offset += tlv_len - 1; + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Invalid management address length (%d)\n", + __LINE__, + (int)management_address->len); + + return; + } + + management_address->subtype = pf_get_byte (parse_info, offset); + pf_get_mem ( + parse_info, + offset, + management_address->len, + management_address->value); + + management_address->interface_number.subtype = + pf_get_byte (parse_info, offset); + management_address->interface_number.value = + pf_get_uint32 (parse_info, offset); + + /* We don't save the Object ID */ + oid_len = pf_get_byte (parse_info, offset); + *offset += oid_len; + + management_address->is_valid = true; +} + +/** + * Get signal delay values from received packet + * + * Layout of LLDP_PNIO_DELAY TLV: + * 2 TLV header (already processed) + * 3 LLDP_PNIO_Header (already processed) + * 1 LLDP_PNIO_SubType (already processed) + * 4 PTCP_PortRxDelayLocal + * 4 PTCP_PortRxDelayRemote + * 4 PTCP_PortTxDelayLocal + * 4 PTCP_PortTxDelayRemote + * 4 CableDelayLocal + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param delay Out: Parsed delay values. + */ +static void pf_lldp_get_signal_delay_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + pf_lldp_signal_delay_t * delay) +{ + if (tlv_len != LLDP_PNIO_SUBTYPE_MEAS_DELAY_VALUES_TLV_LEN) + { + parse_info->result = PF_PARSE_ERROR; + + return; + } + + delay->rx_delay_local = pf_get_uint32 (parse_info, offset); + delay->rx_delay_remote = pf_get_uint32 (parse_info, offset); + delay->tx_delay_local = pf_get_uint32 (parse_info, offset); + delay->tx_delay_remote = pf_get_uint32 (parse_info, offset); + delay->cable_delay_local = pf_get_uint32 (parse_info, offset); + delay->is_valid = true; +} + +/** + * Get port status from received packet + * + * Layout of LLDP_PNIO_PORTSTATUS TLV: + * 2 TLV header (already processed) + * 3 LLDP_PNIO_Header (already processed) + * 1 LLDP_PNIO_SubType (already processed) + * 2 RTClass2_PortStatus + * 2 RTClass3_PortStatus + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param status Out: Parsed port status. + */ +static void pf_lldp_get_port_status_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + uint8_t status[4]) +{ + if (tlv_len != LLDP_PNIO_SUBTYPE_PORT_STATUS_TLV_LEN) + { + parse_info->result = PF_PARSE_ERROR; + + return; + } + + /* FIXME: Should interpret as two 16-bit integers */ + pf_get_mem (parse_info, offset, 4, status); +} + +/** + * Get Chassis MAC address from received packet + * + * Layout of LLDP_PNIO_CHASSIS_MAC TLV: + * 2 TLV header (already processed) + * 3 LLDP_PNIO_Header (already processed) + * 1 LLDP_PNIO_SubType (already processed) + * 6 Chassis MAC address + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param chassis_mac Out: Parsed Chassis MAC address. + */ +static void pf_lldp_get_chassis_mac_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + pnet_ethaddr_t * chassis_mac) +{ + if (tlv_len != LLDP_PNIO_SUBTYPE_CHASSIS_MAC_TLV_LEN) + { + parse_info->result = PF_PARSE_ERROR; + + return; + } + + pf_get_mem ( + parse_info, + offset, + sizeof (chassis_mac->addr), + chassis_mac->addr); +} + +/** + * Get link status from received packet + * + * Layout of MAC/PHY Configuration/Status TLV: + * 2 TLV header (already processed) + * 4 802.3 header (already processed) + * 1 Auto-negotiation support/status: + * Bit 0: Auto-negotiation support + * Bit 1: Auto-negotiation status + * 2 PMD auto-negotiation advertised capability + * 2 Operational MAU typ + * + * @param parse_info InOut: Parsing information. + * @param offset InOut: Byte offset in buffer. + * @param tlv_len In: Length of TLV in bytes. + * @param link_status Out: Parsed link status. + */ +static void pf_lldp_get_link_status_from_packet ( + pf_get_info_t * parse_info, + uint16_t * const offset, + const size_t tlv_len, + pf_lldp_link_status_t * link_status) +{ + uint8_t aneg_support_status; + + memset (link_status, 0, sizeof (*link_status)); + + if (tlv_len != LLDP_IEEE_SUBTYPE_MAC_PHY_TLV_LEN) + { + parse_info->result = PF_PARSE_ERROR; + + return; + } + + aneg_support_status = pf_get_byte (parse_info, offset); + link_status->is_autonegotiation_supported = aneg_support_status & + LLDP_AUTONEG_SUPPORTED; + link_status->is_autonegotiation_enabled = aneg_support_status & + LLDP_AUTONEG_ENABLED; + + link_status->autonegotiation_advertised_capabilities = + pf_get_uint16 (parse_info, offset); + + link_status->operational_mau_type = pf_get_uint16 (parse_info, offset); + + link_status->is_valid = true; +} + +/** + * @internal + * Parse LLDP frame buffer + * + * @param buf In: LLDP data. + * @param len In: Length of LLDP data. + * @param lldp_peer_info Out: Record holding parsed LLDP information. + * Nulled initially (also on failure). + * @return 0 if parsing is successful, -1 on error. + */ +int pf_lldp_parse_packet ( + const uint8_t buf[], + uint16_t len, + pf_lldp_peer_info_t * lldp_peer_info) +{ + lldp_tlv_t tlv; + pf_lldp_org_header_t org; + pf_get_info_t parse_info; + pf_lldp_management_address_t management_address; + uint16_t offset = 0; + + memset (lldp_peer_info, 0, sizeof (*lldp_peer_info)); + + parse_info.result = PF_PARSE_OK; + parse_info.is_big_endian = true; + parse_info.len = len; + parse_info.p_buf = buf; + + tlv = pf_lldp_get_tlv_from_packet (&parse_info, &offset); + + while (tlv.type != LLDP_TYPE_END) + { + switch (tlv.type) + { + case LLDP_TYPE_CHASSIS_ID: + pf_lldp_get_chassis_id_from_packet ( + &parse_info, + &offset, + tlv.len, + &lldp_peer_info->chassis_id); + break; + case LLDP_TYPE_PORT_ID: + pf_lldp_get_port_id_from_packet ( + &parse_info, + &offset, + tlv.len, + &lldp_peer_info->port_id); + break; + case LLDP_TYPE_TTL: + pf_lldp_get_ttl_from_packet ( + &parse_info, + &offset, + tlv.len, + &lldp_peer_info->ttl); + break; + case LLDP_TYPE_PORT_DESCRIPTION: + pf_lldp_get_port_description_from_packet ( + &parse_info, + &offset, + tlv.len, + &lldp_peer_info->port_description); + break; + case LLDP_TYPE_MANAGEMENT_ADDRESS: + pf_lldp_get_management_address_from_packet ( + &parse_info, + &offset, + tlv.len, + &management_address); + if (pf_lldp_management_address_is_ipv4 (&management_address)) + { + lldp_peer_info->management_address = management_address; + } + break; + case LLDP_TYPE_ORG_SPEC: + { + pf_lldp_get_org_header_from_packet (&parse_info, &offset, &org); + if (memcmp (org_id_pnio, org.id, sizeof (org.id)) == 0) + { + switch (org.subtype) + { + case LLDP_PNIO_SUBTYPE_MEAS_DELAY_VALUES: + pf_lldp_get_signal_delay_from_packet ( + &parse_info, + &offset, + tlv.len, + &lldp_peer_info->port_delay); + break; + case LLDP_PNIO_SUBTYPE_PORT_STATUS: + pf_lldp_get_port_status_from_packet ( + &parse_info, + &offset, + tlv.len, + lldp_peer_info->port_status); + break; + case LLDP_PNIO_SUBTYPE_CHASSIS_MAC: + pf_lldp_get_chassis_mac_from_packet ( + &parse_info, + &offset, + tlv.len, + &lldp_peer_info->mac_address); + break; + default: + offset += (tlv.len - 4); + break; + } + } + else if (memcmp (org_id_ieee_8023, org.id, sizeof (org.id)) == 0) + { + switch (org.subtype) + { + case LLDP_IEEE_SUBTYPE_MAC_PHY: + pf_lldp_get_link_status_from_packet ( + &parse_info, + &offset, + tlv.len, + &lldp_peer_info->phy_config); + break; + default: + offset += (tlv.len - 4); + break; + } + } + else + { + offset += (tlv.len - 4); + } + } + break; + default: + offset += tlv.len; + break; + } + tlv = pf_lldp_get_tlv_from_packet (&parse_info, &offset); + } + + if (parse_info.result != PF_PARSE_OK) + { + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Received LLDP frame, but failed to parse it.\n", + __LINE__); + + return -1; + } + + return 0; +} + +/** + * @internal + * Generate an alias name (for me) from the peer port_id and peer chassis_id + * + * See PN-Topology 6.4.2 and PN-Protocol 4.3.1.4.18 + * + * @param port_id In: Peer port ID. Terminated string. + * @param chassis_id In: Peer chassis ID. Terminated string. + * @param alias Out: Alias name. Terminated string. + * @param len In: Size of alias name output buffer + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_lldp_generate_alias_name ( + const char * port_id, + const char * chassis_id, + char * alias, + uint16_t len) +{ + int res = 0; + + if (port_id == NULL || chassis_id == NULL || alias == NULL) + { + return -1; + } + + if (strchr (port_id, '.') != NULL) + { + /* Assume PN v2.3+ device. PortId include NameOfStation. + * and the PortId can can used as alias. + */ + if (strlen (port_id) >= len) + { + return -1; + } + strncpy (alias, port_id, len); + } + else + { + /* Assume PN v2.2 device. ChassisId is same as NameOfStation + * Alias includes PortId and ChassisId + */ + if ((strlen (port_id) + strlen (chassis_id) + 1) >= len) + { + return -1; + } + + res = snprintf (alias, len, "%s.%s", port_id, chassis_id); + if ((res < 0) || (res >= (int)len)) + { + return -1; + } + } + + return 0; +} + +bool pf_lldp_is_alias_matching (pnet_t * net, const char * alias) +{ + char port_alias[PF_ALIAS_NAME_MAX_SIZE]; /** Terminated */ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + if ( + p_port_data->lldp.is_peer_info_received && + (pf_lldp_generate_alias_name ( + p_port_data->lldp.peer_info.port_id.string, + p_port_data->lldp.peer_info.chassis_id.string, + port_alias, + sizeof (port_alias)) == 0)) + { + if (strcmp (alias, port_alias) == 0) + { + return true; + } + } + + port = pf_port_get_next (&port_iterator); + } + + return false; +} + +/** + * @internal + * Store peer information + * + * Information is stored atomically (by means of the LLDP mutex). + * This ensures that other threads (e.g. SNMP) will read consistent data. + * + * A timestamp is also generated and stored. + * + * @param net InOut: p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param new_info In: Peer data to be stored + */ +void pf_lldp_store_peer_info ( + pnet_t * net, + int loc_port_num, + const pf_lldp_peer_info_t * new_info) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + uint32_t timestamp = pnal_get_system_uptime_10ms(); + + os_mutex_lock (net->lldp_mutex); + + p_port_data->lldp.is_peer_info_received = true; + p_port_data->lldp.timestamp_for_last_peer_change = timestamp; + p_port_data->lldp.peer_info = *new_info, + + os_mutex_unlock (net->lldp_mutex); +} + +/** + * Apply updated peer information + * + * Trigger a diagnosis if the peer information not is as expected. + * Resets the peer watchdog timer. + * + * @param net InOut: p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param lldp_peer_info In: Peer data to be applied + */ +void pf_lldp_update_peer ( + pnet_t * net, + int loc_port_num, + const pf_lldp_peer_info_t * lldp_peer_info) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + pf_lldp_restart_peer_timeout (net, loc_port_num, lldp_peer_info->ttl); + + if ( + memcmp ( + lldp_peer_info, + &p_port_data->lldp.peer_info, + sizeof (pf_lldp_peer_info_t)) == 0) + { + /* No changes */ + return; + } + + if (p_port_data->lldp.is_peer_info_received) + { + if ( + p_port_data->lldp.peer_info.chassis_id.subtype == + PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED && + p_port_data->lldp.peer_info.port_id.subtype == + PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED) + { + LOG_INFO ( + PF_LLDP_LOG, + "LLDP(%d): Peer info changed on port %d. Old peer info - MAC: " + "%02X:%02X:%02X:%02X:%02X:%02X " + "Chassis ID: %s Port ID: %s\n", + __LINE__, + loc_port_num, + p_port_data->lldp.peer_info.mac_address.addr[0], + p_port_data->lldp.peer_info.mac_address.addr[1], + p_port_data->lldp.peer_info.mac_address.addr[2], + p_port_data->lldp.peer_info.mac_address.addr[3], + p_port_data->lldp.peer_info.mac_address.addr[4], + p_port_data->lldp.peer_info.mac_address.addr[5], + p_port_data->lldp.peer_info.chassis_id.string, + p_port_data->lldp.peer_info.port_id.string); + } + else + { + LOG_INFO ( + PF_LLDP_LOG, + "LLDP(%d): Peer info changed on port %d.\n", + __LINE__, + loc_port_num); + } + } + + if ( + lldp_peer_info->chassis_id.subtype == PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED && + lldp_peer_info->port_id.subtype == PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED) + { + LOG_INFO ( + PF_LLDP_LOG, + "LLDP(%d): New peer info on port %d - MAC: " + "%02X:%02X:%02X:%02X:%02X:%02X " + "Chassis ID: %s Port ID: %s\n", + __LINE__, + loc_port_num, + lldp_peer_info->mac_address.addr[0], + lldp_peer_info->mac_address.addr[1], + lldp_peer_info->mac_address.addr[2], + lldp_peer_info->mac_address.addr[3], + lldp_peer_info->mac_address.addr[4], + lldp_peer_info->mac_address.addr[5], + lldp_peer_info->chassis_id.string, + lldp_peer_info->port_id.string); + } + + pf_lldp_store_peer_info (net, loc_port_num, lldp_peer_info); + pf_pdport_peer_indication (net, loc_port_num); +} + +int pf_lldp_recv ( + pnet_t * net, + int loc_port_num, + pnal_buf_t * p_frame_buf, + uint16_t offset) +{ + uint8_t * buf = p_frame_buf->payload + offset; + uint16_t buf_len = p_frame_buf->len - offset; + pf_lldp_peer_info_t peer_data; + int err = 0; + + err = pf_lldp_parse_packet (buf, buf_len, &peer_data); + + if (!err) + { + if ( + peer_data.chassis_id.subtype == PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED && + peer_data.port_id.subtype == PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED) + { + LOG_DEBUG ( + PF_LLDP_LOG, + "LLDP(%d): Receive port %u MAC: " + "%02X:%02X:%02X:%02X:%02X:%02X " + "Len: %d Chassis ID: %s Port ID: %s\n", + __LINE__, + loc_port_num, + peer_data.mac_address.addr[0], + peer_data.mac_address.addr[1], + peer_data.mac_address.addr[2], + peer_data.mac_address.addr[3], + peer_data.mac_address.addr[4], + peer_data.mac_address.addr[5], + p_frame_buf->len, + peer_data.chassis_id.string, + peer_data.port_id.string); + } + else if (peer_data.port_id.subtype == PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED) + { + LOG_DEBUG ( + PF_LLDP_LOG, + "LLDP(%d): LLDP frame received on port %u Len: %d Port " + "ID: %s\n", + __LINE__, + loc_port_num, + p_frame_buf->len, + peer_data.port_id.string); + } + else + { + LOG_DEBUG ( + PF_LLDP_LOG, + "LLDP(%d): Non-profinet LLDP frame received on port %u " + "Len: " + "%d\n", + __LINE__, + loc_port_num, + p_frame_buf->len); + } + + if (pf_port_is_valid (net, loc_port_num)) + { + pf_lldp_update_peer (net, loc_port_num, &peer_data); + } + else + { + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Unexpected local port %u, frame discarded.\n", + __LINE__, + loc_port_num); + } + } + + pnal_buf_free (p_frame_buf); + + return 1; /* Means: handled */ +} diff --git a/profinet_stack/p-net/src/common/pf_lldp.h b/profinet_stack/p-net/src/common/pf_lldp.h new file mode 100644 index 0000000..e390b2f --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_lldp.h @@ -0,0 +1,703 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Periodical transmission outgoing LLDP frames + * Reception of incoming LLDP frames + */ + +#ifndef PF_LLDP_H +#define PF_LLDP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pnal.h" + +#define PF_LLDP_SUBTYPE_MAC 4 /** Valid for ChassisID */ +#define PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED 7 + +/** + * Link status + * + * See IEEE 802.1AB-2005 (LLDPv1) Annex G.2 "MAC/PHY Configuration/Status TLV". + * See also pnal_eth_status_t + */ +typedef struct pf_lldp_link_status +{ + bool is_autonegotiation_supported; + bool is_autonegotiation_enabled; + uint16_t autonegotiation_advertised_capabilities; + uint16_t operational_mau_type; + bool is_valid; +} pf_lldp_link_status_t; + +/** + * Port description + * + * "The port description field shall contain an alphanumeric string + * that indicates the port's description. If RFC 2863 + * is implemented, the ifDescr object should be used for this field." + * - IEEE 802.1AB (LLDP) ch. 9.5.5.2 "port description". + * + * Note: According to the SNMP specification, the string could be up + * to 255 characters (excluding termination). The p-net stack limits it to + * PNET_MAX_PORT_DESCRIPTION_SIZE (including termination). + */ +typedef struct pf_lldp_port_description +{ + char string[PNET_MAX_PORT_DESCRIPTION_SIZE]; /* Terminated string */ + bool is_valid; + size_t len; +} pf_lldp_port_description_t; + +/** + * Port list + * + * "Each octet within this value specifies a set of eight ports, + * with the first octet specifying ports 1 through 8, the second + * octet specifying ports 9 through 16, etc. Within each octet, + * the most significant bit represents the lowest numbered port, + * and the least significant bit represents the highest numbered + * port. Thus, each port of the system is represented by a + * single bit within the value of this object. If that bit has + * a value of '1' then that port is included in the set of ports; + * the port is not included if its bit has a value of '0'." + * - IEEE 802.1AB (LLDP) ch. 12.2 "LLDP MIB module" (lldpPortList). + * + * Se also section about lldpConfigManAddrTable in PN-Topology. + */ +typedef struct pf_lldp_port_list_t +{ + uint8_t ports[2]; +} pf_lldp_port_list_t; + +/** + * Port iterator + * + * This iterator may be used for iterating over all physical ports + * on the local interface. The management port is not included. + */ +typedef struct pf_port_iterator +{ + int next_port; + int number_of_ports; +} pf_port_iterator_t; + +/** + * Chassis ID + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.2.3 "chassis ID". + */ +typedef struct pf_lldp_chassis_id +{ + /** Terminated string + Typically this field is a string (PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED) + If the subtype indicates a MAC address, the content is the + raw MAC address bytes (and trailing null termination). + Similarly for other subtypes */ + char string[PNET_LLDP_CHASSIS_ID_MAX_SIZE]; + + /** PF_LLDP_SUBTYPE_xxx */ + uint8_t subtype; + + bool is_valid; + + size_t len; +} pf_lldp_chassis_id_t; + +/** + * Port ID + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.3 "Port ID TLV". + */ +typedef struct pf_lldp_port_id +{ + /** Terminated string + Typically this field is a string (PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED) + If the subtype indicates a MAC address, the content is the + raw MAC address bytes (and trailing null termination). + Similarly for other subtypes */ + char string[PNET_LLDP_PORT_ID_MAX_SIZE]; + + /** PF_LLDP_SUBTYPE_xxx */ + uint8_t subtype; + + bool is_valid; + + size_t len; +} pf_lldp_port_id_t; + +/** + * Interface number + * + * "The interface number field shall contain the assigned number within + * the system that identifies the specific interface associated with this + * management address. If the value of the interface subtype is unknown, + * this field shall be set to zero." + * - IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.9 "Management Address TLV". + * + * Also see PN-Topology ch. 6.5.1 "Mapping of Ports and PROFINET Interfaces + * in LLDP MIB and MIB-II". + */ +typedef struct pf_lldp_interface_number +{ + int32_t value; + uint8_t subtype; /* 1 = unknown, 2 = ifIndex, 3 = systemPortNumber */ +} pf_lldp_interface_number_t; + +/** + * Management address + * + * Usually, this is an IPv4 address. It may also be a MAC address + * or other kinds of addresses, such as IPv6. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.9 "Management Address TLV". + */ +typedef struct pf_lldp_management_address +{ + uint8_t value[31]; + uint8_t subtype; + size_t len; + pf_lldp_interface_number_t interface_number; + bool is_valid; +} pf_lldp_management_address_t; + +/** + * Station name + * + * This is the name of an interface. It is called NameOfStation in + * the Profinet specification. It is usually a string, but may also + * be a MAC address. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) ch. 4.3.1.4.16. + */ +typedef struct pf_lldp_station_name +{ + char string[PNET_STATION_NAME_MAX_SIZE]; /** Terminated string */ + size_t len; +} pf_lldp_station_name_t; + +/** + * Port Name + * In standard name-of-device mode this is first part of Port ID + * In legacy mode Port ID and Port Name are the same. + */ +typedef struct pf_lldp_port_name +{ + char string[PNET_PORT_NAME_MAX_SIZE]; /** Terminated string */ + size_t len; +} pf_lldp_port_name_t; + +/** + * Measured signal delays in nanoseconds + * + * Valid range is 0x1 - 0xFFF. + * If a signal delay was not measured, its value is zero. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) ch. 4.11.2.2: "LLDP APDU + * abstract syntax", element "LLDP_PNIO_DELAY". + * + * See also pf_snmp_signal_delay_t. + */ +typedef struct pf_lldp_signal_delay +{ + uint32_t rx_delay_local; + uint32_t rx_delay_remote; + uint32_t tx_delay_local; + uint32_t tx_delay_remote; + uint32_t cable_delay_local; + bool is_valid; +} pf_lldp_signal_delay_t; + +typedef struct pf_lldp_boundary +{ + bool not_send_LLDP_Frames; + bool send_PTCP_Delay; + bool send_PATH_Delay; + bool reserved_bit4; + uint8_t reserved_8; + uint16_t rserved_16; +} pf_lldp_boundary_t; + +typedef struct pf_lldp_peer_to_peer_boundary +{ + pf_lldp_boundary_t boundary; + uint16_t properites; +} pf_lldp_peer_to_peer_boundary_t; + +/** + * LLDP Peer information used by the Profinet stack. + */ +typedef struct pf_lldp_peer_info +{ + /* LLDP TLVs */ + pf_lldp_chassis_id_t chassis_id; + pf_lldp_port_id_t port_id; + uint16_t ttl; + pf_lldp_port_description_t port_description; + pf_lldp_management_address_t management_address; + /* PROFIBUS TLVs */ + pf_lldp_signal_delay_t port_delay; + uint8_t port_status[4]; + pnet_ethaddr_t mac_address; + uint32_t line_delay; + pf_lldp_link_status_t phy_config; +} pf_lldp_peer_info_t; + +/** + * Get time when new information about remote device was received. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_timestamp_10ms Out: Time when the LLDP packet with the info + * was first received, in units of + * 10 milliseconds with system startup + * as the zero point (as in SNMP sysUpTime). + * Note that if an LLDP packet with identical + * info is received, the timestamp is not + * updated. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_timestamp ( + pnet_t * net, + int loc_port_num, + uint32_t * p_timestamp_10ms); + +/** + * Get Chassis ID of local device. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.2 "Chassis ID TLV". + * + * @param net In: The p-net stack instance. + * @param p_chassis_id Out: Chassis ID of local device. + */ +void pf_lldp_get_chassis_id (pnet_t * net, pf_lldp_chassis_id_t * p_chassis_id); + +/** + * Get Chassis ID for remote device connected to local port. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.2 "Chassis ID TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_chassis_id Out: Chassis ID of remote device. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_chassis_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_chassis_id_t * p_chassis_id); + +/** + * Get Port ID for local port. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.3 "Port ID TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_port_id Out: Port ID of local port. + */ +void pf_lldp_get_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id); + +/** + * Get Port ID for remote port. + * + * Note that the remote device may have multiple ports. Only the remote + * port connected to the local port is relevant here. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.3 "Port ID TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_port_id Out: Port ID of remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id); + +/** + * Get port description for local port. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.5 "Port Description TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_port_desc Out: Port description of local port. + */ +void pf_lldp_get_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_desc); + +/** + * Get port description for remote port. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.5 "Port Description TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_port_desc Out: Port description of remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_desc); + +/** + * Get Management Address for local interface. + * + * The management address should usually be the IP address for the local + * interface the local ports belong to. It could also be the MAC address of + * the local interface in case no IP address has been assigned. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.9 "Management Address TLV". + * + * @param net In: The p-net stack instance. + * @param p_man_address Out: Management address of local interface. + */ +void pf_lldp_get_management_address ( + pnet_t * net, + pf_lldp_management_address_t * p_man_address); + +/** + * Get Management Address for remote interface connected to local port. + * + * The management address should usually be the IP address for the remote + * interface the remote port belongs to. It could also be the MAC address of + * the remote interface in case no IP address has been assigned. + * + * Note that the remote device may have multiple interfaces. Only the remote + * interface connected to the local port is relevant here. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 9.5.9 "Management Address TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_man_address Out: Management address of remote interface. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_management_address ( + pnet_t * net, + int loc_port_num, + pf_lldp_management_address_t * p_man_address); + +/** + * Get remote station name for remote interface connected to local port. + * + * The station name is usually a string, but may also be the MAC address + * (as a string "AB-CD-EF-01-23-45") of the remote interface in case no string + * has been assigned. + * + * Note that the remote device may have multiple interfaces. Only the remote + * interface connected to the local port is relevant here. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_station_name Out: Station name of remote interface. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_station_name ( + pnet_t * net, + int loc_port_num, + pf_lldp_station_name_t * p_station_name); + +/** + * Get port name for remote port connected to local port. + * + * The port name is typically a string "port-001" or "port-001-00000" + * + * Note that the remote device may have multiple ports. Only the remote + * port connected to the local port is relevant here. + * + * See Profinet 2.4 Protocol 4.3.1.4.17 "Coding of the field NameOfPort" + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_port_name Out: Port name of remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_port_name ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_name_t * p_port_name); + +/** + * Get measured signal delays on local port. + * + * If a signal delay was not measured, its value is zero. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol): LLDP_PNIO_DELAY. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_delays Out: Measured signal delays on local port. + */ +void pf_lldp_get_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_lldp_signal_delay_t * p_delays); + +/** + * Get measured signal delays on remote port. + * + * If a signal delay was not measured, its value is zero. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol): LLDP_PNIO_DELAY. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_delays Out: Measured signal delays on remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_lldp_signal_delay_t * p_delays); + +/** + * Get link status of local port. + * + * See IEEE 802.1AB-2005 (LLDPv1) Annex G.2 "MAC/PHY Configuration/Status TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_link_status Out: Link status of local port. + */ +void pf_lldp_get_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status); + +/** + * Get link status of remote port. + * + * See IEEE 802.1AB-2005 (LLDPv1) Annex G.2 "MAC/PHY Configuration/Status TLV". + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * @param p_link_status Out: Link status of remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_lldp_get_peer_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status); + +/** + * Get formatted system description + * + * Encode as per Profinet specification: + * DeviceType, Blank, OrderID, Blank, IM_Serial_Number, Blank, HWRevision, + * Blank, SWRevisionPrefix, SWRevision. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) ch. 5.1.2 "APDU abstract syntax", + * field "SystemIdentification". + * + * @param net In: The p-net stack instance. + * @param system_description Out: System description. Terminated + * string. + * @param system_description_max_len In: Size of buffer. + * return Length of resulting string. + */ +size_t pf_lldp_get_system_description ( + const pnet_t * net, + char * system_description, + size_t system_description_max_len); + +/** + * Generate a string from a byte array representing + * a mac address. Format: "%02X-%02X-%02X-%02X-%02X-%02X". + * + * @param mac In: Mac address pointer. + * @param mac_str Out: Formatted MAC address string, null-terminated. + * @param mac_str_max_len In: Max length of mac_str buffer. + */ +void pf_lldp_mac_address_to_string ( + const uint8_t * mac, + char * mac_str, + size_t mac_str_max_len); + +/** + * Initialize the LLDP component. + * + * This function initializes the LLDP component and + * sends the initial LLDP message. + * @param net InOut: The p-net stack instance + */ +void pf_lldp_init (pnet_t * net); + +/** + * Start or restart a timer that monitors reception of LLDP frames from peer. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param timeout_in_secs In: TTL of the peer, typically 20 seconds. + */ +void pf_lldp_restart_peer_timeout ( + pnet_t * net, + int loc_port_num, + uint16_t timeout_in_secs); + +/** + * Stop a timer that monitors reception of LLDP frames from peer, if running. + * + * No diagnosis check will be made. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +void pf_lldp_stop_peer_timeout (pnet_t * net, int loc_port_num); + +/** + * Mark LLDP peer data as invalid. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +void pf_lldp_invalidate_peer_info (pnet_t * net, int loc_port_num); + +/** + * Enable sending of LLDP frames on local port + * + * An initial LLDP frame is transmitted when operation is called. + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +void pf_lldp_send_enable (pnet_t * net, int loc_port_num); + +/** + * Disable sending of LLDP frames on local port + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +void pf_lldp_send_disable (pnet_t * net, int loc_port_num); + +/** + * Process received LLDP message. + * + * Parse LLDP TLV format and store selected information. + * Trigger alarms if needed. + * @param net InOut: The p-net stack instance. + * @param loc_port_num In: Local port number the frame was received on. + * Valid range: 1 .. num_physical_ports + * See pf_port_get_list_of_ports(). + * Frame is dropped for invalid values. + * @param p_frame_buf InOut: Received LLDP message. + * Will be freed. + * @param offset In: The offset to start of LLDP data. + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +int pf_lldp_recv ( + pnet_t * net, + int loc_port_num, + pnal_buf_t * p_frame_buf, + uint16_t offset); + +/** + * Check if an alias name matches any of the LLDP peers. + * + * @param net InOut: The p-net stack instance. + * @param alias In: Alias to be compared with LLDP peers. + * Null terminated string. + * return true if the alias matches any of the peers, + * false if not. + */ +bool pf_lldp_is_alias_matching (pnet_t * net, const char * alias); + +/************ Internal functions, made available for unit testing ************/ + +int pf_lldp_generate_alias_name ( + const char * port_id, + const char * chassis_id, + char * alias, + uint16_t len); + +size_t pf_lldp_construct_frame (pnet_t * net, int loc_port_num, uint8_t buf[]); + +int pf_lldp_parse_packet ( + const uint8_t buf[], + uint16_t len, + pf_lldp_peer_info_t * peer_info); + +void pf_lldp_store_peer_info ( + pnet_t * net, + int loc_port_num, + const pf_lldp_peer_info_t * new_info); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_LLDP_H */ diff --git a/profinet_stack/p-net/src/common/pf_ppm.c b/profinet_stack/p-net/src/common/pf_ppm.c new file mode 100644 index 0000000..b0fbef0 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_ppm.c @@ -0,0 +1,1096 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Cyclic Provider Protocol Machine (PPM) + * + * This handles cyclic sending of data (and IOPS). Initializes transmit buffers. + * + * One instance of PPM exist per CR (together with a CPM). + * + * The states are W_START and RUN. + * + * There are functions used by the application (via pnet_api.c) to set and get + * data, IOCS and IOPS. + * + * A global mutex is used instead of a per-instance mutex. + * The locking time is very low so it should not be very congested. + * The mutex is created on the first call to pf_ppm_create and deleted on + * the last call to pf_ppm_close. + * Keep track of how many instances exist and delete the mutex when the + * number reaches 0 (zero). + * + */ + +#ifdef UNIT_TEST +#define os_get_current_time_us mock_os_get_current_time_us +#endif + +#include "pf_includes.h" + +#include +#include + +void pf_ppm_init (pnet_t * net) +{ + net->ppm_instance_cnt = ATOMIC_VAR_INIT (0); + + LOG_DEBUG (PF_PPM_LOG, "PPM(%d): Init driver\n", __LINE__); + +#if PNET_OPTION_DRIVER_ENABLE + if (net->fspm_cfg.driver_enable) + { + pf_driver_ppm_init (net); + } + else + { + pf_ppm_driver_sw_init (net); + } +#else + pf_ppm_driver_sw_init (net); +#endif +} + +/** + * Return a string representation of the PPM state. + * @param state In: The PPM state. + * @return A string representing the PPM state. + */ +const char * pf_ppm_state_to_string (pf_ppm_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_PPM_STATE_W_START: + s = "PF_PPM_STATE_W_START"; + break; + case PF_PPM_STATE_RUN: + s = "PF_PPM_STATE_RUN"; + break; + } + + return s; +} + +int pf_ppm_state_ind (pnet_t * net, pf_ar_t * p_ar, pf_ppm_t * p_ppm, bool error) +{ + if (error == true) + { + p_ar->err_cls = PNET_ERROR_CODE_1_PPM; + p_ar->err_code = PNET_ERROR_CODE_2_PPM_INVALID; + pf_cmsu_ppm_error_ind (net, p_ar, p_ar->err_cls, p_ar->err_code); + } + + return 0; +} + +/** + * Handle state changes in the PPM instance. + * @param p_ppm InOut: The PPM instance. + * @param state In: The new PPM state. + */ +void pf_ppm_set_state (pf_ppm_t * p_ppm, pf_ppm_state_values_t state) +{ + if (state != p_ppm->state) + { + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): New state %s (was %s)\n", + __LINE__, + pf_ppm_state_to_string (state), + pf_ppm_state_to_string (p_ppm->state)); + } + p_ppm->state = state; +} + +/** + * @internal + * Initialize a transmit buffer of a PPM instance. + * + * Insert destination and source MAC addresses, VLAN tag, Ethertype and + * Profinet frame ID. + * + * Initialize the rest of the buffer to zero. + * + * @param p_ppm In: The PPM instance. + * @param p_buf InOut:The buffer to be initialized. + * @param frame_id In: The frame_id. + * @param p_header In: The VLAN tag header IOCR parameter. + */ +static void pf_ppm_init_buf ( + pf_ppm_t * p_ppm, + pnal_buf_t * p_buf, + uint16_t frame_id, + pf_iocr_tag_header_t * p_header) +{ + uint16_t pos; + uint8_t * p_payload = (uint8_t *)p_buf->payload; + uint16_t u16 = 0; + + p_buf->len = p_ppm->buffer_length; + memset (p_payload, 0, p_buf->len); + pos = 0; + + /* Insert destination MAC address */ + memcpy (&p_payload[pos], &p_ppm->da, sizeof (p_ppm->da)); + pos += sizeof (p_ppm->da); + + /* Insert source MAC address */ + memcpy (&p_payload[pos], &p_ppm->sa, sizeof (p_ppm->sa)); + pos += sizeof (p_ppm->sa); + + /* Insert VLAN Tag protocol identifier (TPID) */ + u16 = PNAL_ETHTYPE_VLAN; + u16 = htons (u16); + memcpy (&p_payload[pos], &u16, sizeof (u16)); + pos += sizeof (u16); + + /* Insert VLAN ID (VID) and priority (Priority Code Point = PCP) */ + u16 = p_header->vlan_id & 0x0FFF; + u16 |= (p_header->iocr_user_priority & 0x0007) << 13; /* Three leftmost bits + */ + u16 = htons (u16); + memcpy (&p_payload[pos], &u16, sizeof (u16)); + pos += sizeof (u16); + + /* Insert EtherType */ + u16 = PNAL_ETHTYPE_PROFINET; + u16 = htons (u16); + memcpy (&p_payload[pos], &u16, sizeof (u16)); + pos += sizeof (u16); + + /* Insert Profinet frame ID (first part of Ethernet frame payload) */ + u16 = htons (frame_id); + memcpy (&p_payload[pos], &u16, sizeof (u16)); + /* No further pos advancement, to suppress clang warning */ +} + +void pf_ppm_finish_buffer (pnet_t * net, pf_ppm_t * p_ppm, uint16_t data_length) +{ + uint8_t * p_payload = ((pnal_buf_t *)p_ppm->p_send_buffer)->payload; + uint16_t u16; + + p_ppm->cycle = pf_ppm_calculate_next_cyclecounter ( + p_ppm->cycle, + p_ppm->send_clock_factor, + p_ppm->reduction_ratio); + + /* Insert data */ + os_mutex_lock (net->ppm_buf_lock); + memcpy (&p_payload[p_ppm->buffer_pos], p_ppm->buffer_data, data_length); + os_mutex_unlock (net->ppm_buf_lock); + + /* Insert cycle counter */ + u16 = htons (p_ppm->cycle); + memcpy (&p_payload[p_ppm->cycle_counter_offset], &u16, sizeof (u16)); + + /* Insert data status */ + memcpy ( + &p_payload[p_ppm->data_status_offset], + &p_ppm->data_status, + sizeof (p_ppm->data_status)); + + /* Insert transfer status */ + memcpy ( + &p_payload[p_ppm->transfer_status_offset], + &p_ppm->transfer_status, + sizeof (p_ppm->transfer_status)); +} + +/** + * Initialize ppm configuration from AR data. + * - Compute and set frame offsets to + * cycle counter, status and more + * - sender address (sa) + * - destination address (da) + * - send interval config + * - status + * + * @param net InOut: The NET instance. + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR instance. + * @return 0 on success. + * -1 if an error occurred. + */ +static int pf_ppm_init_cr_config (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + const uint16_t vlan_size = 4; + pf_iocr_t * p_iocr = &p_ar->iocrs[crep]; + pf_ppm_t * p_ppm = &p_iocr->ppm; + + p_ppm->first_transmit = false; + + memcpy ( + &p_ppm->sa, + &p_ar->ar_result.cm_responder_mac_add, + sizeof (p_ppm->sa)); + + memcpy (&p_ppm->da, &p_ar->ar_param.cm_initiator_mac_add, sizeof (p_ppm->da)); + + p_ppm->buffer_pos = 2 * sizeof (pnet_ethaddr_t) + vlan_size + + sizeof (uint16_t) + sizeof (uint16_t); + p_ppm->cycle = 0; + p_ppm->transfer_status = 0; + + /* Pre-compute send buffer offsets*/ + p_ppm->cycle_counter_offset = p_ppm->buffer_pos + /* ETH frame header */ + p_iocr->param.c_sdu_length; /* Profinet data + length */ + + p_ppm->data_status_offset = p_ppm->buffer_pos + /* ETH frame header */ + p_iocr->param.c_sdu_length + /* Profinet data + length */ + sizeof (uint16_t); /* cycle counter */ + + p_ppm->transfer_status_offset = p_ppm->buffer_pos + /* ETH frame header */ + p_iocr->param.c_sdu_length + /* Profinet + data + length */ + sizeof (uint16_t) + /* cycle counter */ + 1; /* data status */ + + p_ppm->buffer_length = p_ppm->buffer_pos + /* ETH frame header */ + p_iocr->param.c_sdu_length + /* Profinet data + length */ + sizeof (uint16_t) + /* cycle counter */ + 1 + /* data status */ + 1; /* transfer status */ + + p_ppm->data_status = + BIT (PNET_DATA_STATUS_BIT_STATE) | /* PRIMARY */ + BIT (PNET_DATA_STATUS_BIT_DATA_VALID) | + BIT (PNET_DATA_STATUS_BIT_STATION_PROBLEM_INDICATOR); /* Normal */ + + p_ppm->control_interval = ((uint32_t)p_iocr->param.send_clock_factor * + (uint32_t)p_iocr->param.reduction_ratio * 1000U) / + 32U; /* us */ + + p_ppm->send_clock_factor = p_iocr->param.send_clock_factor; + p_ppm->reduction_ratio = p_iocr->param.reduction_ratio; + + return 0; +} + +int pf_ppm_create (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + pf_iocr_t * p_iocr = &p_ar->iocrs[crep]; + pf_ppm_t * p_ppm; + uint32_t cnt; + + CC_ASSERT (net && net->ppm_drv && net->ppm_drv->activate_req && p_ar); + + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Create PPM instance. AREP %u " + "CREP %" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + cnt = atomic_fetch_add (&net->ppm_instance_cnt, 1); + if (cnt == 0) + { + net->ppm_buf_lock = os_mutex_create(); + CC_ASSERT (net->ppm_buf_lock != NULL); + } + + p_ppm = &p_iocr->ppm; + + if (p_ppm->state != PF_PPM_STATE_W_START) + { + p_ar->err_cls = PNET_ERROR_CODE_1_PPM; + p_ar->err_code = PNET_ERROR_CODE_2_PPM_INVALID_STATE; + return -1; + } + + if (pf_ppm_init_cr_config (net, p_ar, crep) != 0) + { + p_ar->err_cls = PNET_ERROR_CODE_1_PPM; + p_ar->err_code = PNET_ERROR_CODE_2_PPM_INVALID_STATE; + return -1; + } + + /* Get the buffer to store the outgoing frame into. */ + p_ppm->p_send_buffer = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + + if (p_ppm->p_send_buffer == NULL) + { + p_ar->err_cls = PNET_ERROR_CODE_1_PPM; + p_ar->err_code = PNET_ERROR_CODE_2_PPM_INVALID_STATE; + return -1; + } + + /* Default_values: Set buffer to zero and IOxS to BAD (=0) */ + /* Default_status: Set cycle_counter to invalid, transfer_status = 0, + * data_status = 0 */ + pf_ppm_init_buf ( + p_ppm, + p_ppm->p_send_buffer, + p_iocr->param.frame_id, + &p_iocr->param.iocr_tag_header); + + return net->ppm_drv->create (net, p_ar, crep); +} + +int pf_ppm_activate_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + int ret = -1; + pf_iocr_t * p_iocr = &p_ar->iocrs[crep]; + pf_ppm_t * p_ppm; + + p_ppm = &p_iocr->ppm; + + if (p_ppm->state == PF_PPM_STATE_RUN) + { + p_ar->err_cls = PNET_ERROR_CODE_1_PPM; + p_ar->err_code = PNET_ERROR_CODE_2_PPM_INVALID_STATE; + return ret; + } + + p_ppm->next_exec = os_get_current_time_us(); + p_ppm->next_exec += p_ppm->control_interval; + + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Activate PPM. AREP %u " + "CREP %" PRIu32 ", period %" PRIu32 " microseconds. FrameID 0x%04x\n", + __LINE__, + p_ar->arep, + crep, + p_ppm->control_interval, + p_iocr->param.frame_id); + + pf_ppm_set_state (p_ppm, PF_PPM_STATE_RUN); + + p_ppm->ci_running = true; + + /* Initialize input data and iops */ + pf_ppm_finish_buffer (net, p_ppm, p_iocr->in_length); + + ret = net->ppm_drv->activate_req (net, p_ar, crep); + + if (ret != 0) + { + pf_ppm_state_ind (net, p_ar, p_ppm, true); /* Error */ + } + + return ret; +} + +int pf_ppm_close_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + + pf_ppm_t * p_ppm = &p_ar->iocrs[crep].ppm; + uint32_t cnt; + + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Closing PPM for AREP %u CREP %" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + p_ppm->ci_running = false; + + /* Stop driver handling cyclic transmits */ + net->ppm_drv->close_req (net, p_ar, crep); + + pnal_buf_free (p_ppm->p_send_buffer); + // TODO - set p_send_buffer to NULL ?? + + pf_ppm_set_state (p_ppm, PF_PPM_STATE_W_START); + p_ppm->data_status = 0; + cnt = atomic_fetch_sub (&net->ppm_instance_cnt, 1); + if (cnt == 1) + { + os_mutex_destroy (net->ppm_buf_lock); + net->ppm_buf_lock = NULL; + } + + return 0; +} + +int pf_ppm_get_ar_iocr_desc ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_ar_t ** pp_ar, + pf_iocr_t ** pp_iocr, + pf_iodata_object_t ** pp_iodata, + uint32_t * p_crep) +{ + int ret = -1; + uint32_t crep; + uint16_t iodata_ix; + pf_subslot_t * p_subslot = NULL; + pf_ar_t * p_ar = NULL; + pf_iocr_t * p_iocr = NULL; + bool found = false; + + if ( + pf_cmdev_get_subslot_full (net, api_id, slot_nbr, subslot_nbr, &p_subslot) == + 0) + { + p_ar = p_subslot->p_ar; + } + + if (p_ar == NULL) + { + LOG_DEBUG (PF_PPM_LOG, "PPM(%d): No AR set in sub-slot\n", __LINE__); + } + else + { + /* + * Search the AR for an INPUT CR or an MC provider CR containing the + * sub-slot. + */ + for (crep = 0; ((found == false) && (crep < p_ar->nbr_iocrs)); crep++) + { + if ( + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_INPUT) || + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_MC_PROVIDER)) + { + p_iocr = &p_ar->iocrs[crep]; + for (iodata_ix = 0; + ((found == false) && (iodata_ix < p_iocr->nbr_data_desc)); + iodata_ix++) + { + if ( + (p_iocr->data_desc[iodata_ix].in_use == true) && + (p_iocr->data_desc[iodata_ix].api_id == api_id) && + (p_iocr->data_desc[iodata_ix].slot_nbr == slot_nbr) && + (p_iocr->data_desc[iodata_ix].subslot_nbr == subslot_nbr)) + { + *pp_iodata = &p_iocr->data_desc[iodata_ix]; + *pp_iocr = p_iocr; + *pp_ar = p_ar; + *p_crep = crep; + found = true; + } + } + } + } + } + + if (found == true) + { + ret = 0; + } + + return ret; +} + +int pf_ppm_set_data_and_iops ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const uint8_t * p_data, + uint16_t data_len, + const uint8_t * p_iops, + uint8_t iops_len) +{ + int ret = -1; + pf_iocr_t * p_iocr = NULL; + pf_iodata_object_t * p_iodata = NULL; + pf_ar_t * p_ar = NULL; + uint32_t crep; + + if ( + pf_ppm_get_ar_iocr_desc ( + net, + api_id, + slot_nbr, + subslot_nbr, + &p_ar, + &p_iocr, + &p_iodata, + &crep) == 0) + { + switch (p_iocr->ppm.state) + { + case PF_PPM_STATE_W_START: + case PF_PPM_STATE_RUN: + if ( + (data_len == p_iodata->data_length) && + (iops_len == p_iodata->iops_length)) + { + ret = net->ppm_drv->write_data_and_iops ( + net, + p_iocr, + p_iodata, + p_data, + data_len, + p_iops, + iops_len); + + p_iodata->data_avail = true; + } + else + { + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Given data size %u and IOPS size %u, " + "but PLC expects sizes %u and %u for slot %u subslot 0x%04x\n", + __LINE__, + data_len, + iops_len, + p_iodata->data_length, + p_iodata->iops_length, + slot_nbr, + subslot_nbr); + } + break; + default: + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Set data in wrong state: %u for AREP %u\n", + __LINE__, + p_iocr->ppm.state, + p_ar->arep); + break; + } + } + else + { + /* May happen after an ABORT */ + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): No data descriptor found for set data\n", + __LINE__); + } + + return ret; +} + +int pf_ppm_set_iocs ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const uint8_t * p_iocs, + uint8_t iocs_len) +{ + int ret = -1; + pf_iocr_t * p_iocr = NULL; + pf_iodata_object_t * p_iodata = NULL; + pf_ar_t * p_ar = NULL; + uint32_t crep; + + if ( + pf_ppm_get_ar_iocr_desc ( + net, + api_id, + slot_nbr, + subslot_nbr, + &p_ar, + &p_iocr, + &p_iodata, + &crep) == 0) + { + switch (p_iocr->ppm.state) + { + case PF_PPM_STATE_W_START: + case PF_PPM_STATE_RUN: + if (iocs_len == p_iodata->iocs_length) + { + ret = net->ppm_drv + ->write_iocs (net, p_iocr, p_iodata, p_iocs, iocs_len); + } + else if (p_iodata->iocs_length == 0) + { + /* ToDo: What does the spec say about this case? */ + LOG_DEBUG (PF_PPM_LOG, "PPM(%d): iocs_len is zero\n", __LINE__); + ret = 0; + } + else + { + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Given IOCS size %u, but PLC expects size %u " + "for slot %u subslot 0x%04x\n", + __LINE__, + iocs_len, + (unsigned)p_iodata->iocs_length, + slot_nbr, + subslot_nbr); + } + break; + default: + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Set data in wrong state: %u for AREP %u\n", + __LINE__, + (unsigned)p_iocr->ppm.state, + p_ar->arep); + break; + } + } + else + { + /* May happen after an ABORT */ + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): No data descriptor found for set iocs\n", + __LINE__); + } + + return ret; +} + +int pf_ppm_get_data_and_iops ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_data, + uint16_t * p_data_len, + uint8_t * p_iops, + uint8_t * p_iops_len) +{ + int ret = -1; + pf_iocr_t * p_iocr = NULL; + pf_iodata_object_t * p_iodata = NULL; + pf_ar_t * p_ar = NULL; + uint32_t crep; + + if ( + pf_ppm_get_ar_iocr_desc ( + net, + api_id, + slot_nbr, + subslot_nbr, + &p_ar, + &p_iocr, + &p_iodata, + &crep) == 0) + { + switch (p_iocr->ppm.state) + { + case PF_PPM_STATE_W_START: + p_ar->err_cls = PNET_ERROR_CODE_1_PPM; + p_ar->err_code = PNET_ERROR_CODE_2_PPM_INVALID_STATE; + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Get data in wrong state: %u for AREP %u\n", + __LINE__, + p_iocr->ppm.state, + p_ar->arep); + break; + case PF_PPM_STATE_RUN: + if ( + (*p_data_len >= p_iodata->data_length) && + (*p_iops_len >= p_iodata->iops_length)) + { + *p_data_len = p_iodata->data_length; + *p_iops_len = p_iodata->iops_length; + ret = net->ppm_drv->read_data_and_iops ( + net, + p_iocr, + p_iodata, + p_data, + *p_data_len, + p_iops, + *p_iops_len); + } + else + { + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Given data buffer size %u and IOPS buffer size " + "%u, but minimum sizes are %u and %u for slot %u subslot " + "0x%04x\n", + __LINE__, + (unsigned)*p_data_len, + (unsigned)*p_iops_len, + (unsigned)p_iodata->data_length, + (unsigned)p_iodata->iops_length, + slot_nbr, + subslot_nbr); + } + break; + default: + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Get data in wrong state: %u for AREP %u\n", + __LINE__, + (unsigned)p_iocr->ppm.state, + p_ar->arep); + break; + } + } + else + { + /* May happen after an ABORT */ + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): No data descriptor found for get data\n", + __LINE__); + } + + return ret; +} + +int pf_ppm_get_iocs ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_iocs, + uint8_t * p_iocs_len) +{ + int ret = -1; + pf_iocr_t * p_iocr = NULL; + pf_iodata_object_t * p_iodata = NULL; + pf_ar_t * p_ar = NULL; + uint32_t crep; + + if ( + pf_ppm_get_ar_iocr_desc ( + net, + api_id, + slot_nbr, + subslot_nbr, + &p_ar, + &p_iocr, + &p_iodata, + &crep) == 0) + { + switch (p_iocr->ppm.state) + { + case PF_PPM_STATE_W_START: + p_ar->err_cls = PNET_ERROR_CODE_1_PPM; + p_ar->err_code = PNET_ERROR_CODE_2_PPM_INVALID_STATE; + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Get iocs in wrong state: %u for AREP %u\n", + __LINE__, + p_iocr->ppm.state, + p_ar->arep); + break; + case PF_PPM_STATE_RUN: + if (*p_iocs_len >= p_iodata->iocs_length) + { + *p_iocs_len = p_iodata->iocs_length; + ret = net->ppm_drv + ->read_iocs (net, p_iocr, p_iodata, p_iocs, *p_iocs_len); + } + else + { + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Given IOCS buffer size %u, but minimum size is %u " + "for slot %u subslot 0x%04x\n", + __LINE__, + (unsigned)*p_iocs_len, + (unsigned)p_iodata->iocs_length, + slot_nbr, + subslot_nbr); + } + break; + default: + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): Get iocs in wrong state: %u for AREP %u\n", + __LINE__, + (unsigned)p_iocr->ppm.state, + p_ar->arep); + break; + } + } + else + { + /* May happen after an ABORT */ + LOG_ERROR ( + PF_PPM_LOG, + "PPM(%d): No data descriptor found for get iocs\n", + __LINE__); + } + + return ret; +} + +int pf_ppm_set_data_status_state ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep, + bool primary) +{ + pf_ppm_t * p_ppm; + + p_ppm = &p_ar->iocrs[crep].ppm; + + if (primary == true) + { + p_ppm->data_status |= BIT (PNET_DATA_STATUS_BIT_STATE); + } + else + { + p_ppm->data_status &= ~BIT (PNET_DATA_STATUS_BIT_STATE); + } + + return net->ppm_drv->write_data_status ( + net, + &p_ar->iocrs[crep], + p_ppm->data_status); +} + +int pf_ppm_set_data_status_redundancy ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep, + bool redundant) +{ + pf_ppm_t * p_ppm; + + p_ppm = &p_ar->iocrs[crep].ppm; + + if (redundant == true) + { + p_ppm->data_status |= BIT (PNET_DATA_STATUS_BIT_REDUNDANCY); + } + else + { + p_ppm->data_status &= ~BIT (PNET_DATA_STATUS_BIT_REDUNDANCY); + } + + return net->ppm_drv->write_data_status ( + net, + &p_ar->iocrs[crep], + p_ppm->data_status); +} + +int pf_ppm_set_data_status_provider ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep, + bool run) +{ + pf_ppm_t * p_ppm; + + p_ppm = &p_ar->iocrs[crep].ppm; + + if (run == true) + { + p_ppm->data_status |= BIT (PNET_DATA_STATUS_BIT_PROVIDER_STATE); + } + else + { + p_ppm->data_status &= ~BIT (PNET_DATA_STATUS_BIT_PROVIDER_STATE); + } + + return net->ppm_drv->write_data_status ( + net, + &p_ar->iocrs[crep], + p_ppm->data_status); +} + +int pf_ppm_get_data_status (const pf_ppm_t * p_ppm, uint8_t * p_data_status) +{ + *p_data_status = p_ppm->data_status; + return 0; +} + +void pf_ppm_set_problem_indicator ( + pnet_t * net, + pf_ar_t * p_ar, + bool problem_indicator) +{ + uint16_t ix; + + for (ix = 0; ix < NELEMENTS (p_ar->iocrs); ix++) + { + if ( + (p_ar->iocrs[ix].param.iocr_type == PF_IOCR_TYPE_INPUT) || + (p_ar->iocrs[ix].param.iocr_type == PF_IOCR_TYPE_MC_PROVIDER)) + { + if (problem_indicator == true) + { + p_ar->iocrs[ix].ppm.data_status &= + ~BIT (PNET_DATA_STATUS_BIT_STATION_PROBLEM_INDICATOR); + } + else + { + p_ar->iocrs[ix].ppm.data_status |= + BIT (PNET_DATA_STATUS_BIT_STATION_PROBLEM_INDICATOR); + } + + net->ppm_drv->write_data_status ( + net, + &p_ar->iocrs[ix], + p_ar->iocrs[ix].ppm.data_status); + } + } +} + +/************************** Utilities ****************************************/ + +/** + * Calculate the current cyclecounter from the current time. + * + * The timebase is 1/32 of a millisecond (31.25 us). + * Note that 31.25 us is 1000/32 us. + * This is the cyclecounter value, which is the background counter used for + * cycles. + * + * One cycle length is send_clock_factor x 31.25 us. + * The cyclenumber value is keeping track of which cycle we are in now. + * + * The reduction_ratio describes if transmit and receive should + * be done every cycle. A value of 1 corresponds to every cycle, + * while a value of 4 corresponds to transmit and receive every fourth cycle. + * + * For example, send_clock_factor=2 and reduction_ratio=4 gives a + * cycle time of 62.5 us, and a frame is sent every 250 us. + * In the first frame the cyclecounter value should be 0, and in the next should + * the value be 8. + * + * We need to normalize the cyclecounter to multiples of + * send_clock_factor x reduction_ratio. + * + * Described in Profinet protocol 2.4 sections + * - 4.5.3 + * - 4.7.2.1.2 Coding of the field CycleCounter + * - 4.12.4.5 + * - 5.2.5.59 and .62 and .63 + * + * @param timestamp In: Current time in microseconds. + * @param send_clock_factor In: Defines the length of a cycle, in units + * of 31.25 us. Allowed 1 .. 128. + * Typically powers of two. + * @param reduction_ratio In: Reduction ratio. If transmission should be + * done every cycle. Allowed 1 .. 512. + * Typically powers of two. + * @return The current cyclecounter + */ +uint16_t pf_ppm_calculate_cyclecounter ( + uint32_t timestamp, + uint16_t send_clock_factor, + uint16_t reduction_ratio) +{ + uint64_t raw_cyclecounter; + uint32_t number_of_transmissions; + const uint32_t transmission_interval_in_timebases = + send_clock_factor * reduction_ratio; + + raw_cyclecounter = (32UL * timestamp) / 1000UL; + + /* Integer division for truncation */ + number_of_transmissions = + raw_cyclecounter / transmission_interval_in_timebases; + + /* Now a multiple of transmission intervals */ + return number_of_transmissions * transmission_interval_in_timebases; +} + +/** + * Calculate the next cyclecounter value from the previous one. + * + * The timebase is 1/32 of a millisecond (31.25 us). + * + * One cycle length is send_clock_factor x 31.25 us. + * + * The reduction_ratio describes if transmit and receive should + * be done every cycle. A value of 1 corresponds to every cycle, + * while a value of 4 corresponds to transmit and receive every fourth cycle. + * + * For example, send_clock_factor=2 and reduction_ratio=4 gives a + * cycle time of 62.5 us, and a frame is sent every 250 us. + * In the first frame the cyclecounter value should be 0, and in the next should + * the value be 8. + * + * @param previous_cyclecounter In: Previous cyclecounter value + * @param send_clock_factor In: Defines the length of a cycle, in units + * of 31.25 us. Allowed 1 .. 128. + * Typically powers of two. + * @param reduction_ratio In: Reduction ratio. If transmission should be + * done every cycle. Allowed 1 .. 512. + * Typically powers of two. + * @return Next cyclecounter value + */ +uint16_t pf_ppm_calculate_next_cyclecounter ( + uint16_t previous_cyclecounter, + uint16_t send_clock_factor, + uint16_t reduction_ratio) +{ + uint16_t number_of_previous_transmissions; + const uint32_t transmission_interval_in_timebases = + send_clock_factor * reduction_ratio; + + /* Integer division for truncation */ + number_of_previous_transmissions = + previous_cyclecounter / transmission_interval_in_timebases; + + /* Make sure we return a multiple of transmission intervals, regardless + of the previous_cyclecounter value */ + return (number_of_previous_transmissions + 1) * + transmission_interval_in_timebases; +} + +/**************** Diagnostic strings *****************************************/ + +void pf_ppm_show (const pf_ppm_t * p_ppm) +{ + printf ("ppm:\n"); + printf ( + " state = %s\n", + pf_ppm_state_to_string (p_ppm->state)); + printf (" errline = %u\n", (unsigned)p_ppm->errline); + printf (" errcnt = %u\n", (unsigned)p_ppm->errcnt); + printf ( + " first_transmit = %u\n", + (unsigned)p_ppm->first_transmit); + printf (" trx_cnt = %u\n", (unsigned)p_ppm->trx_cnt); + printf (" p_send_buffer = %p\n", p_ppm->p_send_buffer); + printf ( + " p_send_buffer->len = %u\n", + p_ppm->p_send_buffer ? ((pnal_buf_t *)(p_ppm->p_send_buffer))->len : 0); + printf (" new_buf = %u\n", (unsigned)p_ppm->new_buf); + printf ( + " control_interval = %u\n", + (unsigned)p_ppm->control_interval); + printf (" next_exec = %" PRIu32 "\n", p_ppm->next_exec); + printf (" cycle = %u\n", (unsigned)p_ppm->cycle); + printf ( + " cycle_counter_off = %u\n", + (unsigned)p_ppm->cycle_counter_offset); + printf ( + " data_status_offset = %u\n", + (unsigned)p_ppm->data_status_offset); + printf ( + " transfer_status_of = %u\n", + (unsigned)p_ppm->transfer_status_offset); + printf ( + " ci_running = %u\n", + (unsigned)p_ppm->ci_running); + printf ( + " ci_timer = %u\n", + (unsigned)pf_scheduler_get_value (&p_ppm->ci_timeout)); + printf ( + " transfer_status = %u\n", + (unsigned)p_ppm->transfer_status); + printf ( + " data_status = %x\n", + (unsigned)p_ppm->data_status); + printf ( + " buffer_length = %u\n", + (unsigned)p_ppm->buffer_length); + printf ( + " buffer_pos = %u\n", + (unsigned)p_ppm->buffer_pos); +} diff --git a/profinet_stack/p-net/src/common/pf_ppm.h b/profinet_stack/p-net/src/common/pf_ppm.h new file mode 100644 index 0000000..8aa3302 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_ppm.h @@ -0,0 +1,312 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_PPM_H +#define PF_PPM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the PPM component. + * @param net InOut: The p-net stack instance + */ +void pf_ppm_init (pnet_t * net); + +/** + * Create a PPM instance. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the PPM instance was created. + * -1 if an error occurred. + */ +int pf_ppm_create (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + +/** + * Instantiate and start a PPM instance. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the PPM instance was activated. + * -1 if an error occurred. + */ +int pf_ppm_activate_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + +/** + * Close and de-commission a PPM instance. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the PPM instance was closed. + * -1 if an error occurred. + */ +int pf_ppm_close_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + +/** + * Find the AR, input IOCR and IODATA object instances for the specified + * sub-slot. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param pp_ar Out: The AR instance. + * @param pp_iocr Out: The IOCR instance. + * @param pp_iodata Out: The IODATA object instance. + * @param p_crep Out: The CREP index + * @return 0 If the information has been found. + * -1 If the information was not found. + */ +int pf_ppm_get_ar_iocr_desc ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_ar_t ** pp_ar, + pf_iocr_t ** pp_iocr, + pf_iodata_object_t ** pp_iodata, + uint32_t * p_crep); + +/** + * Set the data and IOPS for a sub-module. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_data In: The application data. + * If NULL is passed, frame data is + * not updated. + * @param data_len In: The length of the application data. + * @param p_iops In: The IOPS of the application data. + * @param iops_len In: The length of the IOPS. + * @return 0 if the input data and IOPS was set. + * -1 if an error occurred. + */ +int pf_ppm_set_data_and_iops ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const uint8_t * p_data, + uint16_t data_len, + const uint8_t * p_iops, + uint8_t iops_len); + +/** + * Set IOCS for a sub-module. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_iocs In: The IOCS of the application data. + * @param iocs_len In: The length of the IOCS data. + * @return 0 if the IOCS was set. + * -1 if an error occurred. + */ +int pf_ppm_set_iocs ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + const uint8_t * p_iocs, + uint8_t iocs_len); + +/** + * Retrieve the data and IOPS for a sub-module. + * + * Note that this is not from the PLC, but previously set by the application. + * + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_data Out: The application data. + * @param p_data_len In: The length of the p_data buffer. + * Out: Actual length of the sub-module data. + * @param p_iops Out: The IOPS of the application data. + * @param p_iops_len In: Size of buffer at p_iops. + * Out: Actual length of IOPS data. + * @return 0 if the input data and IOPS could e retrieved. + * -1 if an error occurred. + */ +int pf_ppm_get_data_and_iops ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_data, + uint16_t * p_data_len, + uint8_t * p_iops, + uint8_t * p_iops_len); + +/** + * Retrieve IOCS for a sub-module. + * + * Note that this is not from the PLC, but previously set by the application. + * + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param p_iocs Out: The IOCS of the application data. + * @param p_iocs_len In: Size of buffer at p_iocs. + * Out: Actual length of IOCS data. + * @return 0 if the IOCS could be retrieved. + * -1 if an error occurred. + */ +int pf_ppm_get_iocs ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_iocs, + uint8_t * p_iocs_len); + +/** + * Set the state to "Primary" or "Backup" in the cyclic data sent to the + * IO-Controller. + * + * Implements the "Local Set State" primitive. + * + * See Profinet 2.4 Protocol, section 4.7.2.1.3 "Coding of the field DataStatus" + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR instance. + * @param primary In: true if the state is "primary". + * @return 0 if the state was set. + * -1 if an error occurred. + */ +int pf_ppm_set_data_status_state ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep, + bool primary); + +/** + * Set the redundant bit in the cyclic data sent to the IO-Controller. + * + * The interpretation of this bit is dependent on whether the state is + * "Primary" or "Backup" . + * + * Implements the "Local Set Redundancy State" primitive. + * + * See Profinet 2.4 Protocol, section 4.7.2.1.3 "Coding of the field DataStatus" + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR instance. + * @param redundant In: true if the state is "redundant". + * @return 0 if the redundancy state was set. + * -1 if an error occurred. + */ +int pf_ppm_set_data_status_redundancy ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep, + bool redundant); + +/** + * Set the stop/running bit in cyclic data sent to the IO-Controller. + * + * Implements the "Local set Provider State" primitive. + * + * See Profinet 2.4 Protocol, section 4.7.2.1.3 "Coding of the field DataStatus" + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR instance. + * @param run In: true if the application is "running". + * @return 0 if the provider status was set. + * -1 if an error occurred. + */ +int pf_ppm_set_data_status_provider ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep, + bool run); + +/** + * Get the data status of the PPM connection. + * @param p_ppm In: The PPM instance. + * @param p_data_status Out: The PPM data status. + * @return + */ +int pf_ppm_get_data_status (const pf_ppm_t * p_ppm, uint8_t * p_data_status); + +/** + * Set/Reset the station problem indicator which is included in all data + * messages. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param problem_indicator In: The problem indicator. + */ +void pf_ppm_set_problem_indicator ( + pnet_t * net, + pf_ar_t * p_ar, + bool problem_indicator); + +/** + * Show information about a PPM instance. + * @param p_ppm In: The PPM instance. + */ +void pf_ppm_show (const pf_ppm_t * p_ppm); + +/************ Internal functions, used by PPM driver ************/ + +/** + * Finalize a PPM transmit message in the send buffer. + * + * Insert data, cycle counter, data status and transfer status. + * + * @param net InOut: The p-net stack instance + * @param p_ppm In: The PPM instance. + * @param data_length In: The length of the message. + */ +void pf_ppm_finish_buffer (pnet_t * net, pf_ppm_t * p_ppm, uint16_t data_length); + +/** + * Send error indications to other components. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_ppm In: The PPM instance. + * @param error In: An error flag. + * @return 0 always. + */ +int pf_ppm_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pf_ppm_t * p_ppm, + bool error); + +/************ Internal functions, made available for unit testing ************/ + +uint16_t pf_ppm_calculate_cyclecounter ( + uint32_t timestamp, + uint16_t send_clock_factor, + uint16_t reduction_ratio); + +uint16_t pf_ppm_calculate_next_cyclecounter ( + uint16_t previous_cyclecounter, + uint16_t send_clock_factor, + uint16_t reduction_ratio); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_PPM_H */ diff --git a/profinet_stack/p-net/src/common/pf_ppm_driver_sw.c b/profinet_stack/p-net/src/common/pf_ppm_driver_sw.c new file mode 100644 index 0000000..8964592 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_ppm_driver_sw.c @@ -0,0 +1,293 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pf_includes.h" + +#include +#include + +/** + * @internal + * Send the process data frame. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * If the PPM has not been stopped during the wait, then a data message + * is sent and the function is rescheduled. + * + * @param net InOut: The p-net stack instance + * @param arg In: The IOCR instance. + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +static void pf_ppm_drv_sw_send (pnet_t * net, void * arg, uint32_t current_time) +{ + pf_iocr_t * p_arg = (pf_iocr_t *)arg; + uint32_t delay = 0; + + pf_scheduler_reset_handle (&p_arg->ppm.ci_timeout); + if (p_arg->ppm.ci_running == true) + { + /* Insert data, status etc. The in_length is the size of input to the + * controller */ + pf_ppm_finish_buffer (net, &p_arg->ppm, p_arg->in_length); + + /* Now send it */ + if (pf_eth_send_on_management_port (net, p_arg->ppm.p_send_buffer) > 0) + { + /* Schedule next execution */ + p_arg->ppm.next_exec += p_arg->ppm.control_interval; + delay = p_arg->ppm.next_exec - current_time; + if ( + pf_scheduler_add ( + net, + delay, + pf_ppm_drv_sw_send, + arg, + &p_arg->ppm.ci_timeout) == 0) + { + p_arg->ppm.trx_cnt++; + if (p_arg->ppm.first_transmit == false) + { + pf_ppm_state_ind (net, p_arg->p_ar, &p_arg->ppm, false); + p_arg->ppm.first_transmit = true; + } + } + else + { + /* Indidate error */ + pf_ppm_state_ind (net, p_arg->p_ar, &p_arg->ppm, true); + } + } + } +} + +int pf_ppm_drv_sw_create (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + return 0; +} + +int pf_ppm_drv_sw_activate_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + int ret = -1; + pf_iocr_t * p_iocr = &p_ar->iocrs[crep]; + pf_ppm_t * p_ppm = &p_ar->iocrs[crep].ppm; + + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Start scheduling of process data frames for AREP %u CREP " + "%" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + pf_scheduler_init_handle (&p_ppm->ci_timeout, "ppm"); + ret = pf_scheduler_add ( + net, + p_ppm->control_interval, + pf_ppm_drv_sw_send, + p_iocr, + &p_ppm->ci_timeout); + + return ret; +} + +int pf_ppm_drv_sw_close_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + pf_ppm_t * p_ppm = &p_ar->iocrs[crep].ppm; + + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Stop scheduling of process data frames for AREP %u CREP " + "%" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + pf_scheduler_remove_if_running (net, &p_ppm->ci_timeout); + + return 0; +} + +static int pf_ppm_drv_sw_write_frame_buffer ( + pnet_t * net, + pf_iocr_t * iocr, + uint32_t offset, + const uint8_t * data, + uint16_t len) +{ + memcpy (&iocr->ppm.buffer_data[offset], data, len); + return 0; +} + +static int pf_ppm_drv_sw_read_frame_buffer ( + pnet_t * net, + pf_iocr_t * iocr, + uint32_t offset, + uint8_t * data, + uint16_t len) +{ + memcpy (data, &iocr->ppm.buffer_data[offset], len); + return 0; +} + +int pf_ppm_drv_sw_write_data_and_iops ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + const uint8_t * data, + uint16_t len, + const uint8_t * iops, + uint8_t iops_len) +{ + int ret = 0; + os_mutex_lock (net->ppm_buf_lock); + + if (data != NULL) + { + ret = pf_ppm_drv_sw_write_frame_buffer ( + net, + iocr, + p_iodata->data_offset, + data, + len); + } + + if (ret == 0) + { + ret = pf_ppm_drv_sw_write_frame_buffer ( + net, + iocr, + p_iodata->iops_offset, + iops, + iops_len); + } + os_mutex_unlock (net->ppm_buf_lock); + + return ret; +} + +int pf_ppm_drv_sw_read_data_and_iops ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * data, + uint16_t data_len, + uint8_t * iops, + uint8_t iops_len) +{ + int ret; + + os_mutex_lock (net->ppm_buf_lock); + + ret = pf_ppm_drv_sw_read_frame_buffer ( + net, + iocr, + p_iodata->data_offset, + data, + data_len); + if (ret == 0) + { + ret = pf_ppm_drv_sw_read_frame_buffer ( + net, + iocr, + p_iodata->iops_offset, + iops, + iops_len); + } + + os_mutex_unlock (net->ppm_buf_lock); + + return ret; +} + +int pf_ppm_drv_sw_write_iocs ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + const uint8_t * iocs, + uint8_t len) +{ + int ret; + + os_mutex_lock (net->ppm_buf_lock); + ret = pf_ppm_drv_sw_write_frame_buffer ( + net, + iocr, + p_iodata->iocs_offset, + iocs, + len); + os_mutex_unlock (net->ppm_buf_lock); + + return ret; +} + +int pf_ppm_drv_sw_read_iocs ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * iocs, + uint8_t iocs_len) +{ + int ret; + + os_mutex_lock (net->ppm_buf_lock); + ret = pf_ppm_drv_sw_read_frame_buffer ( + net, + iocr, + p_iodata->iocs_offset, + iocs, + iocs_len); + os_mutex_unlock (net->ppm_buf_lock); + + return ret; +} + +int pf_ppm_drv_sw_write_data_status ( + pnet_t * net, + pf_iocr_t * iocr, + uint8_t data_status) +{ + return 0; +} + +void pf_ppm_drv_sw_show (const pf_ppm_t * p_ppm) +{ + printf ("pf_ppm_drv_sw_show not implemented\n"); +} + +void pf_ppm_driver_sw_init (pnet_t * net) +{ + static const pf_ppm_driver_t drv = { + .create = pf_ppm_drv_sw_create, + .activate_req = pf_ppm_drv_sw_activate_req, + .close_req = pf_ppm_drv_sw_close_req, + .write_data_and_iops = pf_ppm_drv_sw_write_data_and_iops, + .read_data_and_iops = pf_ppm_drv_sw_read_data_and_iops, + .write_iocs = pf_ppm_drv_sw_write_iocs, + .read_iocs = pf_ppm_drv_sw_read_iocs, + .write_data_status = pf_ppm_drv_sw_write_data_status, + /*.read_data_status = pf_ppm_drv_sw_read_data_status, */ + .show = pf_ppm_drv_sw_show}; + + net->ppm_drv = &drv; + + LOG_INFO ( + PF_PPM_LOG, + "PPM_DRIVER_SW(%d): Default PPM driver installed\n", + __LINE__); +} diff --git a/profinet_stack/p-net/src/common/pf_ppm_driver_sw.h b/profinet_stack/p-net/src/common/pf_ppm_driver_sw.h new file mode 100644 index 0000000..47b9662 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_ppm_driver_sw.h @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_PPM_DRIVER_SW_H +#define PF_PPM_DRIVER_SW_H + +#ifdef __cplusplus +extern "C" { +#endif + +void pf_ppm_driver_sw_init (pnet_t * net); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_DRIVER_SW_H */ diff --git a/profinet_stack/p-net/src/common/pf_ptcp.c b/profinet_stack/p-net/src/common/pf_ptcp.c new file mode 100644 index 0000000..3aa04c7 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_ptcp.c @@ -0,0 +1,20 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST + +#endif + +#include "pf_includes.h" diff --git a/profinet_stack/p-net/src/common/pf_ptcp.h b/profinet_stack/p-net/src/common/pf_ptcp.h new file mode 100644 index 0000000..a20ce0d --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_ptcp.h @@ -0,0 +1,27 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_PTCP_H +#define PF_PTCP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* PF_PTCP_H */ diff --git a/profinet_stack/p-net/src/common/pf_scheduler.c b/profinet_stack/p-net/src/common/pf_scheduler.c new file mode 100644 index 0000000..661e744 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_scheduler.c @@ -0,0 +1,642 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Scheduler implementation + * + * Use the scheduler to execute callbacks after a known delay time. + * + */ + +#ifdef UNIT_TEST +#define os_get_current_time_us mock_os_get_current_time_us +#endif + +#include "pf_includes.h" + +#include +#include + +static bool pf_scheduler_is_linked (pnet_t * net, uint32_t first, uint32_t ix) +{ + bool ret = false; + uint32_t cnt = 0; /* Guard against infinite loop */ + + if (ix < PF_MAX_TIMEOUTS) + { + while (first < PF_MAX_TIMEOUTS) + { + CC_ASSERT (cnt < PF_MAX_TIMEOUTS); + if (first == ix) + { + ret = true; + } + first = net->scheduler_timeouts[first].next; + cnt++; + } + } + + return ret; +} + +static void pf_scheduler_unlink ( + pnet_t * net, + volatile uint32_t * p_q, + uint32_t ix) +{ + /* Unlink from busy list */ + uint32_t prev_ix; + uint32_t next_ix; + + if (ix >= PF_MAX_TIMEOUTS) + { + LOG_ERROR ( + PNET_LOG, + "Sched(%d): ix (%u) is invalid\n", + __LINE__, + (unsigned)ix); + } + else if (pf_scheduler_is_linked (net, *p_q, ix) == false) + { + LOG_ERROR ( + PNET_LOG, + "Sched(%d): %s is not in Q\n", + __LINE__, + net->scheduler_timeouts[ix].name); + } + else + { + prev_ix = net->scheduler_timeouts[ix].prev; + next_ix = net->scheduler_timeouts[ix].next; + if (*p_q == ix) + { + *p_q = next_ix; + } + if (next_ix < PF_MAX_TIMEOUTS) + { + net->scheduler_timeouts[next_ix].prev = prev_ix; + } + if (prev_ix < PF_MAX_TIMEOUTS) + { + net->scheduler_timeouts[prev_ix].next = next_ix; + } + } +} + +static void pf_scheduler_link_after ( + pnet_t * net, + volatile uint32_t * p_q, + uint32_t ix, + uint32_t pos) +{ + uint32_t next_ix; + + if (ix >= PF_MAX_TIMEOUTS) + { + LOG_ERROR ( + PNET_LOG, + "Sched(%d): ix (%u) is invalid\n", + __LINE__, + (unsigned)ix); + } + else if (pf_scheduler_is_linked (net, *p_q, ix) == true) + { + LOG_ERROR ( + PNET_LOG, + "Sched(%d): %s is already in Q\n", + __LINE__, + net->scheduler_timeouts[ix].name); + } + else if (pos >= PF_MAX_TIMEOUTS) + { + /* Put first in possible non-empty Q */ + net->scheduler_timeouts[ix].prev = PF_MAX_TIMEOUTS; + net->scheduler_timeouts[ix].next = *p_q; + if (*p_q < PF_MAX_TIMEOUTS) + { + net->scheduler_timeouts[*p_q].prev = ix; + } + + *p_q = ix; + } + else if (*p_q >= PF_MAX_TIMEOUTS) + { + /* Q is empty - insert first in Q */ + net->scheduler_timeouts[ix].prev = PF_MAX_TIMEOUTS; + net->scheduler_timeouts[ix].next = PF_MAX_TIMEOUTS; + + *p_q = ix; + } + else + { + next_ix = net->scheduler_timeouts[pos].next; + + if (next_ix < PF_MAX_TIMEOUTS) + { + net->scheduler_timeouts[next_ix].prev = ix; + } + net->scheduler_timeouts[pos].next = ix; + + net->scheduler_timeouts[ix].prev = pos; + net->scheduler_timeouts[ix].next = next_ix; + } +} + +static void pf_scheduler_link_before ( + pnet_t * net, + volatile uint32_t * p_q, + uint32_t ix, + uint32_t pos) +{ + uint32_t prev_ix; + + if (ix >= PF_MAX_TIMEOUTS) + { + LOG_ERROR ( + PNET_LOG, + "Sched(%d): ix (%u) is invalid\n", + __LINE__, + (unsigned)ix); + } + else if (pf_scheduler_is_linked (net, *p_q, ix) == true) + { + LOG_ERROR ( + PNET_LOG, + "Sched(%d): %s is already in Q\n", + __LINE__, + net->scheduler_timeouts[ix].name); + } + else if (pos >= PF_MAX_TIMEOUTS) + { + /* Put first in possible non-empty Q */ + net->scheduler_timeouts[ix].prev = PF_MAX_TIMEOUTS; + net->scheduler_timeouts[ix].next = *p_q; + if (*p_q < PF_MAX_TIMEOUTS) + { + net->scheduler_timeouts[*p_q].prev = ix; + } + + *p_q = ix; + } + else if (*p_q >= PF_MAX_TIMEOUTS) + { + /* Q is empty - insert first in Q */ + net->scheduler_timeouts[ix].prev = PF_MAX_TIMEOUTS; + net->scheduler_timeouts[ix].next = PF_MAX_TIMEOUTS; + + *p_q = ix; + } + else + { + prev_ix = net->scheduler_timeouts[pos].prev; + + if (prev_ix < PF_MAX_TIMEOUTS) + { + net->scheduler_timeouts[prev_ix].next = ix; + } + net->scheduler_timeouts[pos].prev = ix; + + net->scheduler_timeouts[ix].next = pos; + net->scheduler_timeouts[ix].prev = prev_ix; + + if (*p_q == pos) + { + /* ix is now first in the Q */ + *p_q = ix; + } + } +} + +void pf_scheduler_reset_handle (pf_scheduler_handle_t * handle) +{ + handle->timer_index = UINT32_MAX; +} + +void pf_scheduler_init_handle (pf_scheduler_handle_t * handle, const char * name) +{ + handle->name = name; + pf_scheduler_reset_handle (handle); +} + +const char * pf_scheduler_get_name (const pf_scheduler_handle_t * handle) +{ + return handle->name; +} + +uint32_t pf_scheduler_get_value (const pf_scheduler_handle_t * handle) +{ + return handle->timer_index; +} + +bool pf_scheduler_is_running (const pf_scheduler_handle_t * handle) +{ + return (handle->timer_index == UINT32_MAX) ? false : true; +} + +void pf_scheduler_remove_if_running ( + pnet_t * net, + pf_scheduler_handle_t * handle) +{ + if (pf_scheduler_is_running (handle)) + { + pf_scheduler_remove (net, handle); + } +} + +int pf_scheduler_restart ( + pnet_t * net, + uint32_t delay, + pf_scheduler_timeout_ftn_t cb, + void * arg, + pf_scheduler_handle_t * handle) +{ + pf_scheduler_remove_if_running (net, handle); + return pf_scheduler_add (net, delay, cb, arg, handle); +} + +void pf_scheduler_init (pnet_t * net, uint32_t tick_interval) +{ + uint32_t ix; + + net->scheduler_timeout_first = PF_MAX_TIMEOUTS; /* Nothing in queue */ + net->scheduler_timeout_free = PF_MAX_TIMEOUTS; /* Nothing in queue. */ + + if (net->scheduler_timeout_mutex == NULL) + { + net->scheduler_timeout_mutex = os_mutex_create(); + } + memset ((void *)net->scheduler_timeouts, 0, sizeof (net->scheduler_timeouts)); + + net->scheduler_tick_interval = tick_interval; + CC_ASSERT (net->scheduler_tick_interval > 0); + + /* Link all entries into a list and put them into the free queue. */ + for (ix = PF_MAX_TIMEOUTS; ix > 0; ix--) + { + net->scheduler_timeouts[ix - 1].name = ""; + net->scheduler_timeouts[ix - 1].in_use = false; + pf_scheduler_link_before ( + net, + &net->scheduler_timeout_free, + ix - 1, + net->scheduler_timeout_free); + } +} + +int pf_scheduler_add ( + pnet_t * net, + uint32_t delay, + pf_scheduler_timeout_ftn_t cb, + void * arg, + pf_scheduler_handle_t * handle) +{ + uint32_t ix_this; + uint32_t ix_prev; + uint32_t ix_free; + uint32_t now = os_get_current_time_us(); + + delay = + pf_scheduler_sanitize_delay (delay, net->scheduler_tick_interval, true); + + os_mutex_lock (net->scheduler_timeout_mutex); + /* Unlink from the free list */ + ix_free = net->scheduler_timeout_free; + pf_scheduler_unlink (net, &net->scheduler_timeout_free, ix_free); + os_mutex_unlock (net->scheduler_timeout_mutex); + + if (ix_free >= PF_MAX_TIMEOUTS) + { + LOG_ERROR ( + PNET_LOG, + "SCHEDULER(%d): Out of timeout resources!!\n", + __LINE__); + handle->timer_index = UINT32_MAX; + return -1; + } + + net->scheduler_timeouts[ix_free].in_use = true; + net->scheduler_timeouts[ix_free].name = handle->name; + net->scheduler_timeouts[ix_free].cb = cb; + net->scheduler_timeouts[ix_free].arg = arg; + net->scheduler_timeouts[ix_free].when = now + delay; + + os_mutex_lock (net->scheduler_timeout_mutex); + if (net->scheduler_timeout_first >= PF_MAX_TIMEOUTS) + { + /* Put into empty q */ + pf_scheduler_link_before ( + net, + &net->scheduler_timeout_first, + ix_free, + PF_MAX_TIMEOUTS); + } + else if ( + ((int32_t) ( + net->scheduler_timeouts[ix_free].when - + net->scheduler_timeouts[net->scheduler_timeout_first].when)) <= 0) + { + /* Put first in non-empty q */ + pf_scheduler_link_before ( + net, + &net->scheduler_timeout_first, + ix_free, + net->scheduler_timeout_first); + } + else + { + /* Find pos in non-empty q */ + ix_prev = net->scheduler_timeout_first; + ix_this = net->scheduler_timeouts[net->scheduler_timeout_first].next; + while ((ix_this < PF_MAX_TIMEOUTS) && + (((int32_t) ( + net->scheduler_timeouts[ix_free].when - + net->scheduler_timeouts[ix_this].when)) > 0)) + { + ix_prev = ix_this; + ix_this = net->scheduler_timeouts[ix_this].next; + } + + /* Put after ix_prev */ + pf_scheduler_link_after ( + net, + &net->scheduler_timeout_first, + ix_free, + ix_prev); + } + os_mutex_unlock (net->scheduler_timeout_mutex); + + handle->timer_index = ix_free + 1; /* Make sure 0 is invalid. */ + + return 0; +} + +void pf_scheduler_remove (pnet_t * net, pf_scheduler_handle_t * handle) +{ + uint16_t ix; + + if (handle->timer_index == 0 || handle->timer_index > PF_MAX_TIMEOUTS) + { + LOG_ERROR ( + PNET_LOG, + "SCHEDULER(%d): Invalid value %" PRIu32 " for timeout \"%s\". No " + "removal.\n", + __LINE__, + handle->timer_index, + handle->name); + } + else + { + /* See pf_scheduler_add() for handle->timer_index details */ + ix = handle->timer_index - 1; + os_mutex_lock (net->scheduler_timeout_mutex); + + if (net->scheduler_timeouts[ix].name != handle->name) + { + LOG_ERROR ( + PNET_LOG, + "SCHEDULER(%d): Expected %s but got %s. No removal.\n", + __LINE__, + net->scheduler_timeouts[ix].name, + handle->name); + } + else if (net->scheduler_timeouts[ix].in_use == false) + { + LOG_DEBUG ( + PNET_LOG, + "SCHEDULER(%d): Tried to remove timeout \"%s\", but it has already " + "been triggered.\n", + __LINE__, + handle->name); + } + else + { + /* Unlink from busy list */ + pf_scheduler_unlink (net, &net->scheduler_timeout_first, ix); + + /* Insert into free list. */ + net->scheduler_timeouts[ix].in_use = false; + pf_scheduler_link_before ( + net, + &net->scheduler_timeout_free, + ix, + net->scheduler_timeout_free); + + handle->timer_index = UINT32_MAX; + } + + os_mutex_unlock (net->scheduler_timeout_mutex); + } +} + +void pf_scheduler_tick (pnet_t * net) +{ + uint32_t ix; + pf_scheduler_timeout_ftn_t ftn; + void * arg; + uint32_t pf_current_time = os_get_current_time_us(); + + os_mutex_lock (net->scheduler_timeout_mutex); + + /* Send event to all expired delay entries. */ + while ((net->scheduler_timeout_first < PF_MAX_TIMEOUTS) && + ((int32_t) ( + pf_current_time - + net->scheduler_timeouts[net->scheduler_timeout_first].when) >= 0)) + { + /* Unlink from busy list */ + ix = net->scheduler_timeout_first; + pf_scheduler_unlink (net, &net->scheduler_timeout_first, ix); + + ftn = net->scheduler_timeouts[ix].cb; + arg = net->scheduler_timeouts[ix].arg; + + /* Insert into free list. */ + net->scheduler_timeouts[ix].in_use = false; + pf_scheduler_link_before ( + net, + &net->scheduler_timeout_free, + ix, + net->scheduler_timeout_free); + + /* Send event without holding the mutex. */ + os_mutex_unlock (net->scheduler_timeout_mutex); + ftn (net, arg, pf_current_time); + os_mutex_lock (net->scheduler_timeout_mutex); + } + + os_mutex_unlock (net->scheduler_timeout_mutex); +} + +void pf_scheduler_show (pnet_t * net) +{ + uint32_t ix; + + printf ( + "Scheduler (time now=%u microseconds):\n", + (unsigned)os_get_current_time_us()); + + if (net->scheduler_timeout_mutex != NULL) + { + os_mutex_lock (net->scheduler_timeout_mutex); + } + + printf ( + "%-4s %-14s %-6s %-6s %-6s %s\n", + "idx", + "owner", + "in_use", + "next", + "prev", + "when"); + for (ix = 0; ix < PF_MAX_TIMEOUTS; ix++) + { + printf ( + "[%02u] %-14s %-6s %-6u %-6u %u\n", + (unsigned)ix, + net->scheduler_timeouts[ix].name, + net->scheduler_timeouts[ix].in_use ? "true" : "false", + (unsigned)net->scheduler_timeouts[ix].next, + (unsigned)net->scheduler_timeouts[ix].prev, + (unsigned)net->scheduler_timeouts[ix].when); + } + + if (net->scheduler_timeout_mutex != NULL) + { + printf ("Free list:\n"); + ix = net->scheduler_timeout_free; + while (ix < PF_MAX_TIMEOUTS) + { + printf ("%u ", (unsigned)ix); + ix = net->scheduler_timeouts[ix].next; + } + + printf ("\nBusy list:\n"); + ix = net->scheduler_timeout_first; + while (ix < PF_MAX_TIMEOUTS) + { + printf ( + "%u (%u) ", + (unsigned)ix, + (unsigned)net->scheduler_timeouts[ix].when); + ix = net->scheduler_timeouts[ix].next; + } + + os_mutex_unlock (net->scheduler_timeout_mutex); + } + printf ("\n"); + printf ( + "Uptime (in quanta of 10 ms): %" PRIu32 " \n", + pnal_get_system_uptime_10ms()); +} + +/** + * @internal + * Sanitize the delay to use with the scheduler, by taking the stack cycle time + * into account. + * + * If the requested delay is in the range 1.5 to 2.5 stack cycle times, this + * function will return a calculated delay giving a periodicity of 2 stack cycle + * times. If the requested time is less than 1.5 stack cycle times, the + * resulting periodicity is 1 stack cycle time. + * + * Number of stack cycle times to wait + * + * ^ + * | + * 4 + +-------- + * | | + * | | + * 3 + +-----------+ + * | | + * | | + * 2 + +-----------+ + * | | + * | | + * 1 +-----------------+ + * | + * | Wanted delay + * 0 +-----------+-----------+-----------+-----------+---> (in stack cycle + * times) + * + * 0 1 2 3 4 + * + * Scheduling a delay close to a multiple of the stack cycle time is risky, and + * should be avoided. These measurements were made using a Ubuntu Laptop: + * + * With a stack cycle time of 1 ms, a scheduled delay of 0-700 microseconds + * will cause a nice periodicity of 1 ms. A scheduled delay of 1000 + * microseconds will sometimes fire at next cycle, sometimes not. This gives + * an event spacing of 1 or 2 ms. + * + * Similarly a scheduled delay of 1100 to 1700 microseconds causes + * a nice periodicity of 2 ms, and a scheduled delay of 2100 to 2700 + * microseconds causes a nice periodicity of 3 ms. + * + * Thus calculate the number of stack cycles to wait, and calculate a delay + * corresponding to half a cycle less + * (by setting schedule_half_tick_in_advance = true). Some operating systems + * do not require this. + * + * This function calculates the delay time required to make the + * scheduler fire at a specific stack tick. However the time jitter in the + * firing is largely dependent on the underlying operating system's ablitity to + * trigger the stack execution with a high time precision. + * + * @param wanted_delay In: Delay in microseconds. + * @param stack_cycle_time In: Stack cycle time in + * microseconds. Must be larger than 0. + * @param schedule_half_tick_in_advance In: Schedule event slightly + * earlier, to not be missed. + * @return Number of microseconds of delay to use with the scheduler. + */ +uint32_t pf_scheduler_sanitize_delay ( + uint32_t wanted_delay, + uint32_t stack_cycle_time, + bool schedule_half_tick_in_advance) +{ + uint32_t number_of_stack_ticks = 1; /* We must wait at least one tick */ + uint32_t resulting_delay = 0; + + CC_ASSERT (stack_cycle_time > 0); + + /* Protect against "negative" or unreasonable values. + This might happen when subtracting two timestamps, if a deadline has been + missed. */ + if (wanted_delay > PF_SCHEDULER_MAX_DELAY_US) + { + wanted_delay = 0; + } + + /* Calculate integer number of ticks to wait. Use integer division for + * truncation */ + if (wanted_delay > (stack_cycle_time + stack_cycle_time / 2)) + { + number_of_stack_ticks = + (wanted_delay + (stack_cycle_time / 2)) / stack_cycle_time; + } + CC_ASSERT (number_of_stack_ticks >= 1); + CC_ASSERT (number_of_stack_ticks < 0x80000000); /* No rollover to 'negative' + numbers */ + + /* Calculate delay value for the sheduler */ + resulting_delay = number_of_stack_ticks * stack_cycle_time; + if (schedule_half_tick_in_advance == true) + { + resulting_delay -= (stack_cycle_time / 2); + } + + return resulting_delay; +} diff --git a/profinet_stack/p-net/src/common/pf_scheduler.h b/profinet_stack/p-net/src/common/pf_scheduler.h new file mode 100644 index 0000000..f956dfc --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_scheduler.h @@ -0,0 +1,181 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_SCHEDULER_H +#define PF_SCHEDULER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define PF_SCHEDULER_MAX_DELAY_US 100000000U /* 100 seconds */ + +/** + * Initialize the scheduler. + * @param net InOut: The p-net stack instance + * @param tick_interval In: System calls the tick function at these + * intervals, in microseconds. Must be + * larger than 0. + */ +void pf_scheduler_init (pnet_t * net, uint32_t tick_interval); + +/** + * Initialize a timeout handle. + * @param handle Out: Timeout handle. + * @param name In: Descriptive name for debugging. Not NULL. + */ +void pf_scheduler_init_handle ( + pf_scheduler_handle_t * handle, + const char * name); + +/** + * Reset the value inside the timeout handle to indicate that it's not running. + * + * May be used inside the callback (which is executed by the scheduler). + * + * To unschedule a callback, use \a pf_scheduler_remove() or + * \a pf_scheduler_remove_if_running() instead. + * + * @param handle Out: Timeout handle. + */ +void pf_scheduler_reset_handle (pf_scheduler_handle_t * handle); + +/** + * Report whether the timeout is running (=scheduled) + * + * @param handle InOut: Timeout handle. + * @return true if it is running + * false otherwise + */ +bool pf_scheduler_is_running (const pf_scheduler_handle_t * handle); + +/** + * Report the internal value of a timeout handle. + * + * Intended for debugging and testing. + * + * The value is an index into the timers, and is 0 < x <= PF_MAX_TIMEOUTS when + * the timer is running. The value is UINT32_MAX when it is stopped. + * + * @param handle InOut: Timeout handle. + * @return the internal value (related to index in array of timeouts) + */ +uint32_t pf_scheduler_get_value (const pf_scheduler_handle_t * handle); + +/** + * Report the name of a timeout handle. + * + * Intended for debugging and testing. + * + * @param handle InOut: Timeout handle. + * @return the name given at initialization. + */ +const char * pf_scheduler_get_name (const pf_scheduler_handle_t * handle); + +/** + * Schedule a call-back at a specific time. + * + * The callback must update the internal state stored in the handle. That is + * done by calling \a pf_scheduler_reset_handle() or again calling + * \ pf_scheduler_add(). + * + * @param net InOut: The p-net stack instance + * @param delay In: The delay until the function shall be called, + * in microseconds. Max + * PF_SCHEDULER_MAX_DELAY_US. + * @param cb In: The call-back. + * @param arg In: Argument to the call-back. + * @param handle InOut: Timeout handle. + * @return 0 if the call-back was scheduled. + * -1 if an error occurred. + */ +int pf_scheduler_add ( + pnet_t * net, + uint32_t delay, + pf_scheduler_timeout_ftn_t cb, + void * arg, + pf_scheduler_handle_t * handle); + +/** + * Re-schedule a call-back at a specific time. + * + * Removes the already scheduled instance, if existing. + * + * Do not use in a scheduler callback. + * + * @param net InOut: The p-net stack instance + * @param delay In: The delay until the function shall be called, + * in microseconds. Max + * PF_SCHEDULER_MAX_DELAY_US. + * @param cb In: The call-back. + * @param arg In: Argument to the call-back. + * @param handle InOut: Timeout handle. + * @return 0 if the call-back was scheduled. + * -1 if an error occurred. + */ +int pf_scheduler_restart ( + pnet_t * net, + uint32_t delay, + pf_scheduler_timeout_ftn_t cb, + void * arg, + pf_scheduler_handle_t * handle); + +/** + * Stop a timeout, if it is running. + * + * Silently ignoring the timeout if not running. + * + * @param net InOut: The p-net stack instance + * @param handle InOut: Timeout handle. + */ +void pf_scheduler_remove_if_running ( + pnet_t * net, + pf_scheduler_handle_t * handle); + +/** + * Stop a timeout. + * @param net InOut: The p-net stack instance + * @param handle InOut: Timeout handle. + */ +void pf_scheduler_remove (pnet_t * net, pf_scheduler_handle_t * handle); + +/** + * Check if it is time to call a scheduled call-back. + * Run scheduled call-backs - if any. + * @param net InOut: The p-net stack instance + */ +void pf_scheduler_tick (pnet_t * net); + +/** + * Show scheduler (busy and free) instances. + * + * Locks the mutex temporarily. + * + * @param net InOut: The p-net stack instance + */ +void pf_scheduler_show (pnet_t * net); + +/************ Internal functions, made available for unit testing ************/ + +uint32_t pf_scheduler_sanitize_delay ( + uint32_t wanted_delay, + uint32_t stack_cycle_time, + bool schedule_half_tick_in_advance); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_SCHEDULER_H */ diff --git a/profinet_stack/p-net/src/common/pf_snmp.c b/profinet_stack/p-net/src/common/pf_snmp.c new file mode 100644 index 0000000..9682260 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_snmp.c @@ -0,0 +1,698 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Helper functions for use by platform-dependent SNMP server (agent). + * + * Some SNMP related data needs to be stored to file between invocations. + * We read from file only at startup, but writing to file might happen any time. + * + */ + +#ifdef UNIT_TEST +#define pf_lldp_get_management_address mock_pf_lldp_get_management_address +#define pf_lldp_get_peer_management_address \ + mock_pf_lldp_get_peer_management_address +#define pf_lldp_get_link_status mock_pf_lldp_get_link_status +#define pf_lldp_get_peer_link_status mock_pf_lldp_get_peer_link_status +#define pf_file_load mock_pf_file_load +#define pf_file_save_if_modified mock_pf_file_save_if_modified +#define pf_fspm_get_im_location mock_pf_fspm_get_im_location +#define pf_fspm_save_im_location mock_pf_fspm_save_im_location +#define pnal_get_hostname mock_pnal_get_hostname +#endif + +#include "pf_includes.h" +#include + +#define STRINGIFY(s) STRINGIFIED (s) +#define STRINGIFIED(s) #s + +/* The configurable constant PNET_MAX_FILENAME_SIZE should be at least + * as large as the longest filename used, including termination. + */ +CC_STATIC_ASSERT ( + PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_SNMP_SYSCONTACT)); +CC_STATIC_ASSERT (PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_SNMP_SYSNAME)); +CC_STATIC_ASSERT ( + PNET_MAX_FILENAME_SIZE >= sizeof (PF_FILENAME_SNMP_SYSLOCATION)); + +/** + * Log result of variable load operation + * + * @param error In: Error code returned from pf_file_load(). + * @param variable_name In: Name of variable loaded from file. + * @param variable_value In: Value of variable loaded from file, in + * case no error was detected. + */ +static void pf_snmp_log_loaded_variable ( + int error, + const char * variable_name, + const char * variable_value) +{ + if (error) + { + LOG_INFO ( + PF_SNMP_LOG, + "SNMP(%d): Could not yet read %s from nvm. Using \"%s\"\n", + __LINE__, + variable_name, + variable_value); + } + else + { + LOG_INFO ( + PF_SNMP_LOG, + "SNMP(%d): Did read %s from nvm. %s: %s\n", + __LINE__, + variable_name, + variable_name, + variable_value); + } +} + +/** + * Log result of variable save operation + * + * @param result In: Error code returned from + * pf_file_save_if_modified(). + * @param variable_name In: Name of variable stored to file. + * @param variable_value In: Value of variable save to file. + */ +static void pf_snmp_log_saved_variable ( + int result, + const char * variable_name, + const char * variable_value) +{ + switch (result) + { + case 2: + LOG_INFO ( + PF_SNMP_LOG, + "SNMP(%d): First nvm saving of %s. %s: %s\n", + __LINE__, + variable_name, + variable_name, + variable_value); + break; + case 1: + LOG_INFO ( + PF_SNMP_LOG, + "SNMP(%d): Updating nvm stored %s. %s: %s\n", + __LINE__, + variable_name, + variable_name, + variable_value); + break; + case 0: + LOG_INFO ( + PF_SNMP_LOG, + "SNMP(%d): No storing of nvm %s (no changes). %s: %s\n", + __LINE__, + variable_name, + variable_name, + variable_value); + break; + case -1: + /* Fall-through */ + default: + LOG_ERROR ( + PF_SNMP_LOG, + "SNMP(%d): Failed to store nvm %s.\n", + __LINE__, + variable_name); + break; + } +} + +/** + * Reverse bits in byte. + * + * @param byte In: Byte with bit pattern to reverse. + * @return Bit pattern in reversed order. + */ +static uint8_t pf_snmp_bit_reversed_byte (const uint8_t byte) +{ + uint8_t reversed = 0x00; + + reversed |= (byte & BIT (7)) >> 7; + reversed |= (byte & BIT (0)) << 7; + + reversed |= (byte & BIT (6)) >> 5; + reversed |= (byte & BIT (1)) << 5; + + reversed |= (byte & BIT (5)) >> 3; + reversed |= (byte & BIT (2)) << 3; + + reversed |= (byte & BIT (4)) >> 1; + reversed |= (byte & BIT (3)) << 1; + + return reversed; +} + +/** + * Encode link status for use in SNMP response. + * + * @param encoded Out: Encoded link status. + * @param plain In: Unencoded link status. + */ +static void pf_snmp_encode_link_status ( + pf_snmp_link_status_t * encoded, + const pf_lldp_link_status_t * plain) +{ + /* See RFC 2579 "Textual Conventions for SMIv2": "TruthValue" */ + encoded->auto_neg_supported = plain->is_autonegotiation_supported ? 1 : 2; + encoded->auto_neg_enabled = plain->is_autonegotiation_enabled ? 1 : 2; + + encoded->oper_mau_type = plain->operational_mau_type; + + /* See RFC 1906 "Transport Mappings for Version 2 of the + * Simple Network Management Protocol (SNMPv2)", ch. 8: + * "Serialization using the Basic Encoding Rules", clause 3: + * "When encoding an object whose syntax is described using the BITS + * construct, the value is encoded as an OCTET STRING, in which all + * the named bits in (the definition of) the bitstring, commencing + * with the first bit and proceeding to the last bit, are placed in + * bits 8 to 1 of the first octet, followed by bits 8 to 1 of each + * subsequent octet in turn, followed by as many bits as are needed of + * the final subsequent octet, commencing with bit 8. Remaining bits, + * if any, of the final octet are set to zero on generation and + * ignored on receipt." + */ + encoded->auto_neg_advertised_cap[0] = pf_snmp_bit_reversed_byte ( + (plain->autonegotiation_advertised_capabilities >> 0) & 0xff); + encoded->auto_neg_advertised_cap[1] = pf_snmp_bit_reversed_byte ( + (plain->autonegotiation_advertised_capabilities >> 8) & 0xff); +} + +/** + * Encode management address for use in SNMP response. + * + * @param encoded Out: Encoded management address. + * @param plain In: Unencoded management address. + */ +static void pf_snmp_encode_management_address ( + pf_snmp_management_address_t * encoded, + const pf_lldp_management_address_t * plain) +{ + encoded->subtype = plain->subtype; + + /* See RFC 2578 "Structure of Management Information Version 2 (SMIv2)", + * section 7.7, clause 3: + * "string-valued, variable-length strings (not preceded by the IMPLIED + * keyword): `n+1' sub-identifiers, where `n' is the length of the + * string (the first sub-identifier is `n' itself, following this, + * each octet of the string is encoded in a separate sub-identifier);"" + */ + encoded->len = plain->len + 1; + encoded->value[0] = plain->len; + memcpy (&encoded->value[1], plain->value, plain->len); +} + +/** + * Encode signal delays for use in SNMP response. + * + * @param encoded Out: Encoded signal delays. + * @param plain In: Unencoded signal delays. + */ +static void pf_snmp_encode_signal_delays ( + pf_snmp_signal_delay_t * encoded, + const pf_lldp_signal_delay_t * plain) +{ + encoded->port_tx_delay_ns = plain->tx_delay_local; + encoded->port_rx_delay_ns = plain->rx_delay_local; + encoded->line_propagation_delay_ns = plain->cable_delay_local; +} + +void pf_snmp_data_init (pnet_t * net) +{ + const char * directory = pf_cmina_get_file_directory (net); + pf_snmp_data_t * snmp = &net->snmp_data; + int error; + int result; + + CC_STATIC_ASSERT ( + sizeof (snmp->system_name.string) >= PNAL_HOSTNAME_MAX_SIZE); + + /* sysContact */ + error = pf_file_load ( + directory, + PF_FILENAME_SNMP_SYSCONTACT, + &snmp->system_contact, + sizeof (snmp->system_contact)); + if (error) + { + snmp->system_contact.string[0] = '\0'; + } + snmp->system_contact.string[sizeof (snmp->system_contact.string) - 1] = '\0'; + pf_snmp_log_loaded_variable ( + error, + "sysContact", + snmp->system_contact.string); + + /* sysName */ + error = pf_file_load ( + directory, + PF_FILENAME_SNMP_SYSNAME, + &snmp->system_name, + sizeof (snmp->system_name)); + if (error) + { + result = pnal_get_hostname (snmp->system_name.string); + if (result != 0) + { + snmp->system_name.string[0] = '\0'; + } + } + snmp->system_name.string[sizeof (snmp->system_name.string) - 1] = '\0'; + pf_snmp_log_loaded_variable (error, "sysName", snmp->system_name.string); + + /* sysLocation */ + error = pf_file_load ( + directory, + PF_FILENAME_SNMP_SYSLOCATION, + &snmp->system_location, + sizeof (snmp->system_location)); + if (error) + { + /* Use "IM_Tag_Location" from I&M1 */ + pf_fspm_get_im_location (net, snmp->system_location.string); + } + snmp->system_location.string[sizeof (snmp->system_location.string) - 1] = + '\0'; + pf_snmp_log_loaded_variable ( + error, + "sysLocation", + snmp->system_location.string); +} + +void pf_snmp_remove_data_files (const char * file_directory) +{ + pf_file_clear (file_directory, PF_FILENAME_SNMP_SYSCONTACT); + pf_file_clear (file_directory, PF_FILENAME_SNMP_SYSNAME); + pf_file_clear (file_directory, PF_FILENAME_SNMP_SYSLOCATION); +} + +void pf_snmp_data_clear (pnet_t * net) +{ + const char * p_file_directory = pf_cmina_get_file_directory (net); + pf_snmp_data_t * snmp = &net->snmp_data; + + LOG_DEBUG (PF_SNMP_LOG, "SNMP(%d): Clearing SNMP data.\n", __LINE__); + pf_snmp_remove_data_files (p_file_directory); + + memset ( + snmp->system_contact.string, + '\0', + sizeof (snmp->system_contact.string)); + memset (snmp->system_name.string, '\0', sizeof (snmp->system_name.string)); + memset ( + snmp->system_location.string, + '\0', + sizeof (snmp->system_location.string)); +} + +void pf_snmp_fspm_im_location_ind (pnet_t * net) +{ + const char * p_file_directory = pf_cmina_get_file_directory (net); + pf_snmp_data_t * snmp = &net->snmp_data; + + /* Location data is stored in two different files: the file with + * I&M data and a file used by SNMP containing a larger version + * of the device's location. The larger version has precedence + * over the I&M version, so we need to delete the larger one */ + pf_file_clear (p_file_directory, PF_FILENAME_SNMP_SYSLOCATION); + + /* Use "IM_Tag_Location" from I&M1 */ + pf_fspm_get_im_location (net, snmp->system_location.string); + snmp->system_location.string[sizeof (snmp->system_location.string) - 1] = + '\0'; + + LOG_DEBUG ( + PF_SNMP_LOG, + "SNMP(%d): I&M1 location has been updated, set SNMP SysLocation to %s.\n", + __LINE__, + snmp->system_location.string); +} + +void pf_snmp_get_system_name (pnet_t * net, pf_snmp_system_name_t * name) +{ + const pf_snmp_data_t * snmp = &net->snmp_data; + + snprintf ( + name->string, + sizeof (name->string), + "%s", + snmp->system_name.string); +} + +int pf_snmp_set_system_name (pnet_t * net, const pf_snmp_system_name_t * name) +{ + const char * directory = pf_cmina_get_file_directory (net); + pf_snmp_data_t * snmp = &net->snmp_data; + pf_snmp_system_name_t temporary_buffer; + int res; + + /* Ideally, we should set the Fully Qualified Domain Name here. However, not + * all systems are expected to support doing that, so we don't. + * Instead, we just save the sysName to file as to ensure it is persistent + * across restarts. + */ + + snprintf ( + snmp->system_name.string, + sizeof (snmp->system_name.string), + "%s", + name->string); + + res = pf_file_save_if_modified ( + directory, + PF_FILENAME_SNMP_SYSNAME, + name, + &temporary_buffer, + sizeof (*name)); + pf_snmp_log_saved_variable (res, "sysName", name->string); + + return res == -1 ? -1 : 0; +} + +void pf_snmp_get_system_contact ( + pnet_t * net, + pf_snmp_system_contact_t * contact) +{ + const pf_snmp_data_t * snmp = &net->snmp_data; + + snprintf ( + contact->string, + sizeof (contact->string), + "%s", + snmp->system_contact.string); +} + +int pf_snmp_set_system_contact ( + pnet_t * net, + const pf_snmp_system_contact_t * contact) +{ + const char * directory = pf_cmina_get_file_directory (net); + pf_snmp_data_t * snmp = &net->snmp_data; + pf_snmp_system_contact_t temporary_buffer; + int res; + + snprintf ( + snmp->system_contact.string, + sizeof (snmp->system_contact.string), + "%s", + contact->string); + + res = pf_file_save_if_modified ( + directory, + PF_FILENAME_SNMP_SYSCONTACT, + contact, + &temporary_buffer, + sizeof (*contact)); + pf_snmp_log_saved_variable (res, "sysContact", contact->string); + + return res == -1 ? -1 : 0; +} + +void pf_snmp_get_system_location ( + pnet_t * net, + pf_snmp_system_location_t * location) +{ + const pf_snmp_data_t * snmp = &net->snmp_data; + + snprintf ( + location->string, + sizeof (location->string), + "%s", + snmp->system_location.string); +} + +int pf_snmp_set_system_location ( + pnet_t * net, + const pf_snmp_system_location_t * location) +{ + const char * directory = pf_cmina_get_file_directory (net); + pf_snmp_data_t * snmp = &net->snmp_data; + pf_snmp_system_location_t temporary_buffer; + int res; + + snprintf ( + snmp->system_location.string, + sizeof (snmp->system_location.string), + "%s", + location->string); + + res = pf_file_save_if_modified ( + directory, + PF_FILENAME_SNMP_SYSLOCATION, + location, + &temporary_buffer, + sizeof (*location)); + pf_snmp_log_saved_variable (res, "sysLocation", location->string); + + /* Update 22 byte "IM_Tag_Location" in I&M1 */ + pf_fspm_save_im_location (net, location->string); + + return res == -1 ? -1 : 0; +} + +void pf_snmp_get_system_description ( + pnet_t * net, + pf_snmp_system_description_t * description) +{ + (void)pf_lldp_get_system_description ( + net, + description->string, + sizeof (description->string)); +} + +void pf_snmp_get_port_list (pnet_t * net, pf_lldp_port_list_t * p_list) +{ + pf_port_get_list_of_ports (net, p_list); +} + +void pf_snmp_init_port_iterator (pnet_t * net, pf_port_iterator_t * p_iterator) +{ + pf_port_init_iterator_over_ports (net, p_iterator); +} + +int pf_snmp_get_next_port (pf_port_iterator_t * p_iterator) +{ + return pf_port_get_next (p_iterator); +} + +int pf_snmp_get_peer_timestamp ( + pnet_t * net, + int loc_port_num, + uint32_t * timestamp_10ms) +{ + return pf_lldp_get_peer_timestamp (net, loc_port_num, timestamp_10ms); +} + +void pf_snmp_get_chassis_id (pnet_t * net, pf_lldp_chassis_id_t * p_chassis_id) +{ + pf_lldp_get_chassis_id (net, p_chassis_id); +} + +int pf_snmp_get_peer_chassis_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_chassis_id_t * p_chassis_id) +{ + return pf_lldp_get_peer_chassis_id (net, loc_port_num, p_chassis_id); +} + +void pf_snmp_get_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id) +{ + pf_lldp_get_port_id (net, loc_port_num, p_port_id); +} + +int pf_snmp_get_peer_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id) +{ + return pf_lldp_get_peer_port_id (net, loc_port_num, p_port_id); +} + +void pf_snmp_get_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_descr) +{ + pf_lldp_get_port_description (net, loc_port_num, p_port_descr); +} + +int pf_snmp_get_peer_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_desc) +{ + return pf_lldp_get_peer_port_description (net, loc_port_num, p_port_desc); +} + +void pf_snmp_get_management_address ( + pnet_t * net, + pf_snmp_management_address_t * p_man_address) +{ + pf_lldp_management_address_t man_address; + + pf_lldp_get_management_address (net, &man_address); + pf_snmp_encode_management_address (p_man_address, &man_address); +} + +int pf_snmp_get_peer_management_address ( + pnet_t * net, + int loc_port_num, + pf_snmp_management_address_t * p_man_address) +{ + pf_lldp_management_address_t man_address; + int error; + + error = + pf_lldp_get_peer_management_address (net, loc_port_num, &man_address); + if (error == 0) + { + pf_snmp_encode_management_address (p_man_address, &man_address); + } + + return error; +} + +void pf_snmp_get_management_port_index ( + pnet_t * net, + pf_lldp_interface_number_t * p_man_port_index) +{ + pf_lldp_management_address_t man_address; + + pf_lldp_get_management_address (net, &man_address); + *p_man_port_index = man_address.interface_number; +} + +int pf_snmp_get_peer_management_port_index ( + pnet_t * net, + int loc_port_num, + pf_lldp_interface_number_t * p_man_port_index) +{ + int error; + pf_lldp_management_address_t man_address; + + error = + pf_lldp_get_peer_management_address (net, loc_port_num, &man_address); + if (error == 0) + { + *p_man_port_index = man_address.interface_number; + } + + return error; +} + +void pf_snmp_get_station_name ( + pnet_t * net, + pf_lldp_station_name_t * p_station_name) +{ + char station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated */ + + /* FIXME: Use of pf_cmina_get_station_name() is not thread-safe. + * Fix this, e.g. using a mutex. + */ + pf_cmina_get_station_name (net, station_name); + + snprintf ( + p_station_name->string, + sizeof (p_station_name->string), + "%s", + station_name); + p_station_name->len = strlen (p_station_name->string); +} + +int pf_snmp_get_peer_station_name ( + pnet_t * net, + int loc_port_num, + pf_lldp_station_name_t * p_station_name) +{ + return pf_lldp_get_peer_station_name (net, loc_port_num, p_station_name); +} + +void pf_snmp_get_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_snmp_signal_delay_t * p_delays) +{ + pf_lldp_signal_delay_t delays; + + pf_lldp_get_signal_delays (net, loc_port_num, &delays); + pf_snmp_encode_signal_delays (p_delays, &delays); +} + +int pf_snmp_get_peer_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_snmp_signal_delay_t * p_delays) +{ + pf_lldp_signal_delay_t delays; + int error; + + error = pf_lldp_get_peer_signal_delays (net, loc_port_num, &delays); + if (error == 0) + { + pf_snmp_encode_signal_delays (p_delays, &delays); + } + + return error; +} + +void pf_snmp_get_link_status ( + pnet_t * net, + int loc_port_num, + pf_snmp_link_status_t * p_link_status) +{ + pf_lldp_link_status_t link_status; + + pf_lldp_get_link_status (net, loc_port_num, &link_status); + pf_snmp_encode_link_status (p_link_status, &link_status); +} + +int pf_snmp_get_peer_link_status ( + pnet_t * net, + int loc_port_num, + pf_snmp_link_status_t * p_link_status) +{ + pf_lldp_link_status_t link_status; + int error; + + error = pf_lldp_get_peer_link_status (net, loc_port_num, &link_status); + if (error == 0) + { + pf_snmp_encode_link_status (p_link_status, &link_status); + } + + return error; +} + +void pf_snmp_show (pnet_t * net) +{ + pf_snmp_data_t * snmp = &net->snmp_data; + + printf ("\nSNMP\n"); + printf (" SysName : \"%s\"\n", snmp->system_name.string); + printf (" SysLocation : \"%s\"\n", snmp->system_location.string); + printf (" SysContact : \"%s\"\n", snmp->system_contact.string); +} diff --git a/profinet_stack/p-net/src/common/pf_snmp.h b/profinet_stack/p-net/src/common/pf_snmp.h new file mode 100644 index 0000000..94d0caf --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_snmp.h @@ -0,0 +1,887 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Helper functions for use by platform-dependent SNMP server (agent). + * + * The p-net stack does not contain any SNMP server stack. Instead, it + * relies on external software to supply the needed SNMP services. In + * the p-net abstraction layer (pnal), the file pnal_snmp.c is + * expected to implement the glue code needed make the external SNMP + * server communicate with the p-net stack (in particular, to get + * variables stored in the p-net stack). + * + * The purposes of this header file is to make it easier to implement + * the glue code in pnal_snmp.c by declaring all needed functions in a + * single header file. This API should not be used by other modules + * than the SNMP server. + * + * The API assumes the following model: + * The Profinet device has a single interface (IP address), but may have + * multiple ports. Each port may be connected to 1 or 0 remote network + * entities, where a network entity may be a Profinet Device/Controller/ + * Supervisor or an LLDP-aware network switch. + * + * The SNMP variables are declared in the following MIBs: + * - MIB-II. See IETF RFC 3418 (SNMP MIB). + * - LLDP-MIB. See IEEE 802.1AB-2005 (LLDPv1). + * - LLDP-EXT-DOT3-MIB. See IEEE 802.1AB-2005 (LLDPv1). + * - LLDP-EXT-PNO-MIB. See IEC CDV 61158-6-10 (PN-AL-Protocol). + * The Profinet guideline PN-Topology describes usage of SNMP (see ch. 6.2). + * + * Getter functions for the following variables for the local device, interface + * and ports are provided: + * - sysName + * - sysContact + * - sysLocation + * - sysDescr + * - lldpLocChassisId + * - lldpLocChassisIdSubtype + * - lldpLocPortNum/lldpRemLocalPortNum + * - lldpLocPortId + * - lldpLocPortIdSubtype + * - lldpLocPortDesc + * - lldpLocManAddr + * - lldpLocManAddrSubtype + * - lldpLocManAddrIfId + * - lldpLocManAddrIfSubtype + * - lldpConfigManAddrPortsTxEnable + * - lldpXPnoLocPortNoS + * - lldpXPnoLocLPDValue + * - lldpXPnoLocPortTxDValue + * - lldpXPnoLocPortRxDValue + * - lldpXdot3LocPortAutoNegSupported + * - lldpXdot3LocPortAutoNegEnabled + * - lldpXdot3LocPortAutoNegAdvertisedCap + * - lldpXdot3LocPortOperMauType + * + * Setter functions for the following variables for the local device are + * provided: + * - sysName + * - sysContact + * - sysLocation + * + * Getter functions for the following variables for the remote devices, + * interfaces and ports are provided: + * - lldpRemChassisId + * - lldpRemChassisIdSubtype + * - lldpRemPortId + * - lldpRemPortIdSubtype + * - lldpRemPortDesc + * - lldpRemManAddr + * - lldpRemManAddrSubtype + * - lldpRemManAddrIfId + * - lldpRemManAddrIfIdSubtype + * - lldpRemTimeMark + * - lldpXPnoRemPortNoS + * - lldpXPnoRemLPDValue + * - lldpXPnoRemPortTxDValue + * - lldpXPnoRemPortRxDValue + * - lldpXdot3RemPortAutoNegSupported + * - lldpXdot3RemPortAutoNegEnabled + * - lldpXdot3RemPortAutoNegAdvertisedCap + * - lldpXdot3RemPortOperMauType + * + * All functions are thread-safe. + */ + +#ifndef PF_SNMP_H +#define PF_SNMP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pf_lldp.h" + +/* SNMP-related filenames used by p-net stack. A filename may not be longer + * than PNET_MAX_FILENAME_SIZE (termination included). + */ +#define PF_FILENAME_SNMP_SYSCONTACT "pnet_data_syscontact.bin" +#define PF_FILENAME_SNMP_SYSNAME "pnet_data_sysname.bin" +#define PF_FILENAME_SNMP_SYSLOCATION "pnet_data_syslocation.bin" + +/** + * System name (sysName). + * + * "An administratively-assigned name for this managed + * node. By convention, this is the node's fully-qualified + * domain name. If the name is unknown, the value is + * the zero-length string." + * - IETF RFC 3418 (SNMP MIB-II). + * + * Is the same as LLDP's "System Name". + * See PN-Topology Annex A. + * + * Note: According to the SNMP specification, the string could be up + * to 255 characters (excluding termination). The p-net stack limits it to + * PNAL_HOSTNAME_MAX_SIZE, including null-termination. + * + * This is a writable variable. As such it is be in persistent memory. + * Only writable variables (using SNMP Set) need to be stored + * in persistent memory. + * See IEC CDV 61158-5-10 (PN-AL-Services) ch. 7.3.3.3.6.2: "Persistency". + */ +typedef struct pf_snmp_system_name +{ + char string[PNAL_HOSTNAME_MAX_SIZE]; /* Terminated string */ +} pf_snmp_system_name_t; + +/** + * System contact (sysContact). + * + * "The textual identification of the contact person + * for this managed node, together with information + * on how to contact this person. If no contact information is + * known, the value is the zero-length string." + * - IETF RFC 3418 (SNMP MIB-II). + * + * The value is supplied by network manager. By default, it is + * the zero-length string. + * + * An extra byte is added as to ensure null-termination. + * + * This is a writable variable. As such, it is stored in persistent memory. + * Only writable variables (using SNMP Set) need to be stored + * in persistent memory. + * See IEC CDV 61158-5-10 (PN-AL-Services) ch. 7.3.3.3.6.2: "Persistency". + */ +typedef struct pf_snmp_system_contact +{ + char string[255 + 1]; /* Terminated */ +} pf_snmp_system_contact_t; + +/** + * System description (sysDescr). + * + * "A textual description of the entity. This value + * should include the full name and version + * identification of the system's hardware type, + * software operating-system, and networking + * software. It is mandatory that this only contain + * printable ASCII characters." + * - IETF RFC 3418 (SNMP MIB-II) ch. 6 "Definitions". + * + * Note that MIB-II's sysDescr should have the same value as LLDP's + * lldpLocSysDesc and (preferably) as Profinet's SystemIdentification. + * The Chassis ID may also be encoded as SystemIdentification. + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2 "LLDP MIB module" and + * IEC CDV 61158-6-10 (PN-AL-Protocol) ch. 4.16.4 "IETF RFC 1213-MIB" and + * ch. 4.11.3.18.1 "Coding of the field LLDP_ChassisID". + * + * Note: + * An extra byte is included as to ensure null-termination. + */ +typedef struct pf_snmp_system_description +{ + char string[PNET_LLDP_CHASSIS_ID_MAX_SIZE]; /** Terminated string */ +} pf_snmp_system_description_t; + +/** + * System location (sysLocation). + * + * "The physical location of this node (e.g., + * 'telephone closet, 3rd floor'). If the location is unknown, + * the value is the zero-length string." + * - IETF RFC 3418 (SNMP MIB-II). + * + * The value is supplied by network manager. By default, it is a string with + * 22 space (' ') characters. + * + * The first 22 bytes should have the same value as "IM_Tag_Location" in I&M1. + * See PN-Topology ch. 11.5.2: "Consistency". + * + * This is a writable variable. As such, it is stored in persistent memory. + * Only writable variables (using SNMP Set) need to be stored + * in persistent memory. + * See IEC CDV 61158-5-10 (PN-AL-Services) ch. 7.3.3.3.6.2: "Persistency". + */ +typedef struct pf_snmp_system_location +{ + char string[255 + 1]; /* Terminated string */ +} pf_snmp_system_location_t; + +/** + * Encoded management address. + * + * Contains similar information as pf_lldp_management_address_t, but the + * fields have been encoded so they may be immediately placed in SNMP response. + */ +typedef struct pf_snmp_management_address +{ + /** First byte is size of actual address */ + uint8_t value[1 + 31]; + + /** 1 for IPv4 */ + uint8_t subtype; + + /** 5 for IPv4 */ + size_t len; +} pf_snmp_management_address_t; + +/** + * Encoded link status. + * + * Contains the same information as pf_lldp_link_status_t, but the fields + * have been encoded so they may be immediately placed in SNMP response. + */ +typedef struct pf_snmp_link_status +{ + /** 1 if true, 2 if false */ + int32_t auto_neg_supported; + + /** 1 if true, 2 if false */ + int32_t auto_neg_enabled; + + /** OCTET STRING encoding of BITS */ + uint8_t auto_neg_advertised_cap[2]; + + int32_t oper_mau_type; +} pf_snmp_link_status_t; + +/** + * Measured signal delays in nanoseconds + * + * If a signal delay was not measured, its value is zero. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) Annex U: "LLDP EXT MIB", fields + * lldpXPnoLocLPDValue / lldpXPnoRemLPDValue, + * lldpXPnoLocPortTxDValue / lldpXPnoRemPortTxDValue, + * lldpXPnoLocPortRxDValue / lldpXPnoRemPortRxDValue. + * + * See also pf_lldp_signal_delay_t. + */ +typedef struct pf_snmp_signal_delay +{ + uint32_t port_tx_delay_ns; + uint32_t port_rx_delay_ns; + uint32_t line_propagation_delay_ns; +} pf_snmp_signal_delay_t; + +/** + * Initialize SNMP related data, by reading from file. + * + * @param net InOut: The p-net stack instance. + */ +void pf_snmp_data_init (pnet_t * net); + +/** + * Clear SNMP related data. + * + * Clears in-memory data and calls pf_snmp_remove_data_files(). + * + * Note that it does not clear the IM_Tag_Location" from I&M1 + * (which typically is the same as SNMP SysLocation). + * + * @param net InOut: The p-net stack instance. + */ +void pf_snmp_data_clear (pnet_t * net); + +/** + * Removes SNMP related data files. + * + * Used by pf_snmp_data_clear() and other operations. + * + * @param net InOut: The p-net stack instance. + */ +void pf_snmp_remove_data_files (const char * file_directory); + +/** + * Update SNMP SysLocation with the present I&M1 location value. + * + * Does remove the corresponding file. Upon next startup will we use the + * I&M location value. + * + * @param net InOut: The p-net stack instance. + */ +void pf_snmp_fspm_im_location_ind (pnet_t * net); + +/** + * Get system description. + * + * See IETF RFC 3418 (SNMP MIB-II) ch. 2 "Definitions". Relevant fields: + * - SysDescr. + * + * @param net InOut: The p-net stack instance. + * @param p_description Out: System description. + */ +void pf_snmp_get_system_description ( + pnet_t * net, + pf_snmp_system_description_t * p_description); + +/** + * Get system name. + * + * The value will be loaded from file. + * If no file is available, the hostname is returned. + * + * Note: User may choose to get the fully qualified domain name (FQDN) instead + * of calling this function, assuming the operating system supports it. + * + * See IETF RFC 3418 (SNMP MIB-II) ch. 2 "Definitions". Relevant fields: + * - SysName. + * + * @param net InOut: The p-net stack instance. + * @param p_name Out: System name. + */ +void pf_snmp_get_system_name (pnet_t * net, pf_snmp_system_name_t * p_name); + +/** + * Set system name. + * + * The value will be stored to file. + * The hostname will not be updated. + * + * Note: User may choose to set the fully qualified domain name (FQDN) instead + * of calling this function, assuming the operating system supports it. + * + * See IETF RFC 3418 (SNMP MIB-II) ch. 2 "Definitions". Relevant fields: + * - SysName. + * + * @param net InOut: The p-net stack instance. + * @param p_name In: System name. + * @return 0 if the operation succeeded. + * -1 if an error occurred (could not store to file). + */ +int pf_snmp_set_system_name (pnet_t * net, const pf_snmp_system_name_t * p_name); + +/** + * Get system contact. + * + * The value will be loaded from file. + * If no file is available, an empty string is returned. + * + * See IETF RFC 3418 (SNMP MIB-II) ch. 2 "Definitions". Relevant fields: + * - SysContact. + * + * @param net InOut: The p-net stack instance. + * @param p_contact Out: System contact. + */ +void pf_snmp_get_system_contact ( + pnet_t * net, + pf_snmp_system_contact_t * p_contact); + +/** + * Set system contact. + * + * The value will be stored to file. + * + * See IETF RFC 3418 (SNMP MIB-II) ch. 2 "Definitions". Relevant fields: + * - SysContact. + * + * @param net InOut: The p-net stack instance. + * @param p_contact In: System contact. + * @return 0 if the operation succeeded. + * -1 if an error occurred (could not store to file). + */ +int pf_snmp_set_system_contact ( + pnet_t * net, + const pf_snmp_system_contact_t * p_contact); + +/** + * Get system location. + * + * The value will be loaded from file. If file is not available, + * the device location from I&M1 will be used. + * + * See IETF RFC 3418 (SNMP MIB-II) ch. 2 "Definitions". Relevant fields: + * - SysLocation. + * + * @param net InOut: The p-net stack instance. + * @param p_location Out: System location. + */ +void pf_snmp_get_system_location ( + pnet_t * net, + pf_snmp_system_location_t * p_location); + +/** + * Set system location. + * + * The value will be stored to file. + * The device location in I&M1 will also be replaced with the first + * 22 characters of system location. + * + * See IETF RFC 3418 (SNMP MIB-II) ch. 2 "Definitions". Relevant fields: + * - SysLocation. + * + * @param net InOut: The p-net stack instance. + * @param p_location In: System location. + * @return 0 if the operation succeeded. + * -1 if an error occurred (could not store to file). + */ +int pf_snmp_set_system_location ( + pnet_t * net, + const pf_snmp_system_location_t * p_location); + +/** + * Get list of local ports. + * + * This is the list of physical ports on the local interface. + * The management port is not included. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpConfigManAddrPortsTxEnable. + * + * @param net In: The p-net stack instance. + * @param p_list Out: List of local ports. + */ +void pf_snmp_get_port_list (pnet_t * net, pf_lldp_port_list_t * p_list); + +/** + * Initialise iterator for iterating over local ports. + * + * This iterator may be used for iterating over all physical ports + * on the local interface. The management port is not included. + * See pf_snmp_get_next_port(). + * + * @param net In: The p-net stack instance. + * @param p_iterator Out: Port iterator. + */ +void pf_snmp_init_port_iterator (pnet_t * net, pf_port_iterator_t * p_iterator); + +/** + * Get next local port. + * + * If no more ports are available, 0 is returned. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpLocPortNum, + * - lldpRemLocalPortNum. + * + * @param p_iterator InOut: Port iterator. + * @return Local port number for next port on local interface. + * If no more ports are available, 0 is returned. + */ +int pf_snmp_get_next_port (pf_port_iterator_t * p_iterator); + +/** + * Get time when new information about remote device was received. + * + * Information about the remote device is received in an LLDP packet on a + * local port and relevant information is stored by LLDP stack. + * If an LLDP packet has been received, a timestamp will also be saved. + * This may be used by the SNMP server thread for time-filtering. + * See PN-Topology ch. 6.2.3: "Time-filtered OIDs". + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpRemTimeMark. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_timestamp_10ms Out: Time when the LLDP packet with the info + * was first received, in units of + * 10 milliseconds with system startup + * as the zero point (as in SNMP sysUpTime). + * Note that if an LLDP packet with identical + * info is received, the timestamp is not + * updated. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_timestamp ( + pnet_t * net, + int loc_port_num, + uint32_t * p_timestamp_10ms); + +/** + * Get Chassis ID of local device. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpLocChassisId, + * - lldpLocChassisIdSubtype. + * + * @param net In: The p-net stack instance. + * @param p_chassis_id Out: Chassis ID of local device. + */ +void pf_snmp_get_chassis_id (pnet_t * net, pf_lldp_chassis_id_t * p_chassis_id); + +/** + * Get Chassis ID for remote device connected to local port. + * + * The remote Chassis ID is contained in an LLDP packet sent from a port + * on the remote device to a local port with no intermediate switches. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpRemChassisId, + * - lldpRemChassisIdSubtype, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_chassis_id Out: Chassis ID of remote device. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_chassis_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_chassis_id_t * p_chassis_id); + +/** + * Get Port ID for local port. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpLocPortId, + * - lldpLocPortIdSubtype, + * - lldpLocPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_port_id Out: Port ID of local port. + */ +void pf_snmp_get_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id); + +/** + * Get Port ID for remote port. + * + * The remote Port ID is contained in an LLDP packet sent from a port + * on the remote device to the local port with no intermediate switches. + * + * Note that the remote device may have multiple ports. Only the remote + * port connected to the local port is relevant here. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpRemPortId, + * - lldpRemPortIdSubtype, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_port_id Out: Port ID of remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_port_id ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_id_t * p_port_id); + +/** + * Get port description for local port. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpLocPortDesc, + * - lldpLocPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_port_desc Out: Port description of local port. + */ +void pf_snmp_get_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_desc); + +/** + * Get port description for remote port. + * + * The remote Port description is contained in an LLDP packet sent from a port + * on the remote device to the local port with no intermediate switches. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpRemPortDesc, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_port_desc Out: Port description of remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_port_description ( + pnet_t * net, + int loc_port_num, + pf_lldp_port_description_t * p_port_desc); + +/** + * Get encoded Management Address for local interface. + * + * The returned management address is encoded for use in SNMP response. + * See PN-Topology ch. 6.3.1. + * + * See IEEE 802.1AB-2005 ch. 12.2. Relevant fields: + * - lldpLocManAddr, + * - lldpLocManAddrLen, + * - lldpLocManAddrSubtype. + * + * @param net In: The p-net stack instance. + * @param p_man_address Out: Management address of local interface. + */ +void pf_snmp_get_management_address ( + pnet_t * net, + pf_snmp_management_address_t * p_man_address); + +/** + * Get encoded Management Address for remote interface connected to local port. + * + * The returned management address is encoded for use in SNMP response. + * See PN-Topology ch. 6.3.1. + * + * See IEEE 802.1AB-2005 (LLDPv1) ch. 12.2. Relevant fields: + * - lldpRemManAddr, + * - lldpRemManAddrLen, + * - lldpRemManAddrSubtype, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_man_address Out: Management address of remote interface. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_management_address ( + pnet_t * net, + int loc_port_num, + pf_snmp_management_address_t * p_man_address); + +/** + * Get ManAddrIfId for local interface. + * + * The row in lldpLocManAddrTable will contain a reference to a row in IfTable, + * which is part of MIB-II. + * See PN-Topology ch. 6.5.1 "Mapping of Ports and PROFINET Interfaces + * in LLDP MIB and MIB-II". + * + * Note that the local device may have multiple interfaces (such as a + * loopback interface). Only the local interface used by the p-net stack + * is relevant here. + * + * See IEEE 802.1AB-2005 ch. 12.2. Relevant fields: + * - lldpLocManAddrIfId, + * - lldpLocManAddrIfSubtype. + * + * @param net In: The p-net stack instance. + * @param p_man_port_index Out: Index in IfTable for Management port. + */ +void pf_snmp_get_management_port_index ( + pnet_t * net, + pf_lldp_interface_number_t * p_man_port_index); + +/** + * Get ManAddrIfId for remote interface connected to local port. + * + * The ManAddrIfId of remote device is contained in an LLDP packet sent from a + * port on the remote device to the local port with no intermediate switches. + * + * Note that the remote device may have multiple interfaces. Only the remote + * interface connected to the local port is relevant here. + * + * See IEEE 802.1AB-2005 ch. 12.2. Relevant fields: + * - lldpRemManAddrIfId, + * - lldpRemManAddrIfIdSubtype, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_man_port_index Out: Index in remote IfTable for Management port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_management_port_index ( + pnet_t * net, + int loc_port_num, + pf_lldp_interface_number_t * p_man_port_index); + +/** + * Get local station name. + * + * The local station name (NameOfStation) is the name of the local interface. + * + * The station name is usually a string, but may also be the MAC address + * (as a string "AB-CD-EF-01-23-45") of the remote interface in case no string + * has been assigned. + * + * Note that the local device may have multiple interfaces (such as a + * loopback interface). Only the local interface used by the p-net stack + * is relevant here. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) annex U (LLDP EXT MIB). Relevant + * fields: + * - lldpXPnoLocPortNoS. + * + * @param net In: The p-net stack instance. + * @param p_station_name Out: Station name of local interface. + */ +void pf_snmp_get_station_name ( + pnet_t * net, + pf_lldp_station_name_t * p_station_name); + +/** + * Get remote station name for remote interface connected to local port. + * + * The remote station name (NameOfStation) is the name of the remote interface + * and is contained in an LLDP packet sent from a port + * on the remote device to the local port with no intermediate switches. + * + * The station name is usually a string, but may also be the MAC address + * (as a string "AB-CD-EF-01-23-45") of the remote interface in case no string + * has been assigned. + * + * Note that the remote device may have multiple interfaces. Only the remote + * interface connected to the local port is relevant here. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) annex U (LLDP EXT MIB). Relevant + * fields: + * - lldpXPnoRemPortNoS, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_station_name Out: Station name of remote interface. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_station_name ( + pnet_t * net, + int loc_port_num, + pf_lldp_station_name_t * p_station_name); + +/** + * Get measured signal delays on local port. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) Annex U: "LLDP EXT MIB". Relevant + * fields: + * - lldpXPnoLocLPDValue, + * - lldpXPnoLocPortTxDValue, + * - lldpXPnoLocPortRxDValue, + * - lldpLocPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_delays Out: Measured signal delays on local port. + */ +void pf_snmp_get_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_snmp_signal_delay_t * p_delays); + +/** + * Get measured signal delays on remote port. + * + * See IEC CDV 61158-6-10 (PN-AL-Protocol) Annex U: "LLDP EXT MIB". Relevant + * fields: + * - lldpXPnoRemLPDValue, + * - lldpXPnoRemPortTxDValue, + * - lldpXPnoRemPortRxDValue, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_delays Out: Measured signal delays on remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_signal_delays ( + pnet_t * net, + int loc_port_num, + pf_snmp_signal_delay_t * p_delays); + +/** + * Get encoded link status of local port. + * + * The returned fields are encoded and ready to be put in SNMP response. + * + * See IEEE 802.1AB-2005 (LLDPv1) Annex G.7.1: "IEEE 802.3 LLDP extension + * MIB module". Relevant fields: + * - lldpXdot3LocPortAutoNegSupported, + * - lldpXdot3LocPortAutoNegEnabled, + * - lldpXdot3LocPortAutoNegAdvertisedCap, + * - lldpXdot3LocPortOperMauType, + * - lldpLocPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_link_status Out: Link status of local port. + */ +void pf_snmp_get_link_status ( + pnet_t * net, + int loc_port_num, + pf_snmp_link_status_t * p_link_status); + +/** + * Get encoded link status of remote port. + * + * The returned fields are encoded and ready to be put in SNMP response. + * + * See IEEE 802.1AB-2005 (LLDPv1) annex G.7.1: "IEEE 802.3 LLDP extension + * MIB module". Relevant fields: + * - lldpXdot3RemPortAutoNegSupported, + * - lldpXdot3RemPortAutoNegEnabled, + * - lldpXdot3RemPortAutoNegAdvertisedCap, + * - lldpXdot3RemPortOperMauType, + * - lldpRemLocalPortNum. + * + * @param net In: The p-net stack instance. + * @param loc_port_num In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * See pf_snmp_get_next_port(). + * @param p_link_status Out: Link status of remote port. + * @return 0 if the operation succeeded. + * -1 if an error occurred (no info from remote device received). + */ +int pf_snmp_get_peer_link_status ( + pnet_t * net, + int loc_port_num, + pf_snmp_link_status_t * p_link_status); + +/** + * Show SNMP details + * + * @param net InOut: The p-net stack instance + */ +void pf_snmp_show (pnet_t * net); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_SNMP_H */ diff --git a/profinet_stack/p-net/src/common/pf_udp.c b/profinet_stack/p-net/src/common/pf_udp.c new file mode 100644 index 0000000..c31d6c6 --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_udp.c @@ -0,0 +1,74 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief UDP communications + */ + +#ifdef UNIT_TEST +#define pnal_udp_close mock_pnal_udp_close +#define pnal_udp_open mock_pnal_udp_open +#define pnal_udp_recvfrom mock_pnal_udp_recvfrom +#define pnal_udp_sendto mock_pnal_udp_sendto +#endif + +#include +#include "pf_includes.h" + +int pf_udp_open (pnet_t * net, pnal_ipport_t port) +{ + return pnal_udp_open (PNAL_IPADDR_ANY, port); +} + +int pf_udp_sendto ( + pnet_t * net, + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size) +{ + int sent_len = 0; + + sent_len = pnal_udp_sendto (id, dst_addr, dst_port, data, size); + + if (sent_len != size) + { + LOG_ERROR ( + PNET_LOG, + "UDP(%d): Failed to send %u UDP bytes payload on the socket.\n", + __LINE__, + size); + } + + return sent_len; +} + +int pf_udp_recvfrom ( + pnet_t * net, + uint32_t id, + pnal_ipaddr_t * src_addr, + pnal_ipport_t * src_port, + uint8_t * data, + int size) +{ + return pnal_udp_recvfrom (id, src_addr, src_port, data, size); +} + +void pf_udp_close (pnet_t * net, uint32_t id) +{ + pnal_udp_close (id); +} diff --git a/profinet_stack/p-net/src/common/pf_udp.h b/profinet_stack/p-net/src/common/pf_udp.h new file mode 100644 index 0000000..0fbf78b --- /dev/null +++ b/profinet_stack/p-net/src/common/pf_udp.h @@ -0,0 +1,86 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_UDP_H +#define PF_UDP_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Open an UDP socket. Listens to all IP addresses (PNAL_IPADDR_ANY). + * + * @param net InOut: The p-net stack instance + * @param port In: UDP port to listen to. + * @return Socket ID, or -1 if an error occurred. Note that socket ID 0 + * is valid. + */ +int pf_udp_open (pnet_t * net, pnal_ipport_t port); + +/** + * Send UDP data + * + * @param net InOut: The p-net stack instance + * @param id In: Socket ID + * @param dst_addr In: Destination IP address + * @param dst_port In: Destination UDP port + * @param data In: Data to be sent + * @param size In: Size of data + * @return The number of bytes sent, or -1 if an error occurred. + */ +int pf_udp_sendto ( + pnet_t * net, + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size); + +/** + * Receive UDP data. + * + * This is a nonblocking function, and it + * returns 0 immediately if no data is available. + * + * @param net InOut: The p-net stack instance + * @param id In: Socket ID + * @param dst_addr Out: Source IP address + * @param dst_port Out: Source UDP port + * @param data Out: Received data + * @param size In: Size of buffer for received data + * @return The number of bytes received, or -1 if an error occurred. + */ +int pf_udp_recvfrom ( + pnet_t * net, + uint32_t id, + pnal_ipaddr_t * src_addr, + pnal_ipport_t * src_port, + uint8_t * data, + int size); + +/** + * Close an UDP socket. + * + * @param net InOut: The p-net stack instance + * @param id In: Socket ID + */ +void pf_udp_close (pnet_t * net, uint32_t id); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_UDP_H */ diff --git a/profinet_stack/p-net/src/device/pf_block_reader.c b/profinet_stack/p-net/src/device/pf_block_reader.c new file mode 100644 index 0000000..90cc52a --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_block_reader.c @@ -0,0 +1,1048 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Utility functions for interpreting data in buffers. + * + * For example uint16, protocol headers and protocol messages can be + * read from the buffers. + * + * Most functions have (at least) an input buffer and a position in the buffer + * as input arguments. + * + */ + +#ifdef UNIT_TEST + +#endif + +#include +#include + +#include "pf_includes.h" +#include "pf_block_reader.h" + +void pf_get_mem ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t dest_size, + void * p_dest) +{ + if (p_info->result != PF_PARSE_OK) + { + /* Preserve first error */ + } + else if (((*p_pos) + dest_size) > p_info->len) + { + /* + * Reached end of buffer + * If this is a chain of pbuff then goto the next pbuff, + * else this should never happen. + */ + LOG_DEBUG (PNET_LOG, "BR(%d): Unexpected end of input data\n", __LINE__); + p_info->result = PF_PARSE_END_OF_INPUT; + } + else if (p_info->p_buf == NULL) + { + p_info->result = PF_PARSE_NULL_POINTER; + } + else + { + memcpy (p_dest, &p_info->p_buf[*p_pos], dest_size); + (*p_pos) += dest_size; + } +} + +uint8_t pf_get_byte (pf_get_info_t * p_info, uint16_t * p_pos) +{ + uint8_t res = 0; + + if (p_info->result != PF_PARSE_OK) + { + /* Preserve first error */ + } + else if (*p_pos >= p_info->len) + { + /* Reached end of buffer */ + LOG_DEBUG ( + PNET_LOG, + "BR(%d): End of buffer reached when looking for a byte\n", + __LINE__); + p_info->result = PF_PARSE_END_OF_INPUT; + } + else if (p_info->p_buf == NULL) + { + p_info->result = PF_PARSE_NULL_POINTER; + } + else + { + res = p_info->p_buf[*p_pos]; + (*p_pos)++; + } + + return res; +} + +uint16_t pf_get_uint16 (pf_get_info_t * p_info, uint16_t * p_pos) +{ + uint16_t res = 0; + + if (p_info->result != PF_PARSE_OK) + { + /* Keep first error */ + } + else if (p_info->is_big_endian) + { + res = ((uint16_t)pf_get_byte (p_info, p_pos) << 8) + + (uint16_t)pf_get_byte (p_info, p_pos); + } + else + { + res = (uint16_t)pf_get_byte (p_info, p_pos) + + ((uint16_t)pf_get_byte (p_info, p_pos) << 8); + } + + return res; +} + +uint32_t pf_get_uint32 (pf_get_info_t * p_info, uint16_t * p_pos) +{ + uint32_t res = 0; + + if (p_info->is_big_endian) + { + res = ((uint32_t)pf_get_byte (p_info, p_pos) << 24) + + ((uint32_t)pf_get_byte (p_info, p_pos) << 16) + + ((uint32_t)pf_get_byte (p_info, p_pos) << 8) + + (uint32_t)pf_get_byte (p_info, p_pos); + } + else + { + res = (uint32_t)pf_get_byte (p_info, p_pos) + + ((uint32_t)pf_get_byte (p_info, p_pos) << 8) + + ((uint32_t)pf_get_byte (p_info, p_pos) << 16) + + ((uint32_t)pf_get_byte (p_info, p_pos) << 24); + } + + return res; +} + +/** + * @internal + * Extract a UUID from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut:Position in the buffer. + * @param p_dest Out: Destination buffer. + */ +static void pf_get_uuid ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_uuid_t * p_dest) +{ + p_dest->data1 = pf_get_uint32 (p_info, p_pos); + p_dest->data2 = pf_get_uint16 (p_info, p_pos); + p_dest->data3 = pf_get_uint16 (p_info, p_pos); + pf_get_mem (p_info, p_pos, sizeof (p_dest->data4), p_dest->data4); +} + +/** + * @internal + * Extract a rpc epm handle from a buffer. + * @param p_info InOut:The parser state. + * @param p_pos InOut:Position in the buffer. + * @param p_handle Out: Destination buffer. + */ +static void pf_get_handle ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_rpc_handle_t * p_handle) +{ + p_handle->rpc_entry_handle = pf_get_uint32 (p_info, p_pos); + p_handle->handle_uuid.time_low = pf_get_uint32 (p_info, p_pos); + p_handle->handle_uuid.time_mid = pf_get_uint16 (p_info, p_pos); + p_handle->handle_uuid.time_hi_and_version = pf_get_uint16 (p_info, p_pos); + p_handle->handle_uuid.clock_hi_and_reserved = pf_get_byte (p_info, p_pos); + p_handle->handle_uuid.clock_low = pf_get_byte (p_info, p_pos); + pf_get_mem ( + p_info, + p_pos, + sizeof (p_handle->handle_uuid.node), + p_handle->handle_uuid.node); +} + +/** + * @internal + * Extract bits from a uint32_t. + * The extracted bits are placed at bit 0 in the returned uint32_t. + * + * A B C Input. pos = 6, + * len = 3 Bit number ... 12 11 10 9 8 7 6 5 4 3 2 1 0 + * + * ... 0 0 0 0 0 0 0 0 0 A B C Output + * + * @param bits In: The bit field. + * @param pos In: First bit to extract. + * @param len In: Number of bits to extract. + * @return The extracted bits + */ +uint32_t pf_get_bits (uint32_t bits, uint8_t pos, uint8_t len) +{ + if (len == 0) + { + LOG_DEBUG (PNET_LOG, "BR(%d): len is 0 (zero)\n", __LINE__); + return 0; + } + else if ((pos + len) > 32) + { + LOG_ERROR (PNET_LOG, "BR(%d): pos %u + len %u > 32\n", __LINE__, pos, len); + return 0; + } + else if ((pos == 0) && (len == 32)) + { + return bits; + } + else + { + + return (bits >> pos) & (BIT (len) - 1u); + } +} + +/** + * @internal + * Extract a frame descriptor from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_fd Out: Destination buffer. + */ +static void pf_get_frame_descriptor ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_frame_descriptor_t * p_fd) +{ + p_fd->slot_number = pf_get_uint16 (p_info, p_pos); + p_fd->subslot_number = pf_get_uint16 (p_info, p_pos); + p_fd->frame_offset = pf_get_uint16 (p_info, p_pos); +} + +/** + * @internal + * Extract an IOCR API entry from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ae Out: Destination buffer. + */ +static void pf_get_iocr_api_entry ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_api_entry_t * p_ae) +{ + uint16_t ix; + + p_ae->api = pf_get_uint32 (p_info, p_pos); + + p_ae->nbr_io_data = pf_get_uint16 (p_info, p_pos); + for (ix = 0; ix < p_ae->nbr_io_data; ix++) + { + pf_get_frame_descriptor (p_info, p_pos, &p_ae->io_data[ix]); + } + + p_ae->nbr_iocs = pf_get_uint16 (p_info, p_pos); + for (ix = 0; ix < p_ae->nbr_iocs; ix++) + { + pf_get_frame_descriptor (p_info, p_pos, &p_ae->iocs[ix]); + } +} + +/** + * @internal + * Extract an expected sub-module from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_sub Out: Destination buffer. + */ +static void pf_get_exp_submodule ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_exp_submodule_t * p_sub) +{ + uint16_t temp_u16; + + p_sub->subslot_number = pf_get_uint16 (p_info, p_pos); + p_sub->submodule_ident_number = pf_get_uint32 (p_info, p_pos); + /* subslot_properties */ + temp_u16 = pf_get_uint16 (p_info, p_pos); + p_sub->submodule_properties.type = pf_get_bits (temp_u16, 0, 2); + p_sub->submodule_properties.sharedInput = + (pf_get_bits (temp_u16, 2, 1) != 0); + p_sub->submodule_properties.reduce_input_submodule_data_length = + (pf_get_bits (temp_u16, 3, 1) != 0); + p_sub->submodule_properties.reduce_output_submodule_data_length = + (pf_get_bits (temp_u16, 4, 1) != 0); + p_sub->submodule_properties.discard_ioxs = + (pf_get_bits (temp_u16, 5, 1) != 0); + + /* At least one submodule data descriptor */ + p_sub->data_descriptor[0].data_direction = pf_get_uint16 (p_info, p_pos); + p_sub->data_descriptor[0].submodule_data_length = + pf_get_uint16 (p_info, p_pos); + p_sub->data_descriptor[0].length_iocs = pf_get_byte (p_info, p_pos); + p_sub->data_descriptor[0].length_iops = pf_get_byte (p_info, p_pos); + p_sub->nbr_data_descriptors = 1; + /* May have one more */ + if (p_sub->submodule_properties.type == PNET_DIR_IO) + { + p_sub->data_descriptor[1].data_direction = pf_get_uint16 (p_info, p_pos); + p_sub->data_descriptor[1].submodule_data_length = + pf_get_uint16 (p_info, p_pos); + p_sub->data_descriptor[1].length_iocs = pf_get_byte (p_info, p_pos); + p_sub->data_descriptor[1].length_iops = pf_get_byte (p_info, p_pos); + p_sub->nbr_data_descriptors = 2; + } + + LOG_DEBUG ( + PNET_LOG, + "BR(%d): Subslot 0x%04x. Expected submodule 0x%" PRIx32 + " with direction %s\n", + __LINE__, + p_sub->subslot_number, + p_sub->submodule_ident_number, + pf_cmdev_submod_dir_to_string (p_sub->submodule_properties.type)); +} + +/* ======================== Public functions ======================== */ + +void pf_get_block_header ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_block_header_t * p_hdr) +{ + p_hdr->block_type = pf_get_uint16 (p_info, p_pos); + p_hdr->block_length = pf_get_uint16 (p_info, p_pos); + p_hdr->block_version_high = pf_get_byte (p_info, p_pos); + p_hdr->block_version_low = pf_get_byte (p_info, p_pos); +} + +void pf_get_ar_param (pf_get_info_t * p_info, uint16_t * p_pos, pf_ar_t * p_ar) +{ + uint32_t temp_u32; + uint16_t str_len; + + p_ar->ar_param.ar_type = pf_get_uint16 (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_ar->ar_param.ar_uuid); + p_ar->ar_param.session_key = pf_get_uint16 (p_info, p_pos); + pf_get_mem ( + p_info, + p_pos, + sizeof (p_ar->ar_param.cm_initiator_mac_add), + &p_ar->ar_param.cm_initiator_mac_add); + pf_get_uuid (p_info, p_pos, &p_ar->ar_param.cm_initiator_object_uuid); + /* ar_properties */ + temp_u32 = pf_get_uint32 (p_info, p_pos); + p_ar->ar_param.ar_properties.state = pf_get_bits (temp_u32, 0, 3); + p_ar->ar_param.ar_properties.supervisor_takeover_allowed = + (pf_get_bits (temp_u32, 3, 1) != 0); + p_ar->ar_param.ar_properties.parameterization_server = + pf_get_bits (temp_u32, 4, 1); + p_ar->ar_param.ar_properties.device_access = + (pf_get_bits (temp_u32, 8, 1) != 0); + p_ar->ar_param.ar_properties.companion_ar = + (uint16_t)pf_get_bits (temp_u32, 9, 2); + p_ar->ar_param.ar_properties.acknowledge_companion_ar = + (pf_get_bits (temp_u32, 11, 1) != 0); + p_ar->ar_param.ar_properties.combined_object_container = + (pf_get_bits (temp_u32, 29, 1) != 0); + p_ar->ar_param.ar_properties.startup_mode = + (pf_get_bits (temp_u32, 30, 1) != 0); + p_ar->ar_param.ar_properties.pull_module_alarm_allowed = + (pf_get_bits (temp_u32, 31, 1) != 0); + + p_ar->ar_param.cm_initiator_activity_timeout_factor = + pf_get_uint16 (p_info, p_pos); + p_ar->ar_param.cm_initiator_udp_rt_port = pf_get_uint16 (p_info, p_pos); + + str_len = pf_get_uint16 (p_info, p_pos); + p_ar->ar_param.cm_initiator_station_name_len = str_len; + if (str_len > sizeof (p_ar->ar_param.cm_initiator_station_name) - 1) + { + str_len = sizeof (p_ar->ar_param.cm_initiator_station_name) - 1; + } + pf_get_mem (p_info, p_pos, str_len, p_ar->ar_param.cm_initiator_station_name); + p_ar->ar_param.cm_initiator_station_name[str_len] = '\0'; +} + +void pf_get_iocr_param ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t ix, + pf_ar_t * p_ar) +{ + uint32_t temp_u32; + uint16_t temp_u16; + uint16_t iy; + + p_ar->iocrs[ix].param.iocr_type = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.iocr_reference = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.lt_field = pf_get_uint16 (p_info, p_pos); + /* iocr_Properties */ + temp_u32 = pf_get_uint32 (p_info, p_pos); + p_ar->iocrs[ix].param.iocr_properties.rt_class = + pf_get_bits (temp_u32, 0, 4); + p_ar->iocrs[ix].param.iocr_properties.reserved_1 = + (pf_get_bits (temp_u32, 4, 9) != 0); + p_ar->iocrs[ix].param.iocr_properties.reserved_2 = + (pf_get_bits (temp_u32, 13, 11) != 0); + p_ar->iocrs[ix].param.iocr_properties.reserved_3 = + (pf_get_bits (temp_u32, 24, 8) != 0); + + p_ar->iocrs[ix].param.c_sdu_length = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.frame_id = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.send_clock_factor = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.reduction_ratio = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.phase = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.sequence = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.frame_send_offset = pf_get_uint32 (p_info, p_pos); + p_ar->iocrs[ix].param.watchdog_factor = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.data_hold_factor = pf_get_uint16 (p_info, p_pos); + /* iocr_tag_header */ + temp_u16 = pf_get_uint16 (p_info, p_pos); + p_ar->iocrs[ix].param.iocr_tag_header.vlan_id = + pf_get_bits (temp_u16, 0, 11); + p_ar->iocrs[ix].param.iocr_tag_header.iocr_user_priority = + pf_get_bits (temp_u16, 13, 3); + + pf_get_mem ( + p_info, + p_pos, + sizeof (p_ar->iocrs[ix].param.iocr_multicast_mac_add), + &p_ar->iocrs[ix].param.iocr_multicast_mac_add); + + p_ar->iocrs[ix].param.nbr_apis = pf_get_uint16 (p_info, p_pos); + for (iy = 0; iy < p_ar->iocrs[ix].param.nbr_apis; iy++) + { + pf_get_iocr_api_entry (p_info, p_pos, &p_ar->iocrs[ix].param.apis[iy]); + } +} + +void pf_get_exp_api_module ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar) +{ + uint16_t ix; + uint16_t iy; + uint32_t api; + uint16_t slot_number; + pf_exp_api_t * p_api = NULL; + pf_exp_module_t * p_mod = NULL; + uint16_t nbr_exp_api; + + nbr_exp_api = pf_get_uint16 (p_info, p_pos); /* In this block */ + for (ix = 0; ix < nbr_exp_api; ix++) + { + /* Get one module description */ + api = pf_get_uint32 (p_info, p_pos); + + /* Find the API if we are augmenting it */ + for (iy = 0; iy < p_ar->nbr_exp_apis; iy++) + { + if ((p_ar->exp_apis[iy].valid == true) && (p_ar->exp_apis[iy].api == api)) + { + p_api = &p_ar->exp_apis[iy]; + } + } + + /* If this is the first mention of this API */ + if ((p_api == NULL) && (p_ar->nbr_exp_apis < PNET_MAX_API)) + { + /* Allocate a new API */ + p_api = &p_ar->exp_apis[p_ar->nbr_exp_apis]; + p_ar->nbr_exp_apis++; + + p_api->api = api; + p_api->nbr_modules = 0; + } + + if (p_api == NULL) + { + /* This error condition is reported by caller. */ + p_info->result = PF_PARSE_OUT_OF_API_RESOURCES; + LOG_DEBUG ( + PNET_LOG, + "BR(%d): Out of expected API resources\n", + __LINE__); + } + else + { + slot_number = pf_get_uint16 (p_info, p_pos); + + /* Get a new module. */ + if (p_api->nbr_modules < PNET_MAX_SLOTS) + { + p_mod = &p_api->modules[p_api->nbr_modules]; + p_api->nbr_modules++; + + p_mod->slot_number = slot_number; + p_mod->module_ident_number = pf_get_uint32 (p_info, p_pos); + p_mod->module_properties = pf_get_uint16 (p_info, p_pos); + p_mod->nbr_submodules = pf_get_uint16 (p_info, p_pos); + + LOG_DEBUG ( + PNET_LOG, + "BR(%d): Slot %u. Expected module 0x%" PRIx32 " with %u " + "submodules.\n", + __LINE__, + p_mod->slot_number, + p_mod->module_ident_number, + p_mod->nbr_submodules); + + for (iy = 0; iy < p_mod->nbr_submodules; iy++) + { + pf_get_exp_submodule (p_info, p_pos, &p_mod->submodules[iy]); + } + p_api->valid = true; + } + else + { + /* This error condition is reported by caller. */ + p_info->result = PF_PARSE_OUT_OF_EXP_SUBMODULE_RESOURCES; + LOG_ERROR ( + PNET_LOG, + "BR(%d): Too many modules used (not enough slots). Out of " + "expected module resources.\n", + __LINE__); + } + } + } +} + +void pf_get_alarm_cr_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar) +{ + uint32_t temp_u32; + uint16_t temp_u16; + + p_ar->alarm_cr_request.alarm_cr_type = pf_get_uint16 (p_info, p_pos); + p_ar->alarm_cr_request.lt_field = pf_get_uint16 (p_info, p_pos); + /* alarm_cr_properties */ + temp_u32 = pf_get_uint32 (p_info, p_pos); + p_ar->alarm_cr_request.alarm_cr_properties.priority = + (pf_get_bits (temp_u32, 0, 1) != 0); + p_ar->alarm_cr_request.alarm_cr_properties.transport_udp = + (pf_get_bits (temp_u32, 1, 1) != 0); + + p_ar->alarm_cr_request.rta_timeout_factor = pf_get_uint16 (p_info, p_pos); + p_ar->alarm_cr_request.rta_retries = pf_get_uint16 (p_info, p_pos); + p_ar->alarm_cr_request.local_alarm_reference = pf_get_uint16 (p_info, p_pos); + p_ar->alarm_cr_request.max_alarm_data_length = pf_get_uint16 (p_info, p_pos); + + temp_u16 = pf_get_uint16 (p_info, p_pos); + p_ar->alarm_cr_request.alarm_cr_tag_header_high.vlan_id = + pf_get_bits (temp_u16, 0, 12); + p_ar->alarm_cr_request.alarm_cr_tag_header_high.alarm_user_priority = + pf_get_bits (temp_u16, 13, 3); + + temp_u16 = pf_get_uint16 (p_info, p_pos); + p_ar->alarm_cr_request.alarm_cr_tag_header_low.vlan_id = + pf_get_bits (temp_u16, 0, 12); + p_ar->alarm_cr_request.alarm_cr_tag_header_low.alarm_user_priority = + pf_get_bits (temp_u16, 13, 3); +} + +#if PNET_OPTION_PARAMETER_SERVER +void pf_get_ar_prm_server_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar) +{ + pf_get_uuid (p_info, p_pos, &p_ar->prm_server.parameter_server_object_uuid); + /* parameter_server_properties - not used */ + (void)pf_get_uint32 (p_info, p_pos); + + p_ar->prm_server.cm_initiator_activity_timeout_factor = + pf_get_uint16 (p_info, p_pos); + p_ar->prm_server.parameter_server_station_name_len = + pf_get_uint16 (p_info, p_pos); + pf_get_mem ( + p_info, + p_pos, + p_ar->prm_server.parameter_server_station_name_len, + p_ar->prm_server.parameter_server_station_name); + p_ar->prm_server.parameter_server_station_name + [p_ar->prm_server.parameter_server_station_name_len] = '\0'; +} +#endif + +#if PNET_OPTION_IR +/** + * Extract an DFP IOCR block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_dfp_iocr Out: Contains the destination structure. + */ +static void pf_get_dfp_iocr ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_dfp_iocr_t * p_dfp_iocr) +{ + p_dfp_iocr->iocr_reference = pf_get_uint16 (p_info, p_pos); + p_dfp_iocr->subframe_offset = pf_get_uint16 (p_info, p_pos); + + p_dfp_iocr->subframe_data.subframe_position = pf_get_byte (p_info, p_pos); + p_dfp_iocr->subframe_data.subframe_length = pf_get_byte (p_info, p_pos); +} + +void pf_get_ir_info_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar) +{ + uint16_t ix; + + pf_get_uuid (p_info, p_pos, &p_ar->ir_info.ir_data_uuid); + p_ar->ir_info.nbr_iocrs = pf_get_uint16 (p_info, p_pos); + + for (ix = 0; ix < p_ar->ir_info.nbr_iocrs; ix++) + { + pf_get_dfp_iocr (p_info, p_pos, &p_ar->ir_info.dfp_iocrs[ix]); + } +} +#endif + +#if PNET_OPTION_MC_CR +void pf_get_mcr_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t ix, + pf_ar_t * p_ar) +{ + p_ar->mcr_request[ix].iocr_reference = pf_get_uint16 (p_info, p_pos); + p_ar->mcr_request[ix].address_resolution_properties.protocol = + pf_get_byte (p_info, p_pos); + p_ar->mcr_request[ix].address_resolution_properties.resolution_factor = + pf_get_byte (p_info, p_pos); + p_ar->mcr_request[ix].mci_timeout_factor = pf_get_uint16 (p_info, p_pos); + p_ar->mcr_request[ix].length_provider_station_name = + pf_get_uint16 (p_info, p_pos); + + pf_get_mem ( + p_info, + p_pos, + p_ar->mcr_request[ix].length_provider_station_name, + p_ar->mcr_request[ix].provider_station_name); + p_ar->mcr_request[ix] + .provider_station_name[p_ar->mcr_request[ix].length_provider_station_name] = + '\0'; +} +#endif + +void pf_get_ar_rpc_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar) +{ + p_ar->ar_rpc_request.initiator_rpc_server_port = + pf_get_uint16 (p_info, p_pos); +} + +#if PNET_OPTION_AR_VENDOR_BLOCKS +void pf_get_ar_vendor_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t ix, + pf_ar_t * p_ar) +{ + p_ar->ar_vendor_request[ix].ap_structure_identifier = + pf_get_uint32 (p_info, p_pos); + p_ar->ar_vendor_request[ix].api = pf_get_uint32 (p_info, p_pos); + p_ar->ar_vendor_request[ix].length_data_request = + pf_get_uint16 (p_info, p_pos); + pf_get_mem ( + p_info, + p_pos, + p_ar->ar_vendor_request[ix].length_data_request, + p_ar->ar_vendor_request[ix].data_request); +} +#endif + +void pf_get_control ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_control_block_t * p_req) +{ + /* 2 padding bytes */ + (void)pf_get_uint16 (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_req->ar_uuid); + p_req->session_key = pf_get_uint16 (p_info, p_pos); + + p_req->alarm_sequence_number = pf_get_uint16 (p_info, p_pos); + + /* Command and properties are always Big-Endian on the wire!! */ + p_req->control_command = pf_get_uint16 (p_info, p_pos); + p_req->control_block_properties = pf_get_uint16 (p_info, p_pos); +} + +void pf_get_ndr_data ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ndr_data_t * p_ndr) +{ + p_ndr->args_maximum = pf_get_uint32 (p_info, p_pos); + p_ndr->args_length = pf_get_uint32 (p_info, p_pos); + p_ndr->array.maximum_count = pf_get_uint32 (p_info, p_pos); + p_ndr->array.offset = pf_get_uint32 (p_info, p_pos); + p_ndr->array.actual_count = pf_get_uint32 (p_info, p_pos); +} + +void pf_get_dce_rpc_header ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_rpc_header_t * p_rpc) +{ + uint8_t temp_uint8; + + p_rpc->version = pf_get_byte (p_info, p_pos); + p_rpc->packet_type = pf_get_byte (p_info, p_pos) & 0x1f; /* Only 5 LSB + according to spec + */ + + /* flags */ + temp_uint8 = pf_get_byte (p_info, p_pos); + p_rpc->flags.last_fragment = + pf_get_bits (temp_uint8, PF_RPC_F_LAST_FRAGMENT, 1); + p_rpc->flags.fragment = pf_get_bits (temp_uint8, PF_RPC_F_FRAGMENT, 1); + p_rpc->flags.no_fack = pf_get_bits (temp_uint8, PF_RPC_F_NO_FACK, 1); + p_rpc->flags.maybe = pf_get_bits (temp_uint8, PF_RPC_F_MAYBE, 1); + p_rpc->flags.idempotent = pf_get_bits (temp_uint8, PF_RPC_F_IDEMPOTENT, 1); + p_rpc->flags.broadcast = pf_get_bits (temp_uint8, PF_RPC_F_BROADCAST, 1); + + /* flags2 */ + temp_uint8 = pf_get_byte (p_info, p_pos); + p_rpc->flags2.cancel_pending = + pf_get_bits (temp_uint8, PF_RPC_F2_CANCEL_PENDING, 1); + + /* Data repr */ + temp_uint8 = pf_get_byte (p_info, p_pos); + p_rpc->is_big_endian = (pf_get_bits (temp_uint8, 4, 4) == 0); + p_info->is_big_endian = p_rpc->is_big_endian; + + /* Float repr - Assume IEEE */ + (void)pf_get_byte (p_info, p_pos); + p_rpc->float_repr = 0; + + /* Reserved */ + p_rpc->reserved = pf_get_byte (p_info, p_pos); + + p_rpc->serial_high = pf_get_byte (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_rpc->object_uuid); + pf_get_uuid (p_info, p_pos, &p_rpc->interface_uuid); + pf_get_uuid (p_info, p_pos, &p_rpc->activity_uuid); + p_rpc->server_boot_time = pf_get_uint32 (p_info, p_pos); + p_rpc->interface_version = pf_get_uint32 (p_info, p_pos); + p_rpc->sequence_nmb = pf_get_uint32 (p_info, p_pos); + p_rpc->opnum = pf_get_uint16 (p_info, p_pos); + p_rpc->interface_hint = pf_get_uint16 (p_info, p_pos); + p_rpc->activity_hint = pf_get_uint16 (p_info, p_pos); + p_rpc->length_of_body = pf_get_uint16 (p_info, p_pos); + p_rpc->fragment_nmb = pf_get_uint16 (p_info, p_pos); + p_rpc->auth_protocol = pf_get_byte (p_info, p_pos); + p_rpc->serial_low = pf_get_byte (p_info, p_pos); +} + +void pf_get_read_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_iod_read_request_t * p_req) +{ + uint16_t ix; + + p_req->sequence_number = pf_get_uint16 (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_req->ar_uuid); + p_req->api = pf_get_uint32 (p_info, p_pos); + p_req->slot_number = pf_get_uint16 (p_info, p_pos); + p_req->subslot_number = pf_get_uint16 (p_info, p_pos); + p_req->padding[0] = pf_get_byte (p_info, p_pos); + p_req->padding[1] = pf_get_byte (p_info, p_pos); + p_req->index = pf_get_uint16 (p_info, p_pos); + p_req->record_data_length = pf_get_uint32 (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_req->target_ar_uuid); + for (ix = 0; ix < NELEMENTS (p_req->rw_padding); ix++) + { + p_req->rw_padding[ix] = pf_get_byte (p_info, p_pos); + } +} + +void pf_get_epm_lookup_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_rpc_lookup_req_t * p_req) +{ + p_req->inquiry_type = pf_get_uint32 (p_info, p_pos); + p_req->object_id = pf_get_uint32 (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_req->object_uuid); + p_req->interface_id = pf_get_uint32 (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_req->interface_uuid); + p_req->interface_ver_major = pf_get_uint16 (p_info, p_pos); + p_req->interface_ver_minor = pf_get_uint16 (p_info, p_pos); + p_req->version_option = pf_get_uint32 (p_info, p_pos); + pf_get_handle (p_info, p_pos, &p_req->rpc_handle); + p_req->max_entries = pf_get_uint32 (p_info, p_pos); +} + +void pf_get_write_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_iod_write_request_t * p_req) +{ + uint16_t ix; + + p_req->sequence_number = pf_get_uint16 (p_info, p_pos); + pf_get_uuid (p_info, p_pos, &p_req->ar_uuid); + p_req->api = pf_get_uint32 (p_info, p_pos); + p_req->slot_number = pf_get_uint16 (p_info, p_pos); + p_req->subslot_number = pf_get_uint16 (p_info, p_pos); + p_req->padding[0] = pf_get_byte (p_info, p_pos); + p_req->padding[1] = pf_get_byte (p_info, p_pos); + p_req->index = pf_get_uint16 (p_info, p_pos); + p_req->record_data_length = pf_get_uint32 (p_info, p_pos); + for (ix = 0; ix < NELEMENTS (p_req->rw_padding); ix++) + { + p_req->rw_padding[ix] = pf_get_byte (p_info, p_pos); + } +} + +void pf_get_im_1 (pf_get_info_t * p_info, uint16_t * p_pos, pnet_im_1_t * p_im_1) +{ + pf_get_mem ( + p_info, + p_pos, + sizeof (p_im_1->im_tag_function) - 1, + p_im_1->im_tag_function); + p_im_1->im_tag_function[sizeof (p_im_1->im_tag_function) - 1] = '\0'; + pf_get_mem ( + p_info, + p_pos, + sizeof (p_im_1->im_tag_location) - 1, + p_im_1->im_tag_location); + p_im_1->im_tag_location[sizeof (p_im_1->im_tag_location) - 1] = '\0'; +} + +void pf_get_alarm_fixed ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_alarm_fixed_t * p_alarm_fixed) +{ + uint32_t temp_u32; + + /* The fixed part is not a "block" so do not expect a block header */ + p_alarm_fixed->dst_ref = pf_get_uint16 (p_info, p_pos); + p_alarm_fixed->src_ref = pf_get_uint16 (p_info, p_pos); + + temp_u32 = pf_get_byte (p_info, p_pos); + p_alarm_fixed->pdu_type.type = pf_get_bits (temp_u32, 0, 4); + p_alarm_fixed->pdu_type.version = pf_get_bits (temp_u32, 4, 4); + + temp_u32 = pf_get_byte (p_info, p_pos); + p_alarm_fixed->add_flags.window_size = pf_get_bits (temp_u32, 0, 4); + p_alarm_fixed->add_flags.tack = pf_get_bits (temp_u32, 4, 1); + + p_alarm_fixed->send_seq_num = pf_get_uint16 (p_info, p_pos); + p_alarm_fixed->ack_seq_nbr = pf_get_uint16 (p_info, p_pos); +} + +void pf_get_alarm_data ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_alarm_data_t * p_alarm_data) +{ + uint16_t temp_u16; + + p_alarm_data->alarm_type = pf_get_uint16 (p_info, p_pos); + p_alarm_data->api_id = pf_get_uint32 (p_info, p_pos); + p_alarm_data->slot_nbr = pf_get_uint16 (p_info, p_pos); + p_alarm_data->subslot_nbr = pf_get_uint16 (p_info, p_pos); + p_alarm_data->module_ident = pf_get_uint32 (p_info, p_pos); + p_alarm_data->submodule_ident = pf_get_uint32 (p_info, p_pos); + + /* AlarmSpecifier */ + temp_u16 = pf_get_uint16 (p_info, p_pos); + p_alarm_data->sequence_number = pf_get_bits (temp_u16, 0, 11); + p_alarm_data->alarm_specifier.channel_diagnosis = + pf_get_bits (temp_u16, 11, 1); + p_alarm_data->alarm_specifier.manufacturer_diagnosis = + pf_get_bits (temp_u16, 12, 1); + p_alarm_data->alarm_specifier.submodule_diagnosis = + pf_get_bits (temp_u16, 13, 1); + p_alarm_data->alarm_specifier.ar_diagnosis = pf_get_bits (temp_u16, 15, 1); +} + +void pf_get_alarm_ack ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_alarm_data_t * p_alarm_data) +{ + uint16_t temp_u16; + + p_alarm_data->alarm_type = pf_get_uint16 (p_info, p_pos); + p_alarm_data->api_id = pf_get_uint32 (p_info, p_pos); + p_alarm_data->slot_nbr = pf_get_uint16 (p_info, p_pos); + p_alarm_data->subslot_nbr = pf_get_uint16 (p_info, p_pos); + + /* AlarmSpecifier */ + temp_u16 = pf_get_uint16 (p_info, p_pos); + p_alarm_data->sequence_number = pf_get_bits (temp_u16, 0, 11); + p_alarm_data->alarm_specifier.channel_diagnosis = + pf_get_bits (temp_u16, 11, 1); + p_alarm_data->alarm_specifier.manufacturer_diagnosis = + pf_get_bits (temp_u16, 12, 1); + p_alarm_data->alarm_specifier.submodule_diagnosis = + pf_get_bits (temp_u16, 13, 1); + p_alarm_data->alarm_specifier.ar_diagnosis = pf_get_bits (temp_u16, 15, 1); +} + +void pf_get_pnio_status ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pnet_pnio_status_t * p_status) +{ + p_status->error_code = pf_get_byte (p_info, p_pos); + p_status->error_decode = pf_get_byte (p_info, p_pos); + p_status->error_code_1 = pf_get_byte (p_info, p_pos); + p_status->error_code_2 = pf_get_byte (p_info, p_pos); +} + +void pf_get_port_data_check ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_port_data_check_t * p_port_data_check) +{ + uint8_t dummy[4]; + + pf_get_mem (p_info, p_pos, 2, dummy); /* Padding */ + p_port_data_check->slot_number = pf_get_uint16 (p_info, p_pos); + p_port_data_check->subslot_number = pf_get_uint16 (p_info, p_pos); + pf_get_block_header (p_info, p_pos, &p_port_data_check->block_header); +} + +/** + * Extract CheckPeer data block from buffer. + * + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_check_peer: Out: Destination structure + */ +static void get_check_peer ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_check_peer_t * p_check_peer) +{ + /* TODO: Validate length_peer_port_name and length_peer_station_name */ + + p_check_peer->length_peer_port_name = pf_get_byte (p_info, p_pos); + + pf_get_mem ( + p_info, + p_pos, + p_check_peer->length_peer_port_name, + p_check_peer->peer_port_name); + p_check_peer->peer_port_name[p_check_peer->length_peer_port_name] = '\0'; + + p_check_peer->length_peer_station_name = pf_get_byte (p_info, p_pos); + + pf_get_mem ( + p_info, + p_pos, + p_check_peer->length_peer_station_name, + p_check_peer->peer_station_name); + p_check_peer->peer_station_name[p_check_peer->length_peer_station_name] = + '\0'; +} + +void pf_get_port_data_check_check_peers ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint8_t max_peers, + pf_check_peers_t * p_check_peers) +{ + uint8_t i; + + p_check_peers->number_of_peers = pf_get_byte (p_info, p_pos); + if (p_check_peers->number_of_peers > max_peers) + { + LOG_ERROR ( + PNET_LOG, + "BR(%d): Unsupported number of check peers\n", + __LINE__); + p_check_peers->number_of_peers = max_peers; + } + + for (i = 0; i < p_check_peers->number_of_peers; i++) + { + get_check_peer (p_info, p_pos, &p_check_peers->peers[i]); + } +} + +void pf_get_port_data_adjust ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_port_data_adjust_t * p_port_data_adjust) +{ + uint8_t dummy[4]; + + pf_get_mem (p_info, p_pos, 2, dummy); /* Padding */ + p_port_data_adjust->slot_number = pf_get_uint16 (p_info, p_pos); + p_port_data_adjust->subslot_number = pf_get_uint16 (p_info, p_pos); + pf_get_block_header (p_info, p_pos, &p_port_data_adjust->block_header); +} + +void pf_get_port_data_adjust_peer_to_peer_boundary ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_adjust_peer_to_peer_boundary_t * p_boundary) +{ + uint8_t dummy[4]; + uint32_t temp; + + memset (p_boundary, 0, sizeof (*p_boundary)); + + pf_get_mem (p_info, p_pos, 2, dummy); /* Padding */ + + temp = pf_get_uint32 (p_info, p_pos); + p_boundary->peer_to_peer_boundary.do_not_send_LLDP_frames = + pf_get_bits (temp, 0, 1); + p_boundary->peer_to_peer_boundary.do_not_send_pctp_delay_request_frames = + pf_get_bits (temp, 1, 1); + p_boundary->peer_to_peer_boundary.do_not_send_path_delay_request_frames = + pf_get_bits (temp, 2, 1); + + p_boundary->adjust_properties = pf_get_uint16 (p_info, p_pos); +} + +void pf_get_interface_adjust ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_lldp_name_of_device_mode_t * name_of_device_mode) +{ + uint8_t dummy[2]; + uint32_t temp; + + pf_get_mem (p_info, p_pos, 2, dummy); /* Padding */ + + temp = pf_get_uint32 (p_info, p_pos); + *name_of_device_mode = pf_get_bits (temp, 0, 1); +} diff --git a/profinet_stack/p-net/src/device/pf_block_reader.h b/profinet_stack/p-net/src/device/pf_block_reader.h new file mode 100644 index 0000000..75f3a96 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_block_reader.h @@ -0,0 +1,394 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_BLOCK_READER_H +#define PF_BLOCK_READER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* ======================== Public functions */ + +/** + * @internal + * Return a byte from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @return The byte from the buffer + */ +uint8_t pf_get_byte (pf_get_info_t * p_info, uint16_t * p_pos); + +/** + * @internal + * Return a uint16_t from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @return The uint16_t from the buffer + */ +uint16_t pf_get_uint16 (pf_get_info_t * p_info, uint16_t * p_pos); + +/** + * @internal + * Return a uint32_t from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + */ +uint32_t pf_get_uint32 (pf_get_info_t * p_info, uint16_t * p_pos); + +/** + * @internal + * Extract a sequence of bytes from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param dest_size In: Number of bytes to copy. + * @param p_dest Out: Destination buffer. + */ +void pf_get_mem ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t dest_size, + void * p_dest); + +/** + * Extract a NDR header from a buffer. + * + * This is the first part of the payload of the incoming DCE/RPC message + * (which is sent via UDP). + * + * Reads args_maximum, args_length, maximum_count, offset and actual_count. + * + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ndr Out: Destination buffer. + */ +void pf_get_ndr_data ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ndr_data_t * p_ndr); + +/** + * Extract a block header from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_hdr Out: Destination buffer. + */ +void pf_get_block_header ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_block_header_t * p_hdr); + +/* Readers of specific blocks (do not read the block header). */ + +/** + * Extract an AR param block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_ar_param (pf_get_info_t * p_info, uint16_t * p_pos, pf_ar_t * p_ar); + +/** + * Extract an IOCR param block from a buffer. + * + * Updates p_ar->iocrs[ix].param + * + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param ix In: The current index into p_ar->iocr[]. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_iocr_param ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t ix, + pf_ar_t * p_ar); + +/** + * Extract an expected API block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_exp_api_module ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar); + +/** + * Extract an alarm CR block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_alarm_cr_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar); + +/** + * Extract an AR RPC request block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_ar_rpc_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar); + +#if PNET_OPTION_AR_VENDOR_BLOCKS +/** + * Extract an AR vendor request block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param ix In: The current index into p_ar->ar_vendor_xxx[]. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_ar_vendor_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t ix, + pf_ar_t * p_ar); +#endif + +#if PNET_OPTION_PARAMETER_SERVER +/** + * Extract a parameter server block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_ar_prm_server_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar); +#endif + +#if PNET_OPTION_MC_CR +/** + * Extract an MCR request block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param ix In: The current index into p_ar->ar_vendor_xxx[]. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_mcr_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint16_t ix, + pf_ar_t * p_ar); +#endif + +#if PNET_OPTION_IR +/** + * Extract an IR info request block from a buffer. + * @param p_info InOut: The parser state. + * @param p_pos InOut: Position in the buffer. + * @param p_ar Out: Contains the destination structure. + */ +void pf_get_ir_info_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_ar_t * p_ar); +#endif + +/** + * Extract a DCE RPC header from a raw UDP data buffer. + * @param p_info InOut: The parser information. Sets + * p_info->is_big_endian + * @param p_pos InOut: The current parsing position. + * @param p_rpc Out: Destination structure. + */ +void pf_get_dce_rpc_header ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_rpc_header_t * p_rpc); + +/** + * Extract a control request block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_req Out: Destination structure. + */ +void pf_get_control ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_control_block_t * p_req); + +/** + * Extract a read request block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_req Out: Destination structure. + */ +void pf_get_read_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_iod_read_request_t * p_req); + +/** + * Extract a lookup request block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_req Out: Destination structure. + */ +void pf_get_epm_lookup_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_rpc_lookup_req_t * p_req); + +/** + * Extract a write request block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_req Out: Destination structure. + */ +void pf_get_write_request ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_iod_write_request_t * p_req); + +/** + * Extract a I&M1 data block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_im_1 Out: Destination structure. + */ +void pf_get_im_1 ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pnet_im_1_t * p_im_1); + +/** + * Extract the fixed part of an alarm frame from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_alarm_fixed Out: Destination structure. + */ +void pf_get_alarm_fixed ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_alarm_fixed_t * p_alarm_fixed); + +/** + * Extract an alarm DATA block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_alarm_data Out: Destination structure. Contains slot, subslot + * etc. + */ +void pf_get_alarm_data ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_alarm_data_t * p_alarm_data); + +/** + * Extract an alarm ACK block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_alarm_data Out: Destination structure. Contains slot, subslot + * etc. + */ +void pf_get_alarm_ack ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_alarm_data_t * p_alarm_data); + +/** + * Extract a PNIO status block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_status Out: Destination structure. + */ +void pf_get_pnio_status ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pnet_pnio_status_t * p_status); + +/** + * Extract PortDataCheck data block from buffer. + * Use the block_header_type to identify which check is + * contained in buffer and use corresponding parsing operation + * to retrieve the check data. For example pf_get_port_data_check_check_peers(). + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_port_data_check: Out: Destination structure + */ +void pf_get_port_data_check ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_port_data_check_t * p_port_data_check); + +/** + * Extract CheckPeers data blocks from buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param max_peers: In: Number of elements in destination array + * @param check_peers: Out: Destination structure + */ +void pf_get_port_data_check_check_peers ( + pf_get_info_t * p_info, + uint16_t * p_pos, + uint8_t max_peers, + pf_check_peers_t * check_peers); + +/** + * Extract PortDataAdjust data block from buffer. + * Use the block_header_type to identify which adjust data is + * contained in buffer and use corresponding parsing operation + * to retrieve the data. For example + * pf_get_port_data_adjust_peer_to_peer_boundary(). + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param p_port_data_check Out: Destination structure + */ +void pf_get_port_data_adjust ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_port_data_adjust_t * p_port_data_check); + +/** + * Extract a AdjustPeerToPeerBoundary data block from a buffer. + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param boundary Out: Destination structure. + */ +void pf_get_port_data_adjust_peer_to_peer_boundary ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_adjust_peer_to_peer_boundary_t * boundary); + +/** + * Extract a pd interface adjust block from a buffer. + * + * @param p_info InOut: The parser information. + * @param p_pos InOut: The current parsing position. + * @param mode Out: Multiple interface mode. + */ +void pf_get_interface_adjust ( + pf_get_info_t * p_info, + uint16_t * p_pos, + pf_lldp_name_of_device_mode_t * name_of_device_mode); + +/************ Internal functions, made available for unit testing ************/ + +uint32_t pf_get_bits (uint32_t bits, uint8_t pos, uint8_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_BLOCK_READER_H */ diff --git a/profinet_stack/p-net/src/device/pf_block_writer.c b/profinet_stack/p-net/src/device/pf_block_writer.c new file mode 100644 index 0000000..b0fad56 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_block_writer.c @@ -0,0 +1,4649 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Utility functions for writing data into buffers. + * + * For example uint16, time stamps and protocol headers can be written to the + * buffers. + * + * Most functions have (at least) a buffer and a position in the buffer + * as input arguments. + * + * Try to keep this file as clean as possible: + * - Avoid calling functions from other files + * - Pass data to be written as arguments (avoid passing in pnet_t * net) + * + * The functions should in general look like: + * + * void pf_put_whatever ( + * bool is_big_endian, + * const whatever * p_whatever, + * uint16_t res_len, + * uint8_t * p_bytes, + * uint16_t * p_pos); + */ + +#ifdef UNIT_TEST + +#endif + +#include + +#include "pf_includes.h" +#include "pf_block_writer.h" + +#define STRINGIFY(s) STRINGIFIED (s) +#define STRINGIFIED(s) #s + +/** + * @internal + * Insert a block header into a buffer. + * @param is_big_endian In: true if buffer is big-endian. + * @param bh_type In: Block type. + * @param bh_length In: Block length. + * @param bh_ver_high In: Block version high. + * @param bh_ver_low In: Block version low. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_block_header ( + bool is_big_endian, + pf_block_type_values_t bh_type, + uint16_t bh_length, + uint8_t bh_ver_high, + uint8_t bh_ver_low, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t temp_u16 = (uint16_t)bh_type; + + pf_put_uint16 (is_big_endian, temp_u16, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + (bh_length + sizeof (pf_block_header_t)) - 4, + res_len, + p_bytes, + p_pos); /* Always (-4)!! */ + pf_put_byte (bh_ver_high, res_len, p_bytes, p_pos); + pf_put_byte (bh_ver_low, res_len, p_bytes, p_pos); +} + +/** + * @internal + * Insert a string into a buffer. + * + * This function inserts a fixed sized string into a buffer. + * If the string is shorter than the destination size then the destination + * is padded with spaces. + * The destination buffer does not contain any terminating NUL byte. + * @param p_src In: The string. + * @param src_size In: The destination string size. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_str ( + const void * p_src, + uint16_t src_size, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t str_len = (uint16_t)strlen (p_src); + uint16_t ix; + + if (((*p_pos) + src_size) > res_len) + { + /* Reached end of buffer */ + LOG_DEBUG (PNET_LOG, "BW(%d): Output buffer is full\n", __LINE__); + p_bytes = NULL; + } + + if (p_bytes != NULL) + { + memcpy (&p_bytes[*p_pos], p_src, str_len); + (*p_pos) += str_len; + /* Pad with spaces - size also includes NUL-terminator. Do not write into + * that pos. */ + for (ix = str_len; ix < src_size - 1; ix++) + { + pf_put_byte (' ', res_len, p_bytes, p_pos); + } + } +} + +/** + * @internal + * Insert a UUID into a buffer. + * @param is_big_endian In: true if buffer is big-endian. + * @param p_uuid In: The UUID to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_uuid ( + bool is_big_endian, + const pf_uuid_t * p_uuid, + uint16_t res_len, /* Sizeof p_bytes buf */ + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint32 (is_big_endian, p_uuid->data1, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_uuid->data2, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_uuid->data3, res_len, p_bytes, p_pos); + pf_put_mem (p_uuid->data4, sizeof (p_uuid->data4), res_len, p_bytes, p_pos); +} + +/** + * Put rpc UUID into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_uuid In: The UUID to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_uuid ( + bool is_big_endian, + const pf_rpc_uuid_type_t * p_uuid, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint32 (is_big_endian, p_uuid->time_low, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_uuid->time_mid, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + p_uuid->time_hi_and_version, + res_len, + p_bytes, + p_pos); + pf_put_byte (p_uuid->clock_hi_and_reserved, res_len, p_bytes, p_pos); + pf_put_byte (p_uuid->clock_low, res_len, p_bytes, p_pos); + pf_put_mem (p_uuid->node, sizeof (p_uuid->node), res_len, p_bytes, p_pos); +} +/* ======================== Public functions */ + +void pf_put_mem ( + const void * p_src, + uint16_t src_size, /* Bytes to copy */ + uint16_t res_len, /* Sizeof p_bytes buf */ + uint8_t * p_bytes, + uint16_t * p_pos) +{ + if (((*p_pos) + src_size) > res_len) + { + /* Reached end of buffer */ + LOG_DEBUG (PNET_LOG, "BW(%d): Output buffer is full\n", __LINE__); + p_bytes = NULL; + } + + if (p_bytes != NULL) + { + memcpy (&p_bytes[*p_pos], p_src, src_size); + (*p_pos) += src_size; + } +} + +void pf_put_mem_overlapping ( + const void * p_src, + uint16_t src_size, /* Bytes to copy */ + uint16_t res_len, /* Sizeof p_bytes buf */ + uint8_t * p_bytes, + uint16_t * p_pos) +{ + if (((*p_pos) + src_size) > res_len) + { + /* Reached end of buffer */ + LOG_DEBUG (PNET_LOG, "BW(%d): Output buffer is full\n", __LINE__); + p_bytes = NULL; + } + + if (p_bytes != NULL) + { + memmove (&p_bytes[*p_pos], p_src, src_size); + (*p_pos) += src_size; + } +} + +void pf_put_byte ( + uint8_t val, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + if (*p_pos + 1 > res_len) + { + /* Reached end of buffer */ + LOG_DEBUG (PNET_LOG, "BW(%d): End of buffer reached\n", __LINE__); + p_bytes = NULL; + } + + if (p_bytes != NULL) + { + p_bytes[*p_pos] = val; + (*p_pos)++; + } +} + +void pf_put_uint16 ( + bool is_big_endian, + uint16_t val, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + if (is_big_endian) + { + pf_put_byte ((val >> 8) & 0xff, res_len, p_bytes, p_pos); + pf_put_byte (val & 0xff, res_len, p_bytes, p_pos); + } + else + { + pf_put_byte (val & 0xff, res_len, p_bytes, p_pos); + pf_put_byte ((val >> 8) & 0xff, res_len, p_bytes, p_pos); + } +} + +void pf_put_uint32 ( + bool is_big_endian, + uint32_t val, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + if (is_big_endian) + { + pf_put_byte ((val >> 24) & 0xff, res_len, p_bytes, p_pos); + pf_put_byte ((val >> 16) & 0xff, res_len, p_bytes, p_pos); + pf_put_byte ((val >> 8) & 0xff, res_len, p_bytes, p_pos); + pf_put_byte (val & 0xff, res_len, p_bytes, p_pos); + } + else + { + pf_put_byte (val & 0xff, res_len, p_bytes, p_pos); + pf_put_byte ((val >> 8) & 0xff, res_len, p_bytes, p_pos); + pf_put_byte ((val >> 16) & 0xff, res_len, p_bytes, p_pos); + pf_put_byte ((val >> 24) & 0xff, res_len, p_bytes, p_pos); + } +} + +void pf_put_padding ( + uint16_t n_bytes, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t i; + for (i = 0; i < n_bytes; i++) + { + pf_put_byte (0, res_len, p_bytes, p_pos); + } +} + +void pf_put_padding_align ( + uint16_t start_position, + uint16_t align, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + while (((*p_pos) - start_position) % align != 0) + { + pf_put_byte (0, res_len, p_bytes, p_pos); + } +} + +/** + * @internal + * Insert timestamp + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_time_ts In: The timestamp + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_time_timestamp ( + bool is_big_endian, + const pf_log_book_ts_t * p_time_ts, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint16 (is_big_endian, p_time_ts->sec_hi, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_time_ts->sec_lo, res_len, p_bytes, p_pos); + pf_put_uint32 (is_big_endian, p_time_ts->nano_sec, res_len, p_bytes, p_pos); +} + +void pf_put_ar_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + pf_put_block_header ( + is_big_endian, + PF_BT_AR_BLOCK_RES, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->ar_param.ar_type, + res_len, + p_bytes, + p_pos); + pf_put_uuid (is_big_endian, &p_ar->ar_param.ar_uuid, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->ar_param.session_key, + res_len, + p_bytes, + p_pos); + + pf_put_mem ( + &p_ar->ar_result.cm_responder_mac_add, + sizeof (p_ar->ar_result.cm_responder_mac_add), + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->ar_result.responder_udp_rt_port, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_iocr_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + pf_put_block_header ( + is_big_endian, + PF_BT_IOCR_BLOCK_RES, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->iocrs[ix].result.iocr_type, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->iocrs[ix].result.iocr_reference, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->iocrs[ix].result.frame_id, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_alarm_cr_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + pf_put_block_header ( + is_big_endian, + PF_BT_ALARM_CR_BLOCK_RES, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->alarm_cr_result.alarm_cr_type, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->alarm_cr_result.remote_alarm_reference, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->alarm_cr_result.max_alarm_data_length, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +/** + * @internal + * Insert bits into a uint32_t. + * @param val In: The value of the bit field. + * @param len In: The number of bits to insert. + * @param pos In: The position in p_bits to insert the bits at. + * @param p_bits InOut: The resulting uint32_t. + */ +static void pf_put_bits ( + uint32_t val, + uint16_t len, + uint16_t pos, + uint32_t * p_bits) +{ + /* Take (len) least significant bits from val and insert them into p_bits at + * the correct position. */ + (*p_bits) |= (val & ((1u << len) - 1)) << pos; +} + +/** + * @internal + * Insert a sub-module diff into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_diff In: The sub-module diff information. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_submodule_diff ( + bool is_big_endian, + const pf_submodule_diff_t * p_diff, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint32_t temp_u32 = 0; + uint16_t temp_u16 = 0; + + pf_put_uint16 ( + is_big_endian, + p_diff->subslot_number, + res_len, + p_bytes, + p_pos); + + if (p_diff->submodule_state.ident_info == PF_SUBMOD_PLUG_NO) + { + pf_put_uint32 (is_big_endian, 0, res_len, p_bytes, p_pos); + } + else + { + pf_put_uint32 ( + is_big_endian, + p_diff->submodule_ident_number, + res_len, + p_bytes, + p_pos); + } + /* submodule_state */ + pf_put_bits (p_diff->submodule_state.add_info, 3, 0, &temp_u32); + pf_put_bits (p_diff->submodule_state.advice_avail, 1, 3, &temp_u32); + pf_put_bits (p_diff->submodule_state.maintenance_required, 1, 4, &temp_u32); + pf_put_bits (p_diff->submodule_state.maintenance_demanded, 1, 5, &temp_u32); + pf_put_bits (p_diff->submodule_state.fault, 1, 6, &temp_u32); + pf_put_bits (p_diff->submodule_state.ar_info, 4, 7, &temp_u32); + pf_put_bits (p_diff->submodule_state.ident_info, 4, 11, &temp_u32); + pf_put_bits (p_diff->submodule_state.format_indicator, 1, 15, &temp_u32); + temp_u16 = (uint16_t)temp_u32; + pf_put_uint16 (is_big_endian, temp_u16, res_len, p_bytes, p_pos); +} + +/** + * @internal + * Insert a module diff into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_diff In: The module diff information. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_module_diff ( + bool is_big_endian, + const pf_module_diff_t * p_diff, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t ix; + + pf_put_uint16 (is_big_endian, p_diff->slot_number, res_len, p_bytes, p_pos); + if (p_diff->module_state == PF_MOD_PLUG_NO_MODULE) + { + pf_put_uint32 (is_big_endian, 0, res_len, p_bytes, p_pos); + } + else + { + pf_put_uint32 ( + is_big_endian, + p_diff->module_ident_number, + res_len, + p_bytes, + p_pos); + } + pf_put_uint16 (is_big_endian, p_diff->module_state, res_len, p_bytes, p_pos); + if (p_diff->module_state == PF_MOD_PLUG_NO_MODULE) + { + pf_put_uint32 (is_big_endian, 0, res_len, p_bytes, p_pos); + } + else + { + pf_put_uint16 ( + is_big_endian, + p_diff->nbr_submodule_diffs, + res_len, + p_bytes, + p_pos); + for (ix = 0; ix < p_diff->nbr_submodule_diffs; ix++) + { + pf_put_submodule_diff ( + is_big_endian, + &p_diff->submodule_diffs[ix], + res_len, + p_bytes, + p_pos); + } + } +} + +/** + * @internal + * Insert AR diff blocks into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR diff to insert. + * @param api_ix In: The specific AR diff to add. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_api_diff ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t api_ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t mod_ix; + + pf_put_uint32 ( + is_big_endian, + p_ar->api_diffs[api_ix].api, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->api_diffs[api_ix].nbr_module_diffs, + res_len, + p_bytes, + p_pos); + for (mod_ix = 0; mod_ix < p_ar->api_diffs[api_ix].nbr_module_diffs; mod_ix++) + { + pf_put_module_diff ( + is_big_endian, + &p_ar->api_diffs[api_ix].module_diffs[mod_ix], + res_len, + p_bytes, + p_pos); + } +} + +void pf_put_ar_diff ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint16_t api_ix; + + if ((p_ar != NULL) && (p_ar->nbr_api_diffs > 0)) + { + pf_put_block_header ( + is_big_endian, + PF_BT_MODULE_DIFF_BLOCK, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->nbr_api_diffs, + res_len, + p_bytes, + p_pos); + + for (api_ix = 0; api_ix < p_ar->nbr_api_diffs; api_ix++) + { + pf_put_api_diff (is_big_endian, p_ar, api_ix, res_len, p_bytes, p_pos); + } + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to + correct place + */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); + } +} + +void pf_put_ar_rpc_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + pf_put_block_header ( + is_big_endian, + PF_BT_AR_RPC_BLOCK_RES, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->ar_rpc_result.responder_rpc_server_port, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_ar_server_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + pf_put_block_header ( + is_big_endian, + PF_BT_AR_SERVER_BLOCK, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->ar_server.length_cm_responder_station_name, + res_len, + p_bytes, + p_pos); + pf_put_mem ( + p_ar->ar_server.cm_responder_station_name, + p_ar->ar_server.length_cm_responder_station_name, + res_len, + p_bytes, + p_pos); + + /* Pad with zeroes until block length is a multiple of 4 bytes */ + pf_put_padding_align (block_pos, 4, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +#if PNET_OPTION_AR_VENDOR_BLOCKS +void pf_put_ar_vendor_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + pf_put_block_header ( + is_big_endian, + PF_BT_AR_VENDOR_BLOCK_RES, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint32 ( + is_big_endian, + p_ar->ar_vendor_request[ix].ap_structure_identifier, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_ar->ar_vendor_request[ix].api, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->ar_vendor_result[ix].length_data_response, + res_len, + p_bytes, + p_pos); + pf_put_mem ( + p_ar->ar_vendor_result[ix].data_response, + p_ar->ar_vendor_result[ix].length_data_response, + res_len, + p_bytes, + p_pos); + + /* Pad with zeroes until block length is a multiple of 4 bytes */ + pf_put_padding_align (block_pos, 4, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} +#endif + +/** + * @internal + * Insert an IOCR struct into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: The AR instance. + * @param ix In: The IOCR indice. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_iocr ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint32_t temp32 = 0; + uint8_t data_status = 0; + + pf_put_bits (p_ar->iocrs[ix].param.iocr_properties.rt_class, 4, 0, &temp32); + pf_put_uint32 (is_big_endian, temp32, res_len, p_bytes, p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->iocrs[ix].param.iocr_type, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->iocrs[ix].result.frame_id, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 (is_big_endian, 0, res_len, p_bytes, p_pos); + if ( + (p_ar->iocrs[ix].param.iocr_type == PF_IOCR_TYPE_INPUT) || + (p_ar->iocrs[ix].param.iocr_type == PF_IOCR_TYPE_MC_PROVIDER)) + { + (void)pf_ppm_get_data_status (&p_ar->iocrs[ix].ppm, &data_status); + } + else + { + (void)pf_cpm_get_data_status (&p_ar->iocrs[ix].cpm, &data_status); + } + pf_put_byte (data_status, res_len, p_bytes, p_pos); + pf_put_byte (0, res_len, p_bytes, p_pos); +} + +#if PNET_OPTION_IR +static void pf_put_ir_info ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* ToDo: Not yet implemented */ +} +#endif + +#if PNET_OPTION_SR +static void pf_put_sr_info ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* ToDo: Not yet implemented */ +} +#endif + +#if PNET_OPTION_AR_VENDOR_BLOCKS +static void pf_put_ar_vendor_block_req ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* ToDo: Not yet implemented */ +} +static void pf_put_ar_vendor_block_res ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* ToDo: Not yet implemented */ +} +#endif + +#if PNET_OPTION_FAST_STARTUP +static void pf_put_fsu_data ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* ToDo: Not yet implemented */ +} +#endif + +#if PNET_OPTION_SRL +static void pf_put_srl_data ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* ToDo: Not yet implemented */ +} +#endif +#if PNET_OPTION_RS +static void pf_put_rs_info ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* ToDo: Not yet implemented */ +} +#endif + +/** + * Insert one AR to a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: The AR to insert. + * @param api_filter In: true => Insert only specific API. + * @param api_id In: The api id. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_one_ar ( + bool is_big_endian, + const pf_ar_t * p_ar, + bool api_filter, + uint32_t api_id, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* + (ARUUID, CMInitiatorObjectUUID, ParameterServerObjectUUID (b), ARProperties, + ARType, AlarmCRType, LocalAlarmReference, RemoteAlarmReference, + InitiatorUDPRTPort, ResponderUDPRTPort, StationNameLength, + CMInitiatorStationName, [Padding*] (d), StationNameLength (c), + [ParameterServerStationName], [Padding*] (d), NumberOfIOCRs, [Padding*] (d), + (IOCRProperties, IOCRType, FrameID, APDU_Status (a))*, NumberOfAPIs, + [Padding*] (d), API*, ARDataInfo)* Special case: IOSAR with + ARProperties.DeviceAccess=1: NumberOfIOCRs := 0 AlarmCRType := 0 NumberOfAPIs + := 0 a The APDU_Status.CycleCounter and APDU_Status.TransferStatus can be + zero b The ParameterServerObjectUUID shall be NIL if not used c The + StationNameLength shall be 0 if not used. In this case the field + ParameterServer-StationName shall be omitted d The number of padding octets + shall be adapted to make the following field Unsigned32 aligned. + */ + uint32_t temp32 = 0; + uint16_t ix; + uint16_t nbr_ar_data_pos; + uint16_t nbr_ar_data = 0; +#if PNET_OPTION_PARAMETER_SERVER == 0 + pf_uuid_t nil_uuid = {0}; +#endif + + /* ar_param */ + pf_put_uuid (is_big_endian, &p_ar->ar_param.ar_uuid, res_len, p_bytes, p_pos); + pf_put_uuid ( + is_big_endian, + &p_ar->ar_param.cm_initiator_object_uuid, + res_len, + p_bytes, + p_pos); +#if PNET_OPTION_PARAMETER_SERVER + pf_put_uuid ( + is_big_endian, + &p_ar->prm_server.parameter_server_object_uuid, + res_len, + p_bytes, + p_pos); +#else + pf_put_uuid (is_big_endian, &nil_uuid, res_len, p_bytes, p_pos); +#endif + + pf_put_bits (p_ar->ar_param.ar_properties.state, 3, 0, &temp32); + pf_put_bits ( + p_ar->ar_param.ar_properties.supervisor_takeover_allowed, + 1, + 3, + &temp32); + pf_put_bits ( + p_ar->ar_param.ar_properties.parameterization_server, + 1, + 4, + &temp32); + pf_put_bits (p_ar->ar_param.ar_properties.device_access, 1, 8, &temp32); + pf_put_bits (p_ar->ar_param.ar_properties.companion_ar, 2, 9, &temp32); + pf_put_bits ( + p_ar->ar_param.ar_properties.acknowledge_companion_ar, + 1, + 11, + &temp32); + pf_put_bits ( + p_ar->ar_param.ar_properties.combined_object_container, + 1, + 29, + &temp32); + pf_put_bits (p_ar->ar_param.ar_properties.startup_mode, 1, 30, &temp32); + pf_put_bits ( + p_ar->ar_param.ar_properties.pull_module_alarm_allowed, + 1, + 31, + &temp32); + pf_put_uint32 (is_big_endian, temp32, res_len, p_bytes, p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->ar_param.ar_type, + res_len, + p_bytes, + p_pos); + + /* alarm_cr_request/response */ + pf_put_uint16 ( + is_big_endian, + p_ar->alarm_cr_request.alarm_cr_type, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->alarm_cr_request.local_alarm_reference, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->alarm_cr_result.remote_alarm_reference, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->ar_param.cm_initiator_udp_rt_port, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_ar->ar_result.responder_udp_rt_port, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_ar->ar_param.cm_initiator_station_name_len, + res_len, + p_bytes, + p_pos); + pf_put_mem ( + p_ar->ar_param.cm_initiator_station_name, + p_ar->ar_param.cm_initiator_station_name_len, + res_len, + p_bytes, + p_pos); + while (((*p_pos) % sizeof (uint32_t)) != 0) + { + pf_put_byte (0, res_len, p_bytes, p_pos); + } +#if PNET_OPTION_PARAMETER_SERVER + pf_put_uint16 ( + is_big_endian, + p_ar->prm_server.parameter_server_station_name_len, + res_len, + p_bytes, + p_pos); + pf_put_mem ( + p_ar->prm_server.parameter_server_station_name, + p_ar->prm_server.parameter_server_station_name_len, + res_len, + p_bytes, + p_pos); +#else + pf_put_uint16 (is_big_endian, 0, res_len, p_bytes, p_pos); +#endif + while (((*p_pos) % sizeof (uint32_t)) != 0) + { + pf_put_byte (0, res_len, p_bytes, p_pos); + } + + pf_put_uint16 (is_big_endian, p_ar->nbr_iocrs, res_len, p_bytes, p_pos); + while (((*p_pos) % sizeof (uint32_t)) != 0) + { + pf_put_byte (0, res_len, p_bytes, p_pos); + } + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + pf_put_iocr (is_big_endian, p_ar, ix, res_len, p_bytes, p_pos); + } + + pf_put_uint16 (is_big_endian, p_ar->nbr_exp_apis, res_len, p_bytes, p_pos); + while (((*p_pos) % sizeof (uint32_t)) != 0) + { + pf_put_byte (0, res_len, p_bytes, p_pos); + } + for (ix = 0; ix < p_ar->nbr_exp_apis; ix++) + { + if ((api_filter == false) || (p_ar->exp_apis[ix].api == api_id)) + { + pf_put_uint32 ( + is_big_endian, + p_ar->exp_apis[ix].api, + res_len, + p_bytes, + p_pos); + } + } + + /* Remember pos so the correct value can be inserted later */ + nbr_ar_data_pos = *p_pos; + pf_put_uint16 (is_big_endian, 0, res_len, p_bytes, p_pos); + while (((*p_pos) % sizeof (uint32_t)) != 0) + { + pf_put_byte (0, res_len, p_bytes, p_pos); + } + +#if PNET_OPTION_IR + if (p_ar->nbr_ir_info > 0) + { + pf_put_ir_info (is_big_endian, p_ar, res_len, p_bytes, p_pos); + nbr_ar_data++; + } +#endif +#if PNET_OPTION_SR + if (p_ar->sr_info.valid == true) + { + pf_put_sr_info (is_big_endian, p_ar, res_len, p_bytes, p_pos); + nbr_ar_data++; + } +#endif +#if PNET_OPTION_AR_VENDOR_BLOCKS + for (ix = 0; ix < p_ar->nbr_ar_vendor; ix++) + { + pf_put_ar_vendor_block_req ( + is_big_endian, + p_ar, + ix, + res_len, + p_bytes, + p_pos); + nbr_ar_data++; + } + for (ix = 0; ix < p_ar->nbr_ar_vendor; ix++) + { + pf_put_ar_vendor_block_res ( + is_big_endian, + p_ar, + ix, + res_len, + p_bytes, + p_pos); + nbr_ar_data++; + } +#endif +#if PNET_OPTION_FAST_STARTUP + if (p_ar->fast_startup_data.valid == true) + { + pf_put_fsu_data (is_big_endian, p_ar, res_len, p_bytes, p_pos); + nbr_ar_data++; + } +#endif +#if PNET_OPTION_SRL + if (p_ar->srl_data.valid == true) + { + pf_put_srl_data (is_big_endian, p_ar, res_len, p_bytes, p_pos); + nbr_ar_data++; + } +#endif +#if PNET_OPTION_RS + if (p_ar->rs_info.valid == true) + { + pf_put_rs_info (is_big_endian, p_ar, res_len, p_bytes, p_pos); + nbr_ar_data++; + } +#endif + pf_put_uint16 ( + is_big_endian, + nbr_ar_data, + res_len, + p_bytes, + &nbr_ar_data_pos); +} + +void pf_put_ar_data ( + pnet_t * net, + bool is_big_endian, + const pf_ar_t * p_ar, + uint32_t api_id, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + /* + BlockHeader, NumberOfARs, (AR)* + */ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint16_t data_pos; + pf_device_t * p_device = NULL; + uint16_t cnt; + uint16_t ix; + pf_ar_t * p_ar_tmp = NULL; + + pf_put_block_header ( + is_big_endian, + PF_BT_AR_DATA, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW_1, + res_len, + p_bytes, + p_pos); + + data_pos = *p_pos; + + if (pf_cmdev_get_device (net, &p_device) == 0) + { + cnt = 0; + if (p_ar == NULL) + { + /* Count the number of ARs */ + for (ix = 0; ix < PNET_MAX_AR; ix++) + { + p_ar_tmp = pf_ar_find_by_index (net, ix); + if (p_ar_tmp != NULL) + { + if ( + (p_ar_tmp->ar_param.ar_uuid.data1 != 0) && + (p_ar_tmp->ar_param.cm_initiator_object_uuid.data1 != 0)) + { + cnt++; + } + } + } + } + else + { + /* Count the number of ARs using this API */ + for (ix = 0; ix < p_ar->nbr_exp_apis; ix++) + { + if (p_ar->exp_apis[ix].api == api_id) + { + cnt++; + } + } + } + if (cnt > 0) + { + pf_put_uint16 (is_big_endian, cnt, res_len, p_bytes, p_pos); + } + if (p_ar == NULL) + { + /* Insert the ARs */ + for (ix = 0; ix < PNET_MAX_AR; ix++) + { + p_ar_tmp = pf_ar_find_by_index (net, ix); + if (p_ar_tmp != NULL) + { + if ( + (p_ar_tmp->ar_param.ar_uuid.data1 != 0) && + (p_ar_tmp->ar_param.cm_initiator_object_uuid.data1 != 0)) + { + pf_put_one_ar ( + is_big_endian, + p_ar_tmp, + false, + 0, + res_len, + p_bytes, + p_pos); + } + } + } + } + else + { + /* Insert the ARs using this API */ + for (ix = 0; ix < p_ar->nbr_exp_apis; ix++) + { + if (p_ar->exp_apis[ix].api == api_id) + { + pf_put_one_ar ( + is_big_endian, + p_ar, + true, + api_id, + res_len, + p_bytes, + p_pos); + } + } + } + } + + if (data_pos < *p_pos) + { + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to + correct place + */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); + } + else + { + /* Nothing was inserted!! Report a NULL record */ + *p_pos = block_pos; + } +} + +void pf_put_control ( + bool is_big_endian, + pf_block_type_values_t block_type, + const pf_control_block_t * p_res, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + pf_put_block_header ( + is_big_endian, + block_type, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_byte (0, res_len, p_bytes, p_pos); + pf_put_byte (0, res_len, p_bytes, p_pos); + + pf_put_uuid (is_big_endian, &p_res->ar_uuid, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->session_key, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + p_res->alarm_sequence_number, + res_len, + p_bytes, + p_pos); + + /* Command and properties are always Big-Endian on the wire!! */ + pf_put_uint16 ( + is_big_endian, + p_res->control_command, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_res->control_block_properties, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pnet_status ( + bool is_big_endian, + const pnet_pnio_status_t * p_status, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint32_t pnio_result; + + pnio_result = 0x01000000u * p_status->error_code + + 0x10000u * p_status->error_decode + + 0x100u * p_status->error_code_1 + p_status->error_code_2; + pf_put_uint32 (is_big_endian, pnio_result, res_len, p_bytes, p_pos); +} + +void pf_put_dce_rpc_header ( + pf_rpc_header_t * p_rpc, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos, + uint16_t * p_pos_body_len) +{ + uint32_t temp_u32; + + pf_put_byte (p_rpc->version, res_len, p_bytes, p_pos); + pf_put_byte (p_rpc->packet_type, res_len, p_bytes, p_pos); + /* flags */ + temp_u32 = 0; + pf_put_bits ( + p_rpc->flags.last_fragment ? 1 : 0, + 1, + PF_RPC_F_LAST_FRAGMENT, + &temp_u32); + pf_put_bits (p_rpc->flags.fragment ? 1 : 0, 1, PF_RPC_F_FRAGMENT, &temp_u32); + pf_put_bits (p_rpc->flags.no_fack ? 1 : 0, 1, PF_RPC_F_NO_FACK, &temp_u32); + pf_put_bits (p_rpc->flags.maybe ? 1 : 0, 1, PF_RPC_F_MAYBE, &temp_u32); + pf_put_bits ( + p_rpc->flags.idempotent ? 1 : 0, + 1, + PF_RPC_F_IDEMPOTENT, + &temp_u32); + pf_put_bits ( + p_rpc->flags.broadcast ? 1 : 0, + 1, + PF_RPC_F_BROADCAST, + &temp_u32); + pf_put_byte (temp_u32 % 0x100, res_len, p_bytes, p_pos); + + /* flags2 */ + pf_put_byte (0, res_len, p_bytes, p_pos); + /* Data repr */ + temp_u32 = 0; /* Always ASCII */ + pf_put_bits (p_rpc->is_big_endian ? 0 : 1, 4, 4, &temp_u32); + pf_put_byte (temp_u32 % 0x100, res_len, p_bytes, p_pos); + /* Float repr */ + pf_put_byte (p_rpc->float_repr, res_len, p_bytes, p_pos); + + pf_put_byte (p_rpc->reserved, res_len, p_bytes, p_pos); + pf_put_byte (p_rpc->serial_high, res_len, p_bytes, p_pos); + pf_put_uuid ( + p_rpc->is_big_endian, + &p_rpc->object_uuid, + res_len, + p_bytes, + p_pos); + pf_put_uuid ( + p_rpc->is_big_endian, + &p_rpc->interface_uuid, + res_len, + p_bytes, + p_pos); + pf_put_uuid ( + p_rpc->is_big_endian, + &p_rpc->activity_uuid, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + p_rpc->is_big_endian, + p_rpc->server_boot_time, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + p_rpc->is_big_endian, + p_rpc->interface_version, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + p_rpc->is_big_endian, + p_rpc->sequence_nmb, + res_len, + p_bytes, + p_pos); + pf_put_uint16 (p_rpc->is_big_endian, p_rpc->opnum, res_len, p_bytes, p_pos); + pf_put_uint16 ( + p_rpc->is_big_endian, + p_rpc->interface_hint, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + p_rpc->is_big_endian, + p_rpc->activity_hint, + res_len, + p_bytes, + p_pos); + *p_pos_body_len = *p_pos; + pf_put_uint16 ( + p_rpc->is_big_endian, + p_rpc->length_of_body, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + p_rpc->is_big_endian, + p_rpc->fragment_nmb, + res_len, + p_bytes, + p_pos); + pf_put_byte (p_rpc->auth_protocol, res_len, p_bytes, p_pos); + pf_put_byte (p_rpc->serial_low, res_len, p_bytes, p_pos); +} + +void pf_put_record_data_read ( + bool is_big_endian, + pf_block_type_values_t block_type, + uint16_t raw_length, + const uint8_t * p_raw_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the read operation */ + pf_put_block_header ( + is_big_endian, + block_type, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_mem (p_raw_data, raw_length, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_read_result ( + bool is_big_endian, + const pf_iod_read_result_t * p_res, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos, + uint16_t * p_data_length_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the read operation */ + pf_put_block_header ( + is_big_endian, + PF_BT_READ_RES, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_res->sequence_number, + res_len, + p_bytes, + p_pos); + pf_put_uuid (is_big_endian, &p_res->ar_uuid, res_len, p_bytes, p_pos); + pf_put_uint32 (is_big_endian, p_res->api, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->slot_number, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->subslot_number, res_len, p_bytes, p_pos); + pf_put_byte (0, res_len, p_bytes, p_pos); + pf_put_byte (0, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->index, res_len, p_bytes, p_pos); + *p_data_length_pos = *p_pos; + pf_put_uint32 ( + is_big_endian, + p_res->record_data_length, + res_len, + p_bytes, + p_pos); + pf_put_uint16 (is_big_endian, p_res->add_data_1, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->add_data_2, res_len, p_bytes, p_pos); + pf_put_padding (NELEMENTS (p_res->rw_padding), res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +/** + * @internal + * Insert the sub-slot information into a buffer. + * + * Inserts subslot number and submodule ID. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param block_type In: Specifies REAL or EXP ident number to insert. + * @param p_subslot In: The sub-slot instance. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_ident_subslot ( + bool is_big_endian, + pf_block_type_values_t block_type, + const pf_subslot_t * p_subslot, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint16 ( + is_big_endian, + p_subslot->subslot_nbr, + res_len, + p_bytes, + p_pos); + if (block_type == PF_BT_EXPECTED_IDENTIFICATION_DATA) + { + pf_put_uint32 ( + is_big_endian, + p_subslot->exp_submodule_ident_number, + res_len, + p_bytes, + p_pos); + } + else + { + pf_put_uint32 ( + is_big_endian, + p_subslot->submodule_ident_number, + res_len, + p_bytes, + p_pos); + } +} + +/** + * @internal + * Insert the slot information into a buffer, with filter. + * + * Inserts slot number, module ID, number of subslots and then uses + * \a pf_put_ident_subslot() to insert more info. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param block_type In: Specifies REAL or EXP ident number to insert. + * @param filter_level In: The filter starting level. + * @param stop_level In: The amount of detail to include (ending + * level). + * @param p_ar In: If != NULL then filter by AR. + * @param p_slot In: The slot instance. + * @param subslot_nbr In: Sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_ident_slot ( + bool is_big_endian, + pf_block_type_values_t block_type, + pf_dev_filter_level_t filter_level, /* API_ID, SLOT or SUBSLOT */ + pf_dev_filter_level_t stop_level, /* DEVICE, API_ID, API, SLOT or SUBSLOT */ + const pf_ar_t * p_ar, + const pf_slot_t * p_slot, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t cnt; + uint16_t ix; + const pf_subslot_t * p_subslot; + + pf_put_uint16 (is_big_endian, p_slot->slot_nbr, res_len, p_bytes, p_pos); + if (block_type == PF_BT_EXPECTED_IDENTIFICATION_DATA) + { + pf_put_uint32 ( + is_big_endian, + p_slot->exp_module_ident_number, + res_len, + p_bytes, + p_pos); + } + else + { + pf_put_uint32 ( + is_big_endian, + p_slot->module_ident_number, + res_len, + p_bytes, + p_pos); + } + + /* Count the number of active sub-slots */ + cnt = 0; + for (ix = 0; ix < NELEMENTS (p_slot->subslots); ix++) + { + p_subslot = &p_slot->subslots[ix]; + if (p_subslot->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_subslot->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_SUBSLOT) + { + if (p_subslot->subslot_nbr == subslot_nbr) + { + cnt++; + } + } + else + { + cnt++; + } + } + } + } + + /* Insert number of subslots in use, even if there are none */ + pf_put_uint16 (is_big_endian, cnt, res_len, p_bytes, p_pos); + + /* Now add the actual subslot info - if requested */ + if (stop_level > PF_DEV_FILTER_LEVEL_SLOT) + { + for (ix = 0; ix < NELEMENTS (p_slot->subslots); ix++) + { + p_subslot = &p_slot->subslots[ix]; + if (p_subslot->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_subslot->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_SUBSLOT) + { + if (p_subslot->subslot_nbr == subslot_nbr) + { + /* Call only for the matching subslot_nbr */ + pf_put_ident_subslot ( + is_big_endian, + block_type, + p_subslot, + res_len, + p_bytes, + p_pos); + } + } + else + { + /* No filter: Call for all sub-slots that are in use */ + pf_put_ident_subslot ( + is_big_endian, + block_type, + p_subslot, + res_len, + p_bytes, + p_pos); + } + } + } + } + } +} + +/** + * @internal + * Insert the API information into a buffer, with filter. + * + * Inserts API ID, number of slots and then uses \a pf_put_ident_slot() to + * insert more info. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param block_type In: Specifies REAL or EXP ident number to insert. + * @param filter_level In: The filter starting level. + * @param stop_level In: The amount of detail to include (ending + * level). + * @param p_ar In: If != NULL then filter by AR. + * @param p_api In: The api instance. + * @param slot_nbr In: Slot number to filter by. + * @param subslot_nbr In: Sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_ident_api ( + bool is_big_endian, + pf_block_type_values_t block_type, + pf_dev_filter_level_t filter_level, /* API_ID, SLOT or SUBSLOT */ + pf_dev_filter_level_t stop_level, /* DEVICE, API_ID, API, SLOT or SUBSLOT */ + const pf_ar_t * p_ar, + const pf_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t cnt; + uint16_t ix; + const pf_slot_t * p_slot; + + pf_put_uint32 (is_big_endian, p_api->api_id, res_len, p_bytes, p_pos); + + if (stop_level > PF_DEV_FILTER_LEVEL_API_ID) + { + /* Count the number of active slots */ + cnt = 0; + for (ix = 0; ix < NELEMENTS (p_api->slots); ix++) + { + p_slot = &p_api->slots[ix]; + if (p_slot->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_slot->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_SLOT) + { + if (p_slot->slot_nbr == slot_nbr) + { + cnt++; + } + } + else + { + cnt++; + } + } + } + } + /* Insert number of slots in use, even if there are none */ + pf_put_uint16 (is_big_endian, cnt, res_len, p_bytes, p_pos); + + if (stop_level > PF_DEV_FILTER_LEVEL_API) + { + /* Include slot (module) information */ + for (ix = 0; ix < NELEMENTS (p_api->slots); ix++) + { + p_slot = &p_api->slots[ix]; + if (p_slot->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_slot->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_SLOT) + { + if (p_slot->slot_nbr == slot_nbr) + { + /* No filter: Call for all slots that are in use */ + pf_put_ident_slot ( + is_big_endian, + block_type, + filter_level, + stop_level, + p_ar, + p_slot, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + else + { + /* No filter: Call for all slots that are in use */ + pf_put_ident_slot ( + is_big_endian, + block_type, + filter_level, + stop_level, + p_ar, + p_slot, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + } + } + } + } +} + +/** + * @internal + * Put requested ident information into a buffer, with filter. + * + * Inserts number of APIs, and then uses \a pf_put_ident_api() to insert + * more info. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param block_type In: Specifies REAL or EXP ident number to insert. + * @param filter_level In: The filter starting level. + * @param stop_level In: The amount of detail to include (ending + * level). + * @param p_ar In: If != NULL then filter by AR. + * @param p_device In: The device instance. + * @param api_id In: API id to filter by. + * @param slot_nbr In: Slot number to filter by. + * @param subslot_nbr In: Sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_ident_device ( + bool is_big_endian, + pf_block_type_values_t block_type, + pf_dev_filter_level_t filter_level, /* API_ID, SLOT or SUBSLOT */ + pf_dev_filter_level_t stop_level, /* DEVICE, API_ID, API, SLOT or SUBSLOT */ + const pf_ar_t * p_ar, + const pf_device_t * p_device, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t ix; + uint16_t cnt; + const pf_api_t * p_api = NULL; + + /* Count the number of active APIs */ + cnt = 0; + for (ix = 0; ix < NELEMENTS (p_device->apis); ix++) + { + p_api = &p_device->apis[ix]; + if (p_api->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_api->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_API) + { + if (p_api->api_id == api_id) + { + cnt++; + } + } + else + { + cnt++; + } + } + } + } + + /* Insert number of APIs in use, even if there are none */ + pf_put_uint16 (is_big_endian, cnt, res_len, p_bytes, p_pos); + + if (stop_level > PF_DEV_FILTER_LEVEL_DEVICE) + { + /* Include at least API ID information */ + for (ix = 0; ix < NELEMENTS (p_device->apis); ix++) + { + p_api = &p_device->apis[ix]; + if (p_api->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_api->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_API) + { + if (p_api->api_id == api_id) + { + /* Call only for the matching API_id */ + pf_put_ident_api ( + is_big_endian, + block_type, + filter_level, + stop_level, + p_ar, + p_api, + slot_nbr, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + else + { + /* No filter: Call for all APIs that are in use */ + pf_put_ident_api ( + is_big_endian, + block_type, + filter_level, + stop_level, + p_ar, + p_api, + slot_nbr, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + } + } + } +} + +void pf_put_ident_data ( + pnet_t * net, + bool is_big_endian, + uint8_t block_version_low, + pf_block_type_values_t block_type, + pf_dev_filter_level_t filter_level, /* DEVICE (no filter), API_ID, SLOT or + SUBSLOT */ + pf_dev_filter_level_t stop_level, /* DEVICE, API_ID, API, SLOT or SUBSLOT */ + const pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos; + uint16_t block_len = 0; + uint16_t data_pos; + pf_device_t * p_device = NULL; + + block_pos = *p_pos; + pf_put_block_header ( + is_big_endian, + block_type, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + block_version_low, + res_len, + p_bytes, + p_pos); + + data_pos = *p_pos; + if (pf_cmdev_get_device (net, &p_device) == 0) + { + if (!p_ar || (p_ar->ar_param.ar_type != PF_ART_IOSAR)) + { + pf_put_ident_device ( + is_big_endian, + block_type, + filter_level, + stop_level, + p_ar, + p_device, + api_id, + slot_nbr, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + + if (data_pos < *p_pos) + { + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to + correct place + */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); + } + else + { + /* Nothing was inserted!! Report a NULL record */ + *p_pos = block_pos; + } +} + +void pf_put_im_0_filter_data ( + pnet_t * net, + bool is_big_endian, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + pf_device_t * p_device = NULL; + + /* Insert block header for the read operation */ + block_pos = *p_pos; + pf_put_block_header ( + is_big_endian, + PF_BT_IM_0_FILTER_DATA_SUBMODULE, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_cmdev_get_device (net, &p_device); + /* + * ToDo: Walk the device tree: + * If we encounter a sub-slot that contains I&M data then add it here. + */ + pf_put_ident_device ( + is_big_endian, + PF_BT_REAL_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DEV_FILTER_LEVEL_SUBSLOT, + NULL, + p_device, + 0, + 0, + 1, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); + + /* I&M0FilterDataDevice - insert only the first submodule of the DAP. */ + block_pos = *p_pos; + pf_put_block_header ( + is_big_endian, + PF_BT_IM_0_FILTER_DATA_DEVICE, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + /* ToDo: Find the DAP and then insert it. */ + pf_put_ident_device ( + is_big_endian, + PF_BT_REAL_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DEV_FILTER_LEVEL_SUBSLOT, + NULL, + p_device, + 0, + 0, + 1, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_im_0 ( + bool is_big_endian, + const pnet_im_0_t * p_im_0, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the read operation */ + pf_put_block_header ( + is_big_endian, + PF_BT_IM_0, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_byte (p_im_0->im_vendor_id_hi, res_len, p_bytes, p_pos); + pf_put_byte (p_im_0->im_vendor_id_lo, res_len, p_bytes, p_pos); + pf_put_str ( + p_im_0->im_order_id, + sizeof (p_im_0->im_order_id), + res_len, + p_bytes, + p_pos); + pf_put_str ( + p_im_0->im_serial_number, + sizeof (p_im_0->im_serial_number), + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_im_0->im_hardware_revision, + res_len, + p_bytes, + p_pos); + pf_put_byte (p_im_0->im_sw_revision_prefix, res_len, p_bytes, p_pos); + pf_put_byte ( + p_im_0->im_sw_revision_functional_enhancement, + res_len, + p_bytes, + p_pos); + pf_put_byte (p_im_0->im_sw_revision_bug_fix, res_len, p_bytes, p_pos); + pf_put_byte (p_im_0->im_sw_revision_internal_change, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + p_im_0->im_revision_counter, + res_len, + p_bytes, + p_pos); + pf_put_uint16 (is_big_endian, p_im_0->im_profile_id, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + p_im_0->im_profile_specific_type, + res_len, + p_bytes, + p_pos); + pf_put_byte (p_im_0->im_version_major, res_len, p_bytes, p_pos); + pf_put_byte (p_im_0->im_version_minor, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_im_0->im_supported, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_im_1 ( + bool is_big_endian, + const pnet_im_1_t * p_im_1, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the read operation */ + pf_put_block_header ( + is_big_endian, + PF_BT_IM_1, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_str ( + p_im_1->im_tag_function, + sizeof (p_im_1->im_tag_function), + res_len, + p_bytes, + p_pos); + pf_put_str ( + p_im_1->im_tag_location, + sizeof (p_im_1->im_tag_location), + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_im_2 ( + bool is_big_endian, + const pnet_im_2_t * p_im_2, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the read operation */ + pf_put_block_header ( + is_big_endian, + PF_BT_IM_2, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_str (p_im_2, sizeof (pnet_im_2_t), res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_im_3 ( + bool is_big_endian, + const pnet_im_3_t * p_im_3, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the read operation */ + pf_put_block_header ( + is_big_endian, + PF_BT_IM_3, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_str (p_im_3, sizeof (pnet_im_3_t), res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_record_data_write ( + bool is_big_endian, + pf_block_type_values_t block_type, + uint16_t raw_length, + uint8_t * p_raw_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the read operation */ + pf_put_block_header ( + is_big_endian, + block_type, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_mem (p_raw_data, raw_length, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_write_result ( + bool is_big_endian, + const pf_iod_write_result_t * p_res, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the write operation */ + pf_put_block_header ( + is_big_endian, + PF_BT_WRITE_RES, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_res->sequence_number, + res_len, + p_bytes, + p_pos); + pf_put_uuid (is_big_endian, &p_res->ar_uuid, res_len, p_bytes, p_pos); + pf_put_uint32 (is_big_endian, p_res->api, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->slot_number, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->subslot_number, res_len, p_bytes, p_pos); + pf_put_byte (0, res_len, p_bytes, p_pos); + pf_put_byte (0, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->index, res_len, p_bytes, p_pos); + pf_put_uint32 ( + is_big_endian, + p_res->record_data_length, + res_len, + p_bytes, + p_pos); + pf_put_uint16 (is_big_endian, p_res->add_data_1, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, p_res->add_data_2, res_len, p_bytes, p_pos); + pf_put_pnet_status ( + is_big_endian, + &p_res->pnio_status, + res_len, + p_bytes, + p_pos); + + pf_put_padding (NELEMENTS (p_res->rw_padding), res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); + + if (*p_pos >= res_len) + { + LOG_ERROR ( + PNET_LOG, + "BW(%d): Output buffer is filled, while preparing DCP Write " + "response.\n", + __LINE__); + } +} + +void pf_put_log_book_data ( + bool is_big_endian, + const pf_log_book_t * p_log_book, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint16_t ix; + uint16_t cnt = 0; + + /* Insert block header for the write operation */ + pf_put_block_header ( + is_big_endian, + PF_BT_LOG_BOOK_DATA, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + if (p_log_book->wrap == true) + { + ix = p_log_book->put + 1; + if (ix >= NELEMENTS (p_log_book->entries)) + { + ix = 0; + } + cnt = NELEMENTS (p_log_book->entries); + } + else + { + ix = 0; + cnt = p_log_book->put; + } + + pf_put_time_timestamp ( + is_big_endian, + &p_log_book->time_ts, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 (is_big_endian, cnt, res_len, p_bytes, p_pos); + + while (ix != p_log_book->put) + { + pf_put_time_timestamp ( + is_big_endian, + &p_log_book->entries[ix].time_ts, + res_len, + p_bytes, + p_pos); + pf_put_uuid ( + is_big_endian, + &p_log_book->entries[ix].ar_uuid, + res_len, + p_bytes, + p_pos); + pf_put_pnet_status ( + is_big_endian, + &p_log_book->entries[ix].pnio_status, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_log_book->entries[ix].entry_detail, + res_len, + p_bytes, + p_pos); + ix++; + if (ix >= NELEMENTS (p_log_book->entries)) + { + ix = 0; + } + } + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +/** + * @internal + * Insert one diagnosis item to a buffer. + * + * The resulting format is controlled by the USI of the diagnosis item: + * - Manufacturer specific format + * - Channel diagnosis (standard format) + * - Extended channel diagnosis (standard format) + * - Qualified channel diagnosis (standard format) + * + * Used both for creating an alarm frame and a diagnostics read answer frame. + * + * @param is_big_endian In: true if buffer is big-endian. + * @param p_item In: The diag item to insert. + * @param insert_usi In: Insert USI field if true. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_diag_item ( + bool is_big_endian, + const pf_diag_item_t * p_item, + bool insert_usi, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + if (insert_usi == true) + { + pf_put_uint16 (is_big_endian, p_item->usi, res_len, p_bytes, p_pos); + } + + switch (p_item->usi) + { + case PF_USI_CHANNEL_DIAGNOSIS: + /* Insert std format diagnosis */ + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_nbr, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_properties, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_error_type, + res_len, + p_bytes, + p_pos); + break; + case PF_USI_EXTENDED_CHANNEL_DIAGNOSIS: + /* Insert std format diagnosis */ + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_nbr, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_properties, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_error_type, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ext_ch_error_type, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_item->fmt.std.ext_ch_add_value, + res_len, + p_bytes, + p_pos); + break; + case PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS: + /* Insert std format diagnosis */ + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_nbr, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_properties, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ch_error_type, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_item->fmt.std.ext_ch_error_type, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_item->fmt.std.ext_ch_add_value, + res_len, + p_bytes, + p_pos); + + pf_put_uint32 ( + is_big_endian, + p_item->fmt.std.qual_ch_qualifier, + res_len, + p_bytes, + p_pos); + break; + default: + pf_put_mem ( + p_item->fmt.usi.manuf_data, + p_item->fmt.usi.len, + res_len, + p_bytes, + p_pos); + break; + } +} + +/** + * @internal + * Insert a diagnosis item list (for a subslot) into a buffer. + * + * Note that all diagnoses inserted into this block in the buffer must be for + * the same API, slot, subslot and USI. + * + * Insertion is done via pf_put_diag_item() + * + * @param net InOut: The p-net stack instance + * @param is_big_endian In: true if buffer is big-endian. + * @param diag_filter In: Type of diag items to insert. + * @param usi_filter In: USI value to filter by. Mandatory. + * @param api_id In: The API number to write to buffer. + * @param slot_nbr In: The slot number to write to buffer. + * @param subslot_nbr In: The subslot number to write to buffer. + * @param list_head In: Index of first item in the list to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_diag_list ( + pnet_t * net, + bool is_big_endian, + pf_diag_filter_level_t diag_filter, + uint16_t usi_filter, + uint16_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t list_head, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t ch_properties = 0; + pf_diag_item_t * p_item = NULL; + bool insert; + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint16_t data_pos; + + /* Walk the list to insert all items */ + pf_cmdev_get_diag_item (net, list_head, &p_item); + if (p_item != NULL) + { + /* Insert block header */ + pf_put_block_header ( + is_big_endian, + PF_BT_DIAGNOSIS_DATA, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW_1, + res_len, + p_bytes, + p_pos); + + /* Insert API, slot and subslot */ + pf_put_uint32 (is_big_endian, api_id, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, slot_nbr, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, subslot_nbr, res_len, p_bytes, p_pos); + + /* Insert summary-channel, and its properties + * The list only contains APPEARS, so it can be hardcoded here. + * ToDo: More info into ch_properties here! + */ + pf_put_uint16 ( + is_big_endian, + PNET_CHANNEL_WHOLE_SUBMODULE, + res_len, + p_bytes, + p_pos); + PF_DIAG_CH_PROP_SPEC_SET (ch_properties, PF_DIAG_CH_PROP_SPEC_APPEARS); + pf_put_uint16 (is_big_endian, ch_properties, res_len, p_bytes, p_pos); + + /* Insert USI */ + pf_put_uint16 (is_big_endian, usi_filter, res_len, p_bytes, p_pos); + + data_pos = *p_pos; + while (p_item != NULL) + { + + /* Filter based on diagnosis type */ + insert = false; + switch (diag_filter) + { + case PF_DIAG_FILTER_FAULT_STD: + if (p_item->usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + insert = true; + } + break; + case PF_DIAG_FILTER_FAULT_ALL: + if ( + (p_item->usi < PF_USI_CHANNEL_DIAGNOSIS) || + (PF_DIAG_CH_PROP_MAINT_GET (p_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_FAULT)) + { + insert = true; + } + break; + case PF_DIAG_FILTER_ALL: + insert = true; + break; + case PF_DIAG_FILTER_M_REQ: + if ( + (p_item->usi < PF_USI_CHANNEL_DIAGNOSIS) || + (PF_DIAG_CH_PROP_MAINT_GET (p_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_REQUIRED)) + { + insert = true; + } + break; + case PF_DIAG_FILTER_M_DEM: + if ( + (p_item->usi < PF_USI_CHANNEL_DIAGNOSIS) || + (PF_DIAG_CH_PROP_MAINT_GET (p_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_DEMANDED)) + { + insert = true; + } + break; + } + + /* Filter based on USI value */ + if (p_item->usi != usi_filter) + { + insert = false; + } + + if (insert == true) + { + /* Insert diagnosis item */ + pf_put_diag_item ( + is_big_endian, + p_item, + false, /* Do not insert USI value */ + res_len, + p_bytes, + p_pos); + } + + pf_cmdev_get_diag_item (net, p_item->next, &p_item); + } + + /* Finally insert the block length into the block header */ + if (*p_pos > data_pos) + { + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); + } + else + { + /* Do not insert an empty block. */ + *p_pos = block_pos; + } + } +} + +/** + * @internal + * Insert diagnosis items of a subslot into a buffer. + * + * This is done by calling pf_put_diag_list() for USI values of found diagnoses. + * + * @param net InOut: The p-net stack instance + * @param is_big_endian In: true if buffer is big-endian. + * @param diag_filter In: Type of diag items to insert. + * @param api_id In: The API number to write to buffer. + * @param slot_nbr In: The slot number to write to buffer. + * @param p_subslot In: The subslot instance. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_diag_subslot ( + pnet_t * net, + bool is_big_endian, + pf_diag_filter_level_t diag_filter, + uint16_t api_id, + uint16_t slot_nbr, + const pf_subslot_t * p_subslot, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t usi = 0; + int err = + pf_cmdev_get_next_diagnosis_usi (net, p_subslot->diag_list, 0, &usi); + + while (err == 0) + { + pf_put_diag_list ( + net, + is_big_endian, + diag_filter, + usi, + api_id, + slot_nbr, + p_subslot->subslot_nbr, + p_subslot->diag_list, + res_len, + p_bytes, + p_pos); + + err = + pf_cmdev_get_next_diagnosis_usi (net, p_subslot->diag_list, usi, &usi); + } +} + +/** + * @internal + * Insert diagnosis items of a slot into a buffer. + * + * This is done by calling pf_put_diag_subslot() for relevant subslots. + * + * @param net InOut: The p-net stack instance + * @param is_big_endian In: true if buffer is big-endian. + * @param filter_level In: The filter ending level. + * @param diag_filter In: The types of diag to insert. + * @param p_ar In: If != NULL then filter by AR. + * @param api_id In: API to insert + * @param p_slot In: The slot instance. + * @param subslot_nbr In: The sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_diag_slot ( + pnet_t * net, + bool is_big_endian, + pf_dev_filter_level_t filter_level, + pf_diag_filter_level_t diag_filter, + const pf_ar_t * p_ar, + uint32_t api_id, + const pf_slot_t * p_slot, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t ix; + const pf_subslot_t * p_subslot; + + for (ix = 0; ix < NELEMENTS (p_slot->subslots); ix++) + { + p_subslot = &p_slot->subslots[ix]; + if (p_subslot->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_subslot->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_SUBSLOT) + { + if (p_subslot->subslot_nbr == subslot_nbr) + { + /* Call only for the matching subslot_nbr */ + pf_put_diag_subslot ( + net, + is_big_endian, + diag_filter, + api_id, + p_slot->slot_nbr, + p_subslot, + res_len, + p_bytes, + p_pos); + } + } + else + { + /* No filter: Call for all sub-slots that are in use */ + pf_put_diag_subslot ( + net, + is_big_endian, + diag_filter, + api_id, + p_slot->slot_nbr, + p_subslot, + res_len, + p_bytes, + p_pos); + } + } + } + } +} + +/** + * @internal + * Insert diagnosis items of an API into a buffer. + * + * This is done by calling pf_put_diag_slot() for relevant slots. + * + * @param net InOut: The p-net stack instance + * @param is_big_endian In: true if buffer is big-endian. + * @param filter_level In: The filter ending level. + * @param diag_filter In: The types of diag to insert. + * @param p_ar In: If != NULL then filter by AR. + * @param p_api In: The API instance. + * @param slot_nbr In: The slot number to filter by. + * @param subslot_nbr In: The sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_diag_api ( + pnet_t * net, + bool is_big_endian, + pf_dev_filter_level_t filter_level, + pf_diag_filter_level_t diag_filter, + const pf_ar_t * p_ar, + const pf_api_t * p_api, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t ix; + const pf_slot_t * p_slot; + + for (ix = 0; ix < NELEMENTS (p_api->slots); ix++) + { + p_slot = &p_api->slots[ix]; + if (p_slot->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_slot->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_SLOT) + { + if (p_slot->slot_nbr == slot_nbr) + { + /* Call only for the matching slot_nbr */ + pf_put_diag_slot ( + net, + is_big_endian, + filter_level, + diag_filter, + p_ar, + p_api->api_id, + p_slot, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + else + { + /* No filter: Call for all slots that are in use */ + pf_put_diag_slot ( + net, + is_big_endian, + filter_level, + diag_filter, + p_ar, + p_api->api_id, + p_slot, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + } + } +} + +/** + * @internal + * Insert diagnosis items of a device into a buffer. + * + * This is done by calling pf_put_diag_api() for all relevant APIs. + * + * @param net InOut: The p-net stack instance + * @param is_big_endian In: true if buffer is big-endian. + * @param filter_level In: The filter ending level. + * @param diag_filter In: The types of diag to insert. + * @param p_ar In: If != NULL then filter by AR. + * @param p_device In: The device instance. + * @param api_id In: The API id to filter by. + * @param slot_nbr In: The slot number to filter by. + * @param subslot_nbr In: The sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_diag_device ( + pnet_t * net, + bool is_big_endian, + pf_dev_filter_level_t filter_level, + pf_diag_filter_level_t diag_filter, + const pf_ar_t * p_ar, /* If != NULL only include those belonging to p_ar */ + const pf_device_t * p_device, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t ix; + const pf_api_t * p_api; + + for (ix = 0; ix < NELEMENTS (p_device->apis); ix++) + { + p_api = &p_device->apis[ix]; + if (p_api->in_use == true) + { + if ((p_ar == NULL) || (p_ar == p_api->p_ar)) + { + if (filter_level >= PF_DEV_FILTER_LEVEL_API) + { + if (p_api->api_id == api_id) + { + /* Call only for the matching API_id */ + pf_put_diag_api ( + net, + is_big_endian, + filter_level, + diag_filter, + p_ar, + p_api, + slot_nbr, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + else + { + /* No filter: Call for all APIs that are in use */ + pf_put_diag_api ( + net, + is_big_endian, + filter_level, + diag_filter, + p_ar, + p_api, + slot_nbr, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } + } + } + } +} + +void pf_put_diag_data ( + pnet_t * net, + bool is_big_endian, + pf_dev_filter_level_t filter_level, + pf_diag_filter_level_t diag_filter, + const pf_ar_t * p_ar, /* If p_ar != NULL only include those belonging to p_ar + */ + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_device_t * p_device = NULL; + + if (pf_cmdev_get_device (net, &p_device) == 0) + { + pf_put_diag_device ( + net, + is_big_endian, + filter_level, + diag_filter, + p_ar, + p_device, + api_id, + slot_nbr, + subslot_nbr, + res_len, + p_bytes, + p_pos); + } +} + +void pf_put_alarm_fixed ( + bool is_big_endian, + const pf_alarm_fixed_t * p_alarm_fixed, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint32_t temp_u32; + uint8_t temp_u8; + + /* The fixed part is not a "block" so do not insert a block header */ + + /* Destination and source */ + pf_put_uint16 ( + is_big_endian, + p_alarm_fixed->dst_ref, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_alarm_fixed->src_ref, + res_len, + p_bytes, + p_pos); + + /* Flags for PDU type, version, window size and transport ACK */ + temp_u32 = 0; + pf_put_bits (p_alarm_fixed->pdu_type.type, 4, 0, &temp_u32); + pf_put_bits (p_alarm_fixed->pdu_type.version, 4, 4, &temp_u32); + temp_u8 = (uint8_t)temp_u32; + pf_put_byte (temp_u8, res_len, p_bytes, p_pos); + + temp_u32 = 0; + pf_put_bits (p_alarm_fixed->add_flags.window_size, 4, 0, &temp_u32); + pf_put_bits (p_alarm_fixed->add_flags.tack, 1, 4, &temp_u32); + temp_u8 = (uint8_t)temp_u32; + pf_put_byte (temp_u8, res_len, p_bytes, p_pos); + + /* Sequence numbers for sending and ACK */ + pf_put_uint16 ( + is_big_endian, + p_alarm_fixed->send_seq_num, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_alarm_fixed->ack_seq_nbr, + res_len, + p_bytes, + p_pos); +} + +/** + * @internal + * Insert maint_status into a buffer + * + * @param is_big_endian In: true if buffer is big-endian. + * @param maint_status In: Maintenance status. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_maint_status ( + bool is_big_endian, + uint32_t maint_status, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = 0; + uint16_t block_len = 0; + + pf_put_uint16 (is_big_endian, PF_USI_MAINTENANCE, res_len, p_bytes, p_pos); + + block_pos = *p_pos; + pf_put_block_header ( + is_big_endian, + PF_BT_MAINTENANCE_ITEM, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_byte (0, res_len, p_bytes, p_pos); + pf_put_byte (0, res_len, p_bytes, p_pos); + + pf_put_uint32 (is_big_endian, maint_status, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + /* Point to correct place */ + block_pos += offsetof (pf_block_header_t, block_length); + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_alarm_block ( + bool is_big_endian, + pf_block_type_values_t bh_type, + const pf_alarm_data_t * p_alarm_data, + uint32_t maint_status, + uint16_t payload_usi, /* pf_usi_values_t */ + uint16_t payload_len, + const uint8_t * p_payload, /* Union of may types - see below */ + const pnet_pnio_status_t * p_status, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint32_t temp_u16; + uint32_t temp_u32; + + /* Insert block header for the alarm block */ + pf_put_block_header ( + is_big_endian, + bh_type, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + /* Insert alarm type */ + pf_put_uint16 ( + is_big_endian, + p_alarm_data->alarm_type, + res_len, + p_bytes, + p_pos); + + /* Insert API, slot and subslot */ + pf_put_uint32 (is_big_endian, p_alarm_data->api_id, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + p_alarm_data->slot_nbr, + res_len, + p_bytes, + p_pos); + pf_put_uint16 ( + is_big_endian, + p_alarm_data->subslot_nbr, + res_len, + p_bytes, + p_pos); + + /* Insert module and submodule info for AlarmNotification, but not for + * AlarmAck */ + if ( + bh_type == PF_BT_ALARM_NOTIFICATION_LOW || + bh_type == PF_BT_ALARM_NOTIFICATION_HIGH) + { + pf_put_uint32 ( + is_big_endian, + p_alarm_data->module_ident, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_alarm_data->submodule_ident, + res_len, + p_bytes, + p_pos); + } + + /* Insert sequence number and boolean flags */ + temp_u32 = 0; + pf_put_bits (p_alarm_data->sequence_number, 11, 0, &temp_u32); + pf_put_bits ( + p_alarm_data->alarm_specifier.channel_diagnosis, + 1, + 11, + &temp_u32); + pf_put_bits ( + p_alarm_data->alarm_specifier.manufacturer_diagnosis, + 1, + 12, + &temp_u32); + pf_put_bits ( + p_alarm_data->alarm_specifier.submodule_diagnosis, + 1, + 13, + &temp_u32); + pf_put_bits (p_alarm_data->alarm_specifier.ar_diagnosis, 1, 15, &temp_u32); + temp_u16 = (uint16_t)temp_u32; + pf_put_uint16 (is_big_endian, temp_u16, res_len, p_bytes, p_pos); + + if ( + bh_type == PF_BT_ALARM_NOTIFICATION_LOW || + bh_type == PF_BT_ALARM_NOTIFICATION_HIGH) + { + /* AlarmNotification DATA message */ + + switch (payload_usi) + { + case 0: + /* Nothing */ + break; + case PF_USI_MAINTENANCE: + /* This is actually an error */ + LOG_ERROR (PNET_LOG, "BW(%d): Illegal USI\n", payload_usi); + break; + case PF_USI_CHANNEL_DIAGNOSIS: + case PF_USI_EXTENDED_CHANNEL_DIAGNOSIS: + case PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS: + /* Insert a maintenance item before the diagnosis item */ + if (maint_status != 0) + { + pf_put_maint_status ( + is_big_endian, + maint_status, + res_len, + p_bytes, + p_pos); + } + pf_put_diag_item ( + is_big_endian, + (pf_diag_item_t *)p_payload, + true, /* Insert the USI field */ + res_len, + p_bytes, + p_pos); + break; + default: + /* Manufacturer data */ + /* Starts with the USI and then followed by raw data bytes */ + pf_put_uint16 (is_big_endian, payload_usi, res_len, p_bytes, p_pos); + if (payload_len > 0) + { + pf_put_mem (p_payload, payload_len, res_len, p_bytes, p_pos); + } + break; + } + } + else + { + /* AlarmAck DATA message */ + pf_put_pnet_status (true, p_status, PF_FRAME_BUFFER_SIZE, p_bytes, p_pos); + } + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_substitute_data ( + bool is_big_endian, + uint16_t sub_mode, + uint8_t iocs_len, + const uint8_t * p_iocs, + uint8_t iops_len, + const uint8_t * p_iops, + uint16_t data_len, + const uint8_t * p_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Now the substitution data - this is an inner block (ToDo: Make own + * function) */ + pf_put_block_header ( + is_big_endian, + PF_BT_SUBSTITUTE_VALUE, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 (is_big_endian, sub_mode, res_len, p_bytes, p_pos); + pf_put_mem (p_iocs, iocs_len, res_len, p_bytes, p_pos); + pf_put_mem (p_data, data_len, res_len, p_bytes, p_pos); + pf_put_mem (p_iops, iops_len, res_len, p_bytes, p_pos); + + /* Insert the block length into the substitution data block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_output_data ( + bool is_big_endian, + bool sub_active, + uint8_t iocs_len, + const uint8_t * p_iocs, + uint8_t iops_len, + const uint8_t * p_iops, + uint16_t data_len, + const uint8_t * p_data, + uint16_t sub_mode, + const uint8_t * p_sub_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint16_t substitute_active_flag = sub_active ? 1 : 0; + + /* Insert block header for the output block */ + pf_put_block_header ( + is_big_endian, + PF_BT_RECORD_OUTPUT_DATA_OBJECT, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + substitute_active_flag, + res_len, + p_bytes, + p_pos); + + pf_put_byte (iocs_len, res_len, p_bytes, p_pos); + pf_put_byte (iops_len, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, data_len, res_len, p_bytes, p_pos); + + pf_put_mem (p_iocs, iocs_len, res_len, p_bytes, p_pos); + pf_put_mem (p_data, data_len, res_len, p_bytes, p_pos); + pf_put_mem (p_iops, iops_len, res_len, p_bytes, p_pos); + + /* -------------- */ + if (p_sub_data != NULL) + { + pf_put_substitute_data ( + is_big_endian, + sub_mode, + iocs_len, + p_iocs, + iops_len, + p_iops, + data_len, + p_sub_data, + res_len, + p_bytes, + p_pos); + } + /* -------------- */ + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_input_data ( + bool is_big_endian, + uint8_t iocs_len, + const uint8_t * p_iocs, + uint8_t iops_len, + const uint8_t * p_iops, + uint16_t data_len, + const uint8_t * p_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Insert block header for the output block */ + pf_put_block_header ( + is_big_endian, + PF_BT_RECORD_INPUT_DATA_OBJECT, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_byte (iocs_len, res_len, p_bytes, p_pos); + pf_put_mem (p_iocs, iocs_len, res_len, p_bytes, p_pos); + + pf_put_byte (iops_len, res_len, p_bytes, p_pos); + pf_put_mem (p_iops, iops_len, res_len, p_bytes, p_pos); + + pf_put_uint16 (is_big_endian, data_len, res_len, p_bytes, p_pos); + pf_put_mem (p_data, data_len, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pdport_data_check ( + bool is_big_endian, + uint16_t slot, + uint16_t subslot, + const pf_check_peer_t * check_peer, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_PDPORTCHECK, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* Slot and subslot info */ + pf_put_uint16 (is_big_endian, slot, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, subslot, res_len, p_bytes, p_pos); + + /* PF_BT_CHECKPEERS Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_CHECKPEERS, + 5 + check_peer->length_peer_port_name + + check_peer->length_peer_station_name, + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + /* Number of Peers */ + pf_put_byte (1, res_len, p_bytes, p_pos); + + /* Length peer_id */ + pf_put_byte (check_peer->length_peer_port_name, res_len, p_bytes, p_pos); + + /* peer_id */ + pf_put_mem ( + &check_peer->peer_port_name, + check_peer->length_peer_port_name, + res_len, + p_bytes, + p_pos); + + /* Length ChassisID */ + pf_put_byte (check_peer->length_peer_station_name, res_len, p_bytes, p_pos); + + /* ChassisID */ + pf_put_mem ( + &check_peer->peer_station_name, + check_peer->length_peer_station_name, + res_len, + p_bytes, + p_pos); + + pf_put_padding_align (block_pos, 4, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pdport_data_real ( + bool is_big_endian, + uint16_t subslot, + const pf_lldp_station_name_t * p_peer_station_name, + const pf_lldp_port_name_t * p_peer_port_name, + const pf_port_t * p_port_data, + pf_mediatype_values_t media_type, + const pnal_eth_status_t * p_eth_status, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint16_t link_state = 0; + uint8_t num_peers = p_port_data->lldp.is_peer_info_received ? 1 : 0; + const pf_lldp_peer_info_t * p_peer_info = &p_port_data->lldp.peer_info; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_PDPORTDATAREAL, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* Slot and subslot */ + pf_put_uint16 (is_big_endian, PNET_SLOT_DAP_IDENT, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, subslot, res_len, p_bytes, p_pos); + + /* Length OwnPortName */ + pf_put_byte (strlen (p_port_data->port_name), res_len, p_bytes, p_pos); + + /* OwnPortName */ + pf_put_mem ( + p_port_data->port_name, + strlen (p_port_data->port_name), + res_len, + p_bytes, + p_pos); + + /* Number of Peers */ + pf_put_byte (num_peers, res_len, p_bytes, p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* Peer info */ + if (num_peers > 0) + { + /* Peer port name */ + pf_put_byte (p_peer_port_name->len, res_len, p_bytes, p_pos); + pf_put_mem ( + p_peer_port_name->string, + p_peer_port_name->len, + res_len, + p_bytes, + p_pos); + + /* Peer station name */ + pf_put_byte (p_peer_station_name->len, res_len, p_bytes, p_pos); + pf_put_mem ( + p_peer_station_name->string, + p_peer_station_name->len, + res_len, + p_bytes, + p_pos); + + pf_put_padding_align (block_pos, 4, res_len, p_bytes, p_pos); + + /* Line Delay */ + pf_put_uint32 ( + is_big_endian, + p_peer_info->line_delay, + res_len, + p_bytes, + p_pos); + + /* Peer MAC Addr */ + pf_put_mem ( + p_peer_info->mac_address.addr, + sizeof (p_peer_info->mac_address.addr), + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + } /* Peer info */ + + /* MAUType */ + pf_put_uint16 ( + is_big_endian, + p_eth_status->operational_mau_type, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* Domain Boundary */ + /* Todo- hardcoded value*/ + pf_put_uint32 (is_big_endian, 0, res_len, p_bytes, p_pos); + + /* Multicast Boundary */ + /* Todo- hardcoded value*/ + pf_put_uint32 (is_big_endian, 0, res_len, p_bytes, p_pos); + + /* PN-AL-protocol (Mar20) section 5.2.13.23 for LinkState encoding */ + /* LinkState.Link */ + if (p_eth_status->running) + { + link_state = PF_PD_LINK_STATE_LINK_UP; + } + else + { + link_state = PF_PD_LINK_STATE_LINK_DOWN; + } + + /* LinkState.Port */ + link_state |= (PF_PD_LINK_STATE_PORT_UNKNOWN << 8); + + pf_put_uint16 (is_big_endian, link_state, res_len, p_bytes, p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + pf_put_uint32 (is_big_endian, media_type, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pdport_statistics ( + bool is_big_endian, + const pnal_port_stats_t * p_port_stats, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_PORT_STATISTICS, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + pf_put_uint32 ( + is_big_endian, + p_port_stats->if_in_octets, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_port_stats->if_out_octets, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_port_stats->if_in_discards, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_port_stats->if_out_discards, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_port_stats->if_in_errors, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + p_port_stats->if_out_errors, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pdinterface_data_real ( + bool is_big_endian, + const pnet_ethaddr_t * mac_address, + pnal_ipaddr_t ip_address, + pnal_ipaddr_t netmask, + pnal_ipaddr_t gateway, + const char * station_name, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_INTERFACE_REAL_DATA, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + /* Station name */ + /* If station name length is 0, maybe mac string shall be used instead */ + pf_put_byte ((uint8_t)strlen (station_name), res_len, p_bytes, p_pos); + pf_put_mem (station_name, strlen (station_name), res_len, p_bytes, p_pos); + + pf_put_padding_align (block_pos, 4, res_len, p_bytes, p_pos); + + /* DAP interface MAC address */ + pf_put_mem ( + &mac_address->addr, + sizeof (pnet_ethaddr_t), + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* IP Address */ + pf_put_uint32 (is_big_endian, ip_address, res_len, p_bytes, p_pos); + + /* Subnet Mask */ + pf_put_uint32 (is_big_endian, netmask, res_len, p_bytes, p_pos); + + /* Router */ + pf_put_uint32 (is_big_endian, gateway, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pd_multiblock_interface_and_statistics ( + bool is_big_endian, + uint32_t api, + const pnet_ethaddr_t * mac_address, + pnal_ipaddr_t ip_address, + pnal_ipaddr_t netmask, + pnal_ipaddr_t gateway, + const char * station_name, + const pnal_port_stats_t * p_port_statistics, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_MULTIPLEBLOCK_HEADER, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* API */ + pf_put_uint32 (is_big_endian, api, res_len, p_bytes, p_pos); + + /* Slot and subslot */ + pf_put_uint16 (is_big_endian, PNET_SLOT_DAP_IDENT, res_len, p_bytes, p_pos); + pf_put_uint16 ( + is_big_endian, + PNET_SUBSLOT_DAP_INTERFACE_1_IDENT, + res_len, + p_bytes, + p_pos); + + /*PDInterfaceDataReal*/ + pf_put_pdinterface_data_real ( + is_big_endian, + mac_address, + ip_address, + netmask, + gateway, + station_name, + res_len, + p_bytes, + p_pos); + + /*PDPortStatistics*/ + pf_put_pdport_statistics ( + is_big_endian, + p_port_statistics, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pd_multiblock_port_and_statistics ( + bool is_big_endian, + uint32_t api, + uint16_t subslot, + const pf_lldp_station_name_t * p_peer_station_name, + const pf_lldp_port_name_t * p_peer_port_name, + const pf_port_t * p_port_data, + pf_mediatype_values_t media_type, + const pnal_eth_status_t * p_eth_status, + const pnal_port_stats_t * p_port_statistics, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_MULTIPLEBLOCK_HEADER, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* API */ + pf_put_uint32 (is_big_endian, api, res_len, p_bytes, p_pos); + + /* Slot and subslot */ + pf_put_uint16 (is_big_endian, PNET_SLOT_DAP_IDENT, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, subslot, res_len, p_bytes, p_pos); + + /*PDPortDataReal*/ + pf_put_pdport_data_real ( + true, + subslot, + p_peer_station_name, + p_peer_port_name, + p_port_data, + media_type, + p_eth_status, + res_len, + p_bytes, + p_pos); + + /*PDPortStatistics*/ + pf_put_pdport_statistics ( + is_big_endian, + p_port_statistics, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +/** + * @internal + * Insert peer to peer boundary + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_peer_to_peer_boundary In: The p-net stack instance + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_peer_to_peer_boundary ( + bool is_big_endian, + const pf_adjust_peer_to_peer_boundary_t * p_peer_to_peer_boundary, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint32_t temp_u32 = 0; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_PEER_TO_PEER_BOUNDARY, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* Adjusted Peer to Peer Boundary */ + temp_u32 = 0; + pf_put_bits ( + p_peer_to_peer_boundary->peer_to_peer_boundary.do_not_send_LLDP_frames, + 1, + 0, + &temp_u32); + pf_put_bits ( + p_peer_to_peer_boundary->peer_to_peer_boundary + .do_not_send_pctp_delay_request_frames, + 1, + 1, + &temp_u32); + pf_put_bits ( + p_peer_to_peer_boundary->peer_to_peer_boundary + .do_not_send_path_delay_request_frames, + 1, + 2, + &temp_u32); + + pf_put_uint32 (is_big_endian, temp_u32, res_len, p_bytes, p_pos); + + /* Adjust Properties */ + pf_put_uint16 ( + is_big_endian, + p_peer_to_peer_boundary->adjust_properties, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pdport_data_adj ( + bool is_big_endian, + uint16_t subslot, + const pf_adjust_peer_to_peer_boundary_t * p_peer_to_peer_boundary, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_BOUNDARY_ADJUST, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* Slot and subslot */ + pf_put_uint16 (is_big_endian, PNET_SLOT_DAP_IDENT, res_len, p_bytes, p_pos); + pf_put_uint16 (is_big_endian, subslot, res_len, p_bytes, p_pos); + + /* PeerToPeer_boundary*/ + pf_put_peer_to_peer_boundary ( + is_big_endian, + p_peer_to_peer_boundary, + res_len, + p_bytes, + p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +void pf_put_pd_interface_adj ( + bool is_big_endian, + uint16_t subslot, + pf_lldp_name_of_device_mode_t name_of_device_mode, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_pos = *p_pos; + uint16_t block_len = 0; + uint32_t temp_u32; + + /* Block header first */ + pf_put_block_header ( + is_big_endian, + PF_BT_INTERFACE_ADJUST, + 0, /* Dont know block_len yet */ + PNET_BLOCK_VERSION_HIGH, + PNET_BLOCK_VERSION_LOW, + res_len, + p_bytes, + p_pos); + + pf_put_padding (2, res_len, p_bytes, p_pos); + + /* MultipleInterfaceMode */ + temp_u32 = 0; + pf_put_bits (name_of_device_mode, 1, 0, &temp_u32); + pf_put_uint32 (is_big_endian, temp_u32, res_len, p_bytes, p_pos); + + /* Pad with zeroes until block length is a multiple of 4 bytes */ + pf_put_padding_align (block_pos, 4, res_len, p_bytes, p_pos); + + /* Finally insert the block length into the block header */ + block_len = *p_pos - (block_pos + 4); + block_pos += offsetof (pf_block_header_t, block_length); /* Point to correct + place */ + pf_put_uint16 (is_big_endian, block_len, res_len, p_bytes, &block_pos); +} + +/** + * Put rpc epm floor count into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param count In: Floor count. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_floor_count ( + bool is_big_endian, + uint32_t count, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint16 (is_big_endian, count, res_len, p_bytes, p_pos); +} + +/** + * Put rpc epm floor 1 configuration into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_floor In: Floor configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_floor_1_uuid ( + bool is_big_endian, + const pf_floor_uuid_t * p_floor, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t lhs_length = (uint16_t) ( + sizeof (p_floor->protocol_id) + sizeof (p_floor->uuid) + + sizeof (p_floor->version_major)); + + pf_put_uint16 (is_big_endian, lhs_length, res_len, p_bytes, p_pos); + + pf_put_byte (p_floor->protocol_id, res_len, p_bytes, p_pos); + + pf_put_rpc_uuid (is_big_endian, &p_floor->uuid, res_len, p_bytes, p_pos); + + pf_put_uint16 ( + is_big_endian, + p_floor->version_major, + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + (uint16_t)(sizeof (p_floor->version_minor)), + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_floor->version_minor, + res_len, + p_bytes, + p_pos); +} + +/** + * Put rpc epm floor 2 configuration into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_floor In: Floor configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_floor_2_uuid ( + bool is_big_endian, + const pf_floor_uuid_t * p_floor, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_rpc_floor_1_uuid (is_big_endian, p_floor, res_len, p_bytes, p_pos); +} + +/** + * Put rpc epm floor 3 configuration into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_floor In: Floor configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_floor_3_rpc ( + bool is_big_endian, + const pf_floor_3_t * p_floor, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint16 ( + is_big_endian, + (uint16_t)(sizeof (p_floor->protocol_id)), + res_len, + p_bytes, + p_pos); + + pf_put_byte (p_floor->protocol_id, res_len, p_bytes, p_pos); + + pf_put_uint16 ( + is_big_endian, + (uint16_t)(sizeof (p_floor->version_minor)), + res_len, + p_bytes, + p_pos); + + pf_put_uint16 ( + is_big_endian, + p_floor->version_minor, + res_len, + p_bytes, + p_pos); +} + +/** + * Put rpc epm floor 4 configuration into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_floor In: Floor configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_floor_4_udp ( + bool is_big_endian, + const pf_floor_4_t * p_floor, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint16 ( + is_big_endian, + (uint16_t)(sizeof (p_floor->protocol_id)), + res_len, + p_bytes, + p_pos); + + pf_put_byte (p_floor->protocol_id, res_len, p_bytes, p_pos); + + pf_put_uint16 ( + is_big_endian, + (uint16_t)(sizeof (p_floor->port)), + res_len, + p_bytes, + p_pos); + + pf_put_uint16 (is_big_endian, p_floor->port, res_len, p_bytes, p_pos); +} + +/** + * Put rpc epm floor 5 configuration into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_floor In: Floor configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_floor_5_ip ( + bool is_big_endian, + const pf_floor_5_t * p_floor, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint16 ( + is_big_endian, + (uint16_t)(sizeof (p_floor->protocol_id)), + res_len, + p_bytes, + p_pos); + + pf_put_byte (p_floor->protocol_id, res_len, p_bytes, p_pos); + + pf_put_uint16 ( + is_big_endian, + (uint16_t)(sizeof (p_floor->ip_address)), + res_len, + p_bytes, + p_pos); + + pf_put_uint32 (is_big_endian, p_floor->ip_address, res_len, p_bytes, p_pos); +} + +/** + * Put rpc epm annotation into a buffer. + * The annotation string is defined in + * PN-AL-protocol (Mar20) Table 309 RPC Substitutions + * RPC Annotation + * Field Type Section + * DeviceType VisibleString[25] 4.10.3.3.1 + * Blank + * OrderID VisibleString[20] 4.10.3.3.2 + * Blank + * HWRevision VisibleString[5] 4.10.3.3.3 + * Blank + * SWRevisionPrefix VisibleString[1] 4.10.3.3.4 + * SWRevision VisibleString[9] 4.10.3.3.5 + * EndTerm '\0' + * + * @param p_cfg In: Configuration data used to generate + * annotation. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_epm_annotation ( + const pnet_cfg_t * p_cfg, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + char annotation_data[PF_RPC_EPM_ANNOTATION_LENGTH]; + + snprintf ( + annotation_data, + sizeof (annotation_data), + /* clang-format off */ + "%-" STRINGIFY (PNET_PRODUCT_NAME_MAX_LEN) "s " + "%-" STRINGIFY (PNET_ORDER_ID_MAX_LEN) "s " + "%5u " + "%c%3u%3u%3u", + /* clang-format on */ + p_cfg->product_name, + p_cfg->im_0_data.im_order_id, + p_cfg->im_0_data.im_hardware_revision, + p_cfg->im_0_data.im_sw_revision_prefix, + p_cfg->im_0_data.im_sw_revision_functional_enhancement, + p_cfg->im_0_data.im_sw_revision_bug_fix, + p_cfg->im_0_data.im_sw_revision_internal_change); + + pf_put_mem ( + annotation_data, + sizeof (annotation_data), + res_len, + p_bytes, + p_pos); +} + +/** + * Put rpc epm protocol tower into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_tower In: Protocol tower configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_tower_entry ( + bool is_big_endian, + const pf_rpc_tower_t * p_tower, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint32 ( + is_big_endian, + PF_RPC_TOWER_REFERENTID, + res_len, + p_bytes, + p_pos); + pf_put_uint32 ( + is_big_endian, + PF_RPC_EPM_ANNOTATION_OFFSET, + res_len, + p_bytes, + p_pos); + + pf_put_uint32 ( + is_big_endian, + PF_RPC_EPM_ANNOTATION_LENGTH, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_epm_annotation (p_tower->p_cfg, res_len, p_bytes, p_pos); + + pf_put_uint32 ( + is_big_endian, + PF_RPC_EPM_FLOOR_LENGTH, + res_len, + p_bytes, + p_pos); + + pf_put_uint32 ( + is_big_endian, + PF_RPC_EPM_FLOOR_LENGTH, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_floor_count ( + is_big_endian, + PF_RPC_TOWER_FLOOR_COUNT, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_floor_1_uuid ( + is_big_endian, + &p_tower->floor_1, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_floor_2_uuid ( + is_big_endian, + &p_tower->floor_2, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_floor_3_rpc ( + is_big_endian, + &p_tower->floor_3, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_floor_4_udp ( + is_big_endian, + &p_tower->floor_4, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_floor_5_ip ( + is_big_endian, + &p_tower->floor_5, + res_len, + p_bytes, + p_pos); +} + +/** + * Put rpc handle into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_handle In: Rpc handle + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_handle ( + bool is_big_endian, + const pf_rpc_handle_t * p_handle, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint32 ( + is_big_endian, + p_handle->rpc_entry_handle, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_uuid ( + is_big_endian, + &p_handle->handle_uuid, + res_len, + p_bytes, + p_pos); +} + +/** + * Put rpc epm entry into a buffer. If no no protocol tower is + * configured / actual_count == 0, a empty entry is written. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_entry In: Rpc epm entry including protocol + * tower configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +static void pf_put_rpc_epm_entry ( + bool is_big_endian, + const pf_rpc_entry_t * p_entry, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + pf_put_uint32 (is_big_endian, p_entry->max_count, res_len, p_bytes, p_pos); + + pf_put_uint32 (is_big_endian, p_entry->offset, res_len, p_bytes, p_pos); + + pf_put_uint32 (is_big_endian, p_entry->actual_count, res_len, p_bytes, p_pos); + + /* Add entry if not empty */ + if (p_entry->actual_count > 0) + { + pf_put_rpc_uuid ( + is_big_endian, + &p_entry->object_uuid, + res_len, + p_bytes, + p_pos); + + pf_put_tower_entry ( + is_big_endian, + &p_entry->tower_entry, + res_len, + p_bytes, + p_pos); + } +} + +void pf_put_lookup_response_data ( + pnet_t * net, + bool is_big_endian, + const pf_rpc_lookup_rsp_t * p_lookup_rsp, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos) +{ + uint16_t block_position = *p_pos; + pf_put_rpc_handle ( + is_big_endian, + &p_lookup_rsp->rpc_handle, + res_len, + p_bytes, + p_pos); + + pf_put_uint32 ( + is_big_endian, + p_lookup_rsp->num_entry, + res_len, + p_bytes, + p_pos); + + pf_put_rpc_epm_entry ( + is_big_endian, + &p_lookup_rsp->rpc_entry, + res_len, + p_bytes, + p_pos); + + pf_put_padding_align (block_position, 2, res_len, p_bytes, p_pos); + + pf_put_uint32 ( + is_big_endian, + p_lookup_rsp->return_code, + res_len, + p_bytes, + p_pos); +} diff --git a/profinet_stack/p-net/src/device/pf_block_writer.h b/profinet_stack/p-net/src/device/pf_block_writer.h new file mode 100644 index 0000000..88ca2e8 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_block_writer.h @@ -0,0 +1,920 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_BLOCK_WRITER_H +#define PF_BLOCK_WRITER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Insert a sequence of bytes into a buffer. + * @param p_src In: The start of the byte sequence to insert. + * @param src_size In: Number of bytes to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. Silently ignored if NULL. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_mem ( + const void * p_src, + uint16_t src_size, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a sequence of bytes into a buffer, for overlapping source and + * destinations. + * + * Uses memmove() internally. + * + * @param p_src In: The start of the byte sequence to insert. + * @param src_size In: Number of bytes to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_mem_overlapping ( + const void * p_src, + uint16_t src_size, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a byte into a buffer. + * @param val In: The byte to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_byte ( + uint8_t val, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a 16-bit entity into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param val In: The value to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_uint16 ( + bool is_big_endian, + uint16_t val, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a 32-bit entity into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param val In: The value to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_uint32 ( + bool is_big_endian, + uint32_t val, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert fixed length zero padding into a buffer. + * @param n_bytes In: The number of bytes byte to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_padding ( + uint16_t n_bytes, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Pad a buffer with zeroes until block length is aligned + * + * @param start_position In: Start of block to align. + * @param align In: Alignment. 2 for 16 bit alignment, + * 4 for 32bit alignment + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_padding_align ( + uint16_t start_postion, + uint16_t align, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an AR result block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR result to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_ar_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an IOCR result block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR result to insert. + * @param ix In: The index into p_ar->iocr[].result to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_iocr_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an alarm CR block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the alarm CR to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_alarm_cr_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert AR diff blocks into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR diff to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_ar_diff ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an AR RPC result block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR RPC result to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_ar_rpc_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an AR server result block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR server result to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_ar_server_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an AR vendor result block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR vendor result to insert. + * @param ix In: The index into p_ar_ar_vendor_result[]. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_ar_vendor_result ( + bool is_big_endian, + const pf_ar_t * p_ar, + uint16_t ix, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an AR Data block into a buffer. + * @param net InOut: The p-net stack instance + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_ar In: Contains the AR to insert (NULL for "All ARs"). + * @param api_id In: Specific API to insert if p_ar != NULL. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_ar_data ( + pnet_t * net, + bool is_big_endian, + const pf_ar_t * p_ar, + uint32_t api_id, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a RPC control block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param block_type In: The control block type. + * @param p_res In: The control block to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_control ( + bool is_big_endian, + pf_block_type_values_t block_type, + const pf_control_block_t * p_res, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an RPC read result into a buffer. + * Return the position of the data_length member of the read result as it must + * be modified later. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_res In: The read result to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + * @param p_data_length_pos Out: Position of the data length field + */ +void pf_put_read_result ( + bool is_big_endian, + const pf_iod_read_result_t * p_res, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos, + uint16_t * p_data_length_pos); + +/** + * Insert read data (from the application) into a buffer. + * Use a block header with the specified block_type. + * @param is_big_endian In: Endianness of the destination buffer. + * @param block_type In: The block type to use for the data. + * @param raw_length In: The length in bytes of the user data. + * @param p_raw_data In: The data to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_record_data_read ( + bool is_big_endian, + pf_block_type_values_t block_type, + uint16_t raw_length, + const uint8_t * p_raw_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert I&M0 filter data into a buffer. + * @param net InOut: The p-net stack instance + * @param is_big_endian In: Endianness of the destination buffer. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_im_0_filter_data ( + pnet_t * net, + bool is_big_endian, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert I&M0 data into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_im_0 In: The I&M0 data to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_im_0 ( + bool is_big_endian, + const pnet_im_0_t * p_im_0, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert I&M1 data into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_im_1 In: The I&M1 data to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_im_1 ( + bool is_big_endian, + const pnet_im_1_t * p_im_1, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert I&M2 data into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_im_2 In: The I&M2 data to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_im_2 ( + bool is_big_endian, + const pnet_im_2_t * p_im_2, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert I&M3 data into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_im_3 In: The I&M3 data to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_im_3 ( + bool is_big_endian, + const pnet_im_3_t * p_im_3, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert filtered real or expected ident data into a buffer. + * + * filter_level specifies what is being filtered: + * If p_ar is != NULL then only select items that belong to this AR. + * + * PF_DEV_FILTER_LEVEL_SUBSLOT means "Only include sub-slot" + * specified by api_id, slot_nbr and subslot_nbr. + * PF_DEV_FILTER_LEVEL_SLOT means "Include all sub-slots of slot" + * specified by api_id and slot_nbr. + * PF_DEV_FILTER_LEVEL_API means "Include all slots and sub-slots of API id" + * specified by api_id. + * PF_DEV_FILTER_LEVEL_DEVICE essentially means "No filtering" on API id, + * slot_nbr or subslot_nbr. + * + * stop_level specifies how much data is being included: + * PF_DEV_FILTER_LEVEL_SUBSLOT means "Include all levels". + * PF_DEV_FILTER_LEVEL_SLOT means "Do not include sub-slots". + * PF_DEV_FILTER_LEVEL_API means "Do not include slots or sub-slots". + * PF_DEV_FILTER_LEVEL_DEVICE means "Only include API count". + * + * @param net InOut: The p-net stack instance + * @param is_big_endian In: Endianness of the destination buffer. + * @param block_version_low In: The minor version number of the block to + * insert. + * @param block_type In: Specifies REAL or EXP ident number to insert. + * @param filter_level In: The filter level. + * @param stop_level In: The amount of detail to include + * (ending level). + * @param p_ar In: If != NULL then filter by AR. + * @param api_id In: API id to filter by. + * @param slot_nbr In: Slot number to filter by. + * @param subslot_nbr In: Sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_ident_data ( + pnet_t * net, + bool is_big_endian, + uint8_t block_version_low, + pf_block_type_values_t block_type, + pf_dev_filter_level_t filter_level, + pf_dev_filter_level_t stop_level, + const pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a pnet status into a buffer + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_status In: The pnet status to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_pnet_status ( + bool is_big_endian, + const pnet_pnio_status_t * p_status, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a DCE RPC header into a buffer. + * The endianness is determined from a member of the rpc header. + * Return the position of the length_of_body member of the read result as it + * must be modified later. + * @param p_rpc In: The DCE RPC header to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + * @param p_pos_body_len Out: Position of the length_of_body member. + */ +void pf_put_dce_rpc_header ( + pf_rpc_header_t * p_rpc, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos, + uint16_t * p_pos_body_len); + +/** + * Insert a write result into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_res In: The write result to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_write_result ( + bool is_big_endian, + const pf_iod_write_result_t * p_res, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a Log Book entry block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_log_book In: The log book entry to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_log_book_data ( + bool is_big_endian, + const pf_log_book_t * p_log_book, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert the fixed part of the RTA-PDU into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param pf_alarm_fixed_t In: The fixed alarm part to insert. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_alarm_fixed ( + bool is_big_endian, + const pf_alarm_fixed_t * p_alarm_fixed, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert an AlarmNotification or AlarmAck block into a buffer. + * + * This function can insert a complete AlarmNotification-PDU. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param bh_type In: The block type. + * @param p_alarm_data In: Alarm type, slot, subslot etc. Mandatory. + * The payload part of it (if any) is not used. + * @param maint_status In: The Maintenance status used for specific USI + * values (inserted only if not zero). + * @param payload_usi In: The payload USI (may be 0). Only used for + * block type = alarm notify. + * Use 0 for no payload. + * @param payload_len In: Number of bytes of manufaturer data. + * Only used for manufacturer data + * (payload USI <= 0x7FFF). + * May be 0 for manufacturer data. + * @param p_payload In: pf_diag_item_t, manufacturer data or NULL. + * Mandatory if payload_len > 0 or for USI + * values that expect a pf_diag_item_t + * @param p_status In: PNIO status. Only used for block type = alarm + * ack. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_alarm_block ( + bool is_big_endian, + pf_block_type_values_t bh_type, + const pf_alarm_data_t * p_alarm_data, + uint32_t maint_status, + uint16_t payload_usi, + uint16_t payload_len, + const uint8_t * p_payload, + const pnet_pnio_status_t * p_status, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a SubstituteValue block block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param sub_mode In: The substitution mode. + * @param iocs_len In: Length of IOCS data. + * @param p_iocs In: The IOCS data. + * @param iops_len In: Length of IOPS data. + * @param p_iops In: IOPS data. + * @param data_len In: Length of output data. + * @param p_data In: Substitution data. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_substitute_data ( + bool is_big_endian, + uint16_t sub_mode, + uint8_t iocs_len, + const uint8_t * p_iocs, + uint8_t iops_len, + const uint8_t * p_iops, + uint16_t data_len, + const uint8_t * p_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a RecordOutputDataObjectElement block into a buffer. + * + * This block inserts a complete RecordOutputDataObjectElement block into a + * buffer. It includes the substitution data block if p_sub_data != NULL. The + * IOCS and IOPS of the substitution data value is the same as the ordinary IOCS + * and IOPS. The length of the substitution data is the same as data_len. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param sub_active In: true if the substitution data are used. + * @param iocs_len In: Length of IOCS data. + * @param p_iocs In: The IOCS data. + * @param iops_len In: Length of IOPS data. + * @param p_iops In: IOPS data. + * @param data_len In: Length of output data. + * @param p_data In: Output data. + * @param sub_mode In: The substitution mode. + * @param p_sub_data In: The substitution data. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_output_data ( + bool is_big_endian, + bool sub_active, + uint8_t iocs_len, + const uint8_t * p_iocs, + uint8_t iops_len, + const uint8_t * p_iops, + uint16_t data_len, + const uint8_t * p_data, + uint16_t sub_mode, + const uint8_t * p_sub_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a RecordInputDataObjectElement block into a buffer. + * + * This block inserts a complete RecordInputDataObjectElement block into a + * buffer. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param iocs_len In: Length of IOCS data. + * @param p_iocs In: The IOCS data. + * @param iops_len In: Length of IOPS data. + * @param p_iops In: IOPS data. + * @param data_len In: Length of output data. + * @param p_data In: Output data. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut:Position in destination buffer. + */ +void pf_put_input_data ( + bool is_big_endian, + uint8_t iocs_len, + const uint8_t * p_iocs, + uint8_t iops_len, + const uint8_t * p_iops, + uint16_t data_len, + const uint8_t * p_data, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert a DiagnosisData block into a buffer. + * + * Insert filtered Diagnosis, Maintenance, Qualifiers and Status for one + * sub-slot/slot/ar/api. + * + * filter_level specifies what is being filtered: + * If p_ar is != NULL then only select items that belong to this AR. + * PF_DEV_FILTER_LEVEL_SUBSLOT: Only include sub-slot specified by api_id, + * slot_nbr and subslot_nbr. + * PF_DEV_FILTER_LEVEL_SLOT: Include all sub-slots of slot specified by + * api_id and slot_nbr. + * PF_DEV_FILTER_LEVEL_API: Include all slots and sub-slots of API id + * specified by api_id. + * PF_DEV_FILTER_LEVEL_DEVICE: No filtering on API id, slot_nbr or + * subslot_nbr (i.e. all diags) + * + * diag_filter is an ortogonal filter that selects only specific diag types: + * PF_DIAG_FILTER_FAULT_STD: Only STD, severity FAULT. + * PF_DIAG_FILTER_FAULT_ALL: Both USI and STD, severity FAULT. + * PF_DIAG_FILTER_ALL: All types of diag, both USI and STD. + * PF_DIAG_FILTER_M_REQ: Only Maintenance required. + * PF_DIAG_FILTER_M_DEM: Only Maintenance demanded. + * + * Implemented using: + * pf_put_diag_device() + * pf_put_diag_api() for all APIs + * pf_put_diag_slot() for all slots + * pf_put_diag_subslot() for all subslots + * pf_put_diag_list() Header insertion for a USI value + * pf_put_diag_item() Insertion of diag item + * + * @param net InOut: The p-net stack instance + * @param is_big_endian In: Endianness of the destination buffer. + * @param filter_level In: The filter level. + * @param diag_filter In: The diag type filter. + * @param p_ar In: If != NULL then filter by AR. + * @param api_id In: The API id to filter by. + * @param slot_nbr In: The slot number to filter by. + * @param subslot_nbr In: The sub-slot number to filter by. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_diag_data ( + pnet_t * net, + bool is_big_endian, + pf_dev_filter_level_t filter_level, + pf_diag_filter_level_t diag_filter, + const pf_ar_t * p_ar, /* If != NULL only include those belonging to p_ar */ + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert PDport data check block into a buffer. + * @param is_big_endian In: Endianness of the destination buffer. + * @param slot In: Slot number + * @param subslot In: Subslot number + * @param check_peer In: check_peer data. ToDo - add support for misc + * port data checks + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pdport_data_check ( + bool is_big_endian, + uint16_t slot, + uint16_t subslot, + const pf_check_peer_t * check_peer, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert PDport data adjust block into a buffer. + * + * ToDo - Add support for other adjust properties + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param subslot In: DAP subslot identifying the port. + * @param p_peer_to_peer_boundary In: Peer to peer boundary + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pdport_data_adj ( + bool is_big_endian, + uint16_t subslot, + const pf_adjust_peer_to_peer_boundary_t * p_peer_to_peer_boundary, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert pd interface adjust block into a buffer. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param subslot In: DAP subslot identifying the interface + * @param mode In: Multiple interface mode. + * (name of device mode) + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pd_interface_adj ( + bool is_big_endian, + uint16_t subslot, + pf_lldp_name_of_device_mode_t name_of_device_mode, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert PDport real data block into a buffer. + * + * Includes peer chassis ID, peer MAC address and peer MAU type. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param subslot In: Subslot + * @param p_peer_station_name In: Peer station name + * @param p_peer_port_name In: Peer port name + * @param p_port_data In: Port data + * @param media_type In: Media type + * @param p_eth_status In: Ethernet status + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pdport_data_real ( + bool is_big_endian, + uint16_t subslot, + const pf_lldp_station_name_t * p_peer_station_name, + const pf_lldp_port_name_t * p_peer_port_name, + const pf_port_t * p_port_data, + pf_mediatype_values_t media_type, + const pnal_eth_status_t * p_eth_status, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert PD port statistics block into a buffer. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_port_stats In: Port statistics + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pdport_statistics ( + bool is_big_endian, + const pnal_port_stats_t * p_port_stats, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert PD interface real data block into a buffer. + * + * This includes chassis ID, MAC address, IP address, subnet and gateway. + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param mac_address In: MAC address for DAP + * @param ip_address In: IP address + * @param netmask In: Netmask + * @param gateway In: Gateway address + * @param station_name In: Station name, terminated string, buffer + * size PNET_STATION_NAME_MAX_SIZE + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pdinterface_data_real ( + bool is_big_endian, + const pnet_ethaddr_t * mac_address, + pnal_ipaddr_t ip_address, + pnal_ipaddr_t netmask, + pnal_ipaddr_t gateway, + const char * station_name, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert multiblock port and statistics for one port + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param api In: API number + * @param subslot In: DAP subslot for port + * @param p_peer_station_name In: Peer station name + * @param p_peer_port_name In: Peer port name + * @param p_port_data In: Port data + * @param media_type In: Media type + * @param p_eth_status In: Ethernet status + * @param p_port_statistics In: Port statistics + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pd_multiblock_port_and_statistics ( + bool is_big_endian, + uint32_t api, + uint16_t subslot, + const pf_lldp_station_name_t * p_peer_station_name, + const pf_lldp_port_name_t * p_peer_port_name, + const pf_port_t * p_port_data, + pf_mediatype_values_t media_type, + const pnal_eth_status_t * p_eth_status, + const pnal_port_stats_t * p_port_statistics, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert multiblock interface and statistics + * + * @param is_big_endian In: Endianness of the destination buffer. + * @param api In: API number + * @param mac_address In: MAC address + * @param ip_address In: IP address + * @param netmask In: Netmask + * @param gateway In: Gateway address + * @param station_name In: Station name, terminated string, buffer + * size PNET_STATION_NAME_MAX_SIZE + * @param p_port_statistics In: Port statistics + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_pd_multiblock_interface_and_statistics ( + bool is_big_endian, + uint32_t api, + const pnet_ethaddr_t * mac_address, + pnal_ipaddr_t ip_address, + pnal_ipaddr_t netmask, + pnal_ipaddr_t gateway, + const char * station_name, + const pnal_port_stats_t * p_port_statistics, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +/** + * Insert epm lookup response data block into a buffer. + * @param net InOut: The p-net stack instance + * @param is_big_endian In: Endianness of the destination buffer. + * @param p_lookup_rsp In: Epm lookup response including entry and + * protocol tower configuration. + * @param res_len In: Size of destination buffer. + * @param p_bytes Out: Destination buffer. + * @param p_pos InOut: Position in destination buffer. + */ +void pf_put_lookup_response_data ( + pnet_t * net, + bool is_big_endian, + const pf_rpc_lookup_rsp_t * p_lookup_rsp, + uint16_t res_len, + uint8_t * p_bytes, + uint16_t * p_pos); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_BLOCK_WRITER_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmdev.c b/profinet_stack/p-net/src/device/pf_cmdev.c new file mode 100644 index 0000000..55d75e9 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmdev.c @@ -0,0 +1,5224 @@ + +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + * See LICENSE file in the project root for full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Context Management Protocol Machine Device (CMDEV) + * + * This handles connection establishment for IO-Devices. + * + * For example pulling and plugging modules and submodules in slots and + * subslots are done in this file. Also implements handling connect, release, + * ccontrol and dcontrol. + * + * Collect IOCS and IOPS information. + * + * Bookkeeping of diagnosis entries. + * + */ + +#ifdef UNIT_TEST + +#endif + +#include +#include +#include +#include + +#include "pf_includes.h" +#include "pf_block_reader.h" +#include "pf_block_writer.h" + +/* Forward declaration */ + +static int pf_cmdev_get_exp_sub ( + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_exp_submodule_t ** pp_exp_sub); + +/* ====================================================================== */ + +int pf_cmdev_cfg_traverse ( + pnet_t * net, + pf_ftn_device_t p_ftn_dev, + pf_ftn_api_t p_ftn_api, + pf_ftn_slot_t p_ftn_slot, + pf_ftn_subslot_t p_ftn_sub) +{ + int ret = -1; + uint16_t api_ix; + uint16_t slot_ix; + uint16_t subslot_ix; + + if (p_ftn_dev != NULL) + { + if (p_ftn_dev (&net->cmdev_device) != 0) + { + ret = -1; + } + else if (p_ftn_api != NULL) + { + for (api_ix = 0; api_ix < NELEMENTS (net->cmdev_device.apis); api_ix++) + { + if (net->cmdev_device.apis[api_ix].in_use == true) + { + if (p_ftn_api (&net->cmdev_device.apis[api_ix]) != 0) + { + ret = -1; + } + else if (p_ftn_slot != NULL) + { + for (slot_ix = 0; + slot_ix < + NELEMENTS (net->cmdev_device.apis[api_ix].slots); + slot_ix++) + { + if (net->cmdev_device.apis[api_ix].slots[slot_ix].in_use == true) + { + if ( + p_ftn_slot ( + &net->cmdev_device.apis[api_ix].slots[slot_ix]) != + 0) + { + ret = -1; + } + else if (p_ftn_sub != NULL) + { + for (subslot_ix = 0; + subslot_ix < + NELEMENTS (net->cmdev_device.apis[api_ix] + .slots[slot_ix] + .subslots); + subslot_ix++) + { + if ( + net->cmdev_device.apis[api_ix] + .slots[slot_ix] + .subslots[subslot_ix] + .in_use == true) + { + if ( + p_ftn_sub (&net->cmdev_device.apis[api_ix] + .slots[slot_ix] + .subslots[subslot_ix]) != 0) + { + ret = -1; + } + } + } + } + } + } + } + } + } + } + } + + return ret; +} + +/**************************** Getters *************************************/ + +int pf_cmdev_get_device (pnet_t * net, pf_device_t ** pp_device) +{ + *pp_device = &net->cmdev_device; + + return 0; +} + +int pf_cmdev_get_api (pnet_t * net, uint32_t api_id, pf_api_t ** pp_api) +{ + int ret = -1; + pf_api_t * p_api = NULL; + uint16_t ix = 0; + + if (pp_api == NULL) + { + LOG_ERROR (PNET_LOG, "CMDEV(%d): NULL pointer\n", __LINE__); + } + else + { + while ((ix < PNET_MAX_API) && + ((net->cmdev_device.apis[ix].in_use == false) || + (api_id != net->cmdev_device.apis[ix].api_id))) + { + ix++; + } + + if ((ix < PNET_MAX_API) && (net->cmdev_device.apis[ix].in_use == true)) + { + p_api = &net->cmdev_device.apis[ix]; + ret = 0; + } + + *pp_api = p_api; + } + + return ret; +} + +/** + * @internal + * Get an slot instance of an API. + * @param p_api InOut: The API instance. + * @param slot_nbr In: The slot number (must be in_use) + * @param pp_slot Out: The slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_get_slot ( + pf_api_t * p_api, + uint16_t slot_nbr, + pf_slot_t ** pp_slot) +{ + int ret = -1; + pf_slot_t * p_slot = NULL; + uint16_t ix; + + if ((p_api == NULL) || (pp_slot == NULL)) + { + LOG_ERROR (PNET_LOG, "CMDEV(%d): NULL pointer(s)\n", __LINE__); + } + else + { + ix = 0; + while ((ix < PNET_MAX_SLOTS) && ((p_api->slots[ix].in_use == false) || + (slot_nbr != p_api->slots[ix].slot_nbr))) + { + ix++; + } + + if (ix < PNET_MAX_SLOTS) + { + p_slot = &p_api->slots[ix]; + ret = 0; + } + + *pp_slot = p_slot; + } + + return ret; +} + +/** + * @internal + * Get an sub-slot instance of a slot instance. + * @param p_slot InOut: The slot instance. + * @param subslot_nbr In: The sub-slot number (must be in_use). + * @param pp_subslot Out: The sub-slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_get_subslot ( + pf_slot_t * p_slot, + uint16_t subslot_nbr, + pf_subslot_t ** pp_subslot) +{ + int ret = -1; + pf_subslot_t * p_subslot = NULL; + uint16_t ix; + + if ((p_slot == NULL) || (pp_subslot == NULL)) + { + LOG_ERROR (PNET_LOG, "CMDEV(%d): NULL pointer(s)\n", __LINE__); + } + else + { + ix = 0; + while ((ix < PNET_MAX_SUBSLOTS) && + ((p_slot->subslots[ix].in_use == false) || + (subslot_nbr != p_slot->subslots[ix].subslot_nbr))) + { + ix++; + } + + if (ix < PNET_MAX_SUBSLOTS) + { + p_subslot = &p_slot->subslots[ix]; + ret = 0; + } + + *pp_subslot = p_subslot; + } + + return ret; +} + +int pf_cmdev_get_subslot_full ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_subslot_t ** pp_subslot) +{ + int ret = -1; + pf_api_t * p_api = NULL; + pf_slot_t * p_slot = NULL; + + if (pf_cmdev_get_api (net, api_id, &p_api) == 0) + { + if (pf_cmdev_get_slot (p_api, slot_nbr, &p_slot) == 0) + { + if (pf_cmdev_get_subslot (p_slot, subslot_nbr, pp_subslot) == 0) + { + ret = 0; + } + } + } + + return ret; +} + +int pf_cmdev_get_slot_full ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + pf_slot_t ** pp_slot) +{ + int ret = -1; + pf_api_t * p_api = NULL; + + if (pf_cmdev_get_api (net, api_id, &p_api) == 0) + { + if (pf_cmdev_get_slot (p_api, slot_nbr, pp_slot) == 0) + { + ret = 0; + } + } + + return ret; +} + +int pf_cmdev_get_module_ident ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + uint32_t * p_module_ident) +{ + pf_slot_t * p_slot = NULL; + if (pf_cmdev_get_slot_full (net, api_id, slot_nbr, &p_slot) == 0) + { + *p_module_ident = p_slot->module_ident_number; + return 0; + } + return -1; +} + +int pf_cmdev_get_submodule_ident ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t * p_submodule_ident) +{ + pf_subslot_t * p_subslot = NULL; + if ( + pf_cmdev_get_subslot_full (net, api_id, slot_nbr, subslot_nbr, &p_subslot) == + 0) + { + *p_submodule_ident = p_subslot->submodule_ident_number; + return 0; + } + return -1; +} + +/******************** Diagnosis **********************************************/ + +int pf_cmdev_get_diag_item ( + pnet_t * net, + uint16_t item_ix, + pf_diag_item_t ** pp_item) +{ + int ret = -1; + + if (item_ix < NELEMENTS (net->cmdev_device.diag_items)) + { + *pp_item = &net->cmdev_device.diag_items[item_ix]; + + ret = 0; + } + else + { + *pp_item = NULL; + } + + return ret; +} + +int pf_cmdev_new_diag (pnet_t * net, uint16_t * p_item_ix) +{ + int ret = -1; + + /* Return an item from the free list (after removing it). */ + *p_item_ix = net->cmdev_device.diag_items_free; + if (*p_item_ix != PF_DIAG_IX_NULL) + { + net->cmdev_device.diag_items_free = + net->cmdev_device.diag_items[*p_item_ix].next; + + /* Clear the entry */ + memset ( + &net->cmdev_device.diag_items[*p_item_ix], + 0, + sizeof (net->cmdev_device.diag_items[*p_item_ix])); + net->cmdev_device.diag_items[*p_item_ix].in_use = true; + + ret = 0; + } + + return ret; +} + +void pf_cmdev_free_diag (pnet_t * net, uint16_t item_ix) +{ + if (item_ix < NELEMENTS (net->cmdev_device.diag_items)) + { + /* Put it first in the free list. */ + net->cmdev_device.diag_items[item_ix].in_use = false; + net->cmdev_device.diag_items[item_ix].next = + net->cmdev_device.diag_items_free; + net->cmdev_device.diag_items_free = item_ix; + } +} + +int pf_cmdev_get_next_diagnosis_usi ( + pnet_t * net, + uint16_t list_head, + uint16_t low_usi_limit, + uint16_t * p_next_usi) +{ + int ret = -1; + pf_diag_item_t * p_item = NULL; + uint16_t resulting_value = UINT16_MAX; + + /* Walk the list to find next larger USI value */ + pf_cmdev_get_diag_item (net, list_head, &p_item); + while (p_item != NULL) + { + if (p_item->usi > low_usi_limit) + { + if (p_item->usi < resulting_value) + { + resulting_value = p_item->usi; + } + } + + pf_cmdev_get_diag_item (net, p_item->next, &p_item); + } + + if (resulting_value != UINT16_MAX) + { + *p_next_usi = resulting_value; + ret = 0; + } + + return ret; +} + +/*****************************************************************************/ + +/** + * @internal + * Instantiate a new API structure. + * If the API identifier already exists the the operation fails. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param pp_api Out: The new API instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_new_api (pnet_t * net, uint32_t api_id, pf_api_t ** pp_api) +{ + int ret = -1; + pf_api_t * p_api = NULL; + uint16_t ix; + + if (pp_api == NULL) + { + LOG_ERROR (PNET_LOG, "CMDEV(%d): NULL pointer(s)\n", __LINE__); + } + else if (pf_cmdev_get_api (net, api_id, &p_api) == 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): API %u already exists\n", + __LINE__, + (unsigned)api_id); + } + else + { + ix = 0; + while ((ix < PNET_MAX_API) && (net->cmdev_device.apis[ix].in_use == true)) + { + ix++; + } + + if (ix < PNET_MAX_API) + { + p_api = &net->cmdev_device.apis[ix]; + + memset (p_api, 0, sizeof (*p_api)); + p_api->api_id = api_id; + p_api->in_use = true; + + ret = 0; + } + + *pp_api = p_api; + } + + return ret; +} + +/******************* Slots, subslots, modules and submodules *****************/ + +/** + * @internal + * Instantiate a new slot structure. + * If the slot number already exists the the operation fails. + * @param p_api InOut: The API instance. + * @param slot_nbr In: The slot number. + * @param pp_slot Out: The new slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_new_slot ( + pf_api_t * p_api, + uint16_t slot_nbr, + pf_slot_t ** pp_slot) +{ + int ret = -1; + pf_slot_t * p_slot = NULL; + uint16_t ix; + + if ((p_api == NULL) || (pp_slot == NULL)) + { + LOG_ERROR (PNET_LOG, "CMDEV(%d): NULL pointer(s)\n", __LINE__); + } + else if (pf_cmdev_get_slot (p_api, slot_nbr, &p_slot) == 0) + { + /* Slot already exists */ + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Slot %u already exists\n", + __LINE__, + (unsigned)slot_nbr); + } + else + { + ix = 0; + while ((ix < PNET_MAX_SLOTS) && (p_api->slots[ix].in_use == true)) + { + ix++; + } + + if (ix < PNET_MAX_SLOTS) + { + p_slot = &p_api->slots[ix]; + + memset (p_slot, 0, sizeof (*p_slot)); + p_slot->slot_nbr = slot_nbr; + p_slot->in_use = true; + + ret = 0; + } + + *pp_slot = p_slot; + } + + return ret; +} + +/** + * @internal + * Instantiate a new sub-slot structure. + * If the sub-slot number already exists the the operation fails. + * @param p_slot InOut: The slot instance. + * @param subslot_nbr In: The sub-slot number. + * @param pp_subslot Out: The new sub-slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_new_subslot ( + pf_slot_t * p_slot, + uint16_t subslot_nbr, + pf_subslot_t ** pp_subslot) +{ + int ret = -1; + pf_subslot_t * p_subslot = NULL; + uint16_t ix; + + if ((p_slot == NULL) || (pp_subslot == NULL)) + { + LOG_ERROR (PNET_LOG, "CMDEV(%d): NULL pointer(s)\n", __LINE__); + } + else if (pf_cmdev_get_subslot (p_slot, subslot_nbr, &p_subslot) == 0) + { + /* Subslot already exists */ + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Subslot %u already exists\n", + __LINE__, + (unsigned)subslot_nbr); + } + else + { + ix = 0; + while ((ix < PNET_MAX_SUBSLOTS) && (p_slot->subslots[ix].in_use == true)) + { + ix++; + } + + if (ix < PNET_MAX_SUBSLOTS) + { + p_subslot = &p_slot->subslots[ix]; + + memset (p_subslot, 0, sizeof (*p_subslot)); + p_subslot->subslot_nbr = subslot_nbr; + p_subslot->diag_list = PF_DIAG_IX_NULL; + p_subslot->in_use = true; + + ret = 0; + } + + *pp_subslot = p_subslot; + } + + return ret; +} + +int pf_cmdev_plug_module ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint32_t module_ident_nbr) +{ + int ret = -1; + pf_api_t * p_api = NULL; + pf_slot_t * p_slot = NULL; + + if (pf_cmdev_get_api (net, api_id, &p_api) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): API %u does not exist\n", + __LINE__, + (unsigned)api_id); + } + else if (pf_cmdev_get_slot (p_api, slot_nbr, &p_slot) == 0) + { + /* Slot already has a plugged module. Check ident numbers */ + if (p_slot->module_ident_number == module_ident_nbr) + { + /* Correct module already plugged */ + p_slot->plug_state = PF_MOD_PLUG_PROPER_MODULE; + + ret = 0; + } + else + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Wrong plugged module ident %u in api %u slot %u\n", + __LINE__, + (unsigned)module_ident_nbr, + (unsigned)api_id, + (unsigned)slot_nbr); + p_slot->plug_state = PF_MOD_PLUG_WRONG_MODULE; + } + } + else if (pf_cmdev_new_slot (p_api, slot_nbr, &p_slot) != 0) + { + /* Out of slot resources */ + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Out of slot resources for api %u slot %u\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr); + } + else + { + /* Slot allocated */ + p_slot->module_ident_number = module_ident_nbr; + p_slot->plug_state = PF_MOD_PLUG_PROPER_MODULE; + + /* Inherit AR from API */ + p_slot->p_ar = p_api->p_ar; + + ret = 0; + } + + return ret; +} + +int pf_cmdev_pull_submodule ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr) +{ + int ret = -1; + pf_api_t * p_api = NULL; + pf_slot_t * p_slot = NULL; + pf_subslot_t * p_subslot = NULL; + + if (pf_cmdev_get_api (net, api_id, &p_api) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): API %u does not exist\n", + __LINE__, + (unsigned)api_id); + } + else if (pf_cmdev_get_slot (p_api, slot_nbr, &p_slot) != 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): No module in slot %u\n", + __LINE__, + (unsigned)slot_nbr); + } + else if (pf_cmdev_get_subslot (p_slot, subslot_nbr, &p_subslot) != 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): No submodule in api_id %u slot %u subslot %u\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr); + } + else + { + p_subslot->in_use = false; + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_NO; + + ret = pf_alarm_send_pull ( + net, + p_subslot->p_ar, + api_id, + slot_nbr, + subslot_nbr); + } + + return ret; +} + +int pf_cmdev_plug_submodule ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t module_ident_nbr, + uint32_t submod_ident_nbr, + pnet_submodule_dir_t direction, + uint16_t length_input, + uint16_t length_output, + bool update) +{ + int ret = -1; + pf_api_t * p_api = NULL; + pf_slot_t * p_slot = NULL; + pf_subslot_t * p_subslot = NULL; + pf_exp_submodule_t * p_exp_sub = NULL; + bool already_plugged = false; + + if (pf_cmdev_get_api (net, api_id, &p_api) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): API %u does not exist\n", + __LINE__, + (unsigned)api_id); + } + else if (pf_cmdev_get_slot (p_api, slot_nbr, &p_slot) != 0) + { + /* Auto-plug the module */ + if (pf_cmdev_plug_module (net, api_id, slot_nbr, module_ident_nbr) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Out of slot resources for api %u\n", + __LINE__, + (unsigned)api_id); + } + } + else if (pf_cmdev_get_subslot (p_slot, subslot_nbr, &p_subslot) == 0) + { + if (update == true) + { + if (pf_cmdev_pull_submodule (net, api_id, slot_nbr, subslot_nbr) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Could not pull submodule in api %u slot %u subslot " + "%u\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr); + } + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_NO; + } + else if (p_subslot->submodule_ident_number == submod_ident_nbr) + { + already_plugged = true; + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_OK; + } + else + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Substitute submodule ident 0x%08x number in api %u " + "slot %u subslot %u\n", + __LINE__, + (unsigned)submod_ident_nbr, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr); + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_SUBSTITUTE; + } + } + + if (already_plugged == true) + { + LOG_INFO ( + PNET_LOG, + "CMDEV(%d): Submodule ident 0x%08x number already plugged in api %u " + "slot %u subslot %u\n", + __LINE__, + (unsigned)submod_ident_nbr, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr); + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_OK; + + ret = 0; + } + else if (pf_cmdev_get_slot (p_api, slot_nbr, &p_slot) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): No module in slot %u\n", + __LINE__, + (unsigned)slot_nbr); + } + else if (pf_cmdev_new_subslot (p_slot, subslot_nbr, &p_subslot) == 0) + { + /* Sub-slot created */ + p_subslot->submodule_ident_number = submod_ident_nbr; + + /* + * While plugging the DAP sub-modules there is no AR yet, so the + * sub-modules are always right. + */ + if ( + (p_slot->p_ar == NULL) || (pf_cmdev_get_exp_sub ( + p_slot->p_ar, + api_id, + slot_nbr, + subslot_nbr, + &p_exp_sub) != 0)) + { + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_OK; + + ret = 0; + } + else + { + if (p_exp_sub->submodule_ident_number == submod_ident_nbr) + { + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_OK; + + ret = pf_alarm_send_plug ( + net, + p_slot->p_ar, + api_id, + slot_nbr, + subslot_nbr, + module_ident_nbr, + submod_ident_nbr); + } + else + { + p_subslot->submodule_state.ident_info = PF_SUBMOD_PLUG_SUBSTITUTE; + + ret = pf_alarm_send_plug_wrong ( + net, + p_slot->p_ar, + api_id, + slot_nbr, + subslot_nbr, + module_ident_nbr, + submod_ident_nbr); + } + } + p_subslot->submodule_state.format_indicator = 1; + + p_subslot->direction = direction; + p_subslot->length_input = length_input; + p_subslot->length_output = length_output; + + /* Inherit AR from slot module */ + p_subslot->p_ar = p_slot->p_ar; + + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Plugged submodule ident 0x%08x number in api %u " + "slot %u subslot %u\n", + __LINE__, + (unsigned)submod_ident_nbr, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr); + } + else + { + /* Out of resources */ + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Out of subslot resources for api %u slot %u\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr); + } + + return ret; +} + +int pf_cmdev_pull_module (pnet_t * net, uint32_t api_id, uint16_t slot_nbr) +{ + int ret = -1; + pf_api_t * p_api = NULL; + pf_slot_t * p_slot = NULL; + uint16_t ix; + + /* Pull all submodules. Then pull the module */ + if (pf_cmdev_get_api (net, api_id, &p_api) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): API %u does not exist\n", + __LINE__, + (unsigned)api_id); + } + else if (pf_cmdev_get_slot (p_api, slot_nbr, &p_slot) != 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): No module in slot %u\n", + __LINE__, + (unsigned)slot_nbr); + } + else + { + ret = 0; /* Assume all OK */ + ix = 0; + while ((ix < PNET_MAX_SUBSLOTS) && (ret == 0)) + { + if ( + (p_slot->subslots[ix].in_use == true) && + (pf_cmdev_pull_submodule ( + net, + api_id, + slot_nbr, + p_slot->subslots[ix].subslot_nbr) != 0)) + { + ret = -1; + } + ix++; + } + + if (ret == 0) + { + p_slot->in_use = false; + p_slot->plug_state = PF_MOD_PLUG_NO_MODULE; + } + else + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Could not pull all submodules for api %u slot %u\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr); + } + } + + return ret; +} + +/** + * @internal + * Remove all entries that refer to the AR. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR entries to remove. + */ +static void pf_device_clear (pnet_t * net, pf_ar_t * p_ar) +{ + uint16_t api_ix; + uint16_t slot_ix; + uint16_t sub_ix; + + for (api_ix = 0; api_ix < NELEMENTS (net->cmdev_device.apis); api_ix++) + { + for (slot_ix = 0; + slot_ix < NELEMENTS (net->cmdev_device.apis[api_ix].slots); + slot_ix++) + { + for (sub_ix = 0; + sub_ix < + NELEMENTS ( + net->cmdev_device.apis[api_ix].slots[slot_ix].subslots); + sub_ix++) + { + if ( + net->cmdev_device.apis[api_ix] + .slots[slot_ix] + .subslots[sub_ix] + .p_ar == p_ar) + { + net->cmdev_device.apis[api_ix] + .slots[slot_ix] + .subslots[sub_ix] + .p_ar = NULL; + } + } + if (net->cmdev_device.apis[api_ix].slots[slot_ix].p_ar == p_ar) + { + net->cmdev_device.apis[api_ix].slots[slot_ix].p_ar = NULL; + } + } + if (net->cmdev_device.apis[api_ix].p_ar == p_ar) + { + net->cmdev_device.apis[api_ix].p_ar = NULL; + } + } +} + +/*************** Diagnostic strings ****************************************/ + +const char * pf_cmdev_submod_dir_to_string (pnet_submodule_dir_t direction) +{ + const char * s = ""; + + switch (direction) + { + case PNET_DIR_NO_IO: + s = "NO_IO"; + break; + case PNET_DIR_INPUT: + s = "INPUT"; + break; + case PNET_DIR_OUTPUT: + s = "OUTPUT"; + break; + case PNET_DIR_IO: + s = "INPUT_OUTPUT"; + break; + } + + return s; +} + +const char * pf_cmdev_state_to_string (pf_cmdev_state_values_t state) +{ + const char * s = "unknown"; + + switch (state) + { + case PF_CMDEV_STATE_POWER_ON: + s = "PF_CMDEV_STATE_POWER_ON"; + break; + case PF_CMDEV_STATE_W_CIND: + s = "PF_CMDEV_STATE_W_CIND"; + break; + case PF_CMDEV_STATE_W_CRES: + s = "PF_CMDEV_STATE_W_CRES"; + break; + case PF_CMDEV_STATE_W_SUCNF: + s = "PF_CMDEV_STATE_W_SUCNF"; + break; + case PF_CMDEV_STATE_W_PEIND: + s = "PF_CMDEV_STATE_W_PEIND"; + break; + case PF_CMDEV_STATE_W_PERES: + s = "PF_CMDEV_STATE_W_PERES"; + break; + case PF_CMDEV_STATE_W_ARDY: + s = "PF_CMDEV_STATE_W_ARDY"; + break; + case PF_CMDEV_STATE_W_ARDYCNF: + s = "PF_CMDEV_STATE_W_ARDYCNF"; + break; + case PF_CMDEV_STATE_WDATA: + s = "PF_CMDEV_STATE_WDATA"; + break; + case PF_CMDEV_STATE_DATA: + s = "PF_CMDEV_STATE_DATA"; + break; + case PF_CMDEV_STATE_ABORT: + s = "PF_CMDEV_STATE_ABORT"; + break; + default: + break; + } + return s; +} + +const char * pf_cmdev_event_to_string (pnet_event_values_t event) +{ + const char * s = "unknown"; + + switch (event) + { + case PNET_EVENT_ABORT: + s = "PNET_EVENT_ABORT"; + break; + case PNET_EVENT_STARTUP: + s = "PNET_EVENT_STARTUP"; + break; + case PNET_EVENT_PRMEND: + s = "PNET_EVENT_PRMEND"; + break; + case PNET_EVENT_APPLRDY: + s = "PNET_EVENT_APPLRDY"; + break; + case PNET_EVENT_DATA: + s = "PNET_EVENT_DATA"; + break; + default: + break; + } + return s; +} + +/** + * @internal + * Return a string representation of the specified sub-module direction. + * @param direction In: The direction + * @return a string representation of the sub-module direction. + */ +static const char * pf_cmdev_direction_to_string (pnet_submodule_dir_t direction) +{ + const char * s = "unknown"; + + switch (direction) + { + case PNET_DIR_NO_IO: + s = "PNET_DIR_NO_IO"; + break; + case PNET_DIR_INPUT: + s = "PNET_DIR_INPUT"; + break; + case PNET_DIR_OUTPUT: + s = "PNET_DIR_OUTPUT"; + break; + case PNET_DIR_IO: + s = "PNET_DIR_IO"; + break; + default: + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the specified module plug state. + * @param plug_state In: The plug state + * @return a string representation of the module plug state. + */ +static const char * pf_cmdev_mod_plug_state_to_string ( + pf_mod_plug_state_t plug_state) +{ + const char * s = "unknown"; + + switch (plug_state) + { + case PF_MOD_PLUG_NO_MODULE: + s = "PF_MOD_PLUG_NO_MODULE"; + break; + case PF_MOD_PLUG_WRONG_MODULE: + s = "PF_MOD_PLUG_WRONG_MODULE"; + break; + case PF_MOD_PLUG_PROPER_MODULE: + s = "PF_MOD_PLUG_PROPER_MODULE"; + break; + case PF_MOD_PLUG_SUBSTITUTE: + s = "PF_MOD_PLUG_SUBSTITUTE"; + break; + default: + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the specified sub-module plug state. + * @param plug_state In: The plug state + * @return a string representation of the sub-module plug state. + */ +static const char * pf_cmdev_submod_plug_state_to_string ( + pf_submod_plug_state_t plug_state) +{ + const char * s = "unknown"; + + switch (plug_state) + { + case PF_SUBMOD_PLUG_OK: + s = "PF_SUBMOD_PLUG_OK"; + break; + case PF_SUBMOD_PLUG_SUBSTITUTE: + s = "PF_SUBMOD_PLUG_SUBSTITUTE"; + break; + case PF_SUBMOD_PLUG_WRONG: + s = "PF_SUBMOD_PLUG_WRONG"; + break; + case PF_SUBMOD_PLUG_NO: + s = "PF_SUBMOD_PLUG_NO"; + break; + default: + break; + } + + return s; +} + +void pf_cmdev_ar_show (const pf_ar_t * p_ar) +{ + printf ( + "CMDEV state = %s\n", + pf_cmdev_state_to_string (p_ar->cmdev_state)); +} + +/** + * @internal + * Show everything about the device instance. + * @param p_dev In: The device instance. + * @return 0 Always + */ +static int pf_cmdev_cfg_dev_show (pf_device_t * p_dev) +{ + printf (">>>DEVICE<<<\n"); + printf ( + "The device can use max %u APIs, and %u diag items.\n", + (unsigned)PNET_MAX_API, + (unsigned)PNET_MAX_DIAG_ITEMS); + printf ("First unused diag item: %u.\n", (unsigned)p_dev->diag_items_free); + + return 0; +} + +/** + * @internal + * Show everything about the API instance. + * @param p_api In: The API instance. + * @return 0 Always + */ +static int pf_cmdev_cfg_api_show (pf_api_t * p_api) +{ + printf (">>>API<<<\n"); + printf ("device api_id = %u\n", (unsigned)p_api->api_id); + printf ( + " Each API can use max %u slots (each with max %u subslots).\n", + (unsigned)PNET_MAX_SLOTS, + (unsigned)PNET_MAX_SUBSLOTS); + + return 0; +} + +/** + * @internal + * Show everything about the slot instance. + * @param p_slot In: The slot instance. + * @return 0 Always + */ +static int pf_cmdev_cfg_slot_show (pf_slot_t * p_slot) +{ + printf (" >>>SLOT<<<\n"); + printf (" slot_nbr = %u\n", (unsigned)p_slot->slot_nbr); + printf (" in_use = %u\n", (unsigned)p_slot->in_use); + printf ( + " plug_state = %s\n", + pf_cmdev_mod_plug_state_to_string (p_slot->plug_state)); + printf (" AR = %p\n", p_slot->p_ar); + printf ( + " module_ident = %u\n", + (unsigned)p_slot->module_ident_number); + printf ( + " exp_mod_ident = %u\n", + (unsigned)p_slot->exp_module_ident_number); + + return 0; +} + +/** + * @internal + * Show everything about the subslot instance. + * @param p_subslot In: The subslot instance. + * @return 0 Always + */ +static int pf_cmdev_cfg_subslot_show (pf_subslot_t * p_subslot) +{ + printf (" >>>SUBSLOT<<<\n"); + printf (" subslot_nbr = 0x%04X\n", (unsigned)p_subslot->subslot_nbr); + printf (" in_use = %u\n", (unsigned)p_subslot->in_use); + printf ( + " plug_state = %s\n", + pf_cmdev_submod_plug_state_to_string ( + p_subslot->submodule_state.ident_info)); + printf (" AR = %p\n", p_subslot->p_ar); + printf ( + " submod_ident = 0x%08X\n", + (unsigned)p_subslot->submodule_ident_number); + printf ( + " exp_sub_ident = 0x%08X\n", + (unsigned)p_subslot->exp_submodule_ident_number); + printf ( + " direction = %s\n", + pf_cmdev_direction_to_string (p_subslot->direction)); + printf (" length_input = %u\n", (unsigned)p_subslot->length_input); + printf (" length_output = %u\n", (unsigned)p_subslot->length_output); + printf (" diag list start = "); + if (p_subslot->diag_list == PF_DIAG_IX_NULL) + { + printf ("PF_DIAG_IX_NULL (no diagnosis items)\n"); + } + else + { + printf ("%u\n", (unsigned)p_subslot->diag_list); + } + + return 0; +} + +void pf_cmdev_device_show (pnet_t * net) +{ + printf ("\nCMDEV:\n"); + (void)pf_cmdev_cfg_traverse ( + net, + pf_cmdev_cfg_dev_show, + pf_cmdev_cfg_api_show, + pf_cmdev_cfg_slot_show, + pf_cmdev_cfg_subslot_show); +} + +void pf_cmdev_diag_show (const pnet_t * net) +{ + uint16_t ix = 0; + uint16_t total = 0; + const pf_diag_item_t * p_diag; + + printf ("DIAGNOSIS\n"); + printf ( + "Max: %u items\n", + (uint16_t)NELEMENTS (net->cmdev_device.diag_items)); + + for (ix = 0; ix < NELEMENTS (net->cmdev_device.diag_items); ix++) + { + if (net->cmdev_device.diag_items[ix].in_use == true) + { + total++; + } + } + printf ("Items in use: %u\n", total); + + for (ix = 0; ix < NELEMENTS (net->cmdev_device.diag_items); ix++) + { + p_diag = &net->cmdev_device.diag_items[ix]; + if (p_diag->in_use == true) + { + printf ("[%3u] USI: 0x%04X ", ix, p_diag->usi); + if (p_diag->next == UINT16_MAX) + { + printf (" [Last] "); + } + else + { + printf ("Next: %3u", p_diag->next); + } + + if (p_diag->usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + printf ( + " Channel: 0x%04X Ch.error: 0x%04X Ext.error 0x%04X " + "Add.value 0x%08" PRIX32 " Qualifier 0x%08" PRIX32 "\n", + p_diag->fmt.std.ch_nbr, + p_diag->fmt.std.ch_error_type, + p_diag->fmt.std.ext_ch_error_type, + p_diag->fmt.std.ext_ch_add_value, + p_diag->fmt.std.qual_ch_qualifier); + } + else + { + printf ("\n"); + } + } + } + printf ("\n"); +} + +/********************** CMDEV init, exit and state ****************************/ + +void pf_cmdev_exit (pnet_t * net) +{ + if (net->cmdev_initialized == true) + { + (void)pf_diag_exit(); + os_mutex_destroy (net->cmdev_device.diag_mutex); + memset (&net->cmdev_device, 0, sizeof (net->cmdev_device)); + net->cmdev_initialized = false; + } +} + +void pf_cmdev_init (pnet_t * net) +{ + uint16_t ix; + pf_api_t * p_api = NULL; + + if (net->cmdev_initialized == false) + { + net->cmdev_initialized = true; + + memset (&net->cmdev_device, 0, sizeof (net->cmdev_device)); + net->cmdev_device.diag_mutex = os_mutex_create(); + + /* Create a list of free diag items. */ + net->cmdev_device.diag_items_free = 0; + for (ix = 0; ix < NELEMENTS (net->cmdev_device.diag_items) - 1; ix++) + { + net->cmdev_device.diag_items[ix].next = ix + 1; + } + net->cmdev_device.diag_items[NELEMENTS (net->cmdev_device.diag_items) - 1] + .next = PF_DIAG_IX_NULL; + + (void)pf_diag_init(); + + /* Create the default API */ + pf_cmdev_new_api (net, 0, &p_api); + } +} + +int pf_cmdev_get_state (const pf_ar_t * p_ar, pf_cmdev_state_values_t * p_state) +{ + int ret = -1; + + if ((p_ar != NULL) && (p_state != NULL)) + { + *p_state = p_ar->cmdev_state; + ret = 0; + } + + return ret; +} + +/** + * @internal + * Request a state transition of the specified AR. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param state In: New state. Use PF_CMDEV_STATE_xxx + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_set_state ( + pnet_t * net, + pf_ar_t * p_ar, + pf_cmdev_state_values_t state) +{ + if (state != p_ar->cmdev_state) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): New state: %s for AREP %u (was %s)\n", + __LINE__, + pf_cmdev_state_to_string (state), + p_ar->arep, + pf_cmdev_state_to_string (p_ar->cmdev_state)); + } + p_ar->cmdev_state = state; + + switch (state) + { + case PF_CMDEV_STATE_ABORT: + pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + pf_device_clear (net, p_ar); + break; + default: + /* Nothing (yet) */ + break; + } + + return 0; +} + +/************************ Local primitives *****************************/ + +int pf_cmdev_state_ind (pnet_t * net, pf_ar_t * p_ar, pnet_event_values_t event) +{ + if (p_ar != NULL) + { + /* If we move pf_fspm_state_ind(), which triggers a user callback, last in + this list then it would be possible for users to invoke + pnet_application_ready() directly in the callback and not to use + some delay mechanism. + + However then the users could not use pnet_get_ar_error_codes() at + ABORT, as the AR would already be gone when the callback is triggered. + */ + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Sending event %s for AREP %u. Current state %s\n", + __LINE__, + pf_cmdev_event_to_string (event), + p_ar->arep, + pf_cmdev_state_to_string (p_ar->cmdev_state)); + + pf_fspm_state_ind (net, p_ar, event); + pf_cmsu_cmdev_state_ind (net, p_ar, event); + pf_cmio_cmdev_state_ind (net, p_ar, event); + pf_cmwrr_cmdev_state_ind (net, p_ar, event); + pf_cmsm_cmdev_state_ind (net, p_ar, event); + pf_cmpbe_cmdev_state_ind (p_ar, event); + pf_cmrpc_cmdev_state_ind (net, p_ar, event); + } + else + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): pf_cmdev_state_ind: p_ar is NULL\n", + __LINE__); + } + + return 0; +} + +int pf_cmdev_cmio_info_ind (pnet_t * net, pf_ar_t * p_ar, bool data_possible) +{ + if (data_possible != p_ar->ready_4_data) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Incoming DataPossible indication from CMIO. value = %s\n", + __LINE__, + data_possible ? "true" : "false"); + } + switch (p_ar->cmdev_state) + { + case PF_CMDEV_STATE_W_ARDY: + case PF_CMDEV_STATE_W_ARDYCNF: + p_ar->ready_4_data = data_possible; + break; + case PF_CMDEV_STATE_WDATA: + p_ar->ready_4_data = data_possible; + if (data_possible == true) + { + pf_cmdev_state_ind (net, p_ar, PNET_EVENT_DATA); + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_DATA); + } + break; + default: + /* Ignore */ + break; + } + + return 0; +} + +int pf_cmdev_cm_abort (pnet_t * net, pf_ar_t * p_ar) +{ + int res = -1; + + if (p_ar != NULL) + { + if (p_ar->ar_param.ar_properties.device_access == true) + { + switch (p_ar->cmdev_state) + { + case PF_CMDEV_STATE_W_CRES: + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_ABORT); + res = 0; + break; + case PF_CMDEV_STATE_DATA: + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_ABORT); + res = 0; + break; + default: + /* Ignore */ + break; + } + } + else /* CMDEV */ + { + /* Any state */ + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_ABORT); + res = 0; + } + } + else + { + /* p_ar may be NULL when handling controller induced aborts */ + LOG_INFO ( + PNET_LOG, + "CMDEV(%d): pf_cmdev_cm_abort_req: p_ar is NULL\n", + __LINE__); + } + + /* cm_abort_cnf */ + return res; +} + +/** + * @internal + * ? + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR entries to remove. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_cm_init_req (pnet_t * net, pf_ar_t * p_ar) +{ + int res = -1; + + if (p_ar != NULL) + { + if (p_ar->cmdev_state == PF_CMDEV_STATE_POWER_ON) + { + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_CIND); + res = 0; + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): pf_cmdev_cm_init_req: p_ar is NULL\n", + __LINE__); + } + + /* cm_init_cnf */ + return res; +} + +/** + * @internal + * Check if a buffer contains only zero bytes. + * @param p_start In: Start of buffer. + * @param len In: Length of buffer. + * @return 0 if all bytes are zero. + * -1 if at least one byte is non-zero. + */ +int pf_cmdev_check_zero (uint8_t * p_start, uint16_t len) +{ + int ret = -1; + int zeros = (int)len; + + while ((zeros > 0) && (*p_start == 0)) + { + p_start++; + zeros--; + } + + if (zeros <= 0) + { + /* Found nothing but zeroes. */ + ret = 0; + } + else + { + /* Found something not zero. */ + ret = -1; + } + return ret; +} + +/** + * Verify that at least one operational port has a speed of at + * least 100 MBit/s, full duplex. + * + * Otherwise we should refuse to make a connection. + * + * See PdevCheckFailed() in Profinet 2.4 Protocol 5.6.3.2.1.4 CMDEV state table + * + * @param net InOut: The p-net stack instance + * @return 0 if at least one port has the required speed, else -1. + */ +static int pf_cmdev_check_pdev (pnet_t * net) +{ + return (pf_pdport_is_a_fast_port_in_use (net)) ? 0 : -1; +} + +/** + * @internal + * Check if the UUID is the CM originator UUID. + * + * ToDo: Move to pf_types and create a struct to compare against instead + * of hardcoding here. + * @param p_uuid In: The UUID to check. + * @return 0 if the UUID is correct. + * -1 if the UUID is not correct. + */ +static int pf_cmdev_check_cm_initiator_object_uuid (const pf_uuid_t * p_uuid) +{ + int ret = -1; + + if ( + (p_uuid->data1 == 0xDEA00000) && (p_uuid->data2 == 0x6c97) && + (p_uuid->data3 == 0x11D1) && (p_uuid->data4[0] == 0x82) && + (p_uuid->data4[1] == 0x71)) + { + ret = 0; + } + + return ret; +} + +/** + * @internal + * Check if the string contains only visible characters. + * + * A visible character has its ASCII value 0x20 <= x <= 0x7E. + * + * @param s In: The string to check. + * @return 0 if the string contains only visible characters. + * -1 if at least one character is invalid, or string length is zero. + */ +int pf_cmdev_check_visible_string (const char * s) +{ + int ret = -1; + + if (*s != 0) + { + while ((*s != 0) && (isprint ((int)(*s)) != 0)) + { + s++; + } + if (*s == 0) + { + ret = 0; + } + } + + return ret; +} + +/** + * @internal + * Check if the Specified AR type is supported. + * + * @param ar_type In: The AR type to check. + * @return 0 if the AR type is supported. + * -1 if the AR type is not supported. + */ +int pf_cmdev_check_ar_type (uint16_t ar_type) +{ + int ret = -1; + + if (ar_type == PF_ART_IOCAR_SINGLE) + { + ret = 0; + } + + return ret; +} + +/** + * @internal + * Check the AR param for errors. + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if no error was detected. + * -1 if an error was detected. + */ +static int pf_cmdev_check_ar_param (pf_ar_t * p_ar, pnet_result_t * p_stat) +{ + int ret = 0; /* OK until error found. */ + + if (pf_cmdev_check_ar_type (p_ar->ar_param.ar_type) != 0) + { + LOG_INFO (PNET_LOG, "CMDEV(%d): Wrong incoming AR type\n", __LINE__); + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 4); + ret = -1; + } + else if ( + pf_cmdev_check_zero ( + (uint8_t *)&p_ar->ar_param.ar_uuid, + sizeof (p_ar->ar_param.ar_uuid)) == 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 5); + ret = -1; + } + /* cm_initiator_mac_add also checked iocr_check */ + else if ((p_ar->ar_param.cm_initiator_mac_add.addr[0] & 0x01) != 0) + { + /* Is multicast MAC address */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 7); + ret = -1; + } + else if ( + pf_cmdev_check_cm_initiator_object_uuid ( + &p_ar->ar_param.cm_initiator_object_uuid) != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 8); + ret = -1; + } + else if (p_ar->ar_param.ar_properties.state != 0x1) /* Must be ACTIVE (1) */ + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 9); + ret = -1; + } + else if ( + p_ar->ar_param.ar_properties.parameterization_server == + PF_PS_EXTERNAL_PARAMETER_SERVER) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 9); + ret = -1; + } + else if ( + (p_ar->ar_param.ar_properties.device_access == true) && + (p_ar->ar_param.ar_type != PF_ART_IOSAR)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 9); + ret = -1; + } + else if (p_ar->ar_param.ar_properties.companion_ar == 3) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 9); + ret = -1; + } + else if ( + (p_ar->ar_param.cm_initiator_activity_timeout_factor < 1) || + (p_ar->ar_param.cm_initiator_activity_timeout_factor > 1000)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 10); + ret = -1; + } + /* cm_initiator_udp_rt_port is checked in iocr check */ + else if ( + (p_ar->ar_param.cm_initiator_station_name_len == 0) || + (p_ar->ar_param.cm_initiator_station_name_len > + sizeof (p_ar->ar_param.cm_initiator_station_name) - 1)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 12); + ret = -1; + } + else if ( + pf_cmdev_check_visible_string ( + p_ar->ar_param.cm_initiator_station_name) != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 13); + ret = -1; + } + else + { + p_ar->ar_param.valid = true; + } + + return ret; +} + +/** + * Find a specific expected API instance in the AR. + * @param p_ar In: The AR instance. + * @param api_id In: The API identifier to find. + * @param pp_api Out: The expected APi instance. + * @return 0 if a API instance was found. + * -1 if the API instance was not found. + */ +static int pf_cmdev_get_exp_api ( + pf_ar_t * p_ar, + uint32_t api_id, + pf_exp_api_t ** pp_api) +{ + int ret = -1; + uint16_t ix; + + ix = 0; + while ((ix < p_ar->nbr_exp_apis) && (p_ar->exp_apis[ix].api != api_id)) + { + ix++; + } + + if (ix < p_ar->nbr_exp_apis) + { + *pp_api = &p_ar->exp_apis[ix]; + + ret = 0; + } + + return ret; +} + +/** + * @internal + * Find the expected sub-module struct indicated by the API identifier and the + * slot and sub-slot numbers. + * @param p_ar In: The AR instance. + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param pp_exp_sub Out: The sub-module instance. + * @return 0 if a sub-module instance was found. + * -1 if the sub-module instance was not found. + */ +static int pf_cmdev_get_exp_sub ( + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_exp_submodule_t ** pp_exp_sub) +{ + int ret = -1; + uint16_t exp_mod_ix; + uint16_t exp_sub_ix; + pf_exp_api_t * p_exp_api = NULL; + pf_exp_module_t * p_exp_mod = NULL; + pf_exp_submodule_t * p_exp_sub = NULL; + + if ( + (pf_cmdev_get_exp_api (p_ar, api_id, &p_exp_api) == 0) && + (p_exp_api != NULL)) + { + exp_mod_ix = 0; + while ((exp_mod_ix < p_exp_api->nbr_modules) && + (p_exp_api->modules[exp_mod_ix].slot_number != slot_nbr)) + { + exp_mod_ix++; + } + + if (exp_mod_ix < p_exp_api->nbr_modules) + { + p_exp_mod = &p_exp_api->modules[exp_mod_ix]; + + exp_sub_ix = 0; + while ( + (exp_sub_ix < p_exp_mod->nbr_submodules) && + (p_exp_mod->submodules[exp_sub_ix].subslot_number != subslot_nbr)) + { + exp_sub_ix++; + } + + if (exp_sub_ix < p_exp_mod->nbr_submodules) + { + p_exp_sub = &p_exp_mod->submodules[exp_sub_ix]; + ret = 0; + } + } + } + + *pp_exp_sub = p_exp_sub; + + return ret; +} + +/** + * @internal + * Calculate which data direction we should use when looking for a data + * descriptor. + * + * For example an output module has only a single data descriptor, which has + * an output data direction. The IOPS is sent in the output CR, and the IOCS + * is sent in the input CR. + * + * An input + output module has two data descriptors. + * + * A module with no cyclic data (PNET_DIR_NO_IO) is implemented an input module. + * + * @param submodule_dir In: Whether the submodule is IN, IN+OUT etc + * @param data_dir In: The data direction for the IOCR we are + * working on (input CR or output CR) + * @param status_type In: Whether we are interested in IOCS or IOPS + * @param resulting_data_dir Out: The resulting data direction for use in the + * search. + * @return 0 if the direction could be calculated. + * -1 for illegal input combinations. + */ +int pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + pnet_submodule_dir_t submodule_dir, + pf_data_direction_values_t data_dir, + pf_dev_status_type_t status_type, + pf_data_direction_values_t * resulting_data_dir) +{ + int ret = -1; + + if (submodule_dir == PNET_DIR_NO_IO || submodule_dir == PNET_DIR_INPUT) + { + if ( + (data_dir == PF_DIRECTION_INPUT && + status_type == PF_DEV_STATUS_TYPE_IOPS) || + (data_dir == PF_DIRECTION_OUTPUT && + status_type == PF_DEV_STATUS_TYPE_IOCS)) + { + *resulting_data_dir = PF_DIRECTION_INPUT; + ret = 0; + } + } + else if (submodule_dir == PNET_DIR_IO) + { + if ( + (data_dir == PF_DIRECTION_INPUT && + status_type == PF_DEV_STATUS_TYPE_IOPS) || + (data_dir == PF_DIRECTION_OUTPUT && + status_type == PF_DEV_STATUS_TYPE_IOCS)) + { + *resulting_data_dir = PF_DIRECTION_INPUT; + ret = 0; + } + else + { + *resulting_data_dir = PF_DIRECTION_OUTPUT; + ret = 0; + } + } + else if (submodule_dir == PNET_DIR_OUTPUT) + { + if ( + (data_dir == PF_DIRECTION_INPUT && + status_type == PF_DEV_STATUS_TYPE_IOCS) || + (data_dir == PF_DIRECTION_OUTPUT && + status_type == PF_DEV_STATUS_TYPE_IOPS)) + { + *resulting_data_dir = PF_DIRECTION_OUTPUT; + ret = 0; + } + } + return ret; +} + +/** + * @internal + * Find the data descriptor within the submodule with specified direction. + * @param p_exp_sub In: The expected sub-module instance. + * @param dir In: The data direction. + * @param status_type In: Whether we are interested in IOCS or IOPS + * @param pp_desc Out: The data descriptor. + * @return 0 if the data descriptor was found. + * -1 if the data descriptor was not found. + */ +static int pf_cmdev_get_exp_sub_data_descriptor ( + pf_exp_submodule_t * p_exp_sub, + pf_data_direction_values_t dir, + pf_dev_status_type_t status_type, + pf_data_descriptor_t ** pp_desc) +{ + int ret = -1; + uint16_t ix = 0; + pf_data_direction_values_t data_direction_to_look_for = PF_DIRECTION_INPUT; + + if (p_exp_sub != NULL) + { + if ( + pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + p_exp_sub->submodule_properties.type, + dir, + status_type, + &data_direction_to_look_for) == 0) + { + for (ix = 0; ix < p_exp_sub->nbr_data_descriptors; ix++) + { + if ( + p_exp_sub->data_descriptor[ix].data_direction == + data_direction_to_look_for) + { + *pp_desc = &p_exp_sub->data_descriptor[ix]; + ret = 0; + break; + } + } + } + } + + return ret; +} + +/** + * @internal + * Collect iodata object IOCS information from IOCR param and expected + * (sub-)modules. + * @param p_ar In: The AR instance. + * @param p_iocr InOut: The IOCR instance. + * @param dir In: The data direction to consider. + * @param p_stat Out: Detailed error information. Not used. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_iocr_setup_iocs ( + pf_ar_t * p_ar, + pf_iocr_t * p_iocr, + pf_data_direction_values_t dir, + pnet_result_t * p_stat) +{ + int ret = 0; + uint32_t api_id; + uint16_t slot_nbr; + uint16_t subslot_nbr; + pf_iocr_param_t * p_iocr_param; + pf_iodata_object_t * p_iodata; + pf_data_descriptor_t * p_desc; + pf_exp_submodule_t * p_exp_sub; + uint16_t api_ix; + uint16_t ix; + uint16_t iy; + uint16_t iodata_cnt; + uint16_t in_len; + uint16_t out_len; + + in_len = p_iocr->in_length; + out_len = p_iocr->out_length; + iodata_cnt = p_iocr->nbr_data_desc; + + p_iocr_param = &p_iocr->param; + + for (api_ix = 0; api_ix < p_iocr_param->nbr_apis; api_ix++) + { + for (ix = 0; ix < p_iocr_param->apis[api_ix].nbr_iocs; ix++) + { + api_id = p_iocr_param->apis[api_ix].api; + slot_nbr = p_iocr_param->apis[api_ix].iocs[ix].slot_number; + subslot_nbr = p_iocr_param->apis[api_ix].iocs[ix].subslot_number; + if ( + pf_cmdev_get_exp_sub ( + p_ar, + api_id, + slot_nbr, + subslot_nbr, + &p_exp_sub) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): api %u exp slot %u subslot %u not found\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr); + ret = -1; + } + else if ( + pf_cmdev_get_exp_sub_data_descriptor ( + p_exp_sub, + dir, + PF_DEV_STATUS_TYPE_IOCS, + &p_desc) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): api %u exp slot %u subslot %u and dir %u not " + "found\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr, + (unsigned)dir); + } + else + { + LOG_INFO ( + PNET_LOG, + "CMDEV(%d) Read IOCS size from API %u slot %u subslot " + "0x%04x with data direction %s. AREP %u CREP %" PRIu32 ". Data+" + "IOPS+IOCS %3u+%u+%u bytes\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr, + p_desc->data_direction == PF_DIRECTION_INPUT ? " input" + : "output", + p_ar->arep, + p_iocr->crep, + (unsigned)p_desc->submodule_data_length, + (unsigned)p_desc->length_iops, + p_desc->length_iocs); + + /* Put the iocs into the same as data + iops (if it exists) */ + iy = 0; + while ((iy < iodata_cnt) && + ((p_iocr->data_desc[iy].api_id != api_id) || + (p_iocr->data_desc[iy].slot_nbr != slot_nbr) || + (p_iocr->data_desc[iy].subslot_nbr != subslot_nbr))) + { + iy++; + } + if (iy < iodata_cnt) + { + /* Re-use any pre-existing data + iops descriptor */ + p_iodata = &p_iocr->data_desc[iy]; + } + else + { + /* Use a new data descriptor. There is no data+iops descriptor */ + p_iodata = &p_iocr->data_desc[iodata_cnt]; + p_iodata->in_use = true; + iodata_cnt++; + + p_iodata->api_id = api_id; + p_iodata->slot_nbr = slot_nbr; + p_iodata->subslot_nbr = subslot_nbr; + + p_iodata->data_length = 0; + p_iodata->data_offset = 0; + p_iodata->iops_offset = 0; + p_iodata->iops_length = 0; + } + + if (p_exp_sub->submodule_properties.discard_ioxs == true) + { + p_iodata->iocs_offset = 0; + p_iodata->iocs_length = 0; + } + else + { + p_iodata->iocs_offset = + p_iocr_param->apis[api_ix].iocs[ix].frame_offset; + p_iodata->iocs_length = p_desc->length_iocs; + } + + if (dir == PF_DIRECTION_INPUT) + { + in_len += p_iodata->iocs_length; + } + else if (dir == PF_DIRECTION_OUTPUT) + { + out_len += p_iodata->iocs_length; + } + } + } + } + + p_iocr->in_length = in_len; + p_iocr->out_length = out_len; + p_iocr->nbr_data_desc = iodata_cnt; + + return ret; +} + +/** + * @internal + * Collect iodata object data and IOPS information from IOCR param and expected + * (sub-)modules. + * @param p_ar In: The AR instance. + * @param p_iocr InOut: The IOCR instance. + * @param dir In: The data direction to consider. + * @param p_stat Out: Detailed error information. Not used. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_iocr_setup_data_iops ( + pf_ar_t * p_ar, + pf_iocr_t * p_iocr, + pf_data_direction_values_t dir, + pnet_result_t * p_stat) +{ + int ret = 0; + uint32_t api_id; + uint16_t slot_nbr; + uint16_t subslot_nbr; + pf_iocr_param_t * p_iocr_param; + pf_iodata_object_t * p_iodata; + pf_data_descriptor_t * p_desc; + pf_exp_submodule_t * p_exp_sub; + uint32_t api_ix; + uint16_t ix; + uint16_t iy; + uint16_t iodata_cnt = 0; + uint16_t in_len = 0; + uint16_t out_len = 0; + uint16_t in_user_len = 0; + uint16_t out_user_len = 0; + + in_len = p_iocr->in_length; + out_len = p_iocr->out_length; + iodata_cnt = p_iocr->nbr_data_desc; + + p_iocr_param = &p_iocr->param; + + for (api_ix = 0; api_ix < p_iocr_param->nbr_apis; api_ix++) + { + for (ix = 0; ix < p_iocr_param->apis[api_ix].nbr_io_data; ix++) + { + api_id = p_iocr_param->apis[api_ix].api; + slot_nbr = p_iocr_param->apis[api_ix].io_data[ix].slot_number; + subslot_nbr = p_iocr_param->apis[api_ix].io_data[ix].subslot_number; + if ( + pf_cmdev_get_exp_sub ( + p_ar, + api_id, + slot_nbr, + subslot_nbr, + &p_exp_sub) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): api %u exp slot %u subslot %u not found\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr); + } + else if ( + pf_cmdev_get_exp_sub_data_descriptor ( + p_exp_sub, + dir, + PF_DEV_STATUS_TYPE_IOPS, + &p_desc) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): api %u exp slot %u subslot %u and dir %u not " + "found\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr, + (unsigned)dir); + } + else + { + LOG_INFO ( + PNET_LOG, + "CMDEV(%d) Read data and IOPS size from API %u slot %u subslot " + "0x%04x with data direction %s. AREP %u CREP %" PRIu32 ". Data+" + "IOPS+IOCS %3u+%u+%u bytes\n", + __LINE__, + (unsigned)api_id, + (unsigned)slot_nbr, + (unsigned)subslot_nbr, + p_desc->data_direction == PF_DIRECTION_INPUT ? " input" + : "output", + p_ar->arep, + p_iocr->crep, + (unsigned)p_desc->submodule_data_length, + (unsigned)p_desc->length_iops, + p_desc->length_iocs); + + /* Put the data+iops into the same as iocs (if it exists) */ + iy = 0; + while ((iy < iodata_cnt) && + ((p_iocr->data_desc[iy].api_id != api_id) || + (p_iocr->data_desc[iy].slot_nbr != slot_nbr) || + (p_iocr->data_desc[iy].subslot_nbr != subslot_nbr))) + { + iy++; + } + if (iy < iodata_cnt) + { + /* Re-use existing data+iops descriptor */ + p_iodata = &p_iocr->data_desc[iy]; + } + else + { + /* Use a new data descriptor */ + p_iodata = &p_iocr->data_desc[iodata_cnt]; + p_iodata->in_use = true; + iodata_cnt++; + + p_iodata->api_id = api_id; + p_iodata->slot_nbr = slot_nbr; + p_iodata->subslot_nbr = subslot_nbr; + + p_iodata->iocs_offset = 0; + p_iodata->iocs_length = 0; + } + + if ( + ((dir == PF_DIRECTION_INPUT) && + (p_exp_sub->submodule_properties + .reduce_input_submodule_data_length == true)) || + ((dir == PF_DIRECTION_OUTPUT) && + (p_exp_sub->submodule_properties + .reduce_output_submodule_data_length == true))) + { + p_iodata->data_length = 0; + p_iodata->data_offset = 0; + } + else + { + p_iodata->data_length = p_desc->submodule_data_length; + p_iodata->data_offset = + p_iocr_param->apis[api_ix].io_data[ix].frame_offset; + } + + if (p_exp_sub->submodule_properties.discard_ioxs == true) + { + /* Only allowed for NO_IO so data_length = 0 */ + p_iodata->data_length = 0; + p_iodata->data_offset = 0; + + p_iodata->iops_length = 0; + p_iodata->iops_offset = 0; + } + else + { + p_iodata->iops_length = p_desc->length_iops; + p_iodata->iops_offset = + p_iodata->data_offset + p_iodata->data_length; + } + + if (dir == PF_DIRECTION_INPUT) + { + in_len += p_iodata->data_length + p_iodata->iops_length + + p_iodata->iocs_length; + in_user_len += p_iodata->data_length + p_iodata->iops_length + + p_iodata->iocs_length; + } + else if (dir == PF_DIRECTION_OUTPUT) + { + out_len += p_iodata->data_length + p_iodata->iops_length + + p_iodata->iocs_length; + out_user_len += p_iodata->data_length + p_iodata->iops_length + + p_iodata->iocs_length; + } + } + } + } + + p_iocr->in_length = in_len; + p_iocr->out_length = out_len; + p_iocr->nbr_data_desc = iodata_cnt; + + return ret; +} + +/** + * @internal + * Collect iodata object IOCS, data and IOPS information from IOCR param and + * expected (sub-)modules. + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @param p_stat Out: Detailed error information. Not used. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_iocr_setup_desc ( + pf_ar_t * p_ar, + uint32_t crep, + pnet_result_t * p_stat) +{ + int ret = 0; + pf_data_direction_values_t dir; + pf_iocr_param_t * p_iocr_param; + pf_iocr_t * p_iocr; + + p_iocr = &p_ar->iocrs[crep]; + p_iocr->p_ar = p_ar; + p_iocr->crep = crep; + + p_iocr->in_length = 0; + p_iocr->out_length = 0; + p_iocr->nbr_data_desc = 0; + + p_iocr_param = &p_iocr->param; + + if ( + (p_iocr_param->iocr_type == PF_IOCR_TYPE_INPUT) || + (p_iocr_param->iocr_type == PF_IOCR_TYPE_MC_PROVIDER)) + { + dir = PF_DIRECTION_INPUT; + } + else + { + dir = PF_DIRECTION_OUTPUT; + } + + if (pf_cmdev_iocr_setup_data_iops (p_ar, p_iocr, dir, p_stat) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Failed to collect IOPS information\n", + __LINE__); + ret = -1; + } + else if (pf_cmdev_iocr_setup_iocs (p_ar, p_iocr, dir, p_stat) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): Failed to collect IOCS information\n", + __LINE__); + ret = -1; + } + + return ret; +} + +/** + * Check if two areas overlap (straddle). + * + * x x x Area 1 (Start = 4, Length = 3) + * 0 1 2 3 4 5 6 7 8 9 + * x x No overlap + * x x No overlap + * x x No overlap + * x x Overlap + * x x Overlap + * x x Overlap + * x x Overlap + * x x No overlap + * x x No overlap + * + * @param start_1 In: The start of area 1. + * @param length_1 In: The length of area 1. + * @param start_2 In: The start of area 2. + * @param length_2 In: The length of area 2. + * @return 0 If the areas do NOT straddle each other. + * -1 If the areas do overlap. + */ +int pf_cmdev_check_no_straddle ( + uint16_t start_1, + uint16_t length_1, + uint16_t start_2, + uint16_t length_2) +{ + int ret = 0; + + if ((length_1 > 0) && (length_2 > 0)) + { + if (start_1 <= start_2) + { + if ((start_1 + length_1) > start_2) + { + ret = -1; + } + } + else + { + if (start_1 < (start_2 + length_2)) + { + ret = -1; + } + } + } + + return ret; +} + +/** + * Check if a data_desc overlaps any previous data_desc in same iocrs[]. + * @param p_iocr In: The iocrs instance. + * @param ix_this In: The data_desc index to verify. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 If this area does not overlap any previously defined area. + * -1 If there is overlap. + */ +static int pf_cmdev_check_iocr_overlap ( + pf_iocr_t * p_iocr, + uint16_t ix_this, + pnet_result_t * p_stat) +{ + /* + * If area io_this overlaps with any previous areas [0..(io_this-1)] then + * there is an overlap. + */ + int ret = 0; /* Assume no overlap */ + uint16_t ix; + uint16_t start; + uint16_t len; + + for (ix = 0; ix < ix_this; ix++) + { + /* Check data area against all previous data_desc */ + start = p_iocr->data_desc[ix_this].data_offset; + len = p_iocr->data_desc[ix_this].data_length; + if (len > 0) + { + if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].data_offset, + p_iocr->data_desc[ix].data_length) != 0) + { + ret = -1; + } + else if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].iocs_offset, + p_iocr->data_desc[ix].iocs_length) != 0) + { + ret = -1; + } + else if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].iops_offset, + p_iocr->data_desc[ix].iops_length) != 0) + { + ret = -1; + } + } + /* Check IOPS area against all previous data_desc */ + start = p_iocr->data_desc[ix_this].iops_offset; + len = p_iocr->data_desc[ix_this].iops_length; + if (len > 0) + { + if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].data_offset, + p_iocr->data_desc[ix].data_length) != 0) + { + ret = -1; + } + else if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].iocs_offset, + p_iocr->data_desc[ix].iocs_length) != 0) + { + ret = -1; + } + else if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].iops_offset, + p_iocr->data_desc[ix].iops_length) != 0) + { + ret = -1; + } + } + } + + if (ret != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 24); + } + else + { + for (ix = 0; ix < ix_this; ix++) + { + /* Check IOCS area against all previous data_desc */ + start = p_iocr->data_desc[ix_this].iocs_offset; + len = p_iocr->data_desc[ix_this].iocs_length; + if (len > 0) + { + if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].data_offset, + p_iocr->data_desc[ix].data_length) != 0) + { + ret = -1; + } + else if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].iocs_offset, + p_iocr->data_desc[ix].iocs_length) != 0) + { + ret = -1; + } + else if ( + pf_cmdev_check_no_straddle ( + start, + len, + p_iocr->data_desc[ix].iops_offset, + p_iocr->data_desc[ix].iops_length) != 0) + { + ret = -1; + } + } + } + + if (ret != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 28); + } + } + + return ret; +} + +/** + * @internal + * Perform final validation of IOCR APIs, after the data_desc has been set up. + * @param p_ar In: The AR instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if no error was found. + * -1 if an error was found. + */ +static int pf_cmdev_check_iocr_apis (pf_ar_t * p_ar, pnet_result_t * p_stat) +{ + int ret = 0; + pf_iocr_param_t * p_iocr_param; + uint16_t api_ix = 0; + pf_api_entry_t * p_iocr_api = NULL; + uint16_t io_ix = 0; /* io_data/iocs index */ + uint16_t io2_ix = 0; /* io_data/iocs index */ + uint16_t ix = 0; /* exp_api index */ + uint16_t iy = 0; /* exp_module index */ + uint16_t iz = 0; /* exp_submodule index */ + pf_exp_api_t * p_exp_api = NULL; + pf_exp_module_t * p_exp_mod = NULL; + pf_exp_submodule_t * p_exp_sub = NULL; + uint16_t slot_nbr; + uint16_t subslot_nbr; + uint16_t combo_cnt = 0; + + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + p_iocr_param = &p_ar->iocrs[ix].param; + for (api_ix = 0; api_ix < p_iocr_param->nbr_apis; api_ix++) + { + p_iocr_api = &p_iocr_param->apis[api_ix]; + /* The IOCR API must be present in the expected API */ + p_exp_api = NULL; + if (pf_cmdev_get_exp_api (p_ar, p_iocr_api->api, &p_exp_api) != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 20); + ret = -1; + } + else if ((p_iocr_api->nbr_io_data == 0) && (p_iocr_api->nbr_iocs == 0)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 21); + ret = -1; + } + + if ((ret == 0) && (p_exp_api != NULL)) + { + /* %%%%%%%%%%%%%%%%%% io_data %%%%%%%%%%%%%%%%%%%%% */ + + if (ret == 0) + { + /* Within each io_data in each API the slot/subslot combo must be + * unique. */ + for (io_ix = 0; io_ix < p_iocr_api->nbr_io_data; io_ix++) + { + slot_nbr = p_iocr_api->io_data[io_ix].slot_number; + subslot_nbr = p_iocr_api->io_data[io_ix].subslot_number; + combo_cnt = 0; + for (io2_ix = 0; io2_ix < p_iocr_api->nbr_io_data; io2_ix++) + { + if ( + (p_iocr_api->io_data[io2_ix].slot_number == slot_nbr) && + (p_iocr_api->io_data[io2_ix].subslot_number == + subslot_nbr)) + { + combo_cnt++; + } + } + if (combo_cnt != 1) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 23); + ret = -1; + } + } + } + + for (io_ix = 0; io_ix < p_iocr_api->nbr_io_data; io_ix++) + { + /* io_data slot must be in expected modules list */ + p_exp_mod = NULL; + p_exp_sub = NULL; + for (iy = 0; iy < p_exp_api->nbr_modules; iy++) + { + if ( + p_iocr_api->io_data[io_ix].slot_number == + p_exp_api->modules[iy].slot_number) + { + p_exp_mod = &p_exp_api->modules[iy]; + } + } + if (p_exp_mod == NULL) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 22); + ret = -1; + } + else + { + /* Slot/subslot must be in corresponding exp module/exp + * submodule. */ + for (iz = 0; iz < p_exp_mod->nbr_submodules; iz++) + { + if ( + p_iocr_api->io_data[io_ix].subslot_number == + p_exp_mod->submodules[iz].subslot_number) + { + p_exp_sub = &p_exp_mod->submodules[iz]; + } + } + + if (p_exp_sub == NULL) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 23); + ret = -1; + } + } + + if ((ret == 0) && (p_exp_sub != NULL)) + { + if ( + ((p_exp_sub->submodule_properties.type == PNET_DIR_NO_IO) || + (p_exp_sub->submodule_properties.type == + PNET_DIR_INPUT)) && + ((p_iocr_param->iocr_type == PF_IOCR_TYPE_OUTPUT) || + (p_iocr_param->iocr_type == PF_IOCR_TYPE_MC_CONSUMER))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 23); + ret = -1; + } + else if ( + (p_exp_sub->submodule_properties.type == + PNET_DIR_OUTPUT) && + ((p_iocr_param->iocr_type == PF_IOCR_TYPE_INPUT) || + (p_iocr_param->iocr_type == PF_IOCR_TYPE_MC_PROVIDER))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 23); + ret = -1; + } + } + } + + /* %%%%%%%%%%%%%%%%%% IOCS %%%%%%%%%%%%%%%%%%%%% */ + + if (ret == 0) + { + /* Within each iocs in each API the slot/subslot combo must be + * unique. */ + for (io_ix = 0; io_ix < p_iocr_api->nbr_iocs; io_ix++) + { + slot_nbr = p_iocr_api->iocs[io_ix].slot_number; + subslot_nbr = p_iocr_api->iocs[io_ix].subslot_number; + combo_cnt = 0; + for (io2_ix = 0; io2_ix < p_iocr_api->nbr_iocs; io2_ix++) + { + if ( + (p_iocr_api->iocs[io2_ix].slot_number == slot_nbr) && + (p_iocr_api->iocs[io2_ix].subslot_number == + subslot_nbr)) + { + combo_cnt++; + } + } + if (combo_cnt != 1) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 27); + ret = -1; + } + } + } + + if (ret == 0) + { + for (io_ix = 0; io_ix < p_iocr_api->nbr_iocs; io_ix++) + { + p_exp_mod = NULL; + p_exp_sub = NULL; + for (iy = 0; iy < p_exp_api->nbr_modules; iy++) + { + if ( + p_iocr_api->iocs[io_ix].slot_number == + p_exp_api->modules[iy].slot_number) + { + p_exp_mod = &p_exp_api->modules[iy]; + } + } + if (p_exp_mod == NULL) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 27); + ret = -1; + } + else + { + /* Must be in corresponding exp module/exp submodule. */ + for (iz = 0; iz < p_exp_mod->nbr_submodules; iz++) + { + if ( + p_iocr_api->iocs[io_ix].subslot_number == + p_exp_mod->submodules[iz].subslot_number) + { + p_exp_sub = &p_exp_mod->submodules[iz]; + } + } + if (p_exp_sub == NULL) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 27); + ret = -1; + } + } + + if ((ret == 0) && (p_exp_sub != NULL)) + { + if ( + ((p_exp_sub->submodule_properties.type == + PNET_DIR_NO_IO) || + (p_exp_sub->submodule_properties.type == + PNET_DIR_INPUT)) && + (p_iocr_param->iocr_type == PF_IOCR_TYPE_INPUT)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 27); + ret = -1; + } + else if ( + (p_exp_sub->submodule_properties.type == + PNET_DIR_OUTPUT) && + (p_iocr_param->iocr_type == PF_IOCR_TYPE_OUTPUT)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 27); + ret = -1; + } + + if ( + (ret == 0) && + (p_exp_sub->submodule_properties.type == PNET_DIR_IO)) + { + /* Must have 2 data descriptors */ + if (p_exp_sub->nbr_data_descriptors != 2) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 28); + ret = -1; + } + } + } + } + } + } + } + + for (io_ix = 0; io_ix < p_ar->iocrs[ix].nbr_data_desc; io_ix++) + { + if (ret == 0) + { + if ( + (p_ar->iocrs[ix].data_desc[io_ix].data_offset + + p_ar->iocrs[ix].data_desc[io_ix].data_length) > + p_iocr_param->c_sdu_length) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 24); + ret = -1; + } + else if ( + (p_ar->iocrs[ix].data_desc[io_ix].iops_offset + + p_ar->iocrs[ix].data_desc[io_ix].iops_length) > + p_iocr_param->c_sdu_length) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 24); + ret = -1; + } + else if ( + (p_ar->iocrs[ix].data_desc[io_ix].iocs_offset + + p_ar->iocrs[ix].data_desc[io_ix].iocs_length) > + p_iocr_param->c_sdu_length) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 28); + ret = -1; + } + else if ( + pf_cmdev_check_iocr_overlap (&p_ar->iocrs[ix], io_ix, p_stat) != + 0) + { + ret = -1; + } + } + } + + if (ret != 0) + { + p_iocr_param->valid = false; + } + } + + return ret; +} + +/** + * @internal + * Check the IOCR param of an AR for errors. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if no error was found + * -1 if an error was found. + */ +static int pf_cmdev_check_iocr_param ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_stat) +{ + int ret = 0; /* OK until error found */ + uint16_t ix; + pf_iocr_param_t * p_iocr; + + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + p_iocr = &p_ar->iocrs[ix].param; + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else if ( + (p_iocr->iocr_type < PF_IOCR_TYPE_INPUT) || + (p_iocr->iocr_type > PF_IOCR_TYPE_MC_CONSUMER)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 4); + ret = -1; + } + /* Defer IOCReference checks to below. */ + else if ( + (p_iocr->iocr_properties.rt_class != PF_RT_CLASS_UDP) && + (p_iocr->lt_field != PNAL_ETHTYPE_PROFINET)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 6); + ret = -1; + } + else if ( + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + (p_iocr->lt_field != PNAL_ETHTYPE_IP)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 6); + ret = -1; + } + else if ( + (p_ar->ar_param.ar_properties.startup_mode == false) && + ((p_iocr->iocr_properties.rt_class < PF_RT_CLASS_1) || + (p_iocr->iocr_properties.rt_class > PF_RT_CLASS_STREAM))) + { + /* Legacy startup mode: 1 <= rtclass <= 5 + Profinet 2.4 Protocol table 984 "IOCRBlockReq" */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 7); + ret = -1; + } + else if ( + (p_ar->ar_param.ar_properties.startup_mode == true) && + ((p_iocr->iocr_properties.rt_class < PF_RT_CLASS_2) || + (p_iocr->iocr_properties.rt_class > PF_RT_CLASS_STREAM))) + { + /* Advanced startup mode: 2 <= rtclass <= 5 + Profinet 2.4 Protocol table 984 "IOCRBlockReq" */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 7); + ret = -1; + } + /* + * Spec says to check reserved_1 and reserved_2, nothing about reserved_3. + * Spec also says reserved_2 may have any value and shall not be checked + * by device. Lets do what is intended! + */ + else if ( + (p_iocr->iocr_properties.reserved_1 != 0) || + (p_iocr->iocr_properties.reserved_3 != 0)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 7); + ret = -1; + } + else if ( + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + ((p_iocr->c_sdu_length < 12) || (p_iocr->c_sdu_length > 1440))) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_1) && + ((p_iocr->c_sdu_length < 40) || (p_iocr->c_sdu_length > 1440)))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 8); + ret = -1; + } + else if ( + ((p_iocr->iocr_type == PF_IOCR_TYPE_MC_CONSUMER) || + (p_iocr->iocr_type == PF_IOCR_TYPE_MC_PROVIDER)) && + (((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_1) && + ((p_iocr->frame_id < 0xF800) || (p_iocr->frame_id > 0xFBFF))) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_2) && + ((p_iocr->frame_id < 0xBC00) || (p_iocr->frame_id > 0xBFFF))) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_3) && + ((p_iocr->frame_id < 0x0100) || (p_iocr->frame_id > 0x7FFF))) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + ((p_iocr->frame_id < 0xF800) || (p_iocr->frame_id > 0xFBFF))))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 9); + ret = -1; + } + else if ( + (p_iocr->iocr_type == PF_IOCR_TYPE_INPUT) && + (((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_1) && + ((p_iocr->frame_id < 0xC000) || (p_iocr->frame_id > 0xF7FF))) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_2) && + ((p_iocr->frame_id < 0x8000) || (p_iocr->frame_id > 0xBBFF))) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_3) && + ((p_iocr->frame_id < 0x0100) || (p_iocr->frame_id > 0x7FFF))) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + ((p_iocr->frame_id < 0xC000) || (p_iocr->frame_id > 0xF7FF))))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 9); + ret = -1; + } + else if ( + (p_iocr->send_clock_factor != 1) && (p_iocr->send_clock_factor != 2) && + (p_iocr->send_clock_factor != 4) && (p_iocr->send_clock_factor != 8) && + (p_iocr->send_clock_factor != 16) && + (p_iocr->send_clock_factor != 32) && + (p_iocr->send_clock_factor != 64) && + (p_iocr->send_clock_factor != 128)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 10); + ret = -1; + } + /* ToDo: Handle IOSAR: + * else if (p_iocr->send_clock_factor != local_active_send_clock_factor) + * // From established AR + * { + * pf_set_error(p_stat, PNET_ERROR_CODE_CONNECT, + * PNET_ERROR_DECODE_PNIO, PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + * 10); ret = -1; + * } + */ + else if ( + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_1) || + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_2) || + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_3)) && + (((p_iocr->reduction_ratio < 1) || (p_iocr->reduction_ratio > 512)) || + ((p_iocr->reduction_ratio >= 256) && + (p_iocr->send_clock_factor > 64)) || + ((p_iocr->reduction_ratio == 512) && + (p_iocr->send_clock_factor > 32)) || + (pf_fspm_get_min_device_interval (net) > + p_iocr->send_clock_factor * p_iocr->reduction_ratio))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 11); + ret = -1; + } + else if ( + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + (((p_iocr->reduction_ratio < 1) || + (p_iocr->reduction_ratio > 16384)) || + ((p_iocr->reduction_ratio >= 8192) && + (p_iocr->send_clock_factor > 64)) || + ((p_iocr->reduction_ratio == 16384) && + (p_iocr->send_clock_factor > 32)))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 11); + ret = -1; + } + else if ((p_iocr->phase == 0) || (p_iocr->phase > p_iocr->reduction_ratio)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 12); + ret = -1; + } + else if ( + (p_iocr->frame_send_offset != 0xffffffffUL) && /* Best effort is OK */ + ((p_iocr->frame_send_offset >= + (p_iocr->send_clock_factor * 31250UL)) || /* ns */ + ((p_iocr->frame_send_offset >= 0x003d0900UL) && + (p_iocr->frame_send_offset <= 0xfffffffeUL)))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 14); + ret = -1; + } + /* No check for dataHoldFactor (legacy). */ + else if ( + ((p_iocr->data_hold_factor < 1) || + (p_iocr->data_hold_factor > 0x1e00)) || + ((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + (((uint32_t)p_iocr->data_hold_factor * p_iocr->reduction_ratio * + p_iocr->send_clock_factor * 1000) / + 32 > + 61440000)) || + (((p_iocr->iocr_properties.rt_class == PF_RT_CLASS_1) || + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_2) || + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_3)) && + (((uint32_t)p_iocr->data_hold_factor * p_iocr->reduction_ratio * + p_iocr->send_clock_factor * 1000) / + 32 > + 1920000))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 16); + ret = -1; + } + else if (p_iocr->iocr_tag_header.vlan_id != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 17); + ret = -1; + } + else if (p_iocr->iocr_tag_header.iocr_user_priority != 6) /* IOCR_PRIORITY + */ + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 17); + ret = -1; + } + else if ( + ((p_iocr->iocr_type == PF_IOCR_TYPE_MC_PROVIDER) || + (p_iocr->iocr_type == PF_IOCR_TYPE_MC_CONSUMER)) && + ((p_iocr->iocr_multicast_mac_add.addr[0] & 0x01) == 0)) /* Not an MC + addr */ + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 18); + ret = -1; + } + else if (p_iocr->nbr_apis == 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + 19); + ret = -1; + } + /* Check of apis later */ + else if ( + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + (pf_cmdev_check_zero ( + p_ar->ar_param.cm_initiator_mac_add.addr, + sizeof (p_ar->ar_param.cm_initiator_mac_add.addr)) != 0)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 7); + ret = -1; + } + else if ( + (p_iocr->iocr_properties.rt_class == PF_RT_CLASS_UDP) && + (p_ar->ar_param.cm_initiator_udp_rt_port <= 0x03FF)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 11); + ret = -1; + } + else + { + p_iocr->valid = true; + } + } + + return ret; +} + +/** + * @internal + * Configure an expected sub-module. + * + * This function configures an expected sub-module. + * Perform final checks of parameters and let the application plug the + * sub-module if needed. + * + * Triggers user call-back \a pnet_exp_submodule_ind() + * + * @param net InOut: The p-net stack instance + * @param p_exp_api In: The expected API instance. + * @param p_exp_mod In: The expected sub-module instance. + * @param p_cfg_api InOut: The configured API instance. + * @param p_cfg_slot InOut: The configured module instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_exp_submodule_configure ( + pnet_t * net, + const pf_exp_api_t * p_exp_api, + const pf_exp_module_t * p_exp_mod, + pf_api_t * p_cfg_api, + pf_slot_t * p_cfg_slot, + pnet_result_t * p_stat) +{ + int ret = -1; + uint16_t sub_ix; + uint16_t subslot_nbr; + const pf_exp_submodule_t * p_exp_sub = NULL; + pf_subslot_t * p_cfg_sub = NULL; + uint16_t api_ix; + uint16_t ix; + uint16_t data_ix; + uint16_t cnt; + pf_ar_t * p_ar; + pf_iocr_param_t * p_iocr_param; + pf_api_entry_t * p_iocr_api; + uint16_t i; + pnet_data_cfg_t exp_data = {0}; + + ret = 0; /* Assume all goes well */ + for (sub_ix = 0; sub_ix < p_exp_mod->nbr_submodules; sub_ix++) + { + p_exp_sub = &p_exp_mod->submodules[sub_ix]; + if ( + pf_cmdev_get_subslot ( + p_cfg_slot, + p_exp_sub->subslot_number, + &p_cfg_sub) != 0) + { + memset (&exp_data, 0, sizeof (exp_data)); + for (i = 0; i < p_exp_sub->nbr_data_descriptors; i++) + { + if (p_exp_sub->data_descriptor[i].data_direction == PF_DIRECTION_INPUT) + { + exp_data.data_dir |= PNET_DIR_INPUT; + exp_data.insize = + p_exp_sub->data_descriptor[i].submodule_data_length; + } + if (p_exp_sub->data_descriptor[i].data_direction == PF_DIRECTION_OUTPUT) + { + exp_data.data_dir |= PNET_DIR_OUTPUT; + exp_data.outsize = + p_exp_sub->data_descriptor[i].submodule_data_length; + } + } + + /* + * Return code is not interesting here. + */ + (void)pf_fspm_exp_submodule_ind ( + net, + p_exp_api->api, + p_exp_mod->slot_number, + p_exp_sub->subslot_number, + p_exp_mod->module_ident_number, + p_exp_sub->submodule_ident_number, + &exp_data); + } + else + { + if (p_exp_sub->submodule_ident_number != p_cfg_sub->submodule_ident_number) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Substitute mode for slot %u subslot 0x%04x Sub" + "module expected 0x%08" PRIx32 " configured 0x%08" PRIx32 "\n", + __LINE__, + p_exp_mod->slot_number, + p_exp_sub->subslot_number, + p_exp_sub->submodule_ident_number, + p_cfg_sub->submodule_ident_number); + + p_cfg_sub->submodule_state.ident_info = PF_SUBMOD_PLUG_SUBSTITUTE; + } + else + { + p_cfg_sub->submodule_state.ident_info = PF_SUBMOD_PLUG_OK; + } + } + + if (ret == 0) + { + /* Within each slot a sub-slot number may only be specified once. */ + subslot_nbr = p_exp_sub->subslot_number; + cnt = 0; + for (ix = 0; ix < sub_ix; ix++) + { + if (subslot_nbr == p_exp_mod->submodules[ix].subslot_number) + { + cnt++; + } + } + if (cnt != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 10); + ret = -1; + } + } + + if (ret == 0) + { + /* Each exp sub-slot must be used in at least one IOCR. */ + cnt = 0; + p_ar = p_cfg_slot->p_ar; + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + p_iocr_param = &p_ar->iocrs[ix].param; + for (api_ix = 0; api_ix < p_iocr_param->nbr_apis; api_ix++) + { + if (p_iocr_param->apis[api_ix].api == p_exp_api->api) + { + p_iocr_api = &p_iocr_param->apis[api_ix]; + + for (data_ix = 0; data_ix < p_iocr_api->nbr_io_data; + data_ix++) + { + if ( + (p_iocr_api->io_data[data_ix].slot_number == + p_exp_mod->slot_number) && + (p_iocr_api->io_data[data_ix].subslot_number == + p_exp_sub->subslot_number)) + { + cnt++; + } + } + for (data_ix = 0; data_ix < p_iocr_api->nbr_iocs; data_ix++) + { + if ( + (p_iocr_api->iocs[data_ix].slot_number == + p_exp_mod->slot_number) && + (p_iocr_api->iocs[data_ix].subslot_number == + p_exp_sub->subslot_number)) + { + cnt++; + } + } + } + } + } + + if (cnt == 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 10); + ret = -1; + } + } + + if (ret == 0) + { + if (p_exp_sub->subslot_number > 0x9FFF) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 10); + ret = -1; + } + else if ( + pf_cmdev_get_subslot ( + p_cfg_slot, + p_exp_sub->subslot_number, + &p_cfg_sub) != 0) + { + /* Not supported in GSDML/Application */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 10); + ret = -1; + } + else if ( + (p_exp_sub->subslot_number >= PNET_SUBSLOT_DAP_INTERFACE_1_IDENT) && + (p_exp_api->api != 0)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 10); + ret = -1; + } + else if ( + (((p_exp_sub->submodule_properties.type == PNET_DIR_NO_IO) || + (p_exp_sub->submodule_properties.type == PNET_DIR_INPUT)) && + !((p_exp_sub->nbr_data_descriptors == 1) && + (p_exp_sub->data_descriptor[0].data_direction == + PF_DIRECTION_INPUT))) || /* Must have input descriptor. */ + ((p_exp_sub->submodule_properties.type == PNET_DIR_OUTPUT) && + !((p_exp_sub->nbr_data_descriptors == 1) && + (p_exp_sub->data_descriptor[0].data_direction == + PF_DIRECTION_OUTPUT))) || /* Must have output descriptor. */ + ((p_exp_sub->submodule_properties.type == PNET_DIR_IO) && + !((p_exp_sub->nbr_data_descriptors == 2) && /* Must have both input + and output + descriptors. */ + (((p_exp_sub->data_descriptor[0].data_direction == + PF_DIRECTION_INPUT) || + (p_exp_sub->data_descriptor[1].data_direction == + PF_DIRECTION_INPUT)) && + ((p_exp_sub->data_descriptor[0].data_direction == + PF_DIRECTION_OUTPUT) || + (p_exp_sub->data_descriptor[1].data_direction == + PF_DIRECTION_OUTPUT)))))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 12); + ret = -1; + } + else if ( + (p_exp_sub->submodule_properties.type == PNET_DIR_OUTPUT) && + (p_exp_sub->submodule_properties.sharedInput == true)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 12); + ret = -1; + } + else if ( + (p_exp_sub->submodule_properties + .reduce_input_submodule_data_length == true) && + ((p_exp_sub->submodule_properties.type == PNET_DIR_INPUT) || + (p_exp_sub->submodule_properties.type == PNET_DIR_IO))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 12); + ret = -1; + } + else if ( + (p_exp_sub->submodule_properties + .reduce_output_submodule_data_length == true) && + ((p_exp_sub->submodule_properties.type == PNET_DIR_OUTPUT) || + (p_exp_sub->submodule_properties.type == PNET_DIR_IO))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 12); + ret = -1; + } + else if ( + (p_exp_sub->submodule_properties.discard_ioxs == true) && + (p_exp_sub->submodule_properties.type != PNET_DIR_IO)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 12); + ret = -1; + } + /* Reserved member not checked. */ + else + { + p_cfg_sub->exp_submodule_ident_number = + p_exp_sub->submodule_ident_number; + + /* + * During device init the stack plugs its own DAP. + * At that time there is no AR. + * When the DAP is plugged by the application there is an AR (from + * the connect). Update the AR reference if not already set - this + * is important in case the application wants to set IOCS of a DAP + * sub-module (which it should). + */ + if (p_cfg_sub->p_ar == NULL) + { + /* Inherit AR from slot */ + p_cfg_sub->p_ar = p_cfg_slot->p_ar; + } + + for (data_ix = 0; data_ix < p_exp_sub->nbr_data_descriptors; + data_ix++) + { + if ( + (p_exp_sub->data_descriptor[data_ix].data_direction != + PF_DIRECTION_INPUT) && + (p_exp_sub->data_descriptor[data_ix].data_direction != + PF_DIRECTION_OUTPUT)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 13); + ret = -1; + } + else if ( + (p_exp_sub->data_descriptor[data_ix].data_direction == + PF_DIRECTION_INPUT) && + (p_exp_sub->submodule_properties.type == PNET_DIR_OUTPUT)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 13); + ret = -1; + } + else if ( + (p_exp_sub->data_descriptor[data_ix].data_direction == + PF_DIRECTION_OUTPUT) && + ((p_exp_sub->submodule_properties.type != PNET_DIR_OUTPUT) && + (p_exp_sub->submodule_properties.type != PNET_DIR_IO))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 13); + ret = -1; + } + /* Reserved member not checked. */ + else if ( + (p_exp_sub->data_descriptor[data_ix].submodule_data_length > + 1439) || + ((p_exp_sub->submodule_properties.type == PNET_DIR_NO_IO) && + (p_exp_sub->data_descriptor[data_ix].submodule_data_length != + 0))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 14); + ret = -1; + } + else if (p_exp_sub->data_descriptor[data_ix].length_iops != 1) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 15); + ret = -1; + } + else if (p_exp_sub->data_descriptor[data_ix].length_iocs != 1) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 16); + ret = -1; + } + } + } + } + } + + return ret; +} + +/** + * @internal + * Configure an expected module. + * + * This function configures an expected module. + * Perform final checks of parameters and let the application plug the module if + * needed. + * + * Triggers the user call-back \a pnet_exp_module_ind() and \a + * pnet_exp_submodule_ind(). + * + * @param net InOut: The p-net stack instance + * @param p_exp_api In: The expected API instance. + * @param p_cfg_api InOut: The configured API instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_exp_modules_configure ( + pnet_t * net, + const pf_exp_api_t * p_exp_api, + pf_api_t * p_cfg_api, + pnet_result_t * p_stat) +{ + int ret = -1; + uint16_t mod_ix; + const pf_exp_module_t * p_exp_mod = NULL; + pf_slot_t * p_cfg_slot = NULL; + uint16_t slot; + uint16_t cnt; + uint16_t ix; + + ret = 0; /* Assume all goes well */ + for (mod_ix = 0; mod_ix < p_exp_api->nbr_modules; mod_ix++) + { + if (ret == 0) + { + p_exp_mod = &p_exp_api->modules[mod_ix]; + + /* Within each API a slot_number may only be specified once. */ + slot = p_exp_mod->slot_number; + cnt = 0; + for (ix = 0; ix < mod_ix; ix++) + { + if (slot == p_exp_api->modules[ix].slot_number) + { + cnt++; + } + } + if (cnt != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 6); + ret = -1; + } + + /* If the module is not yet plugged then let the application plug it + * now! */ + if (pf_cmdev_get_slot (p_cfg_api, p_exp_mod->slot_number, &p_cfg_slot) != 0) + { + /* + * Return code is not interesting here. + */ + (void)pf_fspm_exp_module_ind ( + net, + p_exp_api->api, + p_exp_mod->slot_number, + p_exp_mod->module_ident_number); + } + else + { + if (p_exp_mod->module_ident_number != p_cfg_slot->module_ident_number) + { + p_cfg_slot->plug_state = PF_MOD_PLUG_SUBSTITUTE; + } + else + { + p_cfg_slot->plug_state = PF_MOD_PLUG_PROPER_MODULE; + } + } + } + + if (ret == 0) + { + if (p_exp_mod->slot_number > 0x7FFF) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 6); + ret = -1; + } + /* Defer slot_number not unique test to later. */ + else if ( + pf_cmdev_get_slot (p_cfg_api, p_exp_mod->slot_number, &p_cfg_slot) != + 0) + { + /* Not supported in GSDML/Application */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 6); + ret = -1; + } + else if (p_exp_mod->module_ident_number == 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 7); + ret = -1; + } + /* No check of module properties. */ + else if (p_exp_mod->nbr_submodules == 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 9); + ret = -1; + } + else + { + /* + * During device init the stack plugs its own DAP. + * At that time there is no AR. + * When the DAP is plugged by the application there is an AR (from + * the connect). Update the AR reference if not already set. This is + * important in case the application wants (it should!) to set IOCS + * of a DAP sub-module. + */ + if (p_cfg_slot->p_ar == NULL) + { + /* Inherit AR from API */ + p_cfg_slot->p_ar = p_cfg_api->p_ar; + } + + p_cfg_slot->exp_module_ident_number = + p_exp_mod->module_ident_number; + ret = pf_cmdev_exp_submodule_configure ( + net, + p_exp_api, + p_exp_mod, + p_cfg_api, + p_cfg_slot, + p_stat); + } + } + } + + return ret; +} + +/** + * @internal + * Configure an expected API. + * + * This function configures an expected API. + * Perform final checks of parameters and let the application plug the + * (sub-)modules when needed. + * + * Triggers the user call-back \a pnet_exp_module_ind() and \a + * pnet_exp_submodule_ind(). + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_exp_apis_configure ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_stat) +{ + int ret = -1; + pf_exp_api_t * p_exp_api = NULL; + pf_api_t * p_cfg_api = NULL; + uint16_t api_ix; + + /* Error discovery for the whole block */ + if ( + (p_ar->ar_param.ar_properties.device_access == false) && + (p_ar->nbr_exp_apis == 0)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + 4); + } + else + { + ret = 0; /* Assume all goes well */ + for (api_ix = 0; api_ix < p_ar->nbr_exp_apis; api_ix++) + { + if (ret == 0) + { + p_exp_api = &p_ar->exp_apis[api_ix]; + /* If it does not exist then create it */ + if ( + (pf_cmdev_get_api (net, p_exp_api->api, &p_cfg_api) == 0) || + (pf_cmdev_new_api (net, p_exp_api->api, &p_cfg_api) == 0)) + { + p_cfg_api->p_ar = p_ar; + + ret = pf_cmdev_exp_modules_configure ( + net, + p_exp_api, + p_cfg_api, + p_stat); + p_exp_api->valid = (ret == 0); + } + } + } + } + + return ret; +} + +/** + * @internal + * Check the alarm CR block for errors. + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_check_alarm_cr (pf_ar_t * p_ar, pnet_result_t * p_stat) +{ + int ret = 0; + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else if (p_ar->alarm_cr_request.alarm_cr_type != PF_ALARM_CR) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 4); + ret = -1; + } + else if ( + ((p_ar->alarm_cr_request.alarm_cr_properties.transport_udp == false) && + (p_ar->alarm_cr_request.lt_field != PNAL_ETHTYPE_PROFINET)) || + ((p_ar->alarm_cr_request.alarm_cr_properties.transport_udp == true) && + (p_ar->alarm_cr_request.lt_field != PNAL_ETHTYPE_IP))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 5); + ret = -1; + } + /* alarm_cr_properties.reserved not checked */ + else if ( + (p_ar->alarm_cr_request.rta_timeout_factor < 1) || + (p_ar->alarm_cr_request.rta_timeout_factor > 0x0064)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 7); + ret = -1; + } + else if ( + (p_ar->alarm_cr_request.rta_retries < 3) || + (p_ar->alarm_cr_request.rta_retries > 15)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 8); + ret = -1; + } + else if ( + (p_ar->alarm_cr_request.max_alarm_data_length < 200) || + (p_ar->alarm_cr_request.max_alarm_data_length > 1432)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 10); + ret = -1; + } + else if (p_ar->alarm_cr_request.alarm_cr_tag_header_high.vlan_id != 0) /* No + VLAN + */ + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 11); + ret = -1; + } + else if ( + p_ar->alarm_cr_request.alarm_cr_tag_header_high.alarm_user_priority != + 6) /* Alarm CR priority high */ + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 11); + ret = -1; + } + else if (p_ar->alarm_cr_request.alarm_cr_tag_header_low.vlan_id != 0) /* No + VLAN + */ + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 12); + ret = -1; + } + else if ( + p_ar->alarm_cr_request.alarm_cr_tag_header_low.alarm_user_priority != + 5) /* Alarm CR priority low */ + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + 12); + ret = -1; + } + else + { + p_ar->alarm_cr_request.valid = true; + } + + return ret; +} + +/** + * @internal + * Check the AR RPC block for errors. + * + * Validates that the UDP port is in correct range. + * + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error information if return != 0. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_check_ar_rpc (pf_ar_t * p_ar, pnet_result_t * p_stat) +{ + int ret = 0; /* OK until we discover an error. */ + + /* Error discovery */ + if (p_ar->ar_rpc_request.initiator_rpc_server_port < 0x0400) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_RPC_BLOCK_REQ, + 4); + ret = -1; + } + else + { + p_ar->ar_rpc_request.valid = true; + } + + return ret; +} + +/** + * @internal + * Check the AR for errors. + * + * This function implements the APDUCheck function in the Profinet spec. + * + * Triggers the user call-back \a pnet_exp_module_ind() and \a + * pnet_exp_submodule_ind(). + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error information if return != 0; + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_check_apdu ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_stat) +{ + int ret = -1; + uint16_t ix; + + if (p_ar != NULL) + { + if (p_ar->nbr_ar_param > 0) + { + ret = pf_cmdev_check_ar_param (p_ar, p_stat); + } + + if (ret == 0) + { + ret = pf_cmdev_check_iocr_param (net, p_ar, p_stat); + } + + if (ret == 0) + { + /* Let the application plug expected configuration */ + ret = pf_cmdev_exp_apis_configure (net, p_ar, p_stat); + } + + if (ret == 0) + { + /* Build internal data structure for each IOCR */ + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + if ((ret == 0) && (pf_cmdev_iocr_setup_desc (p_ar, ix, p_stat) != 0)) + { + ret = -1; + } + } + } + + if (ret == 0) + { + /* Perform the final checks of the IOCRs */ + ret = pf_cmdev_check_iocr_apis (p_ar, p_stat); + } + + if (ret == 0) + { + if ( + (p_ar->ar_param.ar_properties.device_access == false) && + ((p_ar->input_cr_cnt == 0) || (p_ar->output_cr_cnt == 0))) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_IOCR_MISSING); + ret = -1; + } + else if ( + (p_ar->ar_param.ar_properties.device_access == false) && + (p_ar->nbr_alarm_cr != 1)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_WRONG_BLOCK_COUNT); + ret = -1; + } +#if PNET_OPTION_MC_CR + else if ((p_ar->nbr_mcr == 0) && (p_ar->mcr_cons_cnt > 0)) + { + /* ToDo: Which error_code_2? */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_WRONG_BLOCK_COUNT); + ret = -1; + } +#endif + else if ((p_ar->ir_info.valid == false) && (p_ar->rtc3_present == true)) + { + /* ToDo: Which error_code_2? */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_WRONG_BLOCK_COUNT); + ret = -1; + } + } + + if ((ret == 0) && (p_ar->nbr_alarm_cr > 0)) + { + ret = pf_cmdev_check_alarm_cr (p_ar, p_stat); + } + + if ((ret == 0) && (p_ar->nbr_rpc_server > 0)) + { + ret = pf_cmdev_check_ar_rpc (p_ar, p_stat); + } + } + + return ret; +} + +int pf_cmdev_generate_submodule_diff (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = 0; + uint16_t exp_api_ix; + uint16_t exp_mod_ix; + uint16_t exp_sub_ix; + uint16_t slot_nbr; + uint16_t subslot_nbr; + pf_api_t * p_cfg_api = NULL; + pf_slot_t * p_cfg_slot = NULL; + pf_subslot_t * p_cfg_subslot = NULL; + uint16_t nbr_api_diffs = 0; + uint16_t nbr_mod_diffs = 0; + uint16_t nbr_sub_diffs = 0; + bool has_api_diff = false; + bool has_mod_diff = false; + bool has_sub_diff = false; + pf_submodule_state_t * p_submodule_state; + + if (p_ar == NULL) + { + return -1; + } + + /* Generate a diff including all expected (sub)modules */ + for (exp_api_ix = 0; exp_api_ix < p_ar->nbr_exp_apis; exp_api_ix++) + { + p_ar->api_diffs[nbr_api_diffs].api = p_ar->exp_apis[exp_api_ix].api; + + if (pf_cmdev_get_api (net, p_ar->exp_apis[exp_api_ix].api, &p_cfg_api) != 0) + { + /* API not found in CFG */ + has_api_diff = true; + } + else + { + for (exp_mod_ix = 0; + exp_mod_ix < p_ar->exp_apis[exp_api_ix].nbr_modules; + exp_mod_ix++) + { + slot_nbr = + p_ar->exp_apis[exp_api_ix].modules[exp_mod_ix].slot_number; + + /* In case an error is found */ + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .slot_number = slot_nbr; + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_state.format_indicator = true; + + if (pf_cmdev_get_slot (p_cfg_api, slot_nbr, &p_cfg_slot) != 0) + { + /* slot_number not found in CFG for specified API */ + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .module_state = PF_MOD_PLUG_NO_MODULE; + has_mod_diff = true; + } + else if ( + p_ar->exp_apis[exp_api_ix] + .modules[exp_mod_ix] + .module_ident_number != p_cfg_slot->module_ident_number) + { + /* module ident number does not match CFG */ + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .module_state = PF_MOD_PLUG_WRONG_MODULE; + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .module_ident_number = p_cfg_slot->module_ident_number; + has_mod_diff = true; + } + else + { + /* In case an error is found */ + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .module_ident_number = p_cfg_slot->module_ident_number; + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .module_state = PF_MOD_PLUG_PROPER_MODULE; + + for (exp_sub_ix = 0; exp_sub_ix < p_ar->exp_apis[exp_api_ix] + .modules[exp_mod_ix] + .nbr_submodules; + exp_sub_ix++) + { + subslot_nbr = p_ar->exp_apis[exp_api_ix] + .modules[exp_mod_ix] + .submodules[exp_sub_ix] + .subslot_number; + + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .subslot_number = subslot_nbr; + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_state.format_indicator = true; + + if ( + pf_cmdev_get_subslot ( + p_cfg_slot, + subslot_nbr, + &p_cfg_subslot) != 0) + { + /* subslot_number not found in CFG for specified API and + * slot number */ + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_state.ident_info = PF_SUBMOD_PLUG_NO; + has_sub_diff = true; + } + else + { + /* slot_number/subslot_number configured for this API */ + if ( + p_ar->exp_apis[exp_api_ix] + .modules[exp_mod_ix] + .submodules[exp_sub_ix] + .submodule_ident_number != + p_cfg_subslot->submodule_ident_number) + { + /* submodule ident number does not match CFG */ + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_ident_number = + p_cfg_subslot->submodule_ident_number; + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_state.ident_info = PF_SUBMOD_PLUG_WRONG; + has_sub_diff = true; + } + + p_submodule_state = &p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_state; + + /* Check submodule diagnosis state and update diff. */ + if ( + (p_cfg_subslot->submodule_state.fault == true) || + (p_cfg_subslot->submodule_state.maintenance_demanded == + true) || + (p_cfg_subslot->submodule_state.maintenance_required == + true)) + { + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_ident_number = + p_cfg_subslot->submodule_ident_number; + + p_submodule_state->fault = + p_cfg_subslot->submodule_state.fault; + p_submodule_state->maintenance_demanded = + p_cfg_subslot->submodule_state.maintenance_demanded; + p_submodule_state->maintenance_required = + p_cfg_subslot->submodule_state.maintenance_required; + has_sub_diff = true; + } + + /* Check if other AR owns the submodule and update diff */ + if (p_cfg_subslot->p_ar != NULL && p_cfg_subslot->p_ar != p_ar) + { + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .submodule_diffs[nbr_sub_diffs] + .submodule_ident_number = + p_cfg_subslot->submodule_ident_number; + + p_submodule_state->ar_info = + PF_SUBMOD_AR_INFO_LOCKED_BY_IO_CONTROLLER; + has_sub_diff = true; + } + } + + if (has_sub_diff == true) + { + nbr_sub_diffs++; + has_sub_diff = false; + + has_mod_diff = true; + } + } + p_ar->api_diffs[nbr_api_diffs] + .module_diffs[nbr_mod_diffs] + .nbr_submodule_diffs = nbr_sub_diffs; + nbr_sub_diffs = 0; + } + if (has_mod_diff == true) + { + nbr_mod_diffs++; + has_mod_diff = false; + + has_api_diff = true; + } + } + p_ar->api_diffs[nbr_api_diffs].nbr_module_diffs = nbr_mod_diffs; + nbr_mod_diffs = 0; + } + if (has_api_diff == true) + { + nbr_api_diffs++; + has_api_diff = false; + } + } + p_ar->nbr_api_diffs = nbr_api_diffs; + + return ret; +} + +/** + * @internal + * Verify that the frame id is not already used by any AR. + * + * Looks in all ARs, both in input CR and output CR. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param frame_id In: The frame id to check. + * @return true if the frame_id is free to use. + * false if the frame id is already used. + */ +static bool pf_cmdev_verify_free_frame_id (pnet_t * net, uint16_t frame_id) +{ + int ix = 0; + int iy = 0; + pf_ar_t * p_ar = NULL; + + for (ix = 0; ix < PNET_MAX_AR; ix++) + { + p_ar = pf_ar_find_by_index (net, ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + for (iy = 0; iy < p_ar->nbr_iocrs; iy++) + { + if (p_ar->iocrs[iy].param.frame_id == frame_id) + { + return false; + } + } + } + } + + return true; +} + +/** + * @internal + * The controller may send 0xffff as the frame id for output CRs. + * In that case we must supply a preferred frame id in the response. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + */ +static void pf_cmdev_fix_frame_id (pnet_t * net, pf_ar_t * p_ar) +{ + uint16_t ix; + pf_iocr_param_t * p_iocr_param; + uint16_t start; + uint16_t stop; + uint16_t frame_id; + + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + p_iocr_param = &p_ar->iocrs[ix].param; + if ( + (p_iocr_param->iocr_type == PF_IOCR_TYPE_OUTPUT) && + (p_iocr_param->frame_id == 0xFFFF)) + { + switch (p_iocr_param->iocr_properties.rt_class) + { + case PF_RT_CLASS_1: + start = 0xC000; + stop = 0xF7FF; + break; + case PF_RT_CLASS_2: + start = 0x8000; + stop = 0xBBFF; + break; + case PF_RT_CLASS_3: + start = 0x0100; + stop = 0x7FFF; + break; + case PF_RT_CLASS_UDP: + start = 0xC000; + stop = 0xF7FF; + break; + default: + start = 0xFFFF; + stop = 0xFFFF; + LOG_ERROR (PNET_LOG, "CMDEV(%d): Invalid rt_class\n", __LINE__); + break; + } + + frame_id = start; + while ((frame_id <= stop) && + (pf_cmdev_verify_free_frame_id (net, frame_id) == false)) + { + frame_id++; + } + + if (frame_id <= stop) + { + p_iocr_param->frame_id = frame_id; + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Using FrameID 0x%04x for output CR with AREP %u " + "CREP %" PRIu32 "\n", + __LINE__, + frame_id, + p_ar->arep, + p_ar->iocrs[ix].crep); + } + else + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): No free frame_id found\n", + __LINE__); + } + } + } +} + +/** + * @internal + * Handle a negative result to a connect request. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error information. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_cm_connect_rsp_neg ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_stat) +{ + if (p_ar->ar_param.ar_properties.device_access == false) + { + pnet_create_log_book_entry ( + net, + p_ar->arep, + &p_stat->pnio_status, + (p_stat->add_data_1 << 16) + (p_stat->add_data_2)); + LOG_ERROR (PNET_LOG, "CMDEV(%d): Connect failed\n", __LINE__); + } + + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_CIND); + + return 0; +} + +/** + * @internal + * Handle a positive answer to a connect request. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error info if returning != 0 + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmdev_cm_connect_rsp_pos ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_stat) +{ + int ret = -1; + + if (p_ar->ar_param.ar_properties.device_access == false) + { + if (p_ar->cmdev_state == PF_CMDEV_STATE_W_CRES) + { + ret = pf_cmsu_start_req (net, p_ar, p_stat); /* Start all required + protocol machines. */ + + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_SUCNF); + if (ret == 0) + { + /* pf_cmdev_cmsu_start_cnf_pos */ + pf_cmdev_state_ind (net, p_ar, PNET_EVENT_STARTUP); + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_PEIND); + /* pf_cmdev_rm_connect_rsp_pos handled by caller. */ + } + else + { + /* pf_cmdev_cmsu_start_cnf_neg */ + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_PDEV_ALREADY_OWNED); + pnet_create_log_book_entry ( + net, + p_ar->arep, + &p_stat->pnio_status, + (p_stat->add_data_1 << 16) + (p_stat->add_data_2)); + /* pf_cmdev_rm_connect_rsp_neg handled by caller. */ + } + } + } + else + { + if (p_ar->cmdev_state == PF_CMDEV_STATE_W_CRES) + { + pf_cmdev_state_ind (net, p_ar, PNET_EVENT_STARTUP); + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_DATA); + /* pf_cmdev_rm_connect_rsp_pos handled by caller. */ + ret = 0; + } + else + { + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_CIND); + /* pf_cmdev_rm_connect_rsp_neg handled by caller. */ + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Reset observers / configurable checks + * - Disable configurable checks / observers + * - Observers should remove active diagnostics + * from diagnostics ASE + * + * Todo: Only observers for submodules used within a + * certain AR should be reset. + * + * @param net InOut: The p-net stack instance + */ +static void pf_cmdev_reset_observers (pnet_t * net) +{ + pf_pdport_reset_all (net); +} + +/* ================================================ + * Remote primitives + */ + +int pf_cmdev_rm_connect_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_connect_result) +{ + int ret = -1; + uint16_t ix; + char station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated */ + const pnet_ethaddr_t * mac_address = pf_cmina_get_device_macaddr (net); + + /* RM_Connect.ind */ + if (pf_cmdev_check_apdu (net, p_ar, p_connect_result) == 0) + { + pf_cmdev_reset_observers (net); + pf_pdport_ar_connect_ind (net, p_ar); + + if (pf_cmdev_generate_submodule_diff (net, p_ar) == 0) + { + /* Start building the response to the connect request. */ + memcpy ( + p_ar->ar_result.cm_responder_mac_add.addr, + mac_address->addr, + sizeof (pnet_ethaddr_t)); + p_ar->ar_result.responder_udp_rt_port = PF_UDP_UNICAST_PORT; + + pf_cmdev_fix_frame_id (net, p_ar); + + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + p_ar->iocrs[ix].result.iocr_type = p_ar->iocrs[ix].param.iocr_type; + p_ar->iocrs[ix].result.iocr_reference = + p_ar->iocrs[ix].param.iocr_reference; + p_ar->iocrs[ix].result.frame_id = p_ar->iocrs[ix].param.frame_id; + } + p_ar->alarm_cr_result.alarm_cr_type = + p_ar->alarm_cr_request.alarm_cr_type; + p_ar->alarm_cr_result.remote_alarm_reference = + p_ar->alarm_cr_request.local_alarm_reference; + p_ar->alarm_cr_result.max_alarm_data_length = PF_MAX_ALARM_DATA_LEN; + + pf_cmina_get_station_name (net, station_name); + strncpy ( + p_ar->ar_server.cm_responder_station_name, + station_name, + sizeof (p_ar->ar_server.cm_responder_station_name)); + p_ar->ar_server.cm_responder_station_name + [sizeof (p_ar->ar_server.cm_responder_station_name) - 1] = '\0'; + p_ar->ar_server.length_cm_responder_station_name = + (uint16_t)strlen (station_name); + + p_ar->ready_4_data = false; + + ret = pf_fspm_cm_connect_ind (net, p_ar, p_connect_result); + } + } + + if (ret == 0) + { + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_CRES); + + ret = pf_cmdev_cm_connect_rsp_pos (net, p_ar, p_connect_result); + } + else + { + ret = pf_cmdev_cm_connect_rsp_neg (net, p_ar, p_connect_result); + } + + /* rm_connect.rsp handled by caller */ + return ret; +} + +int pf_cmdev_rm_release_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_release_result) +{ + int ret = -1; + + if (p_ar->ar_param.ar_properties.device_access == false) + { + switch (p_ar->cmdev_state) + { + case PF_CMDEV_STATE_W_PEIND: + case PF_CMDEV_STATE_W_ARDY: + case PF_CMDEV_STATE_W_ARDYCNF: + case PF_CMDEV_STATE_WDATA: + case PF_CMDEV_STATE_DATA: + p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_ar->err_code = PNET_ERROR_CODE_2_ABORT_AR_RELEASE_IND_RECEIVED; + if (pf_fspm_cm_release_ind (net, p_ar, p_release_result) != 0) + { + p_ar->err_code = p_release_result->pnio_status.error_code_2; + } + ret = pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_ABORT); + break; + default: + /* Ignore and stay in all other states. */ + ret = 0; + break; + } + } + else + { + if (p_ar->cmdev_state == PF_CMDEV_STATE_DATA) + { + p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_ar->err_code = PNET_ERROR_CODE_2_ABORT_AR_RELEASE_IND_RECEIVED; + ret = pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + } + else + { + /* Ignore */ + ret = 0; + } + } + + return ret; +} + +int pf_cmdev_rm_dcontrol_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pf_control_block_t * p_control_io, + pnet_result_t * p_dcontrol_result, + bool * p_set_state_prmend) +{ + int ret = -1; + + if (p_ar->ar_param.ar_properties.device_access == false) + { + switch (p_ar->cmdev_state) + { + case PF_CMDEV_STATE_W_PEIND: + if (p_control_io->control_command == BIT (PF_CONTROL_COMMAND_BIT_PRM_END)) + { + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_PERES); + if ( + pf_fspm_cm_dcontrol_ind ( + net, + p_ar, + PNET_CONTROL_COMMAND_PRM_END, + p_dcontrol_result) == 0) + { + pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_ARDY); + if (pf_cmdev_check_pdev (net) == 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Prepare to send event PNET_EVENT_PRMEND\n", + __LINE__); + *p_set_state_prmend = true; + /** + * The application must issue the applicationReady call + * _AFTER_ returning from the pf_fspm_cm_dcontrol_ind_cb() + * call-back. It _may_ be issued at the earliest from the + * state_cb(PNET_EVENT_PRMEND) call-back. + */ + ret = 0; + } + else + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): There is no local Ethernet port with high " + "enough speed.\n", + __LINE__); + p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_ar->err_code = PNET_ERROR_CODE_2_ABORT_PDEV_CHECK_FAILED; + } + } + } + break; + default: + /* Ignore and stay in all other states. */ + ret = 0; + break; + } + } + + if (ret != 0) + { + ret = pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + } + + return ret; +} + +int pf_cmdev_rm_ccontrol_cnf ( + pnet_t * net, + pf_ar_t * p_ar, + pf_control_block_t * p_control_io, + pnet_result_t * p_ccontrol_result) +{ + int ret = -1; + CC_ASSERT (p_ar != NULL); + + if (p_ar->cmdev_state == PF_CMDEV_STATE_W_ARDYCNF) + { + if ( + (p_ccontrol_result->pnio_status.error_code == + PNET_ERROR_CODE_NOERROR) && + (p_ccontrol_result->pnio_status.error_decode == + PNET_ERROR_DECODE_NOERROR) && + (p_ccontrol_result->pnio_status.error_code_1 == 0) && + (p_ccontrol_result->pnio_status.error_code_2 == 0)) + { + /* + * The command_control in the control_block only has the "Done" bit + * set. Assume that it is the confirmation to the appl_rdy + * ccontrol_req. + */ + if (p_control_io->control_command == BIT (PF_CONTROL_COMMAND_BIT_DONE)) + { + /* + * Alarm transmitter enabled on APPLRDY CNF. + * PN-AL-protocol (Mar20) Figure A.7 + */ + pf_alarm_enable (p_ar); + + if (p_ar->ready_4_data == true) + { + (void)pf_cmdev_state_ind (net, p_ar, PNET_EVENT_DATA); + ret = pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_DATA); + } + else + { + ret = pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_WDATA); + } + /* Send result to application */ + pf_fspm_ccontrol_cnf (net, p_ar, p_ccontrol_result); + } + else + { + LOG_ERROR (PNET_LOG, "CMDEV(%d): DONE bit not set\n", __LINE__); + } + } + else + { + pnet_create_log_book_entry ( + net, + p_ar->arep, + &p_ccontrol_result->pnio_status, + (p_ccontrol_result->add_data_1 << 16) + + (p_ccontrol_result->add_data_2)); + /* Send result to application */ + pf_fspm_ccontrol_cnf (net, p_ar, p_ccontrol_result); + (void)pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_ABORT); + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "CMDEV(%d): ccontrol cnf in state %s\n", + __LINE__, + pf_cmdev_state_to_string (p_ar->cmdev_state)); + } + + return ret; +} + +int pf_cmdev_cm_ccontrol_req (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = -1; + uint16_t ix; + uint16_t iy; + bool data_avail = true; /* Assume all OK */ + pf_iodata_object_t * p_iodata = NULL; + pf_ar_t * p_owning_ar = NULL; + pf_iocr_t * p_owning_iocr = NULL; + pf_iodata_object_t * p_owning_iodata = NULL; + uint32_t crep; + + if (p_ar->ar_param.ar_properties.device_access == false) + { + switch (p_ar->cmdev_state) + { + case PF_CMDEV_STATE_W_ARDY: + /* Verify that appl has created input data for all PPM */ + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + if ( + (p_ar->iocrs[ix].param.iocr_type == PF_IOCR_TYPE_INPUT) || + (p_ar->iocrs[ix].param.iocr_type == PF_IOCR_TYPE_MC_PROVIDER)) + { + for (iy = 0; iy < p_ar->iocrs[ix].nbr_data_desc; iy++) + { + p_iodata = &p_ar->iocrs[ix].data_desc[iy]; + + /* The application is limited to setting data for a specific + subslot. Check that it has tried to set the data. + If some other AR owns the subslot, check that data is + set for that AR. + + In the future we might implement reading inputdata from + one subslot to several connections (different PLCs). + + Find owning AR, CR and descriptor */ + if ( + pf_ppm_get_ar_iocr_desc ( + net, + p_iodata->api_id, + p_iodata->slot_nbr, + p_iodata->subslot_nbr, + &p_owning_ar, + &p_owning_iocr, + &p_owning_iodata, + &crep) == 0) + { + if ( + p_owning_ar != p_ar || + p_owning_iocr != &p_ar->iocrs[ix] || + p_owning_iodata != p_iodata) + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Slot %u subslot 0x%04x is owned by " + "AREP %u CREP %" PRIu32 " but AREP %u CREP %" PRIu32 + " is asking to use it.\n", + __LINE__, + p_iodata->slot_nbr, + p_iodata->subslot_nbr, + p_owning_ar->arep, + p_owning_iocr->crep, + p_ar->arep, + p_ar->iocrs[ix].crep); + } + + /* Member data_avail is set directly by the PPM. The value + is true also if only the IOPS is set. */ + if ( + p_owning_iodata->iops_length > 0 && + p_owning_iodata->data_avail == false) + { + data_avail = false; + } + } + else + { + LOG_DEBUG ( + PNET_LOG, + "CMDEV(%d): Could not find owning AR for slot %u " + "subslot 0x%04x\n", + __LINE__, + p_iodata->slot_nbr, + p_iodata->subslot_nbr); + data_avail = false; + } + } + } + } + + if (data_avail == true) + { + pf_cmdev_state_ind (net, p_ar, PNET_EVENT_APPLRDY); + if (pf_cmrpc_rm_ccontrol_req (net, p_ar) == 0) + { + ret = pf_cmdev_set_state (net, p_ar, PF_CMDEV_STATE_W_ARDYCNF); + } + } + + break; + default: + /* Ignore and stay in all other states. */ + ret = 0; + break; + } + } + else + { + (void)pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + } + + return ret; +} diff --git a/profinet_stack/p-net/src/device/pf_cmdev.h b/profinet_stack/p-net/src/device/pf_cmdev.h new file mode 100644 index 0000000..6545e06 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmdev.h @@ -0,0 +1,499 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMDEV_H +#define PF_CMDEV_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Do work on a device instance. + * @param p_dev In: The device instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +typedef int (*pf_ftn_device_t) (pf_device_t * p_dev); + +/** + * Do work on an API instance. + * @param p_api In: The API instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +typedef int (*pf_ftn_api_t) (pf_api_t * p_api); + +/** + * Do work on a slot instance. + * @param p_slot In: The slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +typedef int (*pf_ftn_slot_t) (pf_slot_t * p_slot); + +/** + * Do work on a sub-slot instance. + * @param p_subslot In: The sub-slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +typedef int (*pf_ftn_subslot_t) (pf_subslot_t * p_subslot); + +/** + * Initialize the cmdev component. + * @param net InOut: The p-net stack instance + */ +void pf_cmdev_init (pnet_t * net); + +/** + * Un-initialize the cmdev component. + * Delete all modules and sub-modules. + * @param net InOut: The p-net stack instance + */ +void pf_cmdev_exit (pnet_t * net); + +/** + * Traverse the device tree. + * @param net InOut: The p-net stack instance + * @param p_ftn_dev In: Call this function for the device. + * @param p_ftn_api In: Call this function for each api. + * @param p_ftn_slot In: Call this function for each slot. + * @param p_ftn_sub In: Call this function for each sub-slots. + * @return 0 If all provided functions return 0. + * -1 If any function called returns an error (!= 0).. + */ +int pf_cmdev_cfg_traverse ( + pnet_t * net, + pf_ftn_device_t p_ftn_dev, + pf_ftn_api_t p_ftn_api, + pf_ftn_slot_t p_ftn_slot, + pf_ftn_subslot_t p_ftn_sub); + +/** + * Plug a module into a slot. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param module_ident_nbr In: The module ident number. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_plug_module ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint32_t module_ident_nbr); + +/** + * Plug a sub-module into a sub-slot. + * Sends an alarm describing the result. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param module_ident_nbr In: The module ident number. + * @param submod_ident_nbr In: The sub-module ident number. + * @param direction In: Data direction(s). + * @param length_input In: Number of input bytes. + * @param length_output In: Number of output bytes. + * @param update In: true for update of sub-module. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_plug_submodule ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t module_ident_nbr, + uint32_t submod_ident_nbr, + pnet_submodule_dir_t direction, + uint16_t length_input, + uint16_t length_output, + bool update); + +/** + * Pull a sub-module from a sub-slot. + * Sends an alarm. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_pull_submodule ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr); + +/** + * Pull a module from a slot. + * First pulls all submodules (sends alarms). + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_pull_module (pnet_t * net, uint32_t api_id, uint16_t slot_nbr); + +/** + * Abort request from the application or from RPC. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance (or NULL). + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_cm_abort (pnet_t * net, pf_ar_t * p_ar); + +/** + * Get a pointer to the device configuration. + * @param net InOut: The p-net stack instance + * @param pp_device Out: The device configuration. + * @return 0 Always. + */ +int pf_cmdev_get_device (pnet_t * net, pf_device_t ** pp_device); + +/** + * Get an API instance by its API id. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param pp_api Out: The API instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_get_api (pnet_t * net, uint32_t api_id, pf_api_t ** pp_api); + +/** + * Get a sub-slot instance from the api ID, the slot number and the sub-slot + * number. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param pp_subslot Out: The sub-slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_get_subslot_full ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_subslot_t ** pp_subslot); + +/* Not used */ +/** + * Get a slot instance from the api ID and the slot number. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param pp_slot Out: The slot instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_get_slot_full ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + pf_slot_t ** pp_slot); + +/** + * Get identity for plugged module from the api ID and slot number. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param p_module_ident Out: Identity for plugged module. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_get_module_ident ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + uint32_t * p_module_ident); + +/** + * Get identity for plugged submodule from + * the api ID, slot and subslot number. + * @param net InOut: The p-net stack instance + * @param api_id In: The API identifier. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The subslot number. + * @param p_submodule_ident Out: Identity for plugged submodule. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_get_submodule_ident ( + pnet_t * net, + uint16_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t * p_submodule_ident); + +/** + * Get a diag item, from the array of items (by using index). + * + * @param net InOut: The p-net stack instance + * @param item_ix In: The diag item index. + * @param pp_item Out: The diag item. + * @return 0 If item_ix is valid. + * -1 If item_ix is invalid. + */ +int pf_cmdev_get_diag_item ( + pnet_t * net, + uint16_t item_ix, + pf_diag_item_t ** pp_item); + +/** + * Allocate a new diag item entry from the free list. + * The entry is also cleared. + * @param net InOut: The p-net stack instance + * @param p_item_ix Out: Index of the allocated item. + * @return 0 If an item was allocated. + * -1 If out of diag items. + */ +int pf_cmdev_new_diag (pnet_t * net, uint16_t * p_item_ix); + +/** + * Put a diag item back to the free list. + * @param net InOut: The p-net stack instance + * @param item_ix In: Index of the item to return. + */ +void pf_cmdev_free_diag (pnet_t * net, uint16_t item_ix); + +/** + * Find next diagnosis USI value (sorted) for a subslot + * + * Finds the smallest USI value that is greater than \a low_usi_limit + * + * @param net InOut: The p-net stack instance + * @param list_head In: Index of first item of diagnoses for + * this subslot + * @param low_usi_limit In: Previous USI value + * @param p_next_usi Out: Next USI value + * @return 0 if an USI value was found, -1 otherwise. + */ +int pf_cmdev_get_next_diagnosis_usi ( + pnet_t * net, + uint16_t list_head, + uint16_t low_usi_limit, + uint16_t * p_next_usi); + +/** + * Generate module diffs, when needed, for the specified AR. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_generate_submodule_diff (pnet_t * net, pf_ar_t * p_ar); + +/** + * Return a string representation of the specified CMDEV state. + * @param state In: The CMDEV state. + * @return A string representation of the CMDEV state. + */ +const char * pf_cmdev_state_to_string (pf_cmdev_state_values_t state); + +/** + * Return a string describing the submodule direction. + * @param direction In: submodule direction + * @return A string representation + */ +const char * pf_cmdev_submod_dir_to_string (pnet_submodule_dir_t direction); + +/** + * Return a string representation of the specified CMDEV event. + * @param event In: The CMDEV event. + * @return A string representation of the CMDEV event. + */ +const char * pf_cmdev_event_to_string (pnet_event_values_t event); + +/** + * Show CMDEV information of the AR. + * @param p_ar In: The AR instance. + */ +void pf_cmdev_ar_show (const pf_ar_t * p_ar); + +/** + * Show the plugged modules and sub-modules. + * @param net InOut: The p-net stack instance + */ +void pf_cmdev_device_show (pnet_t * net); + +/** + * Show diagnosis items. + * @param net InOut: The p-net stack instance + */ +void pf_cmdev_diag_show (const pnet_t * net); + +/** + * Send a new state transition event of the CMDEV component. + * + * Among other actions, it calls the \a pnet_state_ind() user callback. + * + * If the event is PNET_EVENT_ABORT, it might send an alarm frame. Make sure + * to set p_ar->err_cls and p_ar->err_code, in order to have the correct + * error values in the alarm frame. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param event In: The CMDEV state transition event. Use + * PNET_EVENT_xxx, not PF_CMDEV_STATE_xxx + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_state_ind (pnet_t * net, pf_ar_t * p_ar, pnet_event_values_t event); + +/** + * Get the CMDEV state of the specified AR. + * @param p_ar In: The AR instance. + * @param p_state Out: The CMDEV state. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_get_state (const pf_ar_t * p_ar, pf_cmdev_state_values_t * p_state); + +/** + * Handle CMIO "data_possibile" indications. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param data_possible In: true if data exchange is possible. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_cmio_info_ind (pnet_t * net, pf_ar_t * p_ar, bool data_possible); + +/* ====================================================================== + * Remote primitives + */ + +/** + * Handle an RPC connect request. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_connect_result Out: Detailed error information if return != 0; + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_rm_connect_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_connect_result); + +/** + * Handle an RPC release request. + * + * This triggers the user callbacks \a pnet_release_ind() and + * \a pnet_state_ind() with PNET_EVENT_ABORT. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_release_result Out: Detailed error information if return != 0; + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_rm_release_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_result_t * p_release_result); + +/** + * Handle an RPC dcontrol request. + * + * This triggers the user callbacks \a pnet_dcontrol_ind() and + * \a pnet_state_ind() with PNET_EVENT_PRMEND. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_control_io In: The control block. + * @param p_release_result Out: Detailed result of the connect operation. + * @param p_set_state_prmend Out: Trigger PNET_EVENT_PRMEND after the + * response have been sent. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_rm_dcontrol_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pf_control_block_t * p_control_io, + pnet_result_t * p_release_result, + bool * p_set_state_prmend); + +/** + * Convey the APPLRDY indication from the application to the controller. + * Ccontrol is a general mechanism, but it currently only has one purpose in the + * device. For this reason alone it does not have an opcode parameter. If this + * function does not see all PPM data areas are set (by the app) then it returns + * error and the application must try again - otherwise the startup will hang. + * + * Triggers the \a pnet_state_ind() user callback with PNET_EVENT_APPLRDY. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if a ccontrol request was sent. + * -1 if a ccontrol request was not sent. + */ +int pf_cmdev_cm_ccontrol_req (pnet_t * net, pf_ar_t * p_ar); + +/** + * Handle the confirmation of the ccontrol request. + * + * Triggers these user callbacks: + * * \a pnet_state_ind() with PNET_EVENT_DATA. + * * \a pnet_ccontrol_cnf() + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_control_io In: The control block. + * @param p_ccontrol_result InOut: Detailed error information if return != 0. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdev_rm_ccontrol_cnf ( + pnet_t * net, + pf_ar_t * p_ar, + pf_control_block_t * p_control_io, + pnet_result_t * p_ccontrol_result); + +/************ Internal functions, made available for unit testing ************/ + +int pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + pnet_submodule_dir_t submodule_dir, + pf_data_direction_values_t data_dir, + pf_dev_status_type_t status_type, + pf_data_direction_values_t * resulting_data_dir); + +int pf_cmdev_check_zero (uint8_t * p_start, uint16_t len); + +int pf_cmdev_check_visible_string (const char * s); + +int pf_cmdev_check_no_straddle ( + uint16_t start_1, + uint16_t length_1, + uint16_t start_2, + uint16_t length_2); + +int pf_cmdev_check_ar_type (uint16_t ar_type); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMDEV_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmdmc.c b/profinet_stack/p-net/src/device/pf_cmdmc.c new file mode 100644 index 0000000..190d692 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmdmc.c @@ -0,0 +1,35 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST + +#endif + +#include "pf_includes.h" + +int pf_cmdmc_activate_req (pf_ar_t * p_ar) +{ + return 0; +} + +int pf_cmdmc_close_req (pf_ar_t * p_ar) +{ + return 0; +} + +int pf_cmdmc_cpm_state_ind (pf_ar_t * p_ar, uint16_t ix, bool start) +{ + return 0; +} diff --git a/profinet_stack/p-net/src/device/pf_cmdmc.h b/profinet_stack/p-net/src/device/pf_cmdmc.h new file mode 100644 index 0000000..5c7feff --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmdmc.h @@ -0,0 +1,53 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMDMC_H +#define PF_CMDMC_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * ? + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdmc_activate_req (pf_ar_t * p_ar); + +/** + * ? + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdmc_close_req (pf_ar_t * p_ar); + +/** + * ? + * @param p_ar InOut: The AR instance. + * @param ix In: ? + * @param start In: ? + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmdmc_cpm_state_ind (pf_ar_t * p_ar, uint16_t ix, bool start); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMDMC_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmina.c b/profinet_stack/p-net/src/device/pf_cmina.c new file mode 100644 index 0000000..b287a4b --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmina.c @@ -0,0 +1,1780 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Context Management Ip and Name Assignment protocol + * machine (CMINA) + * + * Handles assignment of station name and IP address. + * Manages sending DCP hello requests. + * + * Resets the communication stack and user application. + * + * States are SETUP, SET_NAME, SET_IP and W_CONNECT. + * + * Implements setters and getters for net->cmina_current_dcp_ase, so other + * parts of the stack don't need to know these internal details. + * + */ + +#ifdef UNIT_TEST +#define pnal_get_port_statistics mock_pnal_get_port_statistics +#define pnal_set_ip_suite mock_pnal_set_ip_suite +#define pf_bg_worker_start_job mock_pf_bg_worker_start_job +#endif + +#include +#include +#include + +#include "pf_includes.h" + +/** + * @internal + * Trigger sending a DCP HELLO message. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * If this was not the last requested HELLO message then re-schedule another + * call after 1 s. + * + * @param net InOut: The p-net stack instance + * @param arg In: Not used. + * @param current_time In: Not used. + */ +static void pf_cmina_send_hello (pnet_t * net, void * arg, uint32_t current_time) +{ + if ( + (net->cmina_state == PF_CMINA_STATE_W_CONNECT) && + (net->cmina_hello_count > 0)) + { + (void)pf_dcp_hello_req (net); + + /* Reschedule */ + net->cmina_hello_count--; + (void)pf_scheduler_add ( + net, + PF_CMINA_FS_HELLO_INTERVAL * 1000, + pf_cmina_send_hello, + NULL, + &net->cmina_hello_timeout); + } + else + { + pf_scheduler_reset_handle (&net->cmina_hello_timeout); + net->cmina_hello_count = 0; + } +} + +void pf_cmina_save_ase (pnet_t * net, pf_cmina_dcp_ase_t * p_ase) +{ + pf_cmina_dcp_ase_t ase_nvm; + pf_cmina_dcp_ase_t temporary_buffer; + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char netmask_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char gateway_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + const char * p_file_directory = pf_cmina_get_file_directory (net); + int res = 0; + + /* This operation is called from the background worker. + * Ase data is protected by a mutex. + */ + os_mutex_lock (net->cmina_mutex); + ase_nvm = *p_ase; + os_mutex_unlock (net->cmina_mutex); + + pf_cmina_ip_to_string (ase_nvm.full_ip_suite.ip_suite.ip_addr, ip_string); + pf_cmina_ip_to_string ( + ase_nvm.full_ip_suite.ip_suite.ip_mask, + netmask_string); + pf_cmina_ip_to_string ( + ase_nvm.full_ip_suite.ip_suite.ip_gateway, + gateway_string); + + res = pf_file_save_if_modified ( + p_file_directory, + PF_FILENAME_IP, + p_ase, + &temporary_buffer, + sizeof (pf_cmina_dcp_ase_t)); + switch (res) + { + case 2: + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): First nvm saving of IP settings. " + "IP: %s Netmask: %s Gateway: %s Station name: \"%s\"\n", + __LINE__, + ip_string, + netmask_string, + gateway_string, + ase_nvm.station_name); + break; + case 1: + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Updating nvm stored IP settings. " + "IP: %s Netmask: %s Gateway: %s Station name: \"%s\"\n", + __LINE__, + ip_string, + netmask_string, + gateway_string, + ase_nvm.station_name); + break; + case 0: + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): No storing of nvm IP settings (no changes). " + "IP: %s Netmask: %s Gateway: %s Station name: \"%s\"\n", + __LINE__, + ip_string, + netmask_string, + gateway_string, + ase_nvm.station_name); + break; + default: + case -1: + LOG_ERROR ( + PF_DCP_LOG, + "CMINA(%d): Failed to store nvm IP settings.\n", + __LINE__); + break; + } +} + +int pf_cmina_set_default_cfg (pnet_t * net, uint16_t reset_mode) +{ + int ret = -1; + const pnet_cfg_t * p_cfg = NULL; + uint16_t ix; + bool reset_user_application = false; + pf_cmina_dcp_ase_t file_ase; + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char netmask_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char gateway_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + uint32_t ip = 0; + uint32_t netmask = 0; + uint32_t gateway = 0; + const char * p_file_directory = pf_cmina_get_file_directory (net); + + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Setting default configuration. Reset mode: %u\n", + __LINE__, + reset_mode); + + pf_fspm_get_default_cfg (net, &p_cfg); + if (p_cfg != NULL) + { + net->cmina_nonvolatile_dcp_ase.device_initiative = p_cfg->send_hello ? 1 + : 0; + net->cmina_nonvolatile_dcp_ase.device_role = 1; /* Means: PNIO Device */ + + memcpy ( + net->cmina_nonvolatile_dcp_ase.mac_address.addr, + net->pf_interface.main_port.mac_address.addr, + sizeof (pnet_ethaddr_t)); + + strcpy (net->cmina_nonvolatile_dcp_ase.port_name, ""); /* Terminated */ + + net->cmina_nonvolatile_dcp_ase.device_id = p_cfg->device_id; + net->cmina_nonvolatile_dcp_ase.oem_device_id = p_cfg->oem_device_id; + + net->cmina_nonvolatile_dcp_ase.dhcp_enable = + p_cfg->if_cfg.ip_cfg.dhcp_enable; + if (reset_mode == 0) /* Power-on reset */ + { + /* Read from file (nvm) */ + if ( + pf_file_load ( + p_file_directory, + PF_FILENAME_IP, + &file_ase, + sizeof (file_ase)) == 0) + { + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Did read IP parameters from nvm\n", + __LINE__); + ip = file_ase.full_ip_suite.ip_suite.ip_addr; + netmask = file_ase.full_ip_suite.ip_suite.ip_mask; + gateway = file_ase.full_ip_suite.ip_suite.ip_gateway; + memcpy ( + net->cmina_nonvolatile_dcp_ase.station_name, + file_ase.station_name, + sizeof (net->cmina_nonvolatile_dcp_ase.station_name)); + net->cmina_nonvolatile_dcp_ase.station_name + [sizeof (net->cmina_nonvolatile_dcp_ase.station_name) - 1] = + '\0'; + } + else + { + /* Use values for IP and station name from user configuration */ + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Could not yet read IP parameters from nvm. Use " + "values from user configuration.\n", + __LINE__); + PNAL_IP4_ADDR_TO_U32 ( + ip, + p_cfg->if_cfg.ip_cfg.ip_addr.a, + p_cfg->if_cfg.ip_cfg.ip_addr.b, + p_cfg->if_cfg.ip_cfg.ip_addr.c, + p_cfg->if_cfg.ip_cfg.ip_addr.d); + PNAL_IP4_ADDR_TO_U32 ( + netmask, + p_cfg->if_cfg.ip_cfg.ip_mask.a, + p_cfg->if_cfg.ip_cfg.ip_mask.b, + p_cfg->if_cfg.ip_cfg.ip_mask.c, + p_cfg->if_cfg.ip_cfg.ip_mask.d); + PNAL_IP4_ADDR_TO_U32 ( + gateway, + p_cfg->if_cfg.ip_cfg.ip_gateway.a, + p_cfg->if_cfg.ip_cfg.ip_gateway.b, + p_cfg->if_cfg.ip_cfg.ip_gateway.c, + p_cfg->if_cfg.ip_cfg.ip_gateway.d); + memcpy ( + net->cmina_nonvolatile_dcp_ase.station_name, + p_cfg->station_name, + sizeof (net->cmina_nonvolatile_dcp_ase.station_name)); + net->cmina_nonvolatile_dcp_ase.station_name + [sizeof (net->cmina_nonvolatile_dcp_ase.station_name) - 1] = + '\0'; + } + + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_addr = ip; + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_mask = + netmask; + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_gateway = + gateway; + + pf_cmina_ip_to_string (ip, ip_string); + pf_cmina_ip_to_string (netmask, netmask_string); + pf_cmina_ip_to_string (gateway, gateway_string); + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Using " + "IP: %s Netmask: %s Gateway: %s Station name: \"%s\"\n", + __LINE__, + ip_string, + netmask_string, + gateway_string, + net->cmina_nonvolatile_dcp_ase.station_name); + } + + if (reset_mode == 1 || reset_mode >= 3) + { + /* Reset application parameters */ + reset_user_application = true; + + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Reset application parameters.\n", + __LINE__); + + /* Reset I&M data */ + (void)pf_fspm_clear_im_data (net); + +#if PNET_OPTION_SNMP + /* According to section 8.4 "Behavior to ResetToFactory" in + * "Test case specification: Behavior" the MIB data should be reset + * in all supported reset modes. This seems to contradict + * PN-AL-Services ch. 6.3.11.3.2. + */ + pf_snmp_data_clear (net); +#endif + } + + if (reset_mode == 2 || reset_mode >= 3) + { + /* Reset communication parameters */ + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Reset communication parameters.\n", + __LINE__); + + /* Reset IP suite */ + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_addr = 0; + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_mask = 0; + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_gateway = 0; + + /* Clear name of station */ + memset ( + net->cmina_nonvolatile_dcp_ase.station_name, + 0, + sizeof (net->cmina_nonvolatile_dcp_ase.station_name)); + + pf_file_clear (p_file_directory, PF_FILENAME_IP); + pf_file_clear (p_file_directory, PF_FILENAME_DIAGNOSTICS); +#if PNET_OPTION_SNMP + pf_snmp_data_clear (net); +#endif + pf_pdport_reset_all (net); + } + + if (reset_mode > 0) + { + /* User callback */ + pf_fspm_reset_ind (net, reset_user_application, reset_mode); + } + + net->cmina_nonvolatile_dcp_ase.standard_gw_value = 0; /* Means: OwnIP is + treated as no + gateway */ + net->cmina_nonvolatile_dcp_ase.instance_id.high = 0; + net->cmina_nonvolatile_dcp_ase.instance_id.low = 0; + + strncpy ( + net->cmina_nonvolatile_dcp_ase.product_name, + p_cfg->product_name, + sizeof (net->cmina_nonvolatile_dcp_ase.product_name)); + net->cmina_nonvolatile_dcp_ase + .product_name[sizeof (net->cmina_nonvolatile_dcp_ase.product_name) - 1] = + '\0'; + /* Remove trailing spaces */ + ix = (uint16_t)strlen (net->cmina_nonvolatile_dcp_ase.product_name); + while ((ix > 1) && + (net->cmina_nonvolatile_dcp_ase.product_name[ix] == ' ')) + { + ix--; + } + net->cmina_nonvolatile_dcp_ase.product_name[ix] = '\0'; + + (void)pf_bg_worker_start_job (net, PF_BGJOB_SAVE_ASE_NVM_DATA); + + /* Init the current communication values */ + net->cmina_current_dcp_ase = net->cmina_nonvolatile_dcp_ase; + + ret = 0; + } + + return ret; +} + +void pf_cmina_dcp_set_commit (pnet_t * net) +{ + int res = 0; + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char netmask_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char gateway_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + bool permanent = true; + + if (net->cmina_commit_ip_suite == false) + { + LOG_INFO ( + PF_DCP_LOG, + "CMINA(%d): Did not set IP address. Is there a connection to the PLC, " + "or is the station name not set?\n", + __LINE__); + + return; + } + + if ( + memcmp ( + &net->cmina_current_dcp_ase.full_ip_suite.ip_suite, + &net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite, + sizeof (pf_ip_suite_t)) != 0) + { + permanent = false; + } + else if ( + strcmp ( + net->cmina_current_dcp_ase.station_name, + net->cmina_nonvolatile_dcp_ase.station_name) != 0) + { + permanent = false; + } + + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr, + ip_string); + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask, + netmask_string); + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway, + gateway_string); + LOG_INFO ( + PF_DCP_LOG, + "CMINA(%d): Setting IP: %s Netmask: %s Gateway: %s Station name: " + "\"%s\" " + "Permanent: %u\n", + __LINE__, + ip_string, + netmask_string, + gateway_string, + net->cmina_current_dcp_ase.station_name, + permanent); + + net->cmina_commit_ip_suite = false; + res = pnal_set_ip_suite ( + net->pf_interface.main_port.name, + &net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr, + &net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask, + &net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway, + net->cmina_current_dcp_ase.station_name, + permanent); + + if (res != 0) + { + LOG_ERROR ( + PF_DCP_LOG, + "CMINA(%d): Failed to set network parameters\n", + __LINE__); + } +} + +int pf_cmina_init (pnet_t * net) +{ + int ret = 0; + + net->cmina_mutex = os_mutex_create(); + net->cmina_hello_count = 0; + pf_scheduler_init_handle (&net->cmina_hello_timeout, "hello"); + net->cmina_error_decode = 0; + net->cmina_error_code_1 = 0; + net->cmina_commit_ip_suite = false; + net->cmina_state = PF_CMINA_STATE_SETUP; + + /* Collect the DCP ASE database */ + memset ( + &net->cmina_nonvolatile_dcp_ase, + 0, + sizeof (net->cmina_nonvolatile_dcp_ase)); + (void)pf_cmina_set_default_cfg (net, 0); /* Populates + net->cmina_current_dcp_ase and + net->cmina_nonvolatile_dcp_ase */ + + if (strlen (net->cmina_current_dcp_ase.station_name) == 0) + { + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): No station name available. Will not send any HELLO " + "messages.\n", + __LINE__); + + if (net->cmina_current_dcp_ase.dhcp_enable == true) + { + /* Case 4 in Profinet 2.4 Table 1096 */ + /* No station name, DHCP enabled */ + + /* ToDo: Send DHCP discover request */ + } + else if ( + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr == 0) && + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask == 0) && + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway == 0)) + { + /* Case 3 in Profinet 2.4 Table 1096 */ + /* No station name, no DHCP enabled, no IP address */ + /* Ignore - Expect IP to be set by DCP later */ + net->cmina_commit_ip_suite = true; + } + else + { + /* Case 3 in Profinet 2.4 Table 1096 */ + /* No station name, no DHCP enabled, has IP address */ + /* Start IP */ + net->cmina_commit_ip_suite = true; + } + + net->cmina_state = PF_CMINA_STATE_SET_NAME; + } + else + { + if ( + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr == 0) && + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask == 0) && + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway == 0)) + { + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): No IP address available. Will not send any HELLO " + "messages.\n", + __LINE__); + + if (net->cmina_current_dcp_ase.dhcp_enable == true) + { + /* Case 4 in Profinet 2.4 Table 1096 */ + /* Has station name, DHCP enabled, no IP address */ + + /* ToDo: Send DHCP discover request */ + } + else + { + /* Case 3 in Profinet 2.4 Table 1096 */ + /* Has station name, DHCP not enabled, no IP address */ + /* Ignore - Expect station name and IP to be set by DCP later */ + net->cmina_commit_ip_suite = true; + } + + net->cmina_state = PF_CMINA_STATE_SET_IP; + } + else + { + /* Case 2 in Profinet 2.4 Table 1096 */ + /* Has station name, has IP address */ + /* Start IP */ + net->cmina_commit_ip_suite = true; + + if (net->cmina_current_dcp_ase.device_initiative == 1) + { + net->cmina_hello_count = PF_CMINA_FS_HELLO_RETRY; + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Start sending %u HELLO messages.\n", + __LINE__, + net->cmina_hello_count); + + /* Send first HELLO now! */ + (void)pf_scheduler_add ( + net, + 0, + pf_cmina_send_hello, + NULL, + &net->cmina_hello_timeout); + } + else + { + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): Sending of HELLO messages is disabled in " + "configuration.\n", + __LINE__); + } + + net->cmina_state = PF_CMINA_STATE_W_CONNECT; + } + } + + /* Change IP address if necessary */ + pf_cmina_dcp_set_commit (net); + + return ret; +} + +/** + * Abort active ARs with error code + * @param net InOut: The p-net stack instance + * @param err_code In: Reason ARs are aborted + * return Number of aborted ARs + */ +static int pf_cmina_abort_active_ars (pnet_t * net, uint8_t err_code) +{ + int ix = 0; + pf_ar_t * p_ar = NULL; + int nbr_of_aborted_ars = 0; + + for (ix = 0; ix < PNET_MAX_AR; ix++) + { + p_ar = pf_ar_find_by_index (net, ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_ar->err_code = err_code; + pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + nbr_of_aborted_ars++; + } + } + return nbr_of_aborted_ars; +} + +/** + * Count number of active ARs + * @param net InOut: The p-net stack instance + * return Number of active ARs + */ +int pf_cmina_nbr_of_active_ars (pnet_t * net) +{ + int ix = 0; + pf_ar_t * p_ar = NULL; + int nbr_of_active_ars = 0; + + for (ix = 0; ix < PNET_MAX_AR; ix++) + { + p_ar = pf_ar_find_by_index (net, ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + nbr_of_active_ars++; + } + } + return nbr_of_active_ars; +} + +int pf_cmina_dcp_set_ind ( + pnet_t * net, + uint8_t opt, + uint8_t sub, + uint16_t block_qualifier, + uint16_t value_length, + const uint8_t * p_value, + uint8_t * p_block_error) +{ + int ret = -1; + bool have_ip = false; /* cmina_current_dcp_ase has IP address after this call + */ + bool have_name = false; /* cmina_current_dcp_ase has station name after this + call */ + bool have_dhcp = false; /* Not yet supported */ + bool change_ip = false; /* We have got a request to change IP address */ + bool change_name = false; /* We have got a request to change station name */ + bool change_dhcp = false; /* We have got a request to change DHCP settings */ + bool reset_to_factory = false; /* We have got a request to do a factory reset + */ + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char netmask_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + char gateway_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + + bool temp = ((block_qualifier & 1) == 0); + uint16_t reset_mode = 0; + + /* Stop sending Hello packets */ + pf_scheduler_remove_if_running (net, &net->cmina_hello_timeout); + + /* Parse incoming DCP SET, without caring about actual CMINA state. + Update cmina_current_dcp_ase and cmina_nonvolatile_dcp_ase*/ + switch (opt) + { + case PF_DCP_OPT_IP: + switch (sub) + { + case PF_DCP_SUB_IP_PAR: + if ( + value_length == + sizeof (net->cmina_current_dcp_ase.full_ip_suite.ip_suite)) + { + if (pf_cmina_is_ipsuite_valid ((pf_ip_suite_t *)p_value)) + { + /* Case 13 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Set IP address etc */ + change_ip = + (memcmp ( + &net->cmina_current_dcp_ase.full_ip_suite.ip_suite, + p_value, + value_length) != 0); + memcpy ( + &net->cmina_current_dcp_ase.full_ip_suite.ip_suite, + p_value, + sizeof (net->cmina_current_dcp_ase.full_ip_suite.ip_suite)); + + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr, + ip_string); + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask, + netmask_string); + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway, + gateway_string); + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): The incoming set request is about changing IP " + "(not DNS). IP: %s Netmask: %s Gateway: %s Temporary: %u\n", + __LINE__, + ip_string, + netmask_string, + gateway_string, + temp); + + if (temp == true) + { + memset ( + &net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite, + 0, + sizeof (pf_ip_suite_t)); + } + else + { + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite = + net->cmina_current_dcp_ase.full_ip_suite.ip_suite; + } + ret = 0; + } + else + { + /* Case 10, 22 in Profinet 2.4 Table 1096 "CMINA state table" */ + LOG_INFO ( + PF_DCP_LOG, + "CMINA(%d): Got incoming request to change IP suite to an " + "illegal value.\n", + __LINE__); + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SET; + } + } + else + { + /* Case 12, 23 in Profinet 2.4 Table 1096 "CMINA state table" */ + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SET; + } + break; + case PF_DCP_SUB_IP_SUITE: + if ( + value_length < + sizeof (net->cmina_current_dcp_ase.full_ip_suite)) /* Why less than? + */ + { + if (pf_cmina_is_full_ipsuite_valid ((pf_full_ip_suite_t *)p_value)) + { + /* Case 13 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Set IP address and DNS etc */ + change_ip = + (memcmp ( + &net->cmina_current_dcp_ase.full_ip_suite, + p_value, + value_length) != 0); + memcpy ( + &net->cmina_current_dcp_ase.full_ip_suite, + p_value, + value_length); + + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr, + ip_string); + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask, + netmask_string); + pf_cmina_ip_to_string ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway, + gateway_string); + LOG_DEBUG ( + PF_DCP_LOG, + "CMINA(%d): The incoming set request is about changing IP " + "and DNS. IP: %s Netmask: %s Gateway: %s Temporary: %u\n", + __LINE__, + ip_string, + netmask_string, + gateway_string, + temp); + + if (temp == true) + { + memset ( + &net->cmina_nonvolatile_dcp_ase.full_ip_suite, + 0, + sizeof (pf_full_ip_suite_t)); + } + else + { + net->cmina_nonvolatile_dcp_ase.full_ip_suite = + net->cmina_current_dcp_ase.full_ip_suite; + } + ret = 0; + } + else + { + /* Case 10,22 in Profinet 2.4 Table 1096 "CMINA state table" */ + LOG_INFO ( + PF_DCP_LOG, + "CMINA(%d): Got incoming request to change full IP suite to " + "an illegal value.\n", + __LINE__); + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SET; + } + } + else + { + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SET; + } + break; + default: + /* Case 12, 23 in Profinet 2.4 Table 1096 "CMINA state table" */ + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + break; + } + break; + case PF_DCP_OPT_DEVICE_PROPERTIES: + switch (sub) + { + case PF_DCP_SUB_DEV_PROP_NAME: + if (value_length < sizeof (net->cmina_current_dcp_ase.station_name)) + { + if (pf_cmina_is_stationname_valid ((char *)p_value, value_length)) + { + /* Case 13, 27 in Profinet 2.4 Table "CMINA state table" */ + /* Set station name */ + change_name = + ((strncmp ( + net->cmina_current_dcp_ase.station_name, + (char *)p_value, + value_length) != 0) || + (strlen (net->cmina_current_dcp_ase.station_name) != + value_length)); + + strncpy ( + net->cmina_current_dcp_ase.station_name, + (char *)p_value, + value_length); + net->cmina_current_dcp_ase.station_name[value_length] = '\0'; + LOG_INFO ( + PF_DCP_LOG, + "CMINA(%d): The incoming set request is about changing " + "station name. New name: \"%s\" Temporary: %u\n", + __LINE__, + net->cmina_current_dcp_ase.station_name, + temp); + + if (temp == true) + { + memset ( + net->cmina_nonvolatile_dcp_ase.station_name, + 0, + sizeof (net->cmina_nonvolatile_dcp_ase.station_name)); + } + else + { + strcpy ( + net->cmina_nonvolatile_dcp_ase.station_name, + net->cmina_current_dcp_ase.station_name); /* It always + fits */ + } + ret = 0; + } + else + { + /* Case 10, 22 in Profinet 2.4 Table 1096 "CMINA state table"*/ + LOG_INFO ( + PF_DCP_LOG, + "CMINA(%d): Got incoming request to change station name to " + "an illegal value.\n", + __LINE__); + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SET; + } + } + else + { + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SET; + } + break; + default: + /* Case 12, 23 in Profinet 2.4 Table 1096 "CMINA state table" */ + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + break; + } + break; + case PF_DCP_OPT_DHCP: + LOG_INFO (PF_DCP_LOG, "CMINA(%d): Trying to set DHCP\n", __LINE__); + *p_block_error = PF_DCP_BLOCK_ERROR_OPTION_NOT_SUPPORTED; + change_dhcp = true; + break; + case PF_DCP_OPT_CONTROL: + if (sub == PF_DCP_SUB_CONTROL_FACTORY_RESET) + { + /* Case 15, 30 in Profinet 2.4 Table 1096 "CMINA state table" */ + reset_to_factory = true; + reset_mode = 99; /* Reset all */ + + ret = 0; + } + else if (sub == PF_DCP_SUB_CONTROL_RESET_TO_FACTORY) + { + /* For additional information on reset modes see + * PN-AL-protocol (Mar20) Table 85 + * BlockQualifier with option ControlOption and suboption + * SuboptionResetToFactory. + * Note that these values does not match the + * wireshark decoding which includes bit 0. + */ + reset_mode = block_qualifier >> 1; + + /* Case 15, 30 in Profinet 2.4 Table 1096 "CMINA state table" */ + reset_to_factory = true; + + ret = 0; + } + break; + default: + /* Case 12, 23 in Profinet 2.4 Table 1096 "CMINA state table" */ + *p_block_error = PF_DCP_BLOCK_ERROR_OPTION_NOT_SUPPORTED; + break; + } + + /* Use parsed DCP SET request, depending on actual CMINA state */ + if (ret == 0) + { + /* Save to file */ + (void)pf_bg_worker_start_job (net, PF_BGJOB_SAVE_ASE_NVM_DATA); + + /* Evaluate what we have and where to go */ + have_name = (strlen (net->cmina_current_dcp_ase.station_name) > 0); + have_ip = + ((net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr != 0) || + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask != 0) || + (net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway != 0)); + have_dhcp = false; /* ToDo: Not supported here */ + + switch (net->cmina_state) + { + case PF_CMINA_STATE_SETUP: + /* Should not occur */ + net->cmina_error_decode = PNET_ERROR_DECODE_PNIORW; + net->cmina_error_code_1 = PNET_ERROR_CODE_1_ACC_STATE_CONFLICT; + ret = -1; + break; + case PF_CMINA_STATE_SET_NAME: + case PF_CMINA_STATE_SET_IP: + if ((change_name == true) || (change_ip == true)) + { + if ((have_name == true) && (have_ip == true)) + { + /* Case 5, 13 in Profinet 2.4 Table 1096"CMINA state table" */ + /* Change name or change IP */ + /* Stop DHCP timer */ + net->cmina_commit_ip_suite = true; + + net->cmina_state = PF_CMINA_STATE_W_CONNECT; + + ret = 0; + } + else + { + /* Stop IP */ + if (have_name == false) + { + /* Case 15 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Reset name */ + net->cmina_state = PF_CMINA_STATE_SET_NAME; + } + else if (have_ip == false) + { + /* Case 15 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Reset IP */ + net->cmina_state = PF_CMINA_STATE_SET_IP; + } + } + } + else if (change_dhcp == true) + { + if (have_dhcp == true) + { + /* Case 14 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Change DHCP */ + memset ( + &net->cmina_current_dcp_ase.full_ip_suite.ip_suite, + 0, + sizeof (net->cmina_current_dcp_ase.full_ip_suite.ip_suite)); + if (temp == false) + { + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite = + net->cmina_current_dcp_ase.full_ip_suite.ip_suite; + } + /* dhcp_discover_req */ + net->cmina_state = PF_CMINA_STATE_SET_IP; + } + else + { + /* Case 14 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Reset DHCP */ + /* Stop DHCP */ + } + } + break; + case PF_CMINA_STATE_W_CONNECT: + if ( + (change_name == false) && (change_ip == false) && + (reset_to_factory == false)) + { + /* Case 24 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* No change of name or IP. All OK */ + } + else if ((have_name == false) || (reset_to_factory == true)) + { + int aborted_ars = 0; + + /* Case 27, 30 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Reset name or reset to factory */ + /* Abort active ARs */ + if (reset_to_factory == true) + { + aborted_ars = pf_cmina_abort_active_ars ( + net, + PNET_ERROR_CODE_2_ABORT_DCP_RESET_TO_FACTORY); + } + else + { + aborted_ars = pf_cmina_abort_active_ars ( + net, + PNET_ERROR_CODE_2_ABORT_DCP_STATION_NAME_CHANGED); + } + + if (aborted_ars <= 0) + { + /* 37 */ + /* Stop IP */ + } + } + else if ( + ((have_name == true) && (change_name == true)) || + (have_ip == false)) + { + int aborted_ars = 0; + /* Case 27, 28 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Change name or reset IP */ + /* Abort active ARs */ + aborted_ars = pf_cmina_abort_active_ars ( + net, + PNET_ERROR_CODE_2_ABORT_DCP_STATION_NAME_CHANGED); + + if (aborted_ars <= 0) + { + /* 39 */ + net->cmina_commit_ip_suite = true; + } + + net->cmina_state = PF_CMINA_STATE_SET_IP; + + ret = 0; + } + else if ((have_ip == true) && (change_ip == true)) + { + /* Change IP if no active connection*/ + + if (pf_cmina_nbr_of_active_ars (net) == 0) + { + /* Case 25 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Change IP, no active connection */ + net->cmina_commit_ip_suite = true; + + ret = 0; + } + else + { + /* Case 26 in Profinet 2.4 Table 1096 "CMINA state table" */ + /* Change IP, active connection */ + net->cmina_error_decode = PNET_ERROR_DECODE_PNIORW; + net->cmina_error_code_1 = + PNET_ERROR_CODE_1_ACC_INVALID_PARAMETER; + ret = -1; + } + } + else + { + /* 43 */ + net->cmina_error_decode = PNET_ERROR_DECODE_PNIORW; + net->cmina_error_code_1 = PNET_ERROR_CODE_1_ACC_INVALID_PARAMETER; + ret = -1; + } + break; + } + } + + /* + * Case 5 in Profinet 2.4 Table 1096 "CMINA state table" Do_Check + */ + if ( + ((net->cmina_state == PF_CMINA_STATE_SET_IP) || + (net->cmina_state == PF_CMINA_STATE_SET_NAME)) && + (have_name == true) && (have_ip == true)) + { + net->cmina_state = PF_CMINA_STATE_W_CONNECT; + } + + if (reset_to_factory == true) + { + /* Handle reset to factory here */ + /* Case 15 in Profinet 2.4 Table 1096 "CMINA state table" */ + ret = pf_cmina_set_default_cfg (net, reset_mode); + } + + return ret; +} + +int pf_cmina_dcp_get_req ( + pnet_t * net, + uint8_t opt, + uint8_t sub, + uint16_t * p_value_length, + uint8_t ** pp_value, + uint8_t * p_block_error) +{ + int ret = 0; /* Assume all OK */ + + switch (opt) + { + case PF_DCP_OPT_ALL: + break; + case PF_DCP_OPT_IP: + switch (sub) + { + case PF_DCP_SUB_IP_MAC: + *p_value_length = sizeof (net->cmina_current_dcp_ase.mac_address); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.mac_address; + break; + case PF_DCP_SUB_IP_PAR: + *p_value_length = + sizeof (net->cmina_current_dcp_ase.full_ip_suite.ip_suite); + *pp_value = + (uint8_t *)&net->cmina_current_dcp_ase.full_ip_suite.ip_suite; + break; + case PF_DCP_SUB_IP_SUITE: + *p_value_length = sizeof (net->cmina_current_dcp_ase.full_ip_suite); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.full_ip_suite; + break; + default: + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + ret = -1; + break; + } + break; + case PF_DCP_OPT_DEVICE_PROPERTIES: + switch (sub) + { + case PF_DCP_SUB_DEV_PROP_VENDOR: + *p_value_length = sizeof (net->cmina_current_dcp_ase.product_name) - + 1; /* Skip terminator */ + *pp_value = (uint8_t *)net->cmina_current_dcp_ase.product_name; + break; + case PF_DCP_SUB_DEV_PROP_NAME: + *p_value_length = sizeof (net->cmina_current_dcp_ase.station_name); + *pp_value = (uint8_t *)net->cmina_current_dcp_ase.station_name; + break; + case PF_DCP_SUB_DEV_PROP_ID: + *p_value_length = sizeof (net->cmina_current_dcp_ase.device_id); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.device_id; + break; + case PF_DCP_SUB_DEV_PROP_ROLE: + *p_value_length = sizeof (net->cmina_current_dcp_ase.device_role); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.device_role; + break; + case PF_DCP_SUB_DEV_PROP_OPTIONS: + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + ret = -1; + break; + case PF_DCP_SUB_DEV_PROP_INSTANCE: + *p_value_length = sizeof (net->cmina_current_dcp_ase.instance_id); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.instance_id; + break; + case PF_DCP_SUB_DEV_PROP_OEM_ID: + *p_value_length = sizeof (net->cmina_current_dcp_ase.oem_device_id); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.oem_device_id; + break; + case PF_DCP_SUB_DEV_PROP_GATEWAY: + *p_value_length = + sizeof (net->cmina_current_dcp_ase.standard_gw_value); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.standard_gw_value; + break; + case PF_DCP_SUB_DEV_PROP_ALIAS: + default: + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + ret = -1; + break; + } + break; + case PF_DCP_OPT_DHCP: + /* ToDo: No support for DHCP yet, because it needs a way to get/set in + * lpiw. */ + switch (sub) + { + case PF_DCP_SUB_DHCP_HOSTNAME: + case PF_DCP_SUB_DHCP_VENDOR_SPEC: + case PF_DCP_SUB_DHCP_SERVER_ID: + case PF_DCP_SUB_DHCP_PAR_REQ_LIST: + case PF_DCP_SUB_DHCP_CLASS_ID: + case PF_DCP_SUB_DHCP_CLIENT_ID: + /* 61, 1+strlen(), 0, string */ + case PF_DCP_SUB_DHCP_FQDN: + case PF_DCP_SUB_DHCP_UUID_CLIENT_ID: + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + ret = -1; + break; + case PF_DCP_SUB_DHCP_CONTROL: + /* Can't get this */ + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + ret = -1; + break; + default: + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + ret = -1; + break; + } + break; + case PF_DCP_OPT_CONTROL: + /* Can't get control */ + *p_block_error = PF_DCP_BLOCK_ERROR_OPTION_NOT_SUPPORTED; + ret = -1; + break; + case PF_DCP_OPT_DEVICE_INITIATIVE: + switch (sub) + { + case PF_DCP_SUB_DEV_INITIATIVE_SUPPORT: + *p_value_length = + sizeof (net->cmina_current_dcp_ase.device_initiative); + *pp_value = (uint8_t *)&net->cmina_current_dcp_ase.device_initiative; + break; + default: + *p_block_error = PF_DCP_BLOCK_ERROR_SUBOPTION_NOT_SUPPORTED; + ret = -1; + break; + } + break; + default: + *p_block_error = PF_DCP_BLOCK_ERROR_OPTION_NOT_SUPPORTED; + ret = -1; + break; + } + + if (ret == 0) + { + *p_block_error = PF_DCP_BLOCK_ERROR_NO_ERROR; + } + + return ret; +} + +const char * pf_cmina_get_file_directory (const pnet_t * net) +{ + return net->fspm_cfg.file_directory; +} + +void pf_cmina_get_station_name (const pnet_t * net, char * station_name) +{ + snprintf ( + station_name, + PNET_STATION_NAME_MAX_SIZE, + "%s", + net->cmina_current_dcp_ase.station_name); +} + +pnal_ipaddr_t pf_cmina_get_ipaddr (const pnet_t * net) +{ + return net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr; +} + +pnal_ipaddr_t pf_cmina_get_netmask (const pnet_t * net) +{ + return net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask; +} + +pnal_ipaddr_t pf_cmina_get_gateway (const pnet_t * net) +{ + return net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway; +} + +const pnet_ethaddr_t * pf_cmina_get_device_macaddr (const pnet_t * net) +{ + return &net->pf_interface.main_port.mac_address; +} + +/************************* Utilities ******************************************/ + +int pf_cmina_remove_all_data_files (const char * file_directory) +{ + pf_file_clear (file_directory, PF_FILENAME_IM); + pf_file_clear (file_directory, PF_FILENAME_IP); + pf_file_clear (file_directory, PF_FILENAME_DIAGNOSTICS); +#if PNET_OPTION_SNMP + pf_snmp_remove_data_files (file_directory); +#endif + pf_pdport_remove_data_files (file_directory); + + return 0; +} + +bool pf_cmina_has_timed_out ( + uint32_t now_us, + uint32_t previous_us, + uint16_t period, + uint16_t factor) +{ + return (now_us - previous_us) >= ((uint32_t)(period * factor) * 1000 / 32); +} + +/*************** Diagnostic strings *****************************************/ + +void pf_cmina_ip_to_string (pnal_ipaddr_t ip, char * outputstring) +{ + snprintf ( + outputstring, + PNAL_INET_ADDRSTR_SIZE, + "%u.%u.%u.%u", + (uint8_t)((ip >> 24) & 0xFF), + (uint8_t)((ip >> 16) & 0xFF), + (uint8_t)((ip >> 8) & 0xFF), + (uint8_t)(ip & 0xFF)); +} + +/** + * @internal + * Return a string representation of the CMINA state. + * @param net InOut: The p-net stack instance + * @return a string representation of the CMINA state. + */ +static const char * pf_cmina_state_to_string (pnet_t * net) +{ + const char * s = ""; + switch (net->cmina_state) + { + case PF_CMINA_STATE_SETUP: + s = "PF_CMINA_STATE_SETUP"; + break; + case PF_CMINA_STATE_SET_NAME: + s = "PF_CMINA_STATE_SET_NAME"; + break; + case PF_CMINA_STATE_SET_IP: + s = "PF_CMINA_STATE_SET_IP"; + break; + case PF_CMINA_STATE_W_CONNECT: + s = "PF_CMINA_STATE_W_CONNECT"; + break; + } + + return s; +} + +/** + * @internal + * Print an IPv4 address (without newline) + * + * @param ip In: IP address + */ +void pf_ip_address_show (uint32_t ip) +{ + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + + pf_cmina_ip_to_string (ip, ip_string); + printf ("%s", ip_string); +} + +void pf_cmina_port_statistics_show (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + pnal_port_stats_t stats; + pf_port_t * p_port_data; + + if (pnal_get_port_statistics (net->pf_interface.main_port.name, &stats) == 0) + { + printf ( + "Main interface %10s In: %" PRIu32 " bytes %" PRIu32 + " errors %" PRIu32 " discards Out: %" PRIu32 " bytes %" PRIu32 + " errors %" PRIu32 " discards\n", + net->pf_interface.main_port.name, + stats.if_in_octets, + stats.if_in_errors, + stats.if_in_discards, + stats.if_out_octets, + stats.if_out_errors, + stats.if_out_discards); + } + else + { + printf ( + "Did not find main interface %s\n", + net->pf_interface.main_port.name); + } + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + + if (pnal_get_port_statistics (p_port_data->netif.name, &stats) == 0) + { + printf ( + "Port %10s In: %" PRIu32 " bytes %" PRIu32 + " errors %" PRIu32 " discards Out: %" PRIu32 " bytes %" PRIu32 + " errors %" PRIu32 " discards\n", + p_port_data->netif.name, + stats.if_in_octets, + stats.if_in_errors, + stats.if_in_discards, + stats.if_out_octets, + stats.if_out_errors, + stats.if_out_discards); + } + else + { + printf ( + "Did not find statistics for port %s\n", + p_port_data->netif.name); + } + + port = pf_port_get_next (&port_iterator); + } +} + +void pf_cmina_show (pnet_t * net) +{ + const pnet_cfg_t * p_cfg = NULL; + + pf_fspm_get_default_cfg (net, &p_cfg); + + printf ("CMINA\n"); + printf ( + "state : %s\n", + pf_cmina_state_to_string (net)); + printf ("Default station_name : \"%s\"\n", p_cfg->station_name); + printf ( + "Perm station_name : \"%s\"\n", + net->cmina_nonvolatile_dcp_ase.station_name); + printf ( + "Temp station_name : \"%s\"\n", + net->cmina_current_dcp_ase.station_name); + printf ("\n"); + + printf ("Default product_name : \"%s\"\n", p_cfg->product_name); + printf ( + "Perm product_name : \"%s\"\n", + net->cmina_nonvolatile_dcp_ase.product_name); + printf ( + "Temp product_name : \"%s\"\n", + net->cmina_current_dcp_ase.product_name); + printf ("\n"); + + printf ( + "Default IP Netmask Gateway : %u.%u.%u.%u ", + (unsigned)p_cfg->if_cfg.ip_cfg.ip_addr.a, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_addr.b, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_addr.c, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_addr.d); + printf ( + "%u.%u.%u.%u ", + (unsigned)p_cfg->if_cfg.ip_cfg.ip_mask.a, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_mask.b, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_mask.c, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_mask.d); + printf ( + "%u.%u.%u.%u\n", + (unsigned)p_cfg->if_cfg.ip_cfg.ip_gateway.a, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_gateway.b, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_gateway.c, + (unsigned)p_cfg->if_cfg.ip_cfg.ip_gateway.d); + + printf ("Perm IP Netmask Gateway : "); + pf_ip_address_show ( + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_addr); + printf (" "); + pf_ip_address_show ( + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_mask); + printf (" "); + pf_ip_address_show ( + net->cmina_nonvolatile_dcp_ase.full_ip_suite.ip_suite.ip_gateway); + + printf ("\nTemp IP Netmask Gateway : "); + pf_ip_address_show ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_addr); + printf (" "); + pf_ip_address_show ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_mask); + printf (" "); + pf_ip_address_show ( + net->cmina_current_dcp_ase.full_ip_suite.ip_suite.ip_gateway); + printf ("\n"); + + printf ( + "MAC : %02x:%02x:%02x:%02x:%02x:%02x\n\n", + net->pf_interface.main_port.mac_address.addr[0], + net->pf_interface.main_port.mac_address.addr[1], + net->pf_interface.main_port.mac_address.addr[2], + net->pf_interface.main_port.mac_address.addr[3], + net->pf_interface.main_port.mac_address.addr[4], + net->pf_interface.main_port.mac_address.addr[5]); + + pf_cmina_port_statistics_show (net); +} + +/************************* Validate incoming data ***************************/ + +/** + * @internal + * Check if a station name is valid + * + * Defined in Profinet 2.4, section 4.3.1.4.16.2: + * + * - 1 or more labels, separated by [.] + * - Total length is 1 to 240 + * - Label length is 1 to 63 + * - Labels consist of [a-z0-9-] + * - Labels do not start with [-] + * - Labels do not end with [-] + * - Labels do not use multiple concatenated [-] except for IETF RFC 5890 + * - The first label does not have the form "port-xyz" or "port-xyz-abcde" + * with a, b, c, d, e, x, y, z = 0...9 + * - Station-names do not have the form a.b.c.d with a, b, c, d = 0...999 + * + * Also empty station name is valid here (indicating that no station name has + * been set). + * + * @param station_name In: Station name + * @param len In: Length of incoming string without terminator. + * @return 0 if the name is valid + * -1 if the name is invalid + */ + +bool pf_cmina_is_stationname_valid (const char * station_name, uint16_t len) +{ + uint16_t i; + uint16_t label_offset = 0; + char prev = 'a'; + uint16_t n_labels = 1; + uint16_t n_non_digit = 0; + uint8_t c = 0; + + /* - Empty station name is valid here (indicating that no station name has + * been set) */ + if (len == 0) + { + return true; + } + + if (!station_name) + { + return false; + } + + /* + * - Labels do not start with [-] + * - Total length is 1 to 240 + */ + if ((station_name[0] == '-') || (len >= PNET_STATION_NAME_MAX_SIZE)) + { + return false; + } + + /* + * - The first label does not have the form "port-xyz" or "port-xyz-abcde" + * with a, b, c, d, e, x, y, z = 0...9 + */ + if (strncmp (station_name, "port-", 5) == 0) + { + if ((len == 8) || (len == 14)) + { + uint16_t n_char = 0; + for (i = 5; i < len; i++) + { + c = station_name[i]; + if ((!isdigit (c)) && !(c == '-')) + { + n_char++; + } + } + + if (n_char == 0) + { + return false; + } + } + } + + for (i = 0; i < len; i++) + { + c = station_name[i]; + if (c == '.') + { + n_labels++; + label_offset = 0; + + /* - Labels do not end with [-] */ + if (prev == '-') + { + return false; + } + } + /* - Labels consist of [a-z0-9-] */ + else if (!((islower (c) || isdigit (c) || (c == '-')))) + { + return false; + } + else + { + /* - Labels do not start with [-] */ + if ((label_offset == 0) && (c == '-')) + { + return false; + } + + /* - Label length is 1 to 63 */ + if (label_offset >= 63) + { + return false; + } + + if (!isdigit (c)) + { + n_non_digit++; + } + label_offset++; + } + prev = c; + } + + /* - Station-names do not have the form a.b.c.d with a, b, c, d = 0...999 */ + if ((n_labels == 4) && (n_non_digit == 0)) + { + return false; + } + + /* - Labels do not end with [-] */ + if (prev == '-') + { + return false; + } + return true; +} + +/** + * @internal + * Check if an IPv4 suite is valid. + * + * Defined in Profinet 2.4, section 4.3.1.4.21.5 + * + * @param ipsuite In: IP suite with IP, netmask, gateway + * @return 0 if the IP suite is valid + * -1 if the IP suite is invalid + */ +bool pf_cmina_is_ipsuite_valid (const pf_ip_suite_t * p_ipsuite) +{ + if (!pf_cmina_is_netmask_valid (p_ipsuite->ip_mask)) + { + return false; + } + if (!pf_cmina_is_ipaddress_valid (p_ipsuite->ip_mask, p_ipsuite->ip_addr)) + { + return false; + } + if (!pf_cmina_is_gateway_valid ( + p_ipsuite->ip_addr, + p_ipsuite->ip_mask, + p_ipsuite->ip_gateway)) + { + return false; + } + + return true; +} + +/** + * @internal + * Check if a full IPv4 suite is valid. + * + * Defined in Profinet 2.4, section + * + * @param full_ipsuite In: IP suite with IP, netmask, gateway and DNS + * addresses + * @return 0 if the IP suite is valid + * -1 if the IP suite is invalid + */ +bool pf_cmina_is_full_ipsuite_valid (const pf_full_ip_suite_t * p_full_ipsuite) +{ + if (!pf_cmina_is_ipsuite_valid (&p_full_ipsuite->ip_suite)) + { + return false; + } + + /* TODO validate DNS addresses */ + + return true; +} + +/** + * @internal + * Check if an IPv4 address is valid + * + * Defined in Profinet 2.4, section 4.3.1.4.21.2 + * + * @param netmask In: Netmask + * @param netmask In: IP address + * @return 0 if the IP address is valid + * -1 if the IP address is invalid + */ +bool pf_cmina_is_ipaddress_valid (pnal_ipaddr_t netmask, pnal_ipaddr_t ip) +{ + uint32_t host_part = ip & ~netmask; + + if ((netmask == 0) && (ip == 0)) + { + return true; + } + if (!pf_cmina_is_netmask_valid (netmask)) + { + return false; + } + if ((host_part == 0) || (host_part == ~netmask)) + { + return false; + } + if (ip <= PNAL_MAKEU32 (0, 255, 255, 255)) + { + return false; + } + else if ( + (ip >= PNAL_MAKEU32 (127, 0, 0, 0)) && + (ip <= PNAL_MAKEU32 (127, 255, 255, 255))) + { + return false; + } + else if ( + (ip >= PNAL_MAKEU32 (224, 0, 0, 0)) && + (ip <= PNAL_MAKEU32 (239, 255, 255, 255))) + { + return false; + } + else if ( + (ip >= PNAL_MAKEU32 (240, 0, 0, 0)) && + (ip <= PNAL_MAKEU32 (255, 255, 255, 255))) + { + return false; + } + return true; +} + +/** + * @internal + * Check if a netmask is valid + * + * Defined in Profinet 2.4, section 4.3.1.4.21.3 + * + * @param netmask In: netmask + * @return 0 if the netmask is valid + * -1 if the netmask is invalid + */ +bool pf_cmina_is_netmask_valid (pnal_ipaddr_t netmask) +{ + if (!(netmask & (~netmask >> 1))) + { + return true; + } + else + { + return false; + } +} + +/** + * @internal + * Check if a gateway address is valid + * + * Defined in Profinet 2.4, section 4.3.1.4.21.4 + * + * @param ip In: IP address + * @param netmask In: Netmask + * @param gateway In: Gateway address + * @return 0 if the gateway address is valid + * -1 if the gateway address is invalid + */ +bool pf_cmina_is_gateway_valid ( + pnal_ipaddr_t ip, + pnal_ipaddr_t netmask, + pnal_ipaddr_t gateway) +{ + if ((gateway != 0) && ((ip & netmask) != (gateway & netmask))) + { + return false; + } + return true; +} diff --git a/profinet_stack/p-net/src/device/pf_cmina.h b/profinet_stack/p-net/src/device/pf_cmina.h new file mode 100644 index 0000000..abbff91 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmina.h @@ -0,0 +1,249 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMINA_H +#define PF_CMINA_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the CMINA component. + * + * Sets the IP address if necessary. + * + * Schedules sending HELLO frames. + * + * @param net InOut: The p-net stack instance + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmina_init (pnet_t * net); + +/** + * Remove the stack's data files. + * + * @param file_directory In: File directory + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmina_remove_all_data_files (const char * file_directory); + +/** + * Show port statistics + * + * @param net InOut: The p-net stack instance + */ +void pf_cmina_port_statistics_show (pnet_t * net); + +/** + * Show the CMINA status. + * @param net InOut: The p-net stack instance + */ +void pf_cmina_show (pnet_t * net); + +/** + * Convert IPv4 address to string + * @param ip In: IP address + * @param outputstring Out: Buffer with resulting string. Should have + * size PNAL_INET_ADDRSTR_SIZE. + */ +void pf_cmina_ip_to_string (pnal_ipaddr_t ip, char * outputstring); + +/** + * Retrieve the path to the directory for saving files. + * @param net In: The p-net stack instance + * @return the absolute path to the file directory. + * Terminated string or NULL. + */ +const char * pf_cmina_get_file_directory (const pnet_t * net); + +/** + * Retrieve the current station name of the device. + * + * TODO: Not yet thread safe. Fix this, e.g. using a mutex. + * + * @param net In: The p-net stack instance + * @param station_name Out: Resulting string buffer with a terminated + * string. Should have size + * PNET_STATION_NAME_MAX_SIZE. + */ +void pf_cmina_get_station_name (const pnet_t * net, char * station_name); + +/** + * Retrieve the current IP address of the device. + * @param net In: The p-net stack instance + * @return the ip_address. + */ +pnal_ipaddr_t pf_cmina_get_ipaddr (const pnet_t * net); + +/** + * Retrieve the current netmask for the device. + * @param net In: The p-net stack instance + * @return the netmask + */ +pnal_ipaddr_t pf_cmina_get_netmask (const pnet_t * net); + +/** + * Retrieve the gateway address. + * @param net In: The p-net stack instance + * @return the gateway address. + */ +pnal_ipaddr_t pf_cmina_get_gateway (const pnet_t * net); + +/** + * Retrieve the device MAC address. + * @param net In: The p-net stack instance + * @return the MAC address + */ +const pnet_ethaddr_t * pf_cmina_get_device_macaddr (const pnet_t * net); + +/** + * Save one block of data for incoming DCP set command. + * + * Triggers the \a pnet_reset_ind() callback for some incoming DCP set commands. + * + * @param net InOut: The p-net stack instance + * @param opt In: The option key. + * @param sub In: The sub-option key. + * @param block_qualifier In: The block qualifier. + * @param value_length In: The length of the data to set. + * @param p_value In: The data to set. + * @param p_block_error Out: The block error code, if any. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmina_dcp_set_ind ( + pnet_t * net, + uint8_t opt, + uint8_t sub, + uint16_t block_qualifier, + uint16_t value_length, + const uint8_t * p_value, + uint8_t * p_block_error); + +/** + * Reset the configuration to default values. + * + * Triggers the application callback \a pnet_reset_ind() for some \a reset_mode + * values. + * + * Reset modes: + * + * 0: Power-on reset + * 1: Reset application parameters + * 2: Reset communication parameters + * 99: Reset application and communication parameters. + * + * Populates net->cmina_nonvolatile_dcp_ase from file (and/or from default + * configuration), and copies the resulting values to net->cmina_current_dcp_ase + * + * In order to actually change IP etc, the function pf_cmina_dcp_set_commit() + * must be called afterwards. + * + * @param net InOut: The p-net stack instance + * @param reset_mode In: Reset mode. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmina_set_default_cfg (pnet_t * net, uint16_t reset_mode); + +/** + * Commit changes to the IP-suite. + * + * This shall be done _after_ the answer to DCP set has been sent. + * @param net InOut: The p-net stack instance + */ +void pf_cmina_dcp_set_commit (pnet_t * net); + +/** + * Find data and its size for use in one block in response to a DCP get command. + * + * Also used in DCP Hello and DCP Identify. + * + * @param net InOut: The p-net stack instance + * @param opt In: The option key. + * @param sub In: The sub-option key. + * @param value_length In: The length of the pp_value buffer. + * Out: The length of the read data. + * @param pp_value Out: The retrieved data. + * @param p_block_error Out: The block error code, if any. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmina_dcp_get_req ( + pnet_t * net, + uint8_t opt, + uint8_t sub, + uint16_t * p_value_length, + uint8_t ** pp_value, + uint8_t * p_block_error); + +/** + * Check if a time interval has passed + * + * @param now_us In: Interval end in microseconds + * @param previous_us In: Interval start in microseconds + * @param period In: Time interval length in 1/32 of millisecond. + * For 1 millisecond use 32 + * 2 milliseconds use 64 etc + * @param factor In: Multiplication factor for the timeout. + * Use 1 if the given time interval length should + * be used, or 3 if we should return true after + * 3 time interval lengths etc + * @return true if the measured time interval is longer than period*factor + * false if it not has passed + */ +bool pf_cmina_has_timed_out ( + uint32_t now_us, + uint32_t previous_us, + uint16_t period, + uint16_t factor); + +/** + * Save the IP settings (and station name) to nonvolatile memory if necessary. + * This is a blocking call intended to be executed by the background worker. + * + * Compares with the content of already stored settings (in order not to + * wear out the flash chip) + * + * @param net InOut: The p-net stack instance + * @param p_ase In: Settings to be saved + */ +void pf_cmina_save_ase (pnet_t * net, pf_cmina_dcp_ase_t * p_ase); + +/************ Internal functions, made available for unit testing ************/ + +bool pf_cmina_is_stationname_valid (const char * station_name, uint16_t len); + +bool pf_cmina_is_netmask_valid (pnal_ipaddr_t netmask); + +bool pf_cmina_is_ipaddress_valid (pnal_ipaddr_t netmask, pnal_ipaddr_t ip); + +bool pf_cmina_is_gateway_valid ( + pnal_ipaddr_t ip, + pnal_ipaddr_t netmask, + pnal_ipaddr_t gateway); + +bool pf_cmina_is_ipsuite_valid (const pf_ip_suite_t * p_ipsuite); + +bool pf_cmina_is_full_ipsuite_valid (const pf_full_ip_suite_t * p_full_ipsuite); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMINA_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmio.c b/profinet_stack/p-net/src/device/pf_cmio.c new file mode 100644 index 0000000..494a169 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmio.c @@ -0,0 +1,381 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Context Management Input Output protocol machine (CMIO) + * + * This monitors several CPMs during startup, to know when there is incoming + * cyclic data. + * + * States are IDLE, STARTUP, WDATA and DATA. + * + * While waiting for data, it polls the CPMs every 100 ms to check if data has + * arrived, and tells CMDEV the result. + * + */ + +#ifdef UNIT_TEST + +#endif + +#include "pf_includes.h" + +#define PF_CMIO_TIMER_PERIOD (100 * 1000) /* us */ + +/*************** Diagnostic strings *****************************************/ + +/** + * @internal + * Return a string representation of the CMIO state. + * @param state In: The state. + * @return A string representation of the CMIO state. + */ +static const char * pf_cmio_state_to_string (pf_cmio_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_CMIO_STATE_IDLE: + s = "PF_CMIO_STATE_IDLE"; + break; + case PF_CMIO_STATE_STARTUP: + s = "PF_CMIO_STATE_STARTUP"; + break; + case PF_CMIO_STATE_WDATA: + s = "PF_CMIO_STATE_WDATA"; + break; + case PF_CMIO_STATE_DATA: + s = "PF_CMIO_STATE_DATA"; + break; + } + + return s; +} + +void pf_cmio_show (const pf_ar_t * p_ar) +{ + printf ( + "CMIO state = %s\n", + pf_cmio_state_to_string (p_ar->cmio_state)); + printf ( + " timer = %u\n", + (unsigned)pf_scheduler_get_value (&p_ar->cmio_timeout)); + printf ( + " timer should reschedule = %u\n", + p_ar->cmio_timer_should_reschedule); +} + +/****************************************************************************/ + +/** + * @internal + * @param p_ar InOut: The AR instance. + * @param state In: The new CMIO state. + */ +static void pf_cmio_set_state (pf_ar_t * p_ar, pf_cmio_state_values_t state) +{ + if (state != p_ar->cmio_state) + { + LOG_DEBUG ( + PNET_LOG, + "CMIO(%d): New state %s for AREP %u (was %s)\n", + __LINE__, + pf_cmio_state_to_string (state), + p_ar->arep, + pf_cmio_state_to_string (p_ar->cmio_state)); + p_ar->cmio_state = state; + } +} + +/** + * @internal + * Poll each output IOCR (CPM) if they have received data from the controller. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * When in state PF_CMIO_STATE_WDATA this function polls (every 100 ms) all CPM + * to see if data has arrived from the controller. + * If CPM data has arrived in state PF_CMIO_STATE_WDATA then notify CMDEV, else + * schedule another call in 100 ms. + * + * @param net InOut: The p-net stack instance + * @param arg In: The AR instance. pf_ar_t + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. Not used here. + */ +static void pf_cmio_timer_expired ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_ar_t * p_ar = (pf_ar_t *)arg; + uint16_t crep; + bool data_possible = true; + + if ( + (p_ar->cmio_state == PF_CMIO_STATE_WDATA) && + (p_ar->cmio_timer_should_reschedule == true)) + { + for (crep = 0; crep < p_ar->nbr_iocrs; crep++) + { + if ( + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_OUTPUT) || + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_MC_CONSUMER)) + { + if (p_ar->iocrs[crep].cpm.cmio_start == false) + { + data_possible = false; + } + } + } + + pf_cmdev_cmio_info_ind (net, p_ar, data_possible); + + (void)pf_scheduler_add ( + net, + PF_CMIO_TIMER_PERIOD, + pf_cmio_timer_expired, + p_ar, + &p_ar->cmio_timeout); + } + else + { + p_ar->cmio_timer_should_reschedule = false; + pf_scheduler_reset_handle (&p_ar->cmio_timeout); + } +} + +int pf_cmio_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event) +{ + uint16_t crep; + + /* TODO: Double-check the state transitions. + Also split the handling of creps into a separate function. */ + + LOG_DEBUG ( + PNET_LOG, + "CMIO(%d): Received event %s from CMDEV. Initial state %s for AREP %u.\n", + __LINE__, + pf_cmdev_event_to_string (event), + pf_cmio_state_to_string (p_ar->cmio_state), + p_ar->arep); + + switch (p_ar->cmio_state) + { + case PF_CMIO_STATE_IDLE: + if (event == PNET_EVENT_ABORT) + { + /* Ignore */ + p_ar->cmio_timer_should_reschedule = false; + + pf_cmio_set_state (p_ar, PF_CMIO_STATE_STARTUP); /* TODO validate */ + } + else if (event == PNET_EVENT_STARTUP) + { + /* + * CMDEV has received a connect request. + * Start supervision of the data flow. + */ + for (crep = 0; crep < p_ar->nbr_iocrs; crep++) + { + if ( + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_OUTPUT) || + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_MC_CONSUMER)) + { + p_ar->iocrs[crep].cpm.cmio_start = false; + } + } + p_ar->cmio_timer_should_reschedule = false; + + pf_cmio_set_state (p_ar, PF_CMIO_STATE_STARTUP); + } + break; + case PF_CMIO_STATE_STARTUP: + if (event == PNET_EVENT_ABORT) + { + /* Ignore */ + p_ar->cmio_timer_should_reschedule = false; + + pf_cmio_set_state (p_ar, PF_CMIO_STATE_IDLE); + } + else if (event == PNET_EVENT_STARTUP) /* TODO Validate */ + { + for (crep = 0; crep < p_ar->nbr_iocrs; crep++) + { + if ( + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_OUTPUT) || + (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_MC_CONSUMER)) + { + p_ar->iocrs[crep].cpm.cmio_start = false; + } + } + pf_cmio_set_state (p_ar, PF_CMIO_STATE_WDATA); + } + else if (event == PNET_EVENT_PRMEND) + { + /* StartTimer() */ + p_ar->cmio_timer_should_reschedule = true; + (void)pf_scheduler_add ( + net, + PF_CMIO_TIMER_PERIOD, + pf_cmio_timer_expired, + p_ar, + &p_ar->cmio_timeout); + + pf_cmio_set_state (p_ar, PF_CMIO_STATE_WDATA); + } + break; + case PF_CMIO_STATE_WDATA: + if (event == PNET_EVENT_ABORT) + { + p_ar->cmio_timer_should_reschedule = false; + + pf_cmio_set_state (p_ar, PF_CMIO_STATE_IDLE); + } + else if (event == PNET_EVENT_DATA) + { + /* StopTimer() */ + p_ar->cmio_timer_should_reschedule = false; + pf_scheduler_remove_if_running (net, &p_ar->cmio_timeout); + + pf_cmio_set_state (p_ar, PF_CMIO_STATE_DATA); + } + break; + case PF_CMIO_STATE_DATA: + if (event == PNET_EVENT_ABORT) + { + p_ar->cmio_timer_should_reschedule = false; + + pf_cmio_set_state (p_ar, PF_CMIO_STATE_IDLE); + } + break; + } + + return 0; +} + +int pf_cmio_cpm_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint16_t crep, + bool start) +{ + int ret = 0; + switch (p_ar->cmio_state) + { + case PF_CMIO_STATE_IDLE: + /* Ignore */ + break; + case PF_CMIO_STATE_STARTUP: + /* Trigger alarm if we use legacy startup mode + Incoming CPM_Stop.ind, which is "data hold timer expired", triggers + ABORT. See Table 546 "CMDEV state table" in Profinet protocol + ver 2.2 October 2007 */ + if (p_ar->ar_param.ar_properties.startup_mode == false && start == false) + { + LOG_INFO ( + PNET_LOG, + "CMIO(%d): CPM is stopping. CMIO state is PF_CMIO_STATE_STARTUP. " + "Aborting, as Legacy startup mode is used. crep: %u\n", + __LINE__, + crep); + (void)pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + } + break; + case PF_CMIO_STATE_WDATA: + if (crep < p_ar->nbr_iocrs) + { + if (start != p_ar->iocrs[crep].cpm.cmio_start) + { + LOG_INFO ( + PNET_LOG, + "CMIO(%d): CPM state change. CMIO state is WDATA. CREP: %u " + "AREP: %u CPM start=%s\n", + __LINE__, + crep, + p_ar->arep, + start ? "true" : "false"); + } + p_ar->iocrs[crep].cpm.cmio_start = start; + } + else + { + ret = -1; + } + break; + case PF_CMIO_STATE_DATA: + if (start == false) + { + LOG_INFO ( + PNET_LOG, + "CMIO(%d): CPM is stopping. CMIO state is DATA. Aborting. CREP: " + "%u AREP: %u\n", + __LINE__, + crep, + p_ar->arep); + + /* if (crep != crep.mcpm) not possible - handled elsewhere */ + (void)pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + } + break; + } + + return ret; +} + +int pf_cmio_cpm_new_data_ind (pf_ar_t * p_ar, uint16_t crep, bool new_data) +{ + int ret = 0; + + switch (p_ar->cmio_state) + { + case PF_CMIO_STATE_IDLE: + case PF_CMIO_STATE_STARTUP: + case PF_CMIO_STATE_DATA: + /* Ignore */ + break; + case PF_CMIO_STATE_WDATA: + if (crep < p_ar->nbr_iocrs) + { + if (new_data != p_ar->iocrs[crep].cpm.cmio_start) + { + LOG_DEBUG ( + PNET_LOG, + "CMIO(%d): New data ind from CPM. CREP: %u AREP: %u " + "new_data=%s\n", + __LINE__, + crep, + p_ar->arep, + new_data ? "true" : "false"); + } + + p_ar->iocrs[crep].cpm.cmio_start = new_data; + } + else + { + ret = -1; + } + break; + } + + return ret; +} diff --git a/profinet_stack/p-net/src/device/pf_cmio.h b/profinet_stack/p-net/src/device/pf_cmio.h new file mode 100644 index 0000000..780ad1c --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmio.h @@ -0,0 +1,73 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMIO_H +#define PF_CMIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Show CMIO information about an AR. + * @param p_ar In: The AR instance. + */ +void pf_cmio_show (const pf_ar_t * p_ar); + +/** + * Handle CMDEV events. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param event In: The new CMDEV state. Use PNET_EVENT_xxx, not + * PF_CMDEV_STATE_xxx + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmio_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event); + +/** + * Handle CPM start/stop events of a specific AR. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR instance. + * @param start In: Start or Stop information. True if CPM is + * starting. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmio_cpm_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint16_t crep, + bool start); + +/** + * Handle CPM new data events of a specific AR. + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR instance. + * @param new_data In: true => Data, false => NoData. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmio_cpm_new_data_ind (pf_ar_t * p_ar, uint16_t crep, bool new_data); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMIO_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmpbe.c b/profinet_stack/p-net/src/device/pf_cmpbe.c new file mode 100644 index 0000000..f007663 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmpbe.c @@ -0,0 +1,291 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Parameter Begin End Protocol Machine (PBE) + * + * Disables and enables alarm sending. + */ + +#ifdef UNIT_TEST + +#endif + +#include "pf_includes.h" + +/*************** Diagnostic strings *****************************************/ + +/** + * @internal + * Return a string representation of the CMPBE state. + * @param state In: The state. + * @return A string representation of the CMPBE state. + */ +static const char * pf_cmpbe_state_to_string (pf_cmpbe_state_values_t state) +{ + const char * s = ""; + switch (state) + { + case PF_CMPBE_STATE_IDLE: + s = "PF_CMPBE_STATE_IDLE"; + break; + case PF_CMPBE_STATE_WFIND: + s = "PF_CMPBE_STATE_WFIND"; + break; + case PF_CMPBE_STATE_WFRSP: + s = "PF_CMPBE_STATE_WFRSP"; + break; + case PF_CMPBE_STATE_WFPEI: + s = "PF_CMPBE_STATE_WFPEI"; + break; + case PF_CMPBE_STATE_WFPER: + s = "PF_CMPBE_STATE_WFPER"; + break; + case PF_CMPBE_STATE_WFREQ: + s = "PF_CMPBE_STATE_WFREQ"; + break; + case PF_CMPBE_STATE_WFCNF: + s = "PF_CMPBE_STATE_WFCNF"; + break; + } + + return s; +} + +void pf_cmpbe_show (const pf_ar_t * p_ar) +{ + const char * s = pf_cmpbe_state_to_string (p_ar->cmpbe_state); + + printf ("CMPBE state = %s\n", s); + printf (" stored command = %#x\n", p_ar->cmpbe_stored_command); +} + +/****************************************************************************/ + +/** + * @internal + * Handle state changes in the CMPBE component. + * @param p_ar InOut: The AR instance. + * @param state In: The new state. + */ +static void pf_cmpbe_set_state (pf_ar_t * p_ar, pf_cmpbe_state_values_t state) +{ + if (state != p_ar->cmpbe_state) + { + LOG_DEBUG ( + PNET_LOG, + "CMPBE(%d): New state %s for AREP %u (was %s)\n", + __LINE__, + pf_cmpbe_state_to_string (state), + p_ar->arep, + pf_cmpbe_state_to_string (p_ar->cmpbe_state)); + } + p_ar->cmpbe_state = state; +} + +int pf_cmpbe_cmdev_state_ind (pf_ar_t * p_ar, pnet_event_values_t event) +{ + LOG_DEBUG ( + PNET_LOG, + "CMPBE(%d): Received event %s from CMDEV. Initial state %s for AREP " + "%u.\n", + __LINE__, + pf_cmdev_event_to_string (event), + pf_cmpbe_state_to_string (p_ar->cmpbe_state), + p_ar->arep); + switch (p_ar->cmpbe_state) + { + case PF_CMPBE_STATE_IDLE: + if (event == PNET_EVENT_DATA) + { + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFIND); + } + else + { + /* Ignore and stay in state */ + } + break; + case PF_CMPBE_STATE_WFRSP: + case PF_CMPBE_STATE_WFIND: + case PF_CMPBE_STATE_WFPEI: + case PF_CMPBE_STATE_WFPER: + case PF_CMPBE_STATE_WFREQ: + case PF_CMPBE_STATE_WFCNF: + if (event == PNET_EVENT_ABORT) + { + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_IDLE); + } + break; + } + + return 0; +} + +int pf_cmpbe_rm_dcontrol_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pf_control_block_t * p_control_io, + pnet_result_t * p_result) +{ + int ret = -1; + uint16_t temp_control_command; + + LOG_DEBUG ( + PNET_LOG, + "CMPBE(%d): Received DControl command bitfield 0x%04x for AREP %u. " + "Initial state %s\n", + __LINE__, + p_control_io->control_command, + p_ar->arep, + pf_cmpbe_state_to_string (p_ar->cmpbe_state)); + switch (p_ar->cmpbe_state) + { + case PF_CMPBE_STATE_IDLE: + ret = 0; /* Ignore */ + break; + case PF_CMPBE_STATE_WFRSP: + /* Is virtual state - see below! */ + break; + case PF_CMPBE_STATE_WFIND: + if (p_ar->cmpbe_stored_command != 0) + { + /* Save */ + temp_control_command = p_control_io->control_command; + + /* Issue stored control command */ + p_control_io->control_command = p_ar->cmpbe_stored_command; + p_ar->cmpbe_stored_command = 0; + + /* Only stored command is PNET_CONTROL_COMMAND_PRM_BEGIN */ + if ( + pf_fspm_cm_dcontrol_ind ( + net, + p_ar, + PNET_CONTROL_COMMAND_PRM_BEGIN, + p_result) == 0) + { + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFRSP); + /* cm_dcontrol_rsp() */ + ret = 0; + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFPEI); + } + /* Restore */ + p_control_io->control_command = temp_control_command; + } + else if ( + p_control_io->control_command == + BIT (PF_CONTROL_COMMAND_BIT_PRM_BEGIN)) + { + pf_alarm_disable (p_ar); + ret = pf_fspm_cm_dcontrol_ind ( + net, + p_ar, + PNET_CONTROL_COMMAND_PRM_BEGIN, + p_result); + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFRSP); + } + break; + case PF_CMPBE_STATE_WFPEI: + if (p_control_io->control_command == BIT (PF_CONTROL_COMMAND_BIT_PRM_END)) + { + pf_fspm_cm_dcontrol_ind ( + net, + p_ar, + PNET_CONTROL_COMMAND_PRM_END, + p_result); + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFPER); + /* cm_dcontrol_rsp() */ + ret = 0; + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFREQ); + } + break; + case PF_CMPBE_STATE_WFPER: + if (p_control_io->control_command == BIT (PF_CONTROL_COMMAND_BIT_PRM_END)) + { + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFREQ); + ret = 0; + } + break; + case PF_CMPBE_STATE_WFREQ: + if (p_control_io->control_command == BIT (PF_CONTROL_COMMAND_BIT_PRM_BEGIN)) + { + /* Note: PrmEnd is handled by CMRPC as re-run. */ + (void)pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_IDLE); + } + break; + case PF_CMPBE_STATE_WFCNF: + if (p_control_io->control_command == BIT (PF_CONTROL_COMMAND_BIT_PRM_BEGIN)) + { + p_ar->cmpbe_stored_command = p_control_io->control_command; + } + break; + } + + return ret; +} + +int pf_cmpbe_rm_ccontrol_cnf ( + pf_ar_t * p_ar, + const pf_control_block_t * p_control_io, + pnet_result_t * p_result) +{ + int ret = -1; + + if (p_ar->cmpbe_state == PF_CMPBE_STATE_WFCNF) + { + /* + * The only expected bit is the DONE bit. + * Assume it is the confirmation to the APPL_RDY ccontrol_req. + */ + if (p_control_io->control_command == BIT (PF_CONTROL_COMMAND_BIT_DONE)) + { + pf_alarm_enable (p_ar); + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFIND); + ret = 0; + } + } + else + { + ret = 0; /* Ignore */ + } + + return ret; +} + +/* Not used */ +int pf_cmpbe_cm_ccontrol_req (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = -1; + + LOG_DEBUG ( + PNET_LOG, + "CMPBE(%d): ccontrol_req in state: %s\n", + __LINE__, + pf_cmpbe_state_to_string (p_ar->cmpbe_state)); + + if (p_ar->cmpbe_state == PF_CMPBE_STATE_WFREQ) + { + /* Control block is always APPLRDY here */ + if (pf_alarm_pending (p_ar) == false) + { + ret = pf_cmrpc_rm_ccontrol_req (net, p_ar); + pf_cmpbe_set_state (p_ar, PF_CMPBE_STATE_WFCNF); + } + } + + return ret; +} diff --git a/profinet_stack/p-net/src/device/pf_cmpbe.h b/profinet_stack/p-net/src/device/pf_cmpbe.h new file mode 100644 index 0000000..47e2a3a --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmpbe.h @@ -0,0 +1,80 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMPBE_H +#define PF_CMPBE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Show CMPBE instance information about an AR. + * @param p_ar In: The AR instance. + */ +void pf_cmpbe_show (const pf_ar_t * p_ar); + +/** + * Handle CMDEV events. + * @param p_ar InOut: The AR instance. + * @param event In: The new CMDEV state. Use PNET_EVENT_xxx, not + * PF_CMDEV_STATE_xxx + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmpbe_cmdev_state_ind (pf_ar_t * p_ar, pnet_event_values_t event); + +/** + * Handle a CControl request for a specific AR. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmpbe_cm_ccontrol_req (pnet_t * net, pf_ar_t * p_ar); + +/** + * Handle a CControl confirmation for a specific AR. + * @param p_ar InOut: The AR instance. + * @param p_control_io In: The CControl block. + * @param p_result Out: The result information. Not used. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmpbe_rm_ccontrol_cnf ( + pf_ar_t * p_ar, + const pf_control_block_t * p_control_io, + pnet_result_t * p_result); + +/** + * Handle a DControl indication for a specific AR. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_control_io InOut: The DControl block. + * @param p_result Out: The result information. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmpbe_rm_dcontrol_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pf_control_block_t * p_control_io, + pnet_result_t * p_result); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMPBE_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmrdr.c b/profinet_stack/p-net/src/device/pf_cmrdr.c new file mode 100644 index 0000000..4436d6c --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrdr.c @@ -0,0 +1,1195 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST +#endif + +#include +#include "pf_includes.h" +#include "pf_block_writer.h" + +/** + * @file + * @brief Implements the Context Management Read Record Responder protocol + * machine device (CMRDR) + * + * Contains mainly one function \a pf_cmrdr_rm_read_ind(), + * that handles a RPC parameter read request (and read implicit request). + * + * Triggers the \a pnet_read_ind() user callback for some values. + * + * This implementation of CMRDR has no internal state. + * Every call to \a pf_cmrdr_rm_read_ind() finishes by returning the result. + * Since there are no internal static variables there is also no need + * for a POWER-ON state. + */ + +int pf_cmrdr_rm_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_request, + pnet_result_t * p_read_status, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_pos) +{ + int ret = -1; + pf_iod_read_result_t read_result; + uint8_t * p_data = NULL; + uint16_t data_length_pos = 0; + uint16_t start_pos = 0; + uint8_t iocs[255]; /* Max possible array size */ + uint8_t iops[255]; /* Max possible array size */ + uint8_t subslot_data[PF_FRAME_BUFFER_SIZE]; /* Max possible array size */ + uint8_t iocs_len = 0; + uint8_t iops_len = 0; + uint16_t data_len = 0; + bool new_flag = false; + char station_name[PNET_STATION_NAME_MAX_SIZE]; /* Terminated*/ + + if ( + (p_read_request->slot_number == PNET_SLOT_DAP_IDENT) && + (pf_port_subslot_is_dap_port_id (net, p_read_request->subslot_number))) + { + LOG_INFO ( + PNET_LOG, + "CMRDR(%d): PLC is reading slot %u subslot 0x%04X index 0x%04X " + "(local port %d) \"%s\"\n", + __LINE__, + p_read_request->slot_number, + p_read_request->subslot_number, + p_read_request->index, + pf_port_dap_subslot_to_local_port (net, p_read_request->subslot_number), + pf_index_to_logstring (p_read_request->index)); + } + else + { + LOG_INFO ( + PNET_LOG, + "CMRDR(%d): PLC is reading slot %u subslot 0x%04X index 0x%04X " + "\"%s\"\n", + __LINE__, + p_read_request->slot_number, + p_read_request->subslot_number, + p_read_request->index, + pf_index_to_logstring (p_read_request->index)); + } + + read_result.sequence_number = p_read_request->sequence_number; + read_result.ar_uuid = p_read_request->ar_uuid; + read_result.api = p_read_request->api; + read_result.slot_number = p_read_request->slot_number; + read_result.subslot_number = p_read_request->subslot_number; + read_result.index = p_read_request->index; + + /* Get the data from FSPM or the application, if possible. */ + data_len = res_size - *p_pos; + if ( + pf_fspm_cm_read_ind ( + net, + p_ar, + p_read_request, + &p_data, + &data_len, + p_read_status) != 0) + { + data_len = 0; /* Handled: No data available. */ + } + + /* + * Just insert a dummy value for now. + * It will be properly computed after the answer has been written to the + * buffer. + */ + read_result.record_data_length = 0; + + /* + * The only result that can fail is from FSPM or the application. + * All well-known indices are handled without error. + * It is therefore OK to write the result already now. + * This is needed in order to get the correct position for the actual answer. + */ + read_result.add_data_1 = p_read_status->add_data_1; + read_result.add_data_2 = p_read_status->add_data_2; + + /* Also: Retrieve a position to write the total record length to. */ + pf_put_read_result ( + true, + &read_result, + res_size, + p_res, + p_pos, + &data_length_pos); + + /* + * Even if the application or FSPM provided an answer. + * If there is another answer then use that!! + */ + start_pos = *p_pos; + if (p_read_request->index <= PF_IDX_USER_MAX) + { + /* Provided by application - accept whatever it says. */ + if (*p_pos + data_len <= res_size) + { + memcpy (&p_res[*p_pos], p_data, data_len); + *p_pos += data_len; + ret = 0; + } + } + else + { + switch (p_read_request->index) + { + case PF_IDX_DEV_IM_0_FILTER_DATA: + /* Block-writer knows where to fetch and how to build the answer. */ + pf_put_im_0_filter_data (net, true, res_size, p_res, p_pos); + ret = 0; + break; + + /* I&M data has been provided by FSPM. Accept whatever it says. + * Put it into the response after verifying the data length. */ + case PF_IDX_SUB_IM_0: + if ((data_len == sizeof (pnet_im_0_t)) && (*p_pos + data_len < res_size)) + { + pf_put_im_0 (true, (pnet_im_0_t *)p_data, res_size, p_res, p_pos); + ret = 0; + } + break; + case PF_IDX_SUB_IM_1: + if ((data_len == sizeof (pnet_im_1_t)) && (*p_pos + data_len < res_size)) + { + os_mutex_lock (net->fspm_im_mutex); + pf_put_im_1 (true, (pnet_im_1_t *)p_data, res_size, p_res, p_pos); + os_mutex_unlock (net->fspm_im_mutex); + ret = 0; + } + break; + case PF_IDX_SUB_IM_2: + if ((data_len == sizeof (pnet_im_2_t)) && (*p_pos + data_len < res_size)) + { + os_mutex_lock (net->fspm_im_mutex); + pf_put_im_2 (true, (pnet_im_2_t *)p_data, res_size, p_res, p_pos); + os_mutex_unlock (net->fspm_im_mutex); + ret = 0; + } + break; + case PF_IDX_SUB_IM_3: + if ((data_len == sizeof (pnet_im_3_t)) && (*p_pos + data_len < res_size)) + { + os_mutex_lock (net->fspm_im_mutex); + pf_put_im_3 (true, (pnet_im_3_t *)p_data, res_size, p_res, p_pos); + os_mutex_unlock (net->fspm_im_mutex); + ret = 0; + } + break; + case PF_IDX_SUB_IM_4: + if ((data_len == sizeof (pnet_im_4_t)) && (*p_pos + data_len < res_size)) + { + os_mutex_lock (net->fspm_im_mutex); + pf_put_record_data_read ( + true, + PF_BT_IM_4, + data_len, + p_data, + res_size, + p_res, + p_pos); + os_mutex_unlock (net->fspm_im_mutex); + ret = 0; + } + break; + + /* Logbook data has been provided by FSPM. Accept whatever it says. + * Put it into the response after verifying the data length. */ + case PF_IDX_DEV_LOGBOOK_DATA: + pf_put_log_book_data ( + true, + (pf_log_book_t *)p_data, + res_size, + p_res, + p_pos); + ret = 0; + break; + + /* Block-writer knows where to fetch and how to build the answer to ID + * data requests. */ + case PF_IDX_SUB_EXP_ID_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW_1, + PF_BT_EXPECTED_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DEV_FILTER_LEVEL_SUBSLOT, + p_ar, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SUB_REAL_ID_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW_1, + PF_BT_REAL_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DEV_FILTER_LEVEL_SUBSLOT, + NULL, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SLOT_EXP_ID_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW_1, + PF_BT_EXPECTED_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DEV_FILTER_LEVEL_SUBSLOT, + p_ar, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SLOT_REAL_ID_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW_1, + PF_BT_REAL_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DEV_FILTER_LEVEL_SUBSLOT, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_AR_EXP_ID_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW_1, + PF_BT_EXPECTED_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DEV_FILTER_LEVEL_SUBSLOT, + p_ar, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_AR_REAL_ID_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW_1, + PF_BT_REAL_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DEV_FILTER_LEVEL_SUBSLOT, + p_ar, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_API_REAL_ID_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW_1, + PF_BT_REAL_IDENTIFICATION_DATA, + PF_DEV_FILTER_LEVEL_API, + PF_DEV_FILTER_LEVEL_SUBSLOT, + NULL, + p_read_request->api, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + + case PF_IDX_DEV_API_DATA: + pf_put_ident_data ( + net, + true, + PNET_BLOCK_VERSION_LOW, + PF_BT_API_DATA, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DEV_FILTER_LEVEL_API_ID, + NULL, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + + case PF_IDX_SUB_INPUT_DATA: + /* Sub-module data to the controller */ + data_len = sizeof (subslot_data); + iops_len = sizeof (iops); + iocs_len = sizeof (iocs); + if ( + pf_ppm_get_data_and_iops ( + net, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + subslot_data, + &data_len, + iops, + &iops_len) != 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMRDR(%d): Could not get PPM data and IOPS\n", + __LINE__); + data_len = 0; + iops_len = 0; + } + if ( + pf_cpm_get_iocs ( + net, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + iocs, + &iocs_len) != 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMRDR(%d): Could not get CPM IOCS\n", + __LINE__); + iocs_len = 0; + } + if ((data_len + iops_len + iocs_len) > 0) + { + /* Have all data */ + pf_put_input_data ( + true, + iocs_len, + iocs, + iops_len, + iops, + data_len, + subslot_data, + res_size, + p_res, + p_pos); + ret = 0; + } + break; + case PF_IDX_SUB_OUTPUT_DATA: + /* Sub-module data from the controller. */ + data_len = sizeof (subslot_data); + iops_len = sizeof (iops); + iocs_len = sizeof (iocs); + if ( + pf_cpm_get_data_and_iops ( + net, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + &new_flag, + subslot_data, + &data_len, + iops, + &iops_len) != 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMRDR(%d): Could not get CPM data and IOPS\n", + __LINE__); + data_len = 0; + iops_len = 0; + } + if ( + pf_ppm_get_iocs ( + net, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + iocs, + &iocs_len) != 0) + { + LOG_DEBUG ( + PNET_LOG, + "CMRDR(%d): Could not get PPM IOCS\n", + __LINE__); + iocs_len = 0; + } + if ((data_len + iops_len + iocs_len) > 0) + { + /* Have all data */ + /* ToDo: Support substitution values, mode and active flag */ + pf_put_output_data ( + true, + false, + iocs_len, + iocs, + iops_len, + iops, + data_len, + subslot_data, + 0, + subslot_data, + res_size, + p_res, + p_pos); + ret = 0; + } + break; + + /* Block-writer knows where to fetch and how to build the answer to diag + * data requests. */ + case PF_IDX_SUB_DIAGNOSIS_CH: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DIAG_FILTER_FAULT_STD, + NULL, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SUB_DIAGNOSIS_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DIAG_FILTER_FAULT_ALL, + NULL, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SUB_DIAGNOSIS_DMQS: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DIAG_FILTER_ALL, + NULL, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SUB_DIAG_MAINT_REQ_CH: + case PF_IDX_SUB_DIAG_MAINT_REQ_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DIAG_FILTER_M_REQ, + NULL, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SUB_DIAG_MAINT_DEM_CH: + case PF_IDX_SUB_DIAG_MAINT_DEM_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SUBSLOT, + PF_DIAG_FILTER_M_DEM, + NULL, + p_read_request->api, + p_read_request->slot_number, + p_read_request->subslot_number, + res_size, + p_res, + p_pos); + ret = 0; + break; + + case PF_IDX_SLOT_DIAGNOSIS_CH: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DIAG_FILTER_FAULT_STD, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SLOT_DIAGNOSIS_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DIAG_FILTER_FAULT_ALL, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SLOT_DIAGNOSIS_DMQS: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DIAG_FILTER_ALL, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SLOT_DIAG_MAINT_REQ_CH: + case PF_IDX_SLOT_DIAG_MAINT_REQ_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DIAG_FILTER_M_REQ, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_SLOT_DIAG_MAINT_DEM_CH: + case PF_IDX_SLOT_DIAG_MAINT_DEM_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DIAG_FILTER_M_DEM, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + + case PF_IDX_API_DIAGNOSIS_CH: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_API, + PF_DIAG_FILTER_FAULT_STD, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_API_DIAGNOSIS_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_API, + PF_DIAG_FILTER_FAULT_ALL, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_API_DIAGNOSIS_DMQS: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_API, + PF_DIAG_FILTER_ALL, + NULL, + p_read_request->api, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_API_DIAG_MAINT_REQ_CH: + case PF_IDX_API_DIAG_MAINT_REQ_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_API, + PF_DIAG_FILTER_M_REQ, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_API_DIAG_MAINT_DEM_CH: + case PF_IDX_API_DIAG_MAINT_DEM_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_API, + PF_DIAG_FILTER_M_DEM, + NULL, + p_read_request->api, + p_read_request->slot_number, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + + case PF_IDX_AR_DIAGNOSIS_CH: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DIAG_FILTER_FAULT_STD, + p_ar, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_AR_DIAGNOSIS_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DIAG_FILTER_FAULT_ALL, + p_ar, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_AR_DIAGNOSIS_DMQS: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DIAG_FILTER_ALL, + p_ar, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_AR_DIAG_MAINT_REQ_CH: + case PF_IDX_AR_DIAG_MAINT_REQ_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DIAG_FILTER_M_REQ, + p_ar, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_AR_DIAG_MAINT_DEM_CH: + case PF_IDX_AR_DIAG_MAINT_DEM_ALL: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DIAG_FILTER_M_DEM, + p_ar, + 0, + 0, + 0, + res_size, + p_res, + p_pos); + ret = 0; + break; + + case PF_IDX_DEV_DIAGNOSIS_DMQS: + pf_put_diag_data ( + net, + true, + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DIAG_FILTER_ALL, + NULL, /* Do not filter by AR */ + 0, /* API */ + 0, /* Slot */ + 0, /* Subslot */ + res_size, + p_res, + p_pos); + ret = 0; + break; + + case PF_IDX_DEV_CONN_MON_TRIGGER: + /* Restart timeout period. See pf_cmsm_rm_read_ind() */ + ret = 0; + break; + + case PF_IDX_API_AR_DATA: + pf_put_ar_data ( + net, + true, + p_ar, + p_read_request->api, + res_size, + p_res, + p_pos); + ret = 0; + break; + case PF_IDX_DEV_AR_DATA: + pf_put_ar_data (net, true, NULL, 0, res_size, p_res, p_pos); + ret = 0; + break; + case PF_IDX_AR_MOD_DIFF: + /* On implicit with p_ar == NULL pf_cmdev_generate_submodule_diff + * will return -1 + * In this case pf_put_ar_diff will not give an empty response + */ + (void)pf_cmdev_generate_submodule_diff (net, p_ar); + pf_put_ar_diff (true, p_ar, res_size, p_res, p_pos); + ret = 0; + break; + + case PF_IDX_SUB_PDPORT_DATA_REAL: + case PF_IDX_SUB_PDPORT_DATA_ADJ: + case PF_IDX_SUB_PDPORT_DATA_CHECK: + case PF_IDX_DEV_PDREAL_DATA: + case PF_IDX_DEV_PDEXP_DATA: + case PF_IDX_SUB_PDPORT_STATISTIC: + case PF_IDX_SUB_PDINTF_ADJUST: + ret = pf_pdport_read_ind ( + net, + p_ar, + p_read_request, + &read_result, + res_size, + p_res, + p_pos); + break; + + case PF_IDX_SUB_PDINTF_REAL: + /* Only check if this is the port subslot */ + if ( + (p_read_request->slot_number == PNET_SLOT_DAP_IDENT) && + (p_read_request->subslot_number == + PNET_SUBSLOT_DAP_INTERFACE_1_IDENT)) + { + pf_cmina_get_station_name (net, station_name); + + pf_put_pdinterface_data_real ( + true, + pf_cmina_get_device_macaddr (net), + pf_cmina_get_ipaddr (net), + pf_cmina_get_netmask (net), + pf_cmina_get_gateway (net), + station_name, + res_size, + p_res, + p_pos); + ret = 0; + } + else + { + ret = -1; + } + break; + default: + LOG_INFO ( + PNET_LOG, + "CMRDR(%d): Index 0x%04X is not supported.\n", + __LINE__, + p_read_request->index); + ret = -1; + break; + } + } + + if (ret != 0) + { + LOG_INFO ( + PNET_LOG, + "CMRDR(%d): Could not read index 0x%04X for slot %u subslot 0x%04X.\n", + __LINE__, + p_read_request->index, + p_read_request->slot_number, + p_read_request->subslot_number); + + p_read_status->pnio_status.error_code = PNET_ERROR_CODE_READ; + p_read_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_read_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_INVALID_INDEX; + p_read_status->pnio_status.error_code_2 = 10; + ret = 0; + } + + read_result.record_data_length = *p_pos - start_pos; + pf_put_uint32 ( + true, + read_result.record_data_length, + res_size, + p_res, + &data_length_pos); /* Insert actual data length */ + + /* Restart timer */ + if (pf_cmsm_cm_read_ind (net, p_ar, p_read_request) != 0) + { + ret = -1; + } + + return ret; +} + +const char * pf_index_to_logstring (uint16_t index) +{ + if (index <= PF_IDX_USER_MAX) + { + return "user defined"; + } + + switch (index) + { + case PF_IDX_SUB_EXP_ID_DATA: + return "ExpectedIdentificationData for one subslot"; + case PF_IDX_SUB_REAL_ID_DATA: + return "RealIdentificationData for one subslot"; + case PF_IDX_SUB_DIAGNOSIS_CH: + return "Diagnosis in channel coding for one subslot"; + case PF_IDX_SUB_DIAGNOSIS_ALL: + return "Diagnosis in all codings for one subslot"; + case PF_IDX_SUB_DIAGNOSIS_DMQS: + return "Diagnosis, Maintenance, Qualified and Status for one subslot"; + case PF_IDX_SUB_DIAG_MAINT_REQ_CH: + return "Maintenance required in channel coding for one subslot"; + case PF_IDX_SUB_DIAG_MAINT_DEM_CH: + return "Maintenance demanded in channel coding for one subslot"; + case PF_IDX_SUB_DIAG_MAINT_REQ_ALL: + return "Maintenance required in all codings for one subslot"; + case PF_IDX_SUB_DIAG_MAINT_DEM_ALL: + return "Maintenance demanded in all codings for one subslot"; + case PF_IDX_SUB_SUBSTITUTE: + return "SubstituteValue for one subslot"; + case PF_IDX_SUB_PDIR: + return "PDIRSubframeData for one subslot"; + case PF_IDX_SUB_PDPORTDATAREALEXT: + return "PDPortDataRealExtended for one subslot"; + case PF_IDX_SUB_INPUT_DATA: + return "inputdata for one subslot"; + case PF_IDX_SUB_OUTPUT_DATA: + return "outputdata for one subslot"; + case PF_IDX_SUB_PDPORT_DATA_REAL: + return "PDPortDataReal for one subslot"; + case PF_IDX_SUB_PDPORT_DATA_CHECK: + return "PDPortDataCheck for one subslot"; + case PF_IDX_SUB_PDIR_DATA: + return "PDIRData for one subslot"; + case PF_IDX_SUB_PDSYNC_ID0: + return "PDSyncData for one subslot SyncID=0"; + case PF_IDX_SUB_PDPORT_DATA_ADJ: + return "PDPortDataAdjust for one subslot"; + case PF_IDX_SUB_ISOCHRONUOUS_DATA: + return "IsochronousModeData for one subslot"; + case PF_IDX_SUB_PDTIME: + return "PDTimeData for one subslot"; + case PF_IDX_SUB_PDINTF_MRP_REAL: + return "PDInterfaceMrpDataReal for one subslot"; + case PF_IDX_SUB_PDINTF_MRP_CHECK: + return "PDInterfaceMrpDataCheck for one subslot"; + case PF_IDX_SUB_PDINTF_MRP_ADJUST: + return "PDInterfaceMrpDataAdjust for one subslot"; + case PF_IDX_SUB_PDPORT_MRP_ADJUST: + return "PDPortMrpDataAdjust for one subslot"; + case PF_IDX_SUB_PDPORT_MRP_REAL: + return "PDPortMrpDataReal for one subslot"; + case PF_IDX_SUB_PDPORT_MRPIC_REAL: + return "PDPortMrpIcDataAdjust for one subslot"; + case PF_IDX_SUB_PDPORT_MRPIC_CHECK: + return "PDPortMrpIcDataCheck for one subslot"; + case PF_IDX_SUB_PDPORT_MRPIC_ADJUST: + return "PDPortMrpIcDataReal for one subslot"; + case PF_IDX_SUB_PDPORT_FO_REAL: + return "PDPortFODataReal for one subslot"; + case PF_IDX_SUB_PDPORT_FO_CHECK: + return "PDPortFODataCheck for one subslot"; + case PF_IDX_SUB_PDPORT_FO_ADJUST: + return "PDPortFODataAdjust for one subslot"; + case PF_IDX_SUB_PDPORT_SPF_CHECK: + return "PDPortSFPDataCheck for one subslot"; + case PF_IDX_SUB_PDNC_CHECK: + return "PDNCDataCheck for one subslot"; + case PF_IDX_SUB_PDINTF_ADJUST: + return "PDInterfaceAdjust for one subslot"; + case PF_IDX_SUB_PDPORT_STATISTIC: + return "PDPortStatistic for one subslot"; + case PF_IDX_SUB_PDINTF_REAL: + return "PDInterfaceDataReal for one subslot"; + case PF_IDX_SUB_PDINTF_FSU_ADJUST: + return "PDInterfaceFSUDataAdjust"; + case PF_IDX_SUB_PE_ENTITY_STATUS: + return "PE_EntityStatusData for one subslot"; + case PF_IDX_SUB_COMBINED_OBJ_CONTAINER: + return "CombinedObjectContainer"; + case PF_IDX_SUB_RS_ADJUST_OBSERVER: + return "RS_AdjustObserver"; + case PF_IDX_TSN_NETWORK_CONTROL_DATA_REAL: + return "TSNNetworkControlDataReal"; + case PF_IDX_TSN_STREAM_PATH_DATA: + return "TSNStreamPathData"; + case PF_IDX_TSN_SYNC_TREE_DATA: + return "TSNSyncTreeData"; + case PF_IDX_TSN_UPLOAD_NETWORK_ATTRIBUTES: + return "TSNUploadNetworkAttributes"; + case PF_IDX_TSN_EXPECTED_NETWORK_ATTRIBUTES: + return "TSNExpectedNetworkAttributes"; + case PF_IDX_TSN_NETWORK_CONTROL_DATA_ADJUST: + return "TSNNetworkControlDataAdjust"; + case PF_IDX_SUB_PDINTF_SECURITY_ADJUST: + return "PDInterfaceSecurityAdjust for one subslot"; + case PF_IDX_SUB_IM_0: + return "I&M0"; + case PF_IDX_SUB_IM_1: + return "I&M1"; + case PF_IDX_SUB_IM_2: + return "I&M2"; + case PF_IDX_SUB_IM_3: + return "I&M3"; + case PF_IDX_SUB_IM_4: + return "I&M4"; + case PF_IDX_SUB_IM_5: + return "I&M5"; + case PF_IDX_SUB_IM_6: + return "I&M6"; + case PF_IDX_SUB_IM_7: + return "I&M7"; + case PF_IDX_SUB_IM_8: + return "I&M8"; + case PF_IDX_SUB_IM_9: + return "I&M9"; + case PF_IDX_SUB_IM_10: + return "I&M10"; + case PF_IDX_SUB_IM_11: + return "I&M11"; + case PF_IDX_SUB_IM_12: + return "I&M12"; + case PF_IDX_SUB_IM_13: + return "I&M13"; + case PF_IDX_SUB_IM_14: + return "I&M14"; + case PF_IDX_SUB_IM_15: + return "I&M15"; + case PF_IDX_SLOT_EXP_ID_DATA: + return "ExpectedIdentificationData for one slot"; + case PF_IDX_SLOT_REAL_ID_DATA: + return "RealIdentificationData for one slot"; + case PF_IDX_SLOT_DIAGNOSIS_CH: + return "Diagnosis in channel coding for one slot"; + case PF_IDX_SLOT_DIAGNOSIS_ALL: + return "Diagnosis in all codings for one slot"; + case PF_IDX_SLOT_DIAGNOSIS_DMQS: + return "Diagnosis, Maintenance, Qualified and Status for one slot"; + case PF_IDX_SLOT_DIAG_MAINT_REQ_CH: + return "Maintenance required in channel coding for one slot"; + case PF_IDX_SLOT_DIAG_MAINT_DEM_CH: + return "Maintenance demanded in channel coding for one slot"; + case PF_IDX_SLOT_DIAG_MAINT_REQ_ALL: + return "Maintenance required in all codings for one slot"; + case PF_IDX_SLOT_DIAG_MAINT_DEM_ALL: + return "Maintenance demanded in all codings for one slot"; + case PF_IDX_AR_EXP_ID_DATA: + return "ExpectedIdentificationData for one AR"; + case PF_IDX_AR_REAL_ID_DATA: + return "RealIdentificationData for one AR"; + case PF_IDX_AR_MOD_DIFF: + return "ModuleDiffBlock for one AR"; + case PF_IDX_AR_DIAGNOSIS_CH: + return "Diagnosis in channel coding for one AR"; + case PF_IDX_AR_DIAGNOSIS_ALL: + return "Diagnosis in all codings for one AR"; + case PF_IDX_AR_DIAGNOSIS_DMQS: + return "Diagnosis, Maintenance, Qualified and Status for one AR"; + case PF_IDX_AR_DIAG_MAINT_REQ_CH: + return "Maintenance required in channel coding for one AR"; + case PF_IDX_AR_DIAG_MAINT_DEM_CH: + return "Maintenance demanded in channel coding for one AR"; + case PF_IDX_AR_DIAG_MAINT_REQ_ALL: + return "Maintenance required in all codings for one AR"; + case PF_IDX_AR_DIAG_MAINT_DEM_ALL: + return "Maintenance demanded in all codings for one AR"; + case PF_IDX_AR_PE_ENTITY_FILTER_DATA: + return "PE_EntityFilterData for one AR"; + case PF_IDX_AR_PE_ENTITY_STATUS_DATA: + return "PE_EntityStatusData for one AR"; + case PF_IDX_AR_WRITE_MULTIPLE: + return "WriteMultiple"; + case PF_IDX_APPLICATION_READY: + return "ApplicationReadyBlock, closes parameterization"; + case PF_IDX_AR_FSU_DATA_ADJUST: + return "ARFSUDataAdjust data for one AR"; + case PF_IDX_AR_RS_GET_EVENT: + return "RS_GetEvent"; + case PF_IDX_AR_RS_ACK_EVENT: + return "RS_AckEvent"; + case PF_IDX_API_REAL_ID_DATA: + return "RealIdentificationData for one API"; + case PF_IDX_API_DIAGNOSIS_CH: + return "Diagnosis in channel coding for one API"; + case PF_IDX_API_DIAGNOSIS_ALL: + return "Diagnosis in all codings for one API"; + case PF_IDX_API_DIAGNOSIS_DMQS: + return "Diagnosis, Maintenance, Qualified and Status for one API"; + case PF_IDX_API_DIAG_MAINT_REQ_CH: + return "Maintenance required in channel coding for one API"; + case PF_IDX_API_DIAG_MAINT_DEM_CH: + return "Maintenance demanded in channel coding for one API"; + case PF_IDX_API_DIAG_MAINT_REQ_ALL: + return "Maintenance required in all codings for one API"; + case PF_IDX_API_DIAG_MAINT_DEM_ALL: + return "Maintenance demanded in all codings for one API"; + case PF_IDX_API_AR_DATA: + return "ARData for one API"; + case PF_IDX_DEV_DIAGNOSIS_DMQS: + return "Diagnosis, Maintenance, Qualified and Status for one device"; + case PF_IDX_DEV_AR_DATA: + return "ARData"; + case PF_IDX_DEV_API_DATA: + return "APIData"; + case PF_IDX_DEV_LOGBOOK_DATA: + return "LogBookData"; + case PF_IDX_DEV_PDEV_DATA: + return "PdevData"; + case PF_IDX_DEV_IM_0_FILTER_DATA: + return "I&M0FilterData"; + case PF_IDX_DEV_PDREAL_DATA: + return "PDRealData"; + case PF_IDX_DEV_PDEXP_DATA: + return "PDExpectedData"; + case PF_IDX_DEV_AUTO_CONFIGURATION: + return "AutoConfiguration"; + case PF_IDX_DEV_GSD_UPLOAD: + return "GSD upload"; + case PF_IDX_DEV_PE_ENTITY_FILTER_DATA: + return "PE_EntityFilterData"; + case PF_IDX_DEV_PE_ENTITY_STATUS_DATA: + return "PE_EntityStatusData"; + case PF_IDX_DEV_ASSET_MANAGEMENT_1: + return "AssetManagementData1"; + case PF_IDX_DEV_ASSET_MANAGEMENT_2: + return "AssetManagementData2"; + case PF_IDX_DEV_ASSET_MANAGEMENT_3: + return "AssetManagementData3"; + case PF_IDX_DEV_ASSET_MANAGEMENT_4: + return "AssetManagementData4"; + case PF_IDX_DEV_ASSET_MANAGEMENT_5: + return "AssetManagementData5"; + case PF_IDX_DEV_ASSET_MANAGEMENT_6: + return "AssetManagementData6"; + case PF_IDX_DEV_ASSET_MANAGEMENT_7: + return "AssetManagementData7"; + case PF_IDX_DEV_ASSET_MANAGEMENT_8: + return "AssetManagementData8"; + case PF_IDX_DEV_ASSET_MANAGEMENT_9: + return "AssetManagementData9"; + case PF_IDX_DEV_ASSET_MANAGEMENT_10: + return "AssetManagementData10"; + case PF_IDX_TSN_STREAM_ADD: + return "TSN stream add"; + case PF_IDX_PDRSI_INSTANCDES: + return "PDRsiInstances"; + case PF_IDX_TSN_STREAM_REMOVE: + return "TSN stream remove"; + case PF_IDX_TSN_STREAM_RENEW: + return "TSN stream renew"; + case PF_IDX_DEV_CONN_MON_TRIGGER: + return "nothing, but is restarting the timeout period"; + default: + return "unknown"; + } +} diff --git a/profinet_stack/p-net/src/device/pf_cmrdr.h b/profinet_stack/p-net/src/device/pf_cmrdr.h new file mode 100644 index 0000000..3edf6b0 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrdr.h @@ -0,0 +1,58 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMRDR_H +#define PF_CMRDR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Handle a RPC read request. + * Triggers the \a pnet_read_ind() user callback for some values. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_read_request In: The read request. + * @param p_read_status Out: The result information. + * @param res_size In: The size of the output buffer. + * @param p_res Out: The output buffer. + * @param p_pos InOut: Position in the output buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmrdr_rm_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_request, + pnet_result_t * p_read_status, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_pos); + +/** + * Describe an index on a subslot. + * + * @param index In: The index + * @return a descriptive string. + */ +const char * pf_index_to_logstring (uint16_t index); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMRDR_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmrpc.c b/profinet_stack/p-net/src/device/pf_cmrpc.c new file mode 100644 index 0000000..cb7ad34 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrpc.c @@ -0,0 +1,5103 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Context Management RPC Device Protocol Machine (CMRPC) + * + * This handles RPC communication via UDP; for example connect, release, + * dcontrol, ccontrol and parameter read and write. + * + * Is responsible for reruns (retransmissions). + * + * It opens a UDP socket and listens for connections. + * + * CMRPC has only one state - IDLE. I.e., it is state-less. + * + * An AR is created on each connect. + * The ar:s are allocated from the array net->cmrpc_ar + * ToDo: Move to CMDEV device structure. + * + * Sessions are used for sending notifications to the controller, when a + * response is expected. + * Each session uses one entry in net->cmrpc_session_info + * + * The socket net->cmrpc_rpcreq_socket is used for RPC requests (connects etc) + * + */ + +#ifdef UNIT_TEST +#define os_get_current_time_us mock_os_get_current_time_us +#define pf_generate_uuid mock_pf_generate_uuid +#endif + +#include "pf_includes.h" +#include "pf_cmrpc_epm.h" +#include "pf_block_reader.h" +#include "pf_block_writer.h" + +#include +#include +#include +#include + +#if PNET_MAX_CR < 2 +#error "There must be at least 2 CR per AR. Increase PNET_MAX_CR." +#endif + +#if PNET_MAX_SESSION_BUFFER_SIZE > UINT16_MAX +#error "PNET_MAX_SESSION_BUFFER_SIZE must be less than or equal to 65535" +#endif + +static const pf_uuid_t implicit_ar = {0, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}}; + +static const pf_uuid_t uuid_epmap_interface = { + 0xE1AF8308, + 0x5D1F, + 0x11C9, + {0x91, 0xA4, 0x08, 0x00, 0x2B, 0x14, 0xA0, 0xFA}}; + +static const pf_uuid_t uuid_io_device_interface = { + 0xDEA00001, + 0x6C97, + 0x11D1, + {0x82, 0x71, 0x00, 0xA0, 0x24, 0x42, 0xDF, 0x7D}}; + +#define PF_CMRPC_NUMBER_OF_RESENDS 3 + +/**************** Diagnostic strings *****************************************/ + +void pf_memory_contents_show (const uint8_t * data, int size) +{ + int i, j, pos, n, remain; + uint8_t c; + char s[80]; + + /* Note: a line is 67 characters or shorter */ + + for (i = 0; i < size; i += 16) + { + pos = 0; + remain = sizeof (s); + + /* Print hex values */ + for (j = 0; j < 16 && (i + j) < size; j++) + { + n = snprintf (&s[pos], remain, "%02x ", data[i + j]); + if (n >= remain) + return; + + pos += n; + remain -= n; + } + + /* Fill up short line */ + for (; j < 16; j++) + { + n = snprintf (&s[pos], remain, " "); + if (n >= remain) + return; + + pos += n; + remain -= n; + } + + n = snprintf (&s[pos], remain, "|"); + if (n >= remain) + return; + + pos += n; + remain -= n; + + /* Print ASCII values */ + for (j = 0; j < 16 && (i + j) < size; j++) + { + c = data[i + j]; + c = (isprint (c)) ? c : '.'; + n = snprintf (&s[pos], remain, "%c", c); + if (n >= remain) + return; + + pos += n; + remain -= n; + } + + n = snprintf (&s[pos], remain, "|\n"); + if (n >= remain) + return; + + printf ("%s", s); + } +} + +/** + * @internal + * Return a string representation of the AR state. + * @param state In: The AR state. + * @return A string representing the AR state. + */ +static const char * pf_ar_state_to_string (pf_ar_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_AR_STATE_PRIMARY: + s = "PF_AR_STATE_PRIMARY"; + break; + case PF_AR_STATE_FIRST: + s = "PF_AR_STATE_FIRST"; + break; + case PF_AR_STATE_BACKUP: + s = "PF_AR_STATE_BACKUP"; + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the AR sync state. + * @param state In: The AR sync state. + * @return A string representing the AR sync state. + */ +static const char * pf_ar_sync_state_to_string (pf_sync_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_SYNC_STATE_SYNCHRONIZED: + s = "PF_SYNC_STATE_SYNCHRONIZED"; + break; + case PF_SYNC_STATE_NOT_AVAILABLE: + s = "PF_SYNC_STATE_NOT_AVAILABLE"; + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of an IOCR type. + * @param type In: The IOCR type. + * @return A string representing the IOCR type. + */ +static const char * pf_iocr_type_to_string (pf_iocr_type_values_t type) +{ + const char * s = ""; + + switch (type) + { + case PF_IOCR_TYPE_INPUT: + s = "INPUT"; + break; + case PF_IOCR_TYPE_OUTPUT: + s = "OUTPUT"; + break; + case PF_IOCR_TYPE_MC_CONSUMER: + s = "MC CONSUMER"; + break; + case PF_IOCR_TYPE_MC_PROVIDER: + s = "MC PROVIDER"; + break; + default: + break; + } + + return s; +} + +/** + * @internal + * Return a string representation of the AR type. + * @param type In: The AR type. + * @return A string representing the AR type. + */ +static const char * pf_ar_type_to_string (pf_ar_type_values_t type) +{ + const char * s = ""; + + switch (type) + { + case PF_ART_IOCAR_SINGLE: + s = "Controller AR"; + break; + case PF_ART_IOSAR: + s = "Supervisor AR"; + break; + case PF_ART_IOCAR_SINGLE_RTC_3: + s = "RTC_3 AR"; + break; + case PF_ART_IOCAR_SR: + s = "AR Set"; + break; + } + + return s; +} + +void pf_cmrpc_show (pnet_t * net, unsigned level) +{ + uint16_t ix; + uint16_t iy; + uint16_t ar_ix; + pf_ar_t * p_ar = NULL; + pf_iocr_t * p_iocr = NULL; + pf_session_info_t * p_sess = NULL; + pf_iodata_object_t * p_desc; + + if (level & 0x0800) + { + printf ("\nCMRPC sessions:\n"); + printf (" Main socket = %u\n", net->cmrpc_rpcreq_socket); + for (ix = 0; ix < PF_MAX_SESSION; ix++) + { + p_sess = &net->cmrpc_session_info[ix]; + printf ("Session index = %u\n", ix); + printf (" Alloc session ID = %u\n", (unsigned)p_sess->ix); + printf (" in use = %s\n", p_sess->in_use ? "YES" : "NO"); + printf ( + " release in progress= %s\n", + p_sess->release_in_progress ? "YES" : "NO"); + printf (" socket = %d\n", p_sess->socket); + printf (" @AR = %p\n", p_sess->p_ar); + printf (" from me = %s\n", p_sess->from_me ? "YES" : "NO"); + printf ( + " ip_addr = %u.%u.%u.%u\n", + ((unsigned)p_sess->ip_addr >> 24) & 0xff, + ((unsigned)p_sess->ip_addr >> 16) & 0xff, + ((unsigned)p_sess->ip_addr >> 8) & 0xff, + (unsigned)p_sess->ip_addr & 0xff); + printf (" port = %u\n", (unsigned)p_sess->port); + printf ( + " sequence_nmb_send = %u\n", + (unsigned)p_sess->sequence_nmb_send); + printf ( + " fragment_nbr = %u\n", + (unsigned)p_sess->in_fragment_nbr); + printf ( + " dcontrol_sequence_nmb = %u\n", + (unsigned)p_sess->dcontrol_sequence_nmb); + printf ( + " dcontrol result = %02x %02x %02x %02x\n", + (unsigned)p_sess->dcontrol_result.pnio_status.error_code, + (unsigned)p_sess->dcontrol_result.pnio_status.error_decode, + (unsigned)p_sess->dcontrol_result.pnio_status.error_code_1, + (unsigned)p_sess->dcontrol_result.pnio_status.error_code_2); + printf ( + " activity UUID = " + "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x\n", + (unsigned)p_sess->activity_uuid.data1, + (unsigned)p_sess->activity_uuid.data2, + (unsigned)p_sess->activity_uuid.data3, + (unsigned)p_sess->activity_uuid.data4[0], + (unsigned)p_sess->activity_uuid.data4[1], + (unsigned)p_sess->activity_uuid.data4[2], + (unsigned)p_sess->activity_uuid.data4[3], + (unsigned)p_sess->activity_uuid.data4[4], + (unsigned)p_sess->activity_uuid.data4[5], + (unsigned)p_sess->activity_uuid.data4[6], + (unsigned)p_sess->activity_uuid.data4[7]); + } + } + + if (level & 0x1000) + { + printf ("\nCMRPC ARs:\n"); + for (ar_ix = 0; ar_ix < PNET_MAX_AR; ar_ix++) + { + p_ar = pf_ar_find_by_index (net, ar_ix); + printf ("AR index = %u\n", (unsigned)ar_ix); + printf ("AR in use = %s\n", p_ar->in_use ? "YES" : "NO"); + printf ("AR arep = %u\n", (unsigned)p_ar->arep); + printf ( + "AR state = %s\n", + pf_ar_state_to_string (p_ar->ar_state)); + printf ( + "sync state = %s\n", + pf_ar_sync_state_to_string (p_ar->sync_state)); + printf ( + "ready 4 data = %s\n", + p_ar->ready_4_data ? "TRUE" : "FALSE"); + pf_cmdev_ar_show (p_ar); + pf_cmpbe_show (p_ar); + pf_cmio_show (p_ar); + pf_cmsm_show (p_ar); + pf_cmwrr_show (net, p_ar); + printf ("AR param valid = %u\n", (unsigned)p_ar->ar_param.valid); + printf ( + " type = %s\n", + pf_ar_type_to_string (p_ar->ar_param.ar_type)); + printf ( + " session key = %u\n", + (unsigned)p_ar->ar_param.session_key); + printf ( + " prop state = %u\n", + (unsigned)p_ar->ar_param.ar_properties.state); + printf ( + " par server = %s\n", + p_ar->ar_param.ar_properties.parameterization_server == + PF_PS_EXTERNAL_PARAMETER_SERVER + ? "Ext" + : "CM initiator"); + printf ( + " dev access = %u\n", + (unsigned)p_ar->ar_param.ar_properties.device_access); + printf ( + " sup allowed = %u\n", + (unsigned)p_ar->ar_param.ar_properties.supervisor_takeover_allowed); + printf ( + " pull alarm OK= %u\n", + (unsigned)p_ar->ar_param.ar_properties.pull_module_alarm_allowed); + printf ( + " startup mode = %s\n", + p_ar->ar_param.ar_properties.startup_mode ? "Advanced" : "Legacy"); + printf (" err_cls = %x\n", (unsigned)p_ar->err_cls); + printf (" err_code = %x\n", (unsigned)p_ar->err_code); + printf ( + " CM Initiator Activity Timeout Factor = %u\n", + p_ar->ar_param.cm_initiator_activity_timeout_factor); + + pf_alarm_show (p_ar); + + printf ("nbr_iocr = %u\n", (unsigned)p_ar->nbr_iocrs); + if (level & 0x03) + { + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + p_iocr = &p_ar->iocrs[ix]; + printf ("\n--------------CMRPC AR IOCR %u ---------------\n", ix); + printf ( + "%3u : IOCR type = %s\n", + (unsigned)ix, + pf_iocr_type_to_string (p_iocr->param.iocr_type)); + printf ( + "%3u : in_length = %u\n", + (unsigned)ix, + (unsigned)p_iocr->in_length); + printf ( + "%3u : out_length = %u\n", + (unsigned)ix, + (unsigned)p_iocr->out_length); + printf ( + "%3u : nbr_data_desc= %u\n", + (unsigned)ix, + (unsigned)p_iocr->nbr_data_desc); + if (level & 0x02) /* More details */ + { + for (iy = 0; iy < p_iocr->nbr_data_desc; iy++) + { + p_desc = &p_iocr->data_desc[iy]; + + printf ( + "\n%3u,%3u: in_use = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->in_use); + printf ( + "%3u,%3u: data_avail = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->data_avail); + printf ( + "%3u,%3u: api = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->api_id); + printf ( + "%3u,%3u: slot = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->slot_nbr); + printf ( + "%3u,%3u: subslot = 0x%04x\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->subslot_nbr); + printf ( + "%3u,%3u: offset = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->data_offset); + printf ( + "%3u,%3u: len = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->data_length); + printf ( + "%3u,%3u: IOPS off = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->iops_offset); + printf ( + "%3u,%3u: IOPS len = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->iops_length); + printf ( + "%3u,%3u: IOCS off = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->iocs_offset); + printf ( + "%3u,%3u: IOCS len = %u\n", + (unsigned)ix, + (unsigned)iy, + (unsigned)p_desc->iocs_length); + } + if ( + (p_iocr->param.iocr_type == PF_IOCR_TYPE_INPUT) || + (p_iocr->param.iocr_type == PF_IOCR_TYPE_MC_PROVIDER)) + { + pf_ppm_show (&p_iocr->ppm); + } + else + { + pf_cpm_show (net, &p_iocr->cpm); + } + } + } + } + printf ("\n"); + } + } +} + +/*********************** Sessions and ARs ************************************/ + +/** + * @internal + * Allocate a new session instance. + * @param net InOut: The p-net stack instance + * @param pp_sess Out: A pointer to the new session instance. + * @return 0 if operation succeeded. + * -1 if an error occurred (no available sessions) + */ +static int pf_session_allocate (pnet_t * net, pf_session_info_t ** pp_sess) +{ + int ret = -1; + uint16_t ix = 0; + pf_session_info_t * p_sess = NULL; + const pnet_ethaddr_t * mac_address = pf_cmina_get_device_macaddr (net); + + os_mutex_lock (net->p_cmrpc_rpc_mutex); + while ((ix < NELEMENTS (net->cmrpc_session_info)) && + (net->cmrpc_session_info[ix].in_use == true)) + { + ix++; + } + + if (ix < NELEMENTS (net->cmrpc_session_info)) + { + p_sess = &net->cmrpc_session_info[ix]; + memset (p_sess, 0, sizeof (*p_sess)); + p_sess->socket = -1; + p_sess->in_use = true; + p_sess->p_ar = NULL; + p_sess->sequence_nmb_send = 0; + p_sess->dcontrol_sequence_nmb = UINT32_MAX; + p_sess->ix = ix; + pf_scheduler_init_handle (&p_sess->resend_timeout, "rpc"); + + /* Set activity UUID. Will be overwritten for incoming requests. */ + pf_generate_uuid ( + os_get_current_time_us(), + net->cmrpc_session_number, + *mac_address, + &p_sess->activity_uuid); + net->cmrpc_session_number++; + + *pp_sess = p_sess; + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Allocated session %u\n", + __LINE__, + (unsigned)p_sess->ix); + + ret = 0; + } + os_mutex_unlock (net->p_cmrpc_rpc_mutex); + + return ret; +} + +/** + * @internal + * Free the session_info. + * Close the corresponding UDP socket if necessary. + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The session instance. + */ +static void pf_session_release (pnet_t * net, pf_session_info_t * p_sess) +{ + if (p_sess != NULL) + { + if (p_sess->in_use == true) + { + if (p_sess->socket > -1) + { + if (p_sess->from_me) + { + pf_udp_close (net, p_sess->socket); + p_sess->socket = -1; + } + } + + pf_scheduler_remove_if_running (net, &p_sess->resend_timeout); + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Released session %u\n", + __LINE__, + (unsigned)p_sess->ix); + + memset (p_sess, 0, sizeof (*p_sess)); + p_sess->in_use = false; + p_sess->socket = -1; + } + else + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): Session not in use\n", __LINE__); + } + } + else + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): Session is NULL\n", __LINE__); + } +} + +/** + * @internal + * Find a session (in use) by its activity UUID. + * @param net InOut: The p-net stack instance + * @param p_uuid In: The UUID to look for. + * @param pp_sess Out: The session instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_session_locate_by_uuid ( + pnet_t * net, + const pf_uuid_t * p_uuid, + pf_session_info_t ** pp_sess) +{ + int ret = -1; + uint16_t ix; + + ix = 0; + while ((ix < NELEMENTS (net->cmrpc_session_info)) && + ((net->cmrpc_session_info[ix].in_use == false) || + (memcmp ( + p_uuid, + &net->cmrpc_session_info[ix].activity_uuid, + sizeof (*p_uuid)) != 0))) + { + ix++; + } + if (ix < NELEMENTS (net->cmrpc_session_info)) + { + *pp_sess = &net->cmrpc_session_info[ix]; + ret = 0; + } + + return ret; +} + +/** + * @internal + * Find a session by its AR (parent) instance. + * Only find sessions that originate from the device. + * Returns the first session in use. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param pp_sess Out: The session instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_session_locate_by_ar ( + pnet_t * net, + const pf_ar_t * p_ar, + pf_session_info_t ** pp_sess) +{ + int ret = -1; + uint16_t ix; + + ix = 0; + while ((ix < NELEMENTS (net->cmrpc_session_info)) && + ((net->cmrpc_session_info[ix].in_use == false) || + (net->cmrpc_session_info[ix].from_me == false) || + (net->cmrpc_session_info[ix].p_ar != p_ar))) + { + ix++; + } + if (ix < NELEMENTS (net->cmrpc_session_info)) + { + *pp_sess = &net->cmrpc_session_info[ix]; + ret = 0; + } + + return ret; +} + +/** + * @internal + * Allocate and clear a new AR. + * @param net InOut: The p-net stack instance + * @param pp_ar Out: The new AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_ar_allocate (pnet_t * net, pf_ar_t ** pp_ar) +{ + int ret = -1; + uint16_t ix = 0; + os_mutex_lock (net->p_cmrpc_rpc_mutex); + while ((ix < NELEMENTS (net->cmrpc_ar)) && + (net->cmrpc_ar[ix].in_use == true)) + { + ix++; + } + + if (ix < NELEMENTS (net->cmrpc_ar)) + { + memset (&net->cmrpc_ar[ix], 0, sizeof (net->cmrpc_ar[ix])); + net->cmrpc_ar[ix].in_use = true; + pf_cmsu_init (net, &net->cmrpc_ar[ix]); + pf_cmwrr_init (net, &net->cmrpc_ar[ix]); + pf_scheduler_init_handle (&net->cmrpc_ar[ix].cmio_timeout, "cmio"); + net->cmrpc_ar[ix].cmio_timer_should_reschedule = false; + + *pp_ar = &net->cmrpc_ar[ix]; + + ret = 0; + } + os_mutex_unlock (net->p_cmrpc_rpc_mutex); + + if (ix < PNET_MAX_AR) + { + net->cmrpc_ar[ix].arep = ix + 1; /* Avoid AREP == 0 */ + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Allocate AR %u (AREP %u)\n", + __LINE__, + ix, + net->cmrpc_ar[ix].arep); + } + + return ret; +} + +/** + * @internal + * Free the AR. + * @param p_ar InOut: The AR instance. + */ +static void pf_ar_release (pf_ar_t * p_ar) +{ + if (p_ar != NULL) + { + if (p_ar->in_use == true) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Free AR %u (AREP %u)\n", + __LINE__, + p_ar->arep - 1, + p_ar->arep); + memset (p_ar, 0, sizeof (*p_ar)); + } + else + { + LOG_ERROR (PNET_LOG, "CMRPC(%d): AR already released\n", __LINE__); + } + } + else + { + LOG_ERROR (PNET_LOG, "CMRPC(%d): AR is NULL\n", __LINE__); + } +} + +/** + * @internal + * Find an AR by its UUID. The AR must be in a "used" state (not POWER_ON). + * @param net InOut: The p-net stack instance + * @param p_uuid In: The AR_UUID. + * @param pp_ar Out: The AR (or NULL). + * @return 0 if AR is found and in "used" state. + * -1 if the AR_UUID is invalid. + */ +static int pf_ar_find_by_uuid ( + pnet_t * net, + const pf_uuid_t * p_uuid, + pf_ar_t ** pp_ar) +{ + int ret = -1; + uint16_t ix; + pf_cmdev_state_values_t cmdev_state; + + ix = 0; + while ( + (ix < NELEMENTS (net->cmrpc_ar)) && + ((net->cmrpc_ar[ix].in_use == false) || + ((pf_cmdev_get_state (&net->cmrpc_ar[ix], &cmdev_state) == 0) && + (cmdev_state == PF_CMDEV_STATE_POWER_ON)) || + (memcmp (p_uuid, &net->cmrpc_ar[ix].ar_param.ar_uuid, sizeof (*p_uuid)) != + 0))) + { + ix++; + } + if (ix < NELEMENTS (net->cmrpc_ar)) + { + *pp_ar = &net->cmrpc_ar[ix]; + ret = 0; + } + + return ret; +} + +pf_ar_t * pf_ar_find_by_index (pnet_t * net, uint16_t ix) +{ + if (ix < NELEMENTS (net->cmrpc_ar)) + { + return &net->cmrpc_ar[ix]; + } + + return NULL; +} + +int pf_ar_find_by_arep (pnet_t * net, uint32_t arep, pf_ar_t ** pp_ar) +{ + int ret = -1; + uint16_t ix; + + if (arep > 0) + { + ix = arep - 1; /* Convert to index */ + if ((ix < NELEMENTS (net->cmrpc_ar)) && (net->cmrpc_ar[ix].in_use == true)) + { + *pp_ar = &net->cmrpc_ar[ix]; + ret = 0; + } + } + + return ret; +} + +/******************** Error handling *****************************************/ + +void pf_set_error ( + pnet_result_t * p_stat, + uint8_t code, + uint8_t decode, + uint8_t code_1, + uint8_t code_2) +{ + LOG_INFO ( + PNET_LOG, + "CMRPC(%d) Setting error: 0x%02X 0x%02X 0x%02X 0x%02X\n", + __LINE__, + (unsigned)code, + (unsigned)decode, + (unsigned)code_1, + (unsigned)code_2); + + if ( + p_stat->pnio_status.error_code != PNET_ERROR_CODE_NOERROR || + p_stat->pnio_status.error_decode != PNET_ERROR_DECODE_NOERROR) + { + LOG_DEBUG ( + PNET_LOG, + "CMRPC(%d) Did overwrite previous error: 0x%02X 0x%02X 0x%02X " + "0x%02X\n", + __LINE__, + (unsigned)p_stat->pnio_status.error_code, + (unsigned)p_stat->pnio_status.error_decode, + (unsigned)p_stat->pnio_status.error_code_1, + (unsigned)p_stat->pnio_status.error_code_2); + } + + p_stat->pnio_status.error_code = code; + p_stat->pnio_status.error_decode = decode; + p_stat->pnio_status.error_code_1 = code_1; + p_stat->pnio_status.error_code_2 = code_2; +} + +void pf_set_error_if_not_already_set ( + pnet_result_t * p_stat, + uint8_t code, + uint8_t decode, + uint8_t code_1, + uint8_t code_2) +{ + if ( + p_stat->pnio_status.error_code == PNET_ERROR_CODE_NOERROR && + p_stat->pnio_status.error_decode == PNET_ERROR_DECODE_NOERROR) + { + LOG_INFO ( + PNET_LOG, + "CMRPC(%d) Setting error: 0x%02X 0x%02X 0x%02X 0x%02X (not " + "overwriting)\n", + __LINE__, + (unsigned)code, + (unsigned)decode, + (unsigned)code_1, + (unsigned)code_2); + + p_stat->pnio_status.error_code = code; + p_stat->pnio_status.error_decode = decode; + p_stat->pnio_status.error_code_1 = code_1; + p_stat->pnio_status.error_code_2 = code_2; + } + else + { + LOG_DEBUG ( + PNET_LOG, + "CMRPC(%d) No overwrite of error, as these values already are set: " + "0x%02X 0x%02X 0x%02X 0x%02X Ignored error: 0x%02X 0x%02X 0x%02X " + "0x%02X\n", + __LINE__, + (unsigned)p_stat->pnio_status.error_code, + (unsigned)p_stat->pnio_status.error_decode, + (unsigned)p_stat->pnio_status.error_code_1, + (unsigned)p_stat->pnio_status.error_code_2, + (unsigned)code, + (unsigned)decode, + (unsigned)code_1, + (unsigned)code_2); + } +} + +/******************** Send UDP ***********************************************/ + +/** + * @internal + * Send a UDP packet from a buffer + * + * @param p_net InOut: The p-net stack instance + * @param p_sess InOut: The session. + * @param data In: Data + * @param size In: Size in bytes + * @param payload_description In: Payload description for debug printouts + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_send_once_from_buffer ( + pnet_t * p_net, + pf_session_info_t * p_sess, + const uint8_t * data, + int size, + const char * payload_description) +{ + int ret = -1; + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + + if (size != 0) + { + pf_cmina_ip_to_string (p_sess->ip_addr, ip_string); + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Sending %u bytes on socket %u to %s:%u Payload:\"%s\" " + "Session from me:%u Session index:%u\n", + __LINE__, + size, + (unsigned)p_sess->socket, + ip_string, + p_sess->port, + payload_description, + p_sess->from_me, + p_sess->ix); + + if ( + pf_udp_sendto ( + p_net, + p_sess->socket, + p_sess->ip_addr, + p_sess->port, + data, + size) == size) + { + ret = 0; + } + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): No UDP data to send on the socket. Session from me:%u " + "Session index:%u\n", + __LINE__, + p_sess->from_me, + p_sess->ix); + } + + return ret; +} + +/** + * @internal + * Send a UDP packet from the output buffer in the session. + * + * @param p_net InOut: The p-net stack instance + * @param p_sess InOut: The session. + * @param payload_description In: Payload description for debug printouts + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_send_once ( + pnet_t * p_net, + pf_session_info_t * p_sess, + const char * payload_description) +{ + int ret = -1; + + ret = pf_cmrpc_send_once_from_buffer ( + p_net, + p_sess, + p_sess->out_buffer, + p_sess->out_buf_send_len, + payload_description); + + return ret; +} + +/** + * @internal + * Send a UDP packet from the output buffer in the session, with retry. + * Start a timer that calls this function again if the timer expires. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * Typically used for CControl and for fragments. + * + * @param p_net InOut: The p-net stack instance + * @param arg InOut: The session. + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static void pf_cmrpc_send_with_timeout ( + pnet_t * p_net, + void * arg, + uint32_t current_time) +{ + pf_session_info_t * p_sess = (pf_session_info_t *)arg; + uint32_t delay = 0; + const char * payload_description; + + if (p_sess == NULL) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Invalid session, it is NULL\n", + __LINE__); + } + else if (p_sess->socket < 0) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Invalid session, the socket is not open.\n", + __LINE__); + } + else + { + pf_scheduler_reset_handle (&p_sess->resend_timeout); + + if (p_sess->resend_counter > 0) + { + /* Send */ + p_sess->resend_counter--; + delay = p_sess->from_me ? (PF_CCONTROL_TIMEOUT * 1000) + : (PF_FRAG_TIMEOUT * 1000); + payload_description = p_sess->from_me ? "CControl request (possibly " + "fragment)" + : "response fragment"; + if (pf_cmrpc_send_once (p_net, p_sess, payload_description) == 0) + { + if ( + pf_scheduler_add ( + p_net, + delay, + pf_cmrpc_send_with_timeout, + arg, + &p_sess->resend_timeout) != 0) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): pf_scheduler_add failed for " + "pf_cmrpc_send_with_timeout()\n", + __LINE__); + } + } + } + else + { + if (p_sess->from_me == true) + { + /* Error: No response to CControl request. */ + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Last CControl request has been sent, with no " + "response received.\n", + __LINE__); + p_sess->p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_sess->p_ar->err_code = + PNET_ERROR_CODE_2_ABORT_AR_RPC_CONTROL_ERROR; + (void)pf_cmdev_cm_abort (p_net, p_sess->p_ar); + if (p_sess->socket > -1) + { + pf_udp_close (p_net, p_sess->socket); + p_sess->socket = -1; + } + } + else + { + /* Error: No reaction to sent fragment */ + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Fragment has been sent, but timing out due to lack " + "of incoming message.\n", + __LINE__); + p_sess->p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_sess->p_ar->err_code = + PNET_ERROR_CODE_2_ABORT_AR_RPC_CONTROL_ERROR; + (void)pf_cmdev_cm_abort (p_net, p_sess->p_ar); + } + } + } +} + +/******************** RPC parsers *******************************************/ + +/** + * @internal + * Check if block header length and version is valid. + * @param block_length In: The block length. + * @param p_block_header In: The block header instance. + * @param err_code_1 In: error_code_1 to use if an error is found. + * @param p_stat Out: Detailed error information. + * @return 0 if no error was found + * -1 if an error was found. + */ +static int pf_check_block_header ( + uint16_t block_length, + const pf_block_header_t * p_block_header, + uint8_t err_code_1, + pnet_result_t * p_stat) +{ + int ret = 0; /* OK until error found. */ + + if (p_block_header->block_length != block_length) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + err_code_1, + PNET_ERROR_CODE_2_INVALID_BLOCK_LEN); + ret = -1; + } + else if (p_block_header->block_version_high != PNET_BLOCK_VERSION_HIGH) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + err_code_1, + PNET_ERROR_CODE_2_INVALID_BLOCK_VERSION_HIGH); + ret = -1; + } + else if (p_block_header->block_version_low != PNET_BLOCK_VERSION_LOW) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + err_code_1, + PNET_ERROR_CODE_2_INVALID_BLOCK_VERSION_LOW); + ret = -1; + } + + return ret; +} + +/** + * @internal + * Parse all blocks in a Connect RPC request message. + * @param p_sess InOut: The RPC session instance. The rpc_result + * field is written when return != 0. + * @param p_pos InOut: Position in the input buffer. + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_connect_interpret_ind ( + pf_session_info_t * p_sess, + uint16_t * p_pos, + pf_ar_t * p_ar) +{ + int ret = 0; /* OK until error found. */ + uint16_t start_pos; + pf_block_header_t block_header; + uint16_t data_pos; + bool first_block = true; + + start_pos = *p_pos; + while ((ret == 0) && (p_sess->get_info.result == PF_PARSE_OK) && + ((*p_pos + sizeof (block_header)) <= p_sess->get_info.len)) + { + pf_get_block_header (&p_sess->get_info, p_pos, &block_header); + data_pos = *p_pos; + if ( + (p_sess->get_info.result == PF_PARSE_OK) && + (((*p_pos + block_header.block_length + 4) - sizeof (block_header)) <= + p_sess->get_info.len)) + { + switch (block_header.block_type) + { + case PF_BT_AR_BLOCK_REQ: + pf_get_ar_param (&p_sess->get_info, p_pos, p_ar); + + /* Must be first block */ + if (first_block == false) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + 0x00); + ret = -1; + } + else + { + ret = pf_check_block_header ( + 54 + (uint16_t)strlen ( + p_ar->ar_param.cm_initiator_station_name), + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_BLOCK_REQ, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_ar_param++; + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): AR type: \"%s\" Device access: %u Supervisor " + "takeover: %u Requested start up mode: \"%s\" Initiator " + "station name: \"%s\" UDP port: 0x%04x Session: %u " + "Timeout: " + "%u x 100 ms\n", + __LINE__, + pf_ar_type_to_string (p_ar->ar_param.ar_type), + p_ar->ar_param.ar_properties.device_access, + p_ar->ar_param.ar_properties.supervisor_takeover_allowed, + p_ar->ar_param.ar_properties.startup_mode ? "Advanced" + : "Legacy", + p_ar->ar_param.cm_initiator_station_name, + p_ar->ar_param.cm_initiator_udp_rt_port, + p_ar->ar_param.session_key, + p_ar->ar_param.cm_initiator_activity_timeout_factor); + } + } + break; + case PF_BT_IOCR_BLOCK_REQ: + if (p_ar->nbr_iocrs < PNET_MAX_CR) + { + /* Before it is incremented at the end of this block, the + p_ar->nbr_iocrs value is the same as CREP */ + pf_get_iocr_param ( + &p_sess->get_info, + p_pos, + p_ar->nbr_iocrs, + p_ar); + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Requested send cycle time: %u (in 1/32 of " + "millisec) " + "AREP %u CREP %u %6s Reduction ratio: %u Watchdog " + "factor: %" PRIu32 " Data hold factor: %u FrameID: 0x%04x\n", + __LINE__, + p_ar->iocrs[p_ar->nbr_iocrs].param.send_clock_factor, + p_ar->arep, + p_ar->nbr_iocrs, + pf_iocr_type_to_string ( + p_ar->iocrs[p_ar->nbr_iocrs].param.iocr_type), + p_ar->iocrs[p_ar->nbr_iocrs].param.reduction_ratio, + p_ar->iocrs[p_ar->nbr_iocrs].param.watchdog_factor, + p_ar->iocrs[p_ar->nbr_iocrs].param.data_hold_factor, + p_ar->iocrs[p_ar->nbr_iocrs].param.frame_id); + + /* Count the types for error discovery */ + if (p_ar->iocrs[p_ar->nbr_iocrs].param.iocr_type == PF_IOCR_TYPE_INPUT) + { + p_ar->input_cr_cnt++; + } + if ( + p_ar->iocrs[p_ar->nbr_iocrs].param.iocr_type == + PF_IOCR_TYPE_OUTPUT) + { + p_ar->output_cr_cnt++; + } + if ( + p_ar->iocrs[p_ar->nbr_iocrs].param.iocr_type == + PF_IOCR_TYPE_MC_CONSUMER) + { + p_ar->mcr_cons_cnt++; + } + if ( + p_ar->iocrs[p_ar->nbr_iocrs].param.iocr_properties.rt_class == + PF_RT_CLASS_3) + { + p_ar->rtc3_present = true; + } + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_IOCR_BLOCK_REQ, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_iocrs++; + } + } + } + else + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Too many CR given. Max %u CR per AR supported\n", + __LINE__, + PNET_MAX_CR); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_WRONG_BLOCK_COUNT); + ret = -1; + } + break; + case PF_BT_EXPECTED_SUBMODULE_BLOCK: + pf_get_exp_api_module (&p_sess->get_info, p_pos, p_ar); + switch (p_sess->get_info.result) + { + case PF_PARSE_OK: + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_EXP_BLOCK_REQ, + &p_sess->rpc_result); + /* nbr_exp_api is counted in pf_get_exp_api_module() */ + } + break; + case PF_PARSE_NULL_POINTER: + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + break; + case PF_PARSE_END_OF_INPUT: + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Out of expected API resources (end of input)\n", + __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_OUT_OF_MEMORY); + ret = -1; + break; + case PF_PARSE_OUT_OF_API_RESOURCES: + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Out of expected API resources\n", + __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_OUT_OF_MEMORY); + ret = -1; + break; + case PF_PARSE_OUT_OF_EXP_SUBMODULE_RESOURCES: + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Out of expected submodule resources\n", + __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_OUT_OF_MEMORY); + ret = -1; + break; + case PF_PARSE_ERROR: + /* Already handled */ + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + break; + default: + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Unhandled parser error %d\n", + __LINE__, + ret); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + break; + } + break; + case PF_BT_ALARM_CR_BLOCK_REQ: + pf_get_alarm_cr_request (&p_sess->get_info, p_pos, p_ar); + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_ALARM_BLOCK_REQ, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_alarm_cr++; + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Requested max alarm data length: %u bytes\n", + __LINE__, + p_ar->alarm_cr_request.max_alarm_data_length); + } + } + break; + +#if PNET_OPTION_PARAMETER_SERVER + case PF_BT_PRM_SERVER_REQ: + pf_get_ar_prm_server_request (&p_sess->get_info, p_pos, p_ar); + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_PRM_SERVER_BLOCK_REQ, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_prm_server++; + } + } + break; +#endif +#if PNET_OPTION_MC_CR + case PF_BT_MCR_REQ: + pf_get_mcr_request (&p_sess->get_info, p_pos, p_ar->nbr_mcr, p_ar); + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_MCR_BLOCK_REQ, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_mcr++; + } + } + break; +#endif + case PF_BT_RPC_SERVER_REQ: + pf_get_ar_rpc_request (&p_sess->get_info, p_pos, p_ar); + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_AR_RPC_BLOCK_REQ, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_rpc_server++; + } + } + break; +#if PNET_OPTION_AR_VENDOR_BLOCKS + case PF_BT_AR_VENDOR_BLOCK_REQ: + pf_get_ar_vendor_request ( + &p_sess->get_info, + p_pos, + p_ar->nbr_ar_vendor, + p_ar); + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_FAULTY_RECORD, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_ar_vendor++; + } + } + break; +#endif +#if PNET_OPTION_IR + case PF_BT_IR_INFO_BLOCK_REQ: + pf_get_ir_info_request (&p_sess->get_info, p_pos, p_ar); + + if (p_ar->ar_param.ar_properties.device_access == true) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + else + { + ret = pf_check_block_header ( + (*p_pos - data_pos) + 2, + &block_header, + PNET_ERROR_CODE_1_CONN_FAULTY_IR_INFO, + &p_sess->rpc_result); + if (ret == 0) + { + p_ar->nbr_ir_info++; + } + } + break; +#endif + default: + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Unknown block type %u\n", + __LINE__, + block_header.block_type); + ret = -1; + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + break; + } + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Unexpected end of input\n", + __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + ret = -1; + } + first_block = false; + } + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Connect request num AR param: %u CR: %u Input: %u Output: %u " + "Alarm: %u Result: %s\n", + __LINE__, + p_ar->nbr_ar_param, + p_ar->nbr_iocrs, + p_ar->input_cr_cnt, + p_ar->output_cr_cnt, + p_ar->nbr_alarm_cr, + (ret == 0) ? "OK" : "Error"); + + if (ret == 0) + { + /* Error discovery. */ + if (p_sess->get_info.result == PF_PARSE_OK) + { + if (*p_pos != p_sess->get_info.len) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if (((uint32_t)*p_pos - start_pos) != p_sess->ndr_data.args_length) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): args_length %" PRIu32 " != request length %" PRIu32 + "\n", + __LINE__, + p_sess->ndr_data.args_length, + (uint32_t)(*p_pos - start_pos)); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + else + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Create all blocks in a connect response (inside an existing DCE/RPC buffer). + * @param p_sess InOut: The RPC session instance. + * @param ret In: result of the connect request. + * @param p_ar In: The AR instance. + * @param res_size In: Size of the response buffer. + * @param p_res InOut: Response buffer. + * @param p_res_pos InOut: Position in the response buffer. + */ +static void pf_cmrpc_rm_connect_rsp ( + pf_session_info_t * p_sess, + int ret, + const pf_ar_t * p_ar, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos) +{ + uint16_t ix; + uint16_t hdr_pos; + uint16_t start_pos; + + pf_put_pnet_status ( + p_sess->get_info.is_big_endian, + &p_sess->rpc_result.pnio_status, + res_size, + p_res, + p_res_pos); + + hdr_pos = *p_res_pos; /* Save for last */ + /* Insert the response header with dummy length and actual_count */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + p_res_pos); + + start_pos = *p_res_pos; /* Start of blocks - save for last */ + + /** + * ARBlockRes, {[IOCRBlockRes*], [AlarmCRBlockRes], [ModuleDiffBlock], + * [ARRPCBlockRes](a)} + * + * Special case "PF_ART_IOCAR_SINGLE" or "PF_ART_IOSAR": + * ARBlockRes, {IOCRBlockRes*, AlarmCRBlockRes, [ModuleDiffBlock], + * [ARRPCBlockRes](a)} + * + * Special case "PF_ART_IOSAR" with ar_properties.device_access = + * PF_DA_DEVICE_CONTEXT: ARBlockRes + * + * (a) Only if request contains an ARRPCBlockReq + */ + + if ((ret == 0) && (p_ar != NULL)) + { + pf_put_ar_result ( + p_sess->get_info.is_big_endian, + p_ar, + res_size, + p_res, + p_res_pos); + + for (ix = 0; ix < p_ar->nbr_iocrs; ix++) + { + pf_put_iocr_result ( + p_sess->get_info.is_big_endian, + p_ar, + ix, + res_size, + p_res, + p_res_pos); + } + + pf_put_alarm_cr_result ( + p_sess->get_info.is_big_endian, + p_ar, + res_size, + p_res, + p_res_pos); + + if (p_ar->nbr_api_diffs > 0) + { + pf_put_ar_diff ( + p_sess->get_info.is_big_endian, + p_ar, + res_size, + p_res, + p_res_pos); + } + + /* Only if RPC server request */ + if (p_ar->ar_rpc_request.valid) + { + pf_put_ar_rpc_result ( + p_sess->get_info.is_big_endian, + p_ar, + res_size, + p_res, + p_res_pos); + } + + if (p_ar->ar_param.ar_properties.startup_mode) + { + /** + * Additional blocks in advanced start mode: + * {ARServerBlockRes, [ARVendorBlockRes*](b)} + * + * (b) Equal to number of ARVendorBlockReq in request. + */ + + pf_put_ar_server_result ( + p_sess->get_info.is_big_endian, + p_ar, + res_size, + p_res, + p_res_pos); + +#if PNET_OPTION_AR_VENDOR_BLOCKS + for (ix = 0; ix < p_ar->nbr_ar_vendor; ix++) + { + pf_put_ar_vendor_result ( + p_sess->get_info.is_big_endian, + p_ar, + ix, + res_size, + p_res, + p_res_pos); + } +#endif + } + } + + /* Fixup the header with correct length info. */ + p_sess->ndr_data.args_length = *p_res_pos - start_pos; + p_sess->ndr_data.array.actual_count = *p_res_pos - start_pos; + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Connect response args_length = %" PRIu32 "\n", + __LINE__, + p_sess->ndr_data.args_length); + + /* Over-write the response header with correct length and actual_count. */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + &hdr_pos); +} + +/** + * @internal + * Take a DCE RPC connect request and create a DCE RPC connect response. + * + * This will trigger these user callbacks: + * + * * pnet_exp_module_ind() + * * pnet_exp_submodule_ind() + * * pnet_connect_ind() + * * pnet_state_ind() with PNET_EVENT_STARTUP + * + * Creates an AR, and populates the AR-to-session (and back) references. + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The session instance. Will be released on + * error. The rpc_result field is written + * when return != 0. + * @param req_pos In: Position in the request buffer. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. + * @param p_res_pos InOut: Position within the response buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_connect_ind ( + pnet_t * net, + pf_session_info_t * p_sess, + uint16_t req_pos, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos) +{ + int ret = -1; + pf_ar_t * p_ar = NULL; + pf_ar_t * p_ar_2 = NULL; /* When looking for duplicate */ + + if (p_sess->rpc_result.pnio_status.error_code != PNET_ERROR_CODE_NOERROR) + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): RPC request has error\n", __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + } + /* CheckResource */ + else if (pf_ar_allocate (net, &p_ar) == 0) + { + /* Cross-reference */ + p_ar->p_sess = p_sess; + p_sess->p_ar = p_ar; + + /* Parse the Connect request - No support for ArSet (yet) */ + if (pf_cmrpc_rm_connect_interpret_ind (p_sess, &req_pos, p_ar) == 0) + { + if (pf_ar_find_by_uuid (net, &p_ar->ar_param.ar_uuid, &p_ar_2) != 0) + { + /* Valid, unknown AR, NoArSet */ + if (p_ar->arep > 1) + { + /* We only support one AR. + However in order to respond with the correct error code, we + need to know whether a new AR has the same ARUUID as the + previous AR. Thus we need to parse the incoming connect + message, and for that to be possible PNET_MAX_AR must be at + least 2. This is checked by ART Tester. */ + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Only one connection (AR) supported! AREP %u\n", + __LINE__, + p_ar->arep); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_NO_AR_RESOURCES); + } + else + { + p_ar->ar_state = PF_AR_STATE_PRIMARY; + p_ar->sync_state = PF_SYNC_STATE_NOT_AVAILABLE; + + ret = pf_cmdev_rm_connect_ind (net, p_ar, &p_sess->rpc_result); + } + } + else + { + /* Valid, explicit AR - This could be a re-connect */ + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Duplicate Connect request received\n", + __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMDEV, + PNET_ERROR_CODE_2_CMDEV_STATE_CONFLICT); + (void)pf_cmdev_cm_abort (net, p_ar_2); + } + } + else + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Failed to parse incoming connect request\n", + __LINE__); + /* Invalid - ret already set */ + } + } + else + { + /* unavailable */ + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): Out of AR resources (AR)\n", __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_NO_AR_RESOURCES); + } + + /* Create Connect response */ + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Create CONNECT response: ret = %d error: 0x%02X 0x%02X " + "0x%02X 0x%02X\n", + __LINE__, + ret, + p_sess->rpc_result.pnio_status.error_code, + p_sess->rpc_result.pnio_status.error_decode, + p_sess->rpc_result.pnio_status.error_code_1, + p_sess->rpc_result.pnio_status.error_code_2); + pf_cmrpc_rm_connect_rsp (p_sess, ret, p_ar, res_size, p_res, p_res_pos); + + if ( + (ret != 0) || + (p_sess->rpc_result.pnio_status.error_code != PNET_ERROR_CODE_NOERROR)) + { + /* Connect failed: Cleanup and signal to terminate the session. */ + LOG_INFO (PF_RPC_LOG, "CMRPC(%d): Connect failed - Free AR!\n", __LINE__); + pf_ar_release (p_ar); + p_sess->p_ar = NULL; + p_sess->kill_session = true; + } + else + { + pf_pdport_lldp_restart_transmission (net); + } + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Created connect response: ret %d\n", + __LINE__, + ret); + + return ret; +} + +/** + * @internal + * Parse all blocks in a release RPC request. + * @param p_sess InOut: The RPC session instance. The rpc_result + * field is written when return != 0. + * @param req_pos In: Position in the request buffer. + * @param p_release_io Out: The release control block. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_release_interpret_req ( + pf_session_info_t * p_sess, + uint16_t req_pos, + pf_control_block_t * p_release_io) +{ + int ret = 0; + uint16_t start_pos; + pf_block_header_t block_header; + + start_pos = req_pos; + while (((req_pos + sizeof (block_header)) <= p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + pf_get_block_header (&p_sess->get_info, &req_pos, &block_header); + if ( + (((req_pos + block_header.block_length + 4) - sizeof (block_header)) <= + p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + switch (block_header.block_type) + { + case PF_BT_RELEASE_BLOCK_REQ: + pf_get_control (&p_sess->get_info, &req_pos, p_release_io); + break; + default: + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Unknown block type %u\n", + __LINE__, + block_header.block_type); + ret = -1; + break; + } + } + } + + if (ret == 0) + { + if (p_sess->get_info.result == PF_PARSE_OK) + { + /* Error discovery. */ + if (req_pos != p_sess->get_info.len) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_RELEASE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if ((uint32_t)(req_pos - start_pos) != p_sess->ndr_data.args_length) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): args_length %" PRIu32 " != request length %" PRIu32 + "\n", + __LINE__, + p_sess->ndr_data.args_length, + (uint32_t)(req_pos - start_pos)); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_RELEASE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if ( + p_release_io->control_command != + BIT (PF_CONTROL_COMMAND_BIT_RELEASE)) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Release ControlCommand has wrong bit pattern: " + "%04x\n", + __LINE__, + p_release_io->control_command); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_RELEASE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + ret = -1; + } + } + else + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_RELEASE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Create all blocks in a release RPC response (inside an existing DCE/RPC + * buffer). + * @param p_sess InOut: The RPC session instance. + * @param p_release_io InOut: The release control block. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. Already filled with + * DCE/RPC header. + * @param p_res_pos InOut: Position within the response buffer. + * @param p_status_pos Out: Position of the status within the response + * buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static void pf_cmrpc_rm_release_rsp ( + pf_session_info_t * p_sess, + pf_control_block_t * p_release_io, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos, + uint16_t * p_status_pos) +{ + uint16_t hdr_pos; + uint16_t start_pos; + + *p_status_pos = *p_res_pos; + pf_put_pnet_status ( + p_sess->get_info.is_big_endian, + &p_sess->rpc_result.pnio_status, + res_size, + p_res, + p_res_pos); + + hdr_pos = *p_res_pos; /* Save for last. */ + /* Insert the response header with dummy length and actual_count. */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + p_res_pos); + + start_pos = *p_res_pos; /* Start of blocks - save for last */ + + if (p_sess->rpc_result.pnio_status.error_code == PNET_ERROR_CODE_NOERROR) + { + p_release_io->control_command = BIT (PF_CONTROL_COMMAND_BIT_DONE); + pf_put_control ( + p_sess->get_info.is_big_endian, + PF_BT_RELEASE_BLOCK_RES, + p_release_io, + res_size, + p_res, + p_res_pos); + } + + /* Fixup the header with correct length info. */ + p_sess->ndr_data.args_length = *p_res_pos - start_pos; + p_sess->ndr_data.array.actual_count = *p_res_pos - start_pos; + + /* Over-write the response header with correct length and actual_count. */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + &hdr_pos); +} + +/** + * @internal + * Take a DCE RPC release request and create a DCE RPC release response. + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The RPC session instance. The rpc_result + * field is written when return != 0. + * @param req_pos In: Position in the request buffer. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. Already filled with + * DCE/RPC header. + * @param p_res_pos InOut: Position within the response buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_release_ind ( + pnet_t * net, + pf_session_info_t * p_sess, + uint16_t req_pos, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos) +{ + int ret = -1; + pf_control_block_t release_io; + pf_ar_t * p_ar = NULL; + uint16_t status_pos; + uint16_t start_pos; + pnet_result_t rpc_result; + bool is_big_endian; + + memset (&release_io, 0, sizeof (release_io)); + memset (&rpc_result, 0, sizeof (rpc_result)); + + /* Save things for creating the response (incl. status_pos, below...) */ + is_big_endian = p_sess->get_info.is_big_endian; + + /* Create a positive response in case all goes well. */ + start_pos = *p_res_pos; + pf_cmrpc_rm_release_rsp ( + p_sess, + &release_io, + res_size, + p_res, + p_res_pos, + &status_pos); + if (p_sess->rpc_result.pnio_status.error_code != PNET_ERROR_CODE_NOERROR) + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): RPC request has error\n", __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_RELEASE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + } + else + { + ret = pf_cmrpc_rm_release_interpret_req (p_sess, req_pos, &release_io); + } + + rpc_result = p_sess->rpc_result; + + if (ret == 0) + { + /* Check_RPC */ + if (pf_ar_find_by_uuid (net, &release_io.ar_uuid, &p_ar) == 0) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Releasing AR with AREP %u and session key %u. Incoming " + "session key %u\n", + __LINE__, + p_ar->arep, + p_ar->ar_param.session_key, + release_io.session_key); + /* Overwrite response with correct AR UUID etc */ + pf_cmrpc_rm_release_rsp ( + p_sess, + &release_io, + res_size, + p_res, + &start_pos, + &status_pos); + + (void)pf_cmdev_rm_release_ind (net, p_ar, &rpc_result); + } + else + { + pf_set_error ( + &rpc_result, + PNET_ERROR_CODE_RELEASE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_AR_UUID_UNKNOWN); + p_sess->kill_session = true; + } + } + + /* Now insert the actual status */ + pf_put_pnet_status ( + is_big_endian, + &rpc_result.pnio_status, + res_size, + p_res, + &status_pos); + + return ret; +} + +/** + * @internal + * Parse all blocks in a DControl RPC request message. + * @param p_sess InOut: The RPC session instance. The rpc_result + * field is written when return != 0. + * @param req_pos In: Position in the request buffer. + * @param p_control_io Out: The DControl control block. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_dcontrol_interpret_req ( + pf_session_info_t * p_sess, + uint16_t req_pos, + pf_control_block_t * p_control_io) +{ + int ret = 0; + uint16_t start_pos; + pf_block_header_t block_header; + + CC_ASSERT (p_sess != NULL); + + start_pos = req_pos; + while (((req_pos + sizeof (block_header)) <= p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + pf_get_block_header (&p_sess->get_info, &req_pos, &block_header); + if ( + (((req_pos + block_header.block_length + 4) - sizeof (block_header)) <= + p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + switch (block_header.block_type) + { + case PF_BT_PRMBEGIN_REQ: + case PF_BT_PRMEND_REQ: + pf_get_control (&p_sess->get_info, &req_pos, p_control_io); + break; + default: + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Unknown block type %u\n", + __LINE__, + block_header.block_type); + ret = -1; + break; + } + } + } + + if (ret == 0) + { + if (p_sess->get_info.result == PF_PARSE_OK) + { + /* Error discovery. */ + if (req_pos != p_sess->get_info.len) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if ((uint32_t)(req_pos - start_pos) != p_sess->ndr_data.args_length) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): args_length %" PRIu32 " != request length %" PRIu32 + "\n", + __LINE__, + p_sess->ndr_data.args_length, + (uint32_t)(req_pos - start_pos)); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if ( + p_control_io->control_command != + BIT (PF_CONTROL_COMMAND_BIT_PRM_END)) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): DControl ControlCommand has wrong bit pattern: " + "%04x\n", + __LINE__, + p_control_io->control_command); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_DCTRL_FAULTY_CONNECT, + PNET_ERROR_CODE_2_DCTRL_FAULTY_CONNECT_CONTROLCOMMAND); + ret = -1; + } + } + else + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Create all blocks in a DControl RPC response (inside an existing DCE/RPC + * buffer). + * @param p_sess InOut: The RPC session instance. + * @param ret In: Result of the release operation. + * @param p_control_io InOut: The DControl control block. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. + * @param p_pos InOut: Position within the response buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static void pf_cmrpc_rm_dcontrol_rsp ( + pf_session_info_t * p_sess, + int ret, + pf_control_block_t * p_control_io, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos) +{ + uint16_t hdr_pos; + uint16_t start_pos; + + pf_put_pnet_status ( + p_sess->get_info.is_big_endian, + &p_sess->rpc_result.pnio_status, + res_size, + p_res, + p_res_pos); + + hdr_pos = *p_res_pos; /* Save for last */ + /* Insert the response header with dummy length and actual_count */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + p_res_pos); + + start_pos = *p_res_pos; /* Start of blocks - save for last */ + + if (p_sess->rpc_result.pnio_status.error_code == PNET_ERROR_CODE_NOERROR) + { + p_control_io->control_command = BIT (PF_CONTROL_COMMAND_BIT_DONE); + pf_put_control ( + p_sess->get_info.is_big_endian, + PF_BT_PRMEND_RES, + p_control_io, + res_size, + p_res, + p_res_pos); + } + + /* Fixup the header with correct length info. */ + p_sess->ndr_data.args_length = *p_res_pos - start_pos; + p_sess->ndr_data.array.actual_count = *p_res_pos - start_pos; + + /* Over-write the response header with correct length and actual_count. */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + &hdr_pos); +} + +/** + * @internal + * Take a DCE RPC DControl request and create a DCE RPC DControl response. + * + * It triggers these user callbacks: + * + * * pnet_dcontrol_ind() with PNET_CONTROL_COMMAND_PRM_END + * * pnet_state_ind() with PNET_EVENT_PRMEND + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The RPC session instance. The rpc_result + * field is written when return != 0. + * @param p_rpc In: The RPC header. + * @param req_pos In: Position in the request buffer. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. + * @param p_res_pos InOut: Position within the response buffer. + * @param p_set_state_prmend Out: Set state to PNET_EVENT_PRMEND after the + * response have been sent. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_dcontrol_ind ( + pnet_t * net, + pf_session_info_t * p_sess, + const pf_rpc_header_t * p_rpc, + uint16_t req_pos, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos, + bool * p_set_state_prmend) +{ + int ret = -1; + pf_control_block_t control_io; + pf_ar_t * p_ar = NULL; + + memset (&control_io, 0, sizeof (control_io)); + + if (pf_cmrpc_rm_dcontrol_interpret_req (p_sess, req_pos, &control_io) == 0) + { + /* Detect re-run */ + if (p_sess->dcontrol_sequence_nmb != p_rpc->sequence_nmb) + { + if (pf_ar_find_by_uuid (net, &control_io.ar_uuid, &p_ar) == 0) + { + if ( + pf_cmdev_rm_dcontrol_ind ( + net, + p_ar, + &control_io, + &p_sess->rpc_result, + p_set_state_prmend) == 0) + { + ret = pf_cmpbe_rm_dcontrol_ind ( + net, + p_ar, + &control_io, + &p_sess->rpc_result); + } + } + else + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_AR_UUID_UNKNOWN); + p_sess->kill_session = true; + } + + /* Store dcontrol result if we later detect a re-run */ + p_sess->dcontrol_result = p_sess->rpc_result; + p_sess->dcontrol_sequence_nmb = p_rpc->sequence_nmb; + } + else + { + /* Re-send the previous answer */ + p_sess->rpc_result = p_sess->dcontrol_result; + } + } + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Prepare DCONTROL response message: ret = %d " + "error: 0x%02X 0x%02X 0x%02X 0x%02X\n", + __LINE__, + ret, + p_sess->rpc_result.pnio_status.error_code, + p_sess->rpc_result.pnio_status.error_decode, + p_sess->rpc_result.pnio_status.error_code_1, + p_sess->rpc_result.pnio_status.error_code_2); + + pf_cmrpc_rm_dcontrol_rsp ( + p_sess, + ret, + &control_io, + res_size, + p_res, + p_res_pos); + + return ret; +} + +/** + * @internal + * Parse all blocks in a IODRead RPC request message. + * @param p_sess InOut: The RPC session instance. The rpc_result + * field is written when return != 0. + * @param req_pos In: Position in the request buffer. + * @param p_read_request Out: The IODRead request block. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_read_interpret_ind ( + pf_session_info_t * p_sess, + uint16_t req_pos, + pf_iod_read_request_t * p_read_request) +{ + int ret = -1; + uint16_t start_pos; + pf_block_header_t block_header; + + start_pos = req_pos; + if ( + ((req_pos + sizeof (block_header)) <= p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + pf_get_block_header (&p_sess->get_info, &req_pos, &block_header); + if ( + (((req_pos + block_header.block_length + 4) - sizeof (block_header)) <= + p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + switch (block_header.block_type) + { + case PF_BT_IOD_READ_REQ_HEADER: + pf_get_read_request (&p_sess->get_info, &req_pos, p_read_request); + ret = 0; + break; + default: + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_FAULTY_RECORD, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Unknown block type %u\n", + __LINE__, + block_header.block_type); + break; + } + } + } + + if (ret == 0) + { + if (p_sess->get_info.result == PF_PARSE_OK) + { + /* Error discovery. */ + if (req_pos != p_sess->get_info.len) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if ((uint32_t)(req_pos - start_pos) != p_sess->ndr_data.args_length) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): args_length %" PRIu32 " != request length %" PRIu32 + "\n", + __LINE__, + p_sess->ndr_data.args_length, + (uint32_t)(req_pos - start_pos)); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + else + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Take a DCE RPC IODRead request and create a DCE RPC IODRead response. + * Also handles Read Implicit requests (and creates responses). + * + * This will trigger the \a pnet_read_ind() user callback for certain + * parameters. + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The session instance. The rpc_result + * field is written when return != 0. + * @param opnum In: The RPC operation number. Should be + * PF_RPC_DEV_OPNUM_READ or + * PF_RPC_DEV_OPNUM_READ_IMPLICIT. + * @param req_pos In: Position in the request buffer. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. + * @param p_res_pos InOut: Position within the response buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_read_ind ( + pnet_t * net, + pf_session_info_t * p_sess, + uint16_t opnum, + uint16_t req_pos, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos) +{ + int ret = -1; + pf_iod_read_request_t read_request; + pf_ar_t * p_ar = NULL; /* Assume the implicit AR */ + uint16_t status_pos; + uint16_t hdr_pos; + uint16_t start_pos; + pf_api_t * p_api = NULL; + + memset (&read_request, 0, sizeof (read_request)); + + if (p_sess->rpc_result.pnio_status.error_code != PNET_ERROR_CODE_NOERROR) + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): RPC request has error\n", __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + } + else if (pf_cmrpc_rm_read_interpret_ind (p_sess, req_pos, &read_request) == 0) + { + if ( + (memcmp (&read_request.ar_uuid, &implicit_ar, sizeof (implicit_ar)) == + 0) && + (opnum != PF_RPC_DEV_OPNUM_READ_IMPLICIT)) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_FAULTY_RECORD, + PNET_ERROR_CODE_2_CMRPC_AR_UUID_UNKNOWN); + p_sess->kill_session = true; + } + else if ( + (pf_ar_find_by_uuid (net, &read_request.ar_uuid, &p_ar) == 0) && + (opnum != PF_RPC_DEV_OPNUM_READ)) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_AR_UUID_UNKNOWN); + p_sess->kill_session = true; + } + else if ( + (read_request.api != 0) && + (pf_cmdev_get_api (net, read_request.api, &p_api) != 0)) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIORW, + PNET_ERROR_CODE_1_ACC_INVALID_AREA_API, + 6); + } + else + { + /* Note that for "read implicit" we have no AR */ + if ((p_ar == NULL) && (opnum != PF_RPC_DEV_OPNUM_READ_IMPLICIT)) + { + /* In case an AR is needed try to get the target AR */ + pf_ar_find_by_uuid (net, &read_request.target_ar_uuid, &p_ar); + } + /* + * Read requests are special: There are so many different formats that + * we will let the read functions read directly from the request buffer + * and create their responses directly into the response buffer. + */ + status_pos = *p_res_pos; /* Save for last. */ + pf_put_pnet_status ( + p_sess->get_info.is_big_endian, + &p_sess->rpc_result.pnio_status, + res_size, + p_res, + p_res_pos); + + hdr_pos = *p_res_pos; /* Save for last. */ + /* Insert the response header with dummy length and actual_count. */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + p_res_pos); + + start_pos = *p_res_pos; /* Start of blocks - save for last */ + + /* Do actual reading */ + if ( + pf_cmrdr_rm_read_ind ( + net, + p_ar, + &read_request, + &p_sess->rpc_result, + res_size, + p_res, + p_res_pos) == 0) + { + ret = pf_cmsm_rm_read_ind (net, p_ar, &read_request); + } + else + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Error from pf_cmrdr_rm_read_ind\n", + __LINE__); + } + + /* Insert the actual operation result */ + pf_put_pnet_status ( + p_sess->get_info.is_big_endian, + &p_sess->rpc_result.pnio_status, + res_size, + p_res, + &status_pos); + + /* Fixup the header with correct length info. */ + p_sess->ndr_data.args_length = *p_res_pos - start_pos; + p_sess->ndr_data.array.actual_count = *p_res_pos - start_pos; + + /* Over-write the response header with correct length and actual_count. + */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + &hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + &hdr_pos); + } + } + + return ret; +} + +/** + * @internal + * Take a DCE RPC EPM LOOKUP request and create a response. + * No callbacks or updates of configuration is triggered + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The session instance. The rpc_result + * field is written when return != 0. + * @param p_rpc_req In: RPC Request + * @param req_pos In: Position in the request buffer. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. + * @param p_res_pos InOut: Position within the response buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_lookup_ind ( + pnet_t * net, + pf_session_info_t * p_sess, + const pf_rpc_header_t * p_rpc_req, + uint16_t req_pos, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos) +{ + int ret = -1; + pf_rpc_lookup_req_t lookup_request; + uint16_t p_pos = req_pos; + + memset (&lookup_request, 0, sizeof (lookup_request)); + + if (p_sess->rpc_result.pnio_status.error_code != PNET_ERROR_CODE_NOERROR) + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): RPC request has error\n", __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_READ, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + } + else + { + pf_get_epm_lookup_request (&p_sess->get_info, &p_pos, &lookup_request); + lookup_request.udpPort = p_sess->port; + ret = pf_cmrpc_lookup_request ( + net, + p_rpc_req, + &lookup_request, + &p_sess->rpc_result, + res_size, + p_res, + p_res_pos); + } + + return ret; +} + +/** + * @internal + * Parse one block in a IODWrite RPC request message. + * @param p_get_info InOut: Parser data for the input buffer. + * @param p_write_request Out: The IODWrite request block. + * @param p_pos InOut: Position in the input buffer. + * @param p_stat Out: Detailed error information if returning != 0 + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_write_interpret_ind ( + pf_get_info_t * p_get_info, + pf_iod_write_request_t * p_write_request, + uint16_t * p_pos, + pnet_result_t * p_stat) +{ + int ret = -1; + pf_block_header_t block_header; + + if ((*p_pos + sizeof (block_header)) <= p_get_info->len) + { + pf_get_block_header (p_get_info, p_pos, &block_header); + if ( + (((*p_pos + block_header.block_length + 4) - sizeof (block_header)) <= + p_get_info->len) && + (p_get_info->result == PF_PARSE_OK)) + { + switch (block_header.block_type) + { + case PF_BT_IOD_WRITE_REQ_HEADER: + pf_get_write_request (p_get_info, p_pos, p_write_request); + ret = 0; + break; + case PF_BT_INTERFACE_ADJUST: + ret = 0; + break; + default: + pf_set_error ( + p_stat, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CONN_FAULTY_FAULTY_RECORD, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Unknown block type %u\n", + __LINE__, + block_header.block_type); + break; + } + } + } + + return ret; +} + +/** + * @internal + * Perform write of one data record. + * + * Triggers the \a pnet_write_ind() user callback for some values. + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The session instance. + * @param p_write_request In: The IODWrite request block. + * @param p_write_result Out: The IODWrite result block. + * @param p_stat Out: Detailed error information if returning != 0 + * @param p_req_pos InOut: Position in the request buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_perform_one_write ( + pnet_t * net, + pf_session_info_t * p_sess, + const pf_iod_write_request_t * p_write_request, + pf_iod_write_result_t * p_write_result, + pnet_result_t * p_stat, + uint16_t * p_req_pos) +{ + int ret = -1; + pf_ar_t * p_ar = NULL; + pf_block_header_t block_header; + pf_api_t * p_api = NULL; + + if (pf_ar_find_by_uuid (net, &p_write_request->ar_uuid, &p_ar) != 0) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_AR_UUID_UNKNOWN); + p_sess->kill_session = true; + } + else if ( + (p_write_request->api != 0) && + (pf_cmdev_get_api (net, p_write_request->api, &p_api) != 0)) + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIORW, + PNET_ERROR_CODE_1_ACC_INVALID_AREA_API, + 1); + } + else if ( + (*p_req_pos + p_write_request->record_data_length) <= + p_sess->get_info.len) + { + if (p_write_request->index <= PF_IDX_USER_MAX) + { + /* This is a write of a user defined index. No block header in this + * case. */ + if ( + pf_cmwrr_rm_write_ind ( + net, + p_ar, + p_write_request, + p_write_result, + p_stat, + p_sess->get_info.p_buf, + p_write_request->record_data_length, + p_req_pos) == 0) + { + ret = 0; + } + else + { + pf_set_error_if_not_already_set ( + p_stat, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIORW, + PNET_ERROR_CODE_1_ACC_INVALID_AREA_API, + 2); + } + } + else + { + /* + * Write requests are special: There are so many different block types + * that we will let CMWRR read data (sans block header) directly from + * the request buffer. + * + * Skip the block header - it is not needed in the stack but + * advances position in the buffer. + */ + pf_get_block_header (&p_sess->get_info, p_req_pos, &block_header); + if ( + pf_cmwrr_rm_write_ind ( + net, + p_ar, + p_write_request, + p_write_result, + p_stat, + p_sess->get_info.p_buf, + p_write_request->record_data_length - sizeof (pf_block_header_t), + p_req_pos) == 0) + { + ret = 0; + } + else + { + pf_set_error_if_not_already_set ( + p_stat, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIORW, + PNET_ERROR_CODE_1_ACC_INVALID_AREA_API, + 2); + } + } + } + else + { + pf_set_error ( + p_stat, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIORW, + PNET_ERROR_CODE_1_ACC_INVALID_AREA_API, + 4); + } + + if (ret != 0) + { + p_write_result->pnio_status = p_stat->pnio_status; + } + + return ret; +} + +/** + * @internal + * Take a DCE RPC IODWrite request and create a DCE RPC IODWrite response. + * + * This will trigger the \a pnet_write_ind() user callback for certain + * parameters. + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The RPC session instance. The rpc_result + * field is written when return != 0. + * @param req_pos In: Position in the request buffer. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. + * @param p_res_pos InOut: Position within the response buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_write_ind ( + pnet_t * net, + pf_session_info_t * p_sess, + uint16_t req_pos, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos) +{ + int ret = -1; + pf_iod_write_request_t write_request; + pf_iod_write_result_t write_result; + pf_iod_write_request_t write_request_multi; + pf_iod_write_result_t write_result_multi; + pnet_result_t write_stat_multi; + uint16_t req_start_pos; + uint16_t res_hdr_pos; + uint16_t res_start_pos; + uint16_t res_status_pos; + + memset (&write_request, 0, sizeof (write_request)); + memset (&write_result, 0, sizeof (write_result)); + memset (&write_request_multi, 0, sizeof (write_request_multi)); + memset (&write_result_multi, 0, sizeof (write_result_multi)); + memset (&write_stat_multi, 0, sizeof (write_stat_multi)); + + /* + * Read the first request block. + * It might be the only one, but that will be dealt with below. + */ + req_start_pos = req_pos; /* Start of request blocks */ + ret = pf_cmrpc_rm_write_interpret_ind ( + &p_sess->get_info, + &write_request, + &req_pos, + &p_sess->rpc_result); + + /* + * Prepare the response buffer by writing dummy data and recording their + * positions. This allows us to insert the actual values later. + */ + + res_status_pos = *p_res_pos; /* Save for last. */ + pf_put_pnet_status ( + p_sess->get_info.is_big_endian, + &p_sess->rpc_result.pnio_status, + res_size, + p_res, + p_res_pos); + + res_hdr_pos = *p_res_pos; /* Save for last. */ + /* Insert the response NDR header with dummy values. */ + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + p_res_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + p_res_pos); + + write_result.sequence_number = write_request.sequence_number; + write_result.ar_uuid = write_request.ar_uuid; + write_result.api = write_request.api; + write_result.slot_number = write_request.slot_number; + write_result.subslot_number = write_request.subslot_number; + write_result.index = write_request.index; + write_result.record_data_length = 0; + write_result.pnio_status = p_sess->rpc_result.pnio_status; + + res_start_pos = *p_res_pos; /* Save for last. */ + + if (p_sess->get_info.result == PF_PARSE_OK) + { + /* + * get request header + * if (index == PF_IDX_AR_WRITE_MULTIPLE) + * while (more data available) + * get request header + * perform write + * write record_data_write + * else + * perform write + * write record_data_write + */ + if (ret == 0) + { + if (write_request.index == PF_IDX_AR_WRITE_MULTIPLE) /* Handle + multi-write */ + { + /* Store the first response block, that indicates that it is a + * multiple write */ + pf_put_write_result ( + p_sess->get_info.is_big_endian, + &write_result, + res_size, + p_res, + p_res_pos); + + /* Do each write, and store the corresponding response blocks */ + memset (&write_result_multi, 0, sizeof (write_result_multi)); + while (((req_pos + 58) < + p_sess->get_info.len) && /* 58 == + sizeof((PACKED)write_request)+2 + */ + (write_result_multi.pnio_status.error_code == + PNET_ERROR_CODE_NOERROR) && + (pf_cmrpc_rm_write_interpret_ind ( + &p_sess->get_info, + &write_request_multi, + &req_pos, + &write_stat_multi) == 0)) + { + ret = pf_cmrpc_perform_one_write ( + net, + p_sess, + &write_request_multi, + &write_result_multi, + &write_stat_multi, + &req_pos); + pf_put_write_result ( + p_sess->get_info.is_big_endian, + &write_result_multi, + res_size, + p_res, + p_res_pos); + + /* Align on 32-bits to point to next write request */ + while ((req_pos % sizeof (uint32_t)) != 0) + { + req_pos++; + } + /* Prepare for next block*/ + memset (&write_stat_multi, 0, sizeof (write_stat_multi)); + } + if (req_pos != p_sess->get_info.len) + { + ret = -1; + } + } + else /* single write */ + { + /* Do the write, and store the corresponding response block */ + ret = pf_cmrpc_perform_one_write ( + net, + p_sess, + &write_request, + &write_result, + &p_sess->rpc_result, + &req_pos); + pf_put_write_result ( + p_sess->get_info.is_big_endian, + &write_result, + res_size, + p_res, + p_res_pos); + } + } + + if (ret == 0) + { + if (p_sess->get_info.result == PF_PARSE_OK) + { + if ((uint32_t)(req_pos - req_start_pos) != p_sess->ndr_data.args_length) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): args_length %u != request length %u\n", + __LINE__, + (unsigned)p_sess->ndr_data.args_length, + (unsigned)(req_pos - req_start_pos)); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + else + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): parse result = %u\n", + __LINE__, + p_sess->get_info.result); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_WRITE, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + } + + /* Fixup the NDR header with correct length info. */ + /* For responses the actual_count = args_length. Offset is always zero. */ + p_sess->ndr_data.args_length = *p_res_pos - res_start_pos; + p_sess->ndr_data.array.actual_count = p_sess->ndr_data.args_length; + + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.args_length, + res_size, + p_res, + &res_hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.maximum_count, + res_size, + p_res, + &res_hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.offset, + res_size, + p_res, + &res_hdr_pos); + pf_put_uint32 ( + p_sess->get_info.is_big_endian, + p_sess->ndr_data.array.actual_count, + res_size, + p_res, + &res_hdr_pos); + + /* Insert the actual result of the write operation into the first result + * block */ + pf_put_pnet_status ( + p_sess->get_info.is_big_endian, + &p_sess->rpc_result.pnio_status, + res_size, + p_res, + &res_status_pos); + } + + return ret; +} + +/** + * @internal + * Take a DCE RPC request and create a DCE RPC response. + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The session instance. Will be released on + * error. + * @param req_pos In: Position in the input buffer. + * @param p_rpc In: The RPC header. + * @param res_size In: The size of the response buffer. + * @param p_res Out: The response buffer. Already filled with + * a DCE/RPC header. + * @param p_res_pos InOut: Position in the response buffer. + * @param p_set_state_prmend Out: Set state to PNET_EVENT_PRMEND after the + * response have been sent. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rpc_request ( + pnet_t * net, + pf_session_info_t * p_sess, + uint16_t req_pos, + const pf_rpc_header_t * p_rpc, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_res_pos, + bool * p_set_state_prmend) +{ + int ret = -1; + + switch (p_rpc->opnum) + { + case PF_RPC_DEV_OPNUM_CONNECT: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming CONNECT request via DCE RPC on UDP\n", + __LINE__); + ret = pf_cmrpc_rm_connect_ind ( + net, + p_sess, + req_pos, + res_size, + p_res, + p_res_pos); + break; + case PF_RPC_DEV_OPNUM_RELEASE: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming RELEASE request via DCE RPC on UDP\n", + __LINE__); + ret = pf_cmrpc_rm_release_ind ( + net, + p_sess, + req_pos, + res_size, + p_res, + p_res_pos); + p_sess->kill_session = true; + break; + case PF_RPC_DEV_OPNUM_READ: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming READ request via DCE RPC on UDP\n", + __LINE__); + ret = pf_cmrpc_rm_read_ind ( + net, + p_sess, + p_rpc->opnum, + req_pos, + res_size, + p_res, + p_res_pos); + break; + case PF_RPC_DEV_OPNUM_WRITE: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming WRITE request via DCE RPC on UDP\n", + __LINE__); + ret = pf_cmrpc_rm_write_ind ( + net, + p_sess, + req_pos, + res_size, + p_res, + p_res_pos); + break; + case PF_RPC_DEV_OPNUM_CONTROL: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming DCONTROL request via DCE RPC on UDP\n", + __LINE__); + ret = pf_cmrpc_rm_dcontrol_ind ( + net, + p_sess, + p_rpc, + req_pos, + res_size, + p_res, + p_res_pos, + p_set_state_prmend); + break; + case PF_RPC_DEV_OPNUM_READ_IMPLICIT: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming READ_IMPLICIT request via DCE RPC on UDP\n", + __LINE__); + ret = pf_cmrpc_rm_read_ind ( + net, + p_sess, + p_rpc->opnum, + req_pos, + res_size, + p_res, + p_res_pos); + p_sess->kill_session = true; + break; + default: + LOG_ERROR ( + PNET_LOG, + "CMRPC(%d): Unknown opnum %" PRIu16 "\n", + __LINE__, + p_rpc->opnum); + break; + } + + return ret; +} + +int pf_cmrpc_rm_ccontrol_req (pnet_t * net, pf_ar_t * p_ar) +{ + int ret = -1; + pf_rpc_header_t rpc_req; + pf_ndr_data_t ndr_data; + pf_control_block_t control_io; + uint16_t start_pos = 0; + uint16_t control_pos = 0; + uint16_t rpc_hdr_start_pos = 0; + uint16_t length_of_body_pos = 0; + pf_session_info_t * p_sess = NULL; + uint16_t max_req_len = PF_MAX_UDP_PAYLOAD_SIZE; /* Reduce to 125 to test + fragmented sending */ + + memset (&rpc_req, 0, sizeof (rpc_req)); + memset (&ndr_data, 0, sizeof (ndr_data)); + memset (&control_io, 0, sizeof (control_io)); + + if (pf_session_allocate (net, &p_sess) != 0) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Out of session resources for outgoing CControl.\n", + __LINE__); + } + else + { + p_sess->p_ar = p_ar; + p_sess->ip_addr = p_ar->p_sess->ip_addr; + p_sess->port = PF_RPC_SERVER_PORT; /* Destination port on IO-controller */ + p_sess->from_me = true; + p_sess->is_big_endian = true; + + memset (&rpc_req, 0, sizeof (rpc_req)); + memset (&ndr_data, 0, sizeof (ndr_data)); + memset (&control_io, 0, sizeof (control_io)); + + rpc_req.version = 4; + rpc_req.packet_type = PF_RPC_PT_REQUEST; + rpc_req.flags.idempotent = true; + rpc_req.is_big_endian = p_sess->is_big_endian; + + rpc_req.float_repr = 0; /* IEEE */ + rpc_req.serial_high = 0; + + rpc_req.object_uuid = p_ar->ar_param.cm_initiator_object_uuid; + + /* Controller RPC id */ + rpc_req.interface_uuid.data1 = 0xdea00002; + rpc_req.interface_uuid.data2 = 0x6c97; + rpc_req.interface_uuid.data3 = 0x11d1; + rpc_req.interface_uuid.data4[0] = 0x82; + rpc_req.interface_uuid.data4[1] = 0x71; + rpc_req.interface_uuid.data4[2] = 0x00; + rpc_req.interface_uuid.data4[3] = 0xa0; + rpc_req.interface_uuid.data4[4] = 0x24; + rpc_req.interface_uuid.data4[5] = 0x42; + rpc_req.interface_uuid.data4[6] = 0xdf; + rpc_req.interface_uuid.data4[7] = 0x7d; + + rpc_req.activity_uuid = p_sess->activity_uuid; + + rpc_req.server_boot_time = 0; /* Unknown */ + rpc_req.interface_version = 0x00000001; /* 1.0 */ + rpc_req.sequence_nmb = p_sess->sequence_nmb_send++; + rpc_req.opnum = PF_RPC_DEV_OPNUM_CONTROL; + rpc_req.interface_hint = 0xffff; + rpc_req.activity_hint = 0xffff; + rpc_req.length_of_body = 0; /* To be filled later */ + rpc_req.serial_low = 0; + + control_io.ar_uuid = p_ar->ar_param.ar_uuid; + control_io.session_key = p_ar->ar_param.session_key; + control_io.control_command = BIT (PF_CONTROL_COMMAND_BIT_APP_RDY); + control_io.alarm_sequence_number = 0; /* Reserved */ + control_io.control_block_properties = 0; + + memset (p_sess->out_buffer, 0, sizeof (p_sess->out_buffer)); + p_sess->out_buf_len = 0; + p_sess->out_buf_sent_pos = 0; + p_sess->out_fragment_nbr = 0; + length_of_body_pos = 0; + + /* Insert RPC header */ + rpc_hdr_start_pos = p_sess->out_buf_len; + pf_put_dce_rpc_header ( + &rpc_req, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len, + &length_of_body_pos); + start_pos = p_sess->out_buf_len; + + /* Write temporary values to the NDR header */ + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.args_maximum, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.args_length, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.array.maximum_count, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.array.offset, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.array.actual_count, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len); + + /* Save the _real_ value for args_maximum */ + ndr_data.args_maximum = max_req_len - p_sess->out_buf_len; + + /* Always little-endian on the wire */ + control_pos = p_sess->out_buf_len; + pf_put_control ( + true, + PF_BT_APPRDY_REQ, + &control_io, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len); + + pf_put_ar_diff ( + rpc_req.is_big_endian, + p_ar, + sizeof (p_sess->out_buffer), + p_sess->out_buffer, + &p_sess->out_buf_len); + + /* Save the _real_ value for args_length */ + ndr_data.args_length = p_sess->out_buf_len - control_pos; + + /* + * The complete request is stored in p_sess->out_buffer. It may be too + * large for one UDP frame so we may need to sent it as fragments. In that + * case the first fragment is sent from here and the rest is handled by + * the PT_FRAG_ACK handler in pf_cmrpc_dce_packet. + */ + if (p_sess->out_buf_len < max_req_len) + { + /* NOT a fragmented request - all fits into send buffer */ + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Send CControl request for AREP %u, total %u bytes (not " + "fragmented).\n", + __LINE__, + p_ar->arep, + p_sess->out_buf_len); + p_sess->out_buf_send_len = p_sess->out_buf_len; + } + else + { + /* We need to send a fragmented answer - Send the first fragment now */ + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Send fragmented CControl request AREP %u, total %u, " + "max per frame %u bytes.\n", + __LINE__, + p_ar->arep, + p_sess->out_buf_len, + max_req_len); + p_sess->out_buf_send_len = max_req_len; /* This is what is sent in the + first fragment */ + + /* Also set the fragment bit in the RPC response header */ + rpc_req.flags.fragment = true; + rpc_req.flags.no_fack = false; + + /* Re-write the RPC header with the new info */ + pf_put_dce_rpc_header ( + &rpc_req, + max_req_len, + p_sess->out_buffer, + &rpc_hdr_start_pos, + &length_of_body_pos); + } + + /* Finalize */ + /* Insert the real value of length_of_body into the RPC header */ + pf_put_uint16 ( + rpc_req.is_big_endian, + p_sess->out_buf_send_len - start_pos, + max_req_len, + p_sess->out_buffer, + &length_of_body_pos); + + /* Fixup the NDR header with correct values. */ + ndr_data.array.actual_count = ndr_data.args_length; /* Must be equal for + requests */ + ndr_data.array.maximum_count = ndr_data.args_maximum; /* Must be equal for + requests */ + + /* Over-write the NDR data with the correct data */ + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.args_maximum, + max_req_len, + p_sess->out_buffer, + &start_pos); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.args_length, + max_req_len, + p_sess->out_buffer, + &start_pos); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.array.maximum_count, + max_req_len, + p_sess->out_buffer, + &start_pos); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.array.offset, + max_req_len, + p_sess->out_buffer, + &start_pos); + pf_put_uint32 ( + rpc_req.is_big_endian, + ndr_data.array.actual_count, + max_req_len, + p_sess->out_buffer, + &start_pos); + + /* Open socket for CControl interchange */ + p_sess->socket = pf_udp_open (net, PF_RPC_CCONTROL_EPHEMERAL_PORT); + p_sess->resend_counter = PF_CMRPC_NUMBER_OF_RESENDS; + pf_cmrpc_send_with_timeout (net, p_sess, os_get_current_time_us()); + + ret = 0; + } + + return ret; +} + +/** + * @internal + * Parse all blocks in a CControl RPC response message. + * @param p_sess InOut: The session instance. The rpc_result field is + * set if return != 0. + * @param req_pos In: The position in the input buffer. + * @param p_control_io Out: The CControl control block. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_ccontrol_interpret_cnf ( + pf_session_info_t * p_sess, + uint16_t req_pos, + pf_control_block_t * p_control_io) +{ + int ret = 0; + uint16_t start_pos; + pf_block_header_t block_header; + + start_pos = req_pos; + while (((req_pos + sizeof (block_header)) <= p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + pf_get_block_header (&p_sess->get_info, &req_pos, &block_header); + if ( + (((req_pos + block_header.block_length + 4) - sizeof (block_header)) <= + p_sess->get_info.len) && + (p_sess->get_info.result == PF_PARSE_OK)) + { + switch (block_header.block_type) + { + case PF_BT_APPRDY_RES: + pf_get_control (&p_sess->get_info, &req_pos, p_control_io); + break; + default: + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_UNKNOWN_BLOCKS); + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Unknown block type %u\n", + __LINE__, + (unsigned)block_header.block_type); + LOG_DEBUG ( + PNET_LOG, + "CMRPC(%d): Unknown block type %u\n", + __LINE__, + (unsigned)block_header.block_type); + ret = -1; + break; + } + } + else + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): Parse error\n", __LINE__); + } + } + + if (ret == 0) + { + if (p_sess->get_info.result == PF_PARSE_OK) + { + /* Error discovery. */ + if (req_pos != p_sess->get_info.len) + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if ((uint32_t)(req_pos - start_pos) != p_sess->ndr_data.args_length) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): args_length %" PRIu32 " != request length %" PRIu32 + "\n", + __LINE__, + p_sess->ndr_data.args_length, + (uint32_t)(req_pos - start_pos)); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_ARGSLENGTH_INVALID); + ret = -1; + } + else if ( + p_control_io->control_command != BIT (PF_CONTROL_COMMAND_BIT_DONE)) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): CControl ControlCommand has wrong bit pattern: " + "%04x\n", + __LINE__, + p_control_io->control_command); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + ret = -1; + } + } + else + { + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONTROL, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + ret = -1; + } + } + + return ret; +} + +/** + * @internal + * Handle the response from a CControl request + * + * Triggers these user callbacks: + * * \a pnet_state_ind() with PNET_EVENT_DATA. + * * \a pnet_ccontrol_cnf() + * + * @param net InOut: The p-net stack instance + * @param p_sess InOut: The session instance. This is the outgoing + * CControl session. + * @param req_pos In: Position in the input buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rm_ccontrol_cnf ( + pnet_t * net, + pf_session_info_t * p_sess, + uint16_t req_pos) +{ + int ret = -1; + pf_control_block_t ccontrol_io; + pf_ar_t * p_ar = p_sess->p_ar; + + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming CCONTROL response via DCE RPC on UDP\n", + __LINE__); + CC_ASSERT (p_ar != NULL); + + memset (&ccontrol_io, 0, sizeof (ccontrol_io)); + + if (pf_cmrpc_rm_ccontrol_interpret_cnf (p_sess, req_pos, &ccontrol_io) == 0) + { + /* + * The only expected response here is "done". + */ + if (ccontrol_io.control_command == BIT (PF_CONTROL_COMMAND_BIT_DONE)) + { + /* Send the result to CMDEV */ + if ( + pf_cmdev_rm_ccontrol_cnf ( + net, + p_ar, + &ccontrol_io, + &p_sess->rpc_result) == 0) + { + /* Only if result is OK */ + if ( + pf_cmpbe_rm_ccontrol_cnf ( + p_ar, + &ccontrol_io, + &p_sess->rpc_result) == 0) + { + p_sess->kill_session = true; + ret = 0; + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): pf_cmpbe_rm_ccontrol_cnf() failed.\n", + __LINE__); + } + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): pf_cmdev_rm_ccontrol_cnf() failed.\n", + __LINE__); + } + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): CControl ControlCommand has wrong bit pattern: %04x\n", + __LINE__, + ccontrol_io.control_command); + } + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Failed to parse a CControl response message.\n", + __LINE__); + } + + return ret; +} + +/*********************** Incoming packets ************************************/ + +/** + * @internal + * Handle one incoming DCE RPC response type message. + * @param net InOut: The p-net stack instance. + * @param p_sess InOut: The session instance. + * @param req_pos In: The response length. + * @param p_rpc In: The RPC header. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrpc_rpc_response ( + pnet_t * net, + pf_session_info_t * p_sess, + uint16_t req_pos, + const pf_rpc_header_t * p_rpc) +{ + int ret = -1; + + switch (p_rpc->opnum) + { + case PF_RPC_DEV_OPNUM_CONTROL: + ret = pf_cmrpc_rm_ccontrol_cnf (net, p_sess, req_pos); + break; + default: + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Unknown opnum %" PRIu16 "\n", + __LINE__, + p_rpc->opnum); + break; + } + + return ret; +} + +/** + * @internal + * Handle one incoming DCE RPC message, and typically sends a response. + * + * Receives raw UDP data. + * Puts together incoming fragments, and handles the full RPC request by using + * \a pf_cmrpc_rpc_request(). + * + * @param net InOut: The p-net stack instance + * @param ip_addr In: The IP address of the originator + * @param port In: The port of the originator. + * @param p_req In: The input message buffer. + * @param req_len In: The input message length. + * @param p_res Out: The output buffer. + * @param p_res_len In: The size of the output UDP buffer. + * Out: The length of the output message. If set to 0 + * the caller will typically not send any UDP + * response. + * @param p_close_socket Out: Set to true if operation is a release op. The + * caller will then close the corresponding UDP + * socket. + * @return 0 if operation succeeded. (Typically not used by the caller) + * -1 if an error occurred. + */ +static int pf_cmrpc_dce_packet ( + pnet_t * net, + uint32_t ip_addr, + uint16_t port, + const uint8_t * p_req, + uint32_t req_len, + uint8_t * p_res, + uint16_t * p_res_len, + bool * p_close_socket) +{ + int ret = -1; + pf_rpc_header_t rpc_req; + pf_rpc_header_t rpc_res; + uint16_t rpc_hdr_start_pos = 0; + uint16_t start_pos = 0; + uint16_t length_of_body_pos = 0; + uint16_t req_pos = 0; + uint16_t res_pos = 0; + uint16_t max_rsp_len; + uint32_t max_rsp_len_remote; + pf_get_info_t get_info; + pf_session_info_t * p_sess = NULL; + bool is_new_session = false; + uint32_t fault_code = 0; + uint32_t reject_code = 0; + bool set_state_paramend = false; + + get_info.result = PF_PARSE_OK; + get_info.p_buf = p_req; + get_info.len = req_len; + + /* Parse RPC header. This function also sets get_info.is_big_endian */ + pf_get_dce_rpc_header (&get_info, &req_pos, &rpc_req); + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Incoming RPC frame. Seq: %" PRIu32 ", " + "Serial: %u, Frag num: %u, Frag Len: %u " + "Fragment flag: \"%s\" Last Fragment flag: \"%s\", No fack: \"%s\"\n", + __LINE__, + rpc_req.sequence_nmb, + rpc_req.serial_low, + rpc_req.fragment_nmb, + rpc_req.length_of_body, + rpc_req.flags.fragment ? "Set" : "Not set", + rpc_req.flags.last_fragment ? "Set" : "Not set", + rpc_req.flags.no_fack ? "Set" : "Not set"); + + /* Find the session (by RPC activity UUID) or allocate a new session */ + pf_session_locate_by_uuid (net, &rpc_req.activity_uuid, &p_sess); + if (p_sess == NULL) + { + (void)pf_session_allocate (net, &p_sess); + if (p_sess != NULL) + { + is_new_session = true; + p_sess->from_me = false; + + p_sess->ip_addr = ip_addr; + p_sess->port = port; /* Source port on incoming message */ + p_sess->socket = net->cmrpc_rpcreq_socket; + } + } + + if (p_sess == NULL) + { + /* Unavailable */ + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Out of session resources for incoming frame.\n", + __LINE__); + } + else + { + p_sess->get_info = get_info; + memset (&p_sess->rpc_result, 0, sizeof (p_sess->rpc_result)); + + if (rpc_req.flags.fragment == false) + { + /* Incoming message is a request or response contained entirely in one + * frame */ + p_sess->in_buf_len = 0; + p_sess->ip_addr = ip_addr; + p_sess->port = port; + p_sess->activity_uuid = rpc_req.activity_uuid; + p_sess->is_big_endian = p_sess->get_info.is_big_endian; + p_sess->in_fragment_nbr = 0; + p_sess->kill_session = false; + } + else + { + /* Incoming message is a fragment of a request or response. + * + * Collect all fragments into the session buffer and + * proceed only when the last fragment has been received. + */ + if (rpc_req.fragment_nmb == 0) + { + /* This is the first incoming fragment. */ + /* Initialize the session */ + p_sess->in_buf_len = 0; + p_sess->ip_addr = ip_addr; + p_sess->port = port; + p_sess->activity_uuid = rpc_req.activity_uuid; + + p_sess->is_big_endian = p_sess->get_info.is_big_endian; + p_sess->in_fragment_nbr = 0; + p_sess->kill_session = false; + } + else + { + /* Intermediate or last incoming fragment. */ + if (p_sess->is_big_endian != rpc_req.is_big_endian) + { + /* Endianness differs. All fragments must have same endianness in + * this implementation */ + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Endianness differs in incoming fragments\n", + __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + } + } + + /* Fragment validation */ + /* Enter here _even_if_ an error is already detected because we need to + * generate an error response. */ + if (p_sess->in_fragment_nbr != rpc_req.fragment_nmb) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Unexpected incoming fragment number. Received %u, " + "expected %u\n", + __LINE__, + (unsigned)rpc_req.fragment_nmb, + (unsigned)p_sess->in_fragment_nbr + 1); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + } + else if ( + (p_sess->in_buf_len + rpc_req.length_of_body) > + sizeof (p_sess->in_buffer)) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Incoming fragments exceed max buffer\n", + __LINE__); + pf_set_error ( + &p_sess->rpc_result, + PNET_ERROR_CODE_CONNECT, + PNET_ERROR_DECODE_PNIO, + PNET_ERROR_CODE_1_CMRPC, + PNET_ERROR_CODE_2_CMRPC_STATE_CONFLICT); + } + else + { + /* Copy to session input buffer */ + memcpy ( + &p_sess->in_buffer[p_sess->in_buf_len], + &p_req[req_pos], + rpc_req.length_of_body); + p_sess->in_buf_len += rpc_req.length_of_body; + p_sess->in_fragment_nbr++; + + if (rpc_req.flags.last_fragment == true) + { + /* Re-route the parser to use the session buffer */ + req_pos = 0; + p_sess->get_info.p_buf = p_sess->in_buffer; + p_sess->get_info.len = p_sess->in_buf_len; + } + } + } + + /* Any incoming message stops resending */ + pf_scheduler_remove_if_running (net, &p_sess->resend_timeout); + + /* Decide what to do with incoming message */ + /* Enter here _even_if_ an error is already detected because we may need + * to generate an error response. */ + if ( + (rpc_req.flags.fragment == false) || + (rpc_req.flags.last_fragment == true)) + { + /* Full message has arrived */ + switch (rpc_req.packet_type) + { + case PF_RPC_PT_PING: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming DCE RPC ping on UDP\n", + __LINE__); + /* A new request - clear the response buffer */ + p_sess->out_buf_len = 0; + p_sess->out_buf_sent_pos = 0; + p_sess->out_buf_send_len = 0; + p_sess->out_fragment_nbr = 0; + + /* Prepare the response */ + rpc_res = rpc_req; + rpc_res.packet_type = PF_RPC_PT_RESP_PING; + rpc_res.flags.last_fragment = false; + rpc_res.flags.fragment = false; + rpc_res.flags.no_fack = true; + rpc_res.flags.maybe = false; + rpc_res.flags.idempotent = true; + rpc_res.flags.broadcast = false; + rpc_res.flags2.cancel_pending = false; + rpc_res.fragment_nmb = 0; + rpc_res.is_big_endian = p_sess->is_big_endian; + + pf_put_dce_rpc_header ( + &rpc_res, + *p_res_len, + p_res, + &res_pos, + &length_of_body_pos); + + pf_cmrpc_send_once_from_buffer ( + net, + p_sess, + p_res, + res_pos, + "PING response"); + + p_sess->kill_session = is_new_session; + break; + case PF_RPC_PT_REQUEST: + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Incoming DCE RPC request on UDP.\n", + __LINE__); + /* A new request - clear the response buffer */ + p_sess->out_buf_len = 0; + p_sess->out_buf_sent_pos = 0; + p_sess->out_buf_send_len = 0; + p_sess->out_fragment_nbr = 0; + + /*Check what type of request this is EPMv4 or PNIO?*/ + if ( + memcmp ( + &rpc_req.interface_uuid, + &uuid_epmap_interface, + sizeof (rpc_req.object_uuid)) != 0) + { + pf_get_ndr_data (&p_sess->get_info, &req_pos, &p_sess->ndr_data); + /* From now on all is big-endian */ + p_sess->get_info.is_big_endian = true; + + /* Our response is limited by the size of the requesters response + * buffer */ + max_rsp_len_remote = req_pos + p_sess->ndr_data.args_maximum; + if (max_rsp_len_remote > sizeof (p_sess->out_buffer)) + { + /* Our response is also limited by what our buffer can + * accommodate */ + max_rsp_len = sizeof (p_sess->out_buffer); + } + else + { + max_rsp_len = max_rsp_len_remote; + } + } + else + { + /* EPM requirement is little endian*/ + p_sess->get_info.is_big_endian = false; + max_rsp_len = sizeof (p_sess->out_buffer); + } + + /* Prepare the response */ + rpc_res = rpc_req; + rpc_res.packet_type = PF_RPC_PT_RESPONSE; + rpc_res.flags.last_fragment = false; + rpc_res.flags.fragment = false; + rpc_res.flags.no_fack = true; + rpc_res.flags.maybe = false; + rpc_res.flags.idempotent = true; + rpc_res.flags.broadcast = false; + rpc_res.flags2.cancel_pending = false; + rpc_res.fragment_nmb = p_sess->out_fragment_nbr; + rpc_res.serial_high = (uint8_t)(rpc_res.fragment_nmb >> 8U); + rpc_res.serial_low = rpc_res.fragment_nmb & UINT8_MAX; + rpc_res.is_big_endian = p_sess->get_info.is_big_endian; + + /* Insert the response header to get pos of rpc response body. */ + rpc_hdr_start_pos = p_sess->out_buf_len; + pf_put_dce_rpc_header ( + &rpc_res, + max_rsp_len, + p_sess->out_buffer, + &p_sess->out_buf_len, + &length_of_body_pos); + start_pos = p_sess->out_buf_len; /* Save for later */ + + if (rpc_req.opnum == PF_RPC_DEV_OPNUM_RELEASE) + { + p_sess->release_in_progress = true; /* Tell everybody */ + *p_close_socket = p_sess->from_me; + } + + if ( + memcmp ( + &rpc_req.interface_uuid, + &uuid_io_device_interface, + sizeof (rpc_req.object_uuid)) == 0) + { + /* Handle PNIO requests */ + ret = pf_cmrpc_rpc_request ( + net, + p_sess, + req_pos, + &rpc_req, + max_rsp_len, + p_sess->out_buffer, + &p_sess->out_buf_len, + &set_state_paramend); + } + else if ( + memcmp ( + &rpc_req.interface_uuid, + &uuid_epmap_interface, + sizeof (rpc_req.object_uuid)) == 0) + { + rpc_res.fragment_nmb = 0; + rpc_res.flags.idempotent = false; + + /* Handle Endpoint Map request */ + ret = pf_cmrpc_lookup_ind ( + net, + p_sess, + &rpc_req, + req_pos, + max_rsp_len, + p_sess->out_buffer, + &p_sess->out_buf_len); + *p_close_socket = p_sess->from_me; + + /* Close session after each EPM request + If future more advanced EPM usage is required, implement a + timeout for closing the session */ + p_sess->kill_session = true; + } + else + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Unhandled Object or Interface UUID!\n", + __LINE__); + /*ToDo: Report NULL endpoint with proper error code*/ + } + + if (p_sess->out_buf_len < PF_MAX_UDP_PAYLOAD_SIZE) + { + /* Our response will fit into send buffer (not fragmented) */ + p_sess->out_buf_send_len = p_sess->out_buf_len; /* Send + everything */ + } + else + { + /* Send a fragmented response - Send the first fragment now */ + p_sess->out_buf_send_len = PF_MAX_UDP_PAYLOAD_SIZE; /* Send as + much as + can fit */ + + /* Also set the fragment bit in the RPC response header */ + rpc_res.flags.fragment = true; + rpc_res.flags.no_fack = false; + + /* Re-write the header with the new info */ + pf_put_dce_rpc_header ( + &rpc_res, + max_rsp_len, + p_sess->out_buffer, + &rpc_hdr_start_pos, + &length_of_body_pos); + } + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Send RPC response. Total response length %u, " + "sending %u bytes. Start of RPC payload: %u\n", + __LINE__, + p_sess->out_buf_len, + p_sess->out_buf_send_len, + start_pos); + + /* Insert the real value of length_of_body in the rpc header */ + pf_put_uint16 ( + rpc_res.is_big_endian, + (uint16_t)(p_sess->out_buf_send_len - start_pos), + p_sess->out_buf_send_len, + p_sess->out_buffer, + &length_of_body_pos); + + if (rpc_res.flags.fragment == true) + { + /* Fragmented respones from us (with ack) are supposed to be + * re-transmitted according to the spec. */ + p_sess->resend_counter = PF_CMRPC_NUMBER_OF_RESENDS; + pf_cmrpc_send_with_timeout ( + net, + p_sess, + os_get_current_time_us()); + } + else + { + /* Non-fragmented responses from us are not re-transmitted */ + ret = pf_cmrpc_send_once (net, p_sess, "response"); + } + + if (set_state_paramend && p_sess->p_ar != NULL) + { + pf_cmdev_state_ind (net, p_sess->p_ar, PNET_EVENT_PRMEND); + } + break; + case PF_RPC_PT_FRAG_ACK: + /* Handle fragment acknowledgment from the controller. */ + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Received fragment ACK.\n", + __LINE__); + + /* Update how much the controller has received (ack'ed) */ + p_sess->out_buf_sent_pos += p_sess->out_buf_send_len; + p_sess->out_fragment_nbr++; + + /* The fragment acknowledgment is valid (expected) */ + if (p_sess->out_buf_len > p_sess->out_buf_sent_pos) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): More fragments to send. Total RPC payload %u, " + "already sent %u, max fragment UDP payload %u (incl RPC " + "header). args_length %" PRIu32 "\n", + __LINE__, + p_sess->out_buf_len, + p_sess->out_buf_sent_pos, + *p_res_len, + p_sess->ndr_data.args_length); + + /* res_pos holds the number of bytes we are preparing to send in + * this frame */ + res_pos = 0; + + /* Prepare the RPC header of next fragment */ + rpc_res = rpc_req; + rpc_res.packet_type = p_sess->from_me ? PF_RPC_PT_REQUEST + : PF_RPC_PT_RESPONSE; + rpc_res.flags.last_fragment = false; + rpc_res.flags.fragment = true; + rpc_res.flags.no_fack = false; + rpc_res.flags.maybe = false; + rpc_res.flags.idempotent = true; + rpc_res.flags.broadcast = false; + rpc_res.flags2.cancel_pending = false; + rpc_res.fragment_nmb = p_sess->out_fragment_nbr; + rpc_res.serial_high = (uint8_t)(rpc_res.fragment_nmb >> 8U); + rpc_res.serial_low = rpc_res.fragment_nmb & UINT8_MAX; + rpc_res.is_big_endian = + p_sess->from_me ? true : p_sess->get_info.is_big_endian; + + rpc_hdr_start_pos = res_pos; + /* Insert the response header to get pos of rpc response length. + */ + pf_put_dce_rpc_header ( + &rpc_res, + PF_MAX_UDP_PAYLOAD_SIZE, + p_sess->out_buffer, + &res_pos, + &length_of_body_pos); + start_pos = res_pos; /* Save for later */ + + if ( + (uint16_t)(p_sess->out_buf_len - p_sess->out_buf_sent_pos) < + (PF_MAX_UDP_PAYLOAD_SIZE - start_pos)) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Sending last fragment.\n", + __LINE__); + + /* We need to send one last fragment */ + p_sess->out_buf_send_len = + p_sess->out_buf_len - p_sess->out_buf_sent_pos; /* Send all + the rest + */ + + /* Send last fragment */ + rpc_res.flags.last_fragment = true; + + /* Update the response header to get pos of rpc response + * length. */ + pf_put_dce_rpc_header ( + &rpc_res, + PF_MAX_UDP_PAYLOAD_SIZE, + p_sess->out_buffer, + &rpc_hdr_start_pos, + &length_of_body_pos); + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Sending intermediate fragment.\n", + __LINE__); + + /* Send next fragment */ + p_sess->out_buf_send_len = + PF_MAX_UDP_PAYLOAD_SIZE - start_pos; /* Send as much as we + can */ + } + + /* Send a fragment now */ + /* This is essentially a memmove() in order to make the next + * fragment data follow the RPC header at the start of out_buffer + */ + pf_put_mem_overlapping ( + &p_sess->out_buffer[p_sess->out_buf_sent_pos], + p_sess->out_buf_send_len, + PF_MAX_UDP_PAYLOAD_SIZE, + p_sess->out_buffer, + &res_pos); + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Send RPC fragment. Total response length %u, " + "already sent %u, sending %u bytes RPC payload. RPC payload " + "start pos %u, Fragment size: %u bytes\n", + __LINE__, + p_sess->out_buf_len, + p_sess->out_buf_sent_pos, + p_sess->out_buf_send_len, + start_pos, + res_pos); + + /* Insert the real value of length_of_body in the rpc header */ + pf_put_uint16 ( + rpc_res.is_big_endian, + p_sess->out_buf_send_len, + PF_MAX_UDP_PAYLOAD_SIZE, + p_sess->out_buffer, + &length_of_body_pos); + + p_sess->out_buf_send_len += start_pos; + + /* Fragments from us are supposed to be re-transmitted */ + p_sess->resend_counter = PF_CMRPC_NUMBER_OF_RESENDS; + pf_cmrpc_send_with_timeout ( + net, + p_sess, + os_get_current_time_us()); + } + else + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): No more fragments to send. Total %u, already " + "sent %u.\n", + __LINE__, + p_sess->out_buf_len, + p_sess->out_buf_sent_pos); + /* The last fragment has been acknowledged */ + p_sess->out_buf_len = 0; + p_sess->out_buf_sent_pos = 0; + p_sess->out_buf_send_len = 0; + p_sess->out_fragment_nbr = 0; + res_pos = 0; /* Nothing more to do */ + } + break; + case PF_RPC_PT_RESPONSE: + /* A response to a CCONTROL request */ + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Received an RPC response.\n", + __LINE__); + if (is_new_session) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Responses should be part of existing sessions. " + "Unknown incoming activity UUID.\n", + __LINE__); + *p_close_socket = p_sess->from_me; + p_sess->kill_session = true; + res_pos = 0; /* Send nothing in response */ + ret = 0; + } + else + { + pf_get_ndr_data (&p_sess->get_info, &req_pos, &p_sess->ndr_data); + p_sess->get_info.is_big_endian = true; /* From now on all is + big-endian */ + + ret = pf_cmrpc_rpc_response (net, p_sess, req_pos, &rpc_req); + } + break; + case PF_RPC_PT_CL_CANCEL: + /* + * Cancel requests may be sent from the controller to the device. + * The ProfiNet spec say little about how to handle these packets. + * The DCE RPC spec says to cancel the connection. + */ + LOG_INFO (PF_RPC_LOG, "CMRPC(%d): CANCEL received\n", __LINE__); + + (void)pf_cmdev_cm_abort (net, p_sess->p_ar); + + *p_close_socket = p_sess->from_me; + p_sess->kill_session = true; + res_pos = 0; /* Send nothing in response */ + ret = 0; + break; + case PF_RPC_PT_FAULT: + /* + * Faults occur for any number of reasons. + * They are detected by the controller and sent to the device. + * The DCE RPC spec says to go to the fault state. + * Lets just cancel the connection. + */ + fault_code = pf_get_uint32 (&p_sess->get_info, &req_pos); + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): FAULT (code %" PRIx32 ") received\n", + __LINE__, + fault_code); + (void)fault_code; + + (void)pf_cmdev_cm_abort (net, p_sess->p_ar); + + *p_close_socket = p_sess->from_me; + p_sess->kill_session = true; + res_pos = 0; /* Send nothing in response */ + ret = 0; + break; + case PF_RPC_PT_REJECT: + /* + * Rejects occur for any number of reasons. + * They are detected by the controller and sent to the device. + * The ProfiNet spec say little about how to handle these packets + * but at least some controllers restart the entire connection. + */ + reject_code = pf_get_uint32 (&p_sess->get_info, &req_pos); + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): REJECT (code %" PRIx32 ") received\n", + __LINE__, + reject_code); + (void)reject_code; + + (void)pf_cmdev_cm_abort (net, p_sess->p_ar); + + *p_close_socket = p_sess->from_me; + p_sess->kill_session = true; + res_pos = 0; /* Send nothing in response */ + ret = 0; + break; + default: + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): Unknown packet_type %" PRIu8 "\n", + __LINE__, + rpc_req.packet_type); + res_pos = 0; /* Nothing more to do */ + break; + } + } + else + { + /* Respond to intermediate incoming fragments */ + if ((rpc_req.flags.fragment == true) && (rpc_req.flags.no_fack == false)) + { + LOG_INFO (PF_RPC_LOG, "CMRPC(%d): Send fragment ACK\n", __LINE__); + /* Create Fragment ACK */ + /* Send ACK */ + rpc_res = rpc_req; + rpc_res.packet_type = PF_RPC_PT_FRAG_ACK; + rpc_res.flags.last_fragment = false; + rpc_res.flags.fragment = false; + rpc_res.flags.no_fack = false; + rpc_res.flags.maybe = false; + rpc_res.flags.idempotent = false; + rpc_res.flags.broadcast = false; + rpc_res.flags2.cancel_pending = false; + rpc_res.length_of_body = 0; + rpc_res.is_big_endian = p_sess->is_big_endian; + + pf_put_dce_rpc_header ( + &rpc_res, + *p_res_len, + p_res, + &res_pos, + &length_of_body_pos); + + pf_cmrpc_send_once_from_buffer ( + net, + p_sess, + p_res, + res_pos, + "fragment ack"); + } + else + { + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Received a fragment of a DCE RPC message on UDP, " + "but will not send an ACK.\n", + __LINE__); + /* Waiting for the next fragment. */ + res_pos = 0; /* Nothing more to do */ + } + + ret = 0; + } + + /* Kill session if necessary */ + if ((p_sess != NULL) && (p_sess->kill_session == true)) + { + LOG_DEBUG (PF_RPC_LOG, "CMRPC(%d): Kill session\n", __LINE__); + pf_session_release (net, p_sess); + } + } + + return ret; +} + +void pf_cmrpc_periodic (pnet_t * net) +{ + uint32_t dcerpc_addr; + uint16_t dcerpc_port; + int dcerpc_input_len; + uint16_t dcerpc_resp_len = 0; + uint16_t ix; + bool close_socket = false; + char ip_string[PNAL_INET_ADDRSTR_SIZE] = {0}; /** Terminated string */ + + /* TODO Use a common function to avoid code duplication, remove some + * arguments for pf_cmrpc_dce_packet() */ + + /* Poll for RPC session confirmations */ + for (ix = 0; ix < NELEMENTS (net->cmrpc_session_info); ix++) + { + if ( + (net->cmrpc_session_info[ix].in_use == true) && + (net->cmrpc_session_info[ix].from_me == true)) + { + /* We are waiting for a response from the IO-controller */ + dcerpc_input_len = pf_udp_recvfrom ( + net, + net->cmrpc_session_info[ix].socket, + &dcerpc_addr, + &dcerpc_port, + net->cmrpc_dcerpc_input_frame, + sizeof (net->cmrpc_dcerpc_input_frame)); + if (dcerpc_input_len > 0) + { + pf_cmina_ip_to_string (dcerpc_addr, ip_string); + dcerpc_resp_len = PF_MAX_UDP_PAYLOAD_SIZE; + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Received %u bytes UDP payload from remote %s:%u, on " + "socket %d used in session with index %u\n", + __LINE__, + dcerpc_input_len, + ip_string, + dcerpc_port, + net->cmrpc_session_info[ix].socket, + ix); + close_socket = false; + (void)pf_cmrpc_dce_packet ( + net, + dcerpc_addr, + dcerpc_port, + net->cmrpc_dcerpc_input_frame, + dcerpc_input_len, + net->cmrpc_dcerpc_output_frame, + &dcerpc_resp_len, + &close_socket); + + if (close_socket) + { + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Closing socket used in session with index %u\n", + __LINE__, + ix); + pf_udp_close (net, net->cmrpc_session_info[ix].socket); + net->cmrpc_session_info[ix].socket = -1; + } + } + } + } + + /* Poll RPC requests */ + dcerpc_input_len = pf_udp_recvfrom ( + net, + net->cmrpc_rpcreq_socket, + &dcerpc_addr, + &dcerpc_port, + net->cmrpc_dcerpc_input_frame, + sizeof (net->cmrpc_dcerpc_input_frame)); + if (dcerpc_input_len > 0) + { + pf_cmina_ip_to_string (dcerpc_addr, ip_string); + dcerpc_resp_len = PF_MAX_UDP_PAYLOAD_SIZE; + LOG_INFO ( + PF_RPC_LOG, + "CMRPC(%d): Received %u bytes UDP payload from remote %s:%u, on " + "socket %u for incoming DCE RPC requests.\n", + __LINE__, + dcerpc_input_len, + ip_string, + dcerpc_port, + net->cmrpc_rpcreq_socket); + close_socket = false; + (void)pf_cmrpc_dce_packet ( + net, + dcerpc_addr, + dcerpc_port, + net->cmrpc_dcerpc_input_frame, + dcerpc_input_len, + net->cmrpc_dcerpc_output_frame, + &dcerpc_resp_len, + &close_socket); + + if (close_socket) + { + LOG_ERROR ( + PF_RPC_LOG, + "CMRPC(%d): pf_cmrpc_dce_packet() wants to close the main RPC UDP " + "socket, but that is not valid.\n", + __LINE__); + } + } +} + +/*********************** Initialize ******************************************/ + +void pf_cmrpc_init (pnet_t * net) +{ + uint16_t ix = 0; + + if (net->p_cmrpc_rpc_mutex == NULL) + { + net->p_cmrpc_rpc_mutex = os_mutex_create(); + memset (net->cmrpc_ar, 0, sizeof (net->cmrpc_ar)); + memset (net->cmrpc_session_info, 0, sizeof (net->cmrpc_session_info)); + for (ix = 0; ix < NELEMENTS (net->cmrpc_session_info); ix++) + { + net->cmrpc_session_info[ix].socket = -1; + } + + net->cmrpc_rpcreq_socket = pf_udp_open (net, PF_RPC_SERVER_PORT); + } + + /* Save for later (put it into each session */ + net->cmrpc_session_number = 0x12345678; /* Starting number */ +} + +/* Not used */ +void pf_cmrpc_exit (pnet_t * net) +{ + if (net->p_cmrpc_rpc_mutex != NULL) + { + os_mutex_destroy (net->p_cmrpc_rpc_mutex); + memset (net->cmrpc_ar, 0, sizeof (net->cmrpc_ar)); + memset (net->cmrpc_session_info, 0, sizeof (net->cmrpc_session_info)); + } +} + +/*********************** Local primitives ************************************/ + +int pf_cmrpc_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event) +{ + int res = -1; + pf_session_info_t * p_sess = NULL; + + LOG_DEBUG ( + PF_RPC_LOG, + "CMRPC(%d): Received event %s from CMDEV for AREP %u.\n", + __LINE__, + pf_cmdev_event_to_string (event), + p_ar->arep); + + switch (event) + { + case PNET_EVENT_ABORT: + /* + * ABORT may be called from many places within the stack. + * Simply setting p_sess->kill_session to true only works + * for RPC requests, but we need to free the session also for + * internal errors. + * Do it explicitly here. + */ + if (p_ar != NULL) + { + if (p_ar->p_sess != NULL) + { + if (p_ar->p_sess->release_in_progress == false) + { + pf_session_release (net, p_ar->p_sess); + } + } + else + { + LOG_ERROR (PF_RPC_LOG, "CMRPC(%d): Session is NULL\n", __LINE__); + } + + /* Free other (CCONTROL) sessions and close all RPC sockets. */ + while (pf_session_locate_by_ar (net, p_ar, &p_sess) == 0) + { + pf_session_release (net, p_sess); + } + + /* Finally free the AR */ + pf_ar_release (p_ar); + } + break; + default: + break; + } + + return res; +} diff --git a/profinet_stack/p-net/src/device/pf_cmrpc.h b/profinet_stack/p-net/src/device/pf_cmrpc.h new file mode 100644 index 0000000..c57fc01 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrpc.h @@ -0,0 +1,175 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMRPC_H +#define PF_CMRPC_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* ================================================ + * Local primitives + */ + +/** + * Initialize the CMRPC component. + * + * Opens the UDP socket for incoming RPC requests. + * + * @param net InOut: The p-net stack instance + */ +void pf_cmrpc_init (pnet_t * net); + +/** + * Cleanup the CMRPC component. + * @param net InOut: The p-net stack instance + */ +void pf_cmrpc_exit (pnet_t * net); + +/** + * Handle periodic RPC tasks. + * Check for DCE RPC requests. + * Check for DCE RPC confirmations. + * @param net InOut: The p-net stack instance + */ +void pf_cmrpc_periodic (pnet_t * net); + +/** + * Find an AR by its AREP. + * @param net InOut: The p-net stack instance + * @param arep In: The AREP. + * @param pp_ar Out: The AR (or NULL). + * @return 0 if AR is found and valid. + * -1 if AREP is invalid. + */ +int pf_ar_find_by_arep (pnet_t * net, uint32_t arep, pf_ar_t ** pp_ar); + +/** + * Return pointer to specific AR. + * @param net InOut: The p-net stack instance + * @param ix In: The AR array index. + * @return The AR instance. + */ +pf_ar_t * pf_ar_find_by_index (pnet_t * net, uint16_t ix); + +/** + * Insert detailed error information into the result structure (of a session). + * + * If you need to set error information after some function has returned an + * error (and possibly already has updated the error information), use + * \a pf_set_error_if_not_already_set() instead. + * + * @param p_stat Out: The result structure. + * @param code In: The error_code. + * @param decode In: The error_decode. + * @param code_1 In: The error_code_1. + * @param code_2 In: The error_code_2. + */ +void pf_set_error ( + pnet_result_t * p_stat, + uint8_t code, + uint8_t decode, + uint8_t code_1, + uint8_t code_2); + +/** + * Insert detailed error information into the result structure (of a session). + * Does not overwrite existing error information. + * + * This function is typically called after some other function has returned an + * error and possibly have set an error code. + * For other usecases, use pf_set_error() instead. + * + * @param p_stat Out: The result structure. + * @param code In: The error_code. + * @param decode In: The error_decode. + * @param code_1 In: The error_code_1. + * @param code_2 In: The error_code_2. + */ +void pf_set_error_if_not_already_set ( + pnet_result_t * p_stat, + uint8_t code, + uint8_t decode, + uint8_t code_1, + uint8_t code_2); + +/** + * Handle CMDEV events. + * + * For an ABORT event (given some conditions) the related sessions and the AR + * are released, and the global RPC socket is re-opened. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param event In: The new CMDEV state. Use PNET_EVENT_xxx, not + * PF_CMDEV_STATE_xxx + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmrpc_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event); + +/** + * Send a DCE RPC request to the controller. + * The only request handled is the CControl (APPL_RDY) request. + * + * Opens a new UDP socket for the session. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmrpc_rm_ccontrol_req (pnet_t * net, pf_ar_t * p_ar); + +/** + * Show AR and session information. + * + * Use level 0xFFFF to show everything. + * + * @param net InOut: The p-net stack instance + * @param level In: Level of detail. + */ +void pf_cmrpc_show (pnet_t * net, unsigned level); + +/************************* Utilities ******************************************/ + +/** + * Display buffer contents + * + * For example: + * + * uint8_t mybuffer[18] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + * 'K', 'L', 0, 1, 2, 3, 4, 5}; pf_memory_contents_show(mybuffer, 18); + * + * will be displayed as: + * + * 41 42 43 44 45 46 47 48 49 4a 4b 4c 00 01 02 03 |ABCDEFGHIJKL....| + * 04 05 |..| + * + * @param data In: Buffer contents to be displayed + * @param size In: Buffer size (or smaller, to only display the + * beginning) + */ +void pf_memory_contents_show (const uint8_t * data, int size); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMRPC_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmrpc_epm.c b/profinet_stack/p-net/src/device/pf_cmrpc_epm.c new file mode 100644 index 0000000..c60dab0 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrpc_epm.c @@ -0,0 +1,383 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief RPC End Point Mapper + * + */ + +#include +#include "pf_includes.h" +#include "pf_block_writer.h" + +static const pf_rpc_uuid_type_t null_rpc_handle = + {0, 0, 0, 0, 0, {0, 0, 0, 0, 0, 0}}; + +static const pf_rpc_uuid_type_t uuid_object = { + 0x00000000, + 0x0000, + 0x0000, + 0x00, + 0x00, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + +static const pf_rpc_uuid_type_t uuid_epmap_interface = { + 0xE1AF8308, + 0x5D1F, + 0x11C9, + 0x91, + 0xA4, + {0x08, 0x00, 0x2B, 0x14, 0xA0, 0xFA}}; + +static const pf_rpc_uuid_type_t uuid_io_device_interface = { + 0xDEA00001, + 0x6C97, + 0x11D1, + 0x82, + 0x71, + {0x00, 0xA0, 0x24, 0x42, 0xDF, 0x7D}}; + +static const pf_rpc_uuid_type_t uuid_io_object_instance = { + 0xDEA00000, + 0x6C97, + 0x11D1, + 0x82, + 0x71, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + +static const pf_rpc_uuid_type_t uuid_ndr = { + 0x8A885D04, + 0x1CEB, + 0x11C9, + 0x9F, + 0xE8, + {0x08, 0x00, 0x2B, 0x10, 0x48, 0x60}}; + +/** + * Generate rpc epm handle + * @param timestamp In: Timestamp + * @param mac_address In: Mac address + * @param p_handle Out: Generated handle + */ +static void pf_generate_epm_handle ( + uint32_t timestamp, + pnet_ethaddr_t mac_address, + pf_rpc_handle_t * p_handle) +{ + p_handle->rpc_entry_handle = 0; + p_handle->handle_uuid.time_low = (timestamp & 0x0000FFFF); + p_handle->handle_uuid.time_mid = (timestamp & 0x00FF0000); + p_handle->handle_uuid.time_hi_and_version = (timestamp & 0xFF000000) | + 0x1000; + p_handle->handle_uuid.clock_hi_and_reserved = 0x80; + p_handle->handle_uuid.clock_low = /* 0x11 */ 0x0C; + p_handle->handle_uuid.node[0] = mac_address.addr[0]; + p_handle->handle_uuid.node[1] = mac_address.addr[1]; + p_handle->handle_uuid.node[2] = mac_address.addr[2]; + p_handle->handle_uuid.node[3] = mac_address.addr[3]; + p_handle->handle_uuid.node[4] = mac_address.addr[4]; + p_handle->handle_uuid.node[5] = mac_address.addr[5]; +} + +/** + * Initialize protocol tower configuration including floors + * @param p_tower Out: Protocol tower configuration + * @param epm_type In: End point type, EPM or PNIO + * @param udp_port In: UDP protocol port + */ +static void pf_init_rpc_tower_entry ( + pf_rpc_tower_t * p_tower, + pf_rpc_epm_type_t epm_type, + uint16_t udp_port) +{ + /* Floor 1 EPMv4 */ + p_tower->floor_1.protocol_id = PF_RPC_PROTOCOL_UUID; + if (epm_type == PF_EPM_TYPE_EPMV4) + { + memcpy ( + &p_tower->floor_1.uuid, + &uuid_epmap_interface, + sizeof (p_tower->floor_1.uuid)); + p_tower->floor_1.version_major = PF_RPC_FLOOR_VERSION_EPMv4; + } + else + { + memcpy ( + &p_tower->floor_1.uuid, + &uuid_io_device_interface, + sizeof (p_tower->floor_1.uuid)); + p_tower->floor_1.version_major = PF_RPC_FLOOR_VERSION_PNIO; + } + p_tower->floor_1.version_minor = PF_RPC_FLOOR_VERSION_MINOR; + + /* Floor 2 32bit NDR*/ + p_tower->floor_2.protocol_id = PF_RPC_PROTOCOL_UUID; + memcpy (&p_tower->floor_2.uuid, &uuid_ndr, sizeof (p_tower->floor_2.uuid)); + p_tower->floor_2.version_major = PF_RPC_FLOOR_VERSION_NPR; + p_tower->floor_2.version_minor = PF_RPC_FLOOR_VERSION_MINOR; + + /* Floor 3 RPC*/ + p_tower->floor_3.protocol_id = PF_RPC_PROTOCOL_CONNECTIONLESS; + p_tower->floor_3.version_minor = PF_RPC_FLOOR_VERSION_MINOR; + + /* Floor 4 UDP Listening port*/ + p_tower->floor_4.protocol_id = PF_RPC_PROTOCOL_DOD_UDP; + p_tower->floor_4.port = htons (udp_port); + + /* Floor 5 IP Address */ + p_tower->floor_5.protocol_id = PF_RPC_PROTOCOL_DOD_IP; + p_tower->floor_5.ip_address = 0; +} + +/** + * Add EPMv4 endpoint to lookup response + * @param net InOut: The p-net stack instance + * @param p_rpc_req In: Rpc header. + * @param p_lookup_req In: Lookup request. + * @param p_lookup_rsp Out: Lookup response. + * @param p_read_status Out: Read pnio status + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrdr_add_epmv4_entry ( + pnet_t * net, + const pf_rpc_header_t * p_rpc_req, + const pf_rpc_lookup_req_t * p_lookup_req, + pf_rpc_lookup_rsp_t * p_lookup_rsp, + pnet_result_t * p_read_status) +{ + pf_generate_epm_handle ( + os_get_current_time_us(), + net->pf_interface.main_port.mac_address, + &p_lookup_rsp->rpc_handle); + + p_lookup_rsp->num_entry++; + + memcpy ( + &p_lookup_rsp->rpc_entry.object_uuid, + &uuid_object, + sizeof (uuid_object)); + p_lookup_rsp->rpc_entry.offset = 0; + p_lookup_rsp->rpc_entry.actual_count++; + p_lookup_rsp->rpc_entry.max_count = p_lookup_rsp->rpc_entry.actual_count; + + pf_init_rpc_tower_entry ( + &p_lookup_rsp->rpc_entry.tower_entry, + PF_EPM_TYPE_EPMV4, + PF_RPC_SERVER_PORT); + + p_lookup_rsp->return_code = PF_RPC_OK; + return 0; +} + +/** + * Add PNIO endpoint to lookup response + * @param net InOut: The p-net stack instance + * @param p_rpc_req In: Rpc header. + * @param p_lookup_req In: Lookup request. + * @param p_lookup_rsp Out: Lookup response. + * @param p_read_status Out: Read pnio status + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrdr_add_pnio_entry ( + pnet_t * net, + const pf_rpc_header_t * p_rpc_req, + const pf_rpc_lookup_req_t * p_lookup_req, + pf_rpc_lookup_rsp_t * p_lookup_rsp, + pnet_result_t * p_read_status) +{ + pf_generate_epm_handle ( + os_get_current_time_us(), + net->pf_interface.main_port.mac_address, + &p_lookup_rsp->rpc_handle); + + /*Set the number of entries */ + p_lookup_rsp->num_entry++; + + /*Increment the RPC Entry counter*/ + p_lookup_rsp->rpc_entry.actual_count++; + + /*Offset is always 0*/ + p_lookup_rsp->rpc_entry.offset = 0; + + /*Fill in entry object UUID with the correct information*/ + memcpy ( + &p_lookup_rsp->rpc_entry.object_uuid, + &uuid_io_object_instance, + sizeof (uuid_io_object_instance)); + + /*Interface index (Todo Assume 1 interface for now)*/ + p_lookup_rsp->rpc_entry.object_uuid.node[0] = 0x00; + p_lookup_rsp->rpc_entry.object_uuid.node[1] = 0x01; + /*Device ID*/ + p_lookup_rsp->rpc_entry.object_uuid.node[2] = + net->fspm_cfg.device_id.device_id_hi; + p_lookup_rsp->rpc_entry.object_uuid.node[3] = + net->fspm_cfg.device_id.device_id_lo; + /*Vendor ID*/ + p_lookup_rsp->rpc_entry.object_uuid.node[4] = + net->fspm_cfg.device_id.vendor_id_hi; + p_lookup_rsp->rpc_entry.object_uuid.node[5] = + net->fspm_cfg.device_id.vendor_id_lo; + + pf_init_rpc_tower_entry ( + &p_lookup_rsp->rpc_entry.tower_entry, + PF_EPM_TYPE_PNIO, + p_lookup_req->udpPort); + + p_lookup_rsp->return_code = PF_RPC_OK; + return 0; +} + +/** + * Handle epm read all inquiry request + * PN-AL-protocol (Mar20) Section 4.10.3 + * + * PN-AL-services (Mar20) Figure 3 + * Sequence Chart for reading the EndPointMapper + * Base our return on the Sequence Number in RPC Request Header + * + * @param net InOut: The p-net stack instance + * @param p_rpc_req In: Rpc header. + * @param p_lookup_req In: Lookup request. + * @param p_lookup_rsp Out: Lookup response. + * @param p_read_status Out: Read pnio status + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmrdr_inquiry_read_all_reg_ind ( + pnet_t * net, + const pf_rpc_header_t * p_rpc_req, + const pf_rpc_lookup_req_t * p_lookup_req, + pf_rpc_lookup_rsp_t * p_lookup_rsp, + pnet_result_t * p_read_status) +{ + int ret = -1; + + switch (p_rpc_req->sequence_nmb) + { + case 0: + { + /* Check if handle is all NULL */ + if ( + memcmp ( + &p_lookup_req->rpc_handle, + &null_rpc_handle, + sizeof (null_rpc_handle)) == 0) + { + /* Send the EPM interface information */ + LOG_INFO ( + PF_RPC_LOG, + "EPM(%d): PLC is requesting EPM interface information.\n", + __LINE__); + ret = pf_cmrdr_add_epmv4_entry ( + net, + p_rpc_req, + p_lookup_req, + p_lookup_rsp, + p_read_status); + } + } + break; + case 1: + /* Check if handle is NOT NULL */ + if ( + memcmp ( + &p_lookup_req->rpc_handle, + &null_rpc_handle, + sizeof (null_rpc_handle)) != 0) + { + /* Send the PNIO interface information */ + LOG_INFO ( + PF_RPC_LOG, + "EPM(%d): PLC is requesting PNIO interface information via EPM.\n", + __LINE__); + ret = pf_cmrdr_add_pnio_entry ( + net, + p_rpc_req, + p_lookup_req, + p_lookup_rsp, + p_read_status); + } + break; + default: + break; + } + return ret; +} + +/** + * Handle epm lookup request + * + * @param net InOut: The p-net stack instance + * @param p_rpc_req In: Rpc header. + * @param p_lookup_req In: Lookup request. + * @param p_read_status Out: Read pnio status + * @param res_size In: The size of the output buffer. + * @param p_res Out: The output buffer. + * @param p_pos InOut: Position in the output buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmrpc_lookup_request ( + pnet_t * net, + const pf_rpc_header_t * p_rpc_req, + const pf_rpc_lookup_req_t * p_lookup_req, + pnet_result_t * p_read_status, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_pos) +{ + LOG_INFO ( + PF_RPC_LOG, + "EPM(%d): Received endpoint mapper (EPM) request.\n", + __LINE__); + + pf_rpc_lookup_rsp_t lookup_rsp; + + memset (&lookup_rsp, 0, sizeof (lookup_rsp)); + lookup_rsp.rpc_entry.tower_entry.p_cfg = &net->fspm_cfg; + lookup_rsp.rpc_entry.max_count = 1; + lookup_rsp.return_code = PF_RPC_NOT_REGISTERED; + + if (p_lookup_req->max_entries > 0) + { + switch (p_lookup_req->inquiry_type) + { + case PF_RPC_INQUIRY_READ_ALL_REGISTERED_INTERFACES: + pf_cmrdr_inquiry_read_all_reg_ind ( + net, + p_rpc_req, + p_lookup_req, + &lookup_rsp, + p_read_status); + break; + case PF_RPC_INQUIRY_READ_ALL_OBJECTS_FOR_ONE_INTERFACE: + case PF_RPC_INQUIRY_READ_ALL_INTERFACES_INCLUDING_OBJECTS: + case PF_RPC_INQUIRY_READ_ONE_INTERFACE_WITH_ONE_OBJECT: + /* Support is optional and not implemented */ + /* FALLTHROUGH */ + default: + /* Send PF_RPC_NOT_REGISTERED response */ + break; + } + } + + /* Write response buffer */ + pf_put_lookup_response_data (net, false, &lookup_rsp, res_size, p_res, p_pos); + return 0; +} diff --git a/profinet_stack/p-net/src/device/pf_cmrpc_epm.h b/profinet_stack/p-net/src/device/pf_cmrpc_epm.h new file mode 100644 index 0000000..296e883 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrpc_epm.h @@ -0,0 +1,48 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMRPC_EPM_H +#define PF_CMRPC_EPM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Handle rpc lookup request + * @param net InOut: The p-net stack instance + * @param p_rpc_req In: Rpc header. + * @param p_lookup_req In: Lookup request. + * @param p_read_status Out: Read pnio status + * @param res_size In: The size of the output buffer. + * @param p_res Out: The output buffer. + * @param p_pos InOut: Position in the output buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmrpc_lookup_request ( + pnet_t * net, + const pf_rpc_header_t * p_rpc_req, + const pf_rpc_lookup_req_t * p_lookup_req, + pnet_result_t * p_read_status, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_pos); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMRPC_HELPERS_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmrpc_helpers.c b/profinet_stack/p-net/src/device/pf_cmrpc_helpers.c new file mode 100644 index 0000000..3b41912 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrpc_helpers.c @@ -0,0 +1,57 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Helper functions for CMRPC + * + * Located in a separate file to enabling mocking during tests. + * + */ + +#include "pf_includes.h" + +/** + * @internal + * Generate an UUID, inspired by version 1, variant 1 + * + * See RFC 4122 + * + * @param timestamp In: Some kind of timestamp + * @param session_number In: Session number + * @param mac_address In: MAC address + * @param p_uuid Out: The generated UUID + */ +void pf_generate_uuid ( + uint32_t timestamp, + uint32_t session_number, + pnet_ethaddr_t mac_address, + pf_uuid_t * p_uuid) +{ + /* TODO Maybe we should take pointer to mac address instead, to avoid + copying? */ + p_uuid->data1 = timestamp; + p_uuid->data2 = session_number >> 16; + p_uuid->data3 = (session_number >> 8) & 0x00ff; + p_uuid->data3 |= 1 << 12; /* UUID version 1 */ + p_uuid->data4[0] = 0x80; /* UUID variant 1 */ + p_uuid->data4[1] = session_number & 0xff; + p_uuid->data4[2] = mac_address.addr[0]; + p_uuid->data4[3] = mac_address.addr[1]; + p_uuid->data4[4] = mac_address.addr[2]; + p_uuid->data4[5] = mac_address.addr[3]; + p_uuid->data4[6] = mac_address.addr[4]; + p_uuid->data4[7] = mac_address.addr[5]; +} diff --git a/profinet_stack/p-net/src/device/pf_cmrpc_helpers.h b/profinet_stack/p-net/src/device/pf_cmrpc_helpers.h new file mode 100644 index 0000000..8bb864d --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrpc_helpers.h @@ -0,0 +1,35 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMRPC_HELPERS_H +#define PF_CMRPC_HELPERS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/************ Internal functions, made available for unit testing ************/ + +void pf_generate_uuid ( + uint32_t timestamp, + uint32_t session_number, + pnet_ethaddr_t mac_address, + pf_uuid_t * p_uuid); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMRPC_HELPERS_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmrs.c b/profinet_stack/p-net/src/device/pf_cmrs.c new file mode 100644 index 0000000..3aa04c7 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrs.c @@ -0,0 +1,20 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST + +#endif + +#include "pf_includes.h" diff --git a/profinet_stack/p-net/src/device/pf_cmrs.h b/profinet_stack/p-net/src/device/pf_cmrs.h new file mode 100644 index 0000000..3d9b147 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmrs.h @@ -0,0 +1,27 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMRS_H +#define PF_CMRS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMRS_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmsm.c b/profinet_stack/p-net/src/device/pf_cmsm.c new file mode 100644 index 0000000..b116a20 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmsm.c @@ -0,0 +1,339 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Context Management Surveillance Protocol Machine Device + * (CMSM) + * + * The CMSM component monitors the establishment of a connection. + * Once the device enters the DATA state this component is done. + * + * An incoming CMDEV event PNET_EVENT_STARTUP starts the timer, + * and PNET_EVENT_DATA or PNET_EVENT_ABORT stops the timer. + * + * In case the timer times out, the CMDEV state is set to PNET_EVENT_ABORT. + * + * The time is extended (timer is restarted) for incoming RPC read and write + * requests, if the timer is running. + * + * If there is no ongoing connection (p_ar == NULL), then the timer is not + * affected by the rest of the stack. + */ + +#ifdef UNIT_TEST + +#endif + +#include "pf_includes.h" + +int pf_cmsm_activate (pf_ar_t * p_ar) +{ + if (p_ar == NULL) + { + return -1; + } + + pf_scheduler_init_handle (&p_ar->cmsm_timeout, "cmsm"); + return 0; +} + +/** + * @internal + * Return a string representation of the CMSM state. + * @param state In: The CMSM state + * @return A string representing the CMSM state. + */ +static const char * pf_cmsm_state_to_string (pf_cmsm_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_CMSM_STATE_IDLE: + s = "PF_CMSM_STATE_IDLE"; + break; + case PF_CMSM_STATE_RUN: + s = "PF_CMSM_STATE_RUN"; + break; + } + + return s; +} + +void pf_cmsm_show (const pf_ar_t * p_ar) +{ + if (p_ar == NULL) + { + printf ("CMSM: The AR is null\n"); + return; + } + + printf ( + "CMSM state = %s\n", + pf_cmsm_state_to_string (p_ar->cmsm_state)); + printf ( + " timer = %u\n", + (unsigned)pf_scheduler_get_value (&p_ar->cmsm_timeout)); +} + +/** + * @internal + * Set the state of the CMSM component. + * @param p_ar InOut: The AR instance (not NULL) + * @param state In: The new state. + */ +static void pf_cmsm_set_state (pf_ar_t * p_ar, pf_cmsm_state_values_t state) +{ + CC_ASSERT (p_ar != NULL); + + if (state != p_ar->cmsm_state) + { + LOG_DEBUG ( + PNET_LOG, + "CMSM(%d): New state %s (was %s)\n", + __LINE__, + pf_cmsm_state_to_string (state), + pf_cmsm_state_to_string (p_ar->cmsm_state)); + p_ar->cmsm_state = state; + } +} + +/** + * @internal + * Handle timeouts in the connection monitoring. + * Aborts the connection start-up procedure if timing out. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * @param net InOut: The p-net stack instance + * @param arg InOut: The AR instance (not NULL). Void pointer + * converted to pf_ar_t + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. Not used here. + */ +static void pf_cmsm_timeout (pnet_t * net, void * arg, uint32_t current_time) +{ + CC_ASSERT (arg != NULL); + pf_ar_t * p_ar = (pf_ar_t *)arg; + + pf_scheduler_reset_handle (&p_ar->cmsm_timeout); + switch (p_ar->cmsm_state) + { + case PF_CMSM_STATE_IDLE: + /* Ignore */ + break; + case PF_CMSM_STATE_RUN: + LOG_ERROR ( + PNET_LOG, + "CMSM(%d): Timeout for communication start up. CMDEV state: %s \n", + __LINE__, + pf_cmdev_state_to_string (p_ar->cmdev_state)); + + p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_ar->err_code = PNET_ERROR_CODE_2_ABORT_AR_CMI_TIMEOUT; + pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); + pf_cmsm_set_state (p_ar, PF_CMSM_STATE_IDLE); + break; + } +} + +/********************** Public functions affecting the timer *****************/ + +int pf_cmsm_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event) +{ + int ret = -1; + + if (p_ar == NULL) + { + return 0; + } + + LOG_DEBUG ( + PNET_LOG, + "CMSM(%d): Received event %s from CMDEV. Initial state %s for AREP %u.\n", + __LINE__, + pf_cmdev_event_to_string (event), + pf_cmsm_state_to_string (p_ar->cmsm_state), + p_ar->arep); + + switch (p_ar->cmsm_state) + { + case PF_CMSM_STATE_IDLE: + switch (event) + { + case PNET_EVENT_STARTUP: + pf_cmsm_set_state (p_ar, PF_CMSM_STATE_RUN); + LOG_DEBUG ( + PNET_LOG, + "CMSM(%d): Starting timer. cm_initiator_activity_timeout_factor %u " + "x 100 ms\n", + __LINE__, + (unsigned)p_ar->ar_param.cm_initiator_activity_timeout_factor); + (void)pf_scheduler_restart ( + net, + p_ar->ar_param.cm_initiator_activity_timeout_factor * 100 * + 1000, /* time in us */ + pf_cmsm_timeout, + (void *)p_ar, + &p_ar->cmsm_timeout); + ret = 0; + break; + default: + /* Ignore */ + ret = 0; + break; + } + break; + case PF_CMSM_STATE_RUN: + switch (event) + { + case PNET_EVENT_DATA: + case PNET_EVENT_ABORT: + if (pf_scheduler_is_running (&p_ar->cmsm_timeout)) + { + LOG_DEBUG ( + PNET_LOG, + "CMSM(%d): Stopping communication start-up timer. AREP %u\n", + __LINE__, + p_ar->arep); + } + pf_scheduler_remove_if_running (net, &p_ar->cmsm_timeout); + pf_cmsm_set_state (p_ar, PF_CMSM_STATE_IDLE); + ret = 0; + break; + default: + /* Ignore */ + ret = 0; + break; + } + break; + } + + return ret; +} + +int pf_cmsm_rm_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pf_iod_read_request_t * p_read_request) +{ + int ret = -1; + + if (p_ar == NULL) + { + return 0; + } + + switch (p_ar->cmsm_state) + { + case PF_CMSM_STATE_IDLE: + /* Ignore or positive rsp */ + ret = 0; + break; + case PF_CMSM_STATE_RUN: + if (p_read_request->index == PF_IDX_DEV_CONN_MON_TRIGGER) + { + /* Restart timeout period */ + (void)pf_scheduler_restart ( + net, + p_ar->ar_param.cm_initiator_activity_timeout_factor * 100 * + 1000, /* time in us */ + pf_cmsm_timeout, + (void *)p_ar, + &p_ar->cmsm_timeout); + } + ret = 0; + break; + } + + return ret; +} + +int pf_cmsm_cm_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_request) +{ + int ret = -1; + + if (p_ar == NULL) + { + return 0; + } + + switch (p_ar->cmsm_state) + { + case PF_CMSM_STATE_IDLE: + /* Ignore */ + ret = 0; + break; + case PF_CMSM_STATE_RUN: + /* Restart timeout period */ + (void)pf_scheduler_restart ( + net, + p_ar->ar_param.cm_initiator_activity_timeout_factor * 100 * 1000, + pf_cmsm_timeout, + (void *)p_ar, + &p_ar->cmsm_timeout); + ret = 0; + break; + } + + return ret; +} + +int pf_cmsm_cm_write_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_request) +{ + int ret = -1; + + if (p_ar == NULL) + { + return 0; + } + + switch (p_ar->cmsm_state) + { + case PF_CMSM_STATE_IDLE: + /* Ignore */ + ret = 0; + break; + case PF_CMSM_STATE_RUN: + /* Restart timeout period */ + (void)pf_scheduler_restart ( + net, + p_ar->ar_param.cm_initiator_activity_timeout_factor * 100 * 1000, + pf_cmsm_timeout, + (void *)p_ar, + &p_ar->cmsm_timeout); + ret = 0; + break; + } + + if (ret != 0) + { + LOG_ERROR (PNET_LOG, "CMSM(%d): Wrong state\n", __LINE__); + } + + return ret; +} diff --git a/profinet_stack/p-net/src/device/pf_cmsm.h b/profinet_stack/p-net/src/device/pf_cmsm.h new file mode 100644 index 0000000..1eafab6 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmsm.h @@ -0,0 +1,96 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMSM_H +#define PF_CMSM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the CMSM-specific parts of an AR + * @param p_ar In: The AR instance (not NULL) + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsm_activate (pf_ar_t * p_ar); + +/** + * Handle incoming CMDEV events. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance, or NULL. + * @param event In: The new CMDEV state. Use PNET_EVENT_xxx, not + * PF_CMDEV_STATE_xxx + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsm_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event); + +/** + * Handle incoming RPC read requests, by restarting the timer if running. + * Only if index is PF_IDX_DEV_CONN_MON_TRIGGER + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance, or NULL. + * @param p_read_request In: The read request. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsm_rm_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pf_iod_read_request_t * p_read_request); + +/** + * Handle incoming RPC read requests, by restarting the timer if running. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance, or NULL. + * @param p_read_request In: The read request. not used. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsm_cm_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_request); + +/** + * Handle incoming RPC write requests, by restarting the timer if running. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance, or NULL. + * @param p_write_request In: The write request. Not used. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsm_cm_write_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_request); + +/** + * Print information about CMSM for the specified AR. + * @param p_ar In: The AR instance, or NULL. + */ +void pf_cmsm_show (const pf_ar_t * p_ar); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMSM_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmsu.c b/profinet_stack/p-net/src/device/pf_cmsu.c new file mode 100644 index 0000000..1325ca9 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmsu.c @@ -0,0 +1,317 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements Context Management Startup Protocol Machine + * Device (CMSU) The CMSU of an IO device arranges the start of the + * underlying protocol machines. + * The startup of the CPM and PPM state machines shall be done between + * the reception of the connect request service and of the Application + * ready service. The startup shall be finished before issuing the + * Application ready request service. + */ + +#ifdef UNIT_TEST + +#endif + +#include +#include "pf_includes.h" + +void pf_cmsu_init (pnet_t * net, pf_ar_t * p_ar) +{ + p_ar->cmsu_state = PF_CMSU_STATE_IDLE; +} + +/** + * @internal + * Return a string representation of the CMSU state. + * @param state In: The CMSU state + * @return A string representing the CMSU state. + */ +static const char * pf_cmsu_state_to_string (pf_cmsu_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_CMSU_STATE_IDLE: + s = "PF_CMSU_STATE_IDLE"; + break; + case PF_CMSU_STATE_STARTUP: + s = "PF_CMSU_STATE_STARTUP"; + break; + case PF_CMSU_STATE_RUN: + s = "PF_CMSU_STATE_RUN"; + break; + case PF_CMSU_STATE_ABORT: + s = "PF_CMSU_STATE_ABORT"; + break; + } + + return s; +} + +/** + * @internal + * Handle state changes in the CMSU component. + * @param net InOut: The p-net stack instance + * @param state In: The new CMSU state. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmsu_set_state ( + pnet_t * net, + pf_ar_t * p_ar, + pf_cmsu_state_values_t state) +{ + LOG_DEBUG ( + PNET_LOG, + "CMSU(%d): New state %s for AREP %u (was %s)\n", + __LINE__, + pf_cmsu_state_to_string (state), + p_ar->arep, + pf_cmsu_state_to_string (p_ar->cmsu_state)); + + p_ar->cmsu_state = state; + + return 0; +} + +int pf_cmsu_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event) +{ + uint32_t crep; + + LOG_DEBUG ( + PNET_LOG, + "CMSU(%d): Received event %s from CMDEV. Initial state %s for AREP %u.\n", + __LINE__, + pf_cmdev_event_to_string (event), + pf_cmsu_state_to_string (p_ar->cmsu_state), + p_ar->arep); + + switch (p_ar->cmsu_state) + { + case PF_CMSU_STATE_IDLE: + break; + case PF_CMSU_STATE_STARTUP: + if (event == PNET_EVENT_PRMEND) + { + for (crep = 0; crep < p_ar->nbr_iocrs; crep++) + { + if (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_INPUT) + { + if (pf_ppm_activate_req (net, p_ar, crep) != 0) + { + LOG_ERROR ( + PNET_LOG, + "CMSU(%d): Failed to activate_ppm for AREP %u.\n", + __LINE__, + p_ar->arep); + } + } + } + } + /* fallthrough */ + case PF_CMSU_STATE_RUN: + if (event == PNET_EVENT_ABORT) + { + LOG_DEBUG ( + PNET_LOG, + "CMSU(%d): Received event PNET_EVENT_ABORT from CMDEV. Closing " + "PPM, CPM and alarm instances for AREP %u. Sending alarm with " + "error_code2 = 0x%X\n", + __LINE__, + p_ar->arep, + p_ar->err_code); + for (crep = 0; crep < p_ar->nbr_iocrs; crep++) + { + if (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_INPUT) + { + pf_ppm_close_req (net, p_ar, crep); + } + if (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_OUTPUT) + { + pf_cpm_close_req (net, p_ar, crep); + } + } + + pf_alarm_close (net, p_ar); + + pf_cmdmc_close_req (p_ar); + + /* ToDo: Cleanup static ARP cache entry */ + /* ACCM_req(Command: REMOVE, ip_address) */ + pf_cmsu_set_state (net, p_ar, PF_CMSU_STATE_ABORT); + /* All *_cnf have been received */ + pf_cmsu_set_state (net, p_ar, PF_CMSU_STATE_IDLE); + } + break; + case PF_CMSU_STATE_ABORT: + default: + LOG_ERROR ( + PNET_LOG, + "CMSU(%d): Illegal state in cmsu %d\n", + __LINE__, + p_ar->cmsu_state); + break; + } + return 0; +} + +int pf_cmsu_start_req (pnet_t * net, pf_ar_t * p_ar, pnet_result_t * p_stat) +{ + int ret = -1; + uint32_t crep; + + switch (p_ar->cmsu_state) + { + case PF_CMSU_STATE_IDLE: + ret = 0; /* Assume all goes well */ + for (crep = 0; crep < p_ar->nbr_iocrs; crep++) + { + if (ret == 0) + { + if (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_OUTPUT) + { + if (pf_cpm_create (net, p_ar, crep) != 0) + { + p_stat->pnio_status.error_code_2 = + PNET_ERROR_CODE_2_CMSU_AR_ADD_PROV_CONS_FAILED; + ret = -1; + } + else if (pf_cpm_activate_req (net, p_ar, crep) != 0) + { + p_stat->pnio_status.error_code_2 = + PNET_ERROR_CODE_2_CMSU_AR_ADD_PROV_CONS_FAILED; + ret = -1; + } + } + } + if (ret == 0) + { + if (p_ar->iocrs[crep].param.iocr_type == PF_IOCR_TYPE_INPUT) + { + if (pf_ppm_create (net, p_ar, crep) != 0) + { + p_stat->pnio_status.error_code_2 = + PNET_ERROR_CODE_2_CMSU_AR_ADD_PROV_CONS_FAILED; + ret = -1; + } + } + } + } + if ((ret == 0) && (pf_alarm_activate (net, p_ar) != 0)) + { + p_stat->pnio_status.error_code_2 = + PNET_ERROR_CODE_2_CMSU_AR_ALARM_OPEN_FAILED; + ret = -1; + } + if ((ret == 0) && (pf_cmdmc_activate_req (p_ar) != 0)) + { + p_stat->pnio_status.error_code_2 = + PNET_ERROR_CODE_2_CMDEV_STATE_CONFLICT; + ret = -1; + } + if ((ret == 0) && (pf_cmsm_activate (p_ar) != 0)) + { + p_stat->pnio_status.error_code_2 = + PNET_ERROR_CODE_2_CMSM_SIGNALED_ERROR; + ret = -1; + } + +#if 0 + if ((ret == 0) && (pf_dfp_activate_req(p_ar) != 0)) + { + p_stat->pnio_status.error_code_2 = PNET_ERROR_CODE_2_CMSU_AR_ADD_PROV_CONS_FAILED; + ret = -1; + } +#endif + + /* ToDo: Create static ARP cache entry */ + /* ACCM_req(Command: ADD, ip_address, mac_address) */ + + pf_cmsu_set_state (net, p_ar, PF_CMSU_STATE_STARTUP); + /* CMSU_Start.cnf */ + break; + case PF_CMSU_STATE_STARTUP: + p_stat->pnio_status.error_code_1 = PNET_ERROR_CODE_1_CMDEV; + p_stat->pnio_status.error_code_2 = PNET_ERROR_CODE_2_CMDEV_STATE_CONFLICT; + ret = -1; + break; + case PF_CMSU_STATE_RUN: + p_stat->pnio_status.error_code_1 = PNET_ERROR_CODE_1_CMDEV; + p_stat->pnio_status.error_code_2 = PNET_ERROR_CODE_2_CMDEV_STATE_CONFLICT; + ret = -1; + break; + default: + LOG_ERROR ( + PNET_LOG, + "CMSU(%d): Illegal state in cmsu %d\n", + __LINE__, + p_ar->cmsu_state); + break; + } + + return ret; +} + +int pf_cmsu_cmdmc_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code) +{ + p_ar->err_cls = err_cls; + p_ar->err_code = err_code; + return pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); +} + +int pf_cmsu_cpm_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code) +{ + p_ar->err_cls = err_cls; + p_ar->err_code = err_code; + return pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); +} + +int pf_cmsu_ppm_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code) +{ + p_ar->err_cls = err_cls; + p_ar->err_code = err_code; + return pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); +} + +int pf_cmsu_alarm_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code) +{ + p_ar->err_cls = err_cls; + p_ar->err_code = err_code; + return pf_cmdev_state_ind (net, p_ar, PNET_EVENT_ABORT); +} diff --git a/profinet_stack/p-net/src/device/pf_cmsu.h b/profinet_stack/p-net/src/device/pf_cmsu.h new file mode 100644 index 0000000..3bfca6d --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmsu.h @@ -0,0 +1,137 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMSU_H +#define PF_CMSU_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the CMSU component. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + */ +void pf_cmsu_init (pnet_t * net, pf_ar_t * p_ar); + +/** + * Handle CMDEV events. + * + * Can close CPM and PPM instances. Closes alarm instance, which triggers an + * alarm. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param event In: The new CMDEV state. Use PNET_EVENT_xxx, not + * PF_CMDEV_STATE_xxx + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsu_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event); + +/** + * Start all state machines for a specific AR. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_stat Out: Detailed error info if returning != 0 + * Note that only the fields error_code_1 and + * error_code_2 are set. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsu_start_req (pnet_t * net, pf_ar_t * p_ar, pnet_result_t * p_stat); + +/** + * Handle CPM error indications for a specific AR. + * + * Closes the AR with PNET_EVENT_ABORT + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param err_cls In: ERR_CLS. ErrorCode1. + * @param err_code In: ERR_CODE. ErrorCode2. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsu_cpm_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code); + +/** + * Handle PPM error indications for a specific AR. + * + * Closes the AR with PNET_EVENT_ABORT + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param err_cls In: ERR_CLS. ErrorCode1. + * @param err_code In: ERR_CODE. ErrorCode2. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsu_ppm_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code); + +/** + * Handle alarm error indications for a specific AR. + * + * Closes the AR with PNET_EVENT_ABORT + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param err_cls In: ERR_CLS. ErrorCode1. + * @param err_code In: ERR_CODE. ErrorCode2. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsu_alarm_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code); + +/* Not used */ +/** + * Handle DMC error indications for a specific AR. + * + * Closes the AR with PNET_EVENT_ABORT + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param err_cls In: ERR_CLS. ErrorCode1. + * @param err_code In: ERR_CODE. ErrorCode2. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_cmsu_cmdmc_error_ind ( + pnet_t * net, + pf_ar_t * p_ar, + uint8_t err_cls, + uint8_t err_code); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMSU_H */ diff --git a/profinet_stack/p-net/src/device/pf_cmwrr.c b/profinet_stack/p-net/src/device/pf_cmwrr.c new file mode 100644 index 0000000..822f6cd --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmwrr.c @@ -0,0 +1,364 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST + +#endif + +#include "pf_includes.h" +#include "pf_block_reader.h" +#include "pf_block_writer.h" +#include "pf_block_reader.h" + +/** + * @file + * @brief Implements the Context Management Write Record Responder protocol + * machine device (CMWRR) + * + * Handles a RPC parameter write request. + * + * Triggers the \a pnet_write_ind() user callback for some values. + */ + +void pf_cmwrr_init (pnet_t * net, pf_ar_t * p_ar) +{ + p_ar->cmwrr_state = PF_CMWRR_STATE_IDLE; +} + +/** + * @internal + * Return a string representation of a CMWRR state. + * @param state In: The CMWRR state + * @return A string representation of the CMWRR state. + */ +static const char * pf_cmwrr_state_to_string (pf_cmwrr_state_values_t state) +{ + const char * s = ""; + + switch (state) + { + case PF_CMWRR_STATE_IDLE: + s = "PF_CMWRR_STATE_IDLE"; + break; + case PF_CMWRR_STATE_STARTUP: + s = "PF_CMWRR_STATE_STARTUP"; + break; + case PF_CMWRR_STATE_PRMEND: + s = "PF_CMWRR_STATE_PRMEND"; + break; + case PF_CMWRR_STATE_DATA: + s = "PF_CMWRR_STATE_DATA"; + break; + } + + return s; +} + +void pf_cmwrr_show (const pnet_t * net, const pf_ar_t * p_ar) +{ + const char * s = pf_cmwrr_state_to_string (p_ar->cmwrr_state); + + printf ("CMWRR state = %s\n", s); + (void)s; +} + +int pf_cmwrr_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event) +{ + int ret = -1; + pf_cmwrr_state_values_t initial_state = p_ar->cmwrr_state; + + LOG_DEBUG ( + PNET_LOG, + "CMWRR(%d): Received event %s from CMDEV. Initial state %s for AREP " + "%u.\n", + __LINE__, + pf_cmdev_event_to_string (event), + pf_cmwrr_state_to_string (p_ar->cmwrr_state), + p_ar->arep); + + switch (p_ar->cmwrr_state) + { + case PF_CMWRR_STATE_IDLE: + if (event == PNET_EVENT_STARTUP) + { + p_ar->cmwrr_state = PF_CMWRR_STATE_STARTUP; + } + ret = 0; + break; + case PF_CMWRR_STATE_STARTUP: + if (event == PNET_EVENT_PRMEND) + { + p_ar->cmwrr_state = PF_CMWRR_STATE_PRMEND; + } + else if (event == PNET_EVENT_ABORT) + { + p_ar->cmwrr_state = PF_CMWRR_STATE_IDLE; + } + ret = 0; + break; + case PF_CMWRR_STATE_PRMEND: + if (event == PNET_EVENT_ABORT) + { + p_ar->cmwrr_state = PF_CMWRR_STATE_IDLE; + } + else if (event == PNET_EVENT_APPLRDY) + { + p_ar->cmwrr_state = PF_CMWRR_STATE_DATA; + } + ret = 0; + break; + case PF_CMWRR_STATE_DATA: + if (event == PNET_EVENT_ABORT) + { + p_ar->cmwrr_state = PF_CMWRR_STATE_IDLE; + } + ret = 0; + break; + default: + LOG_ERROR ( + PNET_LOG, + "CMWRR(%d): BAD state in cmwrr %u\n", + __LINE__, + (unsigned)p_ar->cmwrr_state); + break; + } + + if (p_ar->cmwrr_state != initial_state) + { + LOG_DEBUG ( + PNET_LOG, + "CMWRR(%d): New state state %s for AREP %u.\n", + __LINE__, + pf_cmwrr_state_to_string (p_ar->cmwrr_state), + p_ar->arep); + } + + return ret; +} + +/** + * @internal + * Write one data record. + * + * This function performs a write of one data record. + * The index in the IODWrite request selects the item to write to. + * + * Triggers the \a pnet_write_ind() user callback for some values. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_write_request In: The IODWrite request. + * @param p_req_buf In: The request buffer. + * @param data_length In: Size of the data to write. + * @param p_req_pos InOut: Position within the request buffer. + * @param p_result Out: Detailed error information if returning != 0 + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_cmwrr_write ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_request, + const uint8_t * p_req_buf, + uint16_t data_length, + uint16_t * p_req_pos, + pnet_result_t * p_result) +{ + int ret = -1; + + if ( + (p_write_request->slot_number == PNET_SLOT_DAP_IDENT) && + (pf_port_subslot_is_dap_port_id (net, p_write_request->subslot_number))) + { + LOG_INFO ( + PNET_LOG, + "CMWRR(%d): PLC is writing slot %u subslot 0x%04X index 0x%04X " + "(local port %u) \"%s\" for AREP %u\n", + __LINE__, + p_write_request->slot_number, + p_write_request->subslot_number, + p_write_request->index, + pf_port_dap_subslot_to_local_port ( + net, + p_write_request->subslot_number), + pf_index_to_logstring (p_write_request->index), + p_ar->arep); + } + else + { + LOG_INFO ( + PNET_LOG, + "CMWRR(%d): PLC is writing slot %u subslot 0x%04X index 0x%04X " + "\"%s\" for AREP %u\n", + __LINE__, + p_write_request->slot_number, + p_write_request->subslot_number, + p_write_request->index, + pf_index_to_logstring (p_write_request->index), + p_ar->arep); + } + + if (p_write_request->index <= PF_IDX_USER_MAX) + { + /* User defined indexes */ + ret = pf_fspm_cm_write_ind ( + net, + p_ar, + p_write_request, + data_length, + &p_req_buf[*p_req_pos], + p_result); + } + else if ( + (PF_IDX_SUB_IM_0 <= p_write_request->index) && + (p_write_request->index <= PF_IDX_SUB_IM_15)) + { + /* I&M data */ + ret = pf_fspm_cm_write_ind ( + net, + p_ar, + p_write_request, + data_length, + &p_req_buf[*p_req_pos], + p_result); + } + else + { + switch (p_write_request->index) + { + case PF_IDX_SUB_PDPORT_DATA_CHECK: + case PF_IDX_SUB_PDPORT_DATA_ADJ: + case PF_IDX_SUB_PDINTF_ADJUST: + ret = pf_pdport_write_req ( + net, + p_ar, + p_write_request, + &p_req_buf[*p_req_pos], + data_length, + p_result); + break; + default: + break; + } + } + + (*p_req_pos) += data_length; + + if (ret != 0) + { + LOG_INFO ( + PNET_LOG, + "CMRWRR(%d): Could not write index 0x%04X for slot %u subslot " + "0x%04X. Error 0x%02X %02X %02X %02X Add %u %u\n", + __LINE__, + p_write_request->index, + p_write_request->slot_number, + p_write_request->subslot_number, + p_result->pnio_status.error_code, + p_result->pnio_status.error_decode, + p_result->pnio_status.error_code_1, + p_result->pnio_status.error_code_2, + p_result->add_data_1, + p_result->add_data_2); + } + return ret; +} + +int pf_cmwrr_rm_write_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_request, + pf_iod_write_result_t * p_write_result, + pnet_result_t * p_result, + const uint8_t * p_req_buf, + uint16_t data_length, + uint16_t * p_req_pos) +{ + int ret = -1; + + p_write_result->sequence_number = p_write_request->sequence_number; + p_write_result->ar_uuid = p_write_request->ar_uuid; + p_write_result->api = p_write_request->api; + p_write_result->slot_number = p_write_request->slot_number; + p_write_result->subslot_number = p_write_request->subslot_number; + p_write_result->index = p_write_request->index; + p_write_result->record_data_length = 0; + + switch (p_ar->cmwrr_state) + { + case PF_CMWRR_STATE_IDLE: + p_result->pnio_status.error_code = PNET_ERROR_CODE_PNIO; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_ACC_STATE_CONFLICT; + break; + case PF_CMWRR_STATE_STARTUP: + if (p_ar->ar_state == PF_AR_STATE_BACKUP) + { + p_result->pnio_status.error_code = PNET_ERROR_CODE_PNIO; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_ACC_BACKUP; + } + else + { + ret = pf_cmwrr_write ( + net, + p_ar, + p_write_request, + p_req_buf, + data_length, + p_req_pos, + p_result); + } + break; + case PF_CMWRR_STATE_PRMEND: + p_result->pnio_status.error_code = PNET_ERROR_CODE_PNIO; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_ACC_STATE_CONFLICT; + break; + case PF_CMWRR_STATE_DATA: + if (p_ar->ar_state == PF_AR_STATE_BACKUP) + { + p_result->pnio_status.error_code = PNET_ERROR_CODE_PNIO; + p_result->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_result->pnio_status.error_code_1 = PNET_ERROR_CODE_1_ACC_BACKUP; + } + else + { + ret = pf_cmwrr_write ( + net, + p_ar, + p_write_request, + p_req_buf, + data_length, + p_req_pos, + p_result); + } + break; + } + + p_write_result->add_data_1 = p_result->add_data_1; + p_write_result->add_data_2 = p_result->add_data_2; + + /* Update the CMSM timer, which monitors start-up timeout */ + if (pf_cmsm_cm_write_ind (net, p_ar, p_write_request) != 0) + { + ret = -1; + } + + return ret; +} diff --git a/profinet_stack/p-net/src/device/pf_cmwrr.h b/profinet_stack/p-net/src/device/pf_cmwrr.h new file mode 100644 index 0000000..9afe526 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_cmwrr.h @@ -0,0 +1,82 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_CMWRR_H +#define PF_CMWRR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the CMWRR component. + * @param net InOut: The p-net stack instance + */ +void pf_cmwrr_init (pnet_t * net, pf_ar_t * p_ar); + +/** + * Show the CMWRR part of the specified AR. + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + */ +void pf_cmwrr_show (const pnet_t * net, const pf_ar_t * p_ar); + +/** + * Handle CMDEV events. + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. Not used, + * @param event In: The new CMDEV state. Use PNET_EVENT_xxx, not + * PF_CMDEV_STATE_xxx + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmwrr_cmdev_state_ind ( + pnet_t * net, + pf_ar_t * p_ar, + pnet_event_values_t event); + +/** + * Handle RPC write requests. + * + * If the state is correct, it will trigger pf_cmwrr_write() + * + * Triggers the \a pnet_write_ind() user callback for some values. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_write_request In: The write request block. + * @param p_write_result Out: The write result block. + * @param p_result Out: Detailed error information if returning != 0 + * @param p_req_buf In: The RPC request buffer. + * @param data_length In: The length of the data to write. + * @param p_req_pos In: Position in p_req_buf. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_cmwrr_rm_write_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_request, + pf_iod_write_result_t * p_write_result, + pnet_result_t * p_result, + const uint8_t * p_req_buf, + uint16_t data_length, + uint16_t * p_req_pos); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_CMWRR_H */ diff --git a/profinet_stack/p-net/src/device/pf_diag.c b/profinet_stack/p-net/src/device/pf_diag.c new file mode 100644 index 0000000..b044b26 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_diag.c @@ -0,0 +1,1165 @@ + +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + * See LICENSE file in the project root for full license information. + ********************************************************************/ + +/** + * @file + * @brief Diagnosis implementation + * + * Add, update and remove diagnosis items (and send alarms accordingly). + * + * Uses the alarm implementation via the pf_alarm_send_diagnosis() function. + * + * The bookkeeping of diag items is done by CMDEV via: + * - pf_cmdev_get_device() + * - pf_cmdev_get_subslot_full() + * - pf_cmdev_new_diag() + * - pf_cmdev_get_diag_item() + * - pf_cmdev_free_diag() + * + * An array of PNET_MAX_DIAG_ITEMS diagnosis items is available for use. + * In CMDEV, each subslot uses a linked list of diagnosis items, and stores the + * index to the head of its (possibly empty) list. + */ + +#ifdef UNIT_TEST +#define pf_alarm_send_diagnosis mock_pf_alarm_send_diagnosis +#endif + +#include +#include +#include +#include + +#include "pf_includes.h" +#include "pf_block_reader.h" +#include "pf_block_writer.h" + +#if PNET_MAX_DIAG_ITEMS > 65534 +#error "PNET_MAX_DIAG_ITEMS is too large" +#endif + +int pf_diag_init (void) +{ + return 0; +} + +int pf_diag_exit (void) +{ + return 0; +} + +/** + * @internal + * Update the problem indicator in the PPM data status + * by looking at all diagnosis items of a sub-slot. + * + * @param net InOut: The p-net stack instance. + * @param p_ar InOut: The AR instance. + * @param p_subslot In: The sub-slot instance. + */ +static void pf_diag_update_station_problem_indicator ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_subslot_t * p_subslot) +{ + bool is_problem = false; + pf_device_t * p_dev = NULL; + uint16_t ix; + pf_diag_item_t * p_item; + + if (pf_cmdev_get_device (net, &p_dev) == 0) + { + /* A problem is indicated if at least one FAULT diagnosis exists. */ + ix = p_subslot->diag_list; + pf_cmdev_get_diag_item (net, ix, &p_item); + while (p_item != NULL) + { + if ( + (p_item->in_use == true) && + ((p_item->usi < PF_USI_CHANNEL_DIAGNOSIS) || + (PF_DIAG_CH_PROP_MAINT_GET (p_item->fmt.std.ch_properties) == + PNET_DIAG_CH_PROP_MAINT_FAULT))) + { + is_problem = true; + } + + ix = p_item->next; + pf_cmdev_get_diag_item (net, ix, &p_item); + } + } + + pf_ppm_set_problem_indicator (net, p_ar, is_problem); +} + +/** + * @internal + * Update the submodule diff state + * + * @param net InOut: The p-net stack instance. + * @param p_ar InOut: The AR instance. + * @param p_subslot In: The sub-slot instance. + */ +static void pf_diag_update_submodule_state ( + pnet_t * net, + pf_ar_t * p_ar, + pf_subslot_t * p_subslot) +{ + pf_device_t * p_dev = NULL; + uint16_t ix; + pf_diag_item_t * p_item; + pf_submod_state_t submodule_state; + pnet_alarm_spec_t alarm_spec = {0}; + uint32_t maint_status = 0; + + memset (&submodule_state, 0, sizeof (submodule_state)); + + if (pf_cmdev_get_device (net, &p_dev) == 0) + { + ix = p_subslot->diag_list; + pf_cmdev_get_diag_item (net, ix, &p_item); + while (p_item != NULL) + { + if (p_item->in_use == true) + { + pf_alarm_add_diag_item_to_summary ( + p_ar, + p_subslot, + p_item, + &alarm_spec, + &maint_status); + + if (alarm_spec.submodule_diagnosis == true) + { + submodule_state.fault = true; + } + + if (maint_status & PF_DIAG_BIT_MAINTENANCE_REQUIRED) + { + submodule_state.maintenance_required = true; + } + + if (maint_status & PF_DIAG_BIT_MAINTENANCE_DEMANDED) + { + submodule_state.maintenance_demanded = true; + } + } + + ix = p_item->next; + pf_cmdev_get_diag_item (net, ix, &p_item); + } + + p_subslot->submodule_state.fault = submodule_state.fault; + p_subslot->submodule_state.maintenance_required = + submodule_state.maintenance_required; + p_subslot->submodule_state.maintenance_demanded = + submodule_state.maintenance_demanded; + } +} + +/** + * @internal + * Find and unlink a diag item in the specified sub-slot. + * + * If the USI of the item is in the manufacturer-specified range then + * the USI is used to identify the item. + * Otherwise the other parameters are used (all must match): + * - p_diag_source (all fields) + * - Channel error type. + * - Extended error type. + * + * Note that the severity or ExtChannelErrorAddValue are not used for + * identification, so there could be no two diagnosis where these values + * differ but the values above are the same. + * + * Similarly for diagnosis in USI format, the ManufacturerData is not used + * for identification. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type, or 0. + * @param usi In: The USI. + * @param pp_subslot Out: The sub-slot instance, or NULL if not found. + * @param p_diag_ix Out: The diag item index. + */ +static void pf_diag_find_entry ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint16_t usi, + pf_subslot_t ** pp_subslot, + uint16_t * p_diag_ix) +{ + pf_diag_item_t * p_item = NULL; + pf_diag_item_t * p_prev = NULL; + uint16_t prev_ix; + uint16_t item_ix; + + *p_diag_ix = PF_DIAG_IX_NULL; + *pp_subslot = NULL; + if ( + pf_cmdev_get_subslot_full ( + net, + p_diag_source->api, + p_diag_source->slot, + p_diag_source->subslot, + pp_subslot) == 0) + { + /* Only search valid sub-modules */ + if ( + (((*pp_subslot)->submodule_state.ident_info == PF_SUBMOD_PLUG_OK) || + ((*pp_subslot)->submodule_state.ident_info == + PF_SUBMOD_PLUG_SUBSTITUTE)) && + (((*pp_subslot)->submodule_state.ar_info == PF_SUBMOD_AR_INFO_OWN) || + ((*pp_subslot)->submodule_state.ar_info == + PF_SUBMOD_AR_INFO_APPLICATION_READY_PENDING))) + { + prev_ix = PF_DIAG_IX_NULL; + item_ix = (*pp_subslot)->diag_list; + pf_cmdev_get_diag_item (net, item_ix, &p_item); + while ((p_item != NULL) && (*p_diag_ix == PF_DIAG_IX_NULL)) + { + /* Found a match if all of: + * format is STD. + * channel number matches. + * individual channel or channel group + * direction matches. + * ch_error_type matches. + * ext_ch_error_type matches. + * ToDo: How do we find manuf_data diag entries?? + */ + if (p_item->usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + if ( + (p_item->fmt.std.ch_nbr == p_diag_source->ch) && + (p_item->fmt.std.ch_error_type == ch_error_type) && + (p_item->fmt.std.ext_ch_error_type == ext_ch_error_type) && + (PF_DIAG_CH_PROP_ACC_GET (p_item->fmt.std.ch_properties) == + p_diag_source->ch_grouping) && + (PF_DIAG_CH_PROP_DIR_GET (p_item->fmt.std.ch_properties) == + p_diag_source->ch_direction)) + { + *p_diag_ix = item_ix; + + /* Unlink it from the list so it can be updated. */ + if (prev_ix != PF_DIAG_IX_NULL) + { + /* Not first in list */ + pf_cmdev_get_diag_item (net, prev_ix, &p_prev); + p_prev->next = p_item->next; + } + else + { + /* Unlink the first item in the list */ + (*pp_subslot)->diag_list = p_item->next; + } + } + } + else if (p_item->usi == usi) + { + *p_diag_ix = item_ix; + + /* Unlink it from the list so it can be updated. */ + if (prev_ix != PF_DIAG_IX_NULL) + { + /* Not first in list */ + pf_cmdev_get_diag_item (net, prev_ix, &p_prev); + p_prev->next = p_item->next; + } + else + { + /* Unlink the first item in the list */ + (*pp_subslot)->diag_list = p_item->next; + } + } + + prev_ix = item_ix; + item_ix = p_item->next; + pf_cmdev_get_diag_item (net, item_ix, &p_item); + } + } + else + { + /* Signal not found! */ + *pp_subslot = NULL; + } + } +} + +/** + * @internal + * Find first used ar. + * + * @param net InOut: The p-net stack instance. + * @param pp_ar Out: Resulting ar + * @return 0 on success, -1 on error. + */ +static int pf_diag_find_first_ar (pnet_t * net, pf_ar_t ** pp_ar) +{ + int ret = -1; + uint16_t ix; + + for (ix = 0; ix < NELEMENTS (net->cmrpc_ar); ix++) + { + if (net->cmrpc_ar[ix].in_use == true) + { + *pp_ar = &net->cmrpc_ar[ix]; + ret = 0; + break; + } + } + + return ret; +} + +int pf_diag_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + int ret = -1; + pf_device_t * p_dev = NULL; + pf_subslot_t * p_subslot = NULL; + uint16_t item_ix = PF_DIAG_IX_NULL; + pf_diag_item_t * p_item = NULL; + bool overwrite = false; + pf_diag_item_t old_item; + uint16_t ch_properties = 0; + pf_ar_t * p_ar = NULL; + + /* Remove reserved bits before we use it */ + /* TODO: Validate qual_ch_qualifier */ + qual_ch_qualifier &= PF_DIAG_QUALIFIED_SEVERITY_MASK; + + if (usi > PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS || usi == PF_USI_ALARM_MULTIPLE) + { + LOG_ERROR (PF_ALARM_LOG, "DIAG(%d): The given USI is invalid\n", __LINE__); + } + else if (usi < PF_USI_CHANNEL_DIAGNOSIS && ch_error_type != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ch_error_type should not be used for USI %u\n", + __LINE__, + usi); + } + else if (usi <= PF_USI_CHANNEL_DIAGNOSIS && ext_ch_error_type != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ext_ch_error_type should not be used for USI %u\n", + __LINE__, + usi); + } + else if (usi <= PF_USI_CHANNEL_DIAGNOSIS && ext_ch_add_value != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ext_ch_add_value should not be used for USI %u\n", + __LINE__, + usi); + } + else if (usi != PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS && qual_ch_qualifier != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The qual_ch_qualifier should only be used when " + "usi=PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS\n", + __LINE__); + } + else if ( + usi != PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS && + severity == PNET_DIAG_CH_PROP_MAINT_QUALIFIED) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The severity PNET_DIAG_CH_PROP_MAINT_QUALIFIED should only " + "be used when usi=PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS\n", + __LINE__); + } + else if (qual_ch_qualifier != 0 && severity != PNET_DIAG_CH_PROP_MAINT_QUALIFIED) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The qual_ch_qualifier should only be used when " + "severity=PNET_DIAG_CH_PROP_MAINT_QUALIFIED\n", + __LINE__); + } + else if ( + usi >= PF_USI_CHANNEL_DIAGNOSIS && + (p_manuf_data != NULL || manuf_data_len != 0)) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Manufacturer specific data should only be given for USI up " + "to 0x7fff\n", + __LINE__); + } + else if (manuf_data_len > 0 && p_manuf_data == NULL) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): manuf_data_len is %u but no data is given.\n", + __LINE__, + manuf_data_len); + } + else if (p_diag_source->ch > PNET_CHANNEL_WHOLE_SUBMODULE) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Illegal channel %u\n", + __LINE__, + p_diag_source->ch); + } + else if (manuf_data_len > sizeof (p_item->fmt.usi.manuf_data)) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Too long manufacturer specific data given: %u Max: %zu " + "Increase PNET_MAX_DIAG_MANUF_DATA_SIZE\n", + __LINE__, + manuf_data_len, + sizeof (p_item->fmt.usi.manuf_data)); + } + else if (pf_cmdev_get_device (net, &p_dev) == 0) + { + os_mutex_lock (p_dev->diag_mutex); + + pf_diag_find_entry ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + usi, + &p_subslot, + &item_ix); + + if (p_subslot != NULL) + { + if (item_ix == PF_DIAG_IX_NULL) + { + /* Allocate a new entry and determine format */ + if (pf_cmdev_new_diag (net, &item_ix) == 0) + { + (void)pf_cmdev_get_diag_item (net, item_ix, &p_item); + } + else + { + /* Diag overflow */ + /* ToDo: Handle diag overflow */ + LOG_ERROR (PF_ALARM_LOG, "DIAG(%d): overflow\n", __LINE__); + } + } + else + { + /* Re-use the old entry */ + (void)pf_cmdev_get_diag_item (net, item_ix, &p_item); + overwrite = true; + } + + if (p_item != NULL) + { + old_item = *p_item; /* In case we need to remove the old - after + inserting the new */ + + if (usi < PF_USI_CHANNEL_DIAGNOSIS) + { + /* Manufacturer specific (USI) format */ + LOG_DEBUG ( + PF_ALARM_LOG, + "DIAG(%d): Adding manufacturer specific diag, ix %u. USI " + "0x%04X Slot %u Subslot 0x%04X\n", + __LINE__, + item_ix, + usi, + p_diag_source->slot, + p_diag_source->subslot); + + p_item->usi = usi; + p_item->fmt.usi.len = manuf_data_len; + if (manuf_data_len > 0) + { + memcpy ( + p_item->fmt.usi.manuf_data, + p_manuf_data, + manuf_data_len); + } + } + else + { + /* Standard format */ + LOG_DEBUG ( + PF_ALARM_LOG, + "DIAG(%d): Adding standard format diagnosis, ix %u. USI " + "0x%04X Slot " + "%u Subslot 0x%04X Channel 0x%04X\n", + __LINE__, + item_ix, + usi, + p_diag_source->slot, + p_diag_source->subslot, + p_diag_source->ch); + + PF_DIAG_CH_PROP_TYPE_SET (ch_properties, ch_bits); + PF_DIAG_CH_PROP_ACC_SET ( + ch_properties, + p_diag_source->ch_grouping == PNET_DIAG_CH_CHANNEL_GROUP); + PF_DIAG_CH_PROP_MAINT_SET (ch_properties, severity); + PF_DIAG_CH_PROP_DIR_SET ( + ch_properties, + p_diag_source->ch_direction); + PF_DIAG_CH_PROP_SPEC_SET ( + ch_properties, + PF_DIAG_CH_PROP_SPEC_APPEARS); + + p_item->usi = usi; + p_item->fmt.std.ch_nbr = p_diag_source->ch; + p_item->fmt.std.ch_properties = ch_properties; + p_item->fmt.std.ch_error_type = ch_error_type; + p_item->fmt.std.ext_ch_error_type = ext_ch_error_type; + p_item->fmt.std.ext_ch_add_value = ext_ch_add_value; + p_item->fmt.std.qual_ch_qualifier = qual_ch_qualifier; + } + + /* Link it into the sub-slot reported list */ + p_item->next = p_subslot->diag_list; + p_subslot->diag_list = item_ix; + + pf_diag_update_submodule_state (net, p_ar, p_subslot); + + /* TODO Use better strategy to find AR */ + if (pf_diag_find_first_ar (net, &p_ar) == 0) + { + ret = pf_alarm_send_diagnosis ( + net, + p_ar, + p_diag_source->api, + p_diag_source->slot, + p_diag_source->subslot, + p_item); + + pf_diag_update_station_problem_indicator (net, p_ar, p_subslot); + + if (overwrite == true) + { + /* Time to remove the previous entry */ + /* Remove the old diag by sending a disappear alarm */ + + if (old_item.usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + /* Standard format */ + PF_DIAG_CH_PROP_SPEC_SET ( + old_item.fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_DIS_OTHERS_REMAIN); + ret = pf_alarm_send_diagnosis ( + net, + p_ar, + p_diag_source->api, + p_diag_source->slot, + p_diag_source->subslot, + &old_item); + } + + /* Do not send a disappear alarm when updating diagnosis in + USI format */ + } + } + else + { + LOG_DEBUG ( + PNET_LOG, + "DIAG(%d): No active connection, so no alarm is sent.\n", + __LINE__); + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "DIAG(%d): Can not add diagnosis. Alarm " + "item is NULL\n", + __LINE__); + } + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Can not add diagnosis. Unknown sub-slot 0x%04x.\n", + __LINE__, + p_diag_source->subslot); + } + + os_mutex_unlock (p_dev->diag_mutex); + } + else + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Can not add diagnosis, as the CMDEV not is initialized.\n", + __LINE__); + } + + return ret; +} + +int pf_diag_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + int ret = -1; + pf_device_t * p_dev = NULL; + pf_subslot_t * p_subslot = NULL; + pf_diag_item_t * p_item = NULL; + pf_diag_item_t old_item; + uint16_t item_ix = PF_DIAG_IX_NULL; + pf_ar_t * p_ar = NULL; + + if (usi > PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS || usi == PF_USI_ALARM_MULTIPLE) + { + LOG_ERROR (PF_ALARM_LOG, "DIAG(%d): The given USI is invalid\n", __LINE__); + } + else if (usi < PF_USI_CHANNEL_DIAGNOSIS && ch_error_type != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ch_error_type should not be used for USI %u\n", + __LINE__, + usi); + } + else if (usi <= PF_USI_CHANNEL_DIAGNOSIS && ext_ch_error_type != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ext_ch_error_type should not be used for USI %u\n", + __LINE__, + usi); + } + else if (usi <= PF_USI_CHANNEL_DIAGNOSIS && ext_ch_add_value != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ext_ch_add_value should not be used for USI %u\n", + __LINE__, + usi); + } + else if ( + usi >= PF_USI_CHANNEL_DIAGNOSIS && + (p_manuf_data != NULL || manuf_data_len != 0)) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Manufacturer specific data should only be given for USI up " + "to 0x7fff\n", + __LINE__); + } + else if (manuf_data_len > 0 && p_manuf_data == NULL) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): manuf_data_len is %u but no data is given.\n", + __LINE__, + manuf_data_len); + } + else if (manuf_data_len > sizeof (p_item->fmt.usi.manuf_data)) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Too long manufacturer specific data given: %u\n", + __LINE__, + manuf_data_len); + } + else if (p_diag_source->ch > PNET_CHANNEL_WHOLE_SUBMODULE) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Illegal channel %u\n", + __LINE__, + p_diag_source->ch); + } + else if (pf_cmdev_get_device (net, &p_dev) == 0) + { + os_mutex_lock (p_dev->diag_mutex); + + pf_diag_find_entry ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + usi, + &p_subslot, + &item_ix); + if (p_subslot != NULL) + { + if (pf_cmdev_get_diag_item (net, item_ix, &p_item) == 0) + { + /* Make a copy of it so it can be removed after inserting the new + * entry */ + old_item = *p_item; + + if (p_item->usi < PF_USI_CHANNEL_DIAGNOSIS) + { + /* Manufacturer specific */ + p_item->fmt.usi.len = manuf_data_len; + if (manuf_data_len > 0) + { + memcpy ( + p_item->fmt.usi.manuf_data, + p_manuf_data, + manuf_data_len); + } + } + else + { + PF_DIAG_CH_PROP_SPEC_SET ( + p_item->fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_APPEARS); + p_item->fmt.std.ext_ch_add_value = ext_ch_add_value; + } + + /* Link it into the sub-slot diag list */ + p_item->next = p_subslot->diag_list; + p_subslot->diag_list = item_ix; + + pf_diag_update_submodule_state (net, p_ar, p_subslot); + + /* TODO Use better strategy to find AR */ + if (pf_diag_find_first_ar (net, &p_ar) == 0) + { + ret = pf_alarm_send_diagnosis ( + net, + p_ar, + p_diag_source->api, + p_diag_source->slot, + p_diag_source->subslot, + p_item); + + pf_diag_update_station_problem_indicator (net, p_ar, p_subslot); + + /* Remove the old diag by sending a disappear alarm. + The old item should be removed after the new is added */ + if (old_item.usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + /* Standard format */ + PF_DIAG_CH_PROP_SPEC_SET ( + old_item.fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_DIS_OTHERS_REMAIN); + ret = pf_alarm_send_diagnosis ( + net, + p_ar, + p_diag_source->api, + p_diag_source->slot, + p_diag_source->subslot, + &old_item); + } + + /* Do not send a disappear alarm when updating diagnosis in + USI format */ + } + else + { + LOG_ERROR ( + PNET_LOG, + "DIAG(%d): No active connection, so no alarm is sent.\n", + __LINE__); + } + } + else + { + /* Diag not found */ + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Diag not found. No update possible.\n", + __LINE__); + } + } + else + { + LOG_ERROR (PF_ALARM_LOG, "DIAG(%d): Unknown sub-slot\n", __LINE__); + } + + os_mutex_unlock (p_dev->diag_mutex); + } + + return ret; +} + +int pf_diag_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint16_t usi) +{ + int ret = -1; + pf_device_t * p_dev = NULL; + pf_subslot_t * p_subslot = NULL; + uint16_t item_ix = PF_DIAG_IX_NULL; + pf_diag_item_t * p_item = NULL; + pf_ar_t * p_ar = NULL; + + if (usi > PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS || usi == PF_USI_ALARM_MULTIPLE) + { + LOG_ERROR (PF_ALARM_LOG, "DIAG(%d): The given USI is invalid\n", __LINE__); + } + else if (usi < PF_USI_CHANNEL_DIAGNOSIS && ch_error_type != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ch_error_type should not be used for USI %u\n", + __LINE__, + usi); + } + else if (usi <= PF_USI_CHANNEL_DIAGNOSIS && ext_ch_error_type != 0) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): The ext_ch_error_type should not be used for USI %u\n", + __LINE__, + usi); + } + else if (p_diag_source->ch > PNET_CHANNEL_WHOLE_SUBMODULE) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Illegal channel %u\n", + __LINE__, + p_diag_source->ch); + } + else if (pf_cmdev_get_device (net, &p_dev) == 0) + { + os_mutex_lock (p_dev->diag_mutex); + + pf_diag_find_entry ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + usi, + &p_subslot, + &item_ix); + + os_mutex_unlock (p_dev->diag_mutex); + + if ((p_subslot != NULL) && (item_ix != PF_DIAG_IX_NULL)) + { + if (pf_cmdev_get_diag_item (net, item_ix, &p_item) == 0) + { + /* TODO Use better strategy to find AR */ + if (pf_diag_find_first_ar (net, &p_ar) == 0) + { + if (p_subslot->diag_list != PF_DIAG_IX_NULL) + { + /* + * If diagnosis information is removed a diagnosis alarm with + * specifier DISAPPEARS is queued into the alarm queue. ToDo: + * If the channel has other diagnosis information of the same + * severity, the specifier PNET_DIAG_CH_SPEC_DIS_OTHERS_REMAIN + * is used instead. + */ + if (p_item->usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + PF_DIAG_CH_PROP_SPEC_SET ( + p_item->fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_DISAPPEARS); + } + } + else + { + /* If the channel has no diagnosis information of any + * severity, + * the specifier PNET_DIAG_CH_SPEC_ALL_DISAPPEARS is used. */ + /* Overwrite the item to remove with correct cr_properties for + * this diag message. */ + p_item->usi = PF_USI_EXTENDED_CHANNEL_DIAGNOSIS; + PF_DIAG_CH_PROP_MAINT_SET ( + p_item->fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_FAULT); + PF_DIAG_CH_PROP_SPEC_SET ( + p_item->fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_ALL_DISAPPEARS); + } + + /* Remove the old diag by sending a disappear alarm */ + if (usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + /* Standard format */ + ret = pf_alarm_send_diagnosis ( + net, + p_ar, + p_diag_source->api, + p_diag_source->slot, + p_diag_source->subslot, + p_item); + } + else + { + /* USI format */ + ret = pf_alarm_send_usi_diagnosis_disappears ( + net, + p_ar, + p_diag_source->api, + p_diag_source->slot, + p_diag_source->subslot, + usi); + } + } + else + { + LOG_DEBUG ( + PNET_LOG, + "DIAG(%d): No active connection, so no alarm is sent.\n", + __LINE__); + } + pf_diag_update_submodule_state (net, p_ar, p_subslot); + } + + /* Free diag entry */ + pf_cmdev_free_diag (net, item_ix); + + if (p_ar != NULL) + { + pf_diag_update_station_problem_indicator (net, p_ar, p_subslot); + } + } + else + { + LOG_DEBUG ( + PF_ALARM_LOG, + "DIAG(%d): Did not find the diagnosis to remove. Slot %u Subslot " + "0x%04x Channel 0x%04x Ch err type 0x%04x Ext err type 0x%04x Usi " + "0x%04x\n", + __LINE__, + p_diag_source->slot, + p_diag_source->subslot, + p_diag_source->ch, + ch_error_type, + ext_ch_error_type, + usi); + } + } + + return ret; +} + +/************************** Diagnosis in standard format *********************/ + +/** + * @internal + * Get the preferred diagnosis USI, for use with standard format. + * + * @param net InOut: The p-net stack instance. + * @return the USI + */ +static uint16_t pf_diag_get_preferred_usi (pnet_t * net) +{ + pnet_cfg_t * p_cfg = NULL; + pf_fspm_get_cfg (net, &p_cfg); + if (p_cfg->use_qualified_diagnosis == true) + { + return PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS; + } + return PF_USI_EXTENDED_CHANNEL_DIAGNOSIS; +} + +int pf_diag_std_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier) +{ + return pf_diag_add ( + net, + p_diag_source, + ch_bits, + severity, + ch_error_type, + ext_ch_error_type, + ext_ch_add_value, + qual_ch_qualifier, + pf_diag_get_preferred_usi (net), + 0, + NULL); +} + +int pf_diag_std_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value) +{ + return pf_diag_update ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + ext_ch_add_value, + pf_diag_get_preferred_usi (net), + 0, + NULL); +} + +int pf_diag_std_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type) +{ + return pf_diag_remove ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + pf_diag_get_preferred_usi (net)); +} + +/************************** Diagnosis in USI format **************************/ + +int pf_diag_usi_add ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + pnet_diag_source_t diag_source = { + .api = api_id, + .slot = slot_nbr, + .subslot = subslot_nbr, + .ch = PNET_CHANNEL_WHOLE_SUBMODULE, + .ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL, + .ch_direction = PNET_DIAG_CH_PROP_DIR_MANUF_SPEC}; + + if (usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Wrong USI given for adding diagnosis: %u Slot: %u Subslot " + "%u\n", + __LINE__, + usi, + slot_nbr, + subslot_nbr); + return -1; + } + + return pf_diag_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + 0, + 0, + 0, + 0, + usi, + manuf_data_len, + p_manuf_data); +} + +int pf_diag_usi_update ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + pnet_diag_source_t diag_source = { + .api = api_id, + .slot = slot_nbr, + .subslot = subslot_nbr, + .ch = PNET_CHANNEL_WHOLE_SUBMODULE, + .ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL, + .ch_direction = PNET_DIAG_CH_PROP_DIR_MANUF_SPEC}; + + if (usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Wrong USI given for updating diagnosis: %u Slot: %u " + "Subslot %u\n", + __LINE__, + usi, + slot_nbr, + subslot_nbr); + return -1; + } + + return pf_diag_update ( + net, + &diag_source, + 0, + 0, + 0, + usi, + manuf_data_len, + p_manuf_data); +} + +int pf_diag_usi_remove ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi) +{ + pnet_diag_source_t diag_source = { + .api = api_id, + .slot = slot_nbr, + .subslot = subslot_nbr, + .ch = PNET_CHANNEL_WHOLE_SUBMODULE, + .ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL, + .ch_direction = PNET_DIAG_CH_PROP_DIR_MANUF_SPEC}; + + if (usi >= PF_USI_CHANNEL_DIAGNOSIS) + { + LOG_ERROR ( + PF_ALARM_LOG, + "DIAG(%d): Wrong USI given for diagnosis removal: %u Slot: %u Subslot " + "%u\n", + __LINE__, + usi, + slot_nbr, + subslot_nbr); + return -1; + } + + return pf_diag_remove (net, &diag_source, 0, 0, usi); +} diff --git a/profinet_stack/p-net/src/device/pf_diag.h b/profinet_stack/p-net/src/device/pf_diag.h new file mode 100644 index 0000000..96e90be --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_diag.h @@ -0,0 +1,343 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_DIAG_H +#define PF_DIAG_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* Masks for the different channel levels */ +#define PF_DIAG_QUAL_FAULT_MASK 0xf8000000 +#define PF_DIAG_QUAL_M_DEM_MASK 0x07fe0000 +#define PF_DIAG_QUAL_M_REQ_MASK 0x0001ff80 +#define PF_DIAG_QUAL_ADVICE_MASK 0x00000078 + +/* Digest bits in maint_status */ +#define PF_DIAG_MAINT_STATUS_REQ_BIT 0 +#define PF_DIAG_MAINT_STATUS_DEM_BIT 1 + +/** + * Initialize the diag component. + * + * @return 0 Always + */ +int pf_diag_init (void); + +/** + * Reset the diag component. + * + * @return 0 Always + */ +int pf_diag_exit (void); + +/************************** General diagnosis functions **********************/ + +/** + * Add a diagnosis entry, in standard or USI format. + * + * Typically it is easier to use these instead: + * - pf_diag_std_add() + * - pf_diag_usi_add() + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_bits In: Number of bits in the channel. + * @param severity In: Diagnosis severity. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type. + * @param ext_ch_add_value In: The extended channel error additional + * value. + * @param qual_ch_qualifier In: The qualified channel qualifier. + * @param usi In: The USI. + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or + * value from PLC. + * (Only needed if USI <= 0x7fff, + * and may still be 0). + * @param p_manuf_data In: The manufacturer specific diagnosis data. + * (Only needed if USI <= 0x7fff). + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Update a diagnosis entry, in standard or USI format. + * + * Typically it is easier to use these instead: + * - pf_diag_std_update() + * - pf_diag_usi_update() + * + * If the USI of the item is in the manufacturer-specified range then + * the USI is used to identify the item. + * Otherwise the other parameters are used (all must match): + * - p_diag_source (all fields) + * - Channel error type. + * - Extended error type. + * + * When the item is found either the manufacturer data is updated (for + * USI in manufacturer-specific range) or the extended channel additional + * value is updated. + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type, or 0. + * @param ext_ch_add_value In: New extended channel error additional value. + * @param usi In: The USI. + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or + * value from PLC. + * (Only needed if USI <= 0x7fff, + * and may still be 0). + * @param p_manuf_data In: New manufacturer specific diagnosis data. + * (Only needed if USI <= 0x7fff). + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Remove a diagnosis entry, in standard or USI format. + * + * Typically it is easier to use these instead: + * - pf_diag_std_remove() + * - pf_diag_usi_remove() + * + * If the USI of the item is in the manufacturer-specified range then + * the USI is used to identify the item. + * Otherwise the other parameters are used (all must match): + * - p_diag_source (all fields) + * - Channel error type. + * - Extended error type. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type, or 0. + * @param usi In: The USI. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint16_t usi); + +/************************** Diagnosis in standard format *********************/ + +/** + * Add a diagnosis entry, in standard format. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_bits In: Number of bits in the channel. + * @param severity In: Diagnosis severity. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type. + * @param ext_ch_add_value In: The extended channel error additional + * value. + * @param qual_ch_qualifier In: The qualified channel qualifier. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_std_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier); + +/** + * Update a diagnosis entry, in standard format. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type. + * @param ext_ch_add_value In: New extended channel error additional value. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_std_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value); + +/** + * Remove a diagnosis entry, in the standard format. + + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param p_diag_source In: Slot, subslot, channel, direction etc. + * @param ch_error_type In: The channel error type. + * @param ext_ch_error_type In: The extended channel error type. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_std_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type); + +/************************** Diagnosis in USI format **************************/ + +/** + * Add a diagnosis entry in manufacturer-specified ("USI") format. + * + * If the diagnosis already exists, it is updated. + * + * A diagnosis in USI format is assigned to the channel "whole submodule" + * (not individual channels). The severity is always "Fault". + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param api_id In: The API. + * @param slot_nbr In: The slot. + * @param subslot_nbr In: The sub-slot. + * @param usi In: The USI. Range 0..0x7fff + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or + * value from PLC. + * @param p_manuf_data In: The manufacturer specific diagnosis data. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_usi_add ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Update a diagnosis entry in manufacturer-specified ("USI") format. + * + * An error is returned if the diagnosis doesn't exist. + * + * Note that the PLC can set the max alarm payload length at startup, and + * that affects how large diagnosis entries can be sent via alarms. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param api_id In: The API. + * @param slot_nbr In: The slot. + * @param subslot_nbr In: The sub-slot. + * @param usi In: The USI. Range 0..0x7fff + * @param manuf_data_len In: Length in bytes of the + * manufacturer specific diagnosis data. + * Max PNET_MAX_DIAG_MANUF_DATA_SIZE or + * value from PLC. + * @param p_manuf_data In: New manufacturer specific diagnosis data. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_usi_update ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data); + +/** + * Remove a diagnosis entry in manufacturer-specified ("USI") format. + * + * This sends a diagnosis alarm. + * + * @param net InOut: The p-net stack instance. + * @param api_id In: The API. + * @param slot_nbr In: The slot. + * @param subslot_nbr In: The sub-slot. + * @param usi In: The USI. Range 0..0x7fff + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pf_diag_usi_remove ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint16_t usi); + +/* Not used yet */ +int pf_diag_get_maintenance_status ( + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t * p_maint_status); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_DIAG_H */ diff --git a/profinet_stack/p-net/src/device/pf_fspm.c b/profinet_stack/p-net/src/device/pf_fspm.c new file mode 100644 index 0000000..655513d --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_fspm.c @@ -0,0 +1,1421 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Implements the Fieldbus Application Layer Service Protocol Machine + * (FSPM) + * + * Stores the user-defined configuration, and calls the user-defined callbacks. + * Create logbook entries. + * Reads and writes identification & maintenance (I&M) records. + */ + +#ifdef UNIT_TEST +#define os_get_current_time_us mock_os_get_current_time_us +#define pf_file_clear mock_pf_file_clear +#define pf_file_load mock_pf_file_load +#define pf_file_save_if_modified mock_pf_file_save_if_modified +#define pf_bg_worker_start_job mock_pf_bg_worker_start_job +#endif + +#include +#include + +#include "pf_includes.h" +#include "pf_block_reader.h" + +typedef struct pf_im_nvm +{ + pnet_im_1_t im1; + pnet_im_2_t im2; + pnet_im_3_t im3; + pnet_im_4_t im4; +} pf_im_nvm_t; + +void pf_fspm_im_show (const pnet_t * net) +{ + printf ("Identification & Maintenance\n"); + printf ( + "I&M1.im_tag_function : \"%s\"\n", + net->fspm_cfg.im_1_data.im_tag_function); + printf ( + "I&M1.im_tag_location : \"%s\"\n", + net->fspm_cfg.im_1_data.im_tag_location); + printf ( + "I&M2.date : \"%s\"\n", + net->fspm_cfg.im_2_data.im_date); + printf ( + "I&M3.im_descriptor : \"%s\"\n", + net->fspm_cfg.im_3_data.im_descriptor); + printf ( + "I&M4.im_signature : \"%s\"\n", + net->fspm_cfg.im_4_data.im_signature); /* Should be binary data, but works + for now */ +} + +void pf_fspm_logbook_show (const pnet_t * net) +{ + uint16_t ix; + + printf ("Logbook. Has wrapped: %u\n", net->fspm_log_book.wrap); + printf ("ix Next Error code Detail Timestamp (s)\n"); + + for (ix = 0; ix < NELEMENTS (net->fspm_log_book.entries); ix++) + { + printf ("[%02u] %s ", ix, (ix == net->fspm_log_book.put) ? "*" : " "); + + if ( + (net->fspm_log_book.entries[ix].ar_uuid.data1 != 0) || + (net->fspm_log_book.entries[ix].ar_uuid.data2 != 0) || + (net->fspm_log_book.entries[ix].ar_uuid.data3 != 0)) + { + printf ( + "0x%02X 0x%02X 0x%02X 0x%02X 0x%08" PRIX32 " %" PRIu32 "\n", + net->fspm_log_book.entries[ix].pnio_status.error_code, + net->fspm_log_book.entries[ix].pnio_status.error_decode, + net->fspm_log_book.entries[ix].pnio_status.error_code_1, + net->fspm_log_book.entries[ix].pnio_status.error_code_2, + net->fspm_log_book.entries[ix].entry_detail, + net->fspm_log_book.entries[ix].time_ts.sec_lo); + } + else + { + printf ("\n"); + } + } +} + +void pf_fspm_option_show (const pnet_t * net) +{ + printf ("Compile time options affecting memory consumption:\n"); + printf ("PNET_MAX_AR : %d\n", PNET_MAX_AR); + printf ( + "PNET_MAX_API : %d\n", + PNET_MAX_API); + printf ("PNET_MAX_CR : %d\n", PNET_MAX_CR); + printf ( + "PNET_MAX_SLOTS : %d\n", + PNET_MAX_SLOTS); + printf ( + "PNET_MAX_SUBSLOTS : %d\n", + PNET_MAX_SUBSLOTS); + printf ( + "PNET_MAX_DFP_IOCR : %d\n", + PNET_MAX_DFP_IOCR); + printf ( + "PNET_MAX_PHYSICAL_PORTS : %d\n", + PNET_MAX_PHYSICAL_PORTS); + printf ( + "PNET_MAX_LOG_BOOK_ENTRIES : %d\n", + PNET_MAX_LOG_BOOK_ENTRIES); + printf ( + "PNET_MAX_ALARMS : %d\n", + PNET_MAX_ALARMS); + printf ( + "PNET_MAX_ALARM_PAYLOAD_DATA_SIZE : %d\n", + PNET_MAX_ALARM_PAYLOAD_DATA_SIZE); + printf ( + "PNET_MAX_DIAG_ITEMS : %d\n", + PNET_MAX_DIAG_ITEMS); + printf ( + "PNET_MAX_DIAG_MANUF_DATA_SIZE : %d\n", + PNET_MAX_DIAG_MANUF_DATA_SIZE); + printf ( + "PNET_MAX_DIRECTORYPATH_SIZE : %d\n", + PNET_MAX_DIRECTORYPATH_SIZE); + printf ( + "PNET_MAX_FILENAME_SIZE : %d\n", + PNET_MAX_FILENAME_SIZE); + printf ( + "PNET_MAX_SESSION_BUFFER_SIZE : %d\n", + PNET_MAX_SESSION_BUFFER_SIZE); + printf ( + "PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH : %d\n", + PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH); + printf ( + "PNET_OPTION_MC_CR : %d\n", + PNET_OPTION_MC_CR); +#if PNET_OPTION_MC_CR + printf ( + "PNET_MAX_MC_CR : %d\n", + PNET_MAX_MC_CR); +#endif + printf ( + "PNET_OPTION_AR_VENDOR_BLOCKS : %d\n", + PNET_OPTION_AR_VENDOR_BLOCKS); +#if PNET_OPTION_AR_VENDOR_BLOCKS + printf ( + "PNET_MAX_AR_VENDOR_BLOCKS : %d\n", + PNET_MAX_AR_VENDOR_BLOCKS); + printf ( + "PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH : %d\n", + PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH); +#endif + printf ( + "PNET_OPTION_FAST_STARTUP : %d\n", + PNET_OPTION_FAST_STARTUP); + printf ( + "PNET_OPTION_PARAMETER_SERVER : %d\n", + PNET_OPTION_PARAMETER_SERVER); + printf ( + "PNET_OPTION_IR : %d\n", + PNET_OPTION_IR); + printf ( + "PNET_OPTION_SR : %d\n", + PNET_OPTION_SR); + printf ( + "PNET_OPTION_REDUNDANCY : %d\n", + PNET_OPTION_REDUNDANCY); + printf ( + "PNET_OPTION_RS : %d\n", + PNET_OPTION_RS); + printf ( + "PNET_OPTION_SRL : %d\n", + PNET_OPTION_SRL); + printf ( + "PNET_OPTION_SNMP : %d\n", + PNET_OPTION_SNMP); + printf ("LOG_LEVEL (DEBUG=0, FATAL=4) : %d\n", LOG_LEVEL); + printf ( + "sizeof(pnet_t) : %" PRIu32 " bytes (for currently selected option " + "values)\n", + (uint32_t)sizeof (pnet_t)); +} + +/** + * @internal + * Validate the configuration from the user. + * + * @param p_cfg In: Configuration object + * @return 0 if the configuration is valid. + * -1 if the configuration is invalid. + */ +int pf_fspm_validate_configuration (const pnet_cfg_t * p_cfg) +{ + const uint16_t im_mask = + ~(PNET_SUPPORTED_IM1 | PNET_SUPPORTED_IM2 | PNET_SUPPORTED_IM3 | + PNET_SUPPORTED_IM4); + + if (p_cfg == NULL) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): You must provide a configuration, but it is NULL.\n", + __LINE__); + return -1; + } + + if (p_cfg->tick_us == 0) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Configured tick interval must not be 0. By default, sample " + "application uses 1000 micro seconds tick interval\n", + __LINE__); + return -1; + } + + if (strlen (p_cfg->if_cfg.main_netif_name) == 0) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Length of network interface name must not be 0.\n", + __LINE__); + return -1; + } + + if ( + p_cfg->num_physical_ports == 0 || + p_cfg->num_physical_ports > PNET_MAX_PHYSICAL_PORTS) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): The number of ports setting is wrong. Given: %d " + "PNET_MAX_PHYSICAL_PORTS: %d\n", + __LINE__, + p_cfg->num_physical_ports, + PNET_MAX_PHYSICAL_PORTS); + return -1; + } + + if (p_cfg->min_device_interval == 0) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): The min_device_interval in the config must not be 0. It " + "should typically be 32, corresponding to 1 ms.\n", + __LINE__); + return -1; + } + else if (p_cfg->min_device_interval > 0x1000) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): The min_device_interval in the config is too large. Max is " + "4096, corresponding to 128 ms.\n", + __LINE__); + return -1; + } + + if ((p_cfg->im_0_data.im_supported & im_mask) > 0) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): The I&M supported setting is wrong. Given: 0x%04x\n", + __LINE__, + p_cfg->im_0_data.im_supported); + return -1; + } + + return 0; +} + +/** + * @internal + * Load the I&M settings from nonvolatile memory, if existing. + * + * @param net InOut: The p-net stack instance + */ +static void pf_fspm_load_im (pnet_t * net) +{ + pf_im_nvm_t file_im; + const char * p_file_directory = pf_cmina_get_file_directory (net); + + if ( + pf_file_load ( + p_file_directory, + PF_FILENAME_IM, + &file_im, + sizeof (pf_im_nvm_t)) == 0) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Did read I&M settings from nvm.\n", + __LINE__); + os_mutex_lock (net->fspm_im_mutex); + memcpy (&net->fspm_cfg.im_1_data, &file_im.im1, sizeof (pnet_im_1_t)); + memcpy (&net->fspm_cfg.im_2_data, &file_im.im2, sizeof (pnet_im_2_t)); + memcpy (&net->fspm_cfg.im_3_data, &file_im.im3, sizeof (pnet_im_3_t)); + memcpy (&net->fspm_cfg.im_4_data, &file_im.im4, sizeof (pnet_im_4_t)); + os_mutex_unlock (net->fspm_im_mutex); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Could not yet read I&M settings from nvm. Use values from " + "user configuration.\n", + __LINE__); + } +} + +void pf_fspm_save_im (pnet_t * net) +{ + pf_im_nvm_t output_im; + pf_im_nvm_t temporary_buffer; + const char * p_file_directory = pf_cmina_get_file_directory (net); + int res = 0; + + os_mutex_lock (net->fspm_im_mutex); + memcpy (&output_im.im1, &net->fspm_cfg.im_1_data, sizeof (pnet_im_1_t)); + memcpy (&output_im.im2, &net->fspm_cfg.im_2_data, sizeof (pnet_im_2_t)); + memcpy (&output_im.im3, &net->fspm_cfg.im_3_data, sizeof (pnet_im_3_t)); + memcpy (&output_im.im4, &net->fspm_cfg.im_4_data, sizeof (pnet_im_4_t)); + os_mutex_unlock (net->fspm_im_mutex); + + res = pf_file_save_if_modified ( + p_file_directory, + PF_FILENAME_IM, + &output_im, + &temporary_buffer, + sizeof (pf_im_nvm_t)); + switch (res) + { + case 2: + LOG_INFO ( + PNET_LOG, + "FSPM(%d): First nvm saving of I&M settings.\n", + __LINE__); + break; + case 1: + LOG_INFO ( + PNET_LOG, + "FSPM(%d): Updating nvm stored I&M settings.\n", + __LINE__); + break; + case 0: + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No storing of nvm I&M settings (no changes).\n", + __LINE__); + break; + default: + case -1: + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Failed to store nvm I&M settings.\n", + __LINE__); + break; + } +} + +/** + * Set device location in I&M data record 1. + * + * If location string does not fit in the I&M1 field "IM_Tag_Location", + * (which is 22 bytes) it will be truncated with termination added. + * + * See pf_fspm_save_im_location() which also saves to file. + * + * @param net InOut: The p-net stack instance + * @param location In: New device location. + */ +static void pf_fspm_set_im_location (pnet_t * net, const char * location) +{ + os_mutex_lock (net->fspm_im_mutex); + snprintf ( + net->fspm_cfg.im_1_data.im_tag_location, + sizeof (net->fspm_cfg.im_1_data.im_tag_location), + "%-22s", + location); + os_mutex_unlock (net->fspm_im_mutex); +} + +void pf_fspm_get_im_location (pnet_t * net, char * location) +{ + os_mutex_lock (net->fspm_im_mutex); + snprintf ( + location, + PNET_LOCATION_MAX_SIZE, + "%-22s", + net->fspm_cfg.im_1_data.im_tag_location); + os_mutex_unlock (net->fspm_im_mutex); +} + +void pf_fspm_save_im_location (pnet_t * net, const char * location) +{ + pf_fspm_set_im_location (net, location); + (void)pf_bg_worker_start_job (net, PF_BGJOB_SAVE_IM_NVM_DATA); +} + +int pf_fspm_init (pnet_t * net, const pnet_cfg_t * p_cfg) +{ + if (pf_fspm_validate_configuration (p_cfg) != 0) + { + return -1; + } + + /* Use a copy of the configuration. For example the I&M data might be updated + * at runtime. */ + net->fspm_cfg = *p_cfg; + + /* Reference to the default settings (used at factory reset) */ + net->p_fspm_default_cfg = p_cfg; + + /* Create mutex for protecting writable I&M data */ + if (net->fspm_im_mutex == NULL) + { + net->fspm_im_mutex = os_mutex_create(); + } + + /* Load I&M data modifications from file, if any */ + pf_fspm_load_im (net); + pf_fspm_save_im (net); + + /* Log book */ + if (net->fspm_log_book_mutex == NULL) + { + net->fspm_log_book_mutex = os_mutex_create(); + } + + /* Turn LED off */ + if (pf_fspm_signal_led_ind (net, false) != 0) + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Could not turn signal LED off\n", + __LINE__); + } + + return 0; +} + +void pf_fspm_create_log_book_entry ( + pnet_t * net, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status, + uint32_t entry_detail) +{ + uint16_t put; + uint32_t time = 0; + pf_ar_t * p_ar = NULL; + pf_uuid_t * p_ar_uuid = NULL; + + if (pf_ar_find_by_arep (net, arep, &p_ar) == 0) + { + p_ar_uuid = &p_ar->ar_param.ar_uuid; + os_mutex_lock (net->fspm_log_book_mutex); + put = net->fspm_log_book.put; + net->fspm_log_book.put++; + if (net->fspm_log_book.put >= NELEMENTS (net->fspm_log_book.entries)) + { + net->fspm_log_book.put = 0; + net->fspm_log_book.wrap = true; + } + + time = os_get_current_time_us(); + net->fspm_log_book.entries[put].time_ts.status = PF_TS_STATUS_LOCAL_ARB; + net->fspm_log_book.entries[put].time_ts.sec_hi = 0; + net->fspm_log_book.entries[put].time_ts.sec_lo = time / 1000000; + net->fspm_log_book.entries[put].time_ts.nano_sec = + (time % 1000000) * 1000; + + net->fspm_log_book.entries[put].ar_uuid = *p_ar_uuid; + net->fspm_log_book.entries[put].pnio_status = *p_pnio_status; + net->fspm_log_book.entries[put].entry_detail = entry_detail; + os_mutex_unlock (net->fspm_log_book_mutex); + + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Added logbook entry to position %u. Error: " + "0x%02X 0x%02X 0x%02X 0x%02X Detail: %" PRIu32 "\n", + __LINE__, + put, + p_pnio_status->error_code, + p_pnio_status->error_decode, + p_pnio_status->error_code_1, + p_pnio_status->error_code_2, + entry_detail); + } +} + +int pf_fspm_clear_im_data (pnet_t * net) +{ + os_mutex_lock (net->fspm_im_mutex); + memset ( + net->fspm_cfg.im_1_data.im_tag_function, + ' ', + sizeof (net->fspm_cfg.im_1_data.im_tag_function)); + net->fspm_cfg.im_1_data + .im_tag_function[sizeof (net->fspm_cfg.im_1_data.im_tag_function) - 1] = + '\0'; + memset ( + net->fspm_cfg.im_1_data.im_tag_location, + ' ', + sizeof (net->fspm_cfg.im_1_data.im_tag_location)); + net->fspm_cfg.im_1_data + .im_tag_location[sizeof (net->fspm_cfg.im_1_data.im_tag_location) - 1] = + '\0'; + memset ( + net->fspm_cfg.im_2_data.im_date, + ' ', + sizeof (net->fspm_cfg.im_2_data.im_date)); + net->fspm_cfg.im_2_data + .im_date[sizeof (net->fspm_cfg.im_2_data.im_date) - 1] = '\0'; + memset ( + net->fspm_cfg.im_3_data.im_descriptor, + ' ', + sizeof (net->fspm_cfg.im_3_data.im_descriptor)); + net->fspm_cfg.im_3_data + .im_descriptor[sizeof (net->fspm_cfg.im_3_data.im_descriptor) - 1] = '\0'; + memset ( + net->fspm_cfg.im_4_data.im_signature, + 0, + sizeof (net->fspm_cfg.im_4_data.im_signature)); + os_mutex_unlock (net->fspm_im_mutex); + + (void)pf_bg_worker_start_job (net, PF_BGJOB_SAVE_IM_NVM_DATA); + + return 0; +} + +void pf_fspm_get_cfg (pnet_t * net, pnet_cfg_t ** pp_cfg) +{ + if (pp_cfg != NULL) + { + *pp_cfg = &net->fspm_cfg; + } +} + +void pf_fspm_get_default_cfg (pnet_t * net, const pnet_cfg_t ** pp_cfg) +{ + if (pp_cfg != NULL) + { + *pp_cfg = net->p_fspm_default_cfg; + } +} + +int16_t pf_fspm_get_min_device_interval (const pnet_t * net) +{ + return net->fspm_cfg.min_device_interval; +} + +/******************* Execute user callbacks *********************************/ + +int pf_fspm_exp_module_ind ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint32_t module_ident) +{ + int ret = -1; + + if (net->fspm_cfg.exp_module_cb != NULL) + { + ret = + net->fspm_cfg + .exp_module_cb (net, net->fspm_cfg.cb_arg, api, slot, module_ident); + } + + return ret; +} + +int pf_fspm_exp_submodule_ind ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_ident, + uint32_t submodule_ident, + const pnet_data_cfg_t * p_exp_data) +{ + int ret = -1; + + if (net->fspm_cfg.exp_submodule_cb != NULL) + { + ret = net->fspm_cfg.exp_submodule_cb ( + net, + net->fspm_cfg.cb_arg, + api, + slot, + subslot, + module_ident, + submodule_ident, + p_exp_data); + } + + return ret; +} + +void pf_fspm_data_status_changed ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iocr_t * p_iocr, + uint8_t changes, + uint8_t data_status) +{ + if (net->fspm_cfg.new_data_status_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering data status change callback for AREP %u. Status " + "0x%02x changes 0x%02x\n", + __LINE__, + p_ar->arep, + data_status, + changes); + + (void)net->fspm_cfg.new_data_status_cb ( + net, + net->fspm_cfg.cb_arg, + p_ar->arep, + p_iocr->crep, + changes, + data_status); + } +} + +void pf_fspm_ccontrol_cnf ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_result_t * p_result) +{ + if (net->fspm_cfg.ccontrol_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering CControl confirmation callback for AREP %u\n", + __LINE__, + p_ar->arep); + + (void)net->fspm_cfg + .ccontrol_cb (net, net->fspm_cfg.cb_arg, p_ar->arep, p_result); + } +} + +int pf_fspm_cm_read_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_request, + uint8_t ** pp_read_data, + uint16_t * p_read_length, + pnet_result_t * p_read_status) +{ + int ret = -1; + uint16_t slot = p_read_request->slot_number; + uint16_t subslot = p_read_request->subslot_number; + uint16_t index = p_read_request->index; + + if (p_read_request->index <= PF_IDX_USER_MAX) + { + /* Trigger callback for application-specific data records */ + if (net->fspm_cfg.read_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering read callback for AREP %u. Slot %u subslot " + "%u index %u\n", + __LINE__, + p_ar->arep, + slot, + subslot, + index); + + ret = net->fspm_cfg.read_cb ( + net, + net->fspm_cfg.cb_arg, + p_ar->arep, + p_read_request->api, + slot, + subslot, + index, + p_read_request->sequence_number, + pp_read_data, + p_read_length, + p_read_status); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for reading records implemented.\n", + __LINE__); + + p_read_status->pnio_status.error_code = PNET_ERROR_CODE_READ; + p_read_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_read_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_APP_NOT_SUPPORTED; + p_read_status->pnio_status.error_code_2 = 0; + } + } + else if ((PF_IDX_SUB_IM_0 <= index) && (index <= PF_IDX_SUB_IM_15)) + { + /* Read I&M data - This is handled internally. */ + switch (index) + { + case PF_IDX_SUB_IM_0: + + if (*p_read_length >= sizeof (net->fspm_cfg.im_0_data)) + { + *pp_read_data = (uint8_t *)&net->fspm_cfg.im_0_data; + *p_read_length = sizeof (net->fspm_cfg.im_0_data); + ret = 0; + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Failed to read I&M0 data\n", + __LINE__); + p_read_status->pnio_status.error_code = PNET_ERROR_CODE_READ; + p_read_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_read_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_APP_READ_ERROR; + p_read_status->pnio_status.error_code_2 = 0; + } + break; + case PF_IDX_SUB_IM_1: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM1) > 0) + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): I&M1 function: \"%s\" Location: \"%s\"\n", + __LINE__, + net->fspm_cfg.im_1_data.im_tag_function, + net->fspm_cfg.im_1_data.im_tag_location); + *pp_read_data = (uint8_t *)&net->fspm_cfg.im_1_data; + *p_read_length = sizeof (net->fspm_cfg.im_1_data); + ret = 0; + } + else + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): PLC is trying to read I&M1 data, but we have not " + "enabled it.\n", + __LINE__); + } + break; + case PF_IDX_SUB_IM_2: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM2) > 0) + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): I&M2 Date: %s\n", + __LINE__, + net->fspm_cfg.im_2_data.im_date); + *pp_read_data = (uint8_t *)&net->fspm_cfg.im_2_data; + *p_read_length = sizeof (net->fspm_cfg.im_2_data); + ret = 0; + } + else + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): PLC is trying to read I&M2 data, but we have not " + "enabled it.\n", + __LINE__); + } + break; + case PF_IDX_SUB_IM_3: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM3) > 0) + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): I&M3 Descriptor: \"%s\"\n", + __LINE__, + net->fspm_cfg.im_3_data.im_descriptor); + *pp_read_data = (uint8_t *)&net->fspm_cfg.im_3_data; + *p_read_length = sizeof (net->fspm_cfg.im_3_data); + ret = 0; + } + else + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): PLC is trying to read I&M3 data, but we have not " + "enabled it.\n", + __LINE__); + } + break; + case PF_IDX_SUB_IM_4: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM4) > 0) + { + *pp_read_data = (uint8_t *)&net->fspm_cfg.im_4_data; + *p_read_length = sizeof (net->fspm_cfg.im_4_data); + ret = 0; + } + else + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): PLC is trying to read I&M4 data, but we have not " + "enabled it.\n", + __LINE__); + } + break; + default: + /* Nothing if data not available here */ + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): PLC is requesting to read non-implemented I&M data.\n", + __LINE__); + break; + } + } + else if (index == PF_IDX_DEV_LOGBOOK_DATA) + { + *pp_read_data = (uint8_t *)&net->fspm_log_book; + *p_read_length = sizeof (net->fspm_log_book); + ret = 0; + } + else + { + /* Nothing if data not available here */ + } + + return ret; +} + +int pf_fspm_cm_write_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_request, + uint16_t write_length, + const uint8_t * p_write_data, + pnet_result_t * p_write_status) +{ + int ret = -1; + pf_get_info_t get_info; + uint16_t pos = 0; + + if (p_write_request->index <= PF_IDX_USER_MAX) + { + /* Trigger callback for application-specific data records */ + if (net->fspm_cfg.write_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering write callback for AREP %u. Slot %u subslot " + "%u index %u len %u\n", + __LINE__, + p_ar->arep, + p_write_request->slot_number, + p_write_request->subslot_number, + p_write_request->index, + write_length); + + ret = net->fspm_cfg.write_cb ( + net, + net->fspm_cfg.cb_arg, + p_ar->arep, + p_write_request->api, + p_write_request->slot_number, + p_write_request->subslot_number, + p_write_request->index, + p_write_request->sequence_number, + write_length, + p_write_data, + p_write_status); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for writing records implemented.\n", + __LINE__); + + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_APP_NOT_SUPPORTED; + p_write_status->pnio_status.error_code_2 = 0; + } + } + else if ( + (PF_IDX_SUB_IM_0 <= p_write_request->index) && + (p_write_request->index <= PF_IDX_SUB_IM_15)) + { + /* Write I&M data - This is handled internally in the stack */ + switch (p_write_request->index) + { + case PF_IDX_SUB_IM_0: /* read-only */ + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): PLC requests to write I&M0 data, but it is read-only.\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_ACCESS_DENIED; + p_write_status->pnio_status.error_code_2 = 0; + break; + case PF_IDX_SUB_IM_1: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM1) > 0) + { + get_info.result = PF_PARSE_OK; + get_info.p_buf = p_write_data; + get_info.is_big_endian = true; + get_info.len = write_length; /* Bytes in input buffer */ + + os_mutex_lock (net->fspm_im_mutex); + pf_get_im_1 (&get_info, &pos, &net->fspm_cfg.im_1_data); + os_mutex_unlock (net->fspm_im_mutex); + if ((get_info.result == PF_PARSE_OK) && (pos == write_length)) + { + LOG_INFO ( + PNET_LOG, + "FSPM(%d): I&M1 data. Function: \"%s\" Location: \"%s\"\n", + __LINE__, + net->fspm_cfg.im_1_data.im_tag_function, + net->fspm_cfg.im_1_data.im_tag_location); + ret = 0; + +#if PNET_OPTION_SNMP + /* Location data is stored in two different files: the file with + * I&M data and a file used by SNMP containing a larger version + * of the device's location. The larger version has precedence + * over the I&M version, so we need to delete the larger one. + */ + pf_snmp_fspm_im_location_ind (net); +#endif + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Wrong length of incoming I&M1 data\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = + PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_WRITE_LENGTH_ERROR; + p_write_status->pnio_status.error_code_2 = 0; + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): PLC is trying to write I&M1 data, but we have not " + "enabled it.\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_INVALID_INDEX; + p_write_status->pnio_status.error_code_2 = 0; + } + break; + case PF_IDX_SUB_IM_2: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM2) > 0) + { + /* Do not count the terminator byte */ + if (write_length == (sizeof (net->fspm_cfg.im_2_data) - 1)) + { + os_mutex_lock (net->fspm_im_mutex); + memcpy ( + &net->fspm_cfg.im_2_data, + p_write_data, + sizeof (net->fspm_cfg.im_2_data) - 1); + net->fspm_cfg.im_2_data + .im_date[sizeof (net->fspm_cfg.im_2_data) - 1] = '\0'; + os_mutex_unlock (net->fspm_im_mutex); + LOG_INFO ( + PNET_LOG, + "FSPM(%d): I&M2 Date: %s\n", + __LINE__, + net->fspm_cfg.im_2_data.im_date); + ret = 0; + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Wrong length of incoming I&M2 data\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = + PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_WRITE_LENGTH_ERROR; + p_write_status->pnio_status.error_code_2 = 0; + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): PLC is trying to write I&M2 data, but we have not " + "enabled it.\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_INVALID_INDEX; + p_write_status->pnio_status.error_code_2 = 0; + } + break; + case PF_IDX_SUB_IM_3: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM3) > 0) + { + /* Do not count the terminator byte */ + if (write_length == (sizeof (net->fspm_cfg.im_3_data) - 1)) + { + os_mutex_lock (net->fspm_im_mutex); + memcpy ( + &net->fspm_cfg.im_3_data, + p_write_data, + sizeof (net->fspm_cfg.im_3_data) - 1); + net->fspm_cfg.im_3_data + .im_descriptor[sizeof (net->fspm_cfg.im_3_data) - 1] = '\0'; + os_mutex_unlock (net->fspm_im_mutex); + LOG_INFO ( + PNET_LOG, + "FSPM(%d): I&M3 Descriptor: %s\n", + __LINE__, + net->fspm_cfg.im_3_data.im_descriptor); + ret = 0; + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Wrong length of incoming I&M3 data\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = + PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_WRITE_LENGTH_ERROR; + p_write_status->pnio_status.error_code_2 = 0; + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): PLC is trying to write I&M3 data, but we have not " + "enabled it.\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_INVALID_INDEX; + p_write_status->pnio_status.error_code_2 = 0; + } + break; + case PF_IDX_SUB_IM_4: + if ((net->fspm_cfg.im_0_data.im_supported & PNET_SUPPORTED_IM4) > 0) + { + if (write_length == sizeof (net->fspm_cfg.im_4_data)) + { + os_mutex_lock (net->fspm_im_mutex); + memcpy ( + &net->fspm_cfg.im_4_data, + p_write_data, + sizeof (net->fspm_cfg.im_4_data)); + os_mutex_unlock (net->fspm_im_mutex); + ret = 0; + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): Wrong length of incoming I&M4 data\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = + PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_WRITE_LENGTH_ERROR; + p_write_status->pnio_status.error_code_2 = 0; + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): PLC is trying to write I&M4 data, but we have not " + "enabled it.\n", + __LINE__); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_INVALID_INDEX; + p_write_status->pnio_status.error_code_2 = 0; + } + break; + default: + LOG_ERROR ( + PNET_LOG, + "FSPM(%d): PLC is trying to write non-implemented I&M data. Index " + "%u\n", + __LINE__, + p_write_request->index); + p_write_status->pnio_status.error_code = PNET_ERROR_CODE_WRITE; + p_write_status->pnio_status.error_decode = PNET_ERROR_DECODE_PNIORW; + p_write_status->pnio_status.error_code_1 = + PNET_ERROR_CODE_1_ACC_INVALID_INDEX; + p_write_status->pnio_status.error_code_2 = 0; + break; + } + + if (ret == 0) + { + pf_fspm_save_im (net); + } + } + else + { + /* Nothing if write not possible here */ + } + + return ret; +} + +int pf_fspm_cm_connect_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_result_t * p_result) +{ + int ret = 0; + + if (net->fspm_cfg.connect_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering connect callback for AREP %u\n", + __LINE__, + p_ar->arep); + + ret = net->fspm_cfg + .connect_cb (net, net->fspm_cfg.cb_arg, p_ar->arep, p_result); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for connect implemented.\n", + __LINE__); + } + + pf_fspm_create_log_book_entry ( + net, + p_ar->arep, + &p_result->pnio_status, + PF_LOG_BOOK_AR_CONNECT); + + return ret; +} + +int pf_fspm_cm_release_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_result_t * p_result) +{ + int ret = 0; + + if (net->fspm_cfg.release_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering release callback for AREP %u\n", + __LINE__, + p_ar->arep); + + ret = net->fspm_cfg + .release_cb (net, net->fspm_cfg.cb_arg, p_ar->arep, p_result); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for release implemented.\n", + __LINE__); + } + + pf_fspm_create_log_book_entry ( + net, + p_ar->arep, + &p_result->pnio_status, + PF_LOG_BOOK_AR_RELEASE); + + return ret; +} + +int pf_fspm_cm_dcontrol_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_control_command_t control_command, + pnet_result_t * p_result) +{ + int ret = 0; + + if (net->fspm_cfg.dcontrol_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering DControl callback with command bitfield 0x%04x " + "for AREP %u\n", + __LINE__, + control_command, + p_ar->arep); + + ret = net->fspm_cfg.dcontrol_cb ( + net, + net->fspm_cfg.cb_arg, + p_ar->arep, + control_command, + p_result); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for dcontrol ind implemented.\n", + __LINE__); + } + + pf_fspm_create_log_book_entry ( + net, + p_ar->arep, + &p_result->pnio_status, + PF_LOG_BOOK_DCONTROL); + + return ret; +} + +void pf_fspm_state_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_event_values_t event) +{ + CC_ASSERT (p_ar != NULL); + + if (net->fspm_cfg.state_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering user state-change callback with event %s for " + "AREP %u\n", + __LINE__, + pf_cmdev_event_to_string (event), + p_ar->arep); + (void) + net->fspm_cfg.state_cb (net, net->fspm_cfg.cb_arg, p_ar->arep, event); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for state change implemented.\n", + __LINE__); + } +} + +int pf_fspm_alpmr_alarm_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + const pnet_alarm_argument_t * p_alarm_arg, + uint16_t data_len, + uint16_t data_usi, + const uint8_t * p_data) +{ + int ret = 0; + + if (net->fspm_cfg.alarm_ind_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering alarm callback with USI %u (%u bytes) for AREP " + "%u\n", + __LINE__, + data_usi, + data_len, + p_ar->arep); + + ret = net->fspm_cfg.alarm_ind_cb ( + net, + net->fspm_cfg.cb_arg, + p_ar->arep, + p_alarm_arg, + data_len, + data_usi, + p_data); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for alarm ind implemented.\n", + __LINE__); + } + + return ret; +} + +void pf_fspm_alpmi_alarm_cnf ( + pnet_t * net, + const pf_ar_t * p_ar, + const pnet_pnio_status_t * p_pnio_status) +{ + if (net->fspm_cfg.alarm_cnf_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering alarm confirmation callback for AREP %u\n", + __LINE__, + p_ar->arep); + + (void)net->fspm_cfg + .alarm_cnf_cb (net, net->fspm_cfg.cb_arg, p_ar->arep, p_pnio_status); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for alarm cnf implemented.\n", + __LINE__); + } +} + +void pf_fspm_alpmr_alarm_ack_cnf (pnet_t * net, const pf_ar_t * p_ar, int res) +{ + if (net->fspm_cfg.alarm_ack_cnf_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering alarm ACK confirmation callback for AREP %u\n", + __LINE__, + p_ar->arep); + + (void)net->fspm_cfg + .alarm_ack_cnf_cb (net, net->fspm_cfg.cb_arg, p_ar->arep, res); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for alarm ack cnf implemented.\n", + __LINE__); + } +} + +void pf_fspm_reset_ind ( + pnet_t * net, + bool should_reset_application, + uint16_t reset_mode) +{ + if (net->fspm_cfg.reset_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering reset callback with mode %u. Application " + "reset: %s\n", + __LINE__, + reset_mode, + should_reset_application ? "true" : "false"); + + (void)net->fspm_cfg.reset_cb ( + net, + net->fspm_cfg.cb_arg, + should_reset_application, + reset_mode); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for reset implemented.\n", + __LINE__); + } +} + +int pf_fspm_signal_led_ind (pnet_t * net, bool led_state) +{ + int ret = 0; + if (net->fspm_cfg.signal_led_cb != NULL) + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): Triggering signal LED callback with state %u.\n", + __LINE__, + led_state); + + ret = net->fspm_cfg.signal_led_cb (net, net->fspm_cfg.cb_arg, led_state); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "FSPM(%d): No user callback for signal LED implemented.\n", + __LINE__); + } + + return ret; +} diff --git a/profinet_stack/p-net/src/device/pf_fspm.h b/profinet_stack/p-net/src/device/pf_fspm.h new file mode 100644 index 0000000..28876c3 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_fspm.h @@ -0,0 +1,418 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Internal header for FSPM + */ + +#ifndef PF_FSPM_H +#define PF_FSPM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the FSPM component. + * + * Validate and store the application configuration. + * + * Turn off the signal LED. + * + * @param net InOut: The p-net stack instance + * @param p_cfg In: The application configuration of the Profinet + * stack. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_init (pnet_t * net, const pnet_cfg_t * p_cfg); + +/** + * Retrieve the minimum device interval, from the configuration. + * + * This is the smallest allowed data exchange interval, in units of 31.25 us. + * Typically 32, which corresponds to 1 ms. + * + * @param net InOut: The p-net stack instance + * @return the minimum device interval. + */ +int16_t pf_fspm_get_min_device_interval (const pnet_t * net); + +/** + * Create a LogBook entry. + * + * Logbooks are described in Profinet 2.4 Services, section 7.3.6 "LogBook ASE" + * + * @param net InOut: The p-net stack instance + * @param arep In: The AREP, identifying the AR. + * @param p_pnio_status In: The PNIO status. + * @param entry_detail In: Additional application information. + */ +void pf_fspm_create_log_book_entry ( + pnet_t * net, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status, + uint32_t entry_detail); + +/** + * Process write record requests from the controller. + * If index indicates I&M data records then handle here. + * + * If index is user-defined then call application + * call-back \a pnet_write_ind() if defined. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_write_request In: The write request record. + * @param write_length In: Length in bytes of write data. + * @param p_write_data In: The data to write. + * @param p_result Out: Detailed error information if returning != 0 + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_cm_write_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_request, + uint16_t write_length, + const uint8_t * p_write_data, + pnet_result_t * p_result); + +/** + * Process read record requests from the controller. + * Triggers the \a pnet_read_ind() user callback for some values. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_read_request In: The read request record. + * @param pp_read_data Out: A pointer to the source data. + * @param p_read_length InOut: The maximum (in) and actual (out) length in + * bytes of the binary value. + * @param p_result Out: The result information. + * @return 0 if operation succeeded. + * -1 if not handled or an error occurred. + */ +int pf_fspm_cm_read_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_request, + uint8_t ** pp_read_data, + uint16_t * p_read_length, + pnet_result_t * p_result); + +/** + * Response from controller to appl_rdy request. + * + * Triggers the \a pnet_ccontrol_cnf() user callback. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_result Out: The result information. + */ +void pf_fspm_ccontrol_cnf ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_result_t * p_result); + +/** + * Call user call-back \a pnet_connect_ind() when a new connection is requested. + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_result Out: The result information if return != 0. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_cm_connect_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_result_t * p_result); + +/** + * Call user call-back \a pnet_release_ind() when a connection is released. + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_result Out: Detailed error information if return != 0. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_cm_release_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_result_t * p_result); + +/** + * Call user call-back \a pnet_dcontrol_ind() to indicate a control request + * from the ProfiNet controller. + * + * Create a logbook entry. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param control_command In: The control command. + * @param p_result Out: The result information. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_cm_dcontrol_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_control_command_t control_command, + pnet_result_t * p_result); + +/** + * Call user call-back \a pnet_state_ind() to indicate a new event + * within the profinet stack. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param event In: The new CMDEV state. Use PNET_EVENT_xxx, not + * PF_CMDEV_STATE_xxx + */ +void pf_fspm_state_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + pnet_event_values_t event); + +/** + * The remote side sends an alarm. + * + * Calls user call-back \a pnet_alarm_ind(). + * We have already sent the transport acknowledge ("TACK") frame. + * + * ALPMR: ALPMR_Alarm_Notification.ind + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_alarm_argument In: The alarm argument (with slot, subslot, + * alarm_type etc) + * @param data_len In: Data length + * @param data_usi In: USI for the alarm + * @param p_data InOut: Alarm data + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_alpmr_alarm_ind ( + pnet_t * net, + const pf_ar_t * p_ar, + const pnet_alarm_argument_t * p_alarm_arg, + uint16_t data_len, + uint16_t data_usi, + const uint8_t * p_data); + +/** + * The remote side acknowledges the alarm sent by this side, by sending an + * AlarmAck. Calls user call-back \a pnet_alarm_cnf(). + * + * ALPMI: ALPMI_Alarm_Notification.cnf(+/-) + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_pnio_status In: Detailed ACK information. + */ +void pf_fspm_alpmi_alarm_cnf ( + pnet_t * net, + const pf_ar_t * p_ar, + const pnet_pnio_status_t * p_pnio_status); + +/** + * Local side has successfully sent the AlarmAck to the remote side. + * Calls user call-back \a pnet_alarm_ack_cnf(). + * + * ALPMR: ALPMR_Alarm_Ack.cnf(+/-) (Implements part of the signal) + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param res In: 0 if ACK was received by the remote side. + * This is cnf(+) + * -1 if ACK was not received. + * This is cnf(-) + */ +void pf_fspm_alpmr_alarm_ack_cnf (pnet_t * net, const pf_ar_t * p_ar, int res); + +/** + * Call user call-back \a pnet_exp_module_ind() to indicate to application + * it needs a specific module in a specific slot. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param module_ident In: The module ident number. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_exp_module_ind ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint32_t module_ident); + +/** + * Call user call-back \a pnet_exp_submodule_ind() to indicate to application + * it needs a specific sub-module in a specific sub-slot. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param slot_nbr In: The slot number. + * @param subslot_nbr In: The sub-slot number. + * @param module_ident In: The module ident number. + * @param submodule_ident In: The sub-module ident number. + * @param p_exp_data In: The expected data configuration + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_exp_submodule_ind ( + pnet_t * net, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint32_t module_ident, + uint32_t submodule_ident, + const pnet_data_cfg_t * exp_data); + +/** + * Notify application that the received data status has changed, + * via the \a pnet_new_data_status_ind() user callback. + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_iocr In: The IOCR instance. + * @param changes In: The changed bits in the data status. + * @param data_status In: Data status (after the changes) + */ +void pf_fspm_data_status_changed ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iocr_t * p_iocr, + uint8_t changes, + uint8_t data_status); + +/** + * Call user call-back when the controller requests a reset. + * + * This uses the \a pnet_reset_ind() callback. + * + * @param net InOut: The p-net stack instance + * @param should_reset_application In: True if the user should reset the + * application data. + * @param reset_mode In: Detailed reset information. + */ +void pf_fspm_reset_ind ( + pnet_t * net, + bool should_reset_application, + uint16_t reset_mode); + +/** + * Call user call-back when the Profinet signal LED should change state. + * + * This uses the \a pnet_signal_led_ind() callback. + * + * @param net InOut: The p-net stack instance + * @param led_state In: True if the signal LED should be on. + * @return 0 if operation succeeded. + * -1 if an error occurred. Will trigger a log message. + */ +int pf_fspm_signal_led_ind (pnet_t * net, bool led_state); + +/** + * Retrieve a pointer to the current configuration data. + * @param net InOut: The p-net stack instance + * @param pp_cfg Out: The current configuration (or NULL). + */ +void pf_fspm_get_cfg (pnet_t * net, pnet_cfg_t ** pp_cfg); + +/** + * Retrieve a pointer to the default configuration data. + * @param net InOut: The p-net stack instance + * @param pp_cfg Out: The default configuration (or NULL). + */ +void pf_fspm_get_default_cfg (pnet_t * net, const pnet_cfg_t ** pp_cfg); + +/** + * Get device location from I&M data record 1. + * + * Device location is always 22 bytes long, possibly padded with + * space characters (' ') to the right. It is always null terminated. + * + * Note that the SNMP thread may call this at any time. + * + * @param net InOut: The p-net stack instance + * @param location Out: Current device location. + * Buffer must at least be of size + * PNET_LOCATION_MAX_SIZE. + */ +void pf_fspm_get_im_location (pnet_t * net, char * location); + +/** + * Set device location in I&M data record 1 and save it to persistent memory. + * + * Only the first 22 bytes will be saved. + * If \a location is less than 22 bytes, it will be padded with spaces + * until it is 22 bytes long. Null termination will also be added. + * + * Note that the SNMP thread may call this at any time. + * + * @param net InOut: The p-net stack instance + * @param location In: New device location. + */ +void pf_fspm_save_im_location (pnet_t * net, const char * location); + +/** + * Clear the I&M data records 1-4. + * @param net InOut: The p-net stack instance + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_fspm_clear_im_data (pnet_t * net); + +/** + * Save the I&M settings to nonvolatile memory, if necessary. + * Blocking call, intended to be called from the background worker. + * + * Compares with the content of already stored settings (in order not to + * wear out the flash chip) + * + * @param net InOut: The p-net stack instance + */ +void pf_fspm_save_im (pnet_t * net); + +/** + * Show identification & maintenance settings. + * + * @param net InOut: The p-net stack instance + */ +void pf_fspm_im_show (const pnet_t * net); + +/** + * Show compile time options, and memory usage. + * + * @param net InOut: The p-net stack instance + */ +void pf_fspm_option_show (const pnet_t * net); + +/** + * Show logbook. + * + * @param net InOut: The p-net stack instance + */ +void pf_fspm_logbook_show (const pnet_t * net); + +/************ Internal functions, made available for unit testing ************/ + +int pf_fspm_validate_configuration (const pnet_cfg_t * p_cfg); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_FSPM_H */ diff --git a/profinet_stack/p-net/src/device/pf_pdport.c b/profinet_stack/p-net/src/device/pf_pdport.c new file mode 100644 index 0000000..ea4df85 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_pdport.c @@ -0,0 +1,1535 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST +#define pnal_eth_get_status mock_pnal_eth_get_status +#define pnal_get_port_statistics mock_pnal_get_port_statistics +#define pf_bg_worker_start_job mock_pf_bg_worker_start_job +#endif + +#include "pf_includes.h" +#include "pf_block_writer.h" +#include "pf_block_reader.h" +#include + +/** + * @file + * @brief Management Physical Device Port (PD Port) data + */ + +/** + * Get configuration file name for one port + * + * Asserts for invalid port number. + * + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return The configuration file name + */ +static const char * pf_pdport_get_filename (int loc_port_num) +{ + const char * filename = NULL; + switch (loc_port_num) + { + case PNET_PORT_1: + filename = PF_FILENAME_PDPORT_1; + break; + case PNET_PORT_2: + filename = PF_FILENAME_PDPORT_2; + break; + case PNET_PORT_3: + filename = PF_FILENAME_PDPORT_3; + break; + case PNET_PORT_4: + filename = PF_FILENAME_PDPORT_4; + break; + default: + CC_ASSERT (false); + } + return filename; +} + +/** + * Load PDPort data for one port from nvm + * + * Enables or disable LLDP transmission. + * Checks presence of peer. + * + * Asserts for invalid port number. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_pdport_load (pnet_t * net, int loc_port_num) +{ + int ret = -1; + pf_pdport_t pdport_config; + const char * p_file_directory = pf_cmina_get_file_directory (net); + pf_port_t * p_port_data = NULL; + + if ( + pf_file_load ( + p_file_directory, + pf_pdport_get_filename (loc_port_num), + &pdport_config, + sizeof (pdport_config)) == 0) + { + p_port_data = pf_port_get_state (net, loc_port_num); + + memcpy (&p_port_data->pdport, &pdport_config, sizeof (pdport_config)); + p_port_data->pdport.lldp_peer_info_updated = false; + + if (p_port_data->pdport.check.active == true) + { + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Did read PDPort settings from nvm. Monitoring peer " + "on port %u. Station name: \"%.*s\" Port: \"%.*s\"\n", + __LINE__, + loc_port_num, + p_port_data->pdport.check.peer.length_peer_station_name, + p_port_data->pdport.check.peer.peer_station_name, + p_port_data->pdport.check.peer.length_peer_port_name, + p_port_data->pdport.check.peer.peer_port_name); + + /* Check peers directly at the next stack invocation. As this is done + at startup, there is likely no peer info available and a diagnosis + will be set. + Note that the application must have time to plug the relevant + submodule before any diagnosis can be triggered, why we must + wait to next periodic invocation before doing the check. */ + pf_lldp_restart_peer_timeout (net, loc_port_num, 0); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Did read PDPort settings from nvm. No monitoring " + "of peer on port %u.\n", + __LINE__, + loc_port_num); + } + + if (p_port_data->pdport.adjust.active == true) + { + if ( + p_port_data->pdport.adjust.peer_to_peer_boundary + .peer_to_peer_boundary.do_not_send_LLDP_frames == 1) + { + pf_lldp_send_disable (net, loc_port_num); + } + else + { + pf_lldp_send_enable (net, loc_port_num); + } + } + else + { + pf_lldp_send_enable (net, loc_port_num); + } + + ret = 0; + } + else + { + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Could not read from nvm, so no monitoring of " + "peer on port %u.\n", + __LINE__, + loc_port_num); + pf_lldp_send_enable (net, loc_port_num); + } + + return ret; +} + +/** + * Store PDPort data for one port to nvm + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_pdport_save (pnet_t * net, int loc_port_num) +{ + int ret = 0; + int save_result = 0; + pf_pdport_t pdport_config = {0}; + pf_pdport_t temporary_buffer; + const char * p_file_directory = pf_cmina_get_file_directory (net); + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->pf_interface.port_mutex); + memcpy (&pdport_config, &p_port_data->pdport, sizeof (pdport_config)); + os_mutex_unlock (net->pf_interface.port_mutex); + + save_result = pf_file_save_if_modified ( + p_file_directory, + pf_pdport_get_filename (loc_port_num), + &pdport_config, + &temporary_buffer, + sizeof (pf_pdport_t)); + + switch (save_result) + { + case 2: + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): First nvm saving of port settings.\n", + __LINE__); + break; + case 1: + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): Updating nvm stored port settings.\n", + __LINE__); + break; + case 0: + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): No storing of nvm port settings (no changes).\n", + __LINE__); + break; + default: + case -1: + ret = -1; + LOG_ERROR ( + PNET_LOG, + "PDPORT(%d): Failed to store nvm port settings.\n", + __LINE__); + break; + } + return ret; +} + +int pf_pdport_save_all (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + + if (net->pf_interface.port_mutex == NULL) + { + /* Handle case during init when port data is not yet initialized. */ + return -1; + } + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + (void)pf_pdport_save (net, port); + port = pf_port_get_next (&port_iterator); + } + return 0; +} + +bool pf_pdport_is_a_fast_port_in_use (pnet_t * net) +{ + int loc_port_num; + pf_port_iterator_t port_iterator; + pnal_eth_status_t eth_status; + bool found = false; + bool fast_enough = false; +#if LOG_DEBUG_ENABLED(PNET_LOG) + const pnet_port_cfg_t * port_config; +#endif + + pf_port_init_iterator_over_ports (net, &port_iterator); + loc_port_num = pf_port_get_next (&port_iterator); + while (loc_port_num != 0) + { + eth_status = pf_pdport_get_eth_status (net, loc_port_num); + fast_enough = eth_status.operational_mau_type >= + PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + +#if LOG_DEBUG_ENABLED(PNET_LOG) + port_config = pf_port_get_config (net, loc_port_num); + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Checking local Ethernet port %u \"%s\". MAU type: %u " + "%s. Running: %s.\n", + __LINE__, + loc_port_num, + port_config->netif_name, + eth_status.operational_mau_type, + fast_enough ? "Fast enough" : "Too slow", + eth_status.running ? "Yes" : "No"); +#endif + if (fast_enough && eth_status.running) + { + /* Do not return early, to list all ports in the debug output */ + found = true; + } + + loc_port_num = pf_port_get_next (&port_iterator); + } + + return found; +} + +/** + * @internal + * Initialize pdport diagnostic source + * + * If the local port number is out of range this operation will assert. + * + * @param net InOut: The p-net stack instance + * @param diag_source InOut: Diag source to be initialized + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_init_diag_source ( + pnet_t * net, + pnet_diag_source_t * diag_source, + int loc_port_num) +{ + diag_source->api = PNET_API_NO_APPLICATION_PROFILE; + diag_source->slot = PNET_SLOT_DAP_IDENT; + diag_source->subslot = + pf_port_loc_port_num_to_dap_subslot (net, loc_port_num); + diag_source->ch = PNET_CHANNEL_WHOLE_SUBMODULE; + diag_source->ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL; + diag_source->ch_direction = PNET_DIAG_CH_PROP_DIR_MANUF_SPEC; +} + +/** + * Delete all active diagnostics handled by pdport + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports. + */ +static void pf_pdport_remove_all_diag (pnet_t * net, int loc_port_num) +{ + pnet_diag_source_t diag_source = {0}; + + pf_pdport_init_diag_source (net, &diag_source, loc_port_num); + + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Remove diagnosis about remote mismatch (if any) " + "for port %u.\n", + __LINE__, + loc_port_num); + + (void)pf_diag_std_remove ( + net, + &diag_source, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_PEER_STATIONNAME_MISMATCH); + + (void)pf_diag_std_remove ( + net, + &diag_source, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_PEER_PORTNAME_MISMATCH); + + (void)pf_diag_std_remove ( + net, + &diag_source, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_NO_PEER_DETECTED); +} + +/** + * Init PDPort data. + * Load port configuration from nvm. + + * If a check is loaded it will be run when PDPort receives data from LLDP. + * + * @param net InOut: The p-net stack instance + * @return 0 if the operation succeeded. + * -1 if not found or an error occurred + */ +int pf_pdport_init (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + if (pf_pdport_load (net, port) != 0) + { + /* Create file if missing */ + (void)pf_pdport_save (net, port); + } + + port = pf_port_get_next (&port_iterator); + } + return 0; +} + +int pf_pdport_reset_all (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + memset (&p_port_data->pdport, 0, sizeof (p_port_data->pdport)); + pf_pdport_remove_all_diag (net, port); + + port = pf_port_get_next (&port_iterator); + } + + (void)pf_bg_worker_start_job (net, PF_BGJOB_SAVE_PDPORT_NVM_DATA); + + return 0; +} + +void pf_pdport_remove_data_files (const char * file_directory) +{ + int port; + + /* Do not use port iterator as net is not available */ + for (port = PNET_PORT_1; port <= PNET_MAX_PHYSICAL_PORTS; port++) + { + pf_file_clear (file_directory, pf_pdport_get_filename (port)); + } +} + +void pf_pdport_ar_connect_ind (pnet_t * net, const pf_ar_t * p_ar) +{ + (void)p_ar; + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + if (p_port_data->pdport.adjust.active == true) + { + if ( + p_port_data->pdport.adjust.peer_to_peer_boundary + .peer_to_peer_boundary.do_not_send_LLDP_frames == 1) + { + pf_lldp_send_disable (net, port); + } + else + { + pf_lldp_send_enable (net, port); + } + } + port = pf_port_get_next (&port_iterator); + } +} + +void pf_pdport_lldp_restart_transmission (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + + if (p_port_data->pdport.adjust.active == true) + { + if ( + p_port_data->pdport.adjust.peer_to_peer_boundary + .peer_to_peer_boundary.do_not_send_LLDP_frames == 1) + { + pf_lldp_send_disable (net, port); + } + else + { + pf_lldp_send_enable (net, port); + } + } + else + { + pf_lldp_send_enable (net, port); + } + + port = pf_port_get_next (&port_iterator); + } +} + +int pf_pdport_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_req, + const pf_iod_read_result_t * p_read_res, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_pos) +{ + int ret = -1; + int loc_port_num; + pf_port_iterator_t port_iterator; + pnal_port_stats_t port_statistics; + pf_port_t * p_port_data; + uint16_t slot = p_read_req->slot_number; + uint16_t subslot = p_read_req->subslot_number; + uint16_t index = p_read_req->index; + const char * netif_name; + pf_lldp_station_name_t peer_station_name; + pf_lldp_port_name_t peer_port_name; + pnal_eth_status_t eth_status; + char station_name[PNET_STATION_NAME_MAX_SIZE]; /* Terminated*/ + uint16_t subslot_for_port; + + switch (index) + { + case PF_IDX_SUB_PDPORT_DATA_REAL: + if ( + (slot == PNET_SLOT_DAP_IDENT) && + (pf_port_subslot_is_dap_port_id (net, subslot))) + { + loc_port_num = pf_port_dap_subslot_to_local_port (net, subslot); + + p_port_data = pf_port_get_state (net, loc_port_num); + eth_status = pf_pdport_get_eth_status_filtered_mau (net, loc_port_num); + /* Look up peer info. Subfields .len indicates whether peer is found */ + (void)pf_lldp_get_peer_station_name ( + net, + loc_port_num, + &peer_station_name); + (void)pf_lldp_get_peer_port_name (net, loc_port_num, &peer_port_name); + + pf_put_pdport_data_real ( + true, + subslot, + &peer_station_name, + &peer_port_name, + p_port_data, + pf_port_get_media_type (eth_status.operational_mau_type), + ð_status, + res_size, + p_res, + p_pos); + ret = 0; + } + break; + + case PF_IDX_SUB_PDPORT_DATA_ADJ: + if ( + (slot == PNET_SLOT_DAP_IDENT) && + (pf_port_subslot_is_dap_port_id (net, subslot))) + { + loc_port_num = pf_port_dap_subslot_to_local_port (net, subslot); + p_port_data = pf_port_get_state (net, loc_port_num); + if (p_port_data->pdport.adjust.active) + { + pf_put_pdport_data_adj ( + true, + subslot, + &p_port_data->pdport.adjust.peer_to_peer_boundary, + res_size, + p_res, + p_pos); + } + ret = 0; + } + break; + + case PF_IDX_SUB_PDPORT_DATA_CHECK: + if (slot == PNET_SLOT_DAP_IDENT) + { + if (pf_port_subslot_is_dap_port_id (net, subslot)) + { + loc_port_num = pf_port_dap_subslot_to_local_port (net, subslot); + p_port_data = pf_port_get_state (net, loc_port_num); + if (p_port_data->pdport.check.active) + { + pf_put_pdport_data_check ( + true, + p_read_res->slot_number, + p_read_res->subslot_number, + &p_port_data->pdport.check.peer, + res_size, + p_res, + p_pos); + } + ret = 0; + } + else if (subslot == PNET_SUBSLOT_DAP_WHOLE_MODULE) + { + pf_port_init_iterator_over_ports (net, &port_iterator); + loc_port_num = pf_port_get_next (&port_iterator); + while (loc_port_num != 0) + { + p_port_data = pf_port_get_state (net, loc_port_num); + if (p_port_data->pdport.check.active) + { + pf_put_pdport_data_check ( + true, + p_read_res->slot_number, + p_read_res->subslot_number, + &p_port_data->pdport.check.peer, + res_size, + p_res, + p_pos); + } + } + ret = 0; + } + } + break; + + case PF_IDX_DEV_PDREAL_DATA: + /* Combined interface and port info and statistics */ + if ( + (slot == PNET_SLOT_DAP_IDENT) && + ((subslot == PNET_SUBSLOT_DAP_WHOLE_MODULE) || + (subslot == PNET_SUBSLOT_DAP_IDENT))) + { + pf_cmina_get_station_name (net, station_name); + if ( + pnal_get_port_statistics ( + net->pf_interface.main_port.name, + &port_statistics) != 0) + { + memset (&port_statistics, 0, sizeof (port_statistics)); + } + + pf_put_pd_multiblock_interface_and_statistics ( + true, + p_read_res->api, + pf_cmina_get_device_macaddr (net), + pf_cmina_get_ipaddr (net), + pf_cmina_get_netmask (net), + pf_cmina_get_gateway (net), + station_name, + &port_statistics, + res_size, + p_res, + p_pos); + + pf_port_init_iterator_over_ports (net, &port_iterator); + loc_port_num = pf_port_get_next (&port_iterator); + while (loc_port_num != 0) + { + subslot_for_port = + pf_port_loc_port_num_to_dap_subslot (net, loc_port_num); + p_port_data = pf_port_get_state (net, loc_port_num); + + eth_status = + pf_pdport_get_eth_status_filtered_mau (net, loc_port_num); + if ( + pnal_get_port_statistics ( + p_port_data->netif.name, + &port_statistics) != 0) + { + memset (&port_statistics, 0, sizeof (port_statistics)); + } + + /* Look up peer info. Subfields .len indicates whether peer is found + */ + (void)pf_lldp_get_peer_station_name ( + net, + loc_port_num, + &peer_station_name); + (void) + pf_lldp_get_peer_port_name (net, loc_port_num, &peer_port_name); + + pf_put_pd_multiblock_port_and_statistics ( + true, + p_read_res->api, + subslot_for_port, + &peer_station_name, + &peer_port_name, + p_port_data, + pf_port_get_media_type (eth_status.operational_mau_type), + ð_status, + &port_statistics, + res_size, + p_res, + p_pos); + + loc_port_num = pf_port_get_next (&port_iterator); + } + + ret = 0; + } + break; + + case PF_IDX_DEV_PDEXP_DATA: + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): PD exp data is not yet implemented.\n", + __LINE__); + + /* ToDo: Implement when LLDP is done */ + /* + * MultipleBlockHeader, { [PDPortDataCheck] a, [PDPortDataAdjust] a, + * [PDInterfaceMrpDataAdjust], + * [PDInterfaceMrpDataCheck], [PDPortMrpDataAdjust], + * [PDPortFODataAdjust], [PDPortFODataCheck], [PDNCDataCheck], + * [PDInterface-FSUDataAdjust], + * [PDInterfaceAdjust], [PDPortMrpIcDataAdjust], [PDPortMrpIcDataCheck] + * } a The fields SlotNumber and SubslotNumber shall be ignored b Each + * submodule's data (for example interface or port) need its own + * MultipleBlockHeader + */ + ret = 0; + break; + + case PF_IDX_SUB_PDPORT_STATISTIC: + if ( + (slot == PNET_SLOT_DAP_IDENT) && + ((subslot == PNET_SUBSLOT_DAP_INTERFACE_1_IDENT) || + (pf_port_subslot_is_dap_port_id (net, subslot)))) + { + loc_port_num = pf_port_dap_subslot_to_local_port (net, subslot); + + if (loc_port_num == 0) + { + netif_name = net->pf_interface.main_port.name; + } + else + { + p_port_data = pf_port_get_state (net, loc_port_num); + netif_name = p_port_data->netif.name; + } + + if (pnal_get_port_statistics (netif_name, &port_statistics) == 0) + { + pf_put_pdport_statistics ( + true, + &port_statistics, + res_size, + p_res, + p_pos); + ret = 0; + } + } + break; + + case PF_IDX_SUB_PDINTF_ADJUST: + if ( + (slot == PNET_SLOT_DAP_IDENT) && + (subslot == PNET_SUBSLOT_DAP_INTERFACE_1_IDENT)) + { + if (net->pf_interface.name_of_device_mode.active == true) + { + pf_put_pd_interface_adj ( + true, + PNET_SUBSLOT_DAP_INTERFACE_1_IDENT, + net->pf_interface.name_of_device_mode.mode, + res_size, + p_res, + p_pos); + } + ret = 0; + } + break; + + default: + ret = -1; + break; + } + return ret; +} + +/** + * @internal + * Set no-peer-detected diagnosis if peer is missing. + * Remove the diagnosis if peer is available. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_check_no_peer_detected (pnet_t * net, int loc_port_num) +{ + pnet_diag_source_t diag_source; + uint32_t peer_timestamp; + + pf_pdport_init_diag_source (net, &diag_source, loc_port_num); + + if (pf_lldp_get_peer_timestamp (net, loc_port_num, &peer_timestamp) == 0) + { + /* Peer available, remove no-peer-detected diagnosis */ + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Peer is available on port %u. Remove no-peer-detected " + "diagnosis, if any.\n", + __LINE__, + loc_port_num); + (void)pf_diag_std_remove ( + net, + &diag_source, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_NO_PEER_DETECTED); + } + else + { + /* Peer missing, set no-peer-detected diagnosis */ + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): Setting no-peer-detected diagnosis for port %u\n", + __LINE__, + loc_port_num); + (void)pf_diag_std_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_NO_PEER_DETECTED, + 0, + 0); + } +} + +/** + * @internal + * Run peer station name check + * + * Compare LLDP peer information with configured pdport peer. + * Trigger a diagnosis if wrong peer is connected. + * Remove diagnosis if expected peer data is received. + * + * Does nothing if no peer is available. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_check_peer_station_name (pnet_t * net, int loc_port_num) +{ + bool peer_stationname_is_correct = false; + pnet_diag_source_t diag_source = {0}; + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + pf_check_peer_t * p_wanted_peer = &p_port_data->pdport.check.peer; + pf_lldp_station_name_t lldp_station_name; + + if (pf_lldp_get_peer_station_name (net, loc_port_num, &lldp_station_name) == 0) + { + pf_pdport_init_diag_source (net, &diag_source, loc_port_num); + + if ( + strncmp ( + lldp_station_name.string, + (char *)p_wanted_peer->peer_station_name, + lldp_station_name.len) == 0) + { + peer_stationname_is_correct = true; + } + + if (peer_stationname_is_correct == false) + { + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): Setting peer station name mismatch diagnosis: " + "\"%.*s\". " + "Wanted peer station name: \"%.*s\"\n", + __LINE__, + (int)lldp_station_name.len, + lldp_station_name.string, + p_wanted_peer->length_peer_station_name, + p_wanted_peer->peer_station_name); + + (void)pf_diag_std_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_PEER_STATIONNAME_MISMATCH, + 0, + 0); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Peer station name is correct: \"%.*s\". " + "Remove diagnosis, if any.\n", + __LINE__, + (int)lldp_station_name.len, + lldp_station_name.string); + (void)pf_diag_std_remove ( + net, + &diag_source, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_PEER_STATIONNAME_MISMATCH); + } + } +} + +/** + * @internal + * Run peer port name check + * + * Compare LLDP peer information with configured pdport peer. + * Trigger an diagnosis if unexpected peer data is connected. + * Remove diagnosis if expected peer data is received. + * + * Does nothing if no peer is available. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_check_peer_port_name (pnet_t * net, int loc_port_num) +{ + bool peer_portname_is_correct = false; + pnet_diag_source_t diag_source = {0}; + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + pf_check_peer_t * p_wanted_peer = &p_port_data->pdport.check.peer; + pf_lldp_port_id_t lldp_port_id; + + if (pf_lldp_get_peer_port_id (net, loc_port_num, &lldp_port_id) == 0) + { + pf_pdport_init_diag_source (net, &diag_source, loc_port_num); + + if ( + strncmp ( + lldp_port_id.string, + (char *)p_wanted_peer->peer_port_name, + lldp_port_id.len) == 0) + { + peer_portname_is_correct = true; + } + + if (peer_portname_is_correct == false) + { + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): Setting peer port name mismatch diagnosis: \"%.*s\". " + "Wanted peer port name: \"%.*s\"\n", + __LINE__, + (int)lldp_port_id.len, + lldp_port_id.string, + p_wanted_peer->length_peer_port_name, + p_wanted_peer->peer_port_name); + + (void)pf_diag_std_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_PEER_PORTNAME_MISMATCH, + 0, + 0); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): Peer port name is correct: \"%.*s\". " + "Remove diagnosis, if any.\n", + __LINE__, + (int)lldp_port_id.len, + lldp_port_id.string); + (void)pf_diag_std_remove ( + net, + &diag_source, + PF_WRT_ERROR_REMOTE_MISMATCH, + PF_WRT_ERROR_PEER_PORTNAME_MISMATCH); + } + } +} + +/** + * @internal + * Run peer check if active + * + * Compare LLDP peer information with configured PDPort peer, if checking is + * active. + * + * Trigger an diagnosis if unexpected peer data is connected. + * Remove diagnosis if expected peer data is received. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_run_peer_check (pnet_t * net, int loc_port_num) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + if (p_port_data->pdport.check.active == true) + { + pf_pdport_check_no_peer_detected (net, loc_port_num); + pf_pdport_check_peer_port_name (net, loc_port_num); + pf_pdport_check_peer_station_name (net, loc_port_num); + } + else + { + LOG_DEBUG ( + PNET_LOG, + "PDPORT(%d): We do not check peer name or portID, so no port-change " + "diagnosis is triggered.\n", + __LINE__); + } +} + +/** + * @internal + * Handle that an Ethernet link goes down. + * + * Trigger a diagnosis if a check is enabled. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_handle_link_down (pnet_t * net, int loc_port_num) +{ + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): Link went down on port %u.\n", + __LINE__, + loc_port_num); + pf_lldp_stop_peer_timeout (net, loc_port_num); + pf_lldp_invalidate_peer_info (net, loc_port_num); + pf_pdport_peer_indication (net, loc_port_num); +} + +/** + * @internal + * Handle that an Ethernet link goes up. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_handle_link_up (pnet_t * net, int loc_port_num) +{ + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): Link went up on port %u.\n", + __LINE__, + loc_port_num); +} + +/** + * @internal + * Monitor the Ethernet link status + * + * Set appropriate diagnosis, if check is enabled. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +static void pf_pdport_monitor_link (pnet_t * net, int loc_port_num) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + pnal_eth_status_t eth_status = pf_pdport_get_eth_status (net, loc_port_num); + + if (p_port_data->netif.previous_is_link_up == false && eth_status.running) + { + pf_pdport_handle_link_up (net, loc_port_num); + } + else if (p_port_data->netif.previous_is_link_up && eth_status.running == false) + { + pf_pdport_handle_link_down (net, loc_port_num); + } + + p_port_data->netif.previous_is_link_up = eth_status.running; +} + +/** + * @internal + * Trigger monitoring the state of one Ethernet link. + * + * Set appropriate diagnosis, if check is enabled. + * Will loop through all Ethernet links, one per call. + * + * This is a callback for the scheduler. Arguments should fulfill + * pf_scheduler_timeout_ftn_t + * + * Re-schedules itself. + * + * @param net InOut: The p-net stack instance + * @param arg In: Not used + * @param current_time In: Not used. + */ +static void pf_lldp_trigger_linkmonitor ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + + int port = + pf_port_get_next_repeat_cyclic (&net->pf_interface.link_monitor_iterator); + + (void)pf_bg_worker_start_job (net, PF_BGJOB_UPDATE_PORTS_STATUS); + + pf_pdport_monitor_link (net, port); + + if ( + pf_scheduler_add ( + net, + PF_LINK_MONITOR_INTERVAL, + pf_lldp_trigger_linkmonitor, + NULL, + &net->pf_interface.link_monitor_timeout) != 0) + { + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Failed to reschedule Ethernet link check\n", + __LINE__); + } +} + +void pf_pdport_start_linkmonitor (pnet_t * net) +{ + if ( + pf_scheduler_add ( + net, + PF_LINK_MONITOR_INTERVAL, + pf_lldp_trigger_linkmonitor, + NULL, + &net->pf_interface.link_monitor_timeout) != 0) + { + LOG_ERROR ( + PF_LLDP_LOG, + "LLDP(%d): Failed to schedule Ethernet link check\n", + __LINE__); + } +} + +void pf_pdport_periodic (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + + if (p_port_data->pdport.lldp_peer_info_updated) + { + p_port_data->pdport.lldp_peer_info_updated = false; + pf_pdport_run_peer_check (net, port); + } + + port = pf_port_get_next (&port_iterator); + } +} + +/** + * @internal + * Write PDPort data check + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_write_req In: The IODWrite request. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param p_bytes In: Input data + * @param p_datalength In: Size of the data to write. + * @param p_result Out: Detailed error information. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_pdport_write_data_check ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_req, + int loc_port_num, + const uint8_t * p_bytes, + uint16_t p_datalength, + pnet_result_t * p_result) +{ + int ret = -1; + uint16_t pos = 0; + pf_get_info_t get_info; + pf_port_data_check_t port_data_check = {0}; + pf_check_peers_t check_peers = {0}; + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + get_info.result = PF_PARSE_OK; + get_info.p_buf = p_bytes; + get_info.is_big_endian = true; + get_info.len = p_datalength; + + pf_get_port_data_check (&get_info, &pos, &port_data_check); + + switch (port_data_check.block_header.block_type) + { + case PF_BT_CHECKPEERS: + pf_get_port_data_check_check_peers ( + &get_info, + &pos, + PF_CHECK_PEERS_PER_PORT, + &check_peers); + if (get_info.result == PF_PARSE_OK) + { + if (check_peers.number_of_peers >= 1) + { + /* There is max one peer in the check_peers */ + p_port_data->pdport.check.peer = check_peers.peers[0]; + p_port_data->pdport.check.active = true; + + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): New PDPort data check. Peers: %u Wanted first peer " + "station name: %.*s Port: %.*s\n", + __LINE__, + check_peers.number_of_peers, + check_peers.peers[0].length_peer_station_name, + check_peers.peers[0].peer_station_name, + check_peers.peers[0].length_peer_port_name, + check_peers.peers[0].peer_port_name); + + pf_pdport_run_peer_check (net, loc_port_num); + + ret = 0; + } + else + { + LOG_ERROR ( + PNET_LOG, + "PDPORT(%d): Wrong incoming PDPort data check number of peers: " + "%u. Slot: %u Subslot: 0x%04x\n", + __LINE__, + check_peers.number_of_peers, + port_data_check.slot_number, + port_data_check.subslot_number); + } + } + else + { + LOG_ERROR ( + PNET_LOG, + "PDPORT(%d): Failed to parse incoming PDPort data check.\n", + __LINE__); + } + break; + default: + LOG_ERROR ( + PF_RPC_LOG, + "PDPORT(%d): Unsupported port data check block type 0x%x\n", + __LINE__, + port_data_check.block_header.block_type); + break; + } + return ret; +} + +/** + * @internal + * Write PDPort data adjust + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_write_req In: The IODWrite request. + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @param p_bytes In: Input data + * @param p_datalength In: Size of the data to write. + * @param p_result Out: Detailed error information. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_pdport_write_data_adj ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_req, + int loc_port_num, + const uint8_t * p_bytes, + uint16_t p_datalength, + pnet_result_t * p_result) +{ + int ret = -1; + uint16_t pos = 0; + pf_get_info_t get_info; + pf_port_data_adjust_t port_data_adjust = {0}; + pf_adjust_peer_to_peer_boundary_t boundary; + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + get_info.result = PF_PARSE_OK; + get_info.p_buf = p_bytes; + get_info.is_big_endian = true; + get_info.len = p_datalength; + + pf_get_port_data_adjust (&get_info, &pos, &port_data_adjust); + + switch (port_data_adjust.block_header.block_type) + { + case PF_BT_PEER_TO_PEER_BOUNDARY: + pf_get_port_data_adjust_peer_to_peer_boundary (&get_info, &pos, &boundary); + if (get_info.result == PF_PARSE_OK) + { + p_port_data->pdport.adjust.active = true; + p_port_data->pdport.adjust.peer_to_peer_boundary = boundary; + + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): New PDPort data adjust. Do not send LLDP: %u\n", + __LINE__, + boundary.peer_to_peer_boundary.do_not_send_LLDP_frames); + + if ( + p_port_data->pdport.adjust.peer_to_peer_boundary + .peer_to_peer_boundary.do_not_send_LLDP_frames == 1) + { + pf_lldp_send_disable (net, loc_port_num); + } + else + { + pf_lldp_send_enable (net, loc_port_num); + } + + ret = 0; + } + else + { + LOG_ERROR ( + PNET_LOG, + "CPDPORT(%d): Failed to parse incoming PDPort data adjust.\n", + __LINE__); + } + break; + default: + LOG_ERROR ( + PF_RPC_LOG, + "PDPORT(%d): Unsupported incoming port data adjust block type 0x%x\n", + __LINE__, + port_data_adjust.block_header.block_type); + break; + } + return ret; +} + +/** + * @internal + * Write PDPort interface adjust + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_write_req In: The IODWrite request. + * @param p_bytes In: Input data + * @param p_datalength In: Size of the data to write. + * @param p_result Out: Detailed error information. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +static int pf_pdport_write_interface_adj ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_req, + const uint8_t * p_bytes, + uint16_t p_datalength, + pnet_result_t * p_result) +{ + uint16_t pos = 0; + pf_get_info_t get_info; + + get_info.result = PF_PARSE_OK; + get_info.p_buf = p_bytes; + get_info.is_big_endian = true; + get_info.len = p_datalength; + + pf_get_interface_adjust ( + &get_info, + &pos, + &net->pf_interface.name_of_device_mode.mode); + net->pf_interface.name_of_device_mode.active = true; + + LOG_INFO ( + PNET_LOG, + "PDPORT(%d): PD interface adjust. PLC is setting LLDP mode: %s\n", + __LINE__, + net->pf_interface.name_of_device_mode.mode == + PF_LLDP_NAME_OF_DEVICE_MODE_STANDARD + ? "Standard" + : "Legacy"); + + pf_pdport_lldp_restart_transmission (net); + + return 0; +} + +int pf_pdport_write_req ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_req, + const uint8_t * p_bytes, + uint16_t data_length, + pnet_result_t * p_result) +{ + int ret = -1; + int loc_port_num = + pf_port_dap_subslot_to_local_port (net, p_write_req->subslot_number); + + if (loc_port_num > 0) + { + switch (p_write_req->index) + { + case PF_IDX_SUB_PDPORT_DATA_CHECK: + ret = pf_pdport_write_data_check ( + net, + p_ar, + p_write_req, + loc_port_num, + p_bytes, + data_length, + p_result); + break; + case PF_IDX_SUB_PDPORT_DATA_ADJ: + ret = pf_pdport_write_data_adj ( + net, + p_ar, + p_write_req, + loc_port_num, + p_bytes, + data_length, + p_result); + break; + default: + break; + } + + if (ret == 0) + { + (void)pf_bg_worker_start_job (net, PF_BGJOB_SAVE_PDPORT_NVM_DATA); + } + } + else + { + switch (p_write_req->index) + { + case PF_IDX_SUB_PDINTF_ADJUST: + ret = pf_pdport_write_interface_adj ( + net, + p_ar, + p_write_req, + p_bytes, + data_length, + p_result); + break; + default: + break; + } + } + + return ret; +} + +void pf_pdport_peer_indication (pnet_t * net, int loc_port_num) +{ + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + p_port_data->pdport.lldp_peer_info_updated = true; +} + +void pf_pdport_update_eth_status (pnet_t * net) +{ + int loc_port_num; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + pnal_eth_status_t eth_status; + + if (net->pf_interface.port_mutex == NULL) + { + /* Handle case during init when port data is not yet initialized. */ + return; + } + + pf_port_init_iterator_over_ports (net, &port_iterator); + loc_port_num = pf_port_get_next (&port_iterator); + + while (loc_port_num != 0) + { + p_port_data = pf_port_get_state (net, loc_port_num); + if (pnal_eth_get_status (p_port_data->netif.name, ð_status) != 0) + { + memset (ð_status, 0, sizeof (eth_status)); + LOG_ERROR ( + PNET_LOG, + "PDPORT(%d): Failed to read link status for port %u.\n", + __LINE__, + loc_port_num); + } + os_mutex_lock (net->pf_interface.port_mutex); + p_port_data->eth_status = eth_status; + os_mutex_unlock (net->pf_interface.port_mutex); + + loc_port_num = pf_port_get_next (&port_iterator); + } +} + +/** + * Replace the Ethernet MAU type with the default value, if + * the actual value is unknown. + * + * @param mau_type In: MAU type read from hardware + * @param running In: True if link is up + * @param default_mau_type In: Default MAU type from config + * @return Filtered MAU type + */ +static pnal_eth_mau_t pf_pdport_filter_mau_type ( + pnal_eth_mau_t mau_type, + bool running, + pnal_eth_mau_t default_mau_type) +{ + /** The MAU type 0 represents "unknown" when the link is down. + * When the link is up a MAU type 0 represents "Radio". */ + if (mau_type == PNAL_ETH_MAU_UNKNOWN && !running) + { + return default_mau_type; + } + + return mau_type; +} + +pnal_eth_status_t pf_pdport_get_eth_status (pnet_t * net, int loc_port_num) +{ + pnal_eth_status_t eth_status; + pf_port_t * p_port_data = pf_port_get_state (net, loc_port_num); + + os_mutex_lock (net->pf_interface.port_mutex); + eth_status = p_port_data->eth_status; + os_mutex_unlock (net->pf_interface.port_mutex); + + return eth_status; +} + +pnal_eth_status_t pf_pdport_get_eth_status_filtered_mau ( + pnet_t * net, + int loc_port_num) +{ + const pnet_port_cfg_t * p_port_config = + pf_port_get_config (net, loc_port_num); + pnal_eth_status_t eth_status = pf_pdport_get_eth_status (net, loc_port_num); + + eth_status.operational_mau_type = pf_pdport_filter_mau_type ( + eth_status.operational_mau_type, + eth_status.running, + p_port_config->default_mau_type); + + return eth_status; +} diff --git a/profinet_stack/p-net/src/device/pf_pdport.h b/profinet_stack/p-net/src/device/pf_pdport.h new file mode 100644 index 0000000..6e3eba8 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_pdport.h @@ -0,0 +1,226 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_PDPORT_H +#define PF_PDPORT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * Initialize PDPort. + * Load PDPortport configuration from nvm. + * + * @param net InOut: The p-net stack instance + * @return 0 if the operation succeeded. + * -1 if not found or an error occurred + */ +int pf_pdport_init (pnet_t * net); + +/** + * Reset PDPort configuration data for all ports. + * Clear PDPort configuration in nvm. + * Remove all active diagnostics items related to PDPort. + * + * @param net InOut: The p-net stack instance + * @return 0 if the operation succeeded. + * -1 if not found or an error occurred + */ +int pf_pdport_reset_all (pnet_t * net); + +/** + * Remove configuration files for all ports. + * + * @param * @param file_directory In: File directory + */ +void pf_pdport_remove_data_files (const char * file_directory); + +/** + * Notify PDPort that a new AR has been set up. + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + */ +void pf_pdport_ar_connect_ind (pnet_t * net, const pf_ar_t * p_ar); + +/** + * Restart LLDP transmission + * + * The current PDPort port adjustments is checked. + * @param net InOut: The p-net stack instance + */ +void pf_pdport_lldp_restart_transmission (pnet_t * net); + +/** + * Read a PDPort data record/index + * Record to read defined by index in request + * Parse request and write response to output buffer. + * The following indexes are supported: + * - PF_IDX_SUB_PDPORT_DATA_REAL + * - PF_IDX_SUB_PDPORT_DATA_ADJ + * - PF_IDX_SUB_PDPORT_DATA_CHECK + * - PF_IDX_DEV_PDREAL_DATA + * - PF_IDX_DEV_PDEXP_DATA + * - PF_IDX_SUB_PDPORT_STATISTIC + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param p_read_req In: The read request. + * @param p_read_res In: The result information. + * @param res_size In: The size of the output buffer. + * @param p_res Out: The output buffer. + * @param p_pos InOut: Position in the output buffer. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_pdport_read_ind ( + pnet_t * net, + pf_ar_t * p_ar, + const pf_iod_read_request_t * p_read_req, + const pf_iod_read_result_t * p_read_res, + uint16_t res_size, + uint8_t * p_res, + uint16_t * p_pos); + +/** + * Write a PDPort data record/index + * Record to write defined by index in request + * The following indexes are supported: + * - PF_IDX_SUB_PDPORT_DATA_REAL + * - PF_IDX_SUB_PDPORT_DATA_ADJ + * - PF_IDX_SUB_PDPORT_DATA_CHECK + * - PF_IDX_DEV_PDREAL_DATA + * - PF_IDX_DEV_PDEXP_DATA + * - PF_IDX_SUB_PDPORT_STATISTIC + * + * @param net InOut: The p-net stack instance + * @param p_ar In: The AR instance. + * @param p_write_req In: The IODWrite request. + * @param p_req_buf In: The request buffer. + * @param data_length In: Size of the data to write. + * @param p_result Out: Detailed error information. + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_pdport_write_req ( + pnet_t * net, + const pf_ar_t * p_ar, + const pf_iod_write_request_t * p_write_req, + const uint8_t * p_req_buf, + uint16_t data_length, + pnet_result_t * p_result); + +/** + * Notify PDPort that the LLDP peer information has changed. + * + * PDPort will later verify that correct peer is connected to the port. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + */ +void pf_pdport_peer_indication (pnet_t * net, int loc_port_num); + +/** + * Start Ethernet link monitoring + * + * @param net InOut: The p-net stack instance + */ +void pf_pdport_start_linkmonitor (pnet_t * net); + +/** + * Run PDPort observers. + * Run enabled checks and set diagnoses. + * + * @param net InOut: The p-net stack instance + */ +void pf_pdport_periodic (pnet_t * net); + +/** + * Save PDPort configuration to NVM for all ports. + * This function is intended to be executed in the background task, + * as the system calls to save files may be blocking. + * + * @param net InOut: The p-net stack instance + * @return 0 if operation succeeded. + * -1 if an error occurred. + */ +int pf_pdport_save_all (pnet_t * net); + +/** + * Verify that at least one operational port has a speed of at + * least 100 MBit/s, full duplex. + * + * Otherwise we should refuse to make a connection. + * + * To support the special case of devices with 10 Mbit/s, this function must be + * modified. + * + * See PdevCheckFailed() in Profinet 2.4 Protocol 5.6.3.2.1.4 CMDEV state table + * + * @param net InOut: The p-net stack instance + * @return true if at least one port has the required speed, else false. + */ +bool pf_pdport_is_a_fast_port_in_use (pnet_t * net); + +/** + * Update the ethernet status for all ports. + * This function is intended to be executed in a background task, + * as the system calls to read the ethernet status may be blocking. + * + * @param net InOut: The p-net stack instance + */ +void pf_pdport_update_eth_status (pnet_t * net); + +/** + * Get current ethernet status for local port. + * This is a non-blocking function reading from mutex-protected memory. + * The status values are updated by the background worker task + * in a periodic job executing the pf_pdport_update_eth_status() function. + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return The ethernet status for local port + */ +pnal_eth_status_t pf_pdport_get_eth_status (pnet_t * net, int loc_port_num); + +/** + * Get current ethernet status for local port. + * This is a non-blocking function reading from mutex-protected memory. + * The status values are updated by the background worker task + * in a periodic job executing the pf_pdport_update_eth_status() function. + * + * The MAU type value in the resulting \a pnal_eth_status_t struct + * will be replaced by the default MAU type from the configuration + * when the link is down (and the actual MAU type not can be read from + * hardware). + * + * @param net InOut: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return The ethernet status for local port + */ +pnal_eth_status_t pf_pdport_get_eth_status_filtered_mau ( + pnet_t * net, + int loc_port_num); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_PDPORT_H */ diff --git a/profinet_stack/p-net/src/device/pf_port.c b/profinet_stack/p-net/src/device/pf_port.c new file mode 100644 index 0000000..5e3a9f6 --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_port.c @@ -0,0 +1,311 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pf_includes.h" + +#include + +#if PNET_MAX_PHYSICAL_PORTS < 1 +#error "PNET_MAX_PHYSICAL_PORTS needs to be at least 1" +#endif + +#if PNET_MAX_SLOTS < 1 +#error "At least one slot must be available for DAP. Increase PNET_MAX_SLOTS." +#endif + +#if PNET_MAX_SUBSLOTS < (2 + PNET_MAX_PHYSICAL_PORTS) +#error \ + "DAP requires 2 + PNET_MAX_PHYSICAL_PORTS subslots. Increase PNET_MAX_SUBSLOTS." +#endif + +/** + * @file + * @brief Manage runtime data for multiple ports + */ + +void pf_port_init (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data = NULL; + + net->pf_interface.port_mutex = os_mutex_create(); + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + + p_port_data->port_num = port; + snprintf ( + p_port_data->port_name, + sizeof (p_port_data->port_name), + "port-%03u", + (uint8_t)port); /* Cast to avoid format-truncation */ + + port = pf_port_get_next (&port_iterator); + } +} + +void pf_port_main_interface_init (pnet_t * net) +{ + /* Format of LLDP frames */ + net->pf_interface.name_of_device_mode.mode = + PF_LLDP_NAME_OF_DEVICE_MODE_STANDARD; + net->pf_interface.name_of_device_mode.active = false; + + /* Ethernet link monitor */ + pf_port_init_iterator_over_ports ( + net, + &net->pf_interface.link_monitor_iterator); + pf_scheduler_init_handle ( + &net->pf_interface.link_monitor_timeout, + "link_monitor"); + pf_pdport_start_linkmonitor (net); +} + +uint8_t pf_port_get_number_of_ports (const pnet_t * net) +{ + return net->fspm_cfg.num_physical_ports; +} + +void pf_port_get_list_of_ports (const pnet_t * net, pf_lldp_port_list_t * p_list) +{ + uint16_t port; + uint16_t index = 0; + uint8_t port_bit_mask = 0; + memset (p_list, 0, sizeof (*p_list)); + + for (port = PNET_PORT_1; port <= net->fspm_cfg.num_physical_ports; port++) + { + /* See documentation of pf_lldp_port_list_t for details*/ + index = (port - 1) / 8; + CC_ASSERT (index < sizeof (*p_list)); + port_bit_mask = BIT (7 - ((port - 1) % 8)); + p_list->ports[index] |= port_bit_mask; + } +} + +void pf_port_init_iterator_over_ports ( + const pnet_t * net, + pf_port_iterator_t * p_iterator) +{ + CC_ASSERT (net != NULL); + p_iterator->next_port = 1; + p_iterator->number_of_ports = net->fspm_cfg.num_physical_ports; +} + +int pf_port_get_next (pf_port_iterator_t * p_iterator) +{ + int ret = p_iterator->next_port; + + if ( + p_iterator->next_port == p_iterator->number_of_ports || + p_iterator->next_port == 0) + { + p_iterator->next_port = 0; + } + else if (p_iterator->next_port > p_iterator->number_of_ports) + { + ret = 0; + p_iterator->next_port = 0; + } + else + { + p_iterator->next_port += 1; + } + + return ret; +} + +int pf_port_get_next_repeat_cyclic (pf_port_iterator_t * p_iterator) +{ + int ret; + + if (p_iterator->next_port > p_iterator->number_of_ports) + { + p_iterator->next_port = 1; + } + + ret = p_iterator->next_port; + + if (p_iterator->next_port == p_iterator->number_of_ports) + { + p_iterator->next_port = 1; + } + else + { + p_iterator->next_port += 1; + } + + return ret; +} + +pf_port_t * pf_port_get_state (pnet_t * net, int loc_port_num) +{ + CC_ASSERT (pf_port_is_valid (net, loc_port_num)); + return &net->pf_interface.port[loc_port_num - 1]; +} + +const pnet_port_cfg_t * pf_port_get_config (pnet_t * net, int loc_port_num) +{ + CC_ASSERT (pf_port_is_valid (net, loc_port_num)); + return &net->fspm_cfg.if_cfg.physical_ports[loc_port_num - 1]; +} + +uint16_t pf_port_loc_port_num_to_dap_subslot ( + const pnet_t * net, + int loc_port_num) +{ + CC_ASSERT (pf_port_is_valid (net, loc_port_num)); + return PNET_SUBSLOT_DAP_INTERFACE_1_PORT_1_IDENT + loc_port_num - + PNET_PORT_1; +} + +int pf_port_dap_subslot_to_local_port (const pnet_t * net, uint16_t subslot) +{ + int port = PNET_PORT_1 + subslot - PNET_SUBSLOT_DAP_INTERFACE_1_PORT_1_IDENT; + + if (pf_port_is_valid (net, port) == false) + { + port = 0; + } + + return port; +} + +bool pf_port_subslot_is_dap_port_id (const pnet_t * net, uint16_t subslot) +{ + int port = pf_port_dap_subslot_to_local_port (net, subslot); + return (port != 0); +} + +bool pf_port_is_valid (const pnet_t * net, int loc_port_num) +{ + CC_ASSERT (net != NULL); + + return ( + loc_port_num > 0 && loc_port_num <= net->fspm_cfg.num_physical_ports); +} + +int pf_port_get_port_number (const pnet_t * net, pnal_eth_handle_t * eth_handle) +{ + int loc_port_num; + + for (loc_port_num = 1; loc_port_num <= net->fspm_cfg.num_physical_ports; + loc_port_num++) + { + if (net->pf_interface.port[loc_port_num - 1].netif.handle == eth_handle) + { + return loc_port_num; + } + } + + return 0; +} + +pf_mediatype_values_t pf_port_get_media_type (pnal_eth_mau_t mau_type) +{ + switch (mau_type) + { + case PNAL_ETH_MAU_RADIO: + return PF_PD_MEDIATYPE_RADIO; + case PNAL_ETH_MAU_COPPER_10BaseT: + case PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX: + case PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX: + case PNAL_ETH_MAU_COPPER_1000BaseT_HALF_DUPLEX: + case PNAL_ETH_MAU_COPPER_1000BaseT_FULL_DUPLEX: + return PF_PD_MEDIATYPE_COPPER; + case PNAL_ETH_MAU_FIBER_100BaseFX_HALF_DUPLEX: + case PNAL_ETH_MAU_FIBER_100BaseFX_FULL_DUPLEX: + case PNAL_ETH_MAU_FIBER_1000BaseX_HALF_DUPLEX: + case PNAL_ETH_MAU_FIBER_1000BaseX_FULL_DUPLEX: + return PF_PD_MEDIATYPE_FIBER; + default: + return PF_PD_MEDIATYPE_UNKNOWN; + } +} + +void pf_port_show (pnet_t * net) +{ + int port; + pf_port_iterator_t port_iterator; + pf_port_t * p_port_data; + const pnet_port_cfg_t * p_port_config; + + printf ("\nPorts\n"); + + pf_port_init_iterator_over_ports (net, &port_iterator); + port = pf_port_get_next (&port_iterator); + while (port != 0) + { + p_port_data = pf_port_get_state (net, port); + p_port_config = pf_port_get_config (net, port); + + printf ("Port %u\n", port); + printf (" Port name : \"%s\"\n", p_port_data->port_name); + printf (" ETH name : \"%s\"\n", p_port_config->netif_name); + printf ( + " MAC : %02x:%02x:%02x:%02x:%02x:%02x\n", + p_port_data->netif.mac_address.addr[0], + p_port_data->netif.mac_address.addr[1], + p_port_data->netif.mac_address.addr[2], + p_port_data->netif.mac_address.addr[3], + p_port_data->netif.mac_address.addr[4], + p_port_data->netif.mac_address.addr[5]); + printf ( + " Link is up? : %u\n", + p_port_data->netif.previous_is_link_up); + printf ( + " Peer check active : %u\n", + p_port_data->pdport.check.active); + printf ( + " Peer check station name : \"%s\"\n", + p_port_data->pdport.check.peer.peer_station_name); + printf ( + " Peer check port name : \"%s\"\n", + p_port_data->pdport.check.peer.peer_port_name); + printf ( + " Adjust active : %u\n", + p_port_data->pdport.adjust.active); + printf ( + " Do not send LLDP : %u\n", + p_port_data->pdport.adjust.peer_to_peer_boundary.peer_to_peer_boundary + .do_not_send_LLDP_frames); + printf ( + " Peer info received : %u\n", + p_port_data->lldp.is_peer_info_received); + printf ( + " Peer chassis id : Valid %u \"%s\"\n", + p_port_data->lldp.peer_info.chassis_id.is_valid, + p_port_data->lldp.peer_info.chassis_id.string); + printf ( + " Peer port id : Valid %u \"%s\"\n", + p_port_data->lldp.peer_info.port_id.is_valid, + p_port_data->lldp.peer_info.port_id.string); + printf ( + " Peer port descr : Valid %u \"%s\"\n", + p_port_data->lldp.peer_info.port_description.is_valid, + p_port_data->lldp.peer_info.port_description.string); + printf ( + " Peer MAU type : Valid %u %u\n", + p_port_data->lldp.peer_info.phy_config.is_valid, + p_port_data->lldp.peer_info.phy_config.operational_mau_type); + + port = pf_port_get_next (&port_iterator); + } +} diff --git a/profinet_stack/p-net/src/device/pf_port.h b/profinet_stack/p-net/src/device/pf_port.h new file mode 100644 index 0000000..1e1572c --- /dev/null +++ b/profinet_stack/p-net/src/device/pf_port.h @@ -0,0 +1,210 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_PORT_H +#define PF_PORT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Init port data + * + * @param net In: The p-net stack instance. + */ +void pf_port_init (pnet_t * net); + +/** + * Init main interface data, and schedule Ethernet link monitoring + * + * @param net In: The p-net stack instance. + */ +void pf_port_main_interface_init (pnet_t * net); + +/** + * Get number of physical ports. + * + * @param net In: The p-net stack instance. + * @return the number of physical ports. + */ +uint8_t pf_port_get_number_of_ports (const pnet_t * net); + +/** + * Get list of local ports. + * + * This is the list of physical ports on the local interface. + * The management port is not included. + * + * @param net In: The p-net stack instance. + * @param p_list Out: List of local ports. + */ +void pf_port_get_list_of_ports ( + const pnet_t * net, + pf_lldp_port_list_t * p_list); + +/** + * Initialise iterator for iterating over local ports. + * + * This iterator may be used for iterating over all physical ports + * on the local interface. The management port is not included. + * See pf_port_get_next(). + * + * @param net In: The p-net stack instance. + * @param p_iterator Out: Port iterator. + */ +void pf_port_init_iterator_over_ports ( + const pnet_t * net, + pf_port_iterator_t * p_iterator); + +/** + * Get next local port. + * + * If no more ports are available, 0 is returned. + * + * Will return: 1 2 3 ... num_physical_ports 0 0 0 + * + * @param p_iterator InOut: Port iterator. + * @return Local port number for next port on local interface. + * If no more ports are available, 0 is returned. + */ +int pf_port_get_next (pf_port_iterator_t * p_iterator); + +/** + * Get next local port, over and over again. + * + * After highest port number has been returned, next time 1 will be returned + * (and then 2 etc) + * + * Will return: 1 2 3 ... num_physical_ports 1 2 3 ... etc + * + * @param p_iterator InOut: Port iterator. + * @return Local port number for next port on local interface. + */ +int pf_port_get_next_repeat_cyclic (pf_port_iterator_t * p_iterator); + +/** + * Get DAP port subslot using local port number + * + * If the local port number is out of range this operation will assert. + * + * @param net In: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return DAP subslot number for port identity + */ +uint16_t pf_port_loc_port_num_to_dap_subslot ( + const pnet_t * net, + int loc_port_num); + +/** + * Check if a DAP port subslot is mapped to a local port + * + * @param net In: The p-net stack instance + * @param subslot In: Subslot number + * @return true if the subslot is mapped to a local port. + * false if the subslot is not supported. + */ +bool pf_port_subslot_is_dap_port_id (const pnet_t * net, uint16_t subslot); + +/** + * Get local port from DAP port subslot + * + * @param net In: The p-net stack instance + * @param subslot In: Subslot number + * @return The port number mapping to the subslot. + * 0 if the subslot is not supported. + */ +int pf_port_dap_subslot_to_local_port (const pnet_t * net, uint16_t subslot); + +/** + * Get port runtime data. + * + * If the local port number is out of range this operation will assert. + * NULL will never be returned. + * + * See also \a pf_port_get_config() for configuration of the port. + * + * @param net In: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return port runtime data + */ +pf_port_t * pf_port_get_state (pnet_t * net, int loc_port_num); + +/** + * Get port configuration. + * + * If the local port number is out of range this operation will assert. + * NULL will never be returned. + * + * See also \a pf_port_get_state() for port runtime data. + * + * @param net In: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return port configuration + */ +const pnet_port_cfg_t * pf_port_get_config (pnet_t * net, int loc_port_num); + +/** + * Check if port number corresponds to a valid physical port. + * + * @param net In: The p-net stack instance + * @param loc_port_num In: Local port number. + * Valid range: 1 .. num_physical_ports + * @return true if port number is valid, + * false if not + */ +bool pf_port_is_valid (const pnet_t * net, int loc_port_num); + +/** + * Get port number for network interface. If network interface + * is not a physical port, zero is returned. + * + * @param net In: The p-net stack instance. + * @param eth_handle In: Network interface handle. + * @return local port number + * 0 if no local port matches the network interface handle + */ +int pf_port_get_port_number (const pnet_t * net, pnal_eth_handle_t * eth_handle); + +/** + * Decode media type from Ethernet MAU type. + * Media types listed in PN-AL-protocol (Mar20) Table 727. + * + * The MAU type 0 represents "unknown" when the link is down. + * When the link is up a MAU type 0 represents "Radio". + * + * This function assumes that the link is up. Make sure you use + * some default MAU type if the link is down, before using this function. + * + * @param mau_type In: Ethernet MAU type + * @return media type + */ +pf_mediatype_values_t pf_port_get_media_type (pnal_eth_mau_t mau_type); + +/** + * Show port details + * + * @param net InOut: The p-net stack instance + */ +void pf_port_show (pnet_t * net); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_PORT_H */ diff --git a/profinet_stack/p-net/src/device/pnet_api.c b/profinet_stack/p-net/src/device/pnet_api.c new file mode 100644 index 0000000..bbd7c5a --- /dev/null +++ b/profinet_stack/p-net/src/device/pnet_api.c @@ -0,0 +1,766 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifdef UNIT_TEST +#define pnal_snmp_init mock_pnal_snmp_init +#define pf_bg_worker_init mock_pf_bg_worker_init +#endif + +#include +#include +#include + +#include "pf_includes.h" +#include "pf_block_reader.h" + +#if PNET_MAX_AR < 1 +#error "PNET_MAX_AR needs to be at least 1" +#endif + +#if PNET_MAX_API < 1 +#error "PNET_MAX_API needs to be at least 1" +#endif + +#define PNET_PERIODIC_WARNING_FACTOR 3 + +int pnet_init_only (pnet_t * net, const pnet_cfg_t * p_cfg) +{ + memset (net, 0, sizeof (*net)); + + /* Initialize configuration */ + if (pf_fspm_init (net, p_cfg) != 0) + { + return -1; + } + + net->cmdev_initialized = false; /* TODO How to handle that pf_cmdev_exit() + is used before pf_cmdev_init()? */ + + pf_scheduler_init (net, p_cfg->tick_us); + +#if PNET_OPTION_DRIVER_ENABLE + if (net->fspm_cfg.driver_enable) + { + pf_driver_init (net); + } +#endif + + pf_cpm_init (net); + pf_ppm_init (net); + pf_alarm_init (net); + + if (pf_eth_init (net, p_cfg) != 0) + { + LOG_ERROR ( + PNET_LOG, + "API(%d): Failed to initialise network interfaces\n", + __LINE__); + return -1; + } + + pf_bg_worker_init (net); + pf_cmina_init (net); /* Read from permanent pool */ + + pf_dcp_exit (net); /* Prepare for re-init. */ + pf_dcp_init (net); /* Start DCP */ + pf_port_init (net); + pf_port_main_interface_init (net); + pf_lldp_init (net); + pf_pdport_init (net); + + /* Configure SNMP server if enabled */ +#if PNET_OPTION_SNMP + pf_snmp_data_init (net); + if (pnal_snmp_init (net, &p_cfg->pnal_cfg) != 0) + { + LOG_ERROR (PNET_LOG, "API(%d): Failed to configure SNMP\n", __LINE__); + return -1; + } +#endif + + pf_cmdev_exit (net); /* Prepare for re-init */ + pf_cmdev_init (net); + + pf_cmrpc_init (net); + + net->timestamp_handle_periodic_us = os_get_current_time_us(); + + return 0; +} + +pnet_t * pnet_init (const pnet_cfg_t * p_cfg) +{ + pnet_t * net = NULL; + + LOG_DEBUG (PNET_LOG, "API(%d): Application calls pnet_init()\n", __LINE__); + + net = os_malloc (sizeof (*net)); + if (net == NULL) + { + LOG_ERROR ( + PNET_LOG, + "API(%d): Failed to allocate memory for pnet_t (%zu bytes)\n", + __LINE__, + sizeof (*net)); + return NULL; + } + + if (pnet_init_only (net, p_cfg) != 0) + { + free (net); + return NULL; + } + + return net; +} + +void pnet_handle_periodic (pnet_t * net) +{ +#if LOG_DEBUG_ENABLED(PNET_LOG) + uint32_t start_time_us = os_get_current_time_us(); + uint32_t end_time_us = 0; + + if (pf_cmina_has_timed_out ( + start_time_us, + net->timestamp_handle_periodic_us, + net->fspm_cfg.min_device_interval, + PNET_PERIODIC_WARNING_FACTOR)) + { + LOG_DEBUG ( + PNET_LOG, + "API(%d): Too long since pnet_handle_periodic() was called: %" PRIu32 + " microseconds\n", + __LINE__, + start_time_us - net->timestamp_handle_periodic_us); + } +#endif + + pf_cmrpc_periodic (net); + pf_alarm_periodic (net); + + /* Handle expired timeout events */ + pf_scheduler_tick (net); + + pf_pdport_periodic (net); + +#if LOG_DEBUG_ENABLED(PNET_LOG) + end_time_us = os_get_current_time_us(); + if (pf_cmina_has_timed_out ( + end_time_us, + start_time_us, + net->fspm_cfg.min_device_interval, + 1)) + { + LOG_DEBUG ( + PNET_LOG, + "API(%d): pnet_handle_periodic() took too long: %" PRIu32 + " microseconds\n", + __LINE__, + end_time_us - start_time_us); + } + net->timestamp_handle_periodic_us = end_time_us; +#endif +} + +void pnet_show (pnet_t * net, unsigned level) +{ + if (net != NULL) + { + if (level & 0x0010) + { + pf_fspm_option_show (net); + } + + if (level & 0x0020) + { + pf_cmdev_device_show (net); + } + + pf_cmrpc_show (net, level); + + if (level & 0x0200) + { + pf_cmdev_diag_show (net); + } + + if (level & 0x0400) + { + pf_fspm_logbook_show (net); + } + + if (level & 0x2000) + { + printf ("\n\n"); + pf_cmina_show (net); + } + if (level & 0x4000) + { + printf ("\n\n"); + pf_scheduler_show (net); + } + if (level & 0x8000) + { + printf ("\n\n"); + pf_fspm_im_show (net); + } + if (level & 0x0100) + { + pf_port_show (net); + } + if (level & 0x0080) + { +#if PNET_OPTION_SNMP + pf_snmp_show (net); +#else + printf ("No support for SNMP\n\n"); +#endif + } + } + else + { + printf ("p-net not yet initialized.\n"); + } +} + +void pnet_create_log_book_entry ( + pnet_t * net, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status, + uint32_t entry_detail) +{ + pf_fspm_create_log_book_entry (net, arep, p_pnio_status, entry_detail); +} + +int pnet_input_set_data_and_iops ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + const uint8_t * p_data, + uint16_t data_len, + uint8_t iops) +{ + uint8_t iops_len = 1; + + return pf_ppm_set_data_and_iops ( + net, + api, + slot, + subslot, + p_data, + data_len, + &iops, + iops_len); +} + +int pnet_input_get_iocs ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint8_t * p_iocs) +{ + uint8_t iocs_len = 1; + + return pf_cpm_get_iocs (net, api, slot, subslot, p_iocs, &iocs_len); +} + +int pnet_output_get_data_and_iops ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + bool * p_new_flag, + uint8_t * p_data, + uint16_t * p_data_len, + uint8_t * p_iops) +{ + uint8_t iops_len = 1; + + return pf_cpm_get_data_and_iops ( + net, + api, + slot, + subslot, + p_new_flag, + p_data, + p_data_len, + p_iops, + &iops_len); +} + +int pnet_output_set_iocs ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint8_t iocs) +{ + uint8_t iocs_len = 1; + + return pf_ppm_set_iocs (net, api, slot, subslot, &iocs, iocs_len); +} + +int pnet_plug_module ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint32_t module_ident) +{ + return pf_cmdev_plug_module (net, api, slot, module_ident); +} + +int pnet_plug_submodule ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_ident, + uint32_t submodule_ident, + pnet_submodule_dir_t direction, + uint16_t length_input, + uint16_t length_output) +{ + return pf_cmdev_plug_submodule ( + net, + api, + slot, + subslot, + module_ident, + submodule_ident, + direction, + length_input, + length_output, + false); +} + +int pnet_pull_module (pnet_t * net, uint32_t api, uint16_t slot) +{ + return pf_cmdev_pull_module (net, api, slot); +} + +int pnet_pull_submodule ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot) +{ + return pf_cmdev_pull_submodule (net, api, slot, subslot); +} + +int pnet_set_primary_state (pnet_t * net, bool primary) +{ + int ret = 0; /* Assume all goes well */ + uint16_t ar_ix; + uint16_t cr_ix; + pf_ar_t * p_ar = NULL; + + LOG_DEBUG ( + PNET_LOG, + "API(%d): Application sets primary state to %s\n", + __LINE__, + primary ? "true" : "false"); + + for (ar_ix = 0; ar_ix < PNET_MAX_AR; ar_ix++) + { + p_ar = pf_ar_find_by_index (net, ar_ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + for (cr_ix = 0; cr_ix < p_ar->nbr_iocrs; cr_ix++) + { + if (pf_ppm_set_data_status_state (net, p_ar, cr_ix, primary) != 0) + { + ret = -1; + } + } + } + } + + return ret; +} + +int pnet_set_redundancy_state (pnet_t * net, bool redundant) +{ + int ret = 0; /* Assume all goes well */ + uint16_t ar_ix; + uint16_t cr_ix; + pf_ar_t * p_ar = NULL; + + LOG_DEBUG ( + PNET_LOG, + "API(%d): Application sets redundancy state to %s\n", + __LINE__, + redundant ? "true" : "false"); + + for (ar_ix = 0; ar_ix < PNET_MAX_AR; ar_ix++) + { + p_ar = pf_ar_find_by_index (net, ar_ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + for (cr_ix = 0; cr_ix < p_ar->nbr_iocrs; cr_ix++) + { + if (pf_ppm_set_data_status_redundancy (net, p_ar, cr_ix, redundant) != 0) + { + ret = -1; + } + } + } + } + + return ret; +} + +int pnet_set_provider_state (pnet_t * net, bool run) +{ + int ret = 0; /* Assume all goes well */ + uint16_t ar_ix; + uint16_t cr_ix; + pf_ar_t * p_ar = NULL; + + LOG_DEBUG ( + PNET_LOG, + "API(%d): Application sets provider state to %d\n", + __LINE__, + run); + + for (ar_ix = 0; ar_ix < PNET_MAX_AR; ar_ix++) + { + p_ar = pf_ar_find_by_index (net, ar_ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + for (cr_ix = 0; cr_ix < p_ar->nbr_iocrs; cr_ix++) + { + if (pf_ppm_set_data_status_provider (net, p_ar, cr_ix, run) != 0) + { + ret = -1; + } + } + } + } + + return ret; +} + +int pnet_application_ready (pnet_t * net, uint32_t arep) +{ + int ret = -1; + pf_ar_t * p_ar = NULL; + + LOG_DEBUG ( + PNET_LOG, + "API(%d): Application calls application ready for AREP %" PRIu32 "\n", + __LINE__, + arep); + + if (pf_ar_find_by_arep (net, arep, &p_ar) == 0) + { + ret = pf_cmdev_cm_ccontrol_req (net, p_ar); + } + + return ret; +} + +int pnet_ar_abort (pnet_t * net, uint32_t arep) +{ + int ret = -1; + pf_ar_t * p_ar = NULL; + + LOG_DEBUG ( + PNET_LOG, + "API(%d): Application calls AR abort for AREP %" PRIu32 "\n", + __LINE__, + arep); + + if (pf_ar_find_by_arep (net, arep, &p_ar) == 0) + { + ret = pf_cmdev_cm_abort (net, p_ar); + } + + return ret; +} + +int pnet_factory_reset (pnet_t * net) +{ + uint16_t ix; + pf_ar_t * p_ar = NULL; + + LOG_DEBUG (PNET_LOG, "API(%d): Application calls factory reset.\n", __LINE__); + + /* Look for active connections */ + for (ix = 0; ix < PNET_MAX_AR; ix++) + { + p_ar = pf_ar_find_by_index (net, ix); + if ((p_ar != NULL) && (p_ar->in_use == true)) + { + (void)pf_cmdev_cm_abort (net, p_ar); + } + } + + (void)pf_cmina_set_default_cfg (net, 99); + pf_cmina_dcp_set_commit (net); + + return 0; +} + +int pnet_remove_data_files (const char * file_directory) +{ + LOG_DEBUG ( + PNET_LOG, + "API(%d): Application removes data files in %s\n", + __LINE__, + file_directory); + + return pf_cmina_remove_all_data_files (file_directory); +} + +int pnet_get_ar_error_codes ( + pnet_t * net, + uint32_t arep, + uint16_t * p_err_cls, + uint16_t * p_err_code) +{ + int ret = -1; + pf_ar_t * p_ar = NULL; + + if (pf_ar_find_by_arep (net, arep, &p_ar) == 0) + { + *p_err_cls = p_ar->err_cls; + *p_err_code = p_ar->err_code; + + ret = 0; + } + + return ret; +} + +int pnet_alarm_send_process_alarm ( + pnet_t * net, + uint32_t arep, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t payload_usi, + uint16_t payload_len, + const uint8_t * p_payload) +{ + int ret = -1; + pf_ar_t * p_ar = NULL; + + if (pf_ar_find_by_arep (net, arep, &p_ar) == 0) + { + ret = pf_alarm_send_process ( + net, + p_ar, + api, + slot, + subslot, + payload_usi, + payload_len, + p_payload); + } + + return ret; +} + +int pnet_alarm_send_ack ( + pnet_t * net, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_argument, + const pnet_pnio_status_t * p_pnio_status) +{ + int ret = -1; + pf_ar_t * p_ar = NULL; + + LOG_DEBUG ( + PNET_LOG, + "API(%d): Application sends alarm ack for slot %u subslot 0x%04x.\n", + __LINE__, + p_alarm_argument->slot_nbr, + p_alarm_argument->subslot_nbr); + + if (pf_ar_find_by_arep (net, arep, &p_ar) == 0) + { + ret = + pf_alarm_alpmr_alarm_ack (net, p_ar, p_alarm_argument, p_pnio_status); + } + + return ret; +} + +/************************** Low-level diagnosis functions ******************/ + +int pnet_diag_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + return pf_diag_add ( + net, + p_diag_source, + ch_bits, + severity, + ch_error_type, + ext_ch_error_type, + ext_ch_add_value, + qual_ch_qualifier, + usi, + manuf_data_len, + p_manuf_data); +} + +int pnet_diag_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + return pf_diag_update ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + ext_ch_add_value, + usi, + manuf_data_len, + p_manuf_data); +} + +int pnet_diag_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint16_t usi) +{ + return pf_diag_remove ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + usi); +} + +/************************** Diagnosis in standard format *******************/ + +int pnet_diag_std_add ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + pnet_diag_ch_prop_type_values_t ch_bits, + pnet_diag_ch_prop_maint_values_t severity, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value, + uint32_t qual_ch_qualifier) +{ + return pf_diag_std_add ( + net, + p_diag_source, + ch_bits, + severity, + ch_error_type, + ext_ch_error_type, + ext_ch_add_value, + qual_ch_qualifier); +} + +int pnet_diag_std_update ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type, + uint32_t ext_ch_add_value) +{ + return pf_diag_std_update ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type, + ext_ch_add_value); +} + +int pnet_diag_std_remove ( + pnet_t * net, + const pnet_diag_source_t * p_diag_source, + uint16_t ch_error_type, + uint16_t ext_ch_error_type) +{ + return pf_diag_std_remove ( + net, + p_diag_source, + ch_error_type, + ext_ch_error_type); +} + +/************************** Diagnosis in USI format ************************/ + +int pnet_diag_usi_add ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + return pf_diag_usi_add ( + net, + api, + slot, + subslot, + usi, + manuf_data_len, + p_manuf_data); +} + +int pnet_diag_usi_update ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t usi, + uint16_t manuf_data_len, + const uint8_t * p_manuf_data) +{ + return pf_diag_usi_update ( + net, + api, + slot, + subslot, + usi, + manuf_data_len, + p_manuf_data); +} + +int pnet_diag_usi_remove ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t usi) +{ + return pf_diag_usi_remove (net, api, slot, subslot, usi); +} diff --git a/profinet_stack/p-net/src/drivers/drivers.cmake b/profinet_stack/p-net/src/drivers/drivers.cmake new file mode 100644 index 0000000..846431f --- /dev/null +++ b/profinet_stack/p-net/src/drivers/drivers.cmake @@ -0,0 +1,17 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2018 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ +if (PNET_OPTION_DRIVER_ENABLE) + include( src/drivers/lan9662/driver.cmake ) +endif() diff --git a/profinet_stack/p-net/src/drivers/lan9662/add_inbound_vcap_rule.sh b/profinet_stack/p-net/src/drivers/lan9662/add_inbound_vcap_rule.sh new file mode 100644 index 0000000..b33febc --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/add_inbound_vcap_rule.sh @@ -0,0 +1,41 @@ +#!/bin/sh +############################################# +# Add vcap rule for inbound data / outgoing cyclic frames +# +# Definitions: +# VLAN_ID :== VLAN ID +# DMAC :== xx:xx:xx:xx:xx:xx +# RT_VLAN_IDX :== 0..7 +# PRIORITY :== 0..0xffff +# VCAM_HANDLE :== 0..0xffffffff +# RT_FRMID :== 0..0xffff +# RTP_ID :== 0..0x1f +# RTP_SUBID :== 0..1 +# RTE_INB_UPD :== 1 for inbound RTE processing, 0 otherwise + +#set -x + +PRIORITY=10 +VCAM_HANDLE=$1 +VLAN_ID=$2 +DMAC=$3 +RT_VLAN_IDX=$4 + +RT_FRMID=$5 +RTP_ID=$6 +RTP_SUBID=0 +RTE_INB_UPD=1 + +COMMAND="vcap add is1 $PRIORITY $VCAM_HANDLE \ +s1_rt first 0 l2_mac $DMAC ff:ff:ff:ff:ff:ff \ +rt_vlan_idx $RT_VLAN_IDX 0x7 rt_frmid $RT_FRMID 0xffff \ +s1_rt rtp_id $RTP_ID rtp_subid $RTP_SUBID \ +rte_inb_upd $RTE_INB_UPD fwd_ena 1 fwd_mask 0x10" + +echo "$COMMAND" +if ! $COMMAND; then + echo "Failed adding vcap rule for inbound data" + exit 1 +fi + +exit 0 diff --git a/profinet_stack/p-net/src/drivers/lan9662/add_mera_lib.cmake b/profinet_stack/p-net/src/drivers/lan9662/add_mera_lib.cmake new file mode 100644 index 0000000..005825a --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/add_mera_lib.cmake @@ -0,0 +1,42 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2020 rt-labs AB, Sweden. +# +# This software is licensed under the terms of the BSD 3-clause +# license. See the file LICENSE distributed with this software for +# full license information. +#*******************************************************************/ + + +cmake_minimum_required(VERSION 3.14) + +# Attempt to find externally built mera library +find_package(mera QUIET) + +if (NOT mera_FOUND) + # Download and build mera locally as a static library + # Todo: this is a private repo. Switch to public repo before release + message(STATUS "Fetch mera from github") + include(FetchContent) + FetchContent_Declare( + mera + GIT_REPOSITORY https://github.com/microchip-ung/rtlabs-mera + GIT_TAG b9d43d5 + ) + + FetchContent_GetProperties(mera) + if(NOT mera_POPULATED) + FetchContent_Populate(mera) + set(BUILD_SHARED_LIBS_OLD ${BUILD_SHARED_LIBS}) + set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "" FORCE) + add_subdirectory(${mera_SOURCE_DIR} ${mera_BINARY_DIR} EXCLUDE_FROM_ALL) + set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_OLD} CACHE BOOL "" FORCE) + endif() + +endif() diff --git a/profinet_stack/p-net/src/drivers/lan9662/add_outbound_vcap_rule.sh b/profinet_stack/p-net/src/drivers/lan9662/add_outbound_vcap_rule.sh new file mode 100644 index 0000000..4d2ee82 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/add_outbound_vcap_rule.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +############################################ +# Add vcap rule for outbound data / incoming cyclic frames +# +# Definitions: +# VLAN_ID :== VLAN ID +# RT_VLAN_IDX :== 0..7 +# PRIORITY :== 0..0xffff +# VCAM_HANDLE :== 0..0xffffffff +# RT_FRMID :== 0..0xffff +# RTP_ID :== 0..0x1f +# RTP_SUBID :== 0..1 +# RTE_INB_UPD :== 1 for inbound RTE processing, 0 otherwise + +# set -x + +PRIORITY=10 +VCAM_HANDLE=$1 +VLAN_ID=$2 +RT_VLAN_IDX=$3 + +RT_FRMID=$4 +RTP_ID=$5 +DMAC=$6 + +# Enable untagged RTP processing +symreg ana_rt_vlan_pcp[$RT_VLAN_IDX].pcp_mask 0xff # Wildcard on pcp value +symreg ana_rt_vlan_pcp[$RT_VLAN_IDX].vlan_id $VLAN_ID # Actual vid +symreg ana_rt_vlan_pcp[$RT_VLAN_IDX].vlan_pcp_ena 1 # Enable entry + +COMMAND="vcap add is1 $PRIORITY $VCAM_HANDLE \ +s1_rt first 0 \ +rt_vlan_idx $RT_VLAN_IDX 0x7 \ +l2_mac $DMAC ff:ff:ff:ff:ff:ff \ +rt_type 1 0x3 \ +rt_frmid $RT_FRMID 0xffff \ +s1_rt rtp_id $RTP_ID \ +fwd_ena 1 fwd_mask 0x10" + +echo "$COMMAND" +if ! $COMMAND; then + echo "Failed adding vcam rule for inbound data" + exit 1 +fi + +exit 0 diff --git a/profinet_stack/p-net/src/drivers/lan9662/del_vcap_rule.sh b/profinet_stack/p-net/src/drivers/lan9662/del_vcap_rule.sh new file mode 100644 index 0000000..a972d61 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/del_vcap_rule.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +############################################ +# Delete vcap rule +# + +#set -x + +PRIORITY=10 +VCAM_HANDLE=$1 + +COMMAND="vcap del is1 $PRIORITY $VCAM_HANDLE" + +echo "$COMMAND" +if ! $COMMAND; then + echo "Failed to delete vcap rule" + exit 1 +fi + +exit 0 diff --git a/profinet_stack/p-net/src/drivers/lan9662/driver.cmake b/profinet_stack/p-net/src/drivers/lan9662/driver.cmake new file mode 100644 index 0000000..6d5b6ed --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/driver.cmake @@ -0,0 +1,154 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2018 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +cmake_dependent_option ( + PNET_OPTION_DRIVER_LAN9662 + "Enable LAN9662 HW Offload driver" OFF + "PNET_OPTION_DRIVER_ENABLE" OFF ) + +if (PNET_OPTION_DRIVER_LAN9662) + +option(PNET_OPTION_LAN9662_SHOW_RTE_INFO "Show RTE config log message" OFF) +set(PNET_LAN9662_MAX_FRAMES 2 CACHE STRING "Max active CPM and PPM instances") +set(PNET_LAN9662_MAX_IDS 16 CACHE STRING "Max subslots for a frame") + +set(PNET_LAN9662_VCAM_BASE 2 CACHE STRING "LAN9662 VCAP base index") +set(PNET_LAN9662_RTP_BASE 4 CACHE STRING "LAN9662 RTP base index") +set(PNET_LAN9662_WAL_BASE 6 CACHE STRING "LAN9662 write action list base index") +set(PNET_LAN9662_RAL_BASE 8 CACHE STRING "LAN9662 read action list base index") + +message(STATUS "LAN9662 pnet_driver_options.h configuration") +configure_file ( + src/drivers/lan9662/pnet_driver_options.h.in + include/pnet_driver_options.h +) + +message(STATUS "Add LAN9662 targets and configurations") + +if(TARGET mera) + message(STATUS "LAN9662 mera lib found") + get_target_property(MERA_INCLUDES mera INCLUDE_DIRECTORIES) +else() + message(STATUS "LAN9662 mera lib not found - fetch repo") + include(src/drivers/lan9662/add_mera_lib.cmake) + get_target_property(MERA_INCLUDES mera INCLUDE_DIRECTORIES) +endif() + +add_executable(pn_lan9662 "") +add_executable(pn_shm_tool "") + +set_target_properties (pn_lan9662 pn_shm_tool + PROPERTIES + C_STANDARD 99 + ) + +add_subdirectory (samples/pn_dev_lan9662) +add_subdirectory (samples/pn_shm_tool) + +target_sources(profinet + PRIVATE + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662/src/pf_cpm_driver_lan9662.c + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662/src/pf_lan9662_mera.c + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662/src/pf_mera_trace.c + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662/src/pf_mera_trace.c + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662/src/pf_rte_uio.c + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662/src/pf_sram_uio.c + ${PROFINET_SOURCE_DIR}/src/drivers/lan9662/src/pf_ppm_driver_lan9662.c + ) + +target_include_directories(profinet + PRIVATE + src/drivers/lan9662 + src/drivers/lan9662/include + ${MERA_INCLUDES} + ) + +target_link_libraries(profinet + PUBLIC + mera + ) + +install ( + TARGETS mera + EXPORT ProfinetConfig + DESTINATION lib + ) + +target_include_directories(pn_dev + PRIVATE + src/drivers/lan9662 + src/drivers/lan9662/include + ) + +target_include_directories(pn_lan9662 + PRIVATE + samples/pn_dev_lan9662 + src/drivers/lan9662 + src/drivers/lan9662/include + src/ports/linux + ) + +target_sources(pn_lan9662 + PRIVATE + samples/pn_dev_lan9662/sampleapp_common.c + samples/pn_dev_lan9662/app_utils.c + samples/pn_dev_lan9662/app_log.c + samples/pn_dev_lan9662/app_gsdml.c + samples/pn_dev_lan9662/app_data.c + samples/pn_dev_lan9662/app_shm.c + src/ports/linux/sampleapp_main.c + ) + +target_compile_options(pn_lan9662 + PRIVATE + ${APP_COMPILER_FLAGS} + ) + +target_link_options(pn_lan9662 + PRIVATE + -Wl,--gc-sections + ) + +target_sources(pn_shm_tool + PRIVATE + samples/pn_shm_tool/pn_shm_tool.c + ) + +target_compile_options(pn_shm_tool + PRIVATE + ${APP_COMPILER_FLAGS} + ) + +target_link_options(pn_shm_tool + PRIVATE + -Wl,--gc-sections + ) + +target_link_libraries (pn_shm_tool PUBLIC rt pthread) + +file(COPY + src/drivers/lan9662/include/driver_config.h + src/drivers/lan9662/include/pnet_lan9662_api.h + DESTINATION include + ) + +file(COPY + src/ports/linux/set_network_parameters + src/drivers/lan9662/set_profinet_leds + DESTINATION + ${PROFINET_BINARY_DIR}/ + ) + +endif() \ No newline at end of file diff --git a/profinet_stack/p-net/src/drivers/lan9662/include/driver_config.h b/profinet_stack/p-net/src/drivers/lan9662/include/driver_config.h new file mode 100644 index 0000000..1c8200a --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/include/driver_config.h @@ -0,0 +1,49 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2022 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief LAN9662-specific configuration + * + * This file contains definitions of configuration settings for the + * LAN9662 driver. + */ + +#ifndef DRIVER_CONFIG_H +#define DRIVER_CONFIG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pnet_driver_options.h" + +typedef struct pnet_driver_config +{ + struct + { + uint16_t vcam_base_id; + uint16_t rtp_base_id; + uint16_t wal_base_id; + uint16_t ral_base_id; + } mera; + +} pnet_driver_config_t; + +#ifdef __cplusplus +} +#endif + +#endif /* DRIVER_CONFIG_H */ \ No newline at end of file diff --git a/profinet_stack/p-net/src/drivers/lan9662/include/pnet_lan9662_api.h b/profinet_stack/p-net/src/drivers/lan9662/include/pnet_lan9662_api.h new file mode 100644 index 0000000..09a582b --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/include/pnet_lan9662_api.h @@ -0,0 +1,89 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_LAN9662_API_H +#define PF_LAN9662_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef enum +{ + PNET_MERA_DATA_TYPE_SRAM, + PNET_MERA_DATA_TYPE_QSPI +} pnet_mera_rte_data_type_t; + +/** + * Submodule RTE data configuration. + * Used for both inputs and outputs configuration. + * The default value is used if the IOXS is bad. + */ +typedef struct pnet_mera_rte_data_cfg +{ + pnet_mera_rte_data_type_t type; + uint16_t address; + const uint8_t * default_data; +} pnet_mera_rte_data_cfg_t; + +/** + * Configure RTE input for a single subslot. + * This operation is used to configure a QSPI data source. + * Called during the AR establishment. + * + * @param net InOut: The p-net stack instance + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param rte_data_cfg In: RTE configuration + * @return 0 if the RTE is successfully configured. + * -1 if an error occurred. For example if HW resources are not + * available. + */ +PNET_EXPORT int pnet_mera_input_set_rte_config ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + const pnet_mera_rte_data_cfg_t * rte_data_cfg); + +/** + * Configure RTE output for a single subslot. + * This operation is used to configure a QSPI data sink. + * Called during the AR establishment. + * + * @param net InOut: The p-net stack instance + * @param api In: The API. + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param rte_data_cfg In: RTE configuration + * @return 0 if the RTE is successfully configured. + * -1 if an error occurred. For example if HW resources are not + * available. + */ +PNET_EXPORT int pnet_mera_output_set_rte_config ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + const pnet_mera_rte_data_cfg_t * rte_data_cfg); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_LAN9662_API_H */ diff --git a/profinet_stack/p-net/src/drivers/lan9662/pnet_driver_options.h.in b/profinet_stack/p-net/src/drivers/lan9662/pnet_driver_options.h.in new file mode 100644 index 0000000..5890b53 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/pnet_driver_options.h.in @@ -0,0 +1,52 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2022 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PNET_DRIVER_OPTIONS_H +#define PNET_DRIVER_OPTIONS_H + + +#if !defined (PNET_OPTION_DRIVER_LAN9662) +#cmakedefine01 PNET_OPTION_DRIVER_LAN9662 +#endif + +#if !defined (PNET_OPTION_LAN9662_SHOW_RTE_INFO) +#cmakedefine01 PNET_OPTION_LAN9662_SHOW_RTE_INFO +#endif + +#if !defined (PNET_LAN9662_MAX_FRAMES) +#define PNET_LAN9662_MAX_FRAMES @PNET_LAN9662_MAX_FRAMES@ +#endif + +#if !defined (PNET_LAN9662_MAX_IDS) +#define PNET_LAN9662_MAX_IDS @PNET_LAN9662_MAX_IDS@ +#endif + +#if !defined (PNET_LAN9662_VCAM_BASE) +#define PNET_LAN9662_VCAM_BASE @PNET_LAN9662_VCAM_BASE@ +#endif + +#if !defined (PNET_LAN9662_RTP_BASE) +#define PNET_LAN9662_RTP_BASE @PNET_LAN9662_RTP_BASE@ +#endif + +#if !defined (PNET_LAN9662_WAL_BASE) +#define PNET_LAN9662_WAL_BASE @PNET_LAN9662_WAL_BASE@ +#endif + +#if !defined (PNET_LAN9662_RAL_BASE) +#define PNET_LAN9662_RAL_BASE @PNET_LAN9662_RAL_BASE@ +#endif + +#endif /* PNET_DRIVER_OPTIONS_H */ diff --git a/profinet_stack/p-net/src/drivers/lan9662/set_profinet_leds b/profinet_stack/p-net/src/drivers/lan9662/set_profinet_leds new file mode 100644 index 0000000..068b8b5 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/set_profinet_leds @@ -0,0 +1,37 @@ +#!/bin/sh + +############################################# +# Script for setting Profinet LEDs on Linux # +############################################# +# +# This writes the output to files instead of real LEDs. +# It can be useful when running on for example Ubuntu or some other +# desktop Linux variant. +# +# Modify this script to fit your setup, by for example changing output path. + +if [ $# -ne 2 ]; then + echo "Usage: ${0} led_number led_state" + echo " where:" + echo " led_number: LED number. Number 1 for sample app data LED" + echo " Number 2 for mandatory Profinet signal LED" + echo " led_state: 1 for on, 0 for off" + echo " Exit code: 0 on success, 1 on error" + exit 1 +fi + +LED_NUMBER=$1 +LED_STATE=$2 + +# Uncomment next line for debugging +#echo "Script for setting Profinet LED state in dummy file. LED number ${LED_NUMBER}, new state ${LED_STATE}" + +if ! [ ${LED_STATE} == "0" -o ${LED_STATE} == "1" ]; then + echo "Wrong LED state: ${LED_STATE}" + exit 1 +fi + +# Todo - map to GPIOs +echo "LED ${LED_NUMBER} new state ${LED_STATE}" + +exit 0 diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_cpm_driver_lan9662.c b/profinet_stack/p-net/src/drivers/lan9662/src/pf_cpm_driver_lan9662.c new file mode 100644 index 0000000..8dfbe89 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_cpm_driver_lan9662.c @@ -0,0 +1,473 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief CPM driver for LAN9662 + * + */ + +#include "pf_includes.h" +#include "pf_lan9662_mera.h" + +#include +#include + +/** + * @internal + * Control interval timeout handler + * This is a callback for the scheduler. + * + * @param net InOut: The p-net stack instance + * @param arg In: The IOCR instance. pf_iocr_t + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +static void pf_cpm_drv_lan9662_ci_timeout ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_iocr_t * p_iocr = (pf_iocr_t *)arg; + uint32_t start = os_get_current_time_us(); + uint32_t exec; + uint64_t rx_count = 0; + uint8_t data_status = 0; + uint8_t changes = 0; + pf_mera_event_t mera_event = {0}; + bool reset_dht = false; + + pf_scheduler_reset_handle (&p_iocr->cpm.ci_timeout); + + if (p_iocr->cpm.ci_running == true) /* Timer running */ + { + if (pf_mera_cpm_read_rx_count (p_iocr->cpm.frame, &rx_count) != 0) + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Failed to read frame rx counter\n", + __LINE__); + } + p_iocr->cpm.recv_cnt += rx_count; + + if (pf_mera_cpm_read_data_status (p_iocr->cpm.frame, &data_status) != 0) + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Failed to read data status\n", + __LINE__); + } + + changes = p_iocr->cpm.data_status ^ data_status; + p_iocr->cpm.data_status = data_status; + if (changes != 0) + { + LOG_INFO ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Data Status changed. New value 0x%x\n", + __LINE__, + data_status); + } + + switch (p_iocr->cpm.state) + { + case PF_CPM_STATE_W_START: + case PF_CPM_STATE_FRUN: + if ( + (rx_count > 0) && + (data_status & BIT (PNET_DATA_STATUS_BIT_PROVIDER_STATE))) + { + LOG_DEBUG ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Data received and status ok\n", + __LINE__); + + pf_cpm_state_ind (net, p_iocr->p_ar, p_iocr->crep, true); + pf_cpm_set_state (&p_iocr->cpm, PF_CPM_STATE_RUN); + + /* Reconfigure RTE to activate DHT */ + pf_mera_cpm_activate_rte_dht (p_iocr->cpm.frame); + } + + /* Always reset DHT while in the inital states. */ + + reset_dht = true; + break; + + case PF_CPM_STATE_RUN: + + if (changes != 0) + { + pf_fspm_data_status_changed ( + net, + p_iocr->p_ar, + p_iocr, + changes, + data_status); + } + + if (rx_count > 0) + { + (void)pf_cmio_cpm_new_data_ind (p_iocr->p_ar, p_iocr->crep, true); + } + break; + + default: + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Unsupported CPM state\n", + __LINE__); + break; + } + + if (pf_mera_poll_cpm_events (net, p_iocr->cpm.frame, &mera_event) != 0) + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Failed to poll rtp events\n", + __LINE__); + } + + if (rx_count > 0 && !mera_event.data_status_mismatch) + { + reset_dht = true; + } + else + { + if ((BIT (PNET_DATA_STATUS_BIT_PROVIDER_STATE) & data_status) == 0) + { + reset_dht = true; + } + if ( + (BIT (PNET_DATA_STATUS_BIT_STATION_PROBLEM_INDICATOR) & + data_status) == 0) + { + reset_dht = true; + } + } + + if (reset_dht) + { + p_iocr->cpm.dht = 0; + } + else + { + p_iocr->cpm.dht++; + } + + if (p_iocr->cpm.dht >= p_iocr->cpm.data_hold_factor) + { + if (p_iocr->cpm.dht == p_iocr->cpm.data_hold_factor) + { + LOG_WARNING ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Data hold timer (DHT) expired\n", + __LINE__); + + if (!mera_event.stopped) + { + /* Workaround for RTC testcase. */ + LOG_INFO ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Generate rtp stopped event\n", + __LINE__); + + mera_event.stopped = true; + } + } + } + + if (mera_event.stopped) + { + p_iocr->p_ar->err_cls = PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL; + p_iocr->p_ar->err_code = + PNET_ERROR_CODE_2_ABORT_AR_CONSUMER_DHT_EXPIRED; + + p_iocr->cpm.dht = 0; + p_iocr->cpm.ci_running = false; /* Stop timer */ + pf_cpm_state_ind (net, p_iocr->p_ar, p_iocr->crep, false); + + pf_cpm_set_state (&p_iocr->cpm, PF_CPM_STATE_W_START); + } + + if (p_iocr->cpm.ci_running == true) + { + /* Timer auto-reload */ + if ( + pf_scheduler_add ( + net, + p_iocr->cpm.control_interval, + pf_cpm_drv_lan9662_ci_timeout, + arg, + &p_iocr->cpm.ci_timeout) != 0) + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Timeout not started\n", + __LINE__); + p_iocr->p_ar->err_cls = PNET_ERROR_CODE_1_CPM; + p_iocr->p_ar->err_code = PNET_ERROR_CODE_2_CPM_INVALID; + pf_cmsu_cpm_error_ind ( + net, + p_iocr->p_ar, + p_iocr->p_ar->err_cls, + p_iocr->p_ar->err_code); + } + } + } + + exec = os_get_current_time_us() - start; + if (exec > p_iocr->cpm.max_exec) + { + p_iocr->cpm.max_exec = exec; + } +} + +static int pf_cpm_drv_lan9662_create ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep) +{ + pf_cpm_t * p_cpm = &p_ar->iocrs[crep].cpm; + + pf_mera_frame_cfg_t mera_cpm_cfg = {0}; + + LOG_DEBUG ( + PF_PPM_LOG, + "CPM_DRV_MERA(%d): Allocate RTE frame (outbound data / rx frames) for " + "AREP %u " + "CREP " + "%" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + mera_cpm_cfg.p_iocr = &p_ar->iocrs[crep]; + mera_cpm_cfg.port = 0; + + pf_mera_get_port_from_remote_mac ( + &p_ar->ar_param.cm_initiator_mac_add, + &mera_cpm_cfg.port); + + memcpy ( + &mera_cpm_cfg.main_mac_addr, + &net->pf_interface.main_port.mac_address, + sizeof (pnet_ethaddr_t)); + + p_cpm->frame = pf_mera_cpm_alloc (net, &mera_cpm_cfg); + + if (p_cpm->frame == NULL) + { + return -1; + } + + return 0; +} + +/** + * @internal + * Close RTE resources used in CPM frame. + * @param net InOut: The p-net stack instance + * @param arg In: CPM frame + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +static void pf_cpm_drv_lan9662_delayed_close ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_drv_frame_t * frame = (pf_drv_frame_t *)arg; + pf_mera_cpm_stop (net, frame); + pf_mera_cpm_free (net, frame); +} + +static int pf_cpm_drv_lan9662_close_req ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep) +{ + pf_cpm_t * p_cpm = &p_ar->iocrs[crep].cpm; + + p_cpm->ci_running = false; + pf_scheduler_reset_handle (&p_cpm->ci_timeout); + + /* Delay close of pf_mera resources + * Workaround for delayed alarm transmission. + * Close RTE resources after alarm is sent. + */ + pf_scheduler_init_handle (&p_cpm->ci_timeout, "cpm_close"); + pf_scheduler_add ( + net, + 2, + pf_cpm_drv_lan9662_delayed_close, + p_cpm->frame, + &p_cpm->ci_timeout); + + return 0; +} + +static int pf_cpm_drv_lan9662_activate_req ( + pnet_t * net, + pf_ar_t * p_ar, + uint32_t crep) +{ + int ret = -1; + pf_cpm_t * p_cpm; + pf_iocr_t * p_iocr; + + p_iocr = &p_ar->iocrs[crep]; + p_cpm = &p_iocr->cpm; + + LOG_DEBUG ( + PF_PPM_LOG, + "CPM_DRV_MERA(%d): Activate RTE (outbound data / rx frames) for AREP %u " + "CREP " + "%" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + ret = pf_mera_cpm_start (net, p_cpm->frame); + if (ret == 0) + { + pf_scheduler_init_handle (&p_cpm->ci_timeout, "cpm"); + ret = pf_scheduler_add ( + net, + p_cpm->control_interval, + pf_cpm_drv_lan9662_ci_timeout, + p_iocr, + &p_cpm->ci_timeout); + + if (ret == 0) + { + p_cpm->ci_running = true; + } + else + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Timeout not started\n", + __LINE__); + } + } + else + { + LOG_ERROR ( + PF_CPM_LOG, + "CPM_DRV_MERA(%d): Failed to start RTE\n", + __LINE__); + } + + return ret; +} + +/** + * Read data and iops of an iodata object in the + * active RTE frame. + * TODO - behaviour for iodata mapped to QSPI is tbd + */ +static int pf_cpm_drv_lan9662_get_data_and_iops ( + pnet_t * net, + pf_iocr_t * p_iocr, + const pf_iodata_object_t * p_iodata, + bool * p_new_flag, + uint8_t * p_data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len) +{ + int ret = -1; + + ret = pf_mera_cpm_read_data_and_iops ( + net, + p_iocr->cpm.frame, + p_iodata->slot_nbr, + p_iodata->subslot_nbr, + p_new_flag, + p_data, + data_len, + p_iops, + iops_len); + + return ret; +} + +/** + * Read the consumer status for an iodata object + * in the active RTE frame. + * @return 0 on success, -1 on error + */ +static int pf_cpm_drv_lan9662_get_iocs ( + pnet_t * net, + pf_iocr_t * p_iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * p_iocs, + uint8_t iocs_len) +{ + return pf_mera_cpm_read_iocs ( + net, + p_iocr->cpm.frame, + p_iodata->slot_nbr, + p_iodata->subslot_nbr, + p_iocs, + iocs_len); +} + +/** + * Read data_status for active CPM frame. + * @param p_cpm In: CPM instance + * @param p_data_status InOt: Ref to data_status value + * @return 0 on success, -1 on error + */ + +static int pf_cpm_drv_lan9662_get_data_status ( + const pf_cpm_t * p_cpm, + uint8_t * p_data_status) +{ + return pf_mera_cpm_read_data_status (p_cpm->frame, p_data_status); +} + +/** + * Show the current state of the driver + * @param net In: The p-net stack instance + * @param p_ppm In: CPM instance + */ +static void pf_cpm_drv_lan9662_show (const pnet_t * net, const pf_cpm_t * p_cpm) +{ + printf ("Not implemented: pf_cpm_drv_mera_show()\n"); +} + +int pf_driver_cpm_init (pnet_t * net) +{ + static const pf_cpm_driver_t drv = { + .create = pf_cpm_drv_lan9662_create, + .activate_req = pf_cpm_drv_lan9662_activate_req, + .close_req = pf_cpm_drv_lan9662_close_req, + .get_data_and_iops = pf_cpm_drv_lan9662_get_data_and_iops, + .get_iocs = pf_cpm_drv_lan9662_get_iocs, + .get_data_status = pf_cpm_drv_lan9662_get_data_status, + .show = pf_cpm_drv_lan9662_show}; + + net->cpm_drv = &drv; + + LOG_INFO ( + PF_CPM_LOG, + "CPM_DRV(%d): LAN9662 CPM driver installed\n", + __LINE__); + return 0; +} diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.c b/profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.c new file mode 100644 index 0000000..990c49f --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.c @@ -0,0 +1,2836 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "microchip/ethernet/rte/api.h" + +#include "pf_includes.h" +#include "pnet_driver_options.h" +#include "pnet_lan9662_api.h" +#include "pf_lan9662_mera.h" +#include "pf_rte_uio.h" +#include "pf_sram_uio.h" +#include "pf_mera_trace.h" +#include "pnal_filetools.h" + +#include +#include +#include + +#define PF_MERA_GROUP_ID_NONE 0 +#define PF_MERA_GROUP_ID_PPM 1 +#define PF_MERA_GROUP_ID_CPM 2 + +#define PF_MERA_INVALID_ID 0xFFFF +#define PF_MERA_DATA_STATUS_MASK 0x35 +#define PF_MERA_SCRIPT_ARG_LEN 24 +#define PF_MERA_RTE_INFO_PERIOD_IN_US 30000000 +#define PF_MERA_POLL_INTERVAL_IN_US 1000000 +#define PF_MERA_PPM_IODATA_OFFSET 6 /* frame ID + vlan ID */ +#define PF_MERA_CPM_IODATA_OFFSET 2 /* frame ID + vlan ID */ +#define PF_MERA_RAL_TIME_OFFSET 100 /* Time in ns */ +#define PF_MERA_OB_RTP_TIME_OFFSET 300 /* Time in ns */ + +/* Default behaviour is that pf_mera assigns SRAM address for iodata/subslot + * Enabling this define allows the application to set the SRAM address using + * rte configuration functions in P-Net API. + */ +#define PF_MERA_ALLOW_APPLICATION_SRAM 0 + +/* Log enable / disable of features shared between PPM and CPM side */ +#define PF_MERA_LOG (PF_PPM_LOG | PF_CPM_LOG) + +typedef struct pf_mera_rte_cfg +{ + bool valid; + pnet_mera_rte_data_cfg_t cfg; +} pf_mera_rte_cfg_t; + +/** + * RTE mapping of an iodata object. + * Same type is used for both inbound and outbound + * data. + */ +typedef struct pf_rte_object +{ + bool in_use; + pf_iodata_object_t * iodata; + mera_io_intf_t io_interface_type; + uint16_t sram_address; + uint16_t qspi_address; + const uint8_t * default_data; + mera_ob_dg_id_t ob_dg_id; + mera_ib_ra_id_t ib_ra_id; + mera_ib_ral_id_t ib_ral_id; + mera_buf_t ib_ral_buf; +} pf_rte_object_t; + +/** + * RTE configuration for a complete frame. + * The frame can be either a PPM or a CPM frame. + * Includes information of the RTE configuration as + * well as an array with the configuration of each + * iodata object / subslot and their RTE configurations. + */ +typedef struct pf_mera_frame +{ + pf_drv_frame_t drv_frame; + bool used; /* true when allocated */ + bool active; /* true when RTE is started */ + bool is_ppm; /* true if ppm / inbound RTE frame */ + bool is_cpm; /* true if cpm / outbound RTE frame */ + + /* Configuration passed on allocation */ + pf_mera_frame_cfg_t config; + + /* General RTE configuration */ + uint16_t port; + uint32_t interval; /* Frame read/write interval [ns] */ + uint16_t vcam_id; + uint16_t vlan_idx; + mera_rtp_id_t rtp_id; + + uint16_t sram_frame_address; /* Start address of SRAM frame */ + uint16_t rte_data_offset; /* RTE io-data offset. Different for inbound and + outbound configurations */ + + /* RTE configuration for individual iodata objects */ + pf_rte_object_t rte_object[PNET_MAX_SLOTS][PNET_MAX_SUBSLOTS]; + + /* Inbound configuration */ + uint16_t ib_ral_id_count; + uint16_t ib_ra_id_count; + /* Internal transfer QSPI -> SRAM */ + mera_ob_wal_id_t internal_wal_id; + + /* Outbound configuration */ + mera_ob_wal_id_t ob_wal_id; + uint16_t ob_wa_count; + uint16_t ob_dg_count; + + uint64_t rx_count; + + /* A ref to the mera lib should not be part + * of the frame. But it is added to avoid updating + * a call chain including the block writer. + * Todo: Fix by adding net ref in CPM interface function + * get_data_status() + */ + struct mera_inst * mera_lib; + +} pf_mera_frame_t; + +typedef struct pf_mera +{ + pf_drv_t hwo_drv; + + struct mera_inst * mera_lib; /* MERA library ref */ + pf_mera_frame_t frame[PNET_LAN9662_MAX_FRAMES]; + pf_scheduler_handle_t sched_handle; /* Debug info timeout handle */ + pf_scheduler_handle_t poll_handle; /* Mera lib poll timeout handle */ + + /* Submodule RTE configuration set by calls to + * pnet_mera_input_set_rte_config and pnet_mera_out_set_rte_config. + * Should be part of frame, but application has to set configuration + * before cpm frame is created and config is stored until ppm/cpm + * are activated. + */ + pf_mera_rte_cfg_t rte_data_cfg[PNET_MAX_SLOTS][PNET_MAX_SUBSLOTS]; + + /* RTE resource management */ + struct + { + uint16_t base_id; + bool used[PNET_LAN9662_MAX_IDS]; + } vcam_id; + + struct + { + uint16_t base_id; + bool used[PNET_LAN9662_MAX_IDS]; + } vlan_index; + + struct + { + uint16_t base_id; + bool used[PNET_LAN9662_MAX_IDS]; + } rtp; + + struct + { + uint16_t base_id; + bool used[PNET_LAN9662_MAX_IDS]; + } ral; + + struct + { + uint16_t base_id; + bool used[PNET_LAN9662_MAX_IDS]; + } wal; + +} pf_mera_t; + +/** + * Get string representation of RTE interface type + * @param t In: RTE interface type + * @return String representation + */ +static const char * pf_mera_rte_data_type_to_str (pnet_mera_rte_data_type_t t) +{ + switch (t) + { + case PNET_MERA_DATA_TYPE_SRAM: + return "SRAM"; + case PNET_MERA_DATA_TYPE_QSPI: + return "QSPI"; + default: + return "UNSUPPORTED"; + } +} + +static const char * pf_mera_interface_type_to_str (mera_io_intf_t if_type) +{ + switch (if_type) + { + case MERA_IO_INTF_QSPI: + return "QSPI"; + case MERA_IO_INTF_PI: + return "PI"; + case MERA_IO_INTF_SRAM: + return "SRAM"; + case MERA_IO_INTF_PCIE: + return "PCIE"; + default: + return "UNKNOWN"; + } +} + +/** + * Get the subslot index + * The subslots numbers starts at 1 and DAP subslots for + * identity and port information at 0x8000. This + * function converts the subslot number to corresponding + * index in the management data structures. + * + * subslot number id + * slot 0 + * subslot 1 0 + * subslot 0x8000 1 + * subslot 0x8001 2 + * subslot 0x8002 3 + * slot 1 + * subslot 1 0 + * subslot 2 1 + * @param subslot In: (Real) Subslot number + * @return Subslot index + */ +static uint16_t get_subslot_index (uint16_t subslot) +{ + uint16_t subslot_index; + if (subslot < PNET_SUBSLOT_DAP_INTERFACE_1_IDENT) + { + subslot_index = subslot - 1; + } + else + { + subslot_index = subslot - PNET_SUBSLOT_DAP_INTERFACE_1_IDENT + 1; + } + + CC_ASSERT (subslot_index < PNET_MAX_SUBSLOTS); + + return subslot_index; +} + +pf_rte_object_t * get_rte_object ( + pf_mera_frame_t * frame, + uint16_t slot, + uint16_t subslot) +{ + uint16_t subslot_index = get_subslot_index (subslot); + + CC_ASSERT (frame != NULL); + CC_ASSERT (subslot < PNET_MAX_SLOTS); + + return &frame->rte_object[slot][subslot_index]; +} + +/** + * Callback for mera library debug info + * Prints debug information using vprintf() + * @param fmt In: Format string + * @param ... In: argumet list + * @return Always returns 0 + */ +static int pf_mera_printf_cb (const char * fmt, ...) +{ + va_list args; + va_start (args, fmt); + vprintf (fmt, args); + va_end (args); + return 0; +} + +#if PNET_OPTION_LAN9662_SHOW_RTE_INFO +/** + * Display RTE status using printf + * @param mera In: mera lib reference + */ +static void pf_mera_rte_show (struct mera_inst * mera) +{ + mera_debug_info_t info; + + info.group = MERA_DEBUG_GROUP_ALL; + info.rtp_id = 0; /* All RTP IDs */ + info.full = false; /* Only active IDs */ + info.clear = true; + + printf ("RTE Status:\n"); + mera_debug_info_print (mera, pf_mera_printf_cb, &info); +} +#endif + +/** + * @internal + * Show current RTE configuration + * This is a callback for the scheduler. + * @param net InOut: The p-net stack instance + * @param arg In: Handle to the LAN9662 driver. + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +#if PNET_OPTION_LAN9662_SHOW_RTE_INFO +static void pf_mera_rte_show_periodic ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + + pf_mera_t * handle = (pf_mera_t *)arg; + pf_mera_rte_show (handle->mera_lib); + + pf_scheduler_add ( + net, + PF_MERA_RTE_INFO_PERIOD_IN_US, + pf_mera_rte_show_periodic, + handle, + &handle->sched_handle); +} +#endif + +/** + * @internal + * Poll mera lib / RTE + * This is a callback for the scheduler. + * @param net InOut: The p-net stack instance + * @param arg In: Handle to the LAN9662 driver. + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +static void pf_mera_poll (pnet_t * net, void * arg, uint32_t current_time) +{ + pf_mera_t * handle = (pf_mera_t *)arg; + + mera_poll (handle->mera_lib); + + pf_scheduler_add ( + net, + PF_MERA_POLL_INTERVAL_IN_US, + pf_mera_poll, + handle, + &handle->poll_handle); +} + +/** + * Add vcam rule for an inbound RTE connection + * Calls the add_inbound_vcap_rule.sh script + * + * @param vcam_handle In: VCAM handle + * @param vlan_id In: VLAN ID + * @param mac_addr In: MAC of management port + * @param rt_vlan_idx In: RTE VLAN index + * @param frame_id In: Profinet frame ID + * @param rtp_id In: RTP ID + * @return 0 on success, -1 on error + */ +static int pf_mera_ib_add_vcam_rule ( + int vcam_handle, + uint16_t vlan_id, + pnet_ethaddr_t * mac_addr, + int rt_vlan_idx, + int frame_id, + int rtp_id) +{ + const char * argv[8]; + char vlan_id_str[PF_MERA_SCRIPT_ARG_LEN]; + char mac_addr_str[PF_MERA_SCRIPT_ARG_LEN]; + char rt_vlan_idx_str[PF_MERA_SCRIPT_ARG_LEN]; + char vcam_handle_str[PF_MERA_SCRIPT_ARG_LEN]; + char frame_id_str[PF_MERA_SCRIPT_ARG_LEN]; + char rtp_id_str[PF_MERA_SCRIPT_ARG_LEN]; + + snprintf (vlan_id_str, sizeof (vlan_id_str), "0x%x", vlan_id); + snprintf ( + mac_addr_str, + sizeof (mac_addr_str), + "%02X:%02X:%02X:%02X:%02X:%02X", + mac_addr->addr[0], + mac_addr->addr[1], + mac_addr->addr[2], + mac_addr->addr[3], + mac_addr->addr[4], + mac_addr->addr[5]); + + snprintf (rt_vlan_idx_str, sizeof (rt_vlan_idx_str), "%d", rt_vlan_idx); + snprintf (vcam_handle_str, sizeof (vcam_handle_str), "%d", vcam_handle); + snprintf (frame_id_str, sizeof (frame_id_str), "%d", frame_id); + snprintf (rtp_id_str, sizeof (rtp_id_str), "%d", rtp_id); + + argv[0] = "add_inbound_vcap_rule.sh"; + argv[1] = vcam_handle_str; + argv[2] = vlan_id_str; + argv[3] = mac_addr_str; + argv[4] = rt_vlan_idx_str; + argv[5] = frame_id_str; + argv[6] = rtp_id_str; + argv[7] = NULL; + + return pnal_execute_script (argv); +} + +/** + * Add vcam rule for an outbound RTE connection + * Calls the add_outbound_vcap_rule.sh script + * + * @param vcam_handle In: VCAM handle + * @param vlan_id In: VLAN ID + * @param mac_addr In: MAC of management port + * @param rt_vlan_idx In: RTE VLAN index + * @param frame_id In: Profinet frame ID + * @param rtp_id In: RTP ID + * @return 0 on success, -1 on error + */ +int ob_add_vcam_rule ( + int vcam_handle, + uint16_t vlan_id, + pnet_ethaddr_t * mac_addr, + int rt_vlan_idx, + int frame_id, + int rtp_id) +{ + const char * argv[7]; + char vlan_id_str[PF_MERA_SCRIPT_ARG_LEN]; + char mac_addr_str[PF_MERA_SCRIPT_ARG_LEN]; + char rt_vlan_idx_str[PF_MERA_SCRIPT_ARG_LEN]; + char vcam_handle_str[PF_MERA_SCRIPT_ARG_LEN]; + char frame_id_str[PF_MERA_SCRIPT_ARG_LEN]; + char rtp_id_str[PF_MERA_SCRIPT_ARG_LEN]; + + snprintf (vlan_id_str, sizeof (vlan_id_str), "0x%x", vlan_id); + snprintf ( + mac_addr_str, + sizeof (mac_addr_str), + "%02X:%02X:%02X:%02X:%02X:%02X", + mac_addr->addr[0], + mac_addr->addr[1], + mac_addr->addr[2], + mac_addr->addr[3], + mac_addr->addr[4], + mac_addr->addr[5]); + snprintf (rt_vlan_idx_str, sizeof (rt_vlan_idx_str), "%d", rt_vlan_idx); + snprintf (vcam_handle_str, sizeof (vcam_handle_str), "%d", vcam_handle); + snprintf (frame_id_str, sizeof (frame_id_str), "%d", frame_id); + snprintf (rtp_id_str, sizeof (rtp_id_str), "%d", rtp_id); + + argv[0] = "add_outbound_vcap_rule.sh"; + argv[1] = vcam_handle_str; + argv[2] = vlan_id_str; + argv[3] = rt_vlan_idx_str; + argv[4] = frame_id_str; + argv[5] = rtp_id_str; + argv[6] = mac_addr_str; + argv[7] = NULL; + + return pnal_execute_script (argv); +} + +/** + * Delete vcam rule + * Calls the delete_vcap_rule.sh script + * @param vcam_handle In: VCAM handle + * @param vlan_id In: VLAN ID + * @param rt_vlan_idx In: RTE VLAN index + * @param frame_id In: Profinet frame ID + * @param rtp_id In: RTP ID + * @return 0 on success, -1 on error + */ +static int pf_mera_delete_vcam_rule (int vcam_handle) +{ + const char * argv[3]; + char vcam_handle_str[16]; + + snprintf (vcam_handle_str, sizeof (vcam_handle_str), "%d", vcam_handle); + + argv[0] = "del_vcap_rule.sh"; + argv[1] = vcam_handle_str; + argv[2] = NULL; + + return pnal_execute_script (argv); +} + +/** + * Allocate a vcam ID for a RTE configuration + * @param handle InOut: Ref to mera instance + * @return vcam ID, PF_MERA_INVALID_ID on error + */ +static uint16_t pf_mera_vcam_id_alloc (pf_mera_t * handle) +{ + uint16_t i; + CC_ASSERT (handle != NULL); + for (i = 0; i < NELEMENTS (handle->vcam_id.used); i++) + { + if (handle->vcam_id.used[i] == false) + { + handle->vcam_id.used[i] = true; + return handle->vcam_id.base_id + i; + } + } + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to alloc VCAM ID \n", __LINE__); + return PF_MERA_INVALID_ID; +} + +/** + * Free a vcam ID + * @param handle InOut: Ref to mera instance + * @param id In: vcam ID + */ +static void pf_mera_vcam_id_free (pf_mera_t * handle, uint16_t id) +{ + CC_ASSERT (handle != NULL); + if (id != PF_MERA_INVALID_ID) + { + handle->vcam_id.used[id - handle->vcam_id.base_id] = false; + } +} + +/** + * Allocate a vlan index for a RTE configuration + * @param handle InOut: Ref to mera instance + * @return vlan index, PF_MERA_INVALID_ID on error + */ +static uint16_t pf_mera_vlan_index_alloc (pf_mera_t * handle) +{ + uint16_t i; + CC_ASSERT (handle != NULL); + for (i = 0; i < NELEMENTS (handle->vlan_index.used); i++) + { + if (handle->vlan_index.used[i] == false) + { + handle->vlan_index.used[i] = true; + return handle->vlan_index.base_id + i; + } + } + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to alloc vlan index\n", __LINE__); + return PF_MERA_INVALID_ID; +} + +/** + * Free a vlan index + * @param handle InOut: Ref to mera instance + * @param id In: vlan index + */ +static void pf_mera_vlan_index_free (pf_mera_t * handle, uint16_t id) +{ + CC_ASSERT (handle != NULL); + if (id != PF_MERA_INVALID_ID) + { + handle->vlan_index.used[id - handle->vlan_index.base_id] = false; + } +} + +/** + * Allocate a RTE ID + * @param handle InOut: Ref to mera instance + * @return RTP ID, PF_MERA_INVALID_ID on error + */ +static uint16_t pf_mera_rtp_id_alloc (pf_mera_t * handle) +{ + uint16_t i; + CC_ASSERT (handle != NULL); + + for (i = 0; i < NELEMENTS (handle->rtp.used); i++) + { + if (handle->rtp.used[i] == false) + { + handle->rtp.used[i] = true; + return handle->rtp.base_id + i; + } + } + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to allocate RTP ID\n", __LINE__); + return PF_MERA_INVALID_ID; +} + +/** + * Free a RTP ID + * @param handle InOut: Ref to mera instance + * @param id In: RTP ID + */ +static void pf_mera_rtp_id_free (pf_mera_t * handle, uint16_t id) +{ + CC_ASSERT (handle != NULL); + if (id != PF_MERA_INVALID_ID) + { + handle->rtp.used[id - handle->rtp.base_id] = false; + } +} + +/** + * Allocate a Read Action List (RAL) ID + * @param handle InOut: Ref to mera instance + * @return ral ID, PF_MERA_INVALID_ID on error + */ +static uint16_t pf_mera_ral_id_alloc (pf_mera_t * handle) +{ + uint16_t i; + CC_ASSERT (handle != NULL); + + for (i = 0; i < NELEMENTS (handle->ral.used); i++) + { + if (handle->ral.used[i] == false) + { + handle->ral.used[i] = true; + return handle->ral.base_id + i; + } + } + LOG_ERROR (PF_PPM_LOG, "MERA(%d): Failed to alloc RAL ID \n", __LINE__); + return PF_MERA_INVALID_ID; +} + +/** + * Free a Read Action List (RAL) ID + * @param handle InOut: Ref to mera instance + * @param id In: ral ID + */ +static void pf_mera_ral_id_free (pf_mera_t * handle, uint16_t id) +{ + CC_ASSERT (handle != NULL); + if (id != PF_MERA_INVALID_ID) + { + handle->ral.used[id - handle->ral.base_id] = false; + } +} + +/** + * Allocate a Write Action List (WAL) ID + * @param handle InOut: Ref to mera instance + * @return wal ID, PF_MERA_INVALID_ID on error + */ +static uint16_t pf_mera_wal_id_alloc (pf_mera_t * handle) +{ + uint16_t i; + CC_ASSERT (handle != NULL); + + for (i = 0; i < NELEMENTS (handle->wal.used); i++) + { + if (handle->wal.used[i] == false) + { + handle->wal.used[i] = true; + return handle->wal.base_id + i; + } + } + LOG_ERROR (PF_PPM_LOG, "MERA(%d): Failed to alloc WAL ID \n", __LINE__); + return PF_MERA_INVALID_ID; +} + +/** + * Free a Write Action List (WAL) ID + * @param handle InOut: Ref to mera instance + * @param id In: wal ID + */ +static void pf_mera_wal_id_free (pf_mera_t * handle, uint16_t id) +{ + CC_ASSERT (handle != NULL); + if (id != PF_MERA_INVALID_ID) + { + handle->wal.used[id - handle->wal.base_id] = false; + } +} + +/** + * Get the wal id from a CPM frame using the iocr ref. + * @param handle In: Ref to mera instance + * @param iocr In: IOCR reference + * @param wal_id Out: Write Action List Id + * @return 0 on success, -1 on error + */ +static int pf_mera_get_cpm_wal_id ( + pf_mera_t * handle, + pf_iocr_t * iocr, + mera_ob_wal_id_t * wal_id) +{ + uint16_t i; + for (i = 0; i < PNET_LAN9662_MAX_FRAMES; i++) + { + if ( + handle->frame[i].used == true && + handle->frame[i].config.p_iocr->p_ar == iocr->p_ar && + handle->frame[i].is_cpm) + { + *wal_id = handle->frame[i].ob_wal_id; + return 0; + } + } + return -1; +} +/** + * Add RTE data group for an iodata object. + * Maps the data source to frame/pdu offset. + * @param mera_lib In: Ref to mera lib instance + * @param frame In: Ref to frame + * @param entry In: Iodata object configuration + * @return 0 on success, -1 on error + */ +static int pf_mera_ib_add_dg ( + struct mera_inst * mera_lib, + pf_mera_frame_t * frame, + pf_rte_object_t * entry) +{ + int err; + mera_ib_dg_conf_t ib_dg_cfg; + + err = mera_ib_dg_init (&ib_dg_cfg); + if (err) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to init data group (dg)\n", + __LINE__); + return -1; + } + + ib_dg_cfg.rtp_id = frame->rtp_id; + ib_dg_cfg.pdu_offset = entry->iodata->data_offset + frame->rte_data_offset; + ib_dg_cfg.valid_offset = entry->iodata->iops_offset + frame->rte_data_offset; + if (entry->io_interface_type == MERA_IO_INTF_SRAM) + { + /* In SRAM mode the IOPS is handled by SW */ + ib_dg_cfg.valid_update = false; + } + else + { + ib_dg_cfg.valid_update = true; + } + + ib_dg_cfg.opc_seq_update = false; + ib_dg_cfg.opc_code_update = false; + + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): mera_ib_dg_add " + "ral_id:%d " + "ra_id:%d " + "cfg.rtp_id:%d " + "cfg.pdu_offset:%d " + "cfg.valid_offset:%d\n", + __LINE__, + entry->ib_ral_id, + entry->ib_ra_id, + ib_dg_cfg.rtp_id, + ib_dg_cfg.pdu_offset, + ib_dg_cfg.valid_offset); + + err = + mera_ib_dg_add (mera_lib, entry->ib_ral_id, entry->ib_ra_id, &ib_dg_cfg); + if (err) + { + LOG_ERROR ( + PF_PPM_LOG, + "MERA(%d): Failed to add inbound data group\n", + __LINE__); + return err; + } + + return err; +} +/** + * Convert P-Net interface data type to mera lib type. + * @param pnet_if_type In: P-Net interface type + * @return mera lib interface type + */ +static mera_io_intf_t pf_mera_interface_type ( + pnet_mera_rte_data_type_t pnet_if_type) +{ + switch (pnet_if_type) + { + case PNET_MERA_DATA_TYPE_SRAM: + return MERA_IO_INTF_SRAM; + case PNET_MERA_DATA_TYPE_QSPI: + return MERA_IO_INTF_QSPI; + default: + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Unsupported interface type\n", + __LINE__); + exit (EXIT_FAILURE); + } +} + +/** + * Add RTE read action for an iodata object. + * Configures the data source + * @param mera_lib In: Ref to mera lib instance + * @param entry In: Iodata object configuration + * @return 0 on success, -1 on error + */ +static int pf_mera_ib_add_ra ( + struct mera_inst * mera_lib, + pf_rte_object_t * entry) +{ + mera_ib_ra_conf_t ib_ra_cfg; + mera_ib_ra_ctrl_t ctrl; + + if (mera_ib_ra_init (&ib_ra_cfg) != 0) + { + LOG_ERROR (PF_PPM_LOG, "MERA(%d): Failed to init read action\n", __LINE__); + return -1; + } + + ib_ra_cfg.ra_id = entry->ib_ra_id; + ib_ra_cfg.rd_addr.intf = entry->io_interface_type; + ib_ra_cfg.rd_addr.addr = entry->sram_address; + if (entry->io_interface_type == MERA_IO_INTF_QSPI) + { + ib_ra_cfg.rd_addr.addr = entry->qspi_address; + } + ib_ra_cfg.length = entry->iodata->data_length; + + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): mera_ib_ra_add " + "ral_id:%d " + "cfg.ra_id:%d " + "cfg.rd_addr.intf:%d-%s " + "cfg.rd_addr.addr: 0x%x " + "cfg.length:%d\n", + __LINE__, + entry->ib_ral_id, + entry->ib_ra_id, + ib_ra_cfg.rd_addr.intf, + pf_mera_interface_type_to_str (ib_ra_cfg.rd_addr.intf), + ib_ra_cfg.rd_addr.addr, + ib_ra_cfg.length); + + if (mera_ib_ra_add (mera_lib, entry->ib_ral_id, &ib_ra_cfg) != 0) + { + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to add read action\n", __LINE__); + return -1; + } + + if (ib_ra_cfg.rd_addr.intf == MERA_IO_INTF_QSPI) + { + ctrl.enable = true; + if ( + mera_ib_ra_ctrl_set ( + mera_lib, + entry->ib_ral_id, + entry->ib_ra_id, + &ctrl) != 0) + { + LOG_ERROR ( + PF_PPM_LOG, + "MERA(%d): mera_ib_ra_ctrl_set() error\n", + __LINE__); + return -1; + } + } + + return 0; +} + +/** + * Add RTE read action list for an iodata object. + * Configures when data source shall be read. + * @param mera_lib In: Ref to mera lib instance + * @param entry In: Iodata object configuration + * @param interval_in_ns In: Read interval in nanoseconds + * @return 0 on success, -1 on error + */ +static int pf_mera_ib_add_ral ( + struct mera_inst * mera_lib, + pf_rte_object_t * entry, + uint32_t interval_in_ns) +{ + mera_ib_ral_conf_t ral_cfg; + + ral_cfg.time.offset = PF_MERA_RAL_TIME_OFFSET * entry->ib_ral_id; + ral_cfg.time.interval = interval_in_ns; + + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): mera_ib_ral_conf_set " + "ral_id:%d " + "cfg.time.offset:%d(ns) " + "cfg.time.interval:%d(ns)\n", + __LINE__, + entry->ib_ral_id, + ral_cfg.time.offset, + ral_cfg.time.interval); + + if (mera_ib_ral_conf_set (mera_lib, entry->ib_ral_id, &ral_cfg) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to set read action configuration\n", + __LINE__); + return -1; + } + + return 0; +} + +/** + * Add RTE configuration for an inbound iodata object. + * @param handle In: Ref to mera instance + * @param frame In: Ref to frame + * @param entry In: Iodata object configuration + * @return 0 on success, -1 on error + */ +static int pf_mera_add_ib_entry ( + pf_mera_t * handle, + pf_mera_frame_t * frame, + pf_rte_object_t * entry) +{ + mera_ob_wa_conf_t wa_cfg; + entry->in_use = true; + + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): [%u,%u] Configure inbound data entry\n", + __LINE__, + entry->iodata->slot_nbr, + entry->iodata->subslot_nbr); + + entry->ib_ral_id = pf_mera_ral_id_alloc (handle); + + if (pf_mera_ib_add_ra (handle->mera_lib, entry) != 0) + { + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed ta add read action\n", __LINE__); + return -1; + } + + if (pf_mera_ib_add_dg (handle->mera_lib, frame, entry) != 0) + { + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed ta add data group\n", __LINE__); + return -1; + } + + if (pf_mera_ib_add_ral (handle->mera_lib, entry, frame->interval) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed ta add read action list\n", + __LINE__); + return -1; + } + + if (entry->io_interface_type == MERA_IO_INTF_QSPI) + { + LOG_DEBUG ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] Add internal transfer QSPI 0x%x -> SRAM 0x%x\n", + __LINE__, + entry->iodata->slot_nbr, + entry->iodata->subslot_nbr, + entry->qspi_address, + entry->sram_address); + + if (mera_ob_wa_init (&wa_cfg) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed init internal write action\n", + __LINE__); + return 0; + } + + wa_cfg.internal = true; + /* wa_cfg.rtp_id not used for internal transfers */ + /* wa_cfg.grp_id not used for internal transfers */ + /* wa_cfg.dg_id not used for internal transfers */ + + /* Read address */ + wa_cfg.rd_addr.intf = MERA_IO_INTF_QSPI; + wa_cfg.rd_addr.addr = entry->qspi_address; + + /* Write address */ + wa_cfg.wr_addr.intf = MERA_IO_INTF_SRAM; + wa_cfg.wr_addr.addr = entry->sram_address; + + wa_cfg.length = entry->iodata->data_length; + + if (mera_ob_wa_add (handle->mera_lib, frame->internal_wal_id, &wa_cfg) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to add internal write action\n", + __LINE__); + return -1; + } + } + + return 0; +} + +/** + * Set the general RTE configuration for a PPM frame. + * @param handle In: Ref to mera instance + * @param frame In: Ref to frame + * @param entry In: Iodata object configuration + * @return 0 on success, -1 on error + */ +static int pf_mera_ib_rtp_config (pf_mera_t * handle, pf_mera_frame_t * frame) +{ + int err = 0; + mera_ib_rtp_conf_t ib_rtp_config = {0}; + pf_ppm_t * p_ppm = &frame->config.p_iocr->ppm; + + ib_rtp_config.type = MERA_RTP_TYPE_PN; + ib_rtp_config.grp_id = PF_MERA_GROUP_ID_NONE; + ib_rtp_config.mode = MERA_RTP_IB_MODE_INJ; + ib_rtp_config.time.offset = MERA_TIME_OFFSET_NONE; + ib_rtp_config.time.interval = frame->interval; + ib_rtp_config.port = frame->port; + ib_rtp_config.length = ((pnal_buf_t *)p_ppm->p_send_buffer)->len; + + memcpy ( + ib_rtp_config.data, + ((pnal_buf_t *)p_ppm->p_send_buffer)->payload, + ((pnal_buf_t *)p_ppm->p_send_buffer)->len); + + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): mera_ib_rtp_conf_set " + "rtp_id:%d " + "type: %d " + "grp_id:%d " + "mode:%d " + "time.offset:%d " + "time.interval:%d " + "port:%d " + "length:%d\n", + __LINE__, + frame->rtp_id, + ib_rtp_config.type, + ib_rtp_config.grp_id, + ib_rtp_config.mode, + ib_rtp_config.time.offset, + ib_rtp_config.time.interval, + ib_rtp_config.port, + ib_rtp_config.length); + + err = mera_ib_rtp_conf_set (handle->mera_lib, frame->rtp_id, &ib_rtp_config); + if (err) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to set inbound RTP configuration\n", + __LINE__); + } + return err; +} + +/** + * Configure and start a RTE configuration for a PPM frame. + * Sets the general RTE configuration and the configuration + * for each iodata object in the communication relationship. + * @param handle In: Ref to mera instance + * @param frame In: Ref to frame + * @return 0 on success, -1 on error + */ +static int pf_mera_ppm_start_rte (pf_mera_t * handle, pf_mera_frame_t * frame) +{ + int err = 0; + pf_iocr_t * p_iocr = frame->config.p_iocr; + uint16_t slot, subslot; + + err = pf_mera_ib_add_vcam_rule ( + frame->vcam_id, + p_iocr->param.iocr_tag_header.vlan_id, + &frame->config.main_mac_addr, /* MAC of sending interface */ + frame->vlan_idx, + p_iocr->param.frame_id, + frame->rtp_id); + + if (err != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to add inbound vcam rule\n", + __LINE__); + return err; + } + + err = pf_mera_ib_rtp_config (handle, frame); + if (err != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to set inbound RTP configuration\n", + __LINE__); + return err; + } + + err = pf_mera_get_cpm_wal_id ( + handle, + frame->config.p_iocr, + &frame->internal_wal_id); + if (err != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to get PPM internal WAL id from CPM\n", + __LINE__); + return err; + } + + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + for (subslot = 0; subslot < PNET_MAX_SUBSLOTS; subslot++) + { + if ( + frame->rte_object[slot][subslot].iodata != NULL && + frame->rte_object[slot][subslot].iodata->data_length > 0) + { + err = pf_mera_add_ib_entry ( + handle, + frame, + &frame->rte_object[slot][subslot]); + + if (err != 0) + { + return err; + } + } + } + } + + frame->active = true; + + return err; +} + +int pf_driver_init (pnet_t * net) +{ + pf_mera_t * handle; + mera_cb_t mera_lib_cb = { + .reg_rd = pf_rte_uio_reg_read, + .reg_wr = pf_rte_uio_reg_write, + .lock = NULL, + .unlock = NULL, + .trace_printf = pf_mera_callout_trace_printf, + .trace_hex_dump = NULL}; + + LOG_DEBUG (PF_MERA_LOG, "MERA(%d): Init RTE UIO driver\n", __LINE__); + + if (pf_rte_uio_init() != 0) + { + LOG_FATAL ( + PF_PPM_LOG, + "MERA(%d): RTE UIO driver initialization error\n", + __LINE__); + exit (EXIT_FAILURE); + } + + LOG_DEBUG (PF_MERA_LOG, "MERA(%d): Init SRAM UIO driver\n", __LINE__); + if (pf_sram_init() != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): SRAM UIO driver initialization error\n", + __LINE__); + exit (EXIT_FAILURE); + } + + handle = (pf_mera_t *)malloc (sizeof (pf_mera_t)); + CC_ASSERT (handle != NULL); + memset (handle, 0, sizeof (pf_mera_t)); + + handle->vcam_id.base_id = + net->p_fspm_default_cfg->driver_config.mera.vcam_base_id; + handle->rtp.base_id = + net->p_fspm_default_cfg->driver_config.mera.rtp_base_id; + handle->ral.base_id = + net->p_fspm_default_cfg->driver_config.mera.ral_base_id; + handle->wal.base_id = + net->p_fspm_default_cfg->driver_config.mera.wal_base_id; + + handle->mera_lib = mera_create (&mera_lib_cb); + if (handle->mera_lib == NULL) + { + LOG_FATAL (PF_MERA_LOG, "MERA(%d): Failed to init MERA lib\n", __LINE__); + exit (EXIT_FAILURE); + } + + /* Start mera poll timeout */ + pf_scheduler_init_handle (&handle->poll_handle, "mera_poll"); + if ( + pf_scheduler_add ( + net, + PF_MERA_POLL_INTERVAL_IN_US, + pf_mera_poll, + handle, + &handle->poll_handle) != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to start poll timer\n", + __LINE__); + exit (EXIT_FAILURE); + } + + handle->hwo_drv.initialized = true; + net->hwo_drv = (pf_drv_t *)handle; + + return 0; +}; + +/** + * Allocate a mera frame + * The frame contains all required RTE configurations for a PPM or CPM. + * Iodata interface type is initialized to SRAM. + * @param handle In: Ref to mera instance + * @return Allocated frame, NULL on error + */ +static pf_mera_frame_t * pf_mera_alloc_frame (pf_mera_t * handle) +{ + uint16_t i, j, k; + pf_mera_frame_t * frame = NULL; + + for (i = 0; i < PNET_LAN9662_MAX_FRAMES; i++) + { + if (handle->frame[i].used == false) + { + frame = &handle->frame[i]; + memset (frame, 0, sizeof (*frame)); + frame->drv_frame.initialized = true; + frame->used = true; + frame->active = false; + frame->ob_dg_count = 1; + + for (j = 0; j < PNET_MAX_SLOTS; j++) + { + for (k = 0; k < PNET_MAX_SUBSLOTS; k++) + { + frame->rte_object[j][k].ib_ral_id = PF_MERA_INVALID_ID; + frame->rte_object[j][k].io_interface_type = MERA_IO_INTF_SRAM; + frame->rte_object[j][k].sram_address = PF_SRAM_INVALID_ADDRESS; + frame->mera_lib = handle->mera_lib; + } + } + LOG_DEBUG ( + PF_MERA_LOG, + "MERA(%d): Frame allocated (index=%d)\n", + __LINE__, + i); + return frame; + } + } + + LOG_FATAL (PF_MERA_LOG, "MERA(%d): Failed to allocate frame\n", __LINE__); + exit (EXIT_FAILURE); + return NULL; +} + +bool pf_mera_is_active_frame (pf_drv_frame_t * frame) +{ + return ((pf_mera_frame_t *)frame)->active; +} + +/** + * Free a mera frame + * @param handle InOut: Ref to mera instance + * @param frame In: The frame to free + */ +static void pf_mera_free_frame (pf_mera_t * handle, pf_mera_frame_t * frame) +{ + if (frame != NULL) + { + frame->used = false; + return; + } + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to free frame\n", __LINE__); +} + +pf_drv_frame_t * pf_mera_ppm_alloc ( + pnet_t * net, + const pf_mera_frame_cfg_t * cfg) +{ + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + pf_mera_frame_t * frame = NULL; + pf_iodata_object_t * p_iodata; + pf_rte_object_t * entry; + uint16_t i; + pf_ppm_t * ppm; + + if (cfg == NULL || cfg->p_iocr == NULL || handle == NULL) + { + return NULL; + } + + frame = pf_mera_alloc_frame (handle); + if (frame == NULL) + { + return NULL; + } + + frame->is_ppm = true; + + frame->sram_frame_address = pf_sram_frame_alloc(); + if (frame->sram_frame_address == 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to allocate SRAM frame\n", + __LINE__); + pf_mera_free_frame (handle, frame); + return NULL; + } + + LOG_DEBUG ( + PF_MERA_LOG, + "MERA(%d): Allocated PPM SRAM frame at 0x%x\n", + __LINE__, + frame->sram_frame_address); + + frame->config = *cfg; + frame->port = cfg->port; + + ppm = &frame->config.p_iocr->ppm; + frame->interval = + (1000000 * ppm->send_clock_factor * ppm->reduction_ratio) / 32; + + for (i = 0; i < frame->config.p_iocr->nbr_data_desc; i++) + { + p_iodata = &frame->config.p_iocr->data_desc[i]; + if (p_iodata->data_length > 0) + { + entry = + get_rte_object (frame, p_iodata->slot_nbr, p_iodata->subslot_nbr); + entry->iodata = p_iodata; + } + } + + frame->vcam_id = pf_mera_vcam_id_alloc (handle); + frame->rtp_id = pf_mera_rtp_id_alloc (handle); + /* frame->internal_wal_id assigned later */ + frame->vlan_idx = pf_mera_vlan_index_alloc (handle); + frame->rte_data_offset = PF_MERA_PPM_IODATA_OFFSET; + + LOG_DEBUG (PF_MERA_LOG, "MERA(%d): PPM frame allocated\n", __LINE__); + return (pf_drv_frame_t *)frame; +} + +int pf_mera_ppm_free (pnet_t * net, pf_drv_frame_t * drv_frame) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + LOG_DEBUG (PF_PPM_LOG, "MERA(%d): Free PPM frame\n", __LINE__); + if (frame != NULL) + { + pf_mera_vcam_id_free (handle, frame->vcam_id); + pf_mera_vlan_index_free (handle, frame->vlan_idx); + pf_mera_rtp_id_free (handle, frame->rtp_id); + pf_sram_frame_free (frame->sram_frame_address); + pf_mera_free_frame (handle, frame); + return 0; + } + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to free PPM frame\n", __LINE__); + return -1; +} + +/** + * Generate RTE configurations for all iodata objects in a PPM type + * of communication relation. + * @param handle InOut: Ref to mera instance + * @param frame InOut: The PPM frame + * @return 0 on success, -1 on error + */ +static int pf_mera_ppm_config (pf_mera_t * handle, pf_mera_frame_t * frame) +{ + int ret = 0; + uint16_t slot, subslot; + pf_rte_object_t * entry; + const pf_mera_rte_cfg_t * entry_rte_data_cfg; /* qspi address */ + + LOG_DEBUG (PF_PPM_LOG, "MERA(%d): Apply RTE configuration (PPM)\n", __LINE__); + + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + for (subslot = 0; subslot < PNET_MAX_SUBSLOTS; subslot++) + { + if ( + frame->rte_object[slot][subslot].iodata != NULL && + frame->rte_object[slot][subslot].iodata->data_length > 0) + { + entry = &frame->rte_object[slot][subslot]; + entry_rte_data_cfg = &handle->rte_data_cfg[slot][subslot]; + + entry->ib_ra_id = frame->ib_ra_id_count++; + + if (entry_rte_data_cfg->valid) + { + entry->io_interface_type = + pf_mera_interface_type (entry_rte_data_cfg->cfg.type); + +#if PF_MERA_ALLOW_APPLICATION_SRAM + if (entry_rte_data_cfg->cfg.type == PNET_MERA_DATA_TYPE_SRAM) + { + entry->sram_address = entry_rte_data_cfg->cfg.address; + } +#endif + entry->qspi_address = entry_rte_data_cfg->cfg.address; + entry->default_data = entry_rte_data_cfg->cfg.default_data; + } + + if (entry->sram_address == PF_SRAM_INVALID_ADDRESS) + { + entry->sram_address = frame->sram_frame_address + + frame->rte_data_offset + + entry->iodata->data_offset; + } + + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): [%u,%u] Interface: %s Start address: 0x%x Size: %d\n", + __LINE__, + entry->iodata->slot_nbr, + entry->iodata->subslot_nbr, + pf_mera_interface_type_to_str (entry->io_interface_type), + entry->io_interface_type == MERA_IO_INTF_QSPI + ? entry->qspi_address + : entry->sram_address, + entry->iodata->data_length); + } + } + } + + return ret; +} + +int pf_mera_ppm_start (pnet_t * net, pf_drv_frame_t * drv_frame) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + if (handle == NULL || frame == NULL || frame->config.p_iocr == NULL) + { + LOG_ERROR ( + PF_PPM_LOG, + "MERA(%d): Failed to start RTE - parameter error\n", + __LINE__); + return -1; + } + + if (pf_mera_ppm_config (handle, frame) != 0) + { + LOG_ERROR (PF_PPM_LOG, "MERA(%d): Failed to configure RTE\n", __LINE__); + return -1; + } + + if (pf_mera_ppm_start_rte (handle, frame) != 0) + { + LOG_ERROR (PF_PPM_LOG, "MERA(%d): Failed to start RTE\n", __LINE__); + return -1; + } + +#if PNET_OPTION_LAN9662_SHOW_RTE_INFO + pf_scheduler_init_handle (&handle->sched_handle, "mera_show"); + if ( + pf_scheduler_add ( + net, + 2000000, + pf_mera_rte_show_periodic, + handle, + &handle->sched_handle) != 0) + { + LOG_FATAL (PF_MERA_LOG, "MERA(%d): Failed to add timer\n", __LINE__); + exit (EXIT_FAILURE); + } +#endif + + return 0; +} + +int pf_mera_ppm_write ( + pnet_t * net, + pf_drv_frame_t * drv_frame, + uint32_t offset, + const uint8_t * data, + uint32_t len) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + uint16_t i; + mera_ib_rtp_data_t write_req; + int ret; + + if (frame->active) + { + for (i = 0; i < len; i++) + { + write_req.value = data[i]; + write_req.offset = offset + i; + ret = + mera_ib_rtp_data_set (handle->mera_lib, frame->rtp_id, &write_req); + + if (ret != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to write frame RTP frame buffer\n", + __LINE__); + return -1; + } + } + } + + return 0; +} + +int pf_mera_ppm_write_input ( + pnet_t * net, + pf_drv_frame_t * drv_frame, + const pf_iodata_object_t * p_iodata, + const uint8_t * data, + uint32_t len) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + int ret = -1; + pf_rte_object_t * entry; + + if (!frame->active) + { + return -1; + } + + entry = get_rte_object (frame, p_iodata->slot_nbr, p_iodata->subslot_nbr); + + switch (entry->io_interface_type) + { + case MERA_IO_INTF_SRAM: + if ( + mera_ib_ral_req ( + handle->mera_lib, + entry->ib_ral_id, + &entry->ib_ral_buf) == 0) + { + ret = pf_sram_write ( + entry->sram_address + entry->ib_ral_buf.addr, + data, + len); + + if (mera_ib_ral_rel (handle->mera_lib, entry->ib_ral_id) != 0) + { + ret = -1; + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Releasing RAL buffer failed\n", + __LINE__); + } + } + else + { + ret = -1; + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Request for RAL buffer failed\n", + __LINE__); + } + + break; + case PNET_MERA_DATA_TYPE_QSPI: + default: + /* Write not supported */ + return 0; + } + return ret; +} + +int pf_mera_ppm_read_data_and_iops ( + pnet_t * net, + pf_drv_frame_t * drv_frame, + const pf_iodata_object_t * p_iodata, + uint8_t * p_data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + int err = 0; + pf_rte_object_t * entry; + mera_buf_t internal_wal_buf; + + *p_iops = PNET_IOXS_GOOD; + + if (!frame->active) + { + return -1; + } + + if (p_iodata->slot_nbr == PNET_SLOT_DAP_IDENT) + { + return 0; + } + + if (data_len == 0 || iops_len == 0) + { + return 0; + } + + entry = get_rte_object (frame, p_iodata->slot_nbr, p_iodata->subslot_nbr); + + switch (entry->io_interface_type) + { + case MERA_IO_INTF_SRAM: + if (pf_sram_read (entry->sram_address, p_data, data_len) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] Failed to read SRAM (input) \n", + __LINE__, + p_iodata->slot_nbr, + p_iodata->subslot_nbr); + + return -1; + } + break; + + case MERA_IO_INTF_QSPI: + err = mera_ob_wal_req ( + handle->mera_lib, + frame->internal_wal_id, + &internal_wal_buf); + if (err != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to req wal buffer for internal transfer\n", + __LINE__); + return err; + } + if ( + pf_sram_read ( + entry->sram_address + internal_wal_buf.addr, + p_data, + data_len) != 0) + { + mera_ob_wal_rel (handle->mera_lib, frame->internal_wal_id); + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] Failed to read SRAM (input) \n", + __LINE__, + p_iodata->slot_nbr, + p_iodata->subslot_nbr); + + return -1; + } + mera_ob_wal_rel (handle->mera_lib, frame->internal_wal_id); + break; + + default: + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] unsupported interface type\n", + __LINE__, + p_iodata->slot_nbr, + p_iodata->subslot_nbr); + return -1; + } + return 0; +} + +int pf_mera_ppm_write_iops ( + pnet_t * net, + pf_drv_frame_t * drv_frame, + const pf_iodata_object_t * p_iodata, + const uint8_t * iops, + uint8_t len) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_rte_object_t * entry; + + if (p_iodata->slot_nbr == PNET_SLOT_DAP_IDENT) + { + /* Setting only supported on data submodules + * DAP iops set during initial RTE configuration + * and never changed. + */ + return -1; + } + + if (!frame->active) + { + return -1; + } + + entry = get_rte_object (frame, p_iodata->slot_nbr, p_iodata->subslot_nbr); + + switch (entry->io_interface_type) + { + case MERA_IO_INTF_SRAM: + return pf_mera_ppm_write ( + net, + (pf_drv_frame_t *)frame, + frame->config.p_iocr->ppm.buffer_pos + p_iodata->iops_offset, + iops, + len); + + case MERA_IO_INTF_QSPI: + default: + /* Writing IOPS is not supported + * Automatically updated by RTE + */ + return -1; + } + return 0; +} + +int pf_mera_ppm_write_iocs ( + pnet_t * net, + pf_drv_frame_t * drv_frame, + const pf_iodata_object_t * p_iodata, + const uint8_t * iops, + uint8_t len) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + + if (!frame->active) + { + return -1; + } + + return pf_mera_ppm_write ( + net, + drv_frame, + frame->config.p_iocr->ppm.buffer_pos + p_iodata->iocs_offset, + iops, + len); +} + +int pf_mera_ppm_write_data_status ( + pnet_t * net, + pf_drv_frame_t * drv_frame, + uint8_t data_status) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + if (frame == NULL || !frame->active) + { + /* Ignore writes during ppm initialization */ + return 0; + } + + return pf_mera_ppm_write ( + net, + drv_frame, + frame->config.p_iocr->ppm.data_status_offset, + &data_status, + 1); +} + +int pf_mera_ppm_stop (pnet_t * net, pf_drv_frame_t * drv_frame) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + uint16_t i, j; + + LOG_DEBUG (PF_PPM_LOG, "MERA(%d): Stop PPM\n", __LINE__); + if (frame->active) + { + mera_ib_flush (handle->mera_lib); + pf_mera_delete_vcam_rule (frame->vcam_id); + + for (i = 0; i < PNET_MAX_SLOTS; i++) + { + for (j = 0; j < PNET_MAX_SUBSLOTS; j++) + { + pf_mera_ral_id_free (handle, frame->rte_object[i][j].ib_ral_id); + } + } + } + else + { + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): No action - PPM is not started\n", + __LINE__); + } + + return 0; +} + +/** + * Set the general RTE configuration for a CPM frame. + * @param mera_lib In: Ref to mera lib instance + * @param frame In: Ref to frame + * @return 0 on success, -1 on error + */ +static int pf_mera_ob_rtp_config ( + struct mera_inst * mera_lib, + pf_mera_frame_t * frame) +{ + int err = 0; + + mera_ob_rtp_conf_t ob_rtp_config = {0}; + + ob_rtp_config.type = MERA_RTP_TYPE_PN; + ob_rtp_config.grp_id = PF_MERA_GROUP_ID_NONE; + ob_rtp_config.length = sizeof (uint16_t) + /* frame_id */ + frame->config.p_iocr->param.c_sdu_length + /* Profinet + data + length + */ + sizeof (uint16_t) + /* cycle counter */ + 1 + /* data status */ + 1; /* transfer status */ + + ob_rtp_config.pn_ds = PF_MERA_DATA_STATUS_MASK; + ob_rtp_config.pn_discard = false; + ob_rtp_config.opc_grp_ver = 0; + ob_rtp_config.wal_enable = true; + ob_rtp_config.wal_id = frame->ob_wal_id; + + ob_rtp_config.time.offset = PF_MERA_OB_RTP_TIME_OFFSET; + ob_rtp_config.time.interval = 0; + ob_rtp_config.time_cnt = 0; + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): mera_ob_rtp_conf_set " + "type:%d " + "grp_id:%d " + "length:%d " + "pn_ds:%d " + "pn_discard:%d " + "opc_grp_ver:%d " + "wal_enable:%d " + "wal_id:%d " + "time.offset:%d " + "time.interval:%d " + "time_cnt:%d\n", + __LINE__, + ob_rtp_config.type, + ob_rtp_config.grp_id, + ob_rtp_config.length, + ob_rtp_config.pn_ds, + ob_rtp_config.pn_discard, + ob_rtp_config.opc_grp_ver, + ob_rtp_config.wal_enable, + ob_rtp_config.wal_id, + ob_rtp_config.time.offset, + ob_rtp_config.time.interval, + ob_rtp_config.time_cnt); + + err = mera_ob_rtp_conf_set (mera_lib, frame->rtp_id, &ob_rtp_config); + if (err) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to set RTP configuration\n", + __LINE__); + } + return err; +} + +/** + * Set the data group (DG) configuration for an outbound iodata object + * part of a CPM frame. + * Sets the frame data offset and length. + * @param mera_lib In: Ref to mera lib instance + * @param frame In: Ref to frame + * @param entry In: Iodata object configuration + * @return 0 on success, -1 on error + */ +static int pf_mera_ob_add_dg ( + struct mera_inst * mera_lib, + pf_mera_frame_t * frame, + pf_rte_object_t * entry) +{ + mera_ob_dg_conf_t dg_cfg; + mera_ob_dg_ctrl_t dg_ctrl; + + if (mera_ob_dg_init (&dg_cfg) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to init outbound data group\n", + __LINE__); + return -1; + } + + dg_cfg.dg_id = entry->ob_dg_id; + dg_cfg.pdu_offset = entry->iodata->data_offset + frame->rte_data_offset; + dg_cfg.length = entry->iodata->data_length; + dg_cfg.valid_offset = entry->iodata->iops_offset + frame->rte_data_offset; + + dg_cfg.valid_chk = true; + dg_cfg.opc_seq_chk = false; + dg_cfg.opc_code_chk = false; + dg_cfg.invalid_default = true; + if (entry->default_data != NULL) + { + memcpy (dg_cfg.data, entry->default_data, entry->iodata->data_length); + } + else + { + memset (dg_cfg.data, 0, entry->iodata->data_length); + } + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): mera_ob_dg_add " + "rtp_id:%d " + "cfg.dg_id:%d " + "cfg.pdu_offset:%d " + "cfg.length:%d " + "cfg.valid_offset:%d " + "cfg.valid_chk:%d " + "cfg.opc_seq_chk:%d " + "cfg.opc_code_chk:%d " + "cfg.invalid_default:%d\n", + __LINE__, + frame->rtp_id, + dg_cfg.dg_id, + dg_cfg.pdu_offset, + dg_cfg.length, + dg_cfg.valid_offset, + dg_cfg.valid_chk, + dg_cfg.opc_seq_chk, + dg_cfg.opc_code_chk, + dg_cfg.invalid_default); + + if (mera_ob_dg_add (mera_lib, frame->rtp_id, &dg_cfg) != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to add outbound data group\n", + __LINE__); + return -1; + } + + dg_ctrl.enable = true; + + if (mera_ob_dg_ctrl_set (mera_lib, frame->rtp_id, entry->ob_dg_id, &dg_ctrl) != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to enable outbound data group\n", + __LINE__); + return -1; + } + + return 0; +} + +/** + * Set the Write Action (WA) for an outbound iodata object + * part of a CPM frame. + * Sets the data sink (SRAM/QSPI address). + * @param mera_lib In: Ref to mera instance + * @param frame In: Ref to frame + * @param entry In: Iodata object configuration + * @return 0 on success, -1 on error + */ +static int pf_mera_ob_add_wa ( + struct mera_inst * mera_lib, + pf_mera_frame_t * frame, + pf_rte_object_t * entry) +{ + mera_ob_wa_conf_t wa_cfg; + if (mera_ob_wa_init (&wa_cfg) != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed init outbound write action\n", + __LINE__); + return 0; + } + + wa_cfg.internal = false; + wa_cfg.rtp_id = frame->rtp_id; + wa_cfg.grp_id = PF_MERA_GROUP_ID_NONE; + wa_cfg.dg_id = entry->ob_dg_id; + wa_cfg.wr_addr.intf = MERA_IO_INTF_SRAM; + wa_cfg.wr_addr.addr = entry->sram_address; + + wa_cfg.length = entry->iodata->data_length; + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): mera_ob_wa_add " + "wal_id:%d " + "cfg.internal:%d " + "cfg.rtp_id:%d " + "cfg.grp_id:%d " + "cfg.dg_id:%d " + "cfg.length:%d " + "cfg.wr_addr.intf:%d-%s " + "cfg.wr_addr.addr:0x%x\n", + __LINE__, + frame->ob_wal_id, + wa_cfg.internal, + wa_cfg.rtp_id, + wa_cfg.grp_id, + wa_cfg.dg_id, + wa_cfg.length, + wa_cfg.wr_addr.intf, + pf_mera_interface_type_to_str (wa_cfg.wr_addr.intf), + wa_cfg.wr_addr.addr); + + if (mera_ob_wa_add (mera_lib, frame->ob_wal_id, &wa_cfg) != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to add outbound read action\n", + __LINE__); + return -1; + } + + if (entry->io_interface_type == MERA_IO_INTF_QSPI) + { + wa_cfg.wr_addr.addr = entry->qspi_address; + wa_cfg.wr_addr.intf = MERA_IO_INTF_QSPI; + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): mera_ob_wa_add " + "wal_id:%d " + "cfg.internal:%d " + "cfg.rtp_id:%d " + "cfg.grp_id:%d " + "cfg.dg_id:%d " + "cfg.length:%d " + "cfg.wr_addr.intf:%d-%s " + "cfg.wr_addr.addr:0x%x\n", + __LINE__, + frame->ob_wal_id, + wa_cfg.internal, + wa_cfg.rtp_id, + wa_cfg.grp_id, + wa_cfg.dg_id, + wa_cfg.length, + wa_cfg.wr_addr.intf, + pf_mera_interface_type_to_str (wa_cfg.wr_addr.intf), + wa_cfg.wr_addr.addr); + + if (mera_ob_wa_add (mera_lib, frame->ob_wal_id, &wa_cfg) != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to add outbound read action\n", + __LINE__); + return -1; + } + } + + return 0; +} + +/** + * Set the RTE configuration for an outbound iodata object + * part of a CPM frame. Sets datagroup and write action. + * @param mera_lib In: Ref to mera lib instance + * @param frame In: Ref to frame + * @param entry In: Iodata object configuration + * @return 0 on success, -1 on error + */ +static int pf_mera_add_ob_entry ( + struct mera_inst * mera_lib, + pf_mera_frame_t * frame, + pf_rte_object_t * entry) +{ + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): [%u,%u] Configure outbound data entry\n", + __LINE__, + entry->iodata->slot_nbr, + entry->iodata->subslot_nbr); + + entry->in_use = true; + + if (pf_mera_ob_add_dg (mera_lib, frame, entry) != 0) + { + return -1; + } + + if (pf_mera_ob_add_wa (mera_lib, frame, entry) != 0) + { + return -1; + } + + return 0; +} + +/** + * Generate RTE configurations for all iodata objects in a CPM type + * of communication relation. + * @param handle InOut: Ref to mera instance + * @param frame InOut: The PPM frame + * @return 0 on success, -1 on error + */ +static int pf_mera_cpm_config (pf_mera_t * handle, pf_mera_frame_t * frame) +{ + int ret = 0; + uint16_t slot, subslot; + pf_rte_object_t * entry; + const pf_mera_rte_cfg_t * entry_rte_data_cfg; /* qspi address */ + + LOG_DEBUG (PF_CPM_LOG, "MERA(%d): Apply RTE configuration (CMP)\n", __LINE__); + + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + for (subslot = 0; subslot < PNET_MAX_SUBSLOTS; subslot++) + { + if ( + frame->rte_object[slot][subslot].iodata != NULL && + frame->rte_object[slot][subslot].iodata->data_length > 0) + { + entry = &frame->rte_object[slot][subslot]; + entry_rte_data_cfg = &handle->rte_data_cfg[slot][subslot]; + entry->ob_dg_id = frame->ob_dg_count++; + + if (entry_rte_data_cfg->valid) + { + entry->io_interface_type = + pf_mera_interface_type (entry_rte_data_cfg->cfg.type); + +#if PF_MERA_ALLOW_APPLICATION_SRAM + if (entry_rte_data_cfg->cfg.type == PNET_MERA_DATA_TYPE_SRAM) + { + entry->sram_address = entry_rte_data_cfg->cfg.address; + } +#endif + entry->qspi_address = entry_rte_data_cfg->cfg.address; + entry->default_data = entry_rte_data_cfg->cfg.default_data; + } + + if (entry->sram_address == PF_SRAM_INVALID_ADDRESS) + { + /* SRAM address has not been set by application + * use address from SRAM frame + */ + entry->sram_address = frame->sram_frame_address + + frame->rte_data_offset + + entry->iodata->data_offset; + } + + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): [%u,%u]: Interface: %s Start address: 0x%x Size: " + "%d\n", + __LINE__, + entry->iodata->slot_nbr, + entry->iodata->subslot_nbr, + pf_mera_interface_type_to_str (entry->io_interface_type), + entry->io_interface_type == MERA_IO_INTF_QSPI + ? entry->qspi_address + : entry->sram_address, + entry->iodata->data_length); + } + } + } + + return ret; +} + +/** + * Configure and start a RTE configuration for a CPM frame. + * Sets the general RTE configuration and the configuration + * for each iodata object in the communication relationship. + * @param handle In: Ref to mera instance + * @param frame In: Ref to frame + * @return 0 on success, -1 on error + */ +static int pf_mera_cpm_start_rte (pf_mera_t * handle, pf_mera_frame_t * frame) +{ + int err = 0; + pf_iocr_t * p_iocr = frame->config.p_iocr; + uint16_t slot, subslot; + mera_ob_wal_conf_t wal_conf; + mera_ob_rtp_state_t state; + + err = ob_add_vcam_rule ( + frame->vcam_id, + p_iocr->param.iocr_tag_header.vlan_id, + &frame->config.main_mac_addr, + frame->vlan_idx, + p_iocr->param.frame_id, + frame->rtp_id); + + if (err != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to add outbound vcam rule\n", + __LINE__); + return err; + } + + err = pf_mera_ob_rtp_config (handle->mera_lib, frame); + if (err != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to set outbound RTP configuration\n", + __LINE__); + return err; + } + + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + for (subslot = 0; subslot < PNET_MAX_SUBSLOTS; subslot++) + { + if ( + frame->rte_object[slot][subslot].iodata != NULL && + frame->rte_object[slot][subslot].iodata->data_length > 0) + { + if ( + pf_mera_add_ob_entry ( + handle->mera_lib, + frame, + &frame->rte_object[slot][subslot]) != 0) + { + return -1; + } + } + } + } + + /* Add WAL to trigger reset to default value on DHT watchdog timeout. */ + mera_ob_wal_conf_get (handle->mera_lib, frame->ob_wal_id, &wal_conf); + wal_conf.time.offset = 10000; + wal_conf.time.interval = frame->config.p_iocr->cpm.control_interval * 1000; + + if (mera_ob_wal_conf_set (handle->mera_lib, frame->ob_wal_id, &wal_conf) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to set outbound RTP WAL \n", + __LINE__); + } + + /* Activate rtp id. Needed if DHT has expired in previous connection. */ + state.active = true; + if (mera_ob_rtp_state_set (handle->mera_lib, frame->rtp_id, &state) != 0) + { + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to set RTP state \n", __LINE__); + } + + frame->active = true; + + if ( + mera_event_enable ( + handle->mera_lib, + (MERA_EVENT_RTP_STATE_STOPPED | MERA_EVENT_PN_DS_MISMATCH | + MERA_EVENT_DG_INVALID), + true) != 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to enable outbound events\n", + __LINE__); + return -1; + } + + return 0; +} + +pf_drv_frame_t * pf_mera_cpm_alloc ( + pnet_t * net, + const pf_mera_frame_cfg_t * cfg) +{ + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + pf_mera_frame_t * frame = NULL; + pf_iodata_object_t * p_iodata; + pf_rte_object_t * entry; + uint16_t i; + + if (cfg == NULL || cfg->p_iocr == NULL || handle == NULL) + { + return NULL; + } + + frame = pf_mera_alloc_frame (handle); + if (frame == NULL) + { + return NULL; + } + + frame->is_cpm = true; + + frame->sram_frame_address = pf_sram_frame_alloc(); + if (frame->sram_frame_address == 0) + { + LOG_FATAL ( + PF_MERA_LOG, + "MERA(%d): Failed to alloc SRAM for CPM frame\n", + __LINE__); + pf_mera_free_frame (handle, frame); + return NULL; + } + + LOG_DEBUG ( + PF_MERA_LOG, + "MERA(%d): Allocated CPM SRAM frame at 0x%x\n", + __LINE__, + frame->sram_frame_address); + + frame->config = *cfg; + frame->port = cfg->port; + + for (i = 0; i < frame->config.p_iocr->nbr_data_desc; i++) + { + p_iodata = &frame->config.p_iocr->data_desc[i]; + if (p_iodata->data_length > 0) + { + entry = + get_rte_object (frame, p_iodata->slot_nbr, p_iodata->subslot_nbr); + entry->iodata = p_iodata; + } + } + + frame->vcam_id = pf_mera_vcam_id_alloc (handle); + frame->vlan_idx = pf_mera_vlan_index_alloc (handle); + frame->rtp_id = pf_mera_rtp_id_alloc (handle); + frame->ob_wal_id = pf_mera_wal_id_alloc (handle); + frame->rte_data_offset = PF_MERA_CPM_IODATA_OFFSET; + + LOG_DEBUG (PF_CPM_LOG, "MERA(%d): CPM frame allocated\n", __LINE__); + + return (pf_drv_frame_t *)frame; +} + +int pf_mera_cpm_start (pnet_t * net, pf_drv_frame_t * drv_frame) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + + if (handle == NULL || frame == NULL || frame->config.p_iocr == NULL) + { + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): Failed to start RTE - parameter error\n", + __LINE__); + return -1; + } + + if (pf_mera_cpm_config (handle, frame) != 0) + { + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): Failed to start RTE - data config error\n", + __LINE__); + return -1; + } + + return pf_mera_cpm_start_rte (handle, frame); +} + +int pf_mera_cpm_activate_rte_dht (pf_drv_frame_t * drv_frame) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + mera_ob_rtp_conf_t ob_rtp_config; + + if (mera_ob_rtp_conf_get (frame->mera_lib, frame->rtp_id, &ob_rtp_config) != 0) + { + LOG_ERROR (PF_CPM_LOG, "MERA(%d): Failed get CPM RTE config\n", __LINE__); + return -1; + } + + /* Configure CPM data hold timeout. + * RTP and group will stop when configured timeout expires. + * Default values will be applied to all DGs on + * writes triggered when group is stopped. + */ + + ob_rtp_config.time.offset = MERA_TIME_OFFSET_NONE; + ob_rtp_config.time.interval = + frame->config.p_iocr->cpm.control_interval * 1000; + ob_rtp_config.time_cnt = frame->config.p_iocr->cpm.data_hold_factor; + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): Reconfigure ob rtp dht timing =%uns cnt=%d\n", + __LINE__, + ob_rtp_config.time.interval, + ob_rtp_config.time_cnt); + + if (mera_ob_rtp_conf_set (frame->mera_lib, frame->rtp_id, &ob_rtp_config) != 0) + { + LOG_ERROR (PF_CPM_LOG, "MERA(%d): Failed set CPM RTE config\n", __LINE__); + return -1; + } + + return 0; +} + +int pf_mera_cpm_free (pnet_t * net, pf_drv_frame_t * drv_frame) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + LOG_DEBUG (PF_MERA_LOG, "MERA(%d): Free CPM frame\n", __LINE__); + if (frame != NULL) + { + pf_mera_vcam_id_free (handle, frame->vcam_id); + pf_mera_vlan_index_free (handle, frame->vlan_idx); + pf_mera_rtp_id_free (handle, frame->rtp_id); + pf_mera_wal_id_free (handle, frame->ob_wal_id); + pf_sram_frame_free (frame->sram_frame_address); + pf_mera_free_frame (handle, frame); + return 0; + } + LOG_ERROR (PF_MERA_LOG, "MERA(%d): Failed to free CPM frame\n", __LINE__); + return -1; +} + +int pf_mera_cpm_stop (pnet_t * net, pf_drv_frame_t * drv_frame) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + + LOG_DEBUG (PF_PPM_LOG, "MERA(%d): Stop CPM\n", __LINE__); + + mera_ob_wal_rel (handle->mera_lib, frame->ob_wal_id); + + if (mera_ob_flush (handle->mera_lib) != 0) + { + LOG_ERROR (PF_CPM_LOG, "MERA(%d): Failed to flush rtp\n", __LINE__); + } + if (pf_mera_delete_vcam_rule (frame->vcam_id) != 0) + { + LOG_ERROR (PF_CPM_LOG, "MERA(%d): Failed to delete VCAM rule\n", __LINE__); + } + + /* Stop periodic debug info */ + pf_scheduler_remove_if_running (net, &handle->sched_handle); + return 0; +} + +/** + * Disable RTE write operations for an iodata object. + * Intended to be used while reading SRAM data groups. + * @param mera InOut: Ref to mera library instance + * @param frame In: CPM frame + * @param entry In: Iodata object configuration + */ +static void pf_mera_disable_ob_entry ( + struct mera_inst * mera, + pf_mera_frame_t * frame, + pf_rte_object_t * entry) +{ + mera_ob_dg_ctrl_t dg_ctrl; + + dg_ctrl.enable = false; + + if (mera_ob_dg_ctrl_set (mera, frame->rtp_id, entry->ob_dg_id, &dg_ctrl) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to disable outbound data group\n", + __LINE__); + } +} + +/** + * Enable RTE write operations for an iodata object. + * @param mera InOut: Ref to mera library instance + * @param frame In: CPM frame + * @param entry In: Iodata object configuration + */ +static void pf_mera_enable_ob_entry ( + struct mera_inst * mera, + pf_mera_frame_t * frame, + pf_rte_object_t * entry) +{ + mera_ob_dg_ctrl_t dg_ctrl; + + dg_ctrl.enable = true; + + if (mera_ob_dg_ctrl_set (mera, frame->rtp_id, entry->ob_dg_id, &dg_ctrl) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): Failed to enable outbound data group\n", + __LINE__); + } +} + +int pf_mera_cpm_read_data_and_iops ( + pnet_t * net, + pf_drv_frame_t * drv_frame, + uint16_t slot_nbr, + uint16_t subslot_nbr, + bool * p_new_flag, + uint8_t * p_data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + mera_ob_dg_status_t status; + pf_rte_object_t * entry; + + *p_iops = PNET_IOXS_BAD; + + if (!frame->active) + { + return -1; + } + + if (slot_nbr == PNET_SLOT_DAP_IDENT) + { + return 0; + } + + if (data_len == 0 || iops_len == 0) + { + return 0; + } + + entry = get_rte_object (frame, slot_nbr, subslot_nbr); + + switch (entry->io_interface_type) + { + case MERA_IO_INTF_SRAM: + case MERA_IO_INTF_QSPI: + if ( + mera_ob_dg_status_get ( + handle->mera_lib, + frame->rtp_id, + entry->ob_dg_id, + &status) != 0) + { + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] Failed to read data group status " + "(output)\n", + __LINE__, + slot_nbr, + subslot_nbr); + return -1; + } + + if (status.valid_chk) + { + LOG_DEBUG ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] IOPS BAD, Invalid output data\n", + __LINE__, + slot_nbr, + subslot_nbr); + } + else + { + *p_iops = PNET_IOXS_GOOD; + } + + pf_mera_disable_ob_entry (handle->mera_lib, frame, entry); + + if (pf_sram_read (entry->sram_address, p_data, data_len) != 0) + { + pf_mera_enable_ob_entry (handle->mera_lib, frame, entry); + + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] Failed to read SRAM (output) \n", + __LINE__, + slot_nbr, + subslot_nbr); + return -1; + } + + pf_mera_enable_ob_entry (handle->mera_lib, frame, entry); + + return 0; + break; + + default: + LOG_ERROR ( + PF_MERA_LOG, + "MERA(%d): [%u,%u] unsupported interface type\n", + __LINE__, + slot_nbr, + subslot_nbr); + return -1; + } + return 0; +} + +int pf_mera_cpm_read_iocs ( + pnet_t * net, + pf_drv_frame_t * frame, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_iocs, + uint8_t iocs_len) +{ + /* TODO - Figure out how this can be read from frame memory */ + /* this is implemented in pf_mera_cpm_read_data_and_iops. + review this and figure out best implementation */ + if (iocs_len > 0) + { + *p_iocs = PNET_IOXS_GOOD; + } + return 0; +} + +int pf_mera_poll_cpm_events ( + pnet_t * net, + pf_drv_frame_t * frame, + pf_mera_event_t * evt) +{ + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + mera_event_t mera_event; + + if (net == NULL || frame == NULL || evt == NULL) + { + return 0; + } + + memset (evt, 0, sizeof (pf_mera_event_t)); + + if (mera_event_poll (handle->mera_lib, (mera_event_t *)&mera_event) == 0) + { + if ((MERA_EVENT_RTP_STATE_STOPPED & mera_event) != 0) + { + LOG_INFO (PF_MERA_LOG, "MERA(%d): CPM RTP stopped event\n", __LINE__); + evt->stopped = true; + } + if ((MERA_EVENT_PN_DS_MISMATCH & mera_event) != 0) + { + evt->data_status_mismatch = true; + } + if ((MERA_EVENT_DG_INVALID & mera_event) != 0) + { + evt->invalid_dg = true; + } + return 0; + } + return -1; +} + +int pf_mera_cpm_read_data_status ( + pf_drv_frame_t * drv_frame, + uint8_t * p_data_status) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + mera_ob_rtp_status_t status; + + if (mera_ob_rtp_status_get (frame->mera_lib, frame->rtp_id, &status) == 0) + { + if (status.pn_ds_chk == 1) + { + /* RTE has a detected a data status mismatch. + Report RTE data status + */ + *p_data_status = status.pn_ds; + } + else + { + /* Data status ok - report checked data status */ + *p_data_status = PF_MERA_DATA_STATUS_MASK; + } + + return 0; + } + + return -1; +} + +int pf_mera_cpm_read_rx_count (pf_drv_frame_t * drv_frame, uint64_t * rx_count) +{ + pf_mera_frame_t * frame = (pf_mera_frame_t *)drv_frame; + mera_ob_rtp_counters_t counters; + + if (mera_ob_rtp_counters_get (frame->mera_lib, frame->rtp_id, &counters) == 0) + { + if (counters.rx_0 >= frame->rx_count) + { + *rx_count = counters.rx_0 - frame->rx_count; + } + else + { + /* Wrap around */ + *rx_count = counters.rx_0 + (UINT64_MAX - counters.rx_0); + } + frame->rx_count = counters.rx_0; + return 0; + } + + *rx_count = 0; + return -1; +} + +void pf_mera_show (pnet_t * net, bool cpm, bool ppm) +{ + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + mera_debug_info_t info; + + info.group = MERA_DEBUG_GROUP_GEN; + info.rtp_id = 0; /* All RTP IDs */ + info.full = false; /* Only active IDs */ + info.clear = true; + + if (cpm && ppm) + { + info.group = MERA_DEBUG_GROUP_ALL; + } + else if (cpm) + { + info.group = MERA_DEBUG_GROUP_OB; + } + else if (ppm) + { + info.group = MERA_DEBUG_GROUP_IB; + } + + printf ("RTE Status:\n"); + mera_debug_info_print (handle->mera_lib, pf_mera_printf_cb, &info); +} + +int pf_mera_get_port_from_remote_mac ( + pnet_ethaddr_t * mac_addr, + uint16_t * p_port) +{ + int ret = -1; + char cmd[128] = {0}; + char result[100] = {0}; + FILE * fd = NULL; + char mac_addr_str[PF_MERA_SCRIPT_ARG_LEN] = {0}; + + *p_port = 0; + + snprintf ( + mac_addr_str, + sizeof (mac_addr_str), + "%02x:%02x:%02x:%02x:%02x:%02x", + mac_addr->addr[0], + mac_addr->addr[1], + mac_addr->addr[2], + mac_addr->addr[3], + mac_addr->addr[4], + mac_addr->addr[5]); + + snprintf ( + cmd, + sizeof (cmd), + "brctl showmacs br0 | grep %s | xargs | cut -c1-1", + mac_addr_str); + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): Use brctl command to get active port: \"%s\"\n", + __LINE__, + cmd); + + fd = popen (cmd, "r"); + if (fd != NULL) + { + if (fgets (result, sizeof (result), fd) != NULL) + { + *p_port = atoi (result); + *p_port = *p_port - 1; + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): Found mac on bridge interface %u, using port index " + "%u\n", + __LINE__, + *p_port + 1, + *p_port); + ret = 0; + } + else + { + LOG_FATAL ( + PF_CPM_LOG, + "MERA(%d): Failed to find mac on bridge, using port index %u\n", + __LINE__, + *p_port); + } + pclose (fd); + } + + return ret; +} + +/* Public function in P-Net API + * For iodata objects not mapped to QSPI this operation + * is not used. Instead the iodata is automatically mapped + * to SRAM by the P-Net stack (this file). + * + * This config should be part of frame, but there is + * no suitable application event at which the configuration + * can be passed when CPM/PPM are created but not yet started. + */ +int pnet_mera_input_set_rte_config ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + const pnet_mera_rte_data_cfg_t * rte_data_cfg) +{ + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + LOG_DEBUG ( + PF_PPM_LOG, + "MERA(%d): [%u,%u] Adding RTE configuration " + "%s at address 0x%x (inbound / iodata input)\n", + __LINE__, + slot, + subslot, + pf_mera_rte_data_type_to_str (rte_data_cfg->type), + rte_data_cfg->address); + + handle->rte_data_cfg[slot][get_subslot_index (subslot)].valid = true; + handle->rte_data_cfg[slot][get_subslot_index (subslot)].cfg = *rte_data_cfg; + + return 0; +} + +int pnet_mera_output_set_rte_config ( + pnet_t * net, + uint32_t api, + uint16_t slot, + uint16_t subslot, + const pnet_mera_rte_data_cfg_t * rte_data_cfg) +{ + pf_mera_t * handle = (pf_mera_t *)net->hwo_drv; + + LOG_DEBUG ( + PF_CPM_LOG, + "MERA(%d): [%u,%u] Adding RTE configuration " + "%s at address 0x%x (outbound / iodata output)\n", + __LINE__, + slot, + subslot, + pf_mera_rte_data_type_to_str (rte_data_cfg->type), + rte_data_cfg->address); + + handle->rte_data_cfg[slot][get_subslot_index (subslot)].valid = true; + handle->rte_data_cfg[slot][get_subslot_index (subslot)].cfg = *rte_data_cfg; + + return 0; +} diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.h b/profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.h new file mode 100644 index 0000000..3f226ab --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_lan9662_mera.h @@ -0,0 +1,335 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_MERA_H +#define PF_MERA_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* PPM/CPM frame configuration. + * The LAN9662 RTE configuration is generated from this. + */ +typedef struct pf_mera_frame_cfg +{ + pf_iocr_t * p_iocr; /* IO-data configuration. From active AR. */ + uint16_t port; /* RTE Interface mapping */ + pnet_ethaddr_t main_mac_addr; /* MAC address for management port */ +} pf_mera_frame_cfg_t; + +typedef struct pf_mera_event +{ + bool stopped; + bool data_status_mismatch; + bool invalid_dg; +} pf_mera_event_t; + +/** + * Allocate a mera PPM frame and its RTE resources. + * @param net InOut: The p-net stack instance + * @param cfg In: PPM configuration + * @return Reference to mera frame, NULL on error. + */ +pf_drv_frame_t * pf_mera_ppm_alloc ( + pnet_t * net, + const pf_mera_frame_cfg_t * cfg); + +/** + * Free a mera PPM frame and its RTE resources. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @return 0 on success. -1 on error. + */ +int pf_mera_ppm_free (pnet_t * net, pf_drv_frame_t * frame); + +/** + * Start cyclic data sender / provider. + * Calling this operation loads and starts the RTE + * configuration defined by the frame. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @return 0 on success. -1 on error. + */ +int pf_mera_ppm_start (pnet_t * net, pf_drv_frame_t * frame); + +/** + * Start cyclic data sender / provider. + * Calling this operation stops the RTE + * configuration defined by the frame. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @return 0 on success. -1 on error. + */ +int pf_mera_ppm_stop (pnet_t * net, pf_drv_frame_t * frame); + +/** + * Update value of an IO object. + * Only allowed for iodata objects mapped to SRAM. + * + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera PPM frame + * @param p_iodata In: Iodata object + * @param data In: Input data + * @param len In: Input data length + * @return 0 on success, -1 on error; + */ +int pf_mera_ppm_write_input ( + pnet_t * net, + pf_drv_frame_t * frame, + const pf_iodata_object_t * p_iodata, + const uint8_t * data, + uint32_t len); + +/** + * Update provider status of an IO object. + * Only allowed for iodata objects mapped to SRAM. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera PPM frame + * @param p_iodata In: Iodata object + * @param iops In: Provider status + * @param len In: Provider status length + * @return 0 on success, -1 on error; + */ +int pf_mera_ppm_write_iops ( + pnet_t * net, + pf_drv_frame_t * frame, + const pf_iodata_object_t * p_iodata, + const uint8_t * iops, + uint8_t len); + +/** + * Update consumer status of an IO object. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera PPM frame + * @param p_iodata In: Iodata object + * @param iocs In: Consumer status + * @param len In: Consumer status length + * @return 0 on success, -1 on error; + */ +int pf_mera_ppm_write_iocs ( + pnet_t * net, + pf_drv_frame_t * frame, + const pf_iodata_object_t * p_iodata, + const uint8_t * iops, + uint8_t len); + +/** + * Update PPM frame data status. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera PPM frame + * @param data_status In: Data status + * @return 0 on success, -1 on error; + */ +int pf_mera_ppm_write_data_status ( + pnet_t * net, + pf_drv_frame_t * frame, + uint8_t data_status); + +/** + * Retrieve the data and IOPS for a sub-module. + * Note that this is not from the PLC, but previously set by the application. + * @param net InOut: The p-net stack instance + * @param api_id In: The API id. + * @param p_iodata In: Iodata object + * @param p_data Out: The application data. + * @param p_data_len In: The length of the p_data buffer. + * @param p_iops Out: The IOPS of the application data. + * @param p_iops_len In: Size of buffer at p_iops. + * @return 0 if the input data and IOPS could e retrieved. + * -1 if an error occurred. + */ +int pf_mera_ppm_read_data_and_iops ( + pnet_t * net, + pf_drv_frame_t * frame, + const pf_iodata_object_t * p_iodata, + uint8_t * p_data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len); + +/** + * Allocate a mera PPM frame and its RTE resources. + * @param net InOut: The p-net stack instance + * @param cfg In: CPM configuration + * @return Reference to mera frame, NULL on error. + */ +pf_drv_frame_t * pf_mera_cpm_alloc ( + pnet_t * net, + const pf_mera_frame_cfg_t * cfg); + +/** + * Free a mera CMP frame and its RTE resources. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @return 0 on success. -1 on error. + */ +int pf_mera_cpm_free (pnet_t * net, pf_drv_frame_t * frame); + +/** + * Start cyclic data receiver / consumer. + * Calling this operation loads and starts the RTE + * configuration defined by the frame. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @return 0 on success. -1 on error. + */ +int pf_mera_cpm_start (pnet_t * net, pf_drv_frame_t * frame); + +/** + * Start cyclic data receiver / consumer. + * Calling this operation stops the RTE + * configuration defined by the frame. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @return 0 on success. -1 on error. + */ +int pf_mera_cpm_stop (pnet_t * net, pf_drv_frame_t * frame); + +/** + * Retrieve latest sub-slot data and IOPS received from the controller. + * This function may be called to retrieve the latest data and IOPS values + * of a sub-slot sent from the controller. + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @param slot In: The slot. + * @param subslot In: The sub-slot. + * @param p_new_flag InOut: Not updated by this function + * @param p_data Out: The received data. + * @param data_len In: Size of data. + * @param p_iops Out: The controller provider status (IOPS). + * See pnet_ioxs_values_t + * @param iops_len In: Size of IOPS + * @return 0 if a sub-module data and IOPS is retrieved. + * -1 if an error occurred. + */ +int pf_mera_cpm_read_data_and_iops ( + pnet_t * net, + pf_drv_frame_t * frame, + uint16_t slot, + uint16_t subslot, + bool * p_new_flag, + uint8_t * p_data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len); + +/** + * Fetch the controller consumer status of one sub-slot. + * + * This function may be called to retrieve the IOCS value of a sub-slot + * sent from the controller. + * + * TODO - Always returns hardcoded GOOD + * + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @param slot_nbr In: The slot. + * @param subslot_nbr In: The sub-slot. + * @param p_iocs Out: The controller consumer status. + * See pnet_ioxs_values_t + * @param iocs_len In: The IOCS length + * @return 0 if a sub-module IOCS was set. + * -1 if an error occurred. + */ +int pf_mera_cpm_read_iocs ( + pnet_t * net, + pf_drv_frame_t * frame, + uint16_t slot_nbr, + uint16_t subslot_nbr, + uint8_t * p_iocs, + uint8_t iocs_len); + +/** + * Get mera CPM events + * @param net InOut: The p-net stack instance + * @param frame InOut: Mera frame + * @param p_data_status Out: CPM Events. + * @return 0 if events was successfully retrieved + * -1 if an error occurred. + */ +int pf_mera_poll_cpm_events ( + pnet_t * net, + pf_drv_frame_t * frame, + pf_mera_event_t * event); + +/** + * Get the data status of a CPM connection. + * @param frame InOut: Mera frame + * @param p_data_status Out: The CPM data status. + * @return 0 if data_status was successfully retrieved + * -1 if an error occurred. + */ +int pf_mera_cpm_read_data_status ( + pf_drv_frame_t * frame, + uint8_t * p_data_status); + +/** + * Read the number of received frames. Calling this operation + * resets the counter and the returned value is the number of + * received frame since last call to this function. + * @param frame InOut: Mera frame + * @param rx_count Out: Number of received frames since last call. + * @return 0 if frame counter was successfully read + * -1 if an error occurred. + */ +int pf_mera_cpm_read_rx_count (pf_drv_frame_t * frame, uint64_t * rx_count); + +/** + * Enable the RTE frame received timeout. + * A RTE stopped event will occur if no io data frames are + * received within the DHT period. + * + * @param frame InOut: CPM frame + * @return 0 if RTP ID was successfully updated + * -1 if an error occurred. + */ +int pf_mera_cpm_activate_rte_dht (pf_drv_frame_t * drv_frame); + +/** + * Get active status for a frame. A frame is active + * if the corresponding RTE configuration is running. + * @param frame In: PPM or CPM frame + * @return true if frame is active + * false in not + */ +bool pf_mera_is_active_frame (pf_drv_frame_t * frame); + +/** + * Show the status of the pf_mera component + * @param net InOut: The p-net stack instance + * @param cpm In: Show CPM info + * @param ppm In: Show PPM info + */ +void pf_mera_show (pnet_t * net, bool cpm, bool ppm); + +/** + * Utility for finding the RTE port on which a + * mac address is found when a network bridge is used. + * If no bridge is configured operation fails and *p_port + * is set to 0. + * @param mac_address In: Controller mac address + * @param p_port Out: RTE port on which controller is found + * @return 0 on success, -1 on error + */ +int pf_mera_get_port_from_remote_mac ( + pnet_ethaddr_t * mac_address, + uint16_t * p_port); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_MERA_H */ diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.c b/profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.c new file mode 100644 index 0000000..66e4c43 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.c @@ -0,0 +1,129 @@ +// Copyright (c) 2004-2020 Microchip Technology Inc. and its subsidiaries. +// SPDX-License-Identifier: MIT + +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * + ********************************************************************/ + +/** + * Mera lib log callbacks adapted for P-Net. + * Based on code in mera demo. + */ + +#include "pf_includes.h" + +#include +#include +#include +#include +#include + +#include +#include +#include "microchip/ethernet/rte/api.h" + +#define TRACE_GROUP_DEFAULT 0 +#define TRACE_GROUP_CNT 1 + +typedef struct +{ + const char * name; + mera_trace_level_t level; +} trace_group_t; + +static trace_group_t trace_groups[] = { + {.name = "default", .level = MERA_TRACE_LEVEL_NOISE}, + {.name = "ib", .level = MERA_TRACE_LEVEL_NOISE}, + {.name = "ob", .level = MERA_TRACE_LEVEL_NOISE}, +}; + +static void printf_trace_head ( + const char * mname, + const char * gname, + const mera_trace_level_t level, + const char * file, + const int line, + const char * function, + const char * lcont) +{ + struct timeval tv; + int h, m, s; + const char *p, *base_name = file; + + for (p = file; *p != 0; p++) + { + if (*p == '/' || *p == '\\') + { + base_name = (p + 1); + } + } + + (void)gettimeofday (&tv, NULL); + h = (tv.tv_sec / 3600 % 24); + m = (tv.tv_sec / 60 % 60); + s = (tv.tv_sec % 60); + printf ( + "%u:%02u:%02u:%05lu %s/%s/%s %s(%u) %s%s", + h, + m, + s, + tv.tv_usec, + mname, + gname, + level == MERA_TRACE_LEVEL_ERROR + ? "error" + : level == MERA_TRACE_LEVEL_INFO + ? "info" + : level == MERA_TRACE_LEVEL_DEBUG + ? "debug" + : level == MERA_TRACE_LEVEL_NOISE ? "noise" : "?", + base_name, + line, + function, + lcont); +} + +static void mera_printf_trace_head ( + const mera_trace_group_t group, + const mera_trace_level_t level, + const char * file, + const int line, + const char * function, + const char * lcont) +{ + printf_trace_head ( + "p-net", + (int)group < (int)NELEMENTS (trace_groups) ? trace_groups[group].name + : "?", + level, + file, + line, + function, + lcont); +} + +void pf_mera_callout_trace_printf ( + const mera_trace_group_t group, + const mera_trace_level_t level, + const char * file, + const int line, + const char * function, + const char * format, + ...) +{ + va_list args; + + mera_printf_trace_head (group, level, file, line, function, ": "); + va_start (args, format); + vprintf (format, args); + va_end (args); + printf ("\n"); + fflush (stdout); +} diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.h b/profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.h new file mode 100644 index 0000000..47fffe8 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_mera_trace.h @@ -0,0 +1,38 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_MERA_TRACE_H +#define PF_MERA_TRACE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "microchip/ethernet/rte/api.h" + +void pf_mera_callout_trace_printf ( + const mera_trace_group_t group, + const mera_trace_level_t level, + const char * file, + const int line, + const char * function, + const char * format, + ...); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_MERA_TRACE_H */ diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_ppm_driver_lan9662.c b/profinet_stack/p-net/src/drivers/lan9662/src/pf_ppm_driver_lan9662.c new file mode 100644 index 0000000..85919be --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_ppm_driver_lan9662.c @@ -0,0 +1,328 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pf_includes.h" +#include "pf_lan9662_mera.h" + +#include +#include + +/** + * @file + * @brief PPM driver for LAN9662 + * + */ + +int pf_ppm_drv_lan9662_create (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + pf_mera_frame_cfg_t mera_ppm_cfg = {0}; + + LOG_DEBUG ( + PF_PPM_LOG, + "PPM_DRV_MERA(%d): Allocate RTE frame (inbound data / tx frames) for " + "AREP %u " + "CREP " + "%" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + mera_ppm_cfg.p_iocr = &p_ar->iocrs[crep]; + + pf_mera_get_port_from_remote_mac ( + &p_ar->ar_param.cm_initiator_mac_add, + &mera_ppm_cfg.port); + + memcpy ( + &mera_ppm_cfg.main_mac_addr, + &net->pf_interface.main_port.mac_address, + sizeof (pnet_ethaddr_t)); + + p_ar->iocrs[crep].ppm.frame = pf_mera_ppm_alloc (net, &mera_ppm_cfg); + + if (p_ar->iocrs[crep].ppm.frame == NULL) + { + LOG_ERROR ( + PF_PPM_LOG, + "PPM_DRV_MERA(%d): Frame allocation failed\n", + __LINE__); + return -1; + } + return 0; +} + +int pf_ppm_drv_lan9662_activate_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + LOG_DEBUG ( + PF_PPM_LOG, + "PPM_DRV_MERA(%d): Activate RTE (inbound data / tx frames) for AREP %u " + "CREP " + "%" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + if (p_ar->iocrs[crep].ppm.frame == NULL) + { + LOG_ERROR ( + PF_PPM_LOG, + "PPM_DRV_MERA(%d): No mera frame allocated\n", + __LINE__); + return -1; + } + + return pf_mera_ppm_start (net, p_ar->iocrs[crep].ppm.frame); +} + +/** + * @internal + * Close RTE resources used in PPM frame. + * @param net InOut: The p-net stack instance + * @param arg In: PPM frame + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +static void pf_ppm_drv_lan9662_delayed_close ( + pnet_t * net, + void * arg, + uint32_t current_time) +{ + pf_drv_frame_t * frame = (pf_drv_frame_t *)arg; + pf_mera_ppm_stop (net, frame); + pf_mera_ppm_free (net, frame); +} + +int pf_ppm_drv_lan9662_close_req (pnet_t * net, pf_ar_t * p_ar, uint32_t crep) +{ + LOG_DEBUG ( + PF_PPM_LOG, + "PPM(%d): Stop RTE for AREP %u CREP " + "%" PRIu32 "\n", + __LINE__, + p_ar->arep, + crep); + + /* Delay close of pf_mera resources + * Workaround for delayed alarm transmission. + * Close RTE resources after alarm is sent. + */ + pf_scheduler_init_handle (&p_ar->iocrs[crep].ppm.ci_timeout, "ppm_close"); + pf_scheduler_add ( + net, + 2, + pf_ppm_drv_lan9662_delayed_close, + p_ar->iocrs[crep].ppm.frame, + &p_ar->iocrs[crep].ppm.ci_timeout); + + return 0; +} + +/** + * Write the ppm frame buffer + * @param iocr InOut: Communication relation + * @param offset In: Buffer offset to write + * @param data In: Data source + * @param len In: Data length + * @return 0 on success, -1 on error + */ +static int pf_ppm_drv_lan9662_write_frame_buffer ( + pf_iocr_t * iocr, + uint32_t offset, + const uint8_t * data, + uint16_t len) +{ + memcpy (&iocr->ppm.buffer_data[offset], data, len); + return 0; +} +/** + * Read the ppm frame buffer + * @param iocr InOut: Communication relation + * @param offset In: Buffer offset to read + * @param data In: Data destination + * @param len In: Data length + * @return 0 on success, -1 on error + */ +static int pf_ppm_drv_lan9662_read_frame_buffer ( + pf_iocr_t * iocr, + uint32_t offset, + uint8_t * data, + uint16_t len) +{ + memcpy (data, &iocr->ppm.buffer_data[offset], len); + return 0; +} + +/** + * Update the PPM frame buffer and write + * data and iops to the active RTE frame. + */ +static int pf_ppm_drv_lan9662_write_data_and_iops ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + const uint8_t * data, + uint16_t len, + const uint8_t * iops, + uint8_t iops_len) +{ + int ret = 0; + + if (data != NULL) + { + pf_ppm_drv_lan9662_write_frame_buffer ( + iocr, + p_iodata->data_offset, + data, + len); + } + pf_ppm_drv_lan9662_write_frame_buffer ( + iocr, + p_iodata->iops_offset, + iops, + iops_len); + + if (pf_mera_is_active_frame (iocr->ppm.frame)) + { + /* Only initiate write to RTE if frame is active. + * Initialization values are still written to + * frame buffer and are used as initial SRAM values. + */ + + if (data != NULL) + { + ret = + pf_mera_ppm_write_input (net, iocr->ppm.frame, p_iodata, data, len); + } + + if (ret == 0) + { + ret = pf_mera_ppm_write_iops ( + net, + iocr->ppm.frame, + p_iodata, + iops, + iops_len); + } + } + + return ret; +} + +/** + * Read data and iops from SRAM + */ +static int pf_ppm_drv_lan9662_read_data_and_iops ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * data, + uint16_t data_len, + uint8_t * iops, + uint8_t iops_len) +{ + return pf_mera_ppm_read_data_and_iops ( + net, + iocr->ppm.frame, + p_iodata, + data, + data_len, + iops, + iops_len); +} + +/** + * Write consumer status for a subslot. + * Write the PPM frame buffer and the active RTE frame. + */ +static int pf_ppm_drv_lan9662_write_iocs ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + const uint8_t * iocs, + uint8_t iocs_len) +{ + return pf_ppm_drv_lan9662_write_frame_buffer ( + iocr, + p_iodata->iocs_offset, + iocs, + iocs_len); +} + +/** + * Read consumer status previously written + * using pf_ppm_drv_lan9662_write_iocs(). + * Value is fetched from the PPM frame buffer, + * not from the RTE. + */ +static int pf_ppm_drv_lan9662_read_iocs ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * iocs, + uint8_t iocs_len) +{ + + return pf_ppm_drv_lan9662_read_frame_buffer ( + iocr, + p_iodata->iocs_offset, + iocs, + iocs_len); +} + +static int pf_ppm_drv_lan9662_write_data_status ( + pnet_t * net, + pf_iocr_t * iocr, + uint8_t data_status) +{ + pf_ppm_drv_lan9662_write_frame_buffer ( + iocr, + iocr->ppm.data_status_offset, + &data_status, + sizeof (data_status)); + + return pf_mera_ppm_write_data_status (net, iocr->ppm.frame, data_status); +} + +/** + * Show the current state of the driver + * @param p_ppm In: PPM instance + */ +void pf_ppm_drv_lan9662_show (const pf_ppm_t * p_ppm) +{ + printf ("Not implemented: pf_ppm_drv_lan9662_show()\n"); +} + +int pf_driver_ppm_init (pnet_t * net) +{ + static const pf_ppm_driver_t drv = { + .create = pf_ppm_drv_lan9662_create, + .activate_req = pf_ppm_drv_lan9662_activate_req, + .close_req = pf_ppm_drv_lan9662_close_req, + .write_data_and_iops = pf_ppm_drv_lan9662_write_data_and_iops, + .read_data_and_iops = pf_ppm_drv_lan9662_read_data_and_iops, + .write_iocs = pf_ppm_drv_lan9662_write_iocs, + .read_iocs = pf_ppm_drv_lan9662_read_iocs, + .write_data_status = pf_ppm_drv_lan9662_write_data_status, + .show = pf_ppm_drv_lan9662_show}; + + net->ppm_drv = &drv; + + LOG_INFO ( + PF_PPM_LOG, + "PPM_DRV(%d): LAN9662 PPM driver installed\n", + __LINE__); + return 0; +} diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.c b/profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.c new file mode 100644 index 0000000..ae3ba82 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.c @@ -0,0 +1,182 @@ +// Copyright (c) 2004-2020 Microchip Technology Inc. and its subsidiaries. +// SPDX-License-Identifier: MIT + +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * + ********************************************************************/ + +/** + * RTE UIO driver adapted for P-Net. + * Implements register read and write operations. + * Based on code in mera demo. + */ + +#include +#include +#include +#include +#include /* I2C support */ +#include /* I2C support */ +#include +#include +#include +#include +#include +#include +#include + +#include "microchip/ethernet/rte/api.h" +#include "pf_includes.h" + +#define T_D(...) LOG_DEBUG (PNET_LOG, __VA_ARGS__) +#define T_I(...) LOG_INFO (PNET_LOG, __VA_ARGS__) +#define T_E(...) LOG_ERROR (PNET_LOG, __VA_ARGS__) + +#if (__BYTE_ORDER == __BIG_ENDIAN) +#define PCIE_HOST_CVT(x) \ + __builtin_bswap32 ((x)) /* PCIe is LE - we're BE, so swap */ +#else +#define PCIE_HOST_CVT(x) (x) /* We're LE already */ +#endif + +volatile uint32_t * base_mem = NULL; + +int pf_rte_uio_reg_read ( + struct mera_inst * inst, + const uintptr_t addr, + uint32_t * data) +{ + if (base_mem != NULL) + { + *data = PCIE_HOST_CVT (base_mem[addr]); + return 0; + } + return -1; +} + +int pf_rte_uio_reg_write ( + struct mera_inst * inst, + const uintptr_t addr, + const uint32_t data) +{ + if (base_mem != NULL) + { + base_mem[addr] = PCIE_HOST_CVT (data); + return 0; + } + return -1; +} + +int pf_rte_uio_init (void) +{ + const char * driver = "mscc_switch"; + const char * top = "/sys/class/uio"; + DIR * dir; + struct dirent * dent; + char fn[PATH_MAX], devname[128]; + FILE * fp; + char iodev[512]; + size_t mapsize; + int dev_fd, len, rc = -1; + + if (!(dir = opendir (top))) + { + T_E ("operdir(%s) failed\n", top); + return rc; + } + + while ((dent = readdir (dir)) != NULL) + { + if (dent->d_name[0] == '.') + { + continue; + } + + snprintf (fn, sizeof (fn), "%s/%s/name", top, dent->d_name); + fp = fopen (fn, "r"); + if (!fp) + { + T_E ("UIO: Failed to open: %s\n", fn); + continue; + } + + const char * rrd = fgets (devname, sizeof (devname), fp); + fclose (fp); + + if (!rrd) + { + T_E ("UIO: Failed to read: %s\n", fn); + continue; + } + + len = strlen (devname); + if (len > 0 && devname[len - 1] == '\n') + { + devname[len - 1] = '\0'; + } + T_D ("UIO: %s -> %s\n", fn, devname); + if ( + !strstr (devname, "mscc_switch") && + !strstr (devname, "vcoreiii_switch") && + !strstr (devname, "lan966x_uio")) + { + continue; + } + + snprintf (iodev, sizeof (iodev), "/dev/%s", dent->d_name); + snprintf (fn, sizeof (fn), "%s/%s/maps/map0/size", top, dent->d_name); + fp = fopen (fn, "r"); + if (!fp) + { + continue; + } + + if (fscanf (fp, "%zi", &mapsize)) + { + fclose (fp); + rc = 0; + T_I ("Using UIO device: %s\n", devname); + break; + } + fclose (fp); + } + closedir (dir); + + if (rc < 0) + { + T_E ("No suitable UIO device found!\n"); + return rc; + } + + /* Open the UIO device file */ + T_D ("Using UIO, found '%s' driver at %s\n", driver, iodev); + dev_fd = open (iodev, O_RDWR); + if (dev_fd < 1) + { + T_E ("open(%s) failed\n", iodev); + rc = -1; + } + else + { + /* mmap the UIO device */ + base_mem = + mmap (NULL, mapsize, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, 0); + if (base_mem != MAP_FAILED) + { + T_D ("Mapped register memory @ %p\n", base_mem); + } + else + { + T_E ("mmap failed\n"); + rc = -1; + } + } + return rc; +} diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.h b/profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.h new file mode 100644 index 0000000..4d9c764 --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_rte_uio.h @@ -0,0 +1,59 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_UIO_H +#define PF_UIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Init RTE UIO driver + * The RTE driver provides access to RTE registers from user space. + * The register read and write operations are used by the MERA library. + * @return 0 on success, - 1 on error error + */ +int pf_rte_uio_init (void); + +/** + * Read RTE register + * @param inst In: MERA library instance + * @param addr In: Register address + * @param data In: Register value + * @return 0 on success, - 1 on error error + */ +int pf_rte_uio_reg_read ( + struct mera_inst * inst, + const uintptr_t addr, + uint32_t * data); + +/** + * Write RTE register + * @param inst In: MERA library instance + * @param addr In: Register address + * @param data InOut: Register value + * @return 0 on success, - 1 on error error + */ +int pf_rte_uio_reg_write ( + struct mera_inst * inst, + const uintptr_t addr, + const uint32_t data); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_RTE_UIO_H */ diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.c b/profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.c new file mode 100644 index 0000000..d1e678c --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.c @@ -0,0 +1,343 @@ +// Copyright (c) 2004-2020 Microchip Technology Inc. and its subsidiaries. +// SPDX-License-Identifier: MIT + +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * + ********************************************************************/ + +/** + * SRAM UIO driver adapted for P-Net. + * Implements read and write operations as well as + * a allocation/frame concept. + * Based on code in mera demo. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "microchip/ethernet/rte/api.h" +#include "pf_includes.h" + +#include "pf_sram_uio.h" + +#if (__BYTE_ORDER == __BIG_ENDIAN) +#define HOST_CVT(x) (x) +#else +#define HOST_CVT(x) __builtin_bswap32 ((x)) /* PCIe is LE, so swap */ +#endif + +/* Do not start on address 0 - 0 is used "NULL"*/ +#define PF_SRAM_AREA_START_ADDRESS 100 +#define PF_SRAM_FRAME_SIZE 1600 +#define PF_SRAM_MAX_FRAMES 4 +#define PF_SRAM_MAX_ADDRESS 0x1FFFF + +typedef union pf_uio_reg +{ + uint32_t val; + uint8_t data[4]; +} pf_uio_reg_t; + +typedef struct pf_sram_frame +{ + bool used; + uint16_t start_address; +} pf_sram_frame_t; + +typedef struct pf_sram_uio +{ + int dev_fd; + volatile uint32_t * base_mem; + pf_sram_frame_t frames[PF_SRAM_MAX_FRAMES]; +} pf_sram_uio_t; + +pf_sram_uio_t sram_uio = {0}; + +static int uio_init (pf_sram_uio_t * handle) +{ + const char * driver = "mscc_sram"; + const char * top = "/sys/class/uio"; + DIR * dir; + struct dirent * dent; + char fn[PATH_MAX], devname[128]; + FILE * fp; + char iodev[512]; + size_t mapsize; + int len, rc = -1; + + handle->dev_fd = -1; + + if (!(dir = opendir (top))) + { + LOG_ERROR (PF_PPM_LOG, "SRAM UIO - opendir(%s) failed", top); + return rc; + } + + while ((dent = readdir (dir)) != NULL) + { + if (dent->d_name[0] == '.') + { + continue; + } + + snprintf (fn, sizeof (fn), "%s/%s/name", top, dent->d_name); + fp = fopen (fn, "r"); + if (!fp) + { + LOG_ERROR (PF_PPM_LOG, "SRAM UIO - Failed to open: %s", fn); + continue; + } + + const char * rrd = fgets (devname, sizeof (devname), fp); + fclose (fp); + + if (!rrd) + { + LOG_ERROR (PF_PPM_LOG, "SRAM UIO - Failed to read: %s", fn); + continue; + } + + len = strlen (devname); + if (len > 0 && devname[len - 1] == '\n') + { + devname[len - 1] = '\0'; + } + + if (!strstr (devname, driver)) + { + continue; + } + + snprintf (iodev, sizeof (iodev), "/dev/%s", dent->d_name); + snprintf (fn, sizeof (fn), "%s/%s/maps/map0/size", top, dent->d_name); + fp = fopen (fn, "r"); + if (!fp) + { + continue; + } + + if (fscanf (fp, "%zi", &mapsize)) + { + fclose (fp); + rc = 0; + LOG_DEBUG (PF_PPM_LOG, "SRAM UIO - Using device: %s\n", devname); + break; + } + fclose (fp); + } + closedir (dir); + + if (rc < 0) + { + LOG_ERROR (PF_PPM_LOG, "SRAM UIO - No suitable UIO device found!\n"); + return rc; + } + + /* Open the UIO device file */ + LOG_INFO ( + PF_PPM_LOG, + "SRAM UIO - found '%s' driver at %s, size: %zu\n", + driver, + iodev, + mapsize); + handle->dev_fd = open (iodev, O_RDWR); + if (handle->dev_fd < 1) + { + LOG_INFO (PF_PPM_LOG, "SRAM UIO - open(%s) failed\n", iodev); + rc = -1; + } + else + { + /* mmap the UIO device */ + handle->base_mem = mmap ( + NULL, + mapsize, + PROT_READ | PROT_WRITE, + MAP_SHARED, + handle->dev_fd, + 0); + if (handle->base_mem != MAP_FAILED) + { + LOG_DEBUG ( + PF_PPM_LOG, + "SRAM UIO - Mapped register memory @ %p\n", + handle->base_mem); + } + else + { + LOG_ERROR (PF_PPM_LOG, "SRAM UIO - mmap failed\n"); + rc = -1; + } + } + return rc; +} + +int pf_sram_init (void) +{ + uint16_t i; + + for (i = 0; i < PF_SRAM_MAX_FRAMES; i++) + { + sram_uio.frames[i].used = false; + sram_uio.frames[i].start_address = + PF_SRAM_AREA_START_ADDRESS + i * PF_SRAM_FRAME_SIZE; + } + + if (uio_init (&sram_uio) != 0) + { + return -1; + } + + return 0; +} + +uint16_t pf_sram_frame_alloc (void) +{ + uint16_t i; + + for (i = 0; i < PF_SRAM_MAX_FRAMES; i++) + { + if (sram_uio.frames[i].used == false) + { + sram_uio.frames[i].used = true; + return sram_uio.frames[i].start_address; + } + } + + return PF_SRAM_INVALID_ADDRESS; +} + +void pf_sram_frame_free (uint16_t frame_start_address) +{ + uint16_t i; + + for (i = 0; i < PF_SRAM_MAX_FRAMES; i++) + { + if (sram_uio.frames[i].start_address == frame_start_address) + { + sram_uio.frames[i].used = false; + return; + } + } + + LOG_ERROR ( + PF_PPM_LOG, + "SRAM UIO - failed to free frame at address 0x%x\n", + frame_start_address); +} + +static int reg_check (uint32_t addr) +{ + if (addr >= PF_SRAM_MAX_ADDRESS) + { + LOG_ERROR (PF_PPM_LOG, "SRAM UIO - address range is 17 bits\n"); + return -1; + } + if (addr & 3) + { + LOG_ERROR (PF_PPM_LOG, "SRAM UIO - address must be 32-bit word aligned\n"); + return -1; + } + return 0; +} + +static int reg_read (uint32_t addr, uint32_t * val) +{ + if (reg_check (addr) < 0) + { + return -1; + } + *val = HOST_CVT (sram_uio.base_mem[addr / 4]); + return 0; +} + +static int reg_write (uint32_t addr, uint32_t val) +{ + if (reg_check (addr) < 0) + { + return -1; + } + sram_uio.base_mem[addr / 4] = HOST_CVT (val); + return 0; +} + +int pf_sram_write (uint16_t address, const uint8_t * data, uint16_t length) +{ + pf_uio_reg_t tmp; + uint32_t uio_reg_addr = address & 0xFFFFFFFC; + uint32_t offset; + uint32_t index = 0; + uint32_t i = 0; + + offset = address - uio_reg_addr; + while (index < length) + { + if (reg_read (uio_reg_addr, &tmp.val) != 0) + { + return -1; + } + + for (i = offset; i <= 3; i++) + { + if (index < length) + { + tmp.data[3 - i] = data[index++]; + } + } + + if (reg_write (uio_reg_addr, tmp.val) != 0) + { + return -1; + } + + uio_reg_addr += 4; + offset = 0; + } + return 0; +} + +int pf_sram_read (uint16_t address, uint8_t * data, uint16_t length) +{ + pf_uio_reg_t tmp; + uint32_t uio_reg_addr = address & 0xFFFFFFFC; + uint32_t offset; + uint32_t index = 0; + uint32_t i = 0; + + offset = address - uio_reg_addr; + while (index < length) + { + if (reg_read (uio_reg_addr, &tmp.val) != 0) + { + return -1; + } + + for (i = offset; i <= 3; i++) + { + if (index < length) + { + data[index++] = tmp.data[3 - i]; + } + } + + uio_reg_addr += 4; + offset = 0; + } + return 0; +} \ No newline at end of file diff --git a/profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.h b/profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.h new file mode 100644 index 0000000..7b8c55a --- /dev/null +++ b/profinet_stack/p-net/src/drivers/lan9662/src/pf_sram_uio.h @@ -0,0 +1,68 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PF_SRAM_UIO_H +#define PF_SRAM_UIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define PF_SRAM_INVALID_ADDRESS 0 + +/** + * Init SRAM UIO driver. + * The SRAM driver provides access to LAN9662 SRAM used by the RTE. + * The driver implements a frame concept for managing the SRAM + * as frames mapping to a inbound or outbound RTE configurations. + * @return 0 on success, - 1 on error error + */ +int pf_sram_init (void); + +/** + * Allocate SRAM frame + * @return Frame start address, PF_SRAM_INVALID_ADDRESS on error + */ +uint16_t pf_sram_frame_alloc (void); + +/** + * Free SRAM frame + * @param Frame start address + */ +void pf_sram_frame_free (uint16_t frame_start_address); + +/** + * Write SRAM + * @param address In: SRAM address to write + * @param data In: Data + * @param length In: Length of data + * @return 0 on success, -1 on error + */ +int pf_sram_write (uint16_t address, const uint8_t * data, uint16_t length); + +/** + * Read SRAM + * @param address In: SRAM address to read + * @param data Out: Data + * @param length In: Length of data + * @return 0 on success, -1 on error + */ +int pf_sram_read (uint16_t address, uint8_t * data, uint16_t length); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_PORT_H */ diff --git a/profinet_stack/p-net/src/pf_driver.h b/profinet_stack/p-net/src/pf_driver.h new file mode 100644 index 0000000..3615a0a --- /dev/null +++ b/profinet_stack/p-net/src/pf_driver.h @@ -0,0 +1,64 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Driver interface + */ + +#ifndef PF_DRIVER_H +#define PF_DRIVER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pf_includes.h" + +/** + * Initialize hardware offload driver + * Called during P-Net initialization if option + * PNET_OPTION_DRIVER_ENABLE is enabled. + * @param net InOut: The p-net stack instance + * @return 0 on success + * -1 on error + */ +int pf_driver_init (pnet_t * net); + +/** + * Install hardware offload ppm driver + * Called during P-Net initialization if option + * PNET_OPTION_DRIVER_ENABLE is enabled. + * + * @return 0 on success + * -1 on error + */ +int pf_driver_ppm_init (pnet_t * net); + +/** + * Install hardware offload cpm driver + * Called during P-Net initialization if option + * PNET_OPTION_DRIVER_ENABLE is enabled. + * + * @return 0 on success + * -1 on error + */ +int pf_driver_cpm_init (pnet_t * net); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_DRIVER_H*/ diff --git a/profinet_stack/p-net/src/pf_includes.h b/profinet_stack/p-net/src/pf_includes.h new file mode 100644 index 0000000..6b12deb --- /dev/null +++ b/profinet_stack/p-net/src/pf_includes.h @@ -0,0 +1,75 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief describe purpose of this file + */ + +#ifndef PF_INCLUDES_H +#define PF_INCLUDES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "options.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal.h" + +#include "pnet_api.h" +#include "pf_driver.h" +#include "pf_types.h" + +/* common */ +#include "pf_alarm.h" +#include "pf_bg_worker.h" +#include "pf_cpm.h" +#include "pf_cpm_driver_sw.h" +#include "pf_dcp.h" +#include "pf_eth.h" +#include "pf_file.h" +#include "pf_lldp.h" +#include "pf_ppm.h" +#include "pf_ppm_driver_sw.h" +#include "pf_ptcp.h" +#include "pf_scheduler.h" +#include "pf_snmp.h" +#include "pf_udp.h" + +/* device */ +#include "pf_cmdev.h" +#include "pf_cmdmc.h" +#include "pf_cmina.h" +#include "pf_cmio.h" +#include "pf_cmpbe.h" +#include "pf_cmrdr.h" +#include "pf_cmrpc.h" +#include "pf_cmrpc_helpers.h" +#include "pf_cmrs.h" +#include "pf_cmsm.h" +#include "pf_cmsu.h" +#include "pf_cmwrr.h" +#include "pf_diag.h" +#include "pf_fspm.h" +#include "pf_pdport.h" +#include "pf_port.h" + +#ifdef __cplusplus +} +#endif + +#endif /* PF_INCLUDES_H */ diff --git a/profinet_stack/p-net/src/pf_types.h b/profinet_stack/p-net/src/pf_types.h new file mode 100644 index 0000000..ef92264 --- /dev/null +++ b/profinet_stack/p-net/src/pf_types.h @@ -0,0 +1,3039 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Internal typedefs for pnet + */ + +#ifndef PF_TYPES_H +#define PF_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pf_lldp.h" +#include "pf_snmp.h" + +#if PNET_USE_ATOMICS +#include +#else +#define atomic_int uint32_t +#ifdef ATOMIC_VAR_INIT +#undef ATOMIC_VAR_INIT +#endif +#define ATOMIC_VAR_INIT(x) x + +#ifdef atomic_fetch_add +#undef atomic_fetch_add +#endif +static inline uint32_t atomic_fetch_add (atomic_int * p, uint32_t v) +{ + uint32_t prev = *p; + *p += v; + + return prev; +} +#ifdef atomic_fetch_sub +#undef atomic_fetch_sub +#endif +static inline uint32_t atomic_fetch_sub (atomic_int * p, uint32_t v) +{ + uint32_t prev = *p; + *p -= v; + + return prev; +} +#endif + +#define PF_RPC_SERVER_PORT 0x8894 /* PROFInet Context Manager */ +#define PF_UDP_UNICAST_PORT 0x8892 +#define PF_RPC_CCONTROL_EPHEMERAL_PORT 0xc001 + +#define PF_ALARM_NUMBER_OF_PRIORITY_LEVELS 2 /* High and low */ + +#define PF_FRAME_BUFFER_SIZE 1500 + +/** This should be smaller than PF_FRAME_BUFFER_SIZE with the maximum size of + * IP- and UDP headers, and some margin. Linux will fragment frames if this is + * larger than 1464. */ +#define PF_MAX_UDP_PAYLOAD_SIZE 1440 + +/** + * Timeout in milliseconds after which the CControl request is re-transmitted. + */ +#define PF_CCONTROL_TIMEOUT 2000 +#define PF_FRAG_TIMEOUT 2000 + +/** Time between Ethernet link status checks, in microseconds */ +#define PF_LINK_MONITOR_INTERVAL 100000 + +/** One neighbour shall be checked for PDPortDataCheck + * See Profinet 2.4 protocol, section 5.2.13.3 + */ +#define PF_CHECK_PEERS_PER_PORT 1 + +/** Including termination */ +#define PF_ALIAS_NAME_MAX_SIZE \ + (PNET_STATION_NAME_MAX_SIZE + PNET_PORT_NAME_MAX_SIZE) + +/************************* Diagnosis ******************************************/ + +#define PF_DIAG_QUALIFIED_SEVERITY_MASK ~0x00000007 +#define PF_DIAG_BIT_MAINTENANCE_REQUIRED BIT (0) +#define PF_DIAG_BIT_MAINTENANCE_DEMANDED BIT (1) + +/** Qualifier 31..27 */ +#define PF_DIAG_QUALIFIER_MASK_FAULT 0xF8000000 +/** Qualifier 26..17 */ +#define PF_DIAG_QUALIFIER_MASK_DEMANDED 0x07FE0000 +/** Qualifier 16..7 */ +#define PF_DIAG_QUALIFIER_MASK_REQUIRED 0x0001FF80 +/** Qualifier 6..3 */ +#define PF_DIAG_QUALIFIER_MASK_ADVICE 0x00000078 + +/* ch_properties channel size in bits + See also pnet_diag_ch_prop_type_values_t */ +#define PF_DIAG_CH_PROP_TYPE_MASK 0x00ff +#define PF_DIAG_CH_PROP_TYPE_POS 0 +#define PF_DIAG_CH_PROP_TYPE_GET(x) \ + (((X)&PF_DIAG_CH_PROP_TYPE_MASK) >> PF_DIAG_CH_PROP_TYPE_POS) +#define PF_DIAG_CH_PROP_TYPE_SET(x, v) \ + do \ + { \ + (x) &= ~PF_DIAG_CH_PROP_TYPE_MASK; \ + (x) |= (v) << PF_DIAG_CH_PROP_TYPE_POS; \ + } while (0) + +/* ch_properties channel group or individual channel + See also pnet_diag_ch_group_values_t */ +#define PF_DIAG_CH_PROP_ACC_MASK 0x0100 +#define PF_DIAG_CH_PROP_ACC_POS 8 +#define PF_DIAG_CH_PROP_ACC_GET(x) \ + (((x)&PF_DIAG_CH_PROP_ACC_MASK) >> PF_DIAG_CH_PROP_ACC_POS) +#define PF_DIAG_CH_PROP_ACC_SET(x, v) \ + do \ + { \ + (x) &= ~PF_DIAG_CH_PROP_ACC_MASK; \ + (x) |= (v) << PF_DIAG_CH_PROP_ACC_POS; \ + } while (0) + +/* ch_properties diagnosis severity + See also pnet_diag_ch_prop_maint_values_t */ +#define PF_DIAG_CH_PROP_MAINT_MASK 0x0600 +#define PF_DIAG_CH_PROP_MAINT_POS 9 +#define PF_DIAG_CH_PROP_MAINT_GET(x) \ + (((x)&PF_DIAG_CH_PROP_MAINT_MASK) >> PF_DIAG_CH_PROP_MAINT_POS) +#define PF_DIAG_CH_PROP_MAINT_SET(x, v) \ + do \ + { \ + (x) &= ~PF_DIAG_CH_PROP_MAINT_MASK; \ + (x) |= (v) << PF_DIAG_CH_PROP_MAINT_POS; \ + } while (0) + +/* ch_properties appears or disappears */ +#define PF_DIAG_CH_PROP_SPEC_MASK 0x1800 +#define PF_DIAG_CH_PROP_SPEC_POS 11 +#define PF_DIAG_CH_PROP_SPEC_GET(x) \ + (((x)&PF_DIAG_CH_PROP_SPEC_MASK) >> PF_DIAG_CH_PROP_SPEC_POS) +#define PF_DIAG_CH_PROP_SPEC_SET(x, v) \ + do \ + { \ + (x) &= ~PF_DIAG_CH_PROP_SPEC_MASK; \ + (x) |= (v) << PF_DIAG_CH_PROP_SPEC_POS; \ + } while (0) + +typedef enum pf_diag_ch_prop_spec_values +{ + PF_DIAG_CH_PROP_SPEC_ALL_DISAPPEARS = 0, + PF_DIAG_CH_PROP_SPEC_APPEARS = 1, + PF_DIAG_CH_PROP_SPEC_DISAPPEARS = 2, + PF_DIAG_CH_PROP_SPEC_DIS_OTHERS_REMAIN = 3, +} pf_diag_ch_prop_spec_values_t; + +/* ch_properties direction + See also pnet_diag_ch_prop_dir_values_t */ +#define PF_DIAG_CH_PROP_DIR_MASK 0xe000 +#define PF_DIAG_CH_PROP_DIR_POS 13 +#define PF_DIAG_CH_PROP_DIR_GET(x) \ + (((x)&PF_DIAG_CH_PROP_DIR_MASK) >> PF_DIAG_CH_PROP_DIR_POS) +#define PF_DIAG_CH_PROP_DIR_SET(x, v) \ + do \ + { \ + (x) &= ~PF_DIAG_CH_PROP_DIR_MASK; \ + (x) |= (v) << PF_DIAG_CH_PROP_DIR_POS; \ + } while (0) + +/*********************** RPC header ******************************************/ + +typedef enum pf_rpc_packet_type_values +{ + PF_RPC_PT_REQUEST = 0, //!< PF_RPC_PT_REQUEST + PF_RPC_PT_PING, //!< PF_RPC_PT_PING + PF_RPC_PT_RESPONSE, //!< PF_RPC_PT_RESPONSE + PF_RPC_PT_FAULT, //!< PF_RPC_PT_FAULT + PF_RPC_PT_WORKING, //!< PF_RPC_PT_WORKING + PF_RPC_PT_RESP_PING, + /* No Call - response to ping */ //!< PF_RPC_PT_RESP_PING + PF_RPC_PT_REJECT, //!< PF_RPC_PT_REJECT + PF_RPC_PT_ACK, //!< PF_RPC_PT_ACK + PF_RPC_PT_CL_CANCEL, //!< PF_RPC_PT_CL_CANCEL + PF_RPC_PT_FRAG_ACK, //!< PF_RPC_PT_FRAG_ACK + PF_RPC_PT_CANCEL_ACK, //!< PF_RPC_PT_CANCEL_ACK +} pf_rpc_packet_type_values_t; + +typedef enum pf_rpc_flags_bits +{ + PF_RPC_F_RESERVED_0 = 0, + PF_RPC_F_LAST_FRAGMENT, + PF_RPC_F_FRAGMENT, + PF_RPC_F_NO_FACK, + PF_RPC_F_MAYBE, + PF_RPC_F_IDEMPOTENT, + PF_RPC_F_BROADCAST, + PF_RPC_F_RESERVED_7, +} pf_rpc_flags_bits_t; + +typedef enum pf_rpc_flags2_bits +{ + PF_RPC_F2_CANCEL_PENDING = 1, +} pf_rpc_flags2_bits_t; + +typedef enum pf_dev_opnum_values +{ + PF_RPC_DEV_OPNUM_CONNECT = 0, + PF_RPC_DEV_OPNUM_RELEASE, + PF_RPC_DEV_OPNUM_READ, + PF_RPC_DEV_OPNUM_WRITE, + PF_RPC_DEV_OPNUM_CONTROL, + PF_RPC_DEV_OPNUM_READ_IMPLICIT, +} pf_dev_opnum_values_t; + +/* ToDo: The EPM is not yet implemented */ +typedef enum pf_epmapper_opnum_values +{ + PF_RPC_EPM_OPNUM_INSERT, + PF_RPC_EPM_OPNUM_DELETE, + PF_RPC_EPM_OPNUM_LOOKUP, /* Mandatory to support */ + PF_RPC_EPM_OPNUM_MAP, + PF_RPC_EPM_OPNUM_LOOKUP_HANDLE_FREE, /* Mandatory */ + PF_RPC_EPM_OPNUM_INP_OBJECT, + PF_RPC_EPM_OPNUM_MGMT_DELETE, +} pf_epmapper_opnum_values_t; + +/* PN-AL-protocol (Mar20) Table 725 */ +typedef enum pf_link_state_link +{ + PF_PD_LINK_STATE_LINK_RESERVED = 0, + PF_PD_LINK_STATE_LINK_UP = 1, + PF_PD_LINK_STATE_LINK_DOWN = 2, + PF_PD_LINK_STATE_LINK_TESTING = 3, + PF_PD_LINK_STATE_LINK_UNKNOWN = 4, + PF_PD_LINK_STATE_LINK_DORMANT = 5, + PF_PD_LINK_STATE_LINK_NOTPRESENT = 6, + PF_PD_LINK_STATE_LINK_LOWERLAYERDOWN = 7 +} pf_link_state_link_t; + +/* PN-AL-protocol (Mar20) Table 726 */ +typedef enum pf_link_state_port +{ + PF_PD_LINK_STATE_PORT_UNKNOWN = 0, + PF_PD_LINK_STATE_PORT_DISABLED = 1, + PF_PD_LINK_STATE_PORT_BLOCKING = 2, + PF_PD_LINK_STATE_PORT_LISTENING = 3, + PF_PD_LINK_STATE_PORT_LEARNING = 4, + PF_PD_LINK_STATE_PORT_FORWARDING = 5, + PF_PD_LINK_STATE_PORT_BROKEN = 6 +} pf_link_state_port_t; + +/* PN-AL-protocol (Mar20) Table 727 */ +typedef enum pf_mediatype_values +{ + PF_PD_MEDIATYPE_UNKNOWN = 0, + PF_PD_MEDIATYPE_COPPER, + PF_PD_MEDIATYPE_FIBER, + PF_PD_MEDIATYPE_RADIO +} pf_mediatype_values_t; + +typedef struct pf_rpc_flags +{ + bool last_fragment; + bool fragment; + bool no_fack; + bool maybe; + bool idempotent; + bool broadcast; +} pf_rpc_flags_t; + +typedef struct pf_rpc_flags2_t +{ + bool cancel_pending; +} pf_rpc_flags2_t; + +typedef struct pf_uuid +{ + uint32_t data1; + uint16_t data2; + uint16_t data3; + uint8_t data4[8]; +} pf_uuid_t; + +typedef struct pf_rpc_header +{ + uint8_t version; /* Allowed: 0x04 */ + uint8_t packet_type; /* rpc_packet_type_values_t */ + pf_rpc_flags_t flags; /* rpc_flags_bits */ + pf_rpc_flags2_t flags2; + bool is_big_endian; + uint8_t float_repr; /* Allowed: IEEE (0) */ + uint8_t reserved; + uint8_t serial_high; /* High octet of fragment number */ + pf_uuid_t object_uuid; /* Identifies IO device, IO controller or IO + supervisor */ + pf_uuid_t interface_uuid; /* Identify PNIO interface */ + pf_uuid_t activity_uuid; + uint32_t server_boot_time; + uint32_t interface_version; + uint32_t sequence_nmb; + uint16_t opnum; /* Dev: pf_dev_opnum_values_t/pf_epmapper_opnum_values */ + uint16_t interface_hint; + uint16_t activity_hint; + uint16_t length_of_body; /* AKA fragment_length */ + uint16_t fragment_nmb; + uint8_t auth_protocol; /* Allowed: 0 (zero) */ + uint8_t serial_low; /* Low octet of fragment number */ +} pf_rpc_header_t; + +typedef enum pf_write_req_error_type_values +{ + PF_WRT_ERROR_REMOTE_MISMATCH = 0x8001 +} pf_write_req_error_type_t; + +typedef enum pf_write_req_ext_error_type_values +{ + PF_WRT_ERROR_PEER_STATIONNAME_MISMATCH = 0x8000, + PF_WRT_ERROR_PEER_PORTNAME_MISMATCH = 0x8001, + PF_WRT_ERROR_NO_PEER_DETECTED = 0x8005 +} pf_write_req_ext_error_type_t; + +/* + * RPC and EPM implementation + * PN-AL-protocol (Mar20) Section 4.10.3 + */ + +#define PF_RPC_TOWER_REFERENTID 3 +#define PF_RPC_TOWER_FLOOR_COUNT 5 + +#define PF_RPC_FLOOR_VERSION_EPMv4 3 +#define PF_RPC_FLOOR_VERSION_NPR 2 +#define PF_RPC_FLOOR_VERSION_PNIO 1 +#define PF_RPC_FLOOR_VERSION_MINOR 0 + +#define PF_RPC_EPM_ANNOTATION_OFFSET 0 +#define PF_RPC_EPM_ANNOTATION_LENGTH 64 +#define PF_RPC_EPM_FLOOR_LENGTH 75 + +typedef enum pf_rpc_protocol_type +{ + PF_RPC_PROTOCOL_DOD_UDP = 0x08, + PF_RPC_PROTOCOL_DOD_IP = 0x09, + PF_RPC_PROTOCOL_CONNECTIONLESS = 0x0a, + PF_RPC_PROTOCOL_UUID = 0x0d +} pf_rpc_protocol_type_t; + +/* PN-AL-protocol (Mar20) Table 328 */ +typedef enum pf_rpc_inquiry_type +{ + PF_RPC_INQUIRY_READ_ALL_REGISTERED_INTERFACES = 0, /*mandatory*/ + PF_RPC_INQUIRY_READ_ALL_OBJECTS_FOR_ONE_INTERFACE = 1, /*optional*/ + PF_RPC_INQUIRY_READ_ALL_INTERFACES_INCLUDING_OBJECTS = 2, /*optional*/ + PF_RPC_INQUIRY_READ_ONE_INTERFACE_WITH_ONE_OBJECT = 3 /*optional*/ +} pf_rpc_inquiry_type_t; + +/* PN-AL-protocol (Mar20) Table 329 */ +typedef enum pf_rpc_error_value +{ + PF_RPC_OK = 0, + PF_RPC_NOT_REGISTERED = 0x16c9a0d6, /* Endpoint not registered */ +} pf_rpc_error_value_t; + +typedef enum +{ + PF_EPM_TYPE_NONE = 0, + PF_EPM_TYPE_EPMV4, + PF_EPM_TYPE_PNIO +} pf_rpc_epm_type_t; + +/* Described in DCP 1.1 Appendix A */ +typedef struct pf_rpc_uuid_type +{ + uint32_t time_low; + uint16_t time_mid; + uint16_t time_hi_and_version; + uint8_t clock_hi_and_reserved; + uint8_t clock_low; + uint8_t node[6]; +} pf_rpc_uuid_type_t; + +/* Used for definition of epm protocol tower floors 1 and 2. */ +typedef struct pf_floor_uuid_t +{ + uint8_t protocol_id; + pf_rpc_uuid_type_t uuid; + uint16_t version_major; + uint16_t version_minor; +} pf_floor_uuid_t; + +typedef struct pf_floor_3_t +{ + uint8_t protocol_id; + uint16_t version_minor; +} pf_floor_3_t; + +typedef struct pf_floor_4_t +{ + uint8_t protocol_id; + uint16_t port; +} pf_floor_4_t; + +typedef struct pf_floor_5_t +{ + uint8_t protocol_id; + uint32_t ip_address; +} pf_floor_5_t; + +typedef struct pf_rpc_tower +{ + pf_floor_uuid_t floor_1; + pf_floor_uuid_t floor_2; + pf_floor_3_t floor_3; + pf_floor_4_t floor_4; + pf_floor_5_t floor_5; + pnet_cfg_t * p_cfg; /* Used for generation of annotation string*/ +} pf_rpc_tower_t; + +typedef struct pf_rpc_entry +{ + pf_rpc_epm_type_t epm_type; + uint32_t max_count; + uint32_t offset; + uint32_t actual_count; + pf_rpc_uuid_type_t object_uuid; + pf_rpc_tower_t tower_entry; +} pf_rpc_entry_t; + +typedef struct pf_rpc_handle +{ + uint32_t rpc_entry_handle; + pf_rpc_uuid_type_t handle_uuid; +} pf_rpc_handle_t; + +/* + * See PN-AL-protocol (Mar20) Table 310 RPC substitutions + * for definitions of rpc lookup response + */ +typedef struct pf_rpc_lookup_rsp +{ + pf_rpc_handle_t rpc_handle; + uint32_t num_entry; + pf_rpc_entry_t rpc_entry; + uint32_t return_code; +} pf_rpc_lookup_rsp_t; + +typedef struct pf_rpc_lookup_req +{ + uint32_t inquiry_type; + uint32_t object_id; + pf_uuid_t object_uuid; + uint32_t interface_id; + pf_uuid_t interface_uuid; + uint16_t interface_ver_major; + uint16_t interface_ver_minor; + uint32_t version_option; + pf_rpc_handle_t rpc_handle; + uint32_t max_entries; + uint16_t udpPort; +} pf_rpc_lookup_req_t; + +/************************** Block header *************************************/ + +typedef enum pf_block_type_values +{ + /* Reserved 0x0000 */ + PF_BT_ALARM_NOTIFICATION_HIGH = 0x0001, + PF_BT_ALARM_NOTIFICATION_LOW = 0x0002, + /* Reserved 0x0003.. 0x0007 */ + PF_BT_IOD_WRITE_REQ_HEADER = 0x0008, + PF_BT_IOD_READ_REQ_HEADER = 0x0009, + /* Reserved 0x000A..0x000F */ + PF_BT_DIAGNOSIS_DATA = 0x0010, + /* Reserved 0x0011 */ + PF_BT_EXPECTED_IDENTIFICATION_DATA = 0x0012, + PF_BT_REAL_IDENTIFICATION_DATA = 0x0013, + PF_BT_SUBSTITUTE_VALUE = 0x0014, + PF_BT_RECORD_INPUT_DATA_OBJECT = 0x0015, + PF_BT_RECORD_OUTPUT_DATA_OBJECT = 0x0016, + + PF_BT_AR_DATA = 0x0018, + PF_BT_LOG_BOOK_DATA = 0x0019, + PF_BT_API_DATA = 0x001A, + PF_BT_SRL_DATA = 0x001B, + + PF_BT_IM_0 = 0x0020, + PF_BT_IM_1 = 0x0021, + PF_BT_IM_2 = 0x0022, + PF_BT_IM_3 = 0x0023, + PF_BT_IM_4 = 0x0024, + PF_BT_IM_5 = 0x0025, + PF_BT_IM_6 = 0x0026, + PF_BT_IM_7 = 0x0027, + PF_BT_IM_8 = 0x0028, + PF_BT_IM_9 = 0x0029, + PF_BT_IM_10 = 0x002a, + PF_BT_IM_11 = 0x002b, + PF_BT_IM_12 = 0x002c, + PF_BT_IM_13 = 0x002d, + PF_BT_IM_14 = 0x002e, + PF_BT_IM_15 = 0x002f, + + PF_BT_IM_0_FILTER_DATA_SUBMODULE = 0x0030, + PF_BT_IM_0_FILTER_DATA_MODULE = 0x0031, + PF_BT_IM_0_FILTER_DATA_DEVICE = 0x0032, + + PF_BT_IM_5_DATA = 0x0034, + + PF_BT_AR_BLOCK_REQ = 0x0101, + PF_BT_IOCR_BLOCK_REQ = 0x0102, + PF_BT_ALARM_CR_BLOCK_REQ = 0x0103, + PF_BT_EXPECTED_SUBMODULE_BLOCK = 0x0104, + PF_BT_PRM_SERVER_REQ = 0x0105, + PF_BT_MCR_REQ = 0x0106, + PF_BT_RPC_SERVER_REQ = 0x0107, + PF_BT_AR_VENDOR_BLOCK_REQ = 0x0108, + PF_BT_IR_INFO_BLOCK_REQ = 0x0109, + PF_BT_SR_INFO_BLOCK_REQ = 0x010A, + PF_BT_AR_FSU_BLOCK_REQ = 0x010B, + PF_BT_RS_INFO_BLOCK_REQ = 0x010C, + /* Reserved 0x010D-0x0x010F */ + PF_BT_PRMEND_REQ = 0x0110, + PF_BT_PRMEND_PLUG_ALARM_REQ = 0x0111, + PF_BT_APPRDY_REQ = 0x0112, + PF_BT_APPRDY_PLUG_ALARM_REQ = 0x0113, + PF_BT_RELEASE_BLOCK_REQ = 0x0114, + /* Reserved 0x0115 */ + PF_BT_XCONTROL_RDYCOMP_REQ = 0x0116, + PF_BT_XCONTROL_RDYRTC3_REQ = 0x0117, + PF_BT_PRMBEGIN_REQ = 0x0118, + PF_BT_SUBMODULE_PRMBEGIN_REQ = 0x0119, + + PF_BT_PDPORTCHECK = 0x0200, + PF_BT_CHECKPEERS = 0x020a, + PF_BT_PDPORTDATAREAL = 0x020f, + PF_BT_BOUNDARY_ADJUST = 0x0202, + PF_BT_PEER_TO_PEER_BOUNDARY = 0x0224, + PF_BT_INTERFACE_REAL_DATA = 0x0240, + PF_BT_INTERFACE_ADJUST = 0x0250, + PF_BT_PORT_STATISTICS = 0x0251, + + PF_BT_MULTIPLEBLOCK_HEADER = 0x0400, + + PF_BT_MAINTENANCE_ITEM = 0x0f00, + + /* Output from a PROFINET device */ + /* Reserved 0x8000 */ + PF_BT_ALARM_ACK_HIGH = 0x8001, + PF_BT_ALARM_ACK_LOW = 0x8002, + PF_BT_WRITE_RES = 0x8008, + PF_BT_READ_RES = 0x8009, + /* Reserved 0x8010.. 0x8100 */ + PF_BT_AR_BLOCK_RES = 0x8101, + PF_BT_IOCR_BLOCK_RES = 0x8102, + PF_BT_ALARM_CR_BLOCK_RES = 0x8103, + PF_BT_MODULE_DIFF_BLOCK = 0x8104, + PF_BT_PRM_SERVER_BLOCK_RES = 0x8105, + PF_BT_AR_SERVER_BLOCK = 0x8106, + PF_BT_AR_RPC_BLOCK_RES = 0x8107, + PF_BT_AR_VENDOR_BLOCK_RES = 0x8108, + /* Reserved 0x8109-0x0x810F */ + PF_BT_PRMEND_RES = 0x8110, + PF_BT_PRMEND_PLUG_ALARM_RES = 0x8111, + PF_BT_APPRDY_RES = 0x8112, + PF_BT_APPRDY_PLUG_ALARM_RES = 0x8113, + PF_BT_RELEASE_BLOCK_RES = 0x8114, +} pf_block_type_values_t; + +typedef enum pf_index_values +{ + PF_IDX_USER_MAX = 0x7fff, + + PF_IDX_SUB_EXP_ID_DATA = 0x8000, + PF_IDX_SUB_REAL_ID_DATA = 0x8001, + + PF_IDX_SUB_DIAGNOSIS_CH = 0x800a, + PF_IDX_SUB_DIAGNOSIS_ALL = 0x800b, + PF_IDX_SUB_DIAGNOSIS_DMQS = 0x800c, + + PF_IDX_SUB_DIAG_MAINT_REQ_CH = 0x8010, + PF_IDX_SUB_DIAG_MAINT_DEM_CH = 0x8011, + PF_IDX_SUB_DIAG_MAINT_REQ_ALL = 0x8012, + PF_IDX_SUB_DIAG_MAINT_DEM_ALL = 0x8013, + + PF_IDX_SUB_SUBSTITUTE = 0x801e, + + PF_IDX_SUB_PDIR = 0x8020, + PF_IDX_SUB_PDPORTDATAREALEXT = 0x8027, + PF_IDX_SUB_INPUT_DATA = 0x8028, + PF_IDX_SUB_OUTPUT_DATA = 0x8029, + PF_IDX_SUB_PDPORT_DATA_REAL = 0x802a, + PF_IDX_SUB_PDPORT_DATA_CHECK = 0x802b, + PF_IDX_SUB_PDIR_DATA = 0x802c, + PF_IDX_SUB_PDSYNC_ID0 = 0x802d, + PF_IDX_SUB_PDPORT_DATA_ADJ = 0x802f, + + PF_IDX_SUB_ISOCHRONUOUS_DATA = 0x8030, + PF_IDX_SUB_PDTIME = 0x8031, + + PF_IDX_SUB_PDINTF_MRP_REAL = 0x8050, + PF_IDX_SUB_PDINTF_MRP_CHECK = 0x8051, + PF_IDX_SUB_PDINTF_MRP_ADJUST = 0x8052, + PF_IDX_SUB_PDPORT_MRP_ADJUST = 0x8053, + PF_IDX_SUB_PDPORT_MRP_REAL = 0x8054, + PF_IDX_SUB_PDPORT_MRPIC_REAL = 0x8055, + PF_IDX_SUB_PDPORT_MRPIC_CHECK = 0x8056, + PF_IDX_SUB_PDPORT_MRPIC_ADJUST = 0x8057, + + PF_IDX_SUB_PDPORT_FO_REAL = 0x8060, + PF_IDX_SUB_PDPORT_FO_CHECK = 0x8061, + PF_IDX_SUB_PDPORT_FO_ADJUST = 0x8062, + PF_IDX_SUB_PDPORT_SPF_CHECK = 0x8063, + + PF_IDX_SUB_PDNC_CHECK = 0x8070, + PF_IDX_SUB_PDINTF_ADJUST = 0x8071, + PF_IDX_SUB_PDPORT_STATISTIC = 0x8072, + + PF_IDX_SUB_PDINTF_REAL = 0x8080, + + PF_IDX_SUB_PDINTF_FSU_ADJUST = 0x8090, + + PF_IDX_SUB_PE_ENTITY_STATUS = 0x80af, + PF_IDX_SUB_COMBINED_OBJ_CONTAINER = 0x80b0, + + PF_IDX_SUB_RS_ADJUST_OBSERVER = 0x80cf, + + PF_IDX_TSN_NETWORK_CONTROL_DATA_REAL = 0x80f0, + PF_IDX_TSN_STREAM_PATH_DATA = 0x80f1, + PF_IDX_TSN_SYNC_TREE_DATA = 0x80f2, + PF_IDX_TSN_UPLOAD_NETWORK_ATTRIBUTES = 0x80f3, + PF_IDX_TSN_EXPECTED_NETWORK_ATTRIBUTES = 0x80f4, + PF_IDX_TSN_NETWORK_CONTROL_DATA_ADJUST = 0x80f5, + + PF_IDX_SUB_PDINTF_SECURITY_ADJUST = 0x8200, + + PF_IDX_SUB_IM_0 = 0xaff0, + PF_IDX_SUB_IM_1 = 0xaff1, + PF_IDX_SUB_IM_2 = 0xaff2, + PF_IDX_SUB_IM_3 = 0xaff3, + PF_IDX_SUB_IM_4 = 0xaff4, + PF_IDX_SUB_IM_5 = 0xaff5, + PF_IDX_SUB_IM_6 = 0xaff6, + PF_IDX_SUB_IM_7 = 0xaff7, + PF_IDX_SUB_IM_8 = 0xaff8, + PF_IDX_SUB_IM_9 = 0xaff9, + PF_IDX_SUB_IM_10 = 0xaffa, + PF_IDX_SUB_IM_11 = 0xaffb, + PF_IDX_SUB_IM_12 = 0xaffc, + PF_IDX_SUB_IM_13 = 0xaffd, + PF_IDX_SUB_IM_14 = 0xaffe, + PF_IDX_SUB_IM_15 = 0xafff, + + PF_IDX_SLOT_EXP_ID_DATA = 0xc000, + PF_IDX_SLOT_REAL_ID_DATA = 0xc001, + + PF_IDX_SLOT_DIAGNOSIS_CH = 0xc00a, + PF_IDX_SLOT_DIAGNOSIS_ALL = 0xc00b, + PF_IDX_SLOT_DIAGNOSIS_DMQS = 0xc00c, + + PF_IDX_SLOT_DIAG_MAINT_REQ_CH = 0xc010, + PF_IDX_SLOT_DIAG_MAINT_DEM_CH = 0xc011, + PF_IDX_SLOT_DIAG_MAINT_REQ_ALL = 0xc012, + PF_IDX_SLOT_DIAG_MAINT_DEM_ALL = 0xc013, + + PF_IDX_AR_EXP_ID_DATA = 0xe000, + PF_IDX_AR_REAL_ID_DATA = 0xe001, + PF_IDX_AR_MOD_DIFF = 0xe002, + + PF_IDX_AR_DIAGNOSIS_CH = 0xe00a, + PF_IDX_AR_DIAGNOSIS_ALL = 0xe00b, + PF_IDX_AR_DIAGNOSIS_DMQS = 0xe00c, + + PF_IDX_AR_DIAG_MAINT_REQ_CH = 0xe010, + PF_IDX_AR_DIAG_MAINT_DEM_CH = 0xe011, + PF_IDX_AR_DIAG_MAINT_REQ_ALL = 0xe012, + PF_IDX_AR_DIAG_MAINT_DEM_ALL = 0xe013, + + PF_IDX_AR_PE_ENTITY_FILTER_DATA = 0xe030, + PF_IDX_AR_PE_ENTITY_STATUS_DATA = 0xe031, + + PF_IDX_AR_WRITE_MULTIPLE = 0xe040, + PF_IDX_APPLICATION_READY = 0xe041, /* Closes parameterization phase */ + + PF_IDX_AR_FSU_DATA_ADJUST = 0xe050, + + PF_IDX_AR_RS_GET_EVENT = 0xe060, + PF_IDX_AR_RS_ACK_EVENT = 0xe061, + + PF_IDX_API_REAL_ID_DATA = 0xf000, + + PF_IDX_API_DIAGNOSIS_CH = 0xf00a, + PF_IDX_API_DIAGNOSIS_ALL = 0xf00b, + PF_IDX_API_DIAGNOSIS_DMQS = 0xf00c, + + PF_IDX_API_DIAG_MAINT_REQ_CH = 0xf010, + PF_IDX_API_DIAG_MAINT_DEM_CH = 0xf011, + PF_IDX_API_DIAG_MAINT_REQ_ALL = 0xf012, + PF_IDX_API_DIAG_MAINT_DEM_ALL = 0xf013, + + PF_IDX_API_AR_DATA = 0xf020, + + PF_IDX_DEV_DIAGNOSIS_DMQS = 0xf80c, + + PF_IDX_DEV_AR_DATA = 0xf820, + PF_IDX_DEV_API_DATA = 0xf821, + + PF_IDX_DEV_LOGBOOK_DATA = 0xf830, + PF_IDX_DEV_PDEV_DATA = 0xf831, + + PF_IDX_DEV_IM_0_FILTER_DATA = 0xf840, + PF_IDX_DEV_PDREAL_DATA = 0xf841, + PF_IDX_DEV_PDEXP_DATA = 0xf842, + + PF_IDX_DEV_AUTO_CONFIGURATION = 0xf850, + PF_IDX_DEV_GSD_UPLOAD = 0xf860, + + PF_IDX_DEV_PE_ENTITY_FILTER_DATA = 0xf870, + PF_IDX_DEV_PE_ENTITY_STATUS_DATA = 0xf871, + PF_IDX_DEV_ASSET_MANAGEMENT_1 = 0xf880, + PF_IDX_DEV_ASSET_MANAGEMENT_2 = 0xf881, + PF_IDX_DEV_ASSET_MANAGEMENT_3 = 0xf882, + PF_IDX_DEV_ASSET_MANAGEMENT_4 = 0xf883, + PF_IDX_DEV_ASSET_MANAGEMENT_5 = 0xf884, + PF_IDX_DEV_ASSET_MANAGEMENT_6 = 0xf885, + PF_IDX_DEV_ASSET_MANAGEMENT_7 = 0xf886, + PF_IDX_DEV_ASSET_MANAGEMENT_8 = 0xf887, + PF_IDX_DEV_ASSET_MANAGEMENT_9 = 0xf888, + PF_IDX_DEV_ASSET_MANAGEMENT_10 = 0xf889, + + PF_IDX_TSN_STREAM_ADD = 0xf8f0, + PF_IDX_PDRSI_INSTANCDES = 0xf8f1, + PF_IDX_TSN_STREAM_REMOVE = 0xf8f2, + PF_IDX_TSN_STREAM_RENEW = 0xf8f3, + + PF_IDX_DEV_CONN_MON_TRIGGER = 0xfbff, +} pf_index_values_t; + +CC_PACKED_BEGIN +typedef struct CC_PACKED pf_block_header +{ + uint16_t block_type; /** pf_block_type_values_t */ + uint16_t block_length; + uint8_t block_version_high; + uint8_t block_version_low; +} pf_block_header_t; +CC_PACKED_END +/* The following line guarantees that offsetof works as expected on received + * data. */ +CC_STATIC_ASSERT (sizeof (pf_block_header_t) == (2 + 2 + 1 + 1)); + +/* <============================ Block header */ + +typedef struct pf_ndr_array +{ + uint32_t maximum_count; + uint32_t offset; + uint32_t actual_count; +} pf_ndr_array_t; + +typedef struct pf_ndr_data +{ + uint32_t args_maximum; + uint32_t args_length; + pf_ndr_array_t array; +} pf_ndr_data_t; + +typedef enum pf_control_command_bit_positions +{ + PF_CONTROL_COMMAND_BIT_PRM_END = 0, + PF_CONTROL_COMMAND_BIT_APP_RDY = 1, + PF_CONTROL_COMMAND_BIT_RELEASE = 2, + PF_CONTROL_COMMAND_BIT_DONE = 3, + PF_CONTROL_COMMAND_BIT_RDY_FOR_COMPANION = 4, + PF_CONTROL_COMMAND_BIT_RDY_FOR_RTC3 = 5, + PF_CONTROL_COMMAND_BIT_PRM_BEGIN = 6, +} pf_control_command_bit_positions_t; + +/** Used for both RPC control/release requests and responses */ +typedef struct pf_control_block +{ + pf_uuid_t ar_uuid; + uint16_t session_key; + uint16_t control_command; + uint16_t control_block_properties; + uint16_t alarm_sequence_number; +} pf_control_block_t; + +typedef struct pf_ip_suite +{ + /* Order is important!! */ + pnal_ipaddr_t ip_addr; + pnal_ipaddr_t ip_mask; + pnal_ipaddr_t ip_gateway; +} pf_ip_suite_t; + +typedef struct pf_full_ip_suite +{ + /* Order is important!! */ + pf_ip_suite_t ip_suite; + pnal_ipaddr_t dns_addr[4]; +} pf_full_ip_suite_t; + +/* + * ============= im_0_filter_data typedefs ================== + */ +typedef struct pf_im_0_filter_data_sub +{ + uint16_t subslot_number; /** Allowed 0x0000.0x7FFF */ + uint32_t submodule_ident_number; +} pf_im_0_filter_data_sub_t; + +typedef struct pf_im_0_filter_data_mod +{ + uint32_t module_ident_number; + uint16_t slot_number; /** Allowed 0x0000.0x7FFF */ + uint16_t nbr_submodules; + pf_im_0_filter_data_sub_t submodules[1]; +} pf_im_0_filter_data_mod_t; + +typedef struct pf_im_0_filter_data_api +{ + uint32_t api; + uint16_t nbr_modules; + pf_im_0_filter_data_mod_t modules[1]; +} pf_im_0_filter_data_api_t; + +typedef struct pf_im_0_filter_data +{ + uint16_t nbr_apis; + pf_im_0_filter_data_api_t apis[1]; +} pf_im_0_filter_data_t; + +/* + * ============= Alarm typedefs ================== + */ + +#define PF_ALARM_PDU_TYPE_VERSION_1 0x01 + +typedef enum pf_alarm_pdu_type_values +{ + PF_RTA_PDU_TYPE_RESERVED = 0, + PF_RTA_PDU_TYPE_DATA = 1, + PF_RTA_PDU_TYPE_NACK = 2, + PF_RTA_PDU_TYPE_ACK = 3, + PF_RTA_PDU_TYPE_ERR = 4, + /* 0x05..0x0f Reserved */ +} pf_alarm_pdu_type_values_t; + +typedef enum pf_alarm_type_values +{ + PF_ALARM_TYPE_DIAGNOSIS = 0x0001, + PF_ALARM_TYPE_PROCESS = 0x0002, + PF_ALARM_TYPE_PULL = 0x0003, + PF_ALARM_TYPE_PLUG = 0x0004, + PF_ALARM_TYPE_STATUS = 0x0005, + PF_ALARM_TYPE_UPDATE = 0x0006, + PF_ALARM_TYPE_MEDIA_REDUNDANCY = 0x0007, + PF_ALARM_TYPE_CONTROLLED_BY_SUPERVISOR = 0x0008, + PF_ALARM_TYPE_RELEASED = 0x0009, + PF_ALARM_TYPE_PLUG_WRONG_MODULE = 0x000a, + PF_ALARM_TYPE_RETURN_OF_SUBMODULE = 0x000b, + PF_ALARM_TYPE_DIAGNOSIS_DISAPPEARS = 0x000c, + PF_ALARM_TYPE_MC_COMM_MISMATCH = 0x000d, + PF_ALARM_TYPE_PORT_DATA_CHANGE = 0x000e, + PF_ALARM_TYPE_SYNC_DATA_CHANGE = 0x000f, + PF_ALARM_TYPE_ISOCHRONUOUS_MODE_PROBLEM = 0x0010, + PF_ALARM_TYPE_NETWORK_COMPONENT_PROBLEM = 0x0011, + PF_ALARM_TYPE_TIME_DATA_CHANGE = 0x0012, + PF_ALARM_TYPE_DYNAMIC_FRAME_PACKING_PROBLEM = 0x0013, + PF_ALARM_TYPE_MRPD_PROBLEM = 0x0014, + /* Reserved 0x0015 */ + PF_ALARM_TYPE_MILTIPLE_INTERFACE_MISMATCH = 0x0016, + /* Reserved 0x0017..0x001d */ + PF_ALARM_TYPE_UPLOAD_AND_RETRIVAL = 0x001e, + PF_ALARM_TYPE_PULL_MODULE = 0x001f, + /* Manufaturer specific 0x0020..0x007f */ + /* Reserved for profiles 0x0080..0x00ff */ + /* Reserved 0x0100.. 0xffff */ +} pf_alarm_type_values_t; + +typedef struct pf_alarm_pdu_type +{ + uint8_t type; /* pf_alarm_pdu_type_values_t */ + uint8_t version; +} pf_alarm_pdu_type_t; + +typedef struct pf_alarm_add_flags +{ + uint8_t window_size; + bool tack; +} pf_alarm_add_flags_t; + +typedef struct pf_alarm_fixed +{ + uint16_t dst_ref; + uint16_t src_ref; + pf_alarm_pdu_type_t pdu_type; + pf_alarm_add_flags_t add_flags; + uint16_t send_seq_num; + uint16_t ack_seq_nbr; + /* + * pf_alarm_fixed_t is followed by: + * uint16_t var_part_len; (0..1432) + * [block_header (pf_alarm_data_t/pf_alarm_err_t)] + */ +} pf_alarm_fixed_t; + +typedef struct pf_alarm_err +{ + pnet_pnio_status_t pnio_status; + /* + * pf_alarm_err_t may be followed by alarm_payload: + * vendor_device_error_info = VendorId, DeviceId, data* + */ +} pf_alarm_err_t; + +typedef struct pf_diag_std +{ + uint32_t ext_ch_add_value; + uint32_t qual_ch_qualifier; + uint16_t ch_nbr; + uint16_t ch_properties; /* Appears, direction, channelgroup etc */ + uint16_t ch_error_type; + uint16_t ext_ch_error_type; +} pf_diag_std_t; + +typedef struct pf_diag_usi +{ + /* Number of bytes of manufacturer data */ + uint16_t len; + + uint8_t manuf_data[PNET_MAX_DIAG_MANUF_DATA_SIZE]; +} pf_diag_usi_t; + +/* This is the end of list designator. */ +#define PF_DIAG_IX_NULL UINT16_MAX + +typedef struct pf_diag_item +{ + /* The selector is the usi member (0x0000..0x7fff => usi, else std) */ + union fmt + { + pf_diag_std_t std; + pf_diag_usi_t usi; + } fmt; + + bool in_use; + uint16_t usi; /* pf_usi_values_t */ + uint16_t next; /* Next in list (array index) */ +} pf_diag_item_t; + +/* Incoming alarm frames */ +typedef struct pf_apmr_msg +{ + uint16_t frame_id_pos; + pnal_buf_t * p_buf; +} pf_apmr_msg_t; + +typedef struct pf_alarm_payload +{ + uint16_t usi; + + /* Number of bytes of manufacturer data */ + uint16_t len; + + /* pf_diag_item_t or manufacturer data */ + uint8_t data[PNET_MAX_ALARM_PAYLOAD_DATA_SIZE]; +} pf_alarm_payload_t; + +/* See also pnet_alarm_argument_t for a subset */ +typedef struct pf_alarm_data +{ + uint16_t alarm_type; /* pf_alarm_type_values_t */ + uint32_t api_id; + uint16_t slot_nbr; + uint16_t subslot_nbr; + uint32_t module_ident; + uint16_t submodule_ident; + + pnet_alarm_spec_t alarm_specifier; /* Booleans for diagnosis alarms. */ + uint16_t sequence_number; + + /* + * pf_alarm_data_t may be followed by alarm_payload: + * RTA-SDU = alarm_noftification_pdu | alarm_ack_pdu + */ + pf_alarm_payload_t payload; +} pf_alarm_data_t; + +typedef struct pf_queue_accountant +{ + uint16_t write_index; + uint16_t read_index; + uint16_t count; + uint16_t max_items; + os_mutex_t * mutex; +} pf_queue_accountant_t; + +typedef struct pf_alarm_send_queue +{ + pf_queue_accountant_t accountant; + pf_alarm_data_t items[PNET_MAX_ALARMS]; +} pf_alarm_send_queue_t; + +typedef struct pf_alarm_receive_queue +{ + pf_queue_accountant_t accountant; + pf_apmr_msg_t items[PNET_MAX_ALARMS]; +} pf_alarm_receive_queue_t; + +#define PF_MAX_SESSION (2 * (PNET_MAX_AR) + 1) /* 2 per AR, and one spare. */ + +/* + * Keep this value small as it define the number of entries in the + * frame id map and all entries are probed until a match is found. + * + * Each input CR may have 2 frameIds (for RTC3) + * Add space for DCP: 0xfefc..0xfeff. + * Add space for alarms: 0xfc01, 0xfe01. + */ +#define PF_ETH_MAX_MAP \ + ((PNET_MAX_API) * (PNET_MAX_AR) * (PNET_MAX_CR)*2 + 4 + 2) + +/** + * The scheduler is used by both the CPM and PPM machines. + * The DCP uses the scheduler for responding to multi-cast messages. + * pf_cmsm uses it to supervise the startup sequence. + */ +#define PF_MAX_TIMEOUTS \ + (2 * (PNET_MAX_AR) * (PNET_MAX_CR) + 2 * (PNET_MAX_PHYSICAL_PORTS) + 9) + +#define PF_CMINA_FS_HELLO_RETRY 3 +#define PF_CMINA_FS_HELLO_INTERVAL \ + (3 * 1000) /* milliseconds. Default is 30 ms */ +#define PF_LLDP_SEND_INTERVAL (5 * 1000) /* milliseconds */ +#define PF_LLDP_TTL 20 /* seconds */ + +typedef enum pf_cmina_state_values +{ + PF_CMINA_STATE_SETUP, + PF_CMINA_STATE_SET_NAME, + PF_CMINA_STATE_SET_IP, + PF_CMINA_STATE_W_CONNECT, +} pf_cmina_state_values_t; + +typedef enum pf_cmsu_state_values +{ + PF_CMSU_STATE_IDLE = 0, + PF_CMSU_STATE_STARTUP, + PF_CMSU_STATE_RUN, + PF_CMSU_STATE_ABORT +} pf_cmsu_state_values_t; + +typedef enum pf_cmwrr_state_values +{ + PF_CMWRR_STATE_IDLE, + PF_CMWRR_STATE_STARTUP, + PF_CMWRR_STATE_PRMEND, + PF_CMWRR_STATE_DATA, +} pf_cmwrr_state_values_t; + +/** + * The prototype of the externally supplied call-back functions. + * @param net InOut: The p-net stack instance + * @param arg In: User-defined (may be NULL). + * @param current_time In: The current system time, in microseconds, + * when the scheduler is started to execute + * stored tasks. + */ +typedef void (*pf_scheduler_timeout_ftn_t) ( + pnet_t * net, + void * arg, + uint32_t current_time); + +typedef struct pf_scheduler_timeouts +{ + /** For debugging only */ + const char * name; + + /** Will be set to false when the timer is triggered. + For debugging only */ + bool in_use; + + uint32_t when; /** Absolute time of timeout, in microseconds */ + uint32_t next; /** Next in list. PF_MAX_TIMEOUTS if none. */ + uint32_t prev; /** Previous in list. PF_MAX_TIMEOUTS if none. */ + + pf_scheduler_timeout_ftn_t cb; /** Call-back to call on timeout */ + void * arg; /** Call-back argument */ +} pf_scheduler_timeouts_t; + +typedef struct pf_scheduler_handle +{ + const char * name; /* private */ + uint32_t timer_index; /* private */ +} pf_scheduler_handle_t; + +/** + * This is the prototype for the Profinet frame handler. + * + * @param net InOut: The p-net stack instance + * @param frame_id In: The frame ID. + * @param p_buf In: The Ethernet frame. + * @param frame_id_pos In: The position of the frame ID. + * @param p_arg In: User-defined argument. + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +typedef int (*pf_eth_frame_handler_t) ( + pnet_t * net, + uint16_t frame_id, + pnal_buf_t * p_buf, + uint16_t frame_id_pos, + void * p_arg); + +typedef struct pf_eth_frame_id_map +{ + bool in_use; + uint16_t frame_id; + pf_eth_frame_handler_t frame_handler; + void * p_arg; +} pf_eth_frame_id_map_t; + +/* + * Each struct in pf_cmina_dcp_ase_t is carefully laid out in order to use + * strncmp/memcmp in the DCP identity request and strncpy/memcpy in the + * get/set requests. + */ +typedef struct pf_cmina_dcp_ase +{ + char station_name[PNET_STATION_NAME_MAX_SIZE]; /* Terminated string */ + + /* + * DCP DeviceVendorValue is set to configured product_name + * See PN-AL-protocol (Mar20) ch 4.3.1.4.26 + * product_name is also known as DeviceType + */ + char product_name[PNET_PRODUCT_NAME_MAX_LEN + 1]; /* Terminated string */ + uint8_t device_role; /* Only value "1" supported */ + uint16_t device_initiative; /* 1: Should send hello. 0: No sending of hello + */ + struct + { + /* Order is important!! */ + uint8_t high; + uint8_t low; + } instance_id; + + pnet_cfg_device_id_t device_id; + pnet_cfg_device_id_t oem_device_id; + + char port_name[PNET_PORT_NAME_MAX_SIZE]; /* Terminated. Not used. */ + + pnet_ethaddr_t mac_address; + uint16_t standard_gw_value; + + bool dhcp_enable; + pf_full_ip_suite_t full_ip_suite; + + struct + { + /* Not yet used */ + char hostname[240 + 1]; + } dhcp_info; +} pf_cmina_dcp_ase_t; + +/* ====== AR typedefs ============== */ + +typedef enum pf_ppm_state_values +{ + PF_PPM_STATE_W_START = 0, + PF_PPM_STATE_RUN, +} pf_ppm_state_values_t; + +typedef enum pf_cpm_state_values +{ + PF_CPM_STATE_W_START = 0, + PF_CPM_STATE_FRUN, + PF_CPM_STATE_RUN, +} pf_cpm_state_values_t; + +typedef enum pf_parameter_server_values +{ + PF_PS_EXTERNAL_PARAMETER_SERVER, + PF_PS_CM_INITIATOR +} pf_parameter_server_values_t; + +typedef enum pf_ar_type_values +{ + PF_ART_IOCAR_SINGLE = 0x0001, + PF_ART_IOSAR = 0x0006, /** Supervisor AR */ + PF_ART_IOCAR_SINGLE_RTC_3 = 0x0010, + PF_ART_IOCAR_SR = 0x0020, /** Set of AR */ +} pf_ar_type_values_t; + +typedef enum pf_iocr_type_values +{ + PF_IOCR_TYPE_RESERVED_0, + PF_IOCR_TYPE_INPUT, + PF_IOCR_TYPE_OUTPUT, + PF_IOCR_TYPE_MC_PROVIDER, + PF_IOCR_TYPE_MC_CONSUMER, +} pf_iocr_type_values_t; + +typedef enum pf_companion_ar_values +{ + PF_CAR_SINGLE_AR, + PF_CAR_FIRST_COMPANION_AR, /** For future use */ + PF_CAR_SECOND_COMPANION_AR /** For future use */ +} pf_companion_ar_values_t; + +typedef enum pf_rt_class_values +{ + PF_RT_CLASS_1 = 1, /** Legacy: FrameID range 7 */ + PF_RT_CLASS_2, /** FrameID range 6 */ + PF_RT_CLASS_3, /** FrameID range 3 */ + PF_RT_CLASS_UDP, /** FrameID range 7 */ + PF_RT_CLASS_STREAM /** FrameID range 3 */ +} pf_rt_class_values_t; + +typedef enum pf_protocol_values +{ + PF_PROTOCOL_DNS, + PF_PROTOCOL_DCP +} pf_protocol_values_t; + +typedef enum pf_priority_values +{ + PF_PRIORITY_USER, + PF_PRIORITY_FIXED +} pf_priority_values_t; + +/* See also pnet_submodule_dir_t */ +typedef enum pf_data_direction_values +{ + PF_DIRECTION_INPUT = 1, + PF_DIRECTION_OUTPUT = 2, +} pf_data_direction_values_t; + +typedef enum pf_dev_status_type +{ + PF_DEV_STATUS_TYPE_IOCS, + PF_DEV_STATUS_TYPE_IOPS, +} pf_dev_status_type_t; + +typedef enum pf_end_point_1_values +{ + PF_EP1_NEITHER_LEFT_NOR_ABOVE, + PF_EP1_LEFT_OR_ABOVE +} pf_end_point_1_values_t; + +typedef enum pf_end_point_2_values +{ + PF_EP2_NEITHER_RIGHT_NOR_BELOW, + PF_EP2_RIGHT_OR_BELOW +} pf_end_point_2_values_t; + +typedef enum pf_rs_alarm_transport_mode_values +{ + PF_RSA_TRANSPORT_MODE_0 = 0, + PF_RSA_TRANSPORT_MODE_1 = 1, +} pf_rs_alarm_transport_mode_values_t; + +typedef enum pf_fs_parameter_mode_values +{ + PF_FS_PARAMETER_MODE_ON, + PF_FS_PARAMETER_MODE__OFF +} pf_fs_parameter_mode_values_t; + +typedef enum pf_ar_state_values +{ + PF_AR_STATE_PRIMARY, /** default value */ + PF_AR_STATE_FIRST, + PF_AR_STATE_BACKUP +} pf_ar_state_values_t; + +typedef enum pf_sync_state_values +{ + PF_SYNC_STATE_SYNCHRONIZED, + PF_SYNC_STATE_NOT_AVAILABLE +} pf_sync_state_values_t; + +typedef enum pf_alarm_cr_type_values +{ + PF_ALARM_CR = 0x0001, +} pf_alarm_cr_type_values_t; + +/** According to the spec these are the states used by CMDEV/CMDEV_DA. + * + * The states for the AR is stored in cmdev_state, within the AR. + * + * An AR can be controlled by either a CMDEV or a CMDEV_DA state machine. + * This is selected by looking at ar_parameters.device_access. + * A CMDEV_DA state machine has only the states POWER_ON, W_CIND, W_CRES and + * DATA. A CMDEV state machine uses all states listed below. + */ +typedef enum pf_cmdev_state_values +{ + PF_CMDEV_STATE_POWER_ON = 0, /* Data initialization. (Must be first) */ + PF_CMDEV_STATE_W_CIND, /* Wait for connect indcation. */ + PF_CMDEV_STATE_W_CRES, /* Wait for connect response from app and CMSU + startup. */ + PF_CMDEV_STATE_W_SUCNF, /* Wait for CMSU confirmation. */ + PF_CMDEV_STATE_W_PEIND, /* Wait for PrmEnd indication. */ + PF_CMDEV_STATE_W_PERES, /* Wait for PrmEnd response from app. */ + PF_CMDEV_STATE_W_ARDY, /* Wait for app ready from app. */ + PF_CMDEV_STATE_W_ARDYCNF, /* Wait for app ready confirmation from controller. + */ + PF_CMDEV_STATE_WDATA, /* Wait for established cyclic data exchange. */ + PF_CMDEV_STATE_DATA, /* Data exchange and connection monitoring. */ + PF_CMDEV_STATE_ABORT /* Abort application relation. */ +} pf_cmdev_state_values_t; + +/* ====================================================================== */ + +typedef struct pf_ar_properties +{ + uint16_t state; /** Allowed values: ACTIVE (=1) */ + bool supervisor_takeover_allowed; + uint8_t parameterization_server; /** pf_parameter_server_values_t */ + bool device_access; /** See IEC61158-6-10 */ + uint16_t companion_ar; /** pf_companion_ar_values_t */ + bool acknowledge_companion_ar; /** whether companion_ar is needed */ + bool combined_object_container; /** See IEC61158-6-10 */ + bool startup_mode; /** false: legacy, true: advanced */ + bool pull_module_alarm_allowed; /** false: mandatory, true: optional */ +} pf_ar_properties_t; + +typedef struct pf_ar_param +{ + bool valid; + uint16_t ar_type; /** pf_ar_type_values_t */ + pf_uuid_t ar_uuid; + uint16_t session_key; + pnet_ethaddr_t cm_initiator_mac_add; + pf_uuid_t cm_initiator_object_uuid; + pf_ar_properties_t ar_properties; + uint16_t cm_initiator_activity_timeout_factor; /** res: 100ms */ + uint16_t cm_initiator_udp_rt_port; /** 0x8892 */ + uint16_t cm_initiator_station_name_len; + char cm_initiator_station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated + string. */ +} pf_ar_param_t; + +typedef struct pf_ar_result +{ + pnet_ethaddr_t cm_responder_mac_add; + uint16_t responder_udp_rt_port; +} pf_ar_result_t; + +typedef struct pf_iocr_properties +{ + uint32_t rt_class; /** pf_rt_class_values_t */ + uint16_t reserved_1; + uint16_t reserved_2; + uint16_t reserved_3; +} pf_iocr_properties_t; + +typedef struct pf_frame_descriptor +{ + uint16_t slot_number; + uint16_t subslot_number; + uint16_t frame_offset; +} pf_frame_descriptor_t; + +typedef struct pf_api_entry +{ + uint32_t api; + uint16_t nbr_io_data; + pf_frame_descriptor_t io_data[PNET_MAX_SLOTS * PNET_MAX_SUBSLOTS]; + uint16_t nbr_iocs; + pf_frame_descriptor_t iocs[PNET_MAX_SLOTS * PNET_MAX_SUBSLOTS]; +} pf_api_entry_t; + +typedef struct pf_iocr_tag_header +{ + uint16_t vlan_id; /** Bits 0..11. Default: 0 */ + uint16_t iocr_user_priority; /** Bits 12..15. Allowed: IOCR_PRIORITY = 6 */ +} pf_iocr_tag_header_t; + +typedef struct pf_iocr_param +{ + bool valid; + uint16_t iocr_type; /** pf_iocr_type_values_t */ + uint16_t iocr_reference; /** Cross-ref from e.g. mcr_request and ir_info */ + uint16_t lt_field; /** Allowed: 0x8892 */ + pf_iocr_properties_t iocr_properties; + uint16_t c_sdu_length; /** Allowed: RT_CLASS_UDP: 12..1440, others: 40..1440 + */ + uint16_t frame_id; /** Depends on RT_class */ + uint16_t send_clock_factor; /** res: 31.25us, Allowed: 1..128, default: 32 */ + uint16_t reduction_ratio; /** Allowed: 1..512 */ + uint16_t phase; /** 1..reduction_ratio */ + uint16_t sequence; /** Allowed: RT_class_3: 0, others: 0 + (mandatory) 1..0xffff (optional) */ + uint32_t frame_send_offset; /** res: 1ns, Allowed: (All:) BEST_EFFORT + (0xffffffff), RT_class_3: 0x0..0x3D08FF + (mandatory) */ + uint32_t watchdog_factor; + uint16_t data_hold_factor; /** Allowed: 0x0000..0x1e00, more restrictions + apply */ + + pf_iocr_tag_header_t iocr_tag_header; + + pnet_ethaddr_t iocr_multicast_mac_add; /** See IEC61158-6-10 */ + + uint16_t nbr_apis; + pf_api_entry_t apis[PNET_MAX_API]; +} pf_iocr_param_t; + +typedef struct pf_iocr_result +{ + uint16_t iocr_type; /** pf_iocr_type_values_t */ + uint16_t iocr_reference; + uint16_t frame_id; +} pf_iocr_result_t; + +typedef struct pf_data_descriptor +{ + uint16_t data_direction; /** pf_data_direction_values_t */ + uint16_t submodule_data_length; /** Allowed: 0..1439 */ + uint8_t length_iops; /** Allowed: 1 */ + uint8_t length_iocs; /** Allowed: 1 */ +} pf_data_descriptor_t; + +typedef struct pf_submodule_properties +{ + uint8_t type; /** Bits 0..1: pnet_submodule_dir_t */ + bool sharedInput; + bool reduce_input_submodule_data_length; + bool reduce_output_submodule_data_length; + bool discard_ioxs; +} pf_submodule_properties_t; + +typedef struct pf_exp_submodule +{ + uint16_t subslot_number; /** Allowed: 0x0000..0x9FFF */ + uint32_t submodule_ident_number; + pf_submodule_properties_t submodule_properties; + uint16_t nbr_data_descriptors; + pf_data_descriptor_t data_descriptor[2]; /** 2 used for PNET_DIR_IO */ +} pf_exp_submodule_t; + +typedef struct pf_exp_module +{ + uint16_t slot_number; /** Allowed 0x0000.0x7FFF */ + uint32_t module_ident_number; + uint16_t module_properties; /** Reserved - currently unused */ + + uint16_t nbr_submodules; + pf_exp_submodule_t submodules[PNET_MAX_SUBSLOTS]; +} pf_exp_module_t; + +typedef struct pf_exp_api +{ + bool valid; + uint32_t api; + uint16_t nbr_modules; + pf_exp_module_t modules[PNET_MAX_SLOTS]; +} pf_exp_api_t; + +typedef struct pf_submodule_state +{ + uint8_t add_info; /** Bits 0..2: pf_submod_add_info_t */ + bool advice_avail; /** Bit 3 */ + bool maintenance_required; /** Bit 4 */ + bool maintenance_demanded; /** Bit 5 */ + bool fault; /** Bit 6 */ + uint8_t ar_info; /** Bits 7..10: pf_submod_ar_info_t */ + uint8_t ident_info; /** Bits 11..14: pf_submod_plug_state_t */ + bool format_indicator; /** Bit 15: Always 1 (true) */ +} pf_submodule_state_t; + +typedef struct pf_submodule_diff +{ + uint16_t subslot_number; + uint32_t submodule_ident_number; + pf_submodule_state_t submodule_state; +} pf_submodule_diff_t; + +typedef struct pf_module_diff +{ + uint16_t slot_number; + uint32_t module_ident_number; + uint16_t module_state; /** pf_module_state_values_t */ + uint16_t nbr_submodule_diffs; + pf_submodule_diff_t submodule_diffs[PNET_MAX_SUBSLOTS]; +} pf_module_diff_t; + +typedef struct pf_api_diff +{ + uint32_t api; + uint16_t nbr_module_diffs; + pf_module_diff_t module_diffs[PNET_MAX_SLOTS]; +} pf_api_diff_t; + +typedef struct pf_parameter_server_properties +{ + uint32_t reserved; /** Currently unused */ +} pf_parameter_server_properties_t; + +typedef struct pf_prm_server +{ + bool valid; + pf_uuid_t parameter_server_object_uuid; + pf_parameter_server_properties_t parameter_server_properties; /** Allowed: 0 + */ + uint16_t cm_initiator_activity_timeout_factor; /** res: 100ms, + allowed 1..1000 */ + uint16_t parameter_server_station_name_len; + char parameter_server_station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated + string */ +} pf_prm_server_t; + +typedef struct pf_address_resolution_properties +{ + uint8_t protocol; /** pf_protocol_values_t */ + uint8_t resolution_factor; /** range: 1..0x64 */ +} pf_address_resolution_properties_t; + +typedef struct pf_multicast_cr +{ + bool valid; + uint16_t iocr_reference; + pf_address_resolution_properties_t address_resolution_properties; + uint16_t mci_timeout_factor; /* MCI_timeout range 0x0001..0xffff */ + uint16_t length_provider_station_name; + char provider_station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated */ +} pf_multicast_cr_t; + +typedef struct pf_ar_rpc_request +{ + bool valid; /** Requested in connect.req */ + uint16_t initiator_rpc_server_port; /** Allowed 0x0000..0x03ff (optional), + 0x0400..0xbfff (usable), + 0xc000..0xffff (recommended) */ +} pf_ar_rpc_request_t; + +typedef struct pf_ar_rpc_result +{ + uint16_t responder_rpc_server_port; /** Allowed 0x0000..0x03ff (optional), + 0x0400..0xbfff (usable), + 0xc000..0xffff (recommended) */ +} pf_ar_rpc_result_t; + +typedef struct pf_subframe_data +{ + uint8_t subframe_position; + uint8_t subframe_length; +} pf_subframe_data_t; + +typedef struct pf_dfp_iocr +{ + uint16_t iocr_reference; + uint16_t subframe_offset; /** Range: 0..0x59B */ + pf_subframe_data_t subframe_data; +} pf_dfp_iocr_t; + +typedef struct pf_ir_info +{ + bool valid; + pf_uuid_t ir_data_uuid; + uint16_t nbr_iocrs; /** Allowed 0, 2 */ + pf_dfp_iocr_t dfp_iocrs[PNET_MAX_DFP_IOCR]; +} pf_ir_info_t; + +typedef struct pf_sr_info +{ + bool valid; + uint16_t redundancy_data_hold_factor; /** res: 1 ms, range: 1..0xC8 + (optional), 0xc9..0xffff (mandatory) + */ + struct + { + uint8_t input_valid_on_backup; /** Range: 0..1 */ + uint8_t mode; /** Range: 0..1 */ + } sr_properties; +} pf_sr_info_t; + +typedef struct pf_rs_info +{ + bool valid; + uint8_t rs_alarm_transport_mode; /** Range 0 (read by record), 1 (using alarm + reporting) */ +} pf_rs_info_t; + +typedef struct pf_ar_fsu +{ + bool valid; + struct + { + uint32_t fs_parameter_mode; /** Range 0..1 (ON, OFF) */ + pf_uuid_t fs_parameter_uuid; + } fs_parameter_block; +#if PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH + uint16_t length_manufacturer_specific_fast_startup_data; + uint8_t manufacturer_specific_fast_startup_data + [PNET_MAX_MAN_SPECIFIC_FAST_STARTUP_DATA_LENGTH]; +#endif +} pf_ar_fsu_t; + +typedef struct pf_ar_server +{ + uint16_t length_cm_responder_station_name; + char cm_responder_station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated */ +} pf_ar_server_t; + +typedef struct pf_alarm_cr_properties +{ + bool priority; /** pf_priority_values_t */ + bool transport_udp; /** false ==> PF_RT_CLASS_1, true ==> PF_RT_CLASS_UDP */ +} pf_alarm_cr_properties_t; + +typedef struct pf_alarm_cr_tag_header +{ + uint8_t vlan_id; /** Bits 0..11. Allowed: No VLAN (0) */ + uint8_t alarm_user_priority; /** Bits 12..15. Allowed: USER_PRIORITY_LOW (5), + USER_PRIORITY_HIGH (6) */ +} pf_alarm_cr_tag_header_t; + +/** The largest incoming alarmdata we can accept + * Allowed 200..1432 + */ +#define PF_MAX_ALARM_DATA_LEN 200 + +typedef struct pf_alarm_cr_request +{ + bool valid; + uint32_t alarm_cr_type; /** Allowed: 0x0001 */ + uint16_t lt_field; /** Allowed: 0x8892 */ + pf_alarm_cr_properties_t alarm_cr_properties; + uint16_t rta_timeout_factor; /** res: 100ms, allowed: 1..100 (mandatory), + 101..0xffff (optional) */ + uint16_t rta_retries; /** Allowed: 3..15 */ + uint16_t local_alarm_reference; /** Controller reference */ + uint16_t max_alarm_data_length; /** Allowed 200..1432 */ + pf_alarm_cr_tag_header_t alarm_cr_tag_header_high; /** USER_PRIORITY_HIGH (6) + */ + pf_alarm_cr_tag_header_t alarm_cr_tag_header_low; /** USER_PRIORITY_LOW (5) + */ +} pf_alarm_cr_request_t; + +typedef struct pf_alarm_cr_result +{ + uint16_t alarm_cr_type; /** pf_alarm_cr_type_values_t (0x0001) */ + uint16_t remote_alarm_reference; /** Our reference */ + uint16_t max_alarm_data_length; /** Range: 200..1432 */ +} pf_alarm_cr_result_t; + +#if PNET_OPTION_AR_VENDOR_BLOCKS +typedef struct pf_ar_vendor_request +{ + bool valid; + uint32_t ap_structure_identifier; /** 0..7fff (vendor specific), 0x8000 + (extended identification rules), + 0x8001..0xffff (administrative number) + */ + uint32_t api; + uint16_t length_data_request; + uint8_t data_request[PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH]; /** Not + terminated. */ +} pf_ar_vendor_request_t; + +typedef struct pf_ar_vendor_result_t +{ + uint16_t length_data_response; + uint8_t data_response[PNET_MAX_AR_VENDOR_BLOCK_DATA_LENGTH]; /** Not + terminated. + */ +} pf_ar_vendor_result_t; +#endif + +/* HW offload driver */ +typedef struct pf_drv +{ + bool initialized; +} pf_drv_t; + +/* HW offload driver frame */ +typedef struct pf_drv_frame +{ + bool initialized; +} pf_drv_frame_t; + +typedef struct pf_ppm +{ + pf_ppm_state_values_t state; + uint32_t next_exec; /* Next timestamp when a frame should be sent, in + microseconds */ + + int errline; /* Not yet used */ + uint32_t errcnt; /* Not yet used */ + + pnet_ethaddr_t sa; /* Source MAC address */ + pnet_ethaddr_t da; /* Destination MAC address (IO-controller) */ + + bool first_transmit; /* True if first transmission has been done */ + + void * p_send_buffer; /* Output buffer with Ethernet header etc */ + bool new_buf; /* Not yet used */ + + uint16_t cycle; /* Cycle counter, in tics each 31.25 us (thus 16 tics per + ms). */ + uint8_t transfer_status; + uint8_t data_status; /* Valid or invalid, run or stop, redundancy, problem + indicator etc */ + uint16_t buffer_length; + uint16_t buffer_pos; /* Start position of data in frame */ + uint16_t cycle_counter_offset; /* Start position of cycle counter in frame */ + uint16_t data_status_offset; /* Start position of data status in frame */ + uint16_t transfer_status_offset; /* Start position of transfer status in + frame */ + + uint8_t buffer_data[PF_FRAME_BUFFER_SIZE]; /* Max */ + + uint32_t trx_cnt; /* Number of frames sent */ + + uint16_t send_clock_factor; /* Resolution: 31.25us, Allowed: 1..128, Default: + 32 */ + uint16_t reduction_ratio; /* Allowed: 1..512 */ + uint32_t control_interval; /* Period in microseconds between frames */ + bool ci_running; /* True if the timer is running. Used for stopping + transmission before next scheduled sending. */ + pf_scheduler_handle_t ci_timeout; + + pf_drv_frame_t * frame; /* Driver specific ppm frame configuration */ + +} pf_ppm_t; + +typedef struct pf_cpm +{ + pf_cpm_state_values_t state; + uint32_t max_exec; + + int errline; + uint32_t errcnt; + + uint32_t recv_cnt; + uint32_t free_cnt; + + pnet_ethaddr_t sa; /* Mac of the controller */ + + uint16_t nbr_frame_id; /* 1 or 2 */ + uint16_t frame_id[2]; /* 2 needed for some instances of RT_CLASS_3 */ + uint16_t data_hold_factor; + + void * p_buffer_app; /* Owned by app */ + void * p_buffer_cpm; /* owned by cpm */ + bool new_buf; /* New data to be received */ + uint16_t frame_id_pos; /* Handles VLAN in ETH header */ + + uint8_t data_status; + uint16_t buffer_length; + uint16_t buffer_pos; /* Start of PROFINET data in frame */ + + uint16_t dht; /* Set to zero at incoming cyclic frame, increased by + pf_cpm_control_interval_expired() */ + bool new_data; + uint32_t rxa[PNET_MAX_PHYSICAL_PORTS][2]; /* Max 2 frame_ids */ + int32_t cycle; /* value -1 means "never" */ + + uint32_t control_interval; + bool ci_running; + + pf_scheduler_handle_t ci_timeout; + + /* CMIO data */ + bool cmio_start; /* cmInstance.start/stop */ + + pf_drv_frame_t * frame; /* Driver specific cpm frame configuration */ + +} pf_cpm_t; + +typedef struct pf_iodata_object +{ + bool in_use; + uint32_t api_id; + uint16_t slot_nbr; + uint16_t subslot_nbr; + + /* Individual sub-slot data */ + uint16_t data_offset; + uint16_t data_length; + + /* The provider status */ + uint16_t iops_offset; + uint16_t iops_length; + + /* The consumer status */ + uint16_t iocs_offset; + uint16_t iocs_length; + + bool data_avail; + +#if PNET_MAX_DFP_IOCR > 0 + pf_subframe_data_t subframe_data[PNET_MAX_DFP_IOCR]; +#endif +} pf_iodata_object_t; + +/* Forward declaration of some useful types */ +struct pf_ar; +struct pf_alpmx; + +/* IOCR = IO Communication Relation */ +typedef struct pf_iocr +{ + struct pf_ar * p_ar; + uint32_t crep; /* Index to self in pf_ar_t ->iocr array */ + + pf_cpm_t cpm; /* Handles output */ + pf_ppm_t ppm; /* Handles input */ + + uint16_t mppm; + uint16_t cmdmc; + uint16_t dfp; + + /* Total frame content (data, IOCS and IOPS) length after L2 header + * (buffer_pos) */ + uint16_t in_length; + uint16_t out_length; + + uint16_t nbr_data_desc; + pf_iodata_object_t + data_desc[PNET_MAX_API * PNET_MAX_SLOTS * PNET_MAX_SUBSLOTS]; + + pf_iocr_param_t param; /* From connect.req */ + pf_iocr_result_t result; /* From connect.ind */ +} pf_iocr_t; + +typedef enum pf_cmsm_state_values +{ + PF_CMSM_STATE_IDLE, + PF_CMSM_STATE_RUN +} pf_cmsm_state_values_t; + +typedef enum pf_cmio_state_values +{ + PF_CMIO_STATE_IDLE = 0, + PF_CMIO_STATE_STARTUP, + PF_CMIO_STATE_WDATA, + PF_CMIO_STATE_DATA, +} pf_cmio_state_values_t; + +typedef enum pf_cmpbe_state_values +{ + PF_CMPBE_STATE_IDLE, + PF_CMPBE_STATE_WFIND, + PF_CMPBE_STATE_WFRSP, + PF_CMPBE_STATE_WFPEI, + PF_CMPBE_STATE_WFPER, + PF_CMPBE_STATE_WFREQ, + PF_CMPBE_STATE_WFCNF +} pf_cmpbe_state_values_t; + +typedef enum pf_apms_state_values +{ + PF_APMS_STATE_CLOSED, + PF_APMS_STATE_OPEN, + PF_APMS_STATE_WTACK +} pf_apms_state_values_t; + +typedef enum pf_apmr_state_values +{ + PF_APMR_STATE_CLOSED, + PF_APMR_STATE_OPEN, + PF_APMR_STATE_WCNF +} pf_apmr_state_values_t; + +/* + * This type contains all the information needed for one + * APMS/APMR pair. + */ +typedef struct pf_apmx +{ + struct pf_ar * p_ar; + struct pf_alpmx * p_alpmx; + + pnet_ethaddr_t da; /* Destination MAC address (IO-controller) */ + + uint16_t src_ref; /* Our ref */ + uint16_t dst_ref; /* Controller local_alarm_reference */ + + pf_apms_state_values_t apms_state; + pf_apmr_state_values_t apmr_state; + + uint16_t send_seq_count; + uint16_t send_seq_count_o; + + uint16_t exp_seq_count; + uint16_t exp_seq_count_o; + + /* The alarm frame receive queue */ + pf_alarm_receive_queue_t alarm_receive_q; + + /* Latest sent alarm frame, for possible retransmission */ + pnal_buf_t * p_rta; + + bool high_priority; /* True for high priority APMX. For printouts. */ + uint16_t vlan_prio; /* 5 or 6 */ + uint16_t block_type_alarm_notify; + uint16_t block_type_alarm_ack; + uint16_t frame_id; + + uint32_t timeout_us; + + pf_scheduler_handle_t resend_timeout; /* Scheduler handle for Alarm + retransmission */ + uint32_t resend_counter; +} pf_apmx_t; + +typedef enum pf_alpmr_state_values +{ + PF_ALPMR_STATE_W_START, + PF_ALPMR_STATE_W_NOTIFY, + PF_ALPMR_STATE_W_USER_ACK, + PF_ALPMR_STATE_W_TACK +} pf_alpmr_state_values_t; + +typedef enum pf_alpmi_state_values +{ + PF_ALPMI_STATE_W_START, + PF_ALPMI_STATE_W_ALARM, + PF_ALPMI_STATE_W_ACK, +} pf_alpmi_state_values_t; + +/* + * This type contains all the information needed for one + * ALPMI/ALPMR pair. + */ +typedef struct pf_alpmx +{ + pnet_ethaddr_t da; /* Destination MAC address (IO-controller) */ + + /* + * The sequence number is used for PULL/PLUG requests. + * It is updated by the alarm component and checked by the CMDEV component. + */ + uint16_t prev_sequence_number; + uint16_t sequence_number; + + pf_alpmi_state_values_t alpmi_state; + pf_alpmr_state_values_t alpmr_state; +} pf_alpmx_t; + +typedef enum pf_get_result +{ + PF_PARSE_OK, + PF_PARSE_NULL_POINTER, /* Input buffer is NULL */ + PF_PARSE_END_OF_INPUT, /* Unexpected */ + PF_PARSE_OUT_OF_API_RESOURCES, + PF_PARSE_OUT_OF_EXP_SUBMODULE_RESOURCES, + PF_PARSE_ERROR /* Other */ +} pf_get_result_t; + +typedef struct pf_get_info +{ + pf_get_result_t result; + bool is_big_endian; + const uint8_t * p_buf; + uint16_t len; +} pf_get_info_t; + +/* + * A session stores information used for supervision of connection activity. + * A session is allocated for each connect in order to handle fragmented RPC + * requests. In order to find this session A session is also needed for each + * CCONTROL request. Use pf_session_locate_by_uuid. + */ +typedef struct pf_session_info +{ + /* The session uses pf_eth_send_on_management_port() for sending raw + * Ethernet frames on the management port */ + + uint16_t ix; + bool in_use; + bool release_in_progress; /* The session handles an incoming release request + */ + bool kill_session; /* On error or when done. This will kill the session at + the end of handling the incoming RPC frame. */ + int socket; /* Socket for CControl messaging, or reference to the main CMRPC + socket. Close it only if from_me==true */ + struct pf_ar * p_ar; /* Parent AR */ + bool from_me; /* True if the session originates from the device (i.e. + CControl requests and responses). */ + pf_uuid_t activity_uuid; + uint32_t ip_addr; + pnal_ipport_t port; /* Source port on incoming message, destination port on + outgoing message */ + uint32_t sequence_nmb_send; /* rm_ccontrol_req */ + + /* + * Small devices should be able to cope with a single Ethernet frame. + * Large devices may however require considerable longer Connect + * Requests/responses, Reads responses and Write requests may also require + * longer buffers. These are sent/received via fragmented RPC + * requests/responses. Allocate buffers to handle these large request here. + */ + uint8_t in_buffer[PNET_MAX_SESSION_BUFFER_SIZE]; /* Typically request buffer + */ + uint16_t in_buf_len; + uint16_t in_fragment_nbr; + + uint8_t out_buffer[PNET_MAX_SESSION_BUFFER_SIZE]; /* Typically response + buffer */ + uint16_t out_buf_len; + uint16_t out_buf_sent_pos; /* Number of bytes sent so far */ + uint16_t out_buf_send_len; /* Size of current packet to send */ + uint16_t out_fragment_nbr; + + pf_get_info_t get_info; + bool is_big_endian; /* From rpc_header_t in first fragment */ + pnet_result_t rpc_result; + pf_ndr_data_t ndr_data; + + /* This item is used to handle dcontrol re-runs */ + uint32_t dcontrol_sequence_nmb; /* From dcontrol request */ + pnet_result_t dcontrol_result; + + /* This timer is used to handle ccontrol and fragment re-transmissions */ + pf_scheduler_handle_t resend_timeout; + uint32_t resend_counter; +} pf_session_info_t; + +typedef struct pf_ar +{ + bool in_use; + uint16_t arep; + + pf_session_info_t * p_sess; /* Incoming session, not the outgoing CControl + session. Use pf_session_locate_by_ar() to find + the CControl session. */ + + pf_cmdev_state_values_t cmdev_state; /* pf_cmdev_state_values_t */ + pnet_ethaddr_t src_addr; /* Connect client MAC address */ + + uint16_t nbr_ar_param; + pf_ar_param_t ar_param; /* From connect.req */ + pf_ar_result_t ar_result; /* From connect.ind */ + + uint16_t nbr_ar_server; + pf_ar_server_t ar_server; /* From connect.ind */ + + uint16_t nbr_alarm_cr; /* 0 or 1 */ + pf_alarm_cr_request_t alarm_cr_request; /* From connect.req */ + pf_alarm_cr_result_t alarm_cr_result; /* From connect.ind */ + + bool alarm_enable; + /* Alarm handling ALPMI/ALPMR machines: one for LOW (0) prio and one for HIGH + * (1) prio. */ + pf_alpmx_t alpmx[PF_ALARM_NUMBER_OF_PRIORITY_LEVELS]; + /* Alarm handling APMS/APMR machines: one for LOW (0) prio and one for HIGH + * (1) prio. */ + pf_apmx_t apmx[PF_ALARM_NUMBER_OF_PRIORITY_LEVELS]; + + /* Alarm queues for outgoing alarms: one for LOW (0) prio and one for HIGH + * (1) prio. */ + pf_alarm_send_queue_t alarm_send_q[PF_ALARM_NUMBER_OF_PRIORITY_LEVELS]; + + uint16_t nbr_ar_rpc; + pf_ar_rpc_request_t ar_rpc_request; /* From connect.req */ + pf_ar_rpc_result_t ar_rpc_result; /* From connect.ind */ + + uint16_t nbr_iocrs; /* From connect.req, typically 2*/ + pf_iocr_t iocrs[PNET_MAX_CR]; /* Each has a CPM and a PPM */ + + uint16_t nbr_exp_apis; + pf_exp_api_t exp_apis[PNET_MAX_API]; /* From connect.req */ + + uint16_t nbr_api_diffs; /* From connect.ind */ + pf_api_diff_t api_diffs[PNET_MAX_API]; + + uint16_t nbr_prm_server; + uint16_t nbr_rpc_server; + uint16_t nbr_ir_info; + + /* Used for error discovery in connect RPC */ + uint16_t input_cr_cnt; + uint16_t output_cr_cnt; + uint16_t mcr_cons_cnt; + bool rtc3_present; + + /* Internal state */ + uint16_t remote_application_ready_timeout; /** res: 1s, allowed: 300 */ + uint16_t ar_state; /** pf_ar_state_values_t */ + uint16_t sync_state; /** pf_sync_state_values_t */ + bool ready_4_data; + + /* Global error codes */ + uint8_t err_cls; /* Error code 1 */ + uint8_t err_code; /* Error code 2 */ + + pf_cmwrr_state_values_t cmwrr_state; + + pf_cmsu_state_values_t cmsu_state; + + pf_cmsm_state_values_t cmsm_state; + pf_scheduler_handle_t cmsm_timeout; + + pf_cmio_state_values_t cmio_state; + pf_scheduler_handle_t cmio_timeout; + bool cmio_timer_should_reschedule; + + pf_cmpbe_state_values_t cmpbe_state; + uint16_t cmpbe_stored_command; + + /* Optional data */ +#if PNET_OPTION_MC_CR + uint16_t nbr_mcr; + pf_multicast_cr_t mcr_request[PNET_MAX_MC_CR]; +#endif +#if PNET_OPTION_PARAMETER_SERVER + pf_prm_server_t prm_server; +#endif +#if PNET_OPTION_IR + pf_ir_info_t ir_info; +#endif +#if PNET_OPTION_SR + pf_sr_info_t sr_info; +#endif +#if PNET_OPTION_REDUNDANCY + struct + { + uint8_t end_point_1; /** pf_end_point_1_values_t */ + uint8_t end_point_2; /** pf_end_point_2_values_t */ + } redundancy_info; +#endif +#if PNET_OPTION_AR_VENDOR_BLOCKS + uint16_t nbr_ar_vendor; + pf_ar_vendor_request_t ar_vendor_request[PNET_MAX_AR_VENDOR_BLOCKS]; + pf_ar_vendor_result_t ar_vendor_result[PNET_MAX_AR_VENDOR_BLOCKS]; +#endif +#if PNET_OPTION_RS + pf_rs_info_t rs_info; +#endif +#if PNET_OPTION_FAST_STARTUP + pf_ar_fsu_t fast_startup_data; +#endif +} pf_ar_t; + +/* + * ============= Pluggable typedefs ================== + */ + +typedef enum pf_submod_plug_state +{ + PF_SUBMOD_PLUG_OK, + PF_SUBMOD_PLUG_SUBSTITUTE, /** module_state == SUBSTITUTE */ + PF_SUBMOD_PLUG_WRONG, /** module_state == WRONG_MODULE */ + PF_SUBMOD_PLUG_NO /** module_state == WRONG_MODULE */ +} pf_submod_plug_state_t; + +typedef enum pf_submod_owner +{ + PF_SUBMOD_OWNER_FREE, + PF_SUBMOD_OWNER_SOL, /* Superordinator locked */ + PF_SUBMOD_OWNER_IOS, /* Owned by IO supervisor */ + PF_SUBMOD_OWNER_IOC, /* Owned by IO controller */ +} pf_submod_owner; + +typedef enum pf_mod_plug_state +{ + PF_MOD_PLUG_NO_MODULE, /** No module plugged into slot. */ + PF_MOD_PLUG_WRONG_MODULE, /** Module/submodule ident number(s) do not match + cfg. */ + PF_MOD_PLUG_PROPER_MODULE, /** All OK. Submodule may have diff. */ + PF_MOD_PLUG_SUBSTITUTE /** There is another module plugged into the slot. */ +} pf_mod_plug_state_t; + +typedef enum pf_submod_add_info +{ + PF_SUBMOD_ADD_INFO_NONE, + PF_SUBMOD_ADD_INFO_TAKEOVER_NOT_ALLOWED +} pf_submod_add_info_t; + +typedef enum pf_submod_ar_info +{ + PF_SUBMOD_AR_INFO_OWN, /** The AR is owner */ + PF_SUBMOD_AR_INFO_APPLICATION_READY_PENDING, /** Parameterization in progress + */ + PF_SUBMOD_AR_INFO_SUPERORDINATED_LOCKED, /** Locked by application (ToDo: API + for that) */ + PF_SUBMOD_AR_INFO_LOCKED_BY_IO_CONTROLLER, /** Locked by other IO controller + */ + PF_SUBMOD_AR_INFO_LOCKED_BY_IO_SUPERVISOR /** Locked by other IO Supervisor + */ +} pf_submod_ar_info_t; + +typedef struct pf_submod_state +{ + pf_submod_add_info_t add_info; /** Bits 0..2 */ + bool advice_avail; /** Bit 3 */ + bool maintenance_required; /** Bit 4 */ + bool maintenance_demanded; /** Bit 5 */ + bool fault; /** Bit 6 */ + pf_submod_ar_info_t ar_info; /** Bits 7..10 */ + pf_submod_plug_state_t ident_info; /** Bits 11..14 */ + bool format_indicator; /** Bit 15: Always 1 (true) */ +} pf_submod_state_t; + +typedef enum pf_usi_values +{ + /* 0x0000..0x7fff Manufacturer specific */ + PF_USI_CHANNEL_DIAGNOSIS = 0x8000, + PF_USI_ALARM_MULTIPLE = 0x8001, + PF_USI_EXTENDED_CHANNEL_DIAGNOSIS = 0x8002, + PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS = 0x8003, + /* 0x8004..0x80ff Reserved */ + PF_USI_MAINTENANCE = 0x8100, + /* 0x8101..0x81ff Reserved */ + PF_USI_UPLOAD_AND_RETRIEVAL = 0x8200, + PF_USI_IPARAMETER = 0x8201, + /* 0x8202..0x82ff Reserved */ + PF_USI_RS_LOW_WATERMARK = 0x8300, + PF_USI_RS_TIMEOUT = 0x8301, + PF_USI_RS_OVERFLOW = 0x8302, + PF_USI_RS_EVENT = 0x8303, + /* 0x8304..0x8309 Reserved */ + PF_USI_PE_ENERGY_SAVING_STATUS = 0x8310, + /* 0x8311..0x8319 Reserved */ + PF_USI_PROCESS_ALARM_REASON = 0x8320, + /* 0x8321..0x8fff Reserved */ + /* 0x9000..0x9fff Reserved for profiles */ + /* 0xa000..0xffff Reserved */ +} pf_usi_values_t; + +/* + * The diag filter is used when reading record data in CMRDR. + */ +typedef enum pf_diag_filter_level +{ + PF_DIAG_FILTER_FAULT_STD, /* Insert all non-manufacturer specific */ + PF_DIAG_FILTER_FAULT_ALL, /* Manufacturer specific or fault */ + PF_DIAG_FILTER_ALL, /* Insert all diagnosis */ + PF_DIAG_FILTER_M_REQ, /* Manufacturer specific or maintenance required */ + PF_DIAG_FILTER_M_DEM /* Manufacturer specific or maintenance demanded */ +} pf_diag_filter_level_t; + +typedef struct pf_subslot +{ + bool in_use; + uint16_t subslot_nbr; + + /* Submodule plug information */ + uint32_t exp_submodule_ident_number; + uint32_t submodule_ident_number; + uint16_t length_input; + uint16_t length_output; + pnet_submodule_dir_t direction; + + /* Run-time information */ + pf_ar_t * p_ar; + pf_submod_state_t submodule_state; + + /* The following members shall be protected by the device.diag_mutex. */ + /* + * This is an index into device.diag_items[]. + * It points to the list of reported diag alarms for this specific sub-slot. + * + * Each subslot has its own list of diagnosis items. + */ + uint16_t diag_list; +} pf_subslot_t; + +typedef struct pf_slot +{ + bool in_use; + /* Module plug information */ + uint32_t exp_module_ident_number; + uint32_t module_ident_number; + pf_mod_plug_state_t plug_state; + pf_subslot_t subslots[PNET_MAX_SUBSLOTS]; + + /* Run-time information */ + pf_ar_t * p_ar; + uint16_t slot_nbr; +} pf_slot_t; + +typedef struct pf_api +{ + bool in_use; + uint32_t api_id; + + pf_slot_t slots[PNET_MAX_SLOTS]; + + pf_ar_t * p_ar; +} pf_api_t; + +/* + * The device struct contains information about the configured API's. + * The api member contains a hierarchy which may be traversed using + * the function pf_cmdev_cfg_traverse(). + */ +typedef struct pf_device +{ + /* + * Record things that are needed for the read/write record data service. + */ + pf_api_t apis[PNET_MAX_API]; + + /* + * This is the pool of diag items. + * It is used instead of dynamic memory to avoid fragmentation. + * + * Each subslot uses its own list of diag items, and stores the index to + * the head of its list. + */ + os_mutex_t * diag_mutex; /* Protect the diag items */ + pf_diag_item_t diag_items[PNET_MAX_DIAG_ITEMS]; + uint16_t diag_items_free; /* Head of the unused list */ +} pf_device_t; + +/* + * This enum is sometimes used to limit the depth when traversing + * the pf_device_t structure. + */ +typedef enum pf_dev_filter_level +{ + /* Order is important. */ + PF_DEV_FILTER_LEVEL_DEVICE, + PF_DEV_FILTER_LEVEL_API_ID, + PF_DEV_FILTER_LEVEL_API, + PF_DEV_FILTER_LEVEL_SLOT, + PF_DEV_FILTER_LEVEL_SUBSLOT, +} pf_dev_filter_level_t; + +/* =================== read/write data record structures ================= */ + +typedef struct pf_iod_read_request /* Read from dcontrol */ +{ + uint16_t sequence_number; + pf_uuid_t ar_uuid; + uint32_t api; + uint16_t slot_number; + uint16_t subslot_number; + uint8_t padding[2]; + uint16_t index; + uint32_t record_data_length; + pf_uuid_t target_ar_uuid; /* Only used if implicit AR */ + uint8_t rw_padding[8]; +} pf_iod_read_request_t; + +typedef struct pf_iod_read_result +{ + uint16_t sequence_number; + pf_uuid_t ar_uuid; + uint32_t api; + uint16_t slot_number; + uint16_t subslot_number; + uint8_t padding[2]; + uint16_t index; + uint32_t record_data_length; + uint16_t add_data_1; + uint16_t add_data_2; + uint8_t rw_padding[20]; +} pf_iod_read_result_t; + +typedef struct pf_iod_write_request /* Write from dcontrol */ +{ + uint16_t sequence_number; + pf_uuid_t ar_uuid; + uint32_t api; + uint16_t slot_number; + uint16_t subslot_number; + uint8_t padding[2]; + uint16_t index; + uint32_t record_data_length; + uint8_t rw_padding[24]; +} pf_iod_write_request_t; + +typedef struct pf_iod_write_result +{ + uint16_t sequence_number; + pf_uuid_t ar_uuid; + uint32_t api; + uint16_t slot_number; + uint16_t subslot_number; + uint8_t padding[2]; + uint16_t index; + uint32_t record_data_length; + uint16_t add_data_1; + uint16_t add_data_2; + pnet_pnio_status_t pnio_status; + uint8_t rw_padding[16]; +} pf_iod_write_result_t; + +/* ============= LogBook typedefs ================== */ +/* block_version_low == 1 */ + +typedef enum pf_ts_status_values +{ + PF_TS_STATUS_GLOBAL = 0, + PF_TS_STATUS_LOCAL = 1, + PF_TS_STATUS_LOCAL_ARB = 2, /* Arbitrary time scale */ +} pf_ts_status_values_t; + +typedef struct pf_log_book_ts +{ + uint16_t status; /* pf_ts_status_values_t */ + uint16_t sec_hi; + uint32_t sec_lo; + uint32_t nano_sec; +} pf_log_book_ts_t; + +/* + * p-net stack specific log book entry detail values. + * Values are not defined by Profinet specification + * but device/stack specific. + * See PN-AL-protocol (Mar20) 7.3.6 LogBook ASE + */ +typedef enum +{ + PF_LOG_BOOK_AR_CONNECT = 1, + PF_LOG_BOOK_AR_RELEASE, + PF_LOG_BOOK_DCONTROL +} pf_log_book_entry_detail_values_t; + +typedef struct pf_log_book_entry +{ + pf_log_book_ts_t time_ts; + pf_uuid_t ar_uuid; + pnet_pnio_status_t pnio_status; + uint32_t entry_detail; +} pf_log_book_entry_t; + +typedef struct pf_log_book +{ + pf_log_book_ts_t time_ts; /* Local timestamp when reading the logbook */ + pf_log_book_entry_t entries[PNET_MAX_LOG_BOOK_ENTRIES]; + uint16_t put; /* Points to oldest entry if wrap */ + bool wrap; /* All entries valid */ +} pf_log_book_t; + +typedef struct pf_ppm_driver +{ + /** + * Create a PPM driver instance. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the PPM driver instance was created. + * -1 if an error occurred. + */ + int (*create) (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + + /** + * Start a PPM driver instance. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the PPM driver instance was activated. + * -1 if an error occurred. + */ + int (*activate_req) (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + + /** + * Close and de-commission a PPM driver instance. + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the PPM driver instance was closed. + * -1 if an error occurred. + */ + int (*close_req) (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + + /** + * Set the data and IOPS for a sub-module. + * @param net InOut: The p-net stack instance + * @param iocr InOut: The IOCR instance. + * @param p_iodata In: Iodata object information + * @param data In: The application data. + * If NULL is passed, frame data is + * not updated. + * @param len In: The length of the application data. + * @param p_iops In: The IOPS of the application data. + * @param iops_len In: The length of the IOPS. + * @return 0 if the input data and IOPS was set. + * -1 if an error occurred. + */ + int (*write_data_and_iops) ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + const uint8_t * data, + uint16_t len, + const uint8_t * p_iops, + uint8_t iops_len); + + /** + * Retrieve the data and IOPS for a sub-module. + * + * Note that this is not from the PLC, but previously set by the application. + * + * @param net InOut: The p-net stack instance + * @param iocr InOut: The IOCR instance. + * @param p_iodata In: Iodata object information + * @param data Out: Data buffer. + * @param data_len In: The length of the data. + * @param p_iops Out: The IOPS of the application data. + * @param iops_len In: The size of iops. + * @return 0 if the input data and IOPS could e retrieved. + * -1 if an error occurred. + */ + int (*read_data_and_iops) ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len); + + /** + * Set IOCS for a sub-module. + * @param net InOut: The p-net stack instance + * @param iocr InOut: The IOCR instance. + * @param p_iodata In: Iodata object information + * @param iocs In: The IOCS of the application data. + * @param iocs_len In: The length of the IOCS data. + * @return 0 if the IOCS was set. + * -1 if an error occurred. + */ + int (*write_iocs) ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + const uint8_t * iocs, + uint8_t iocs_len); + + /** + * Retrieve IOCS for a sub-module. + * Note that this is not from the PLC, but previously set by the application. + * @param net InOut: The p-net stack instance + * @param iocr InOut: The IOCR instance. + * @param p_iodata In: Iodata object information + * @param iocs Out: The IOCS of the application data. + * @param iocs_len In: The size of iocs. + * @return 0 if the IOCS could be retrieved. + * -1 if an error occurred. + */ + int (*read_iocs) ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * iocs, + uint8_t iocs_len); + + /** + * Set the data status of the PPM connection. + * @param net InOut: The p-net stack instance + * @param iocr InOut: The IOCR instance. + * @param data_status In: The PPM data status. See + * pnet_data_status_bits_t for details. + * @return 0 if the data status could be retrieved. + * -1 if an error occurred. + */ + int ( + *write_data_status) (pnet_t * net, pf_iocr_t * iocr, uint8_t data_status); + + /* int (*read_data_status) (pf_ppm_t * p_ppm, uint8_t * p_data_status); */ + + /** + * Show PPM instance details + * @param p_ppm In: The PPM instance + */ + void (*show) (const pf_ppm_t * p_ppm); + +} pf_ppm_driver_t; + +typedef struct pf_cpm_driver +{ + /** + * Create a CPM for a specific IOCR instance. + * + * This function creates a CPM instance for the specified IOCR instance. + * Set the CPM state to W_START. + * Allocate the buffer mutex on first call. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the CPM instance was created. + * -1 if an error occurred. + */ + int (*create) (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + + /** + * Activate a CPM instance of the specified CR instance. + * + * Registers a frame handler for incoming Ethernet frames. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the CPM instance could be activated. + * -1 if an error occurred. + */ + int (*activate_req) (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + + /** + * Close a CPM instance. + * + * This function terminates the specified CPM instance. + * De-registers a frame handler for incoming Ethernet frames. + * The buffer mutex is destroyed on the last call. + * + * @param net InOut: The p-net stack instance + * @param p_ar InOut: The AR instance. + * @param crep In: The IOCR index. + * @return 0 if the CPM instance was closed. + * -1 if an error occurred. + */ + int (*close_req) (pnet_t * net, pf_ar_t * p_ar, uint32_t crep); + + /** + * Retrieve the specified sub-slot IOCS sent from the controller. + * User must supply a buffer large enough to hold the received IOCS. + * Maximum buffer size is 255 bytes. + * @param net InOut: The p-net stack instance + * @param iocr InOut: The IOCR instance. + * @param p_iodata In: Iodata object information + * @param p_iocs Out: Copy of the received IOCS. + * @param iocs_len In: Size of IOCS. + * @return 0 on success, -1 on error + */ + int (*get_iocs) ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + uint8_t * p_iocs, + uint8_t iocs_len); + + /** + * Retrieve the specified sub-slot data and IOPS received from the + * controller. + * + * User must supply a buffer large enough to hold the received + * data. Maximum buffer size is PF_FRAME_BUFFER_SIZE (1500) bytes. + * + * User must also supply a buffer large enough to hold the received IOPS. + * Maximum buffer size is 255 bytes. + * + * @param net InOut: The p-net stack instance + * @param iocr InOut: The IOCR instance. + * @param p_iodata In: Iodata object information + * @param p_new_flag Out: true means new valid data (and IOPS) frame + * available since last call. + * @param p_data Out: Copy of the received data. + * @param data_len In: Buffer size. + * @param p_iops Out: The received IOPS. + * @param iops_len In: Size of buffer at IOPS. + * @return 0 if the data and IOPS could be retrieved. + * -1 if an error occurred. + */ + int (*get_data_and_iops) ( + pnet_t * net, + pf_iocr_t * iocr, + const pf_iodata_object_t * p_iodata, + bool * p_new_flag, + uint8_t * p_data, + uint16_t data_len, + uint8_t * p_iops, + uint8_t iops_len); + + /** + * Get the data status of the CPM connection. + * @param p_cpm In: The CPM instance. + * @param p_data_status Out: The CPM data status. See + * pnet_data_status_bits_t for details. + * @return 0 on success, -1 on error + */ + int (*get_data_status) (const pf_cpm_t * p_cpm, uint8_t * p_data_status); + + /** + * Show CPM instance details + * @param net In: The p-net stack instance + * @param p_cpm In: The CPM instance + */ + void (*show) (const pnet_t * net, const pf_cpm_t * p_cpm); + +} pf_cpm_driver_t; + +/** + * + * Substitution name: PDPortDataCheck + * BlockHeader, Padding, Padding, SlotNumber, SubslotNumber, { [CheckPeers], + * [CheckLineDelay], [CheckMAUType a], [CheckLinkState], [CheckSyncDifference], + * [CheckMAUTypeDifference], [CheckMAUTypeExtension] b } + * + */ +typedef struct pf_port_data_check +{ + uint16_t slot_number; + uint16_t subslot_number; + pf_block_header_t block_header; +} pf_port_data_check_t; + +/** + * Substitution name : CheckPeers + * BlockHeader, NumberOfPeers, (LengthPeerPortName, PeerPortName, + * LengthPeerStationName, PeerStationName)*, [Padding*] + * + * CheckPeers contains an array of peers to check + */ +typedef struct pf_check_peer +{ + uint8_t length_peer_port_name; + uint8_t peer_port_name[PNET_LLDP_PORT_ID_MAX_SIZE]; /** Terminated */ + uint8_t length_peer_station_name; + uint8_t peer_station_name[PNET_STATION_NAME_MAX_SIZE]; /** Terminated */ +} pf_check_peer_t; + +typedef struct pf_check_peers +{ + uint8_t number_of_peers; + pf_check_peer_t peers[PF_CHECK_PEERS_PER_PORT]; +} pf_check_peers_t; + +/** + * Substitution name: PDPortDataAdjust + * BlockHeader, Padding, Padding, SlotNumber, SubslotNumber, { + * [AdjustDomainBoundary], [AdjustMulticastBoundary], [AdjustMAUType ^ + * AdjustLinkState], [AdjustPeerToPeerBoundary], [AdjustDCPBoundary], + * [AdjustPreambleLength], [AdjustMAUTypeExtension] a } + */ +typedef struct pf_port_data_adjust +{ + uint16_t slot_number; + uint16_t subslot_number; + pf_block_header_t block_header; +} pf_port_data_adjust_t; + +/* + * 0x00 The field LLDPChassis contains the NameOfStation and + * the field PortID of LLDP contains the name of the port. + * 0x01 The field LLDPChassis contains the NameOfDevice + * and is defined by local means. The field PortID of LLDP + * contains the name of the port and the NameOfStation. + * + * Example: + * Legacy LLDP mode Standard LLDP mode + * Version v2.4 0x00 v2.4 0x01 + * NameOfStation dut dut + * NameOfPort port-001 port-001 + * LLDP_PortID port-001 port-001.dut + * LLDP_ChassisID dut (or mac) system description + */ +typedef enum +{ + PF_LLDP_NAME_OF_DEVICE_MODE_LEGACY = 0, + PF_LLDP_NAME_OF_DEVICE_MODE_STANDARD = 1 +} pf_lldp_name_of_device_mode_t; + +/** + * Substitution name: PeerToPeerBoundary + * PN-AL-protocol (Mar20) Table 722 + */ +typedef struct pf_peer_to_peer_boundary +{ + uint32_t do_not_send_LLDP_frames : 1; /* 1: The LLDP agent shall not send + LLDP frames */ + uint32_t do_not_send_pctp_delay_request_frames : 1; /* 1: The PTCP ASE shall + not send PTCP_DELAY + request frames */ + uint32_t do_not_send_path_delay_request_frames : 1; /* 1: The Time ASE shall + not send PATH_DELAY + request frames */ + uint32_t reserved : 29; +} pf_peer_to_peer_boundary_t; + +/** + * Substitution name: AdjustPeerToPeerBoundary + * BlockHeader, Padding, Padding, PeerToPeerBoundary, AdjustProperties, + * [Padding*] + */ +typedef struct pf_port_data_adjust_peer_to_peer_boundary +{ + pf_peer_to_peer_boundary_t peer_to_peer_boundary; + uint16_t adjust_properties; /* Always 0, See PN-AL-protocol (Mar20) + Ch.5.2.13.14 */ +} pf_adjust_peer_to_peer_boundary_t; + +typedef struct pf_pdport +{ + bool lldp_peer_info_updated; + + struct + { + bool active; /* Todo maybe a bitmask for different checks*/ + pf_check_peer_t peer; + } check; + struct + { + bool active; /* Todo maybe a bitmask for different checks*/ + pf_adjust_peer_to_peer_boundary_t peer_to_peer_boundary; + } adjust; +} pf_pdport_t; + +typedef struct pf_lldp_port +{ + /* LLDP peer information + * + * The information may be changed anytime as incoming LLDP packet arrives. + * + * Protected by LLDP mutex. + */ + pf_lldp_peer_info_t peer_info; + + /* Timestamp for when LLDP packet with new content was received. + * + * Value is system uptime, in units of 10 milliseconds. + * + * Units are the same as sysUptime in SNMP. + * Protected by LLDP mutex. + */ + uint32_t timestamp_for_last_peer_change; + + /* Scheduler handle for LLDP receive timeout */ + pf_scheduler_handle_t rx_timeout; + + /* Scheduler handle for periodic LLDP sending */ + pf_scheduler_handle_t tx_timeout; + + /* Is information about peer device received? + * + * Information is received in LLDP packets. + * Protected by LLDP mutex. + */ + bool is_peer_info_received; +} pf_lldp_port_t; + +/** Network interface */ +typedef struct pf_netif +{ + /** Terminated string */ + char name[PNET_INTERFACE_NAME_MAX_SIZE]; + + pnet_ethaddr_t mac_address; + pnal_eth_handle_t * handle; + bool previous_is_link_up; +} pf_netif_t; + +/** + * Port runtime data + */ +typedef struct pf_port +{ + pf_netif_t netif; + + /** Port name, for example port-001 + * Terminated string */ + char port_name[PNET_PORT_NAME_MAX_SIZE]; + + uint8_t port_num; + pf_pdport_t pdport; + pf_lldp_port_t lldp; + pnal_eth_status_t eth_status; /* Updated by background task */ +} pf_port_t; + +typedef struct pf_snmp_data +{ + pf_snmp_system_contact_t system_contact; + pf_snmp_system_name_t system_name; + pf_snmp_system_location_t system_location; +} pf_snmp_data_t; + +struct pnet +{ + uint32_t pnal_buf_alloc_cnt; + bool global_alarm_enable; + + /********** CPM **********/ + + os_mutex_t * cpm_buf_lock; + atomic_int cpm_instance_cnt; + + /********** PPM **********/ + + os_mutex_t * ppm_buf_lock; + atomic_int ppm_instance_cnt; + + /********** DCP **********/ + + uint16_t dcp_global_block_qualifier; + + /** Source address (MAC) of current DCP remote peer */ + pnet_ethaddr_t dcp_sam; + + /** A response to DCP IDENTIFY is waiting to be sent */ + bool dcp_delayed_response_waiting; + + pf_scheduler_handle_t dcp_led_timeout; + pf_scheduler_handle_t dcp_sam_timeout; + pf_scheduler_handle_t dcp_identresp_timeout; + + /********** Profinet frame ID mapping **********/ + + pf_eth_frame_id_map_t eth_id_map[PF_ETH_MAX_MAP]; + volatile pf_scheduler_timeouts_t scheduler_timeouts[PF_MAX_TIMEOUTS]; + volatile uint32_t scheduler_timeout_first; + volatile uint32_t scheduler_timeout_free; + os_mutex_t * scheduler_timeout_mutex; + uint32_t scheduler_tick_interval; /* microseconds */ + + /********** CMDEV **********/ + + bool cmdev_initialized; + + /** APIs and diag items */ + pf_device_t cmdev_device; + + /********** CMINA **********/ + + /** Reflects what is/should be stored in NVM */ + pf_cmina_dcp_ase_t cmina_nonvolatile_dcp_ase; + + /** Reflects current settings (possibly not yet committed) */ + pf_cmina_dcp_ase_t cmina_current_dcp_ase; + + os_mutex_t * cmina_mutex; + pf_cmina_state_values_t cmina_state; + uint8_t cmina_error_decode; + uint8_t cmina_error_code_1; + uint16_t cmina_hello_count; + pf_scheduler_handle_t cmina_hello_timeout; + + bool cmina_commit_ip_suite; + + /********** CMRPC **********/ + + os_mutex_t * p_cmrpc_rpc_mutex; + uint32_t cmrpc_session_number; + + /** ARs */ + pf_ar_t cmrpc_ar[PNET_MAX_AR]; + + /** Sessions */ + pf_session_info_t cmrpc_session_info[PF_MAX_SESSION]; + + /** Main socket for incoming requests */ + int cmrpc_rpcreq_socket; + + uint8_t cmrpc_dcerpc_input_frame[PF_FRAME_BUFFER_SIZE]; + uint8_t cmrpc_dcerpc_output_frame[PF_FRAME_BUFFER_SIZE]; + + /********** FSPM **********/ + + /** Default configuration from user. Used at factory reset */ + const pnet_cfg_t * p_fspm_default_cfg; + + /** Configuration from user. Might be updated by stack during runtime */ + pnet_cfg_t fspm_cfg; + + pf_log_book_t fspm_log_book; + os_mutex_t * fspm_log_book_mutex; + + /** Last \a time pnet_handle_periodic() was invoked */ + uint32_t timestamp_handle_periodic_us; + + /* Mutex for protecting access to writable I&M data. + * + * Note I&M may be both read and written by SNMP, which executes from + * its own thread context. + */ + os_mutex_t * fspm_im_mutex; + + /********** LLDP **********/ + + /* LLDP mutex + * + * This mutex protects information about the peer device. + */ + os_mutex_t * lldp_mutex; + + /* Interface data + * + * Interface and ports runtime data. + */ + struct + { + pf_netif_t main_port; /* Management port */ + struct + { + bool active; + pf_lldp_name_of_device_mode_t mode; + } name_of_device_mode; + pf_port_t port[PNET_MAX_PHYSICAL_PORTS]; + pf_port_iterator_t link_monitor_iterator; + + /* Mutex for protecting port data. + * Common for all ports. + */ + os_mutex_t * port_mutex; + + /* Scheduler handle for Ethernet link monitoring */ + pf_scheduler_handle_t link_monitor_timeout; + } pf_interface; + + struct + { + os_event_t * events; + } pf_bg_worker; + + const pf_ppm_driver_t * ppm_drv; + const pf_cpm_driver_t * cpm_drv; + + /** Handle to HW offload driver. NULL if not used */ + pf_drv_t * hwo_drv; + +#if PNET_OPTION_SNMP + pf_snmp_data_t snmp_data; +#endif +}; + +/** + * @internal + * Initialise a pnet_t structure into already allocated memory. + * + * @param net InOut: The p-net stack instance to be initialised. + * @param p_cfg In: Profinet configuration. These values are used + * at first startup and at factory reset. + * @return 0 on success. + * -1 if an error occurred. + */ +int pnet_init_only (pnet_t * net, const pnet_cfg_t * p_cfg); + +#ifdef __cplusplus +} +#endif + +#endif /* PF_TYPES_H */ diff --git a/profinet_stack/p-net/src/pnal.h b/profinet_stack/p-net/src/pnal.h new file mode 100644 index 0000000..03b89c7 --- /dev/null +++ b/profinet_stack/p-net/src/pnal.h @@ -0,0 +1,552 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Definitions for profinet abstraction layer. + * + * The Ethernet-related functions should not use \a loc_port_num, but + * the interface name instead. The lookup from \a loc_port_num to + * interface name must happen somewhere else in the stack. + */ + +#ifndef PNAL_H +#define PNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "options.h" +#include "pnet_api.h" +#include "pnal_sys.h" + +#define PNAL_MAKEU32(a, b, c, d) \ + (((uint32_t)((a)&0xff) << 24) | ((uint32_t)((b)&0xff) << 16) | \ + ((uint32_t)((c)&0xff) << 8) | (uint32_t)((d)&0xff)) + +#define PNAL_MAKEU16(a, b) (((uint16_t)((a)&0xff) << 8) | (uint16_t)((b)&0xff)) + +#define PNAL_INET_ADDRSTR_SIZE 16 /** Incl termination */ +#define PNAL_ETH_ADDRSTR_SIZE 18 /** Incl termination */ +#ifndef PNAL_HOSTNAME_MAX_SIZE +#define PNAL_HOSTNAME_MAX_SIZE 64 /** Incl termination. Value from Linux. */ +#endif + +/** + * Ethernet autonegotiation capabilities (not exhaustive) + * + * See IEEE 802.3 (Ethernet) ch. 30.6 "Management for link Auto-Negotiation". + * + * See IETF RFC 4836 "Definitions of Managed Objects for + * IEEE 802.3 Medium Attachment Units (MAUs)", object + * "IANAifMauAutoNegCapBits" + */ +#define PNAL_ETH_AUTONEG_CAP_1000BaseT_FULL_DUPLEX BIT (0) +#define PNAL_ETH_AUTONEG_CAP_1000BaseT_HALF_DUPLEX BIT (1) +#define PNAL_ETH_AUTONEG_CAP_1000BaseX_FULL_DUPLEX BIT (2) +#define PNAL_ETH_AUTONEG_CAP_1000BaseX_HALF_DUPLEX BIT (3) +#define PNAL_ETH_AUTONEG_CAP_100BaseTX_FULL_DUPLEX BIT (10) +#define PNAL_ETH_AUTONEG_CAP_100BaseTX_HALF_DUPLEX BIT (11) +#define PNAL_ETH_AUTONEG_CAP_10BaseT_FULL_DUPLEX BIT (13) +#define PNAL_ETH_AUTONEG_CAP_10BaseT_HALF_DUPLEX BIT (14) +#define PNAL_ETH_AUTONEG_CAP_UNKNOWN BIT (15) + +/** + * Ethernet MAU type (not exhaustive). + * + * See IEEE 802.3 (Ethernet) ch. 30.5 "Layer management for medium + * attachment units (MAUs)". + * + * See IETF RFC 4836 "Definitions of Managed Objects for + * IEEE 802.3 Medium Attachment Units (MAUs)", objects + * "IANAifMauTypeListBits" and "dot3MauType". + * + * See Profinet "5.2.12.3.12 Coding of the field MAUType" + */ +typedef enum pnal_eth_mau +{ + PNAL_ETH_MAU_UNKNOWN = 0x00, /* When link is down */ + PNAL_ETH_MAU_RADIO = 0x00, /* When link is up */ + PNAL_ETH_MAU_COPPER_10BaseT = 0x05, + PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX = 0x0F, + PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX = 0x10, + PNAL_ETH_MAU_COPPER_1000BaseT_HALF_DUPLEX = 0x1D, + PNAL_ETH_MAU_COPPER_1000BaseT_FULL_DUPLEX = 0x1E, + PNAL_ETH_MAU_FIBER_100BaseFX_HALF_DUPLEX = 0x11, + PNAL_ETH_MAU_FIBER_100BaseFX_FULL_DUPLEX = 0x12, + PNAL_ETH_MAU_FIBER_1000BaseX_HALF_DUPLEX = 0x15, + PNAL_ETH_MAU_FIBER_1000BaseX_FULL_DUPLEX = 0x16, +} pnal_eth_mau_t; + +/* See Profinet 2.4 section 5.2.28 and annex W */ +typedef struct pnal_port_stats +{ + uint32_t if_in_octets; + uint32_t if_out_octets; + uint32_t if_in_discards; + uint32_t if_out_discards; + uint32_t if_in_errors; + uint32_t if_out_errors; +} pnal_port_stats_t; + +/** Set an IP address given by the four byte-parts */ +#define PNAL_IP4_ADDR_TO_U32(ipaddr, a, b, c, d) \ + ipaddr = PNAL_MAKEU32 (a, b, c, d) + +typedef enum pnal_ethertype +{ + PNAL_ETHTYPE_IP = 0x0800U, + PNAL_ETHTYPE_ARP = 0x0806U, + PNAL_ETHTYPE_VLAN = 0x8100U, + PNAL_ETHTYPE_PROFINET = 0x8892U, + PNAL_ETHTYPE_ETHERCAT = 0x88A4U, + PNAL_ETHTYPE_LLDP = 0x88CCU, + PNAL_ETHTYPE_ALL = 0xFFFFU, +} pnal_ethertype_t; + +/* 255.255.255.255 */ +#ifndef PNAL_IPADDR_NONE +#define PNAL_IPADDR_NONE ((uint32_t)0xffffffffUL) +#endif +/* 127.0.0.1 */ +#ifndef PNAL_IPADDR_LOOPBACK +#define PNAL_IPADDR_LOOPBACK ((uint32_t)0x7f000001UL) +#endif +/* 0.0.0.0 */ +#ifndef PNAL_IPADDR_ANY +#define PNAL_IPADDR_ANY ((uint32_t)0x00000000UL) +#endif +#define PNAL_IPADDR_INVALID PNAL_IPADDR_ANY +/* 255.255.255.255 */ +#ifndef PNAL_IPADDR_BROADCAST +#define PNAL_IPADDR_BROADCAST ((uint32_t)0xffffffffUL) +#endif + +typedef uint32_t pnal_ipaddr_t; +typedef uint16_t pnal_ipport_t; + +/** + * The Ethernet MAC address. + * + * TODO: Remove this and use pnet_ethaddr_t instead + */ +typedef struct pnal_ethaddr +{ + uint8_t addr[6]; +} pnal_ethaddr_t; + +/** + * Ethernet link status. + * + * See also type pf_lldp_link_status_t. + */ +typedef struct pnal_eth_status_t +{ + /* Is autonegotiation supported on this port? */ + bool is_autonegotiation_supported; + + /* Is autonegotiation supported on this port? */ + bool is_autonegotiation_enabled; + + /* Capabilities advertised to link partner during autonegotiation. + * + * See macros PNAL_ETH_AUTONEG_CAP_xxx. + */ + uint16_t autonegotiation_advertised_capabilities; + + /* Operational MAU type. + * + * This specifies both the physical medium as well as + * speed and duplex setting used for the link. + */ + pnal_eth_mau_t operational_mau_type; + + /* Corresponds to linux "RUNNING" flag. + * Up and cable connected + */ + bool running; +} pnal_eth_status_t; + +/** + * Get system uptime from the SNMP implementation. + * + * This is the sysUpTime, as used by SNMP: + * "The time (in hundredths of a second) since the network + * management portion of the system was last re-initialized." + * - IETF RFC 3418 (SNMP MIB-2). + * + * Typically used to store the SNMP-timestamp for incoming LLDP-packets. + * This timestamp is later reported back to the SNMP implementation, and + * remote SNMP managers might use it for filtering. + * + * The returned value should be the one used by the SNMP implementation. + * Ask the SNMP implementation for the value, or make sure that this + * function and the SNMP implementation use the same mechanism to get the + * value. + * + * Starts at 0, with wrap-around after ~497 days. + * + * @return System uptime, in units of 10 milliseconds. + */ +uint32_t pnal_get_system_uptime_10ms (void); + +/** + * Load a binary file. + * + * Can load the data into two buffers. + * + * @param fullpath In: Full path to the file + * @param object_1 Out: Data to load, or NULL. Mandatory if size_1 > 0 + * @param size_1 In: Size of object_1. + * @param object_2 Out: Data to load, or NULL. Mandatory if size_2 > 0 + * @param size_2 In: Size of object_2. + * @return 0 if the operation succeeded. + * -1 if not found or an error occurred. + */ +int pnal_load_file ( + const char * fullpath, + void * object_1, + size_t size_1, + void * object_2, + size_t size_2); + +/** + * Save a binary file. + * + * Can handle two output buffers. + * + * @param fullpath In: Full path to the file + * @param object_1 In: Data to save, or NULL. Mandatory if size_1 > 0 + * @param size_1 In: Size of object_1. + * @param object_2 In: Data to save, or NULL. Mandatory if size_2 > 0 + * @param size_2 In: Size of object_2. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pnal_save_file ( + const char * fullpath, + const void * object_1, + size_t size_1, + const void * object_2, + size_t size_2); + +/** + * Clear a binary file. + * + * @param fullpath In: Full path to the file + */ +void pnal_clear_file (const char * fullpath); + +/* + */ + +/** + * Allocate a buffer + * + * The resulting \a pnal_buf_t buffer is defined for + * each operating system, and should at least contain: + * void * payload; + * uint16_t len; + * + * @param length In: Length, in bytes + * @return a pnal_buf_t, or NULL at failure + */ +pnal_buf_t * pnal_buf_alloc (uint16_t length); + +/** + * Free a buffer + * + * @param p In: Buffer to free + */ +void pnal_buf_free (pnal_buf_t * p); + +/** Not yet used */ +uint8_t pnal_buf_header (pnal_buf_t * p, int16_t header_size_increment); + +/** + * Network interface handle, forward declaration. + */ +typedef struct pnal_eth_handle pnal_eth_handle_t; + +/** + * The prototype of raw Ethernet reception call-back functions. + * + * @param eth_handle InOut: Network interface handle + * @param arg InOut: User-defined (may be NULL). + * @param p_buf InOut: The incoming Ethernet frame + * + * @return 0 If the frame was NOT handled by this function. + * 1 If the frame was handled and the buffer freed. + */ +typedef int (pnal_eth_callback_t) ( + pnal_eth_handle_t * eth_handle, + void * arg, + pnal_buf_t * p_buf); + +/** + * Get status of Ethernet link on specified port + * + * @param interface_name In: Ethernet interface name, for example eth0 + * @param status Out: Returned link status (autoneg etc) + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pnal_eth_get_status ( + const char * interface_name, + pnal_eth_status_t * status); + +/** + * Get network interface index + * + * The interface index (ifIndex) is the (row) index for the network interface + * in the table ifTable, which is part of the SNMP MIB-II data structure. + * See RFC 2863 "The Interfaces Group MIB". + * + * @param interface_name In: Ethernet interface name, for example eth0 + * + * @return The interface index, or 0 if not available. + */ +int pnal_get_interface_index (const char * interface_name); + +/** + * Get network interface (port) statistics + * + * In Profinet naming a physical Ethernet interface is a "port". + * This corresponds to for example a Linux Ethernet interface. + * + * @param interface_name In: Ethernet interface name for example eth0 + * @param port_stats Out: Returned statistics + * + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pnal_get_port_statistics ( + const char * interface_name, + pnal_port_stats_t * port_stats); + +/** + * Send raw Ethernet data + * + * @param handle In: Ethernet handle + * @param buf In: Buffer with data to be sent + * @return The number of bytes sent, or -1 if an error occurred. + */ +int pnal_eth_send (pnal_eth_handle_t * handle, pnal_buf_t * buf); + +/** + * Initialize receiving of raw Ethernet frames on one interface (in separate + * thread) + * + * @param if_name In: Ethernet interface name + * @param receive_type In: Ethernet frame types that shall be received + * by the network interface / port. + * @param pnal_cfg In: Operating system dependent configuration + * @param callback In: Callback for received raw Ethernet frames + * @param arg InOut: User argument passed to the callback + * + * @return the Ethernet handle, or NULL if an error occurred. + */ +pnal_eth_handle_t * pnal_eth_init ( + const char * if_name, + pnal_ethertype_t receive_type, + const pnal_cfg_t * pnal_cfg, + pnal_eth_callback_t * callback, + void * arg); + +/** + * Open an UDP socket + * + * @param addr In: IP address to listen to. Typically used with + * PNAL_IPADDR_ANY. + * @param port In: UDP port to listen to. + * @return Socket ID, or -1 if an error occurred. + */ +int pnal_udp_open (pnal_ipaddr_t addr, pnal_ipport_t port); + +/** + * Send UDP data + * + * @param id In: Socket ID + * @param dst_addr In: Destination IP address + * @param dst_port In: Destination UDP port + * @param data In: Data to be sent + * @param size In: Size of data + * @return The number of bytes sent, or -1 if an error occurred. + */ +int pnal_udp_sendto ( + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size); + +/** + * Receive UDP data. + * + * This is a nonblocking function, and it + * returns 0 immediately if no data is available. + * + * @param id In: Socket ID + * @param dst_addr Out: Source IP address + * @param dst_port Out: Source UDP port + * @param data Out: Received data + * @param size In: Size of buffer for received data + * @return The number of bytes received, or -1 if an error occurred. + */ +int pnal_udp_recvfrom ( + uint32_t id, + pnal_ipaddr_t * src_addr, + pnal_ipport_t * src_port, + uint8_t * data, + int size); + +/** + * Close an UDP socket + * + * @param id In: Socket ID + */ +void pnal_udp_close (uint32_t id); + +/** + * Configure SNMP server. + * + * This function configures a platform-specific SNMP server as to + * enable a connected SNMP client to read variables from the p-net stack, + * as well as to write some variables to it. + * + * @param net InOut: The p-net stack instance + * @param pnal_cfg In: Operating system dependent configuration + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pnal_snmp_init (pnet_t * net, const pnal_cfg_t * pnal_cfg); + +/** + * Get network parameters (IP address, netmask etc) + * + * For example: + * + * IP address Represented by + * 1.0.0.0 0x01000000 = 16777216 + * 0.0.0.1 0x00000001 = 1 + * + * @param interface_name In: Ethernet interface name, for example eth0 + * @param p_ipaddr Out: IPv4 address + * @param p_netmask Out: Netmask + * @param p_gw Out: Default gateway + * @param hostname Out: Host name, for example my_laptop_4. Existing + * buffer should have size + * PNAL_HOSTNAME_MAX_SIZE. + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pnal_get_ip_suite ( + const char * interface_name, + pnal_ipaddr_t * p_ipaddr, + pnal_ipaddr_t * p_netmask, + pnal_ipaddr_t * p_gw, + char * hostname); + +/** + * Read the IP address as an integer. For IPv4. + * + * For example: + * + * IP address Represented by + * 1.0.0.0 0x01000000 = 16777216 + * 0.0.0.1 0x00000001 = 1 + * + * @param interface_name In: Name of network interface + * @return IP address on success and + * 0 if an error occurred + */ +pnal_ipaddr_t pnal_get_ip_address (const char * interface_name); + +/** + * Read the netmask as an integer. For IPv4. + * + * @param interface_name In: Name of network interface + * @return netmask + */ +pnal_ipaddr_t pnal_get_netmask (const char * interface_name); + +/** + * Read the default gateway address as an integer. For IPv4. + * + * Assumes the default gateway is found on .1 on same subnet as the IP address. + * + * @param interface_name In: Name of network interface + * @return netmask + */ +pnal_ipaddr_t pnal_get_gateway (const char * interface_name); + +/** + * Read the MAC address. + * + * @param interface_name In: Name of network interface + * @param mac_addr Out: MAC address + * + * @return 0 on success and + * -1 if no such interface is available + */ +int pnal_get_macaddress (const char * interface_name, pnal_ethaddr_t * p_mac); + +/** + * Read the current host name + * + * @param hostname Out: Host name, for example my_laptop_4. Existing + * buffer should have size + * PNAL_HOSTNAME_MAX_SIZE. + * @return 0 on success and + * -1 if an error occurred + */ +int pnal_get_hostname (char * hostname); + +/** + * Set network parameters (IP address, netmask etc) + * + * For example: + * + * IP address Represented by + * 1.0.0.0 0x01000000 = 16777216 + * 0.0.0.1 0x00000001 = 1 + * + * @param interface_name In: Ethernet interface name, for example eth0 + * @param p_ipaddr In: IPv4 address + * @param p_netmask In: Netmask + * @param p_gw In: Default gateway + * @param hostname In: Host name, for example my_laptop_4 + * @param permanent In: 1 if changes are permanent, or 0 if temporary + * @return 0 if the operation succeeded. + * -1 if an error occurred. + */ +int pnal_set_ip_suite ( + const char * interface_name, + const pnal_ipaddr_t * p_ipaddr, + const pnal_ipaddr_t * p_netmask, + const pnal_ipaddr_t * p_gw, + const char * hostname, + bool permanent); + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_H */ diff --git a/profinet_stack/p-net/src/ports/STM32Cube/pnal.c b/profinet_stack/p-net/src/ports/STM32Cube/pnal.c new file mode 100644 index 0000000..2ef6eea --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/pnal.c @@ -0,0 +1,302 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pnal.h" + +#include "options.h" +#include "osal.h" +#include "osal_log.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +int pnal_set_ip_suite ( + const char * interface_name, + const pnal_ipaddr_t * p_ipaddr, + const pnal_ipaddr_t * p_netmask, + const pnal_ipaddr_t * p_gw, + const char * hostname, + bool permanent) +{ + ip_addr_t ip_addr; + ip_addr_t ip_mask; + ip_addr_t ip_gw; + + ip_addr.addr = htonl (*p_ipaddr); + ip_mask.addr = htonl (*p_netmask); + ip_gw.addr = htonl (*p_gw); + netif_set_addr (netif_default, &ip_addr, &ip_mask, &ip_gw); + + return 0; +} + +int pnal_get_macaddress (const char * interface_name, pnal_ethaddr_t * mac_addr) +{ + memcpy (mac_addr, netif_default->hwaddr, sizeof (pnal_ethaddr_t)); + return 0; +} + +pnal_ipaddr_t pnal_get_ip_address (const char * interface_name) +{ + return htonl (netif_default->ip_addr.addr); +} + +pnal_ipaddr_t pnal_get_netmask (const char * interface_name) +{ + return htonl (netif_default->netmask.addr); +} + +pnal_ipaddr_t pnal_get_gateway (const char * interface_name) +{ + /* TODO Read the actual default gateway */ + + pnal_ipaddr_t ip; + pnal_ipaddr_t gateway; + + ip = pnal_get_ip_address (interface_name); + gateway = (ip & 0xFFFFFF00) | 0x00000001; + + return gateway; +} + +int pnal_get_hostname (char * hostname) +{ + strcpy (hostname, netif_default->hostname); + return 0; +} + +int pnal_get_ip_suite ( + const char * interface_name, + pnal_ipaddr_t * p_ipaddr, + pnal_ipaddr_t * p_netmask, + pnal_ipaddr_t * p_gw, + char * hostname) +{ + int ret = -1; + + *p_ipaddr = pnal_get_ip_address (interface_name); + *p_netmask = pnal_get_netmask (interface_name); + *p_gw = pnal_get_gateway (interface_name); + ret = pnal_get_hostname (hostname); + + return ret; +} + +int pnal_get_port_statistics ( + const char * interface_name, + pnal_port_stats_t * port_stats) +{ + port_stats->if_in_octets = netif_default->mib2_counters.ifinoctets; + port_stats->if_in_errors = netif_default->mib2_counters.ifinerrors; + port_stats->if_in_discards = netif_default->mib2_counters.ifindiscards; + port_stats->if_out_octets = netif_default->mib2_counters.ifoutoctets; + port_stats->if_out_errors = netif_default->mib2_counters.ifouterrors; + port_stats->if_out_discards = netif_default->mib2_counters.ifoutdiscards; + + return 0; +} + +int pnal_get_interface_index (const char * interface_name) +{ + return 0; +} + +int pnal_eth_get_status (const char * interface_name, pnal_eth_status_t * status) +{ + status->is_autonegotiation_supported = false; + status->is_autonegotiation_enabled = false; + status->autonegotiation_advertised_capabilities = 0; + + status->operational_mau_type = PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + status->running = true; + + return 0; +} + +int pnal_save_file ( + const char * fullpath, + const void * object_1, + size_t size_1, + const void * object_2, + size_t size_2) +{ + FIL fil; + FRESULT fres; + UINT count; + int ret = 0; /* Assume everything goes well */ + + if (!SDFatFSMounted) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): SD-Card not mounted (%s)\n", + __LINE__, + fullpath); + return -1; + } + + fres = f_open (&fil, fullpath, FA_WRITE | FA_OPEN_ALWAYS | FA_CREATE_ALWAYS); + if (fres != FR_OK) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Could not open file %s\n", + __LINE__, + fullpath); + return -1; + } + + /* Write file contents */ + if (size_1 > 0) + { + fres = f_write (&fil, object_1, size_1, &count); + if (fres != FR_OK || count != size_1) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to write file %s\n", + __LINE__, + fullpath); + } + } + if (size_2 > 0 && ret == 0) + { + fres = f_write (&fil, object_2, size_2, &count); + if (fres != FR_OK || count != size_2) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to write file %s (second buffer)\n", + __LINE__, + fullpath); + } + } + + f_close (&fil); + return ret; +} + +void pnal_clear_file (const char * fullpath) +{ + if (!SDFatFSMounted) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): SD-Card not mounted (%s)\n", + __LINE__, + fullpath); + return; + } + LOG_DEBUG (PF_PNAL_LOG, "PNAL(%d): Clearing file %s\n", __LINE__, fullpath); + f_unlink (fullpath); +} + +int pnal_load_file ( + const char * fullpath, + void * object_1, + size_t size_1, + void * object_2, + size_t size_2) +{ + FIL fil; + FRESULT fres; + UINT count; + int ret = 0; /* Assume everything goes well */ + + if (!SDFatFSMounted) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): SD-Card not mounted (%s)\n", + __LINE__, + fullpath); + return -1; + } + + fres = f_open (&fil, fullpath, FA_READ); + if (fres != FR_OK) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Could not yet open file %s\n", + __LINE__, + fullpath); + return -1; + } + + /* Write file contents */ + if (size_1 > 0) + { + fres = f_read (&fil, object_1, size_1, &count); + if (fres != FR_OK || count != size_1) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to read file %s\n", + __LINE__, + fullpath); + } + } + if (size_2 > 0 && ret == 0) + { + fres = f_read (&fil, object_2, size_2, &count); + if (fres != FR_OK || count != size_2) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to read file %s (second buffer)\n", + __LINE__, + fullpath); + } + } + + f_close (&fil); + return ret; +} + +uint32_t pnal_get_system_uptime_10ms (void) +{ + uint32_t uptime = 0; + + MIB2_COPY_SYSUPTIME_TO (&uptime); + return uptime; +} + +pnal_buf_t * pnal_buf_alloc (uint16_t length) +{ + return pbuf_alloc (PBUF_RAW, length, PBUF_POOL); +} + +void pnal_buf_free (pnal_buf_t * p) +{ + CC_ASSERT (pbuf_free (p) == 1); +} + +uint8_t pnal_buf_header (pnal_buf_t * p, int16_t header_size_increment) +{ + return pbuf_header (p, header_size_increment); +} diff --git a/profinet_stack/p-net/src/ports/STM32Cube/pnal_config.h b/profinet_stack/p-net/src/ports/STM32Cube/pnal_config.h new file mode 100644 index 0000000..3eed49e --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/pnal_config.h @@ -0,0 +1,49 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief PNAL-specific configuration + * + * This file contains definitions of configuration settings for the + * PNAL layer. + */ + +#ifndef PNAL_CONFIG_H +#define PNAL_CONFIG_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct pnal_thread_cfg +{ + uint32_t prio; + size_t stack_size; +} pnal_thread_cfg_t; + +typedef struct pnal_cfg +{ + pnal_thread_cfg_t bg_worker_thread; +} pnal_cfg_t; + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_CONFIG_H */ diff --git a/profinet_stack/p-net/src/ports/STM32Cube/pnal_eth.c b/profinet_stack/p-net/src/ports/STM32Cube/pnal_eth.c new file mode 100644 index 0000000..1c301b2 --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/pnal_eth.c @@ -0,0 +1,179 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief STM32Cube Ethernet related functions that use \a pnal_eth_handle_t + */ + +#include "pnal.h" +#include "osal_log.h" + +#include +#include + +#define MAX_NUMBER_OF_IF 1 + +struct pnal_eth_handle +{ + struct netif * netif; + pnal_eth_callback_t * eth_rx_callback; + void * arg; +}; + +static pnal_eth_handle_t interface[MAX_NUMBER_OF_IF]; +static int nic_index = 0; + +/** + * Find PNAL network interface handle + * + * @param netif In: lwip network interface. + * @return PNAL network interface handle corresponding to \a netif, + * NULL otherwise. + */ +static pnal_eth_handle_t * pnal_eth_find_handle (struct netif * netif) +{ + pnal_eth_handle_t * handle; + int i; + + for (i = 0; i < MAX_NUMBER_OF_IF; i++) + { + handle = &interface[i]; + if (handle->netif == netif) + { + return handle; + } + } + + return NULL; +} + +/** + * Allocate PNAL network interface handle + * + * Handles are allocated from a static array and need never be freed. + * + * @return PNAL network interface handle if available, + * NULL if too many handles were allocated. + */ +static pnal_eth_handle_t * pnal_eth_allocate_handle (void) +{ + pnal_eth_handle_t * handle; + + if (nic_index < MAX_NUMBER_OF_IF) + { + handle = &interface[nic_index]; + nic_index++; + return handle; + } + else + { + return NULL; + } +} + +/** + * Process received Ethernet frame + * + * Called from lwip when an Ethernet frame is received with an EtherType + * lwip is not aware of (e.g. Profinet and LLDP). + * + * @param p_buf InOut: Packet buffer containing Ethernet frame. + * @param netif InOut: Network interface receiving the frame. + * @return ERR_OK if frame was processed and freed, + * ERR_IF if it was ignored. + */ +static err_t pnal_eth_sys_recv (struct pbuf * p_buf, struct netif * netif) +{ + int processed; + pnal_eth_handle_t * handle; + + handle = pnal_eth_find_handle (netif); + if (handle == NULL) + { + /* p-net not started yet, let lwIP handle frame */ + return ERR_IF; + } + + processed = handle->eth_rx_callback (handle, handle->arg, p_buf); + if (processed) + { + /* Frame handled and freed */ + return ERR_OK; + } + else + { + /* Frame not handled */ + return ERR_IF; + } +} + +err_enum_t lwip_hook_unknown_eth_protocol ( + struct pbuf * pbuf, + struct netif * netif) +{ + return pnal_eth_sys_recv (pbuf, netif); +} + +pnal_eth_handle_t * pnal_eth_init ( + const char * if_name, + pnal_ethertype_t receive_type, + const pnal_cfg_t * pnal_cfg, + pnal_eth_callback_t * callback, + void * arg) +{ + pnal_eth_handle_t * handle; + struct netif * netif; + + (void)receive_type; /* Ignore, for now all frames will be received. */ + + netif = netif_find (if_name); + if (netif == NULL) + { + os_log (LOG_LEVEL_ERROR, "Network interface \"%s\" not found!\n", if_name); + return NULL; + } + + handle = pnal_eth_allocate_handle(); + if (handle == NULL) + { + os_log (LOG_LEVEL_ERROR, "Too many network interfaces\n"); + return NULL; + } + + handle->arg = arg; + handle->eth_rx_callback = callback; + handle->netif = netif; + + return handle; +} + +int pnal_eth_send (pnal_eth_handle_t * handle, pnal_buf_t * buf) +{ + int ret = -1; + + CC_ASSERT (handle->netif->linkoutput != NULL); + + /* TODO: Determine if buf could ever be NULL here */ + if (buf != NULL) + { + /* TODO: remove tot_len from os_buff */ + buf->tot_len = buf->len; + + handle->netif->linkoutput (handle->netif, buf); + ret = buf->len; + } + return ret; +} diff --git a/profinet_stack/p-net/src/ports/STM32Cube/pnal_sys.h b/profinet_stack/p-net/src/ports/STM32Cube/pnal_sys.h new file mode 100644 index 0000000..ad9bfc4 --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/pnal_sys.h @@ -0,0 +1,35 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PNAL_SYS_H +#define PNAL_SYS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include /* For htonl etc */ +#include + +#define PNAL_BUF_MAX_SIZE PBUF_POOL_BUFSIZE + +/* Re-use lwIP pbuf for rt-kernel */ +typedef struct pbuf pnal_buf_t; + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_SYS_H */ diff --git a/profinet_stack/p-net/src/ports/STM32Cube/pnal_udp.c b/profinet_stack/p-net/src/ports/STM32Cube/pnal_udp.c new file mode 100644 index 0000000..0d0cd6c --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/pnal_udp.c @@ -0,0 +1,110 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pnal.h" +#include "osal_log.h" + +#include +#include + +int pnal_udp_open (pnal_ipaddr_t addr, pnal_ipport_t port) +{ + struct sockaddr_in local; + int id; + const int enable = 1; + + id = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (id == -1) + { + return -1; + } + + if (setsockopt (id, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof (enable)) != 0) + { + goto error; + } + + /* set IP and port number */ + local = (struct sockaddr_in){ + .sin_family = AF_INET, + .sin_addr.s_addr = htonl (addr), + .sin_port = htons (port), + }; + + if (bind (id, (struct sockaddr *)&local, sizeof (local)) != 0) + { + goto error; + } + + return id; + +error: + close (id); + return -1; +} + +int pnal_udp_sendto ( + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size) +{ + struct sockaddr_in remote; + int len; + + remote = (struct sockaddr_in){ + .sin_family = AF_INET, + .sin_addr.s_addr = htonl (dst_addr), + .sin_port = htons (dst_port), + }; + len = + sendto (id, data, size, 0, (struct sockaddr *)&remote, sizeof (remote)); + + return len; +} + +int pnal_udp_recvfrom ( + uint32_t id, + pnal_ipaddr_t * src_addr, + pnal_ipport_t * src_port, + uint8_t * data, + int size) +{ + struct sockaddr_in remote; + socklen_t addr_len = sizeof (remote); + int len; + + memset (&remote, 0, sizeof (remote)); + len = recvfrom ( + id, + data, + size, + MSG_DONTWAIT, + (struct sockaddr *)&remote, + &addr_len); + if (len > 0) + { + *src_addr = ntohl (remote.sin_addr.s_addr); + *src_port = ntohs (remote.sin_port); + } + + return len; +} + +void pnal_udp_close (uint32_t id) +{ + close (id); +} diff --git a/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_STM32F769I-DISCO.c b/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_STM32F769I-DISCO.c new file mode 100644 index 0000000..e4100ac --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_STM32F769I-DISCO.c @@ -0,0 +1,43 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" + +/************************* Utilities ******************************************/ + +void app_set_led (uint16_t id, bool led_state) +{ + if (id == APP_DATA_LED_ID) + { + HAL_GPIO_WritePin (LD_USER1_GPIO_Port, LD_USER1_Pin, led_state ? 1 : 0); + } + else if (id == APP_PROFINET_SIGNAL_LED_ID) + { + HAL_GPIO_WritePin (LD_USER2_GPIO_Port, LD_USER2_Pin, led_state ? 1 : 0); + } +} + +bool app_get_button (uint16_t id) +{ + if (id == 0) + { + return HAL_GPIO_ReadPin (B_USER_GPIO_Port, B_USER_Pin) == 1; + } + else if (id == 1) + { + /* No more buttons on STM32F769-Discovery */ + } + return false; +} diff --git a/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_board.c b/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_board.c new file mode 100644 index 0000000..044d712 --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_board.c @@ -0,0 +1,25 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" + +void app_set_led (uint16_t id, bool led_state) +{ +} + +bool app_get_button (uint16_t id) +{ + return false; +} diff --git a/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_main.c b/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_main.c new file mode 100644 index 0000000..c4d567b --- /dev/null +++ b/profinet_stack/p-net/src/ports/STM32Cube/sampleapp_main.c @@ -0,0 +1,111 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" +#include "app_utils.h" +#include "app_log.h" +#include "app_gsdml.h" + +#include "osal_log.h" +#include "osal.h" +#include + +#include + +#include + +#define APP_DEFAULT_ETHERNET_INTERFACE "st" +#define APP_DEFAULT_FILE_DIRECTORY "" +#define APP_LOG_LEVEL APP_LOG_LEVEL_INFO + +#define APP_BG_WORKER_THREAD_PRIORITY 2 +#define APP_BG_WORKER_THREAD_STACKSIZE 4096 /* bytes */ + +/********************************** Globals ***********************************/ + +static app_data_t * sample_app = NULL; +static pnet_cfg_t pnet_cfg = {0}; +app_args_t app_args = {0}; + +/****************************** Main ******************************************/ + +int _main (void) +{ + int ret; + app_utils_netif_namelist_t netif_name_list; + pnet_if_cfg_t netif_cfg = {0}; + uint16_t number_of_ports; + + strcpy (app_args.eth_interfaces, APP_DEFAULT_ETHERNET_INTERFACE); + strcpy (app_args.station_name, APP_GSDML_DEFAULT_STATION_NAME); + app_log_set_log_level (APP_LOG_LEVEL); + + APP_LOG_INFO ("\n** Starting P-Net sample application " PNET_VERSION + " **\n"); + APP_LOG_INFO ( + "Number of slots: %u (incl slot for DAP module)\n", + PNET_MAX_SLOTS); + APP_LOG_INFO ("P-net log level: %u (DEBUG=0, FATAL=4)\n", LOG_LEVEL); + APP_LOG_INFO ("App log level: %u (DEBUG=0, FATAL=4)\n", APP_LOG_LEVEL); + APP_LOG_INFO ("Max number of ports: %u\n", PNET_MAX_PHYSICAL_PORTS); + APP_LOG_INFO ("Network interfaces: %s\n", app_args.eth_interfaces); + APP_LOG_INFO ("Default station name: %s\n", app_args.station_name); + + app_pnet_cfg_init_default (&pnet_cfg); + + /* Note: station name is defined by app_gsdml.h */ + + strcpy (pnet_cfg.file_directory, APP_DEFAULT_FILE_DIRECTORY); + + ret = app_utils_pnet_cfg_init_netifs ( + app_args.eth_interfaces, + &netif_name_list, + &number_of_ports, + &netif_cfg); + if (ret != 0) + { + return -1; + } + + pnet_cfg.if_cfg = netif_cfg; + pnet_cfg.num_physical_ports = number_of_ports; + + app_utils_print_network_config (&netif_cfg, number_of_ports); + + pnet_cfg.pnal_cfg.bg_worker_thread.prio = APP_BG_WORKER_THREAD_PRIORITY; + pnet_cfg.pnal_cfg.bg_worker_thread.stack_size = + APP_BG_WORKER_THREAD_STACKSIZE; + + /* Initialize profinet stack */ + sample_app = app_init (&pnet_cfg); + if (sample_app == NULL) + { + printf ("Failed to initialize P-Net.\n"); + printf ("Aborting application\n"); + return -1; + } + + /* Start main loop */ + if (app_start (sample_app, RUN_IN_MAIN_THREAD) != 0) + { + printf ("Failed to start\n"); + printf ("Aborting application\n"); + return -1; + } + + app_loop_forever (sample_app); + + return 0; +} diff --git a/profinet_stack/p-net/src/ports/linux/enable-rpi-gpio-pins.sh b/profinet_stack/p-net/src/ports/linux/enable-rpi-gpio-pins.sh new file mode 100644 index 0000000..83bf0fb --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/enable-rpi-gpio-pins.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Enable GPIO pins for digital inputs (buttons) on Raspberry Pi +echo 22 > /sys/class/gpio/export +echo 27 > /sys/class/gpio/export + +# GPIO pins for digital outputs (LEDs) are enabled in another script diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.c new file mode 100644 index 0000000..b43022d --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.c @@ -0,0 +1,173 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpConfigManAddrTable.h" + +/** Initializes the lldpConfigManAddrTable module */ +void init_lldpConfigManAddrTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpConfigManAddrTable (pnet); +} + +/** Initialize the lldpConfigManAddrTable table by defining its contents and how + * it's structured */ +void initialize_table_lldpConfigManAddrTable (pnet_t * pnet) +{ + const oid lldpConfigManAddrTable_oid[] = {1, 0, 8802, 1, 1, 2, 1, 1, 7}; + const size_t lldpConfigManAddrTable_oid_len = + OID_LENGTH (lldpConfigManAddrTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpConfigManAddrTable", + lldpConfigManAddrTable_handler, + lldpConfigManAddrTable_oid, + lldpConfigManAddrTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_INTEGER, /* index: lldpLocManAddrSubtype */ + ASN_OCTET_STR, /* index: lldpLocManAddr */ + 0); + table_info->min_column = COLUMN_LLDPCONFIGMANADDRPORTSTXENABLE; + table_info->max_column = COLUMN_LLDPCONFIGMANADDRPORTSTXENABLE; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpConfigManAddrTable_get_first_data_point; + iinfo->get_next_data_point = lldpConfigManAddrTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpConfigManAddrTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_snmp_management_address_t address; + + pf_snmp_get_management_address (pnet, &address); + + snmp_set_var_typed_integer (idx, ASN_INTEGER, address.subtype); + idx = idx->next_variable; + + snmp_set_var_value (idx, &address.value[1], address.value[0]); + + /* Set my_data_context to a value that is not NULL */ + *my_data_context = (void *)(uintptr_t) true; + + return put_index_data; +} + +netsnmp_variable_list * lldpConfigManAddrTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + return NULL; +} + +/** handles requests for the lldpConfigManAddrTable table */ +int lldpConfigManAddrTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_lldp_port_list_t port_list; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpConfigManAddrTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPCONFIGMANADDRPORTSTXENABLE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + pf_snmp_get_port_list (pnet, &port_list); + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + port_list.ports, + sizeof (port_list.ports)); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpConfigManAddrTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.h new file mode 100644 index 0000000..25e998b --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpConfigManAddrTable.h @@ -0,0 +1,34 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPCONFIGMANADDRTABLE_H +#define LLDPCONFIGMANADDRTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpConfigManAddrTable (pnet_t * pnet); +void initialize_table_lldpConfigManAddrTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpConfigManAddrTable_handler; +Netsnmp_First_Data_Point lldpConfigManAddrTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpConfigManAddrTable_get_next_data_point; + +/* column number definitions for table lldpConfigManAddrTable */ +#define COLUMN_LLDPCONFIGMANADDRPORTSTXENABLE 1 +#endif /* LLDPCONFIGMANADDRTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.c new file mode 100644 index 0000000..06d130e --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.c @@ -0,0 +1,198 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpLocManAddrTable.h" + +/** Initializes the lldpLocManAddrTable module */ +void init_lldpLocManAddrTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpLocManAddrTable (pnet); +} + +/** Initialize the lldpLocManAddrTable table by defining its contents and how + * it's structured */ +void initialize_table_lldpLocManAddrTable (pnet_t * pnet) +{ + const oid lldpLocManAddrTable_oid[] = {1, 0, 8802, 1, 1, 2, 1, 3, 8}; + const size_t lldpLocManAddrTable_oid_len = + OID_LENGTH (lldpLocManAddrTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpLocManAddrTable", + lldpLocManAddrTable_handler, + lldpLocManAddrTable_oid, + lldpLocManAddrTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_INTEGER, /* index: lldpLocManAddrSubtype */ + ASN_OCTET_STR, /* index: lldpLocManAddr */ + 0); + table_info->min_column = COLUMN_LLDPLOCMANADDRLEN; + table_info->max_column = COLUMN_LLDPLOCMANADDROID; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpLocManAddrTable_get_first_data_point; + iinfo->get_next_data_point = lldpLocManAddrTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpLocManAddrTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_snmp_management_address_t address; + + pf_snmp_get_management_address (pnet, &address); + + snmp_set_var_typed_integer (idx, ASN_INTEGER, address.subtype); + idx = idx->next_variable; + + snmp_set_var_value (idx, &address.value[1], address.value[0]); + + /* Set my_data_context to a value that is not NULL */ + *my_data_context = (void *)(uintptr_t) true; + + return put_index_data; +} + +netsnmp_variable_list * lldpLocManAddrTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + return NULL; +} + +/** handles requests for the lldpLocManAddrTable table */ +int lldpLocManAddrTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_snmp_management_address_t address; + pf_lldp_interface_number_t port_index; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpLocManAddrTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPLOCMANADDRLEN: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + pf_snmp_get_management_address (pnet, &address); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + address.len); + break; + case COLUMN_LLDPLOCMANADDRIFSUBTYPE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + pf_snmp_get_management_port_index (pnet, &port_index); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + port_index.subtype); + break; + case COLUMN_LLDPLOCMANADDRIFID: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + pf_snmp_get_management_port_index (pnet, &port_index); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + port_index.value); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpLocManAddrTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.h new file mode 100644 index 0000000..8bfce8f --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpLocManAddrTable.h @@ -0,0 +1,39 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPLOCMANADDRTABLE_H +#define LLDPLOCMANADDRTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpLocManAddrTable (pnet_t * pnet); +void initialize_table_lldpLocManAddrTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpLocManAddrTable_handler; +Netsnmp_First_Data_Point lldpLocManAddrTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpLocManAddrTable_get_next_data_point; + +/* column number definitions for table lldpLocManAddrTable */ +#define COLUMN_LLDPLOCMANADDRSUBTYPE 1 +#define COLUMN_LLDPLOCMANADDR 2 +#define COLUMN_LLDPLOCMANADDRLEN 3 +#define COLUMN_LLDPLOCMANADDRIFSUBTYPE 4 +#define COLUMN_LLDPLOCMANADDRIFID 5 +#define COLUMN_LLDPLOCMANADDROID 6 +#endif /* LLDPLOCMANADDRTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.c new file mode 100644 index 0000000..5fdf050 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.c @@ -0,0 +1,221 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpLocPortTable.h" + +/** Initializes the lldpLocPortTable module */ +void init_lldpLocPortTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpLocPortTable (pnet); +} + +static void lldpLocPortTable_loop_free ( + void * loopctx, + netsnmp_iterator_info * iinfo) +{ + SNMP_FREE (loopctx); +} + +/** Initialize the lldpLocPortTable table by defining its contents and how it's + * structured */ +void initialize_table_lldpLocPortTable (pnet_t * pnet) +{ + const oid lldpLocPortTable_oid[] = {1, 0, 8802, 1, 1, 2, 1, 3, 7}; + const size_t lldpLocPortTable_oid_len = OID_LENGTH (lldpLocPortTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpLocPortTable", + lldpLocPortTable_handler, + lldpLocPortTable_oid, + lldpLocPortTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_INTEGER, /* index: lldpLocPortNum */ + 0); + table_info->min_column = COLUMN_LLDPLOCPORTIDSUBTYPE; + table_info->max_column = COLUMN_LLDPLOCPORTDESC; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpLocPortTable_get_first_data_point; + iinfo->get_next_data_point = lldpLocPortTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->free_loop_context_at_end = lldpLocPortTable_loop_free; + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpLocPortTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + + iterator = SNMP_MALLOC_TYPEDEF (pf_port_iterator_t); + pf_snmp_init_port_iterator (pnet, iterator); + *my_loop_context = iterator; + + return lldpLocPortTable_get_next_data_point ( + my_loop_context, + my_data_context, + put_index_data, + mydata); +} + +netsnmp_variable_list * lldpLocPortTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pf_port_iterator_t * iterator; + int port; + + iterator = (pf_port_iterator_t *)*my_loop_context; + port = pf_snmp_get_next_port (iterator); + if (port == 0) + { + return NULL; + } + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + + *my_data_context = (void *)(uintptr_t)port; + return put_index_data; +} + +/** handles requests for the lldpLocPortTable table */ +int lldpLocPortTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + pf_lldp_port_id_t port_id; + pf_lldp_port_description_t port_desc; + void * my_data_context; + int port; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpLocPortTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPLOCPORTIDSUBTYPE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_port_id (pnet, port, &port_id); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + port_id.subtype); + break; + case COLUMN_LLDPLOCPORTID: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_port_id (pnet, port, &port_id); + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + port_id.string, + port_id.len); + break; + case COLUMN_LLDPLOCPORTDESC: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_port_description (pnet, port, &port_desc); + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + port_desc.string, + port_desc.len); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpLocPortTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.h new file mode 100644 index 0000000..53a12fa --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpLocPortTable.h @@ -0,0 +1,37 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPLOCPORTTABLE_H +#define LLDPLOCPORTTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpLocPortTable (pnet_t * pnet); +void initialize_table_lldpLocPortTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpLocPortTable_handler; +Netsnmp_First_Data_Point lldpLocPortTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpLocPortTable_get_next_data_point; + +/* column number definitions for table lldpLocPortTable */ +#define COLUMN_LLDPLOCPORTNUM 1 +#define COLUMN_LLDPLOCPORTIDSUBTYPE 2 +#define COLUMN_LLDPLOCPORTID 3 +#define COLUMN_LLDPLOCPORTDESC 4 +#endif /* LLDPLOCPORTTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.c b/profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.c new file mode 100644 index 0000000..cb3e2d4 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.c @@ -0,0 +1,149 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.scalar.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpLocalSystemData.h" + +/** Initializes the lldpLocalSystemData module */ +void init_lldpLocalSystemData (pnet_t * pnet) +{ + const oid lldpLocChassisIdSubtype_oid[] = {1, 0, 8802, 1, 1, 2, 1, 3, 1}; + const oid lldpLocChassisId_oid[] = {1, 0, 8802, 1, 1, 2, 1, 3, 2}; + netsnmp_handler_registration * reg; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "lldpLocChassisIdSubtype", + handle_lldpLocChassisIdSubtype, + lldpLocChassisIdSubtype_oid, + OID_LENGTH (lldpLocChassisIdSubtype_oid), + HANDLER_CAN_RONLY)); + + reg->my_reg_void = pnet; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "lldpLocChassisId", + handle_lldpLocChassisId, + lldpLocChassisId_oid, + OID_LENGTH (lldpLocChassisId_oid), + HANDLER_CAN_RONLY)); + + reg->my_reg_void = pnet; +} + +int handle_lldpLocChassisIdSubtype ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + pnet_t * pnet = reginfo->my_reg_void; + pf_lldp_chassis_id_t chassis_id; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + pf_snmp_get_chassis_id (pnet, &chassis_id); + + switch (reqinfo->mode) + { + + case MODE_GET: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpLocalSystemData(%d): GET chassis ID subtype.\n", + __LINE__); + snmp_set_var_typed_integer ( + requests->requestvb, + ASN_INTEGER, + chassis_id.subtype); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "lldpLocalSystemData(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} + +int handle_lldpLocChassisId ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + pnet_t * pnet = reginfo->my_reg_void; + pf_lldp_chassis_id_t chassis_id; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + pf_snmp_get_chassis_id (pnet, &chassis_id); + + switch (reqinfo->mode) + { + + case MODE_GET: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpLocalSystemData(%d): GET chassis ID.\n", + __LINE__); + + snmp_set_var_typed_value ( + requests->requestvb, + ASN_OCTET_STR, + chassis_id.string, + chassis_id.len); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "lldpLocalSystemData(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.h b/profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.h new file mode 100644 index 0000000..5c252c4 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpLocalSystemData.h @@ -0,0 +1,34 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.scalar.conf + */ +#ifndef LLDPLOCALSYSTEMDATA_H +#define LLDPLOCALSYSTEMDATA_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpLocalSystemData (pnet_t * pnet); +Netsnmp_Node_Handler handle_lldpLocChassisIdSubtype; +Netsnmp_Node_Handler handle_lldpLocChassisId; +Netsnmp_Node_Handler handle_lldpLocSysName; +Netsnmp_Node_Handler handle_lldpLocSysDesc; +Netsnmp_Node_Handler handle_lldpLocSysCapSupported; +Netsnmp_Node_Handler handle_lldpLocSysCapEnabled; + +#endif /* LLDPLOCALSYSTEMDATA_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.c new file mode 100644 index 0000000..2a7be69 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.c @@ -0,0 +1,250 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpRemManAddrTable.h" + +/** Initializes the lldpRemManAddrTable module */ +void init_lldpRemManAddrTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpRemManAddrTable (pnet); +} + +static void lldpRemManAddrTable_loop_free ( + void * loopctx, + netsnmp_iterator_info * iinfo) +{ + SNMP_FREE (loopctx); +} + +/** Initialize the lldpRemManAddrTable table by defining its contents and how + * it's structured */ +void initialize_table_lldpRemManAddrTable (pnet_t * pnet) +{ + const oid lldpRemManAddrTable_oid[] = {1, 0, 8802, 1, 1, 2, 1, 4, 2}; + const size_t lldpRemManAddrTable_oid_len = + OID_LENGTH (lldpRemManAddrTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpRemManAddrTable", + lldpRemManAddrTable_handler, + lldpRemManAddrTable_oid, + lldpRemManAddrTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_TIMETICKS, /* index: lldpRemTimeMark */ + ASN_INTEGER, /* index: lldpRemLocalPortNum */ + ASN_INTEGER, /* index: lldpRemIndex */ + ASN_INTEGER, /* index: lldpRemManAddrSubtype */ + ASN_OCTET_STR, /* index: lldpRemManAddr */ + 0); + table_info->min_column = COLUMN_LLDPREMMANADDRIFSUBTYPE; + table_info->max_column = COLUMN_LLDPREMMANADDROID; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpRemManAddrTable_get_first_data_point; + iinfo->get_next_data_point = lldpRemManAddrTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->free_loop_context_at_end = lldpRemManAddrTable_loop_free; + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpRemManAddrTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + + iterator = SNMP_MALLOC_TYPEDEF (pf_port_iterator_t); + pf_snmp_init_port_iterator (pnet, iterator); + *my_loop_context = iterator; + + return lldpRemManAddrTable_get_next_data_point ( + my_loop_context, + my_data_context, + put_index_data, + mydata); +} + +netsnmp_variable_list * lldpRemManAddrTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + int port; + uint32_t timestamp; + pf_snmp_management_address_t address; + int error; + + iterator = (pf_port_iterator_t *)*my_loop_context; + + do + { + port = pf_snmp_get_next_port (iterator); + if (port == 0) + { + return NULL; + } + + error = pf_snmp_get_peer_timestamp (pnet, port, ×tamp); + if (error == 0) + { + error = pf_snmp_get_peer_management_address (pnet, port, &address); + } + } while (error); + + snmp_set_var_typed_integer (idx, ASN_TIMETICKS, timestamp); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, address.subtype); + idx = idx->next_variable; + + snmp_set_var_value (idx, &address.value[1], address.value[0]); + + *my_data_context = (void *)(uintptr_t)port; + return put_index_data; +} + +/** handles requests for the lldpRemManAddrTable table */ +int lldpRemManAddrTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_lldp_interface_number_t port_index; + int port; + int error; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpRemManAddrTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPREMMANADDRIFSUBTYPE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = + pf_snmp_get_peer_management_port_index (pnet, port, &port_index); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + port_index.subtype); + break; + case COLUMN_LLDPREMMANADDRIFID: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = + pf_snmp_get_peer_management_port_index (pnet, port, &port_index); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + port_index.value); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpRemManAddrTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.h new file mode 100644 index 0000000..f375546 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpRemManAddrTable.h @@ -0,0 +1,38 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPREMMANADDRTABLE_H +#define LLDPREMMANADDRTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpRemManAddrTable (pnet_t * pnet); +void initialize_table_lldpRemManAddrTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpRemManAddrTable_handler; +Netsnmp_First_Data_Point lldpRemManAddrTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpRemManAddrTable_get_next_data_point; + +/* column number definitions for table lldpRemManAddrTable */ +#define COLUMN_LLDPREMMANADDRSUBTYPE 1 +#define COLUMN_LLDPREMMANADDR 2 +#define COLUMN_LLDPREMMANADDRIFSUBTYPE 3 +#define COLUMN_LLDPREMMANADDRIFID 4 +#define COLUMN_LLDPREMMANADDROID 5 +#endif /* LLDPREMMANADDRTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.c new file mode 100644 index 0000000..a432f3a --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.c @@ -0,0 +1,300 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpRemTable.h" + +/** Initializes the lldpRemTable module */ +void init_lldpRemTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpRemTable (pnet); +} + +static void lldpRemTable_loop_free ( + void * loopctx, + netsnmp_iterator_info * iinfo) +{ + SNMP_FREE (loopctx); +} + +/** Initialize the lldpRemTable table by defining its contents and how it's + * structured */ +void initialize_table_lldpRemTable (pnet_t * pnet) +{ + const oid lldpRemTable_oid[] = {1, 0, 8802, 1, 1, 2, 1, 4, 1}; + const size_t lldpRemTable_oid_len = OID_LENGTH (lldpRemTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpRemTable", + lldpRemTable_handler, + lldpRemTable_oid, + lldpRemTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_TIMETICKS, /* index: lldpRemTimeMark */ + ASN_INTEGER, /* index: lldpRemLocalPortNum */ + ASN_INTEGER, /* index: lldpRemIndex */ + 0); + table_info->min_column = COLUMN_LLDPREMCHASSISIDSUBTYPE; + table_info->max_column = COLUMN_LLDPREMSYSCAPENABLED; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpRemTable_get_first_data_point; + iinfo->get_next_data_point = lldpRemTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->free_loop_context_at_end = lldpRemTable_loop_free; + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpRemTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + + iterator = SNMP_MALLOC_TYPEDEF (pf_port_iterator_t); + pf_snmp_init_port_iterator (pnet, iterator); + *my_loop_context = iterator; + + return lldpRemTable_get_next_data_point ( + my_loop_context, + my_data_context, + put_index_data, + mydata); +} + +netsnmp_variable_list * lldpRemTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + int port; + uint32_t timestamp; + int error; + + iterator = (pf_port_iterator_t *)*my_loop_context; + + do + { + port = pf_snmp_get_next_port (iterator); + if (port == 0) + { + return NULL; + } + + error = pf_snmp_get_peer_timestamp (pnet, port, ×tamp); + } while (error); + + snmp_set_var_typed_integer (idx, ASN_TIMETICKS, timestamp); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + + *my_data_context = (void *)(uintptr_t)port; + return put_index_data; +} + +/** handles requests for the lldpRemTable table */ +int lldpRemTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_lldp_chassis_id_t chassis_id; + pf_lldp_port_id_t port_id; + pf_lldp_port_description_t port_desc; + int port; + int error; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpRemTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPREMCHASSISIDSUBTYPE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_chassis_id (pnet, port, &chassis_id); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + chassis_id.subtype); + break; + case COLUMN_LLDPREMCHASSISID: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_chassis_id (pnet, port, &chassis_id); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + chassis_id.string, + chassis_id.len); + break; + case COLUMN_LLDPREMPORTIDSUBTYPE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_port_id (pnet, port, &port_id); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + port_id.subtype); + break; + case COLUMN_LLDPREMPORTID: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_port_id (pnet, port, &port_id); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + port_id.string, + port_id.len); + break; + case COLUMN_LLDPREMPORTDESC: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_port_description (pnet, port, &port_desc); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + port_desc.string, + port_desc.len); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpRemTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.h new file mode 100644 index 0000000..8bca6a4 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpRemTable.h @@ -0,0 +1,45 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPREMTABLE_H +#define LLDPREMTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpRemTable (pnet_t * pnet); +void initialize_table_lldpRemTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpRemTable_handler; +Netsnmp_First_Data_Point lldpRemTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpRemTable_get_next_data_point; + +/* column number definitions for table lldpRemTable */ +#define COLUMN_LLDPREMTIMEMARK 1 +#define COLUMN_LLDPREMLOCALPORTNUM 2 +#define COLUMN_LLDPREMINDEX 3 +#define COLUMN_LLDPREMCHASSISIDSUBTYPE 4 +#define COLUMN_LLDPREMCHASSISID 5 +#define COLUMN_LLDPREMPORTIDSUBTYPE 6 +#define COLUMN_LLDPREMPORTID 7 +#define COLUMN_LLDPREMPORTDESC 8 +#define COLUMN_LLDPREMSYSNAME 9 +#define COLUMN_LLDPREMSYSDESC 10 +#define COLUMN_LLDPREMSYSCAPSUPPORTED 11 +#define COLUMN_LLDPREMSYSCAPENABLED 12 +#endif /* LLDPREMTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.c new file mode 100644 index 0000000..f7fdd0b --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.c @@ -0,0 +1,233 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpXPnoLocTable.h" + +/** Initializes the lldpXPnoLocTable module */ +void init_lldpXPnoLocTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpXPnoLocTable (pnet); +} + +static void lldpXPnoLocTable_loop_free ( + void * loopctx, + netsnmp_iterator_info * iinfo) +{ + SNMP_FREE (loopctx); +} + +/** Initialize the lldpXPnoLocTable table by defining its contents and how it's + * structured */ +void initialize_table_lldpXPnoLocTable (pnet_t * pnet) +{ + const oid lldpXPnoLocTable_oid[] = {1, 0, 8802, 1, 1, 2, 1, 5, 3791, 1, 2, 1}; + const size_t lldpXPnoLocTable_oid_len = OID_LENGTH (lldpXPnoLocTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpXPnoLocTable", + lldpXPnoLocTable_handler, + lldpXPnoLocTable_oid, + lldpXPnoLocTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_INTEGER, /* index: lldpLocPortNum */ + 0); + table_info->min_column = COLUMN_LLDPXPNOLOCLPDVALUE; + table_info->max_column = COLUMN_LLDPXPNOLOCPORTMRPICDOMAINID; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpXPnoLocTable_get_first_data_point; + iinfo->get_next_data_point = lldpXPnoLocTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->free_loop_context_at_end = lldpXPnoLocTable_loop_free; + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpXPnoLocTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + + iterator = SNMP_MALLOC_TYPEDEF (pf_port_iterator_t); + pf_snmp_init_port_iterator (pnet, iterator); + *my_loop_context = iterator; + + return lldpXPnoLocTable_get_next_data_point ( + my_loop_context, + my_data_context, + put_index_data, + mydata); +} + +netsnmp_variable_list * lldpXPnoLocTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pf_port_iterator_t * iterator; + int port; + + iterator = (pf_port_iterator_t *)*my_loop_context; + port = pf_snmp_get_next_port (iterator); + if (port == 0) + { + return NULL; + } + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + + *my_data_context = (void *)(uintptr_t)port; + return put_index_data; +} + +/** handles requests for the lldpXPnoLocTable table */ +int lldpXPnoLocTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_snmp_signal_delay_t delays; + pf_lldp_station_name_t station_name; + int port; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXPnoLocTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPXPNOLOCLPDVALUE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_signal_delays (pnet, port, &delays); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_UNSIGNED, + delays.line_propagation_delay_ns); + break; + case COLUMN_LLDPXPNOLOCPORTTXDVALUE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_signal_delays (pnet, port, &delays); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_UNSIGNED, + delays.port_tx_delay_ns); + break; + case COLUMN_LLDPXPNOLOCPORTRXDVALUE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_signal_delays (pnet, port, &delays); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_UNSIGNED, + delays.port_rx_delay_ns); + break; + case COLUMN_LLDPXPNOLOCPORTNOS: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + pf_snmp_get_station_name (pnet, &station_name); + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + station_name.string, + station_name.len); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXPnoLocTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.h new file mode 100644 index 0000000..4c10b9c --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoLocTable.h @@ -0,0 +1,61 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPXPNOLOCTABLE_H +#define LLDPXPNOLOCTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpXPnoLocTable (pnet_t * pnet); +void initialize_table_lldpXPnoLocTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpXPnoLocTable_handler; +Netsnmp_First_Data_Point lldpXPnoLocTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpXPnoLocTable_get_next_data_point; + +/* column number definitions for table lldpXPnoLocTable */ +#define COLUMN_LLDPXPNOLOCLPDVALUE 1 +#define COLUMN_LLDPXPNOLOCPORTTXDVALUE 2 +#define COLUMN_LLDPXPNOLOCPORTRXDVALUE 3 +#define COLUMN_LLDPXPNOLOCPORTSTATUSRT2 4 +#define COLUMN_LLDPXPNOLOCPORTSTATUSRT3 5 +#define COLUMN_LLDPXPNOLOCPORTNOS 6 +#define COLUMN_LLDPXPNOLOCPORTMRPUUID 7 +#define COLUMN_LLDPXPNOLOCPORTMRRTSTATUS 8 +#define COLUMN_LLDPXPNOLOCPORTPTCPMASTER 9 +#define COLUMN_LLDPXPNOLOCPORTPTCPSUBDOMAINUUID 10 +#define COLUMN_LLDPXPNOLOCPORTPTCPIRDATAUUID 11 +#define COLUMN_LLDPXPNOLOCPORTMODERT3 12 +#define COLUMN_LLDPXPNOLOCPORTPERIODLENGTH 13 +#define COLUMN_LLDPXPNOLOCPORTPERIODVALIDITY 14 +#define COLUMN_LLDPXPNOLOCPORTREDOFFSET 15 +#define COLUMN_LLDPXPNOLOCPORTREDVALIDITY 16 +#define COLUMN_LLDPXPNOLOCPORTORANGEOFFSET 17 +#define COLUMN_LLDPXPNOLOCPORTORANGEVALIDITY 18 +#define COLUMN_LLDPXPNOLOCPORTGREENOFFSET 19 +#define COLUMN_LLDPXPNOLOCPORTGREENVALIDITY 20 +#define COLUMN_LLDPXPNOLOCPORTSTATUSRT3OPTIMIZATIONSUPPORTED 21 +#define COLUMN_LLDPXPNOLOCPORTSTATUSRT3PREAMBLESHORTENINGSUPPORTED 22 +#define COLUMN_LLDPXPNOLOCPORTSTATUSRT3PREAMBLESHORTENING 23 +#define COLUMN_LLDPXPNOLOCPORTSTATUSRT3FRAGMENTATIONSUPPORTED 24 +#define COLUMN_LLDPXPNOLOCPORTSTATUSRT3FRAGMENTATION 25 +#define COLUMN_LLDPXPNOLOCPORTOPERMAUTYPEEXTENSION 26 +#define COLUMN_LLDPXPNOLOCPORTMRPICROLE 27 +#define COLUMN_LLDPXPNOLOCPORTMRPICDOMAINID 28 +#endif /* LLDPXPNOLOCTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.c new file mode 100644 index 0000000..952a5f6 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.c @@ -0,0 +1,273 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpXPnoRemTable.h" + +/** Initializes the lldpXdot3RemPortTable module */ +void init_lldpXPnoRemTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpXPnoRemTable (pnet); +} + +static void lldpXPnoRemTable_loop_free ( + void * loopctx, + netsnmp_iterator_info * iinfo) +{ + SNMP_FREE (loopctx); +} + +/** Initialize the lldpXPnoRemTable table by defining its contents and how it's + * structured */ +void initialize_table_lldpXPnoRemTable (pnet_t * pnet) +{ + const oid lldpXPnoRemTable_oid[] = {1, 0, 8802, 1, 1, 2, 1, 5, 3791, 1, 3, 1}; + const size_t lldpXPnoRemTable_oid_len = OID_LENGTH (lldpXPnoRemTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpXPnoRemTable", + lldpXPnoRemTable_handler, + lldpXPnoRemTable_oid, + lldpXPnoRemTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_TIMETICKS, /* index: lldpRemTimeMark */ + ASN_INTEGER, /* index: lldpRemLocalPortNum */ + ASN_INTEGER, /* index: lldpRemIndex */ + 0); + table_info->min_column = COLUMN_LLDPXPNOREMLPDVALUE; + table_info->max_column = COLUMN_LLDPXPNOREMPORTMRPICDOMAINID; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpXPnoRemTable_get_first_data_point; + iinfo->get_next_data_point = lldpXPnoRemTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->free_loop_context_at_end = lldpXPnoRemTable_loop_free; + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpXPnoRemTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + + iterator = SNMP_MALLOC_TYPEDEF (pf_port_iterator_t); + pf_snmp_init_port_iterator (pnet, iterator); + *my_loop_context = iterator; + + return lldpXPnoRemTable_get_next_data_point ( + my_loop_context, + my_data_context, + put_index_data, + mydata); +} + +netsnmp_variable_list * lldpXPnoRemTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + int port; + uint32_t timestamp; + int error; + + iterator = (pf_port_iterator_t *)*my_loop_context; + + do + { + port = pf_snmp_get_next_port (iterator); + if (port == 0) + { + return NULL; + } + + error = pf_snmp_get_peer_timestamp (pnet, port, ×tamp); + } while (error); + + snmp_set_var_typed_integer (idx, ASN_TIMETICKS, timestamp); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + + *my_data_context = (void *)(uintptr_t)port; + return put_index_data; +} + +/** handles requests for the lldpXPnoRemTable table */ +int lldpXPnoRemTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_snmp_signal_delay_t delays; + pf_lldp_station_name_t station_name; + int port; + int error; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXPnoRemTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPXPNOREMLPDVALUE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_signal_delays (pnet, port, &delays); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + snmp_set_var_typed_integer ( + request->requestvb, + ASN_UNSIGNED, + delays.line_propagation_delay_ns); + break; + case COLUMN_LLDPXPNOREMPORTTXDVALUE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_signal_delays (pnet, port, &delays); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + snmp_set_var_typed_integer ( + request->requestvb, + ASN_UNSIGNED, + delays.port_tx_delay_ns); + break; + case COLUMN_LLDPXPNOREMPORTRXDVALUE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_signal_delays (pnet, port, &delays); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + snmp_set_var_typed_integer ( + request->requestvb, + ASN_UNSIGNED, + delays.port_rx_delay_ns); + break; + case COLUMN_LLDPXPNOREMPORTNOS: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_station_name (pnet, port, &station_name); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + station_name.string, + station_name.len); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXPnoRemTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.h new file mode 100644 index 0000000..e42bc6d --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXPnoRemTable.h @@ -0,0 +1,58 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPXPNOREMTABLE_H +#define LLDPXPNOREMTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpXPnoRemTable (pnet_t * pnet); +void initialize_table_lldpXPnoRemTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpXPnoRemTable_handler; +Netsnmp_First_Data_Point lldpXPnoRemTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpXPnoRemTable_get_next_data_point; + +/* column number definitions for table lldpXPnoRemTable */ +#define COLUMN_LLDPXPNOREMLPDVALUE 1 +#define COLUMN_LLDPXPNOREMPORTTXDVALUE 2 +#define COLUMN_LLDPXPNOREMPORTRXDVALUE 3 +#define COLUMN_LLDPXPNOREMPORTSTATUSRT2 4 +#define COLUMN_LLDPXPNOREMPORTSTATUSRT3 5 +#define COLUMN_LLDPXPNOREMPORTNOS 6 +#define COLUMN_LLDPXPNOREMPORTMRPUUID 7 +#define COLUMN_LLDPXPNOREMPORTMRRTSTATUS 8 +#define COLUMN_LLDPXPNOREMPORTPTCPMASTER 9 +#define COLUMN_LLDPXPNOREMPORTPTCPSUBDOMAINUUID 10 +#define COLUMN_LLDPXPNOREMPORTPTCPIRDATAUUID 11 +#define COLUMN_LLDPXPNOREMPORTMODERT3 12 +#define COLUMN_LLDPXPNOREMPORTPERIODLENGTH 13 +#define COLUMN_LLDPXPNOREMPORTPERIODVALIDITY 14 +#define COLUMN_LLDPXPNOREMPORTREDOFFSET 15 +#define COLUMN_LLDPXPNOREMPORTREDVALIDITY 16 +#define COLUMN_LLDPXPNOREMPORTORANGEOFFSET 17 +#define COLUMN_LLDPXPNOREMPORTORANGEVALIDITY 18 +#define COLUMN_LLDPXPNOREMPORTGREENOFFSET 19 +#define COLUMN_LLDPXPNOREMPORTGREENVALIDITY 20 +#define COLUMN_LLDPXPNOREMPORTSTATUSRT3PREAMBLESHORTENING 21 +#define COLUMN_LLDPXPNOREMPORTSTATUSRT3FRAGMENTATION 22 +#define COLUMN_LLDPXPNOREMPORTOPERMAUTYPEEXTENSION 23 +#define COLUMN_LLDPXPNOREMPORTMRPICROLE 24 +#define COLUMN_LLDPXPNOREMPORTMRPICDOMAINID 25 +#endif /* LLDPXPNOREMTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.c new file mode 100644 index 0000000..bad6e63 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.c @@ -0,0 +1,235 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpXdot3LocPortTable.h" + +/** Initializes the lldpXdot3LocPortTable module */ +void init_lldpXdot3LocPortTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpXdot3LocPortTable (pnet); +} + +static void lldpXdot3LocPortTable_loop_free ( + void * loopctx, + netsnmp_iterator_info * iinfo) +{ + SNMP_FREE (loopctx); +} + +/** Initialize the lldpXdot3LocPortTable table by defining its contents and how + * it's structured */ +void initialize_table_lldpXdot3LocPortTable (pnet_t * pnet) +{ + const oid lldpXdot3LocPortTable_oid[] = + {1, 0, 8802, 1, 1, 2, 1, 5, 4623, 1, 2, 1}; + const size_t lldpXdot3LocPortTable_oid_len = + OID_LENGTH (lldpXdot3LocPortTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpXdot3LocPortTable", + lldpXdot3LocPortTable_handler, + lldpXdot3LocPortTable_oid, + lldpXdot3LocPortTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_INTEGER, /* index: lldpLocPortNum */ + 0); + table_info->min_column = COLUMN_LLDPXDOT3LOCPORTAUTONEGSUPPORTED; + table_info->max_column = COLUMN_LLDPXDOT3LOCPORTOPERMAUTYPE; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpXdot3LocPortTable_get_first_data_point; + iinfo->get_next_data_point = lldpXdot3LocPortTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->free_loop_context_at_end = lldpXdot3LocPortTable_loop_free; + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpXdot3LocPortTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + + iterator = SNMP_MALLOC_TYPEDEF (pf_port_iterator_t); + pf_snmp_init_port_iterator (pnet, iterator); + *my_loop_context = iterator; + + return lldpXdot3LocPortTable_get_next_data_point ( + my_loop_context, + my_data_context, + put_index_data, + mydata); +} + +netsnmp_variable_list * lldpXdot3LocPortTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pf_port_iterator_t * iterator; + int port; + + iterator = (pf_port_iterator_t *)*my_loop_context; + port = pf_snmp_get_next_port (iterator); + if (port == 0) + { + return NULL; + } + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + + *my_data_context = (void *)(uintptr_t)port; + return put_index_data; +} + +/** handles requests for the lldpXdot3LocPortTable table */ +int lldpXdot3LocPortTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_snmp_link_status_t link_status; + int port; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXdot3LocPortTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPXDOT3LOCPORTAUTONEGSUPPORTED: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_link_status (pnet, port, &link_status); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + link_status.auto_neg_supported); + break; + case COLUMN_LLDPXDOT3LOCPORTAUTONEGENABLED: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_link_status (pnet, port, &link_status); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + link_status.auto_neg_enabled); + break; + case COLUMN_LLDPXDOT3LOCPORTAUTONEGADVERTISEDCAP: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_link_status (pnet, port, &link_status); + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + &link_status.auto_neg_advertised_cap, + sizeof (link_status.auto_neg_advertised_cap)); + break; + case COLUMN_LLDPXDOT3LOCPORTOPERMAUTYPE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + pf_snmp_get_link_status (pnet, port, &link_status); + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + link_status.oper_mau_type); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXdot3LocPortTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.h new file mode 100644 index 0000000..0690876 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3LocPortTable.h @@ -0,0 +1,37 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPXDOT3LOCPORTTABLE_H +#define LLDPXDOT3LOCPORTTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpXdot3LocPortTable (pnet_t * pnet); +void initialize_table_lldpXdot3LocPortTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpXdot3LocPortTable_handler; +Netsnmp_First_Data_Point lldpXdot3LocPortTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpXdot3LocPortTable_get_next_data_point; + +/* column number definitions for table lldpXdot3LocPortTable */ +#define COLUMN_LLDPXDOT3LOCPORTAUTONEGSUPPORTED 1 +#define COLUMN_LLDPXDOT3LOCPORTAUTONEGENABLED 2 +#define COLUMN_LLDPXDOT3LOCPORTAUTONEGADVERTISEDCAP 3 +#define COLUMN_LLDPXDOT3LOCPORTOPERMAUTYPE 4 +#endif /* LLDPXDOT3LOCPORTTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.c b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.c new file mode 100644 index 0000000..5d71314 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.c @@ -0,0 +1,277 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "lldpXdot3RemPortTable.h" + +/** Initializes the lldpXdot3RemPortTable module */ +void init_lldpXdot3RemPortTable (pnet_t * pnet) +{ + /* here we initialize all the tables we're planning on supporting */ + initialize_table_lldpXdot3RemPortTable (pnet); +} + +static void lldpXdot3RemPortTable_loop_free ( + void * loopctx, + netsnmp_iterator_info * iinfo) +{ + SNMP_FREE (loopctx); +} + +/** Initialize the lldpXdot3RemPortTable table by defining its contents and how + * it's structured */ +void initialize_table_lldpXdot3RemPortTable (pnet_t * pnet) +{ + const oid lldpXdot3RemPortTable_oid[] = + {1, 0, 8802, 1, 1, 2, 1, 5, 4623, 1, 3, 1}; + const size_t lldpXdot3RemPortTable_oid_len = + OID_LENGTH (lldpXdot3RemPortTable_oid); + netsnmp_handler_registration * reg; + netsnmp_iterator_info * iinfo; + netsnmp_table_registration_info * table_info; + + reg = netsnmp_create_handler_registration ( + "lldpXdot3RemPortTable", + lldpXdot3RemPortTable_handler, + lldpXdot3RemPortTable_oid, + lldpXdot3RemPortTable_oid_len, + HANDLER_CAN_RONLY); + + reg->my_reg_void = pnet; + + table_info = SNMP_MALLOC_TYPEDEF (netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes ( + table_info, + ASN_TIMETICKS, /* index: lldpRemTimeMark */ + ASN_INTEGER, /* index: lldpRemLocalPortNum */ + ASN_INTEGER, /* index: lldpRemIndex */ + 0); + table_info->min_column = COLUMN_LLDPXDOT3REMPORTAUTONEGSUPPORTED; + table_info->max_column = COLUMN_LLDPXDOT3REMPORTOPERMAUTYPE; + + iinfo = SNMP_MALLOC_TYPEDEF (netsnmp_iterator_info); + iinfo->get_first_data_point = lldpXdot3RemPortTable_get_first_data_point; + iinfo->get_next_data_point = lldpXdot3RemPortTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + iinfo->free_loop_context_at_end = lldpXdot3RemPortTable_loop_free; + iinfo->myvoid = pnet; + + netsnmp_register_table_iterator (reg, iinfo); +} + +netsnmp_variable_list * lldpXdot3RemPortTable_get_first_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + + iterator = SNMP_MALLOC_TYPEDEF (pf_port_iterator_t); + pf_snmp_init_port_iterator (pnet, iterator); + *my_loop_context = iterator; + + return lldpXdot3RemPortTable_get_next_data_point ( + my_loop_context, + my_data_context, + put_index_data, + mydata); +} + +netsnmp_variable_list * lldpXdot3RemPortTable_get_next_data_point ( + void ** my_loop_context, + void ** my_data_context, + netsnmp_variable_list * put_index_data, + netsnmp_iterator_info * mydata) +{ + netsnmp_variable_list * idx = put_index_data; + pnet_t * pnet = (pnet_t *)mydata->myvoid; + pf_port_iterator_t * iterator; + int port; + uint32_t timestamp; + int error; + + iterator = (pf_port_iterator_t *)*my_loop_context; + + do + { + port = pf_snmp_get_next_port (iterator); + if (port == 0) + { + return NULL; + } + + error = pf_snmp_get_peer_timestamp (pnet, port, ×tamp); + } while (error); + + snmp_set_var_typed_integer (idx, ASN_TIMETICKS, timestamp); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + idx = idx->next_variable; + + snmp_set_var_typed_integer (idx, ASN_INTEGER, port); + + *my_data_context = (void *)(uintptr_t)port; + return put_index_data; +} + +/** handles requests for the lldpXdot3RemPortTable table */ +int lldpXdot3RemPortTable_handler ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + netsnmp_request_info * request; + netsnmp_table_request_info * table_info; + pnet_t * pnet = reginfo->my_reg_void; + void * my_data_context; + pf_snmp_link_status_t link_status; + int port; + int error; + + switch (reqinfo->mode) + { + /* + * Read-support (also covers GetNext requests) + */ + case MODE_GET: + for (request = requests; request; request = request->next) + { + my_data_context = netsnmp_extract_iterator_context (request); + table_info = netsnmp_extract_table_info (request); + + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXdot3RemPortTable(%d): GET. Column number: %u\n", + __LINE__, + table_info->colnum); + + switch (table_info->colnum) + { + case COLUMN_LLDPXDOT3REMPORTAUTONEGSUPPORTED: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_link_status (pnet, port, &link_status); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + link_status.auto_neg_supported); + break; + case COLUMN_LLDPXDOT3REMPORTAUTONEGENABLED: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_link_status (pnet, port, &link_status); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + link_status.auto_neg_enabled); + break; + case COLUMN_LLDPXDOT3REMPORTAUTONEGADVERTISEDCAP: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_link_status (pnet, port, &link_status); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_value ( + request->requestvb, + ASN_OCTET_STR, + &link_status.auto_neg_advertised_cap, + sizeof (link_status.auto_neg_advertised_cap)); + break; + case COLUMN_LLDPXDOT3REMPORTOPERMAUTYPE: + if (my_data_context == NULL) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + port = (int)(uintptr_t)my_data_context; + error = pf_snmp_get_peer_link_status (pnet, port, &link_status); + if (error) + { + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHINSTANCE); + continue; + } + + snmp_set_var_typed_integer ( + request->requestvb, + ASN_INTEGER, + link_status.oper_mau_type); + break; + default: + netsnmp_set_request_error (reqinfo, request, SNMP_NOSUCHOBJECT); + break; + } + } + break; + default: + LOG_DEBUG ( + PF_SNMP_LOG, + "lldpXdot3RemPortTable(%d): Unknown mode: %u\n", + __LINE__, + reqinfo->mode); + break; + } + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.h b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.h new file mode 100644 index 0000000..cad2ef1 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/lldpXdot3RemPortTable.h @@ -0,0 +1,37 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.iterate.conf + */ +#ifndef LLDPXDOT3REMPORTTABLE_H +#define LLDPXDOT3REMPORTTABLE_H + +#include "pf_includes.h" + +/* function declarations */ +void init_lldpXdot3RemPortTable (pnet_t * pnet); +void initialize_table_lldpXdot3RemPortTable (pnet_t * pnet); +Netsnmp_Node_Handler lldpXdot3RemPortTable_handler; +Netsnmp_First_Data_Point lldpXdot3RemPortTable_get_first_data_point; +Netsnmp_Next_Data_Point lldpXdot3RemPortTable_get_next_data_point; + +/* column number definitions for table lldpXdot3RemPortTable */ +#define COLUMN_LLDPXDOT3REMPORTAUTONEGSUPPORTED 1 +#define COLUMN_LLDPXDOT3REMPORTAUTONEGENABLED 2 +#define COLUMN_LLDPXDOT3REMPORTAUTONEGADVERTISEDCAP 3 +#define COLUMN_LLDPXDOT3REMPORTOPERMAUTYPE 4 +#endif /* LLDPXDOT3REMPORTTABLE_H */ diff --git a/profinet_stack/p-net/src/ports/linux/mib/system_mib.c b/profinet_stack/p-net/src/ports/linux/mib/system_mib.c new file mode 100644 index 0000000..5437e4b --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/system_mib.c @@ -0,0 +1,619 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.scalar.conf + */ + +#include +#include +#include + +#undef LOG_DEBUG +#undef LOG_WARNING +#undef LOG_INFO +#undef LOG_ERROR +#undef LOG_FATAL + +#include "system_mib.h" + +#include + +/** Initializes the system_mib module */ +void init_system_mib (pnet_t * pnet) +{ + const oid sysDescr_oid[] = {1, 3, 6, 1, 2, 1, 1, 1}; + const oid sysObjectID_oid[] = {1, 3, 6, 1, 2, 1, 1, 2}; + const oid sysUpTime_oid[] = {1, 3, 6, 1, 2, 1, 1, 3}; + const oid sysContact_oid[] = {1, 3, 6, 1, 2, 1, 1, 4}; + const oid sysName_oid[] = {1, 3, 6, 1, 2, 1, 1, 5}; + const oid sysLocation_oid[] = {1, 3, 6, 1, 2, 1, 1, 6}; + const oid sysServices_oid[] = {1, 3, 6, 1, 2, 1, 1, 7}; + netsnmp_handler_registration * reg; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "sysDescr", + handle_sysDescr, + sysDescr_oid, + OID_LENGTH (sysDescr_oid), + HANDLER_CAN_RONLY)); + + reg->my_reg_void = pnet; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "sysObjectID", + handle_sysObjectID, + sysObjectID_oid, + OID_LENGTH (sysObjectID_oid), + HANDLER_CAN_RONLY)); + + reg->my_reg_void = pnet; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "sysUpTime", + handle_sysUpTime, + sysUpTime_oid, + OID_LENGTH (sysUpTime_oid), + HANDLER_CAN_RONLY)); + + reg->my_reg_void = pnet; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "sysContact", + handle_sysContact, + sysContact_oid, + OID_LENGTH (sysContact_oid), + HANDLER_CAN_RWRITE)); + + reg->my_reg_void = pnet; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "sysName", + handle_sysName, + sysName_oid, + OID_LENGTH (sysName_oid), + HANDLER_CAN_RWRITE)); + + reg->my_reg_void = pnet; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "sysLocation", + handle_sysLocation, + sysLocation_oid, + OID_LENGTH (sysLocation_oid), + HANDLER_CAN_RWRITE)); + + reg->my_reg_void = pnet; + + netsnmp_register_scalar ( + reg = netsnmp_create_handler_registration ( + "sysServices", + handle_sysServices, + sysServices_oid, + OID_LENGTH (sysServices_oid), + HANDLER_CAN_RONLY)); + + reg->my_reg_void = pnet; +} + +int handle_sysDescr ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + pnet_t * pnet = reginfo->my_reg_void; + pf_snmp_system_description_t sysdescription; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + pf_snmp_get_system_description (pnet, &sysdescription); + + switch (reqinfo->mode) + { + + case MODE_GET: + LOG_DEBUG (PF_SNMP_LOG, "system_mib(%d): GET sysDescr.\n", __LINE__); + snmp_set_var_typed_value ( + requests->requestvb, + ASN_OCTET_STR, + sysdescription.string, + strlen (sysdescription.string)); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "system_mib(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} +int handle_sysObjectID ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + const oid sysoid[] = {1, 3, 6, 1, 4, 1, 24686}; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + switch (reqinfo->mode) + { + + case MODE_GET: + LOG_DEBUG (PF_SNMP_LOG, "system_mib(%d): GET sysObjectID.\n", __LINE__); + snmp_set_var_typed_value ( + requests->requestvb, + ASN_OBJECT_ID, + sysoid, + OID_LENGTH (sysoid) * sizeof (oid)); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "system_mib(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} + +int handle_sysUpTime ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + struct sysinfo systeminfo; /* Field .uptime contains uptime in seconds */ + uint32_t uptime_10ms; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + switch (reqinfo->mode) + { + case MODE_GET: + LOG_DEBUG (PF_SNMP_LOG, "system_mib(%d): GET sysUpTime.\n", __LINE__); + if (sysinfo (&systeminfo) == 0) + { + uptime_10ms = systeminfo.uptime * 100; + } + else + { + uptime_10ms = 0; + } + + snmp_set_var_typed_integer ( + requests->requestvb, + ASN_TIMETICKS, + uptime_10ms); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "system_mib(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} + +int handle_sysContact ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + int ret; + pnet_t * pnet = reginfo->my_reg_void; + pf_snmp_system_contact_t syscontact; + static pf_snmp_system_contact_t * old_syscontact; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + LOG_DEBUG ( + PF_SNMP_LOG, + "system_mib(%d): Handle sysContact. Mode: %d\n", + __LINE__, + reqinfo->mode); + + switch (reqinfo->mode) + { + + case MODE_GET: + pf_snmp_get_system_contact (pnet, &syscontact); + snmp_set_var_typed_value ( + requests->requestvb, + ASN_OCTET_STR, + syscontact.string, + strlen (syscontact.string)); + break; + + /* + * SET REQUEST + * + * multiple states in the transaction. See: + * http://www.net-snmp.org/tutorial/tutorial-5/toolkit/mib_module/set-actions.jpg + */ + case MODE_SET_RESERVE1: + old_syscontact = NULL; + ret = netsnmp_check_vb_type_and_max_size ( + requests->requestvb, + ASN_OCTET_STR, + sizeof (syscontact.string) - 1); + if (ret != SNMP_ERR_NOERROR) + { + netsnmp_set_request_error (reqinfo, requests, ret); + } + break; + + case MODE_SET_RESERVE2: + old_syscontact = SNMP_MALLOC_TYPEDEF (pf_snmp_system_contact_t); + if (old_syscontact == NULL) + { + netsnmp_set_request_error ( + reqinfo, + requests, + SNMP_ERR_RESOURCEUNAVAILABLE); + } + else + { + pf_snmp_get_system_contact (pnet, old_syscontact); + } + break; + + case MODE_SET_FREE: + if (old_syscontact != NULL) + { + SNMP_FREE (old_syscontact); + } + break; + + case MODE_SET_ACTION: + memcpy ( + syscontact.string, + requests->requestvb->val.string, + requests->requestvb->val_len); + syscontact.string[requests->requestvb->val_len] = '\0'; + ret = pf_snmp_set_system_contact (pnet, &syscontact); + if (ret) + { + netsnmp_set_request_error (reqinfo, requests, SNMP_ERR_GENERR); + } + break; + + case MODE_SET_COMMIT: + SNMP_FREE (old_syscontact); + break; + + case MODE_SET_UNDO: + ret = pf_snmp_set_system_contact (pnet, old_syscontact); + if (ret) + { + netsnmp_set_request_error (reqinfo, requests, SNMP_ERR_UNDOFAILED); + } + SNMP_FREE (old_syscontact); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "system_mib(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} + +int handle_sysName ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + int ret; + pnet_t * pnet = reginfo->my_reg_void; + pf_snmp_system_name_t sysname; + static pf_snmp_system_name_t * old_sysname; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + LOG_DEBUG ( + PF_SNMP_LOG, + "system_mib(%d): Handle sysName. Mode: %d\n", + __LINE__, + reqinfo->mode); + + switch (reqinfo->mode) + { + + case MODE_GET: + pf_snmp_get_system_name (pnet, &sysname); + snmp_set_var_typed_value ( + requests->requestvb, + ASN_OCTET_STR, + sysname.string, + strlen (sysname.string)); + break; + + /* + * SET REQUEST + * + * multiple states in the transaction. See: + * http://www.net-snmp.org/tutorial/tutorial-5/toolkit/mib_module/set-actions.jpg + */ + case MODE_SET_RESERVE1: + old_sysname = NULL; + ret = netsnmp_check_vb_type_and_max_size ( + requests->requestvb, + ASN_OCTET_STR, + sizeof (sysname.string) - 1); + if (ret != SNMP_ERR_NOERROR) + { + netsnmp_set_request_error (reqinfo, requests, ret); + } + break; + + case MODE_SET_RESERVE2: + old_sysname = SNMP_MALLOC_TYPEDEF (pf_snmp_system_name_t); + if (old_sysname == NULL) + { + netsnmp_set_request_error ( + reqinfo, + requests, + SNMP_ERR_RESOURCEUNAVAILABLE); + } + else + { + pf_snmp_get_system_name (pnet, old_sysname); + } + break; + + case MODE_SET_FREE: + if (old_sysname != NULL) + { + SNMP_FREE (old_sysname); + } + break; + + case MODE_SET_ACTION: + memcpy ( + sysname.string, + requests->requestvb->val.string, + requests->requestvb->val_len); + sysname.string[requests->requestvb->val_len] = '\0'; + ret = pf_snmp_set_system_name (pnet, &sysname); + if (ret) + { + netsnmp_set_request_error (reqinfo, requests, SNMP_ERR_GENERR); + } + break; + + case MODE_SET_COMMIT: + SNMP_FREE (old_sysname); + break; + + case MODE_SET_UNDO: + ret = pf_snmp_set_system_name (pnet, old_sysname); + if (ret) + { + netsnmp_set_request_error (reqinfo, requests, SNMP_ERR_UNDOFAILED); + } + SNMP_FREE (old_sysname); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "system_mib(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} + +int handle_sysLocation ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + int ret; + pnet_t * pnet = reginfo->my_reg_void; + pf_snmp_system_location_t syslocation; + static pf_snmp_system_location_t * old_syslocation; + + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + LOG_DEBUG ( + PF_SNMP_LOG, + "system_mib(%d): Handle sysLocation. Mode: %d\n", + __LINE__, + reqinfo->mode); + + switch (reqinfo->mode) + { + + case MODE_GET: + pf_snmp_get_system_location (pnet, &syslocation); + snmp_set_var_typed_value ( + requests->requestvb, + ASN_OCTET_STR, + syslocation.string, + strlen (syslocation.string)); + break; + + /* + * SET REQUEST + * + * multiple states in the transaction. See: + * http://www.net-snmp.org/tutorial/tutorial-5/toolkit/mib_module/set-actions.jpg + */ + case MODE_SET_RESERVE1: + old_syslocation = NULL; + ret = netsnmp_check_vb_type_and_max_size ( + requests->requestvb, + ASN_OCTET_STR, + sizeof (syslocation.string) - 1); + if (ret != SNMP_ERR_NOERROR) + { + netsnmp_set_request_error (reqinfo, requests, ret); + } + break; + + case MODE_SET_RESERVE2: + old_syslocation = SNMP_MALLOC_TYPEDEF (pf_snmp_system_location_t); + if (old_syslocation == NULL) + { + netsnmp_set_request_error ( + reqinfo, + requests, + SNMP_ERR_RESOURCEUNAVAILABLE); + } + else + { + pf_snmp_get_system_location (pnet, old_syslocation); + } + break; + + case MODE_SET_FREE: + if (old_syslocation != NULL) + { + SNMP_FREE (old_syslocation); + } + break; + + case MODE_SET_ACTION: + memcpy ( + syslocation.string, + requests->requestvb->val.string, + requests->requestvb->val_len); + syslocation.string[requests->requestvb->val_len] = '\0'; + ret = pf_snmp_set_system_location (pnet, &syslocation); + if (ret) + { + netsnmp_set_request_error (reqinfo, requests, SNMP_ERR_GENERR); + } + break; + + case MODE_SET_COMMIT: + SNMP_FREE (old_syslocation); + break; + + case MODE_SET_UNDO: + ret = pf_snmp_set_system_location (pnet, old_syslocation); + if (ret) + { + netsnmp_set_request_error (reqinfo, requests, SNMP_ERR_UNDOFAILED); + } + SNMP_FREE (old_syslocation); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "system_mib(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} + +int handle_sysServices ( + netsnmp_mib_handler * handler, + netsnmp_handler_registration * reginfo, + netsnmp_agent_request_info * reqinfo, + netsnmp_request_info * requests) +{ + /* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ + + /* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + + switch (reqinfo->mode) + { + + case MODE_GET: + LOG_DEBUG (PF_SNMP_LOG, "system_mib(%d): GET sysServices\n", __LINE__); + /* Supported services. Should be 78 for Profinet. See IETF RFC 3418. */ + snmp_set_var_typed_integer (requests->requestvb, ASN_INTEGER, 78); + break; + + default: + /* we should never get here, so this is a really bad error */ + LOG_ERROR ( + PF_SNMP_LOG, + "system_mib(%d): unknown mode (%d)\n", + __LINE__, + reqinfo->mode); + return SNMP_ERR_GENERR; + } + + return SNMP_ERR_NOERROR; +} diff --git a/profinet_stack/p-net/src/ports/linux/mib/system_mib.h b/profinet_stack/p-net/src/ports/linux/mib/system_mib.h new file mode 100644 index 0000000..22d44ac --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/mib/system_mib.h @@ -0,0 +1,35 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/* + * Note: this file originally auto-generated by mib2c + * using mib2c.scalar.conf + */ +#ifndef SYSTEM_MIB_H +#define SYSTEM_MIB_H + +#include "pf_includes.h" + +/* function declarations */ +void init_system_mib (pnet_t * pnet); +Netsnmp_Node_Handler handle_sysDescr; +Netsnmp_Node_Handler handle_sysObjectID; +Netsnmp_Node_Handler handle_sysUpTime; +Netsnmp_Node_Handler handle_sysContact; +Netsnmp_Node_Handler handle_sysName; +Netsnmp_Node_Handler handle_sysLocation; +Netsnmp_Node_Handler handle_sysServices; + +#endif /* SYSTEM_MIB_H */ diff --git a/profinet_stack/p-net/src/ports/linux/pnal.c b/profinet_stack/p-net/src/ports/linux/pnal.c new file mode 100644 index 0000000..e7d48c9 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal.c @@ -0,0 +1,604 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#define _GNU_SOURCE /* For asprintf() */ + +#include "pnal.h" + +#include "options.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal_filetools.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/********************************* Files *************************************/ + +int pnal_save_file ( + const char * fullpath, + const void * object_1, + size_t size_1, + const void * object_2, + size_t size_2) +{ + int ret = 0; /* Assume everything goes well */ + int outputfile; + + /* Open file + Use synchronized write to make sure it ends up on disk immediately */ + outputfile = open ( + fullpath, + O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (outputfile == -1) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Could not open file %s\n", + __LINE__, + fullpath); + return -1; + } + + /* Write file contents */ + if (size_1 > 0) + { + if (write (outputfile, object_1, size_1) == -1) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to write file %s\n", + __LINE__, + fullpath); + } + } + if (size_2 > 0 && ret == 0) + { + if (write (outputfile, object_2, size_2) == -1) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to write file %s (second buffer)\n", + __LINE__, + fullpath); + } + } + + /* Close file */ + if (close (outputfile) != 0) + { + ret = -1; + } + + return ret; +} + +void pnal_clear_file (const char * fullpath) +{ + LOG_DEBUG (PF_PNAL_LOG, "PNAL(%d): Clearing file %s\n", __LINE__, fullpath); + (void)remove (fullpath); +} + +int pnal_load_file ( + const char * fullpath, + void * object_1, + size_t size_1, + void * object_2, + size_t size_2) +{ + int ret = 0; /* Assume everything goes well */ + int inputfile; + + /* Open file */ + inputfile = open (fullpath, O_RDONLY); + if (inputfile == -1) + { + LOG_DEBUG ( + PF_PNAL_LOG, + "PNAL(%d): Could not yet open file %s\n", + __LINE__, + fullpath); + return -1; + } + + /* Read file contents */ + if (size_1 > 0) + { + if (read (inputfile, object_1, size_1) <= 0) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to read file %s\n", + __LINE__, + fullpath); + } + } + + if (size_2 > 0 && ret == 0) + { + if (read (inputfile, object_2, size_2) <= 0) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to read file %s (second buffer)\n", + __LINE__, + fullpath); + } + } + + /* Close file */ + if (close (inputfile) != 0) + { + ret = -1; + } + + return ret; +} + +/*****************************************************************************/ + +uint32_t pnal_get_system_uptime_10ms (void) +{ + struct sysinfo systeminfo; /* Field .uptime contains uptime in seconds */ + + if (sysinfo (&systeminfo) != 0) + { + return 0; + } + + return systeminfo.uptime * 100; +} + +uint32_t pnal_buf_alloc_cnt = 0; /* Count outstanding buffers */ + +pnal_buf_t * pnal_buf_alloc (uint16_t length) +{ + pnal_buf_t * p = malloc (sizeof (pnal_buf_t) + length); + + if (p != NULL) + { +#if 1 + p->payload = (void *)((uint8_t *)p + sizeof (pnal_buf_t)); /* Payload + follows header + struct */ + p->len = length; +#endif + pnal_buf_alloc_cnt++; + } + else + { + assert ("malloc() failed\n"); + } + + return p; +} + +void pnal_buf_free (pnal_buf_t * p) +{ + free (p); + pnal_buf_alloc_cnt--; + return; +} + +uint8_t pnal_buf_header (pnal_buf_t * p, int16_t header_size_increment) +{ + return 255; +} + +/************************** Networking ***************************************/ + +/** @internal + * Convert IPv4 address to string + * @param ip In: IP address + * @param outputstring Out: Resulting string. Should have size + * PNAL_INET_ADDRSTR_SIZE. + */ +static void os_ip_to_string (pnal_ipaddr_t ip, char * outputstring) +{ + snprintf ( + outputstring, + PNAL_INET_ADDRSTR_SIZE, + "%u.%u.%u.%u", + (uint8_t)((ip >> 24) & 0xFF), + (uint8_t)((ip >> 16) & 0xFF), + (uint8_t)((ip >> 8) & 0xFF), + (uint8_t)(ip & 0xFF)); +} + +int pnal_set_ip_suite ( + const char * interface_name, + const pnal_ipaddr_t * p_ipaddr, + const pnal_ipaddr_t * p_netmask, + const pnal_ipaddr_t * p_gw, + const char * hostname, + bool permanent) +{ + char ip_string[PNAL_INET_ADDRSTR_SIZE]; /** Terminated string */ + char netmask_string[PNAL_INET_ADDRSTR_SIZE]; /** Terminated string */ + char gateway_string[PNAL_INET_ADDRSTR_SIZE]; /** Terminated string */ + const char * argv[8]; + + os_ip_to_string (*p_ipaddr, ip_string); + os_ip_to_string (*p_netmask, netmask_string); + os_ip_to_string (*p_gw, gateway_string); + + argv[0] = "set_network_parameters"; + argv[1] = interface_name; + argv[2] = (char *)&ip_string; + argv[3] = (char *)&netmask_string; + argv[4] = (char *)&gateway_string; + argv[5] = hostname; + argv[6] = permanent ? "1" : "0"; + argv[7] = NULL; + + return pnal_execute_script (argv); +} + +/** + * Calculate MAU type. + * + * @param port_type In: Port type, one of Linux PORT_xxx + * @param speed In: Speed in Mbit/s + * @param duplex In: DUPLEX_FULL, DUPLEX_HALF or DUPLEX_UNKNOWN + * @return The MAU type + */ +static pnal_eth_mau_t calculate_mau_type ( + uint8_t port_type, + uint32_t speed, + uint8_t duplex) +{ + /* Copper cable */ + if (port_type == PORT_TP || port_type == PORT_MII) + { + if (duplex == DUPLEX_FULL) + { + switch (speed) + { + case SPEED_10: + return PNAL_ETH_MAU_COPPER_10BaseT; + case SPEED_100: + return PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + case SPEED_1000: + return PNAL_ETH_MAU_COPPER_1000BaseT_FULL_DUPLEX; + default: + break; + } + } + if (duplex == DUPLEX_HALF) + { + switch (speed) + { + case SPEED_10: + return PNAL_ETH_MAU_COPPER_10BaseT; + case SPEED_100: + return PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX; + case SPEED_1000: + return PNAL_ETH_MAU_COPPER_1000BaseT_HALF_DUPLEX; + default: + break; + } + } + } + + /* Fiber cable */ + if (port_type == PORT_FIBRE) + { + if (duplex == DUPLEX_FULL) + { + switch (speed) + { + case SPEED_100: + return PNAL_ETH_MAU_FIBER_100BaseFX_FULL_DUPLEX; + case SPEED_1000: + return PNAL_ETH_MAU_FIBER_1000BaseX_FULL_DUPLEX; + default: + break; + } + } + if (duplex == DUPLEX_HALF) + { + switch (speed) + { + case SPEED_100: + return PNAL_ETH_MAU_FIBER_100BaseFX_HALF_DUPLEX; + case SPEED_1000: + return PNAL_ETH_MAU_FIBER_1000BaseX_HALF_DUPLEX; + default: + break; + } + } + } + + /* Ethtool can not find details for Wifi interfaces. + The MAU type for "unknown, link down" and for "radio, link up" are the + same in the Profinet standard. + + The pnet stack use the MAU type and the link status to figure out + whether the communication is radio or not. */ + + return PNAL_ETH_MAU_UNKNOWN; +} + +/** + * Calculate advertised capabilities + * + * @param advertised In: Linux advertised capabilities as a bitfield + * @return Profinet advertised capabilities as a bitfield + */ +static uint16_t calculate_capabilities (uint32_t advertised) +{ + uint16_t out = 0; + + if (advertised & ADVERTISED_10baseT_Half) + { + out |= PNAL_ETH_AUTONEG_CAP_10BaseT_HALF_DUPLEX; + } + if (advertised & ADVERTISED_10baseT_Full) + { + out |= PNAL_ETH_AUTONEG_CAP_10BaseT_FULL_DUPLEX; + } + if (advertised & ADVERTISED_100baseT_Half) + { + out |= PNAL_ETH_AUTONEG_CAP_100BaseTX_HALF_DUPLEX; + } + if (advertised & ADVERTISED_100baseT_Full) + { + out |= PNAL_ETH_AUTONEG_CAP_100BaseTX_FULL_DUPLEX; + } + if (advertised & ADVERTISED_1000baseT_Half) + { + out |= PNAL_ETH_AUTONEG_CAP_1000BaseT_HALF_DUPLEX; + } + if (advertised & ADVERTISED_1000baseT_Full) + { + out |= PNAL_ETH_AUTONEG_CAP_1000BaseT_FULL_DUPLEX; + } + + if (out == 0) + { + out |= PNAL_ETH_AUTONEG_CAP_UNKNOWN; + } + + return out; +} + +int pnal_eth_get_status (const char * interface_name, pnal_eth_status_t * status) +{ + /* TODO: Possibly use the new ETHTOOL_GLINKSETTINGS instead of ETHTOOL_GSET + See: + https://stackoverflow.com/questions/41822920/how-to-get-ethtool-settings */ + + int ret = -1; + int control_socket; + struct ifreq ifr; + struct ethtool_cmd eth_status_linux; + uint32_t speed = 0; /* Mbit/s */ + uint8_t port_type = PORT_TP; /* Linux PORT_xxx */ + + control_socket = socket (PF_INET, SOCK_DGRAM, IPPROTO_IP); + if (control_socket < 0) + { + return ret; + } + + snprintf (ifr.ifr_name, sizeof (ifr.ifr_name), "%s", interface_name); + ifr.ifr_data = (char *)ð_status_linux; + eth_status_linux.cmd = ETHTOOL_GSET; + + if (ioctl (control_socket, SIOCETHTOOL, &ifr) >= 0) + { + speed = ethtool_cmd_speed (ð_status_linux); + port_type = eth_status_linux.port; + status->is_autonegotiation_enabled = + (eth_status_linux.autoneg == AUTONEG_ENABLE); + status->is_autonegotiation_supported = eth_status_linux.advertising & + ADVERTISED_Autoneg; + status->operational_mau_type = + calculate_mau_type (port_type, speed, eth_status_linux.duplex); + status->autonegotiation_advertised_capabilities = + calculate_capabilities (eth_status_linux.advertising); + + ret = 0; + } + + if (ioctl (control_socket, SIOCGIFFLAGS, &ifr) >= 0) + { + status->running = (ifr.ifr_flags & IFF_RUNNING) ? true : false; + } + + close (control_socket); + + return ret; +} + +int pnal_get_interface_index (const char * interface_name) +{ + return if_nametoindex (interface_name); +} + +int pnal_get_port_statistics ( + const char * interface_name, + pnal_port_stats_t * port_stats) +{ + struct ifaddrs *p_ifaddr, *p_temp; + struct rtnl_link_stats * p_statistics_linux; + int ret = -1; + + if (getifaddrs (&p_ifaddr) != 0) + { + return ret; + } + + /* Walk the linked list and look for correct interface name */ + for (p_temp = p_ifaddr; p_temp != NULL; p_temp = p_temp->ifa_next) + { + if ( + (p_temp->ifa_addr != NULL) && + (p_temp->ifa_addr->sa_family == AF_PACKET) && + (strcmp (p_temp->ifa_name, interface_name) == 0) && + (p_temp->ifa_data != NULL)) + { + p_statistics_linux = (struct rtnl_link_stats *)p_temp->ifa_data; + + port_stats->if_in_octets = p_statistics_linux->rx_bytes; + port_stats->if_in_errors = p_statistics_linux->rx_errors; + port_stats->if_in_discards = p_statistics_linux->rx_dropped; + port_stats->if_out_octets = p_statistics_linux->tx_bytes; + port_stats->if_out_errors = p_statistics_linux->tx_errors; + port_stats->if_out_discards = p_statistics_linux->tx_dropped; + + ret = 0; + break; + } + } + freeifaddrs (p_ifaddr); + + return ret; +} + +int pnal_get_macaddress (const char * interface_name, pnal_ethaddr_t * mac_addr) +{ + int fd; + int ret = 0; + struct ifreq ifr; + + fd = socket (AF_INET, SOCK_DGRAM, 0); + + ifr.ifr_addr.sa_family = AF_INET; + strncpy (ifr.ifr_name, interface_name, IFNAMSIZ - 1); + + ret = ioctl (fd, SIOCGIFHWADDR, &ifr); + if (ret == 0) + { + memcpy (mac_addr->addr, ifr.ifr_hwaddr.sa_data, 6); + } + close (fd); + return ret; +} + +pnal_ipaddr_t pnal_get_ip_address (const char * interface_name) +{ + int fd; + struct ifreq ifr; + pnal_ipaddr_t ip; + + fd = socket (AF_INET, SOCK_DGRAM, 0); + ifr.ifr_addr.sa_family = AF_INET; + strncpy (ifr.ifr_name, interface_name, IFNAMSIZ - 1); + + if (ioctl (fd, SIOCGIFADDR, &ifr) != 0) + { + close (fd); + return PNAL_IPADDR_INVALID; + } + + ip = ntohl (((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr.s_addr); + close (fd); + + return ip; +} + +pnal_ipaddr_t pnal_get_netmask (const char * interface_name) +{ + int fd; + struct ifreq ifr; + pnal_ipaddr_t netmask; + + fd = socket (AF_INET, SOCK_DGRAM, 0); + + ifr.ifr_addr.sa_family = AF_INET; + strncpy (ifr.ifr_name, interface_name, IFNAMSIZ - 1); + + if (ioctl (fd, SIOCGIFNETMASK, &ifr) != 0) + { + close (fd); + return PNAL_IPADDR_INVALID; + } + + netmask = ntohl (((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr.s_addr); + close (fd); + + return netmask; +} + +pnal_ipaddr_t pnal_get_gateway (const char * interface_name) +{ + /* TODO Read the actual default gateway (somewhat complicated) */ + + pnal_ipaddr_t ip; + pnal_ipaddr_t gateway; + + ip = pnal_get_ip_address (interface_name); + if (ip == PNAL_IPADDR_INVALID) + { + return PNAL_IPADDR_INVALID; + } + + gateway = (ip & 0xFFFFFF00) | 0x00000001; + + return gateway; +} + +int pnal_get_hostname (char * hostname) +{ + int ret = -1; + + ret = gethostname (hostname, PNAL_HOSTNAME_MAX_SIZE); + hostname[PNAL_HOSTNAME_MAX_SIZE - 1] = '\0'; + + return ret; +} + +int pnal_get_ip_suite ( + const char * interface_name, + pnal_ipaddr_t * p_ipaddr, + pnal_ipaddr_t * p_netmask, + pnal_ipaddr_t * p_gw, + char * hostname) +{ + int ret = -1; + + *p_ipaddr = pnal_get_ip_address (interface_name); + *p_netmask = pnal_get_netmask (interface_name); + *p_gw = pnal_get_gateway (interface_name); + ret = pnal_get_hostname (hostname); + + return ret; +} diff --git a/profinet_stack/p-net/src/ports/linux/pnal_config.h b/profinet_stack/p-net/src/ports/linux/pnal_config.h new file mode 100644 index 0000000..6b1be0b --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal_config.h @@ -0,0 +1,54 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief PNAL-specific configuration + * + * This file contains definitions of configuration settings for the + * PNAL layer. + */ + +#ifndef PNAL_CONFIG_H +#define PNAL_CONFIG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * Thread priority and stack size + */ +typedef struct pnal_thread_cfg +{ + uint32_t prio; + size_t stack_size; +} pnal_thread_cfg_t; + +typedef struct pnal_cfg +{ + pnal_thread_cfg_t snmp_thread; + pnal_thread_cfg_t eth_recv_thread; + pnal_thread_cfg_t bg_worker_thread; +} pnal_cfg_t; + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_CONFIG_H */ diff --git a/profinet_stack/p-net/src/ports/linux/pnal_eth.c b/profinet_stack/p-net/src/ports/linux/pnal_eth.c new file mode 100644 index 0000000..832129b --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal_eth.c @@ -0,0 +1,192 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Linux Ethernet related functions that use \a pnal_eth_handle_t + */ + +#include "pnal.h" + +#include "pnet_options.h" +#include "options.h" +#include "osal_log.h" + +#include +#include +#include +#include + +#include +#include + +struct pnal_eth_handle +{ + pnal_eth_callback_t * callback; + void * arg; + int socket; + os_thread_t * thread; +}; + +/** + * @internal + * Run a thread that listens to incoming raw Ethernet sockets. + * Delegate the actual work to thread_arg->callback + * + * This is a function to be passed into os_thread_create() + * Do not change the argument types. + * + * @param thread_arg InOut: Will be converted to pnal_eth_handle_t + */ +static void os_eth_task (void * thread_arg) +{ + pnal_eth_handle_t * eth_handle = thread_arg; + ssize_t readlen; + int handled = 0; + + pnal_buf_t * p = pnal_buf_alloc (PNAL_BUF_MAX_SIZE); + assert (p != NULL); + + while (1) + { + readlen = recv (eth_handle->socket, p->payload, PNAL_BUF_MAX_SIZE, 0); + if (readlen == -1) + continue; + p->len = readlen; + + if (eth_handle->callback != NULL) + { + handled = eth_handle->callback (eth_handle, eth_handle->arg, p); + } + else + { + handled = 0; /* Message not handled */ + } + + if (handled == 1) + { + p = pnal_buf_alloc (PNAL_BUF_MAX_SIZE); + assert (p != NULL); + } + } +} + +pnal_eth_handle_t * pnal_eth_init ( + const char * if_name, + pnal_ethertype_t receive_type, + const pnal_cfg_t * pnal_cfg, + pnal_eth_callback_t * callback, + void * arg) +{ + pnal_eth_handle_t * handle; + int i; + struct ifreq ifr; + struct sockaddr_ll sll; + int ifindex; + struct timeval timeout; + struct packet_mreq mreq; + const uint8_t pn_mcast_addr[ETH_ALEN] = {0x01, 0x0e, 0xcf, 0x00, 0x00, 0x00}; + const uint16_t linux_receive_type = + (receive_type == PNAL_ETHTYPE_ALL) ? ETH_P_ALL : receive_type; + + handle = malloc (sizeof (pnal_eth_handle_t)); + if (handle == NULL) + { + return NULL; + } + + handle->arg = arg; + handle->callback = callback; + handle->socket = socket (PF_PACKET, SOCK_RAW, htons (linux_receive_type)); + + /* Adjust send timeout */ + timeout.tv_sec = 0; + timeout.tv_usec = 1; + setsockopt ( + handle->socket, + SOL_SOCKET, + SO_SNDTIMEO, + &timeout, + sizeof (timeout)); + + /* Send outgoing messages directly to the interface, without using Linux + * routing */ + i = 1; + setsockopt (handle->socket, SOL_SOCKET, SO_DONTROUTE, &i, sizeof (i)); + + /* Read interface index */ + strcpy (ifr.ifr_name, if_name); + ioctl (handle->socket, SIOCGIFINDEX, &ifr); + ifindex = ifr.ifr_ifindex; + + /* Set flags of NIC interface */ + strcpy (ifr.ifr_name, if_name); + ifr.ifr_flags = 0; + ioctl (handle->socket, SIOCGIFFLAGS, &ifr); + ifr.ifr_flags = ifr.ifr_flags | IFF_MULTICAST | IFF_BROADCAST; + if (receive_type == PNAL_ETHTYPE_ALL) + { + ifr.ifr_flags |= IFF_ALLMULTI; /* Receive all multicasts */ + } + ioctl (handle->socket, SIOCSIFFLAGS, &ifr); + + /* Bind socket to relevant protocol */ + sll.sll_family = AF_PACKET; + sll.sll_ifindex = ifindex; + sll.sll_protocol = htons (linux_receive_type); + bind (handle->socket, (struct sockaddr *)&sll, sizeof (sll)); + + /* Join profinet multicast group */ + mreq.mr_ifindex = ifindex; + mreq.mr_type = PACKET_HOST | PACKET_MR_MULTICAST; + mreq.mr_alen = ETH_ALEN; + memcpy (mreq.mr_address, pn_mcast_addr, ETH_ALEN); + + if ( + setsockopt ( + handle->socket, + SOL_PACKET, + PACKET_ADD_MEMBERSHIP, + &mreq, + sizeof (mreq)) != 0) + { + LOG_WARNING ( + PF_PNAL_LOG, + "PNAL(%d): Failed to join Profinet multicast group\n", + __LINE__); + } + + if (handle->socket > -1) + { + handle->thread = os_thread_create ( + "os_eth_task", + pnal_cfg->eth_recv_thread.prio, + pnal_cfg->eth_recv_thread.stack_size, + os_eth_task, + handle); + return handle; + } + else + { + free (handle); + return NULL; + } +} + +int pnal_eth_send (pnal_eth_handle_t * handle, pnal_buf_t * buf) +{ + int ret = send (handle->socket, buf->payload, buf->len, 0); + return ret; +} diff --git a/profinet_stack/p-net/src/ports/linux/pnal_filetools.c b/profinet_stack/p-net/src/ports/linux/pnal_filetools.c new file mode 100644 index 0000000..f19e995 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal_filetools.c @@ -0,0 +1,225 @@ + +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#define _GNU_SOURCE /* Enable execvpe () */ + +#include "pnal_filetools.h" + +#include "pnet_options.h" +#include "options.h" +#include "osal_log.h" + +#include +#include + +#include +#include +#include +#include +#include + +bool pnal_does_file_exist (const char * filepath) +{ + struct stat statbuffer; + + return (stat (filepath, &statbuffer) == 0); +} + +/** + * @internal + * Get the path to the directory where the main binary is located. + * + * @param tempstring Temp: Temporary buffer. + * @param dirpath Out: Path to directory. Terminated string. + * @param size In: Size of the stringbuffers. Should be large + * enough for the full path to the binary. + * + * @return 0 on success. + * -1 if an error occurred. + */ +static int pnal_get_directory_of_binary ( + char * tempstring, + char * dirpath, + size_t size) +{ + ssize_t num_bytes = 0; + memset (tempstring, 0, size); /* Path to executable */ + memset (dirpath, 0, size); + char * resulting_directory = NULL; + + num_bytes = readlink ("/proc/self/exe", tempstring, size); + if (num_bytes == -1) + { + return -1; + } + + /* Verify that we have the full path (size is big enough) */ + if (!pnal_does_file_exist (tempstring)) + { + return -1; + } + + resulting_directory = dirname (tempstring); + snprintf (dirpath, size, "%s", resulting_directory); + + return 0; +} + +/** + * @internal + * Create a PATH string used when searching for scripts + * + * The PATH string is colon separated, and includes the directory where + * the main binary is located. + * + * @param path Out: Resulting path. Terminated string. + * Might be modified also on failure. + * @param size In: Size of the outputbuffer. Should be + * sizeof(PNAL_DEFAULT_SEARCHPATH) + + * PNET_MAX_DIRECTORYPATH_SIZE + * @return 0 on success. + * -1 if an error occurred. + */ +int pnal_create_searchpath (char * path, size_t size) +{ + int written = 0; + + /** Should temporarily hold full path. Resulting size is + * PNET_MAX_DIRECTORYPATH_SIZE */ + char + directory_of_binary[PNET_MAX_DIRECTORYPATH_SIZE + PNET_MAX_FILENAME_SIZE] = + {0}; + char tempstring[PNET_MAX_DIRECTORYPATH_SIZE + PNET_MAX_FILENAME_SIZE] = {0}; + + if ( + pnal_get_directory_of_binary ( + tempstring, + directory_of_binary, + sizeof (directory_of_binary)) != 0) + { + return -1; + } + + written = snprintf ( + path, + size, + "%s:%s", + PNAL_DEFAULT_SEARCHPATH, + directory_of_binary); + if (written < 0 || (unsigned)written >= size) + { + return -1; + } + + return 0; +} + +int pnal_execute_script (const char * argv[]) +{ + /** Terminated string, see pnal_create_searchpath() */ + char child_searchpath + [sizeof (PNAL_DEFAULT_SEARCHPATH) + PNET_MAX_DIRECTORYPATH_SIZE] = {0}; + int childstatus = 0; + pid_t childpid; + char * scriptenviron[] = {NULL}; + int script_returnvalue = 0; +#if LOG_DEBUG_ENABLED(PF_PNAL_LOG) + uint16_t ix; +#endif + + if (argv == NULL) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): No argument vector given for running script.\n", + __LINE__); + return -1; + } + + if (argv[0] == NULL) + { + LOG_ERROR (PF_PNAL_LOG, "PNAL(%d): No script name given.\n", __LINE__); + return -1; + } + + if (pnal_create_searchpath (child_searchpath, sizeof (child_searchpath)) != 0) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Could not build PATH to run script.\n", + __LINE__); + return -1; + } + +#if LOG_DEBUG_ENABLED(PF_PNAL_LOG) + printf ("PNAL(%d): Command for script:", __LINE__); + for (ix = 0; argv[ix] != NULL; ix++) + { + if (strlen (argv[ix]) > 0) + { + printf (" %s", argv[ix]); + } + else + { + printf (" ''"); + } + } + printf ("\n"); +#endif + + /* Fork and exec */ + childpid = fork(); + if (childpid < 0) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to fork the process to run script.\n", + __LINE__); + return -1; + } + else if (childpid == 0) + { + /* We are in the child process */ + + /* setenv() will malloc memory. We will not free() it, but it does not + matter since we do it in the short-lived child process */ + if (setenv ("PATH", child_searchpath, 1) != 0) + { + printf ( + "PNAL(%d): Failed to set PATH in child process to run script.\n", + __LINE__); + exit (EXIT_FAILURE); + } + + execvpe (argv[0], (char * const *)argv, scriptenviron); + + printf ( + "PNAL(%d): Failed to execute in child process. Is the script file " + "missing or lacks execution permission? %s Search path %s\n", + __LINE__, + argv[0], + child_searchpath); + exit (EXIT_FAILURE); + } + else + { + /* We are in the parent (original) process */ + wait (&childstatus); + script_returnvalue = WEXITSTATUS (childstatus); + } + + return (script_returnvalue == 0) ? 0 : -1; +} diff --git a/profinet_stack/p-net/src/ports/linux/pnal_filetools.h b/profinet_stack/p-net/src/ports/linux/pnal_filetools.h new file mode 100644 index 0000000..a988221 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal_filetools.h @@ -0,0 +1,57 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PNAL_FILETOOLS_H +#define PNAL_FILETOOLS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Colon separated paths to search for scripts. No colon at end. */ +#define PNAL_DEFAULT_SEARCHPATH "/bin:/usr/bin" + +/** + * Check if a file or directory exists + * + * @param filepath In: Path to file or directory. Null terminated. + * Trailing slash is optional for directories. + * @return true if file exists + */ +bool pnal_does_file_exist (const char * filepath); + +/** + * Execute a script. + * + * The script will be searched for in: + * - Directories given in PNAL_DEFAULT_SEARCHPATH + * - Same directory as the main binary is located in. + * + * @param argv In: Arguments (null terminated strings). + * The first argument should be the name of + * the script. + * The last element should be NULL. + * @return 0 on success + * -1 if an error occurred + */ +int pnal_execute_script (const char * argv[]); + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_FILETOOLS_H */ diff --git a/profinet_stack/p-net/src/ports/linux/pnal_snmp.c b/profinet_stack/p-net/src/ports/linux/pnal_snmp.c new file mode 100644 index 0000000..04d2311 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal_snmp.c @@ -0,0 +1,81 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pnal.h" +#include "osal.h" +#include "options.h" +#include "osal_log.h" + +#include +#include +#include +#include + +#include "mib/lldpConfigManAddrTable.h" +#include "mib/lldpLocalSystemData.h" +#include "mib/lldpLocManAddrTable.h" +#include "mib/lldpLocPortTable.h" +#include "mib/lldpRemManAddrTable.h" +#include "mib/lldpRemTable.h" +#include "mib/lldpXdot3LocPortTable.h" +#include "mib/lldpXdot3RemPortTable.h" +#include "mib/lldpXPnoLocTable.h" +#include "mib/lldpXPnoRemTable.h" +#include "mib/system_mib.h" + +static void pnal_snmp_thread (void * arg) +{ + struct pnet * pnet = arg; + + snmp_disable_log(); + + /* make us an agentx client. */ + netsnmp_enable_subagent(); + + /* initialize the agent library */ + init_agent ("lldpMIB"); + + /* init mib code */ + init_system_mib (pnet); + init_lldpLocalSystemData (pnet); + init_lldpLocPortTable (pnet); + init_lldpConfigManAddrTable (pnet); + init_lldpLocManAddrTable (pnet); + init_lldpRemManAddrTable (pnet); + init_lldpRemTable (pnet); + init_lldpXdot3LocPortTable (pnet); + init_lldpXdot3RemPortTable (pnet); + init_lldpXPnoLocTable (pnet); + init_lldpXPnoRemTable (pnet); + + /* read lldpMIB.conf files. */ + init_snmp ("lldpMIB"); + + for (;;) + { + agent_check_and_process (1); /* 0 == don't block */ + } +} + +int pnal_snmp_init (struct pnet * pnet, const pnal_cfg_t * pnal_cfg) +{ + os_thread_create ( + "pn_snmp", + pnal_cfg->snmp_thread.prio, + pnal_cfg->snmp_thread.stack_size, + pnal_snmp_thread, + pnet); + return 0; +} diff --git a/profinet_stack/p-net/src/ports/linux/pnal_sys.h b/profinet_stack/p-net/src/ports/linux/pnal_sys.h new file mode 100644 index 0000000..e38d1a5 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal_sys.h @@ -0,0 +1,39 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PNAL_SYS_H +#define PNAL_SYS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "osal.h" + +#include + +#define PNAL_BUF_MAX_SIZE 1522 + +typedef struct os_buf +{ + void * payload; + uint16_t len; +} pnal_buf_t; + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_SYS_H */ diff --git a/profinet_stack/p-net/src/ports/linux/pnal_udp.c b/profinet_stack/p-net/src/ports/linux/pnal_udp.c new file mode 100644 index 0000000..5e44e0b --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnal_udp.c @@ -0,0 +1,112 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pnal.h" +#include "pf_includes.h" + +#include +#include + +int pnal_udp_open (pnal_ipaddr_t addr, pnal_ipport_t port) +{ + struct sockaddr_in local; + int id; + const int enable = 1; + + id = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (id == -1) + { + return -1; + } + + if (setsockopt(id, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) != 0) + { + goto error; + } + + /* set IP and port number */ + local = (struct sockaddr_in){ + .sin_family = AF_INET, + .sin_addr.s_addr = htonl (addr), + .sin_port = htons (port), + .sin_zero = {0}, + }; + + if (bind (id, (struct sockaddr *)&local, sizeof (local)) != 0) + { + goto error; + } + + return id; + +error: + close (id); + return -1; +} + +int pnal_udp_sendto ( + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size) +{ + struct sockaddr_in remote; + int len; + + remote = (struct sockaddr_in){ + .sin_family = AF_INET, + .sin_addr.s_addr = htonl (dst_addr), + .sin_port = htons (dst_port), + .sin_zero = {0}, + }; + len = + sendto (id, data, size, 0, (struct sockaddr *)&remote, sizeof (remote)); + + return len; +} + +int pnal_udp_recvfrom ( + uint32_t id, + pnal_ipaddr_t * src_addr, + pnal_ipport_t * src_port, + uint8_t * data, + int size) +{ + struct sockaddr_in remote; + socklen_t addr_len = sizeof (remote); + int len; + + memset (&remote, 0, sizeof (remote)); + len = recvfrom ( + id, + data, + size, + MSG_DONTWAIT, + (struct sockaddr *)&remote, + &addr_len); + if (len > 0) + { + *src_addr = ntohl (remote.sin_addr.s_addr); + *src_port = ntohs (remote.sin_port); + } + + return len; +} + +void pnal_udp_close (uint32_t id) +{ + close (id); +} diff --git a/profinet_stack/p-net/src/ports/linux/pnet-sampleapp.service b/profinet_stack/p-net/src/ports/linux/pnet-sampleapp.service new file mode 100644 index 0000000..bd3b890 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/pnet-sampleapp.service @@ -0,0 +1,25 @@ +# For usage with the p-net sample application on Raspberry Pi +# Adapt to your hardware and file paths + +[Unit] +Description=p-net sample application +After=network.target +# Change to this line if waiting for snmpd to start +#After=network.target snmpd.service + +[Service] +Type=simple +ExecStartPre=/sbin/ifconfig eth0 up +ExecStartPre=/bin/sh -c '/bin/echo "49152 60999" > /proc/sys/net/ipv4/ip_local_port_range' +ExecStartPre=/usr/bin/enable-rpi-gpio-pins.sh +# Enable this line if waiting for snmpd to start +#ExecStartPre=/usr/bin/sleep 0.3 +WorkingDirectory=/home/pi/profinet/build/ +ExecStart=/home/pi/profinet/build/pn_dev -v -b /sys/class/gpio/gpio27/value -d /sys/class/gpio/gpio22/value +# Use this instead for dual ports: +#ExecStart=/home/pi/profinet/build/pn_dev -v -i br0,eth0,eth1 -b /sys/class/gpio/gpio27/value -d /sys/class/gpio/gpio22/value +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/profinet_stack/p-net/src/ports/linux/sampleapp_main.c b/profinet_stack/p-net/src/ports/linux/sampleapp_main.c new file mode 100644 index 0000000..2f56036 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/sampleapp_main.c @@ -0,0 +1,516 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#define _GNU_SOURCE /* For asprintf() */ + +#include "sampleapp_common.h" +#include "app_gsdml.h" +#include "app_log.h" +#include "app_utils.h" + +#include "osal.h" +#include "osal_log.h" /* For LOG_LEVEL */ +#include "pnal.h" +#include "pnal_filetools.h" +#include + +#include +#include +#include + +#include +#include +#include +#include + +#if PNET_MAX_PHYSICAL_PORTS == 1 +#define APP_DEFAULT_ETHERNET_INTERFACE "eth0" +#else +#define APP_DEFAULT_ETHERNET_INTERFACE "br0,eth0,eth1" +#endif + +#define APP_MAIN_SLEEPTIME_US 5000 * 1000 +#define APP_SNMP_THREAD_PRIORITY 1 +#define APP_SNMP_THREAD_STACKSIZE 256 * 1024 /* bytes */ +#define APP_ETH_THREAD_PRIORITY 10 +#define APP_ETH_THREAD_STACKSIZE 4096 /* bytes */ +#define APP_BG_WORKER_THREAD_PRIORITY 5 +#define APP_BG_WORKER_THREAD_STACKSIZE 4096 /* bytes */ + +/* Note that this sample application uses os_timer_create() for the timer + that controls the ticks. It is implemented in OSAL, and the Linux + implementation uses a thread internally. To modify the timer thread priority, + modify OSAL or use some other timer */ + +app_args_t app_args = {0}; + +/************************* Utilities ******************************************/ + +void show_usage() +{ + printf ("\nSample application for p-net Profinet device stack.\n"); + printf ("\n"); + printf ("Wait for connection from IO-controller.\n"); + printf ("Then read buttons (input) and send to controller.\n"); + printf ("Listen for application LED output (from controller) and set " + "application LED state.\n"); + printf ("It will also send a counter value (useful also without buttons and " + "LED).\n"); + printf ("Button1 value is sent in the periodic data.\n"); + printf ("Button2 cycles through triggering an alarm, setting diagnosis and " + "creating logbook entries.\n"); + printf ("\n"); + printf ("Also the mandatory Profinet signal LED is controlled by this " + "application.\n"); + printf ("\n"); + printf ("The LEDs are controlled by the script set_profinet_leds\n"); + printf ("located in the same directory as the application binary.\n"); + printf ("A version for Raspberry Pi is available, and also a version " + "writing\n"); + printf ("to plain text files (useful for demo if no LEDs are available).\n"); + printf ("\n"); + printf ("Assumes the default gateway is found on .1 on same subnet as the " + "IP address.\n"); + printf ("\n"); + printf ("Optional arguments:\n"); + printf (" --help Show this help text and exit\n"); + printf (" -h Show this help text and exit\n"); + printf (" -v Incresase verbosity. Can be repeated.\n"); + printf (" -f Reset to factory settings, and store to file. " + "Exit.\n"); + printf (" Remember to give the -p flag if necessary.\n"); + printf (" -r Remove stored files and exit.\n"); + printf (" Remember to give the -p flag if necessary.\n"); + printf (" -g Show stack details and exit. Repeat for more " + "details.\n"); + printf ( + " -i INTERF Name of Ethernet interface to use. Defaults to %s\n", + APP_DEFAULT_ETHERNET_INTERFACE); + printf (" Comma separated list if more than one interface " + "given.\n"); + printf ( + " -s NAME Set station name. Defaults to \"%s\". Only used\n", + APP_GSDML_DEFAULT_STATION_NAME); + printf (" if not already available in storage file.\n"); + printf (" -b FILE Path (absolute or relative) to read Button1. " + "Defaults to not read Button1.\n"); + printf (" -d FILE Path (absolute or relative) to read Button2. " + "Defaults to not read Button2.\n"); + printf (" -p PATH Absolute path to storage directory. Defaults to " + "use current directory.\n"); +#if PNET_OPTION_DRIVER_ENABLE + printf (" -m MODE Application offload mode. Only used if P-Net is\n"); + printf (" built with hw offload enabled " + " (PNET_OPTION_DRIVER_ENABLE). \n"); + printf (" Supported modes: none, cpu, full\n"); + printf (" Defaults to none\n"); +#endif + printf ("\n"); + printf ("p-net revision: " PNET_VERSION "\n"); +} + +/** + * Parse command line arguments + * + * @param argc In: Number of arguments + * @param argv In: Arguments + * @return Parsed arguments + */ +app_args_t parse_commandline_arguments (int argc, char * argv[]) +{ + app_args_t output_arguments = {0}; + int option; + + /* Special handling of long argument */ + if (argc > 1) + { + if (strcmp (argv[1], "--help") == 0) + { + show_usage(); + exit (EXIT_FAILURE); + } + } + + /* Default values */ + strcpy (output_arguments.path_button1, ""); + strcpy (output_arguments.path_button2, ""); + strcpy (output_arguments.path_storage_directory, ""); + strcpy (output_arguments.station_name, APP_GSDML_DEFAULT_STATION_NAME); + strcpy (output_arguments.eth_interfaces, APP_DEFAULT_ETHERNET_INTERFACE); + output_arguments.verbosity = 0; + output_arguments.show = 0; + output_arguments.factory_reset = false; + output_arguments.remove_files = false; + output_arguments.mode = MODE_HW_OFFLOAD_NONE; + + while ((option = getopt (argc, argv, "hvgfri:s:b:d:p:m:")) != -1) + { + switch (option) + { + case 'v': + output_arguments.verbosity++; + break; + case 'g': + output_arguments.show++; + break; + case 'f': + output_arguments.factory_reset = true; + break; + case 'r': + output_arguments.remove_files = true; + break; + case 'i': + if ((strlen (optarg) + 1) > sizeof (output_arguments.eth_interfaces)) + { + printf ("Error: The argument to -i is too long.\n"); + exit (EXIT_FAILURE); + } + strcpy (output_arguments.eth_interfaces, optarg); + break; + case 's': + strcpy (output_arguments.station_name, optarg); + break; + case 'b': + if (strlen (optarg) + 1 > PNET_MAX_FILE_FULLPATH_SIZE) + { + printf ("Error: The argument to -b is too long.\n"); + exit (EXIT_FAILURE); + } + strcpy (output_arguments.path_button1, optarg); + break; + case 'd': + if (strlen (optarg) + 1 > PNET_MAX_FILE_FULLPATH_SIZE) + { + printf ("Error: The argument to -d is too long.\n"); + exit (EXIT_FAILURE); + } + strcpy (output_arguments.path_button2, optarg); + break; + case 'p': + if (strlen (optarg) + 1 > PNET_MAX_FILE_FULLPATH_SIZE) + { + printf ("Error: The argument to -p is too long.\n"); + exit (EXIT_FAILURE); + } + strcpy (output_arguments.path_storage_directory, optarg); + break; +#if PNET_OPTION_DRIVER_ENABLE + case 'm': + if (strcmp ("none", optarg) == 0) + { + output_arguments.mode = MODE_HW_OFFLOAD_NONE; + } + else if (strcmp ("cpu", optarg) == 0) + { + output_arguments.mode = MODE_HW_OFFLOAD_CPU; + } + else if (strcmp ("full", optarg) == 0) + { + output_arguments.mode = MODE_HW_OFFLOAD_FULL; + } + else + { + printf ("Error: mode (-m) not supported.\n"); + exit (EXIT_FAILURE); + } + break; +#endif + case 'h': + /* fallthrough */ + case '?': + /* fallthrough */ + default: + show_usage(); + exit (EXIT_FAILURE); + } + } + + /* Use current directory for storage, if not given */ + if (strlen (output_arguments.path_storage_directory) == 0) + { + if ( + getcwd ( + output_arguments.path_storage_directory, + sizeof (output_arguments.path_storage_directory)) == NULL) + { + printf ("Error: Could not read current working directory. Is " + "PNET_MAX_DIRECTORYPATH_SIZE too small?\n"); + exit (EXIT_FAILURE); + } + } + + return output_arguments; +} + +/** + * Read a bool from a file + * + * @param filepath In: Path to file + * @return true if file exists and the first character is '1' + */ +bool read_bool_from_file (const char * filepath) +{ + FILE * fp; + char ch; + int eof_indicator; + + fp = fopen (filepath, "r"); + if (fp == NULL) + { + return false; + } + + ch = fgetc (fp); + eof_indicator = feof (fp); + fclose (fp); + + if (eof_indicator) + { + return false; + } + return ch == '1'; +} + +bool app_get_button (uint16_t id) +{ + if (id == 0) + { + if (app_args.path_button1[0] != '\0') + { + return read_bool_from_file (app_args.path_button1); + } + } + else if (id == 1) + { + if (app_args.path_button2[0] != '\0') + { + return read_bool_from_file (app_args.path_button2); + } + } + return false; +} + +void app_set_led (uint16_t id, bool led_state) +{ + /* Important: + * The Linux sample application uses a script to set the LED state, + * for easy adaption to different development boards. + * + * The script typically writes to files in the /sys directory to set LED + * state via GPIO operations. If you do not have any physical LEDs you can + * use a script that writes to regular files instead. + * + * However, file operations shall be avoided within the main task + * in a real application. File operations may affect the timing of the + * Profinet communication depending on file system implementation. + */ + + char id_str[7] = {0}; /** Terminated string */ + const char * argv[4]; + + sprintf (id_str, "%u", id); + id_str[sizeof (id_str) - 1] = '\0'; + + argv[0] = "set_profinet_leds"; + argv[1] = (char *)&id_str; + argv[2] = (led_state == 1) ? "1" : "0"; + argv[3] = NULL; + + if (pnal_execute_script (argv) != 0) + { + printf ("Failed to set LED state\n"); + } +} + +/** Update configuration with file storage path. + * Validate this path, and Linux button file paths + * + * @param p_cfg InOut: Configuration to be updated + * @param p_args In: Command line arguments + * @return 0 on success, -1 on error. + */ +static int app_pnet_cfg_init_storage ( + pnet_cfg_t * p_cfg, + const app_args_t * p_args) +{ + strcpy (p_cfg->file_directory, p_args->path_storage_directory); + + if (p_args->verbosity > 0) + { + printf ("Storage directory: %s\n\n", p_cfg->file_directory); + } + + /* Validate paths */ + if (!pnal_does_file_exist (p_cfg->file_directory)) + { + printf ( + "Error: The given storage directory does not exist: %s\n", + p_cfg->file_directory); + return -1; + } + + if (p_args->path_button1[0] != '\0') + { + if (!pnal_does_file_exist (p_args->path_button1)) + { + printf ( + "Error: The given input file for Button1 does not exist: %s\n", + p_args->path_button1); + return -1; + } + } + + if (p_args->path_button2[0] != '\0') + { + if (!pnal_does_file_exist (p_args->path_button2)) + { + printf ( + "Error: The given input file for Button2 does not exist: %s\n", + p_args->path_button2); + return -1; + } + } + return 0; +} + +/****************************** Main ******************************************/ + +int main (int argc, char * argv[]) +{ + int ret; + int32_t app_log_level = APP_LOG_LEVEL_FATAL; + pnet_cfg_t pnet_cfg = {0}; + app_data_t * sample_app = NULL; + app_utils_netif_namelist_t netif_name_list; + pnet_if_cfg_t netif_cfg = {0}; + uint16_t number_of_ports = 1; + + /* Enable line buffering for printouts, especially when logging to + the journal (which is default when running as a systemd job) */ + setvbuf (stdout, NULL, _IOLBF, 0); + + /* Parse and display command line arguments */ + app_args = parse_commandline_arguments (argc, argv); + + app_log_level = (app_args.verbosity <= APP_LOG_LEVEL_FATAL) + ? APP_LOG_LEVEL_FATAL - app_args.verbosity + : APP_LOG_LEVEL_DEBUG; + app_log_set_log_level (app_log_level); + printf ("\n** Starting P-Net sample application " PNET_VERSION " **\n"); + + APP_LOG_INFO ( + "Number of slots: %u (incl slot for DAP module)\n", + PNET_MAX_SLOTS); + APP_LOG_INFO ("P-net log level: %u (DEBUG=0, FATAL=4)\n", LOG_LEVEL); + APP_LOG_INFO ("App log level: %u (DEBUG=0, FATAL=4)\n", app_log_level); + APP_LOG_INFO ("Max number of ports: %u\n", PNET_MAX_PHYSICAL_PORTS); + APP_LOG_INFO ("Network interfaces: %s\n", app_args.eth_interfaces); + APP_LOG_INFO ("Button1 file: %s\n", app_args.path_button1); + APP_LOG_INFO ("Button2 file: %s\n", app_args.path_button2); + APP_LOG_INFO ("Default station name: %s\n", app_args.station_name); + + /* Prepare configuration */ + app_pnet_cfg_init_default (&pnet_cfg); + strcpy (pnet_cfg.station_name, app_args.station_name); + ret = app_utils_pnet_cfg_init_netifs ( + app_args.eth_interfaces, + &netif_name_list, + &number_of_ports, + &netif_cfg); + if (ret != 0) + { + exit (EXIT_FAILURE); + } + pnet_cfg.if_cfg = netif_cfg; + pnet_cfg.num_physical_ports = number_of_ports; + + app_utils_print_network_config (&netif_cfg, number_of_ports); + + /* Operating system specific settings */ + pnet_cfg.pnal_cfg.snmp_thread.prio = APP_SNMP_THREAD_PRIORITY; + pnet_cfg.pnal_cfg.snmp_thread.stack_size = APP_SNMP_THREAD_STACKSIZE; + pnet_cfg.pnal_cfg.eth_recv_thread.prio = APP_ETH_THREAD_PRIORITY; + pnet_cfg.pnal_cfg.eth_recv_thread.stack_size = APP_ETH_THREAD_STACKSIZE; + pnet_cfg.pnal_cfg.bg_worker_thread.prio = APP_BG_WORKER_THREAD_PRIORITY; + pnet_cfg.pnal_cfg.bg_worker_thread.stack_size = + APP_BG_WORKER_THREAD_STACKSIZE; + + ret = app_pnet_cfg_init_storage (&pnet_cfg, &app_args); + if (ret != 0) + { + printf ("Failed to initialize storage.\n"); + printf ("Aborting application\n"); + exit (EXIT_FAILURE); + } + + /* Remove files and exit */ + if (app_args.remove_files == true) + { + printf ("\nRemoving stored files\n"); + printf ("Exit application\n"); + (void)pnet_remove_data_files (pnet_cfg.file_directory); + exit (EXIT_SUCCESS); + } + + /* Initialise stack and application */ + sample_app = app_init (&pnet_cfg, &app_args); + if (sample_app == NULL) + { + printf ("Failed to initialize P-Net.\n"); + printf ("Do you have enough Ethernet interface permission?\n"); + printf ("Aborting application\n"); + exit (EXIT_FAILURE); + } + + /* Do factory reset and exit */ + if (app_args.factory_reset == true) + { + printf ("\nPerforming factory reset\n"); + printf ("Exit application\n"); + (void)pnet_factory_reset (app_get_pnet_instance (sample_app)); + exit (EXIT_SUCCESS); + } + + /* Show stack info and exit */ + if (app_args.show > 0) + { + int level = 0xFFFF; + + printf ("\nShowing stack information.\n\n"); + if (app_args.show == 1) + { + level = 0x2010; /* See documentation for pnet_show() */ + } + + pnet_show (app_get_pnet_instance (sample_app), level); + printf ("Exit application\n"); + exit (EXIT_SUCCESS); + } + + /* Start main loop */ + if (app_start (sample_app, RUN_IN_SEPARATE_THREAD) != 0) + { + printf ("Failed to start\n"); + printf ("Aborting application\n"); + exit (EXIT_FAILURE); + } + + for (;;) + { + os_usleep (APP_MAIN_SLEEPTIME_US); + } + + return 0; +} diff --git a/profinet_stack/p-net/src/ports/linux/set_network_parameters b/profinet_stack/p-net/src/ports/linux/set_network_parameters new file mode 100644 index 0000000..2a4936a --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/set_network_parameters @@ -0,0 +1,89 @@ +#!/usr/bin/env sh + +############################################ +# Script for manipulating network settings # +############################################ +# +# This is useful for example on Ubuntu. +# +# You need to adapt this script to fit your Linux distribution. +# Keep the script name, and use the same command line arguments. +# +# Make sure that the script path is available from your Profinet application. +# In case of problems, try the script manually from the directory where your +# application is located. + +if [ $# -ne 6 ]; then + echo "Usage: ${0} interfacename ip netmask gateway hostname permanent" + echo " where:" + echo " interfacename: Network interface name, for example eth0" + echo " ip: IPv4 address, for example 192.168.0.50" + echo " netmask: Netmask" + echo " gateway: Default gateway" + echo " hostname: Host name, for example my_laptop_3" + echo " permanent: 1 if changes are permanent, 0 if temporary" + exit 1 +fi + +INTERFACE=$1 +IP_ADDRESS=$2 +NETMASK=$3 +DEFAULT_GATEWAY=$4 +HOSTNAME=$5 +SET_VALUES_PERMANENTLY=$6 + +# From a Profinet point of view there is no need to change the +# host name of the Linux machine. The p-net stack will use the +# name as station name internally. +SKIP_SETTING_HOSTNAME=true + +echo "Network script for ${INTERFACE}: " \ + "Set IP ${IP_ADDRESS} " \ + "Netmask ${NETMASK} " \ + "Gateway ${DEFAULT_GATEWAY} " \ + "Permanent: ${SET_VALUES_PERMANENTLY} " \ + "Hostname: ${HOSTNAME} " \ + "Skip setting hostname: ${SKIP_SETTING_HOSTNAME}" + +# There is no need to set the changes permanently, +# as the p-net stack will set the IP parameters on each start. +# You are however free to use it if you like. + +if ! ip address flush label $INTERFACE; then + echo "Failed to flush network interface" + exit 1 +fi + +if ! ip address add $IP_ADDRESS/$NETMASK dev $INTERFACE; then + echo "Failed to set IP address and netmask" + exit 1 +fi + +if ! ip link set dev $INTERFACE up; then + echo "Failed to set network interface up" + exit 1 +fi + +if [ "${DEFAULT_GATEWAY}" != "0.0.0.0" ]; then + if ! ip route add default via $DEFAULT_GATEWAY; then + echo "Failed to set default gateway" + exit 1 + fi +else + echo "No valid default gateway given. Skipping setting default gateway." +fi + +if [ "$SKIP_SETTING_HOSTNAME" = false ]; then + if ! hostname $HOSTNAME; then + echo "Failed to set hostname to $HOSTNAME" + hostname_len=${#HOSTNAME} + if [ $hostname_len -gt 64 ]; then + echo "The given hostname length is ${hostname_len} characters." \ + "Many Linux systems have a limit of 64 characters. The p-net stack uses" \ + "a longer stationname internally." + fi + exit 1 + fi +fi + +exit 0 diff --git a/profinet_stack/p-net/src/ports/linux/set_profinet_leds b/profinet_stack/p-net/src/ports/linux/set_profinet_leds new file mode 100644 index 0000000..ce6328f --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/set_profinet_leds @@ -0,0 +1,36 @@ +#!/usr/bin/env sh + +############################################# +# Script for setting Profinet LEDs on Linux # +############################################# +# +# This writes the output to files instead of real LEDs. +# It can be useful when running on for example Ubuntu or some other +# desktop Linux variant. +# +# Modify this script to fit your setup, by for example changing output path. + +if [ $# -ne 2 ]; then + echo "Usage: ${0} led_number led_state" + echo " where:" + echo " led_number: LED number. Number 1 for sample app data LED" + echo " Number 2 for mandatory Profinet signal LED" + echo " led_state: 1 for on, 0 for off" + echo " Exit code: 0 on success, 1 on error" + exit 1 +fi + +LED_NUMBER=$1 +LED_STATE=$2 + +# Uncomment next line for debugging +#echo "Script for setting Profinet LED state in dummy file. LED number ${LED_NUMBER}, new state ${LED_STATE}" + +if ! [ "${LED_STATE}" = "0" -o "${LED_STATE}" = "1" ]; then + echo "Wrong LED state: ${LED_STATE}" + exit 1 +fi + +echo ${LED_STATE} > "pnet_led_${LED_NUMBER}.txt" + +exit 0 diff --git a/profinet_stack/p-net/src/ports/linux/set_profinet_leds.raspberrypi b/profinet_stack/p-net/src/ports/linux/set_profinet_leds.raspberrypi new file mode 100644 index 0000000..2cf2c76 --- /dev/null +++ b/profinet_stack/p-net/src/ports/linux/set_profinet_leds.raspberrypi @@ -0,0 +1,42 @@ +#!/usr/bin/env sh + +############################################# +# Script for setting Profinet LEDs on Linux # +############################################# +# +# Adapt this script to your particular hardware + +if [ $# -ne 2 ]; then + echo "Usage: ${0} led_number led_state" + echo " where:" + echo " led_number: LED number. Number 1 for sample app data LED" + echo " Number 2 for mandatory Profinet signal LED" + echo " led_state: 1 for on, 0 for off" + echo " Exit code: 0 on success, 1 on error" + exit 1 +fi + +LED_NUMBER=$1 +LED_STATE=$2 + +# Uncomment next line for debugging +#echo "Script for Profinet LEDs on RPi. LED number ${LED_NUMBER}, new state ${LED_STATE}" + +if ! [ "${LED_STATE}" = "0" -o "${LED_STATE}" = "1" ]; then + echo "Wrong LED state: ${LED_STATE}" + exit 1 +fi +if [ "${LED_NUMBER}" = "1" ]; then + GPIO=17 +elif [ "${LED_NUMBER}" = "2" ]; then + GPIO=23 +else + echo "Wrong LED number: ${LED_NUMBER}" + exit 1 +fi + +echo ${GPIO} > /sys/class/gpio/export 2> /dev/null +echo out > /sys/class/gpio/gpio${GPIO}/direction 2> /dev/null +echo ${LED_STATE} > /sys/class/gpio/gpio${GPIO}/value + +exit 0 diff --git a/profinet_stack/p-net/src/ports/rt-kernel/0001-rtkernel-for-Profinet.patch b/profinet_stack/p-net/src/ports/rt-kernel/0001-rtkernel-for-Profinet.patch new file mode 100644 index 0000000..a6dd665 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/0001-rtkernel-for-Profinet.patch @@ -0,0 +1,1476 @@ +From 6c80d905968601ea397d63809690b3c9754f3146 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Fredrik=20M=C3=B6ller?= +Date: Tue, 15 Sep 2020 18:22:53 +0200 +Subject: [PATCH] rtkernel for Profinet + +Changes made to rt-kernel tree from rt-collab +Toolbox 2021.1 as to support the p-net Profinet +stack: +- lwip: + - Build SNMP server. + - Add SNMP callbacks for MIB-II system variables. + - Fix SNMP ifDescr in ifTable. + - Discard invalid UDP packets. + - VLAN tag support. + - Lock mutex while handling received frame + instead of passing message to lwip thread + (LWIP_TCPIP_CORE_LOCKING_INPUT). + - Add hook function to be called when frame of + unknown EtherType is received, such as LLDP + and Profinet (LWIP_HOOK_UNKNOWN_ETH_PROTOCOL). +- drivers/eth/dwmac1000: + - Use zero copy for Rx. + - Add ioctl for getting link status. + - Lock mutex for Tx, as p-net stack may send + frames outside of lwip context. + - Report accurate ifSpeed to SNMP. +- drivers/eth/dwgmac: + - Add ioctl for getting link status. + - Lock mutex for Tx, as p-net stack may send + frames outside of lwip context. + - Report accurate ifSpeed to SNMP. +- drivers/eth/phy: + - Add function for retrieving link capabilities. +- bsp/xmc48relax: + - Static IP addressing. + - Increase stack size for main task. + - Increase stack size for Ethernet driver task. + - Let Ethernet driver task have higher priority + than lwip task. + - Put .bss and heap in 256kB memory section. +- bsp/h750dk: + - Increase stack size for main task. + - Increase stack size for Ethernet driver task. + - Let Ethernet driver task have higher priority + than lwip task. + +Change-Id: Ie335fd029b503db842d3ca16331629444db70b9a +--- + bsp/h750dk/include/config.h | 2 +- + bsp/h750dk/src/lwip.c | 8 +- + bsp/xmc48relax/include/config.h | 10 +- + bsp/xmc48relax/src/lwip.c | 4 +- + bsp/xmc48relax/xmc48relax.ld | 24 +- + drivers/eth/dwgmac.c | 89 ++++++- + drivers/eth/dwmac1000.c | 162 ++++++++--- + drivers/eth/phy/mii.c | 13 +- + include/drivers/eth/phy/mii.h | 1 + + include/drivers/eth/phy/phy.h | 9 + + include/drivers/ioctl.h | 51 ++++ + kern/arch/xmc4/hal.h | 2 +- + lwip/CMakeLists.txt | 1 + + lwip/src/apps/Makefile | 12 + + lwip/src/apps/snmp/Makefile | 16 ++ + lwip/src/apps/snmp/snmp_mib2_interfaces.c | 6 +- + lwip/src/apps/snmp/snmp_mib2_system.c | 310 ++-------------------- + lwip/src/core/lwip_hooks.c | 23 ++ + lwip/src/core/udp.c | 12 + + lwip/src/include/lwip/apps/snmp_mib2.h | 24 +- + lwip/src/include/lwip/lwip_hooks.h | 41 +++ + lwip/src/include/lwip/lwipopts.h | 39 ++- + 22 files changed, 511 insertions(+), 348 deletions(-) + create mode 100644 lwip/src/apps/Makefile + create mode 100644 lwip/src/apps/snmp/Makefile + create mode 100644 lwip/src/core/lwip_hooks.c + create mode 100644 lwip/src/include/lwip/lwip_hooks.h + +diff --git a/bsp/h750dk/include/config.h b/bsp/h750dk/include/config.h +index 05ed51b8b..4963aad37 100644 +--- a/bsp/h750dk/include/config.h ++++ b/bsp/h750dk/include/config.h +@@ -107,7 +107,7 @@ + * implemented in the user application and is the task that will be + * started immediately after the kernel has finished initialising. + */ +-#define CFG_MAIN_STACK_SIZE 1280 ++#define CFG_MAIN_STACK_SIZE 14374 + + /* Configure the priority of the main task. */ + #define CFG_MAIN_PRIORITY 10 +diff --git a/bsp/h750dk/src/lwip.c b/bsp/h750dk/src/lwip.c +index e22b17f33..9a7281005 100644 +--- a/bsp/h750dk/src/lwip.c ++++ b/bsp/h750dk/src/lwip.c +@@ -26,8 +26,8 @@ + #define RX_BUFFERS 8 /* Number of Rx buffers */ + #define TX_BUFFERS 8 /* Number of Tx buffers */ + +-#define RX_BUFFER_SIZE 1520 /* Max size of Rx buffers */ +-#define TX_BUFFER_SIZE 1520 /* Max size of Tx buffers */ ++#define RX_BUFFER_SIZE PBUF_POOL_BUFSIZE /* Max size of Rx buffers */ ++#define TX_BUFFER_SIZE PBUF_POOL_BUFSIZE /* Max size of Tx buffers */ + + static dwgmac_descriptor_t rx_dma[RX_BUFFERS] CC_ATTRIBUTE_SECTION (".dma"); + static dwgmac_descriptor_t tx_dma[TX_BUFFERS] CC_ATTRIBUTE_SECTION (".dma"); +@@ -114,8 +114,8 @@ err_t bsp_eth_init (struct netif * netif) + .buffer_size = TX_BUFFER_SIZE, + .num_buffers = TX_BUFFERS, + }, +- .rx_task_prio = TCPIP_THREAD_PRIO - 1, +- .rx_task_stack = 1100, ++ .rx_task_prio = TCPIP_THREAD_PRIO + 1, ++ .rx_task_stack = 6250, + .mac_address = CFG_LWIP_MAC_ADDRESS + }; + +diff --git a/bsp/xmc48relax/include/config.h b/bsp/xmc48relax/include/config.h +index 91549e483..54cb9d39f 100644 +--- a/bsp/xmc48relax/include/config.h ++++ b/bsp/xmc48relax/include/config.h +@@ -53,14 +53,14 @@ + * CFG_LWIP_NAMESERVER IP address of DNS server + */ + #define CFG_LWIP_MAC_ADDRESS {0x1E, 0x30, 0x6C, 0xA2, 0x45, 0x5E} +-#define CFG_LWIP_ADDRESS_DYNAMIC ++#undef CFG_LWIP_ADDRESS_DYNAMIC + #if defined(CFG_LWIP_ADDRESS_DYNAMIC) + #define CFG_LWIP_HOSTNAME "xmc48relax" + #else +- #define CFG_LWIP_IPADDR() IP4_ADDR (&ipaddr, 192, 168, 10, 171) ++ #define CFG_LWIP_IPADDR() IP4_ADDR (&ipaddr, 192, 168, 0, 50) + #define CFG_LWIP_NETMASK() IP4_ADDR (&netmask, 255, 255, 255, 0) +- #define CFG_LWIP_GATEWAY() IP4_ADDR (&gw, 192, 168, 10, 1) +- #define CFG_LWIP_NAMESERVER() IP4_ADDR (&nameserver, 192, 168, 10, 4) ++ #define CFG_LWIP_GATEWAY() IP4_ADDR (&gw, 192, 168, 0, 1) ++ #define CFG_LWIP_NAMESERVER() IP4_ADDR (&nameserver, 0, 0, 0, 0) + #endif + + /* Enable SD Card if defined */ +@@ -132,7 +132,7 @@ + * implemented in the user application and is the task that will be + * started immediately after the kernel has finished initialising. + */ +-#define CFG_MAIN_STACK_SIZE 1280 ++#define CFG_MAIN_STACK_SIZE 14374 + + /* Configure the priority of the main task. */ + #define CFG_MAIN_PRIORITY 10 +diff --git a/bsp/xmc48relax/src/lwip.c b/bsp/xmc48relax/src/lwip.c +index f8b7d94af..858a794bc 100644 +--- a/bsp/xmc48relax/src/lwip.c ++++ b/bsp/xmc48relax/src/lwip.c +@@ -94,8 +94,8 @@ err_t bsp_eth_init (struct netif * netif) + .hclk = CFG_ETHCLK_FREQUENCY, + .rx_buffers = 3, + .tx_buffers = 5, +- .rx_task_prio = TCPIP_THREAD_PRIO - 1, +- .rx_task_stack = 1100, ++ .rx_task_prio = TCPIP_THREAD_PRIO + 1, ++ .rx_task_stack = 6250, + .mac_address = CFG_LWIP_MAC_ADDRESS, + }; + #ifdef CFG_RTC_INIT +diff --git a/bsp/xmc48relax/xmc48relax.ld b/bsp/xmc48relax/xmc48relax.ld +index 9deadfd32..3e1ac030a 100644 +--- a/bsp/xmc48relax/xmc48relax.ld ++++ b/bsp/xmc48relax/xmc48relax.ld +@@ -18,6 +18,7 @@ EXTERN(vectors) + * sram: The Code RAM is intended for user code or Operating System data storage. The + * memory is accessed via the Bus Matrix and provides zero-wait-state access for the CPU + * for code execution or data access. ++ * NOTE: May not be used by USB and ETH modules. + * + * sdsram: The System RAM is intended for general user data storage. The System RAM is + * accessed via the Bus Matrix and provides zero-wait-state access for data. +@@ -30,8 +31,7 @@ MEMORY + { + flash : org = 0x08000000, len = 2M + sram : org = 0x1FFE8000, len = 96K +- sdsram : org = 0x20000000, len = 128K +- cdsram : org = 0x20020000, len = 128K ++ ram : org = 0x20000000, len = 256K /* sdsram + cdsram */ + } + + SECTIONS +@@ -85,19 +85,23 @@ SECTIONS + + /* Put the IRQ-stack at the start of RAM as to + * provoke CPU exception upon overflow. */ +- .bss : ++ .os_stuff : + { + KEEP (*(.irqstack)); + irq_stack_top = .; ++ event_start = .; ++ KEEP (*(.event)); ++ event_end = .; ++ } > sram ++ ++ .bss : ++ { + bss_start = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + bss_end = .; +- event_start = .; +- KEEP (*(.event)); +- event_end = .; +- } > sram ++ } > ram + + .data : AT (data_start_rom) + { +@@ -106,13 +110,13 @@ SECTIONS + *(.data*) + . = ALIGN(4); + data_end_ram = .; +- } > sram ++ } > ram + + .sdsram (NOLOAD) : + { + heap_start = .; +- } > sdsram ++ } > ram + +- heap_end = ORIGIN(sdsram) + LENGTH(sdsram); ++ heap_end = ORIGIN(ram) + LENGTH(ram); + heap_size = heap_end - heap_start; + } +diff --git a/drivers/eth/dwgmac.c b/drivers/eth/dwgmac.c +index 91518e412..e54bc5443 100644 +--- a/drivers/eth/dwgmac.c ++++ b/drivers/eth/dwgmac.c +@@ -274,6 +274,7 @@ typedef struct dwgmac + drv_t drv; + + irq_t irq; ++ mtx_t * mtx_tx; + task_t * tRcv; + phy_t * phy; + volatile eth_mac_t * mac; +@@ -291,6 +292,19 @@ typedef struct dwgmac + + /* MIIAR CR divider */ + uint32_t cr; ++ ++ /* Link state retrieved in last call to probe function ++ * See defines PHY_LINK_OK, PHY_LINK_100MBIT, etc. ++ */ ++ uint8_t last_link_state; ++ ++ /* Link auto-negotiation capabilities read from PHY at startup ++ * See defines PHY_CAPABILITY_100_FD, PHY_CAPABILITY_100, etc. ++ */ ++ uint16_t link_capabilities; ++ ++ /* lwip network interface handle */ ++ struct netif * netif; + } dwgmac_t; + + #ifdef DEBUG_DATA +@@ -325,6 +339,24 @@ static void dwgmac_pbuf_dump (struct pbuf *p) + } + #endif /* DEBUG_DATA */ + ++#if MIB2_STATS ++static uint32_t dwgmac_snmp_link_speed (uint8_t link_state) ++{ ++ if (link_state & PHY_LINK_10MBIT) ++ { ++ return 10 * 1000 * 1000; ++ } ++ else if (link_state & PHY_LINK_100MBIT) ++ { ++ return 100 * 1000 * 1000; ++ } ++ else ++ { ++ return 0; ++ } ++} ++#endif /* MIB2_STATS */ ++ + static uint16_t dwgmac_read_phy (void * arg, uint8_t address, uint8_t reg) + { + dwgmac_t *dwgmac = arg; +@@ -490,6 +522,9 @@ static err_t dwgmac_hw_transmit_frame (struct netif *netif, struct pbuf *p) + return ERR_OK; + } + ++ /* Lock TX handling */ ++ mtx_lock (dwgmac->mtx_tx); ++ + #if ETH_PAD_SIZE + pbuf_header (p, -ETH_PAD_SIZE); /* drop the padding word */ + #endif +@@ -548,6 +583,7 @@ static err_t dwgmac_hw_transmit_frame (struct netif *netif, struct pbuf *p) + pbuf_header (p, ETH_PAD_SIZE); /* reclaim the padding word */ + #endif + ++ mtx_unlock (dwgmac->mtx_tx); + return ERR_OK; + } + +@@ -763,16 +799,64 @@ static dev_state_t dwgmac_probe (drv_t * drv) + + link_state = dwgmac->phy->ops->get_link_state (dwgmac->phy); + ++ /* Save link state for IOCTL_ETH_GET_STATUS */ ++ dwgmac->last_link_state = link_state; ++ ++ /* Set ifSpeed in SNMP MIB-II ifTable */ ++#if MIB2_STATS ++ dwgmac->netif->link_speed = dwgmac_snmp_link_speed (link_state); ++#endif /* MIB2_STATS */ ++ ++ + return (link_state & PHY_LINK_OK) ? StateAttached : StateDetached; + } + ++static void dwgmac_get_status ( ++ dwgmac_t * dwgmac, ++ struct netif * netif, ++ ioctl_eth_status_t * link) ++{ ++ if (dwgmac->phy->loopback_mode) ++ { ++ link->is_autonegotiation_supported = false; ++ link->is_autonegotiation_enabled = false; ++ link->capabilities = 0x0000; ++ } ++ else ++ { ++ link->is_autonegotiation_supported = true; ++ link->is_autonegotiation_enabled = true; ++ link->capabilities = dwgmac->link_capabilities; ++ } ++ ++ link->state = dwgmac->last_link_state; ++ link->is_operational = netif_is_up (netif) && netif_is_link_up (netif); ++} ++ ++static int dwgmac_ioctl (drv_t * drv, void * arg, int req, void * param) ++{ ++ dwgmac_t * dwgmac = (dwgmac_t *)drv; ++ int status = EARG; ++ ++ switch (req) ++ { ++ case IOCTL_ETH_GET_STATUS: ++ dwgmac_get_status (dwgmac, arg, param); ++ return 0; ++ default: ++ UASSERT (0, EARG); ++ } ++ ++ return status; ++} ++ + static const drv_ops_t dwgmac_ops = + { + .open = NULL, + .read = NULL, + .write = NULL, + .close = NULL, +- .ioctl = NULL, ++ .ioctl = dwgmac_ioctl, + .hotplug = dwgmac_hotplug, + }; + +@@ -802,6 +886,7 @@ drv_t * dwgmac_init (const char * name, const dwgmac_cfg_t * cfg, + /* Initialise driver state */ + dwgmac->phy = phy; + dwgmac->irq = cfg->irq; ++ dwgmac->mtx_tx = mtx_create(); + dwgmac->mac = (eth_mac_t *)cfg->base; + dwgmac->mtl = (eth_mtl_t *)(cfg->base + 0xC00); + dwgmac->dma = (eth_dma_t *)(cfg->base + 0x1000); +@@ -813,6 +898,8 @@ drv_t * dwgmac_init (const char * name, const dwgmac_cfg_t * cfg, + dwgmac->tx.num_buffers = cfg->tx.num_buffers; + dwgmac->rx.ix = 0; + dwgmac->tx.ix = 0; ++ dwgmac->last_link_state = 0x00; ++ dwgmac->netif = netif; + + /* Create receive task */ + dwgmac->tRcv = task_spawn ("EthRcv", +diff --git a/drivers/eth/dwmac1000.c b/drivers/eth/dwmac1000.c +index ece017a59..456986211 100644 +--- a/drivers/eth/dwmac1000.c ++++ b/drivers/eth/dwmac1000.c +@@ -26,7 +26,13 @@ + + #include + +-#define BUFFER_SIZE 1520 ++/* Align DMA buffer with lwIP POOL buf size ++ * One RX DMA buffer will fit in one POOL buf. ++ */ ++#define BUFFER_SIZE PBUF_POOL_BUFSIZE ++COMPILETIME_ASSERT (BUFFER_SIZE >= 1520); ++COMPILETIME_ASSERT ((BUFFER_SIZE % 4) == 0); ++ + #define RX_MAX_BUFFERS 10 + #define TX_MAX_BUFFERS 10 + +@@ -194,6 +200,7 @@ typedef struct dma_descriptor + uint32_t des1; + uint8_t * buff; + volatile struct dma_descriptor * next; ++ struct pbuf * pbuf; + } dma_descriptor_t; + + #define xDES0_OWN BIT(31) /* Owned by DMA engine */ +@@ -215,6 +222,7 @@ typedef struct dwmac1000 + drv_t drv; + + irq_t irq; ++ mtx_t * mtx_tx; + task_t * tRcv; + phy_t * phy; + volatile eth_mac_t * mac; +@@ -229,6 +237,19 @@ typedef struct dwmac1000 + + /* MIIAR CR divider */ + uint32_t cr; ++ ++ /* Link state retrieved in last call to probe function ++ * See defines PHY_LINK_OK, PHY_LINK_100MBIT, etc. ++ */ ++ uint8_t last_link_state; ++ ++ /* Link auto-negotiation capabilities read from PHY at startup ++ * See defines PHY_CAPABILITY_100_FD, PHY_CAPABILITY_100, etc. ++ */ ++ uint16_t link_capabilities; ++ ++ /* lwip network interface handle */ ++ struct netif * netif; + } dwmac1000_t; + + #ifdef DEBUG_DATA +@@ -263,6 +284,24 @@ static void dwmac1000_pbuf_dump (struct pbuf *p) + } + #endif /* DEBUG_DATA */ + ++#if MIB2_STATS ++static uint32_t dwmac1000_snmp_link_speed (uint8_t link_state) ++{ ++ if (link_state & PHY_LINK_10MBIT) ++ { ++ return 10 * 1000 * 1000; ++ } ++ else if (link_state & PHY_LINK_100MBIT) ++ { ++ return 100 * 1000 * 1000; ++ } ++ else ++ { ++ return 0; ++ } ++} ++#endif /* MIB2_STATS */ ++ + static uint16_t dwmac1000_read_phy (void * arg, uint8_t address, uint8_t reg) + { + dwmac1000_t *dwmac1000 = arg; +@@ -326,6 +365,13 @@ static void dwmac1000_hw_init (struct netif *netif, const dwmac1000_cfg_t * cfg) + DEBUG_ASSERT (dwmac1000->phy->ops->start != NULL); + dwmac1000->phy->ops->start (dwmac1000->phy); + ++ /* Read auto-negotiation capabilities from PHY */ ++ if (dwmac1000->phy->ops->get_link_capabilities) ++ { ++ dwmac1000->link_capabilities = ++ dwmac1000->phy->ops->get_link_capabilities (dwmac1000->phy); ++ } ++ + /* Enable receiver, transmitter */ + mac->cr = CR_RE | CR_TE; + +@@ -352,13 +398,18 @@ static void dwmac1000_hw_init (struct netif *netif, const dwmac1000_cfg_t * cfg) + /* Setup Rx descriptors using chained mode */ + for (ix = 0; ix < cfg->rx_buffers; ix++) + { +- uint8_t * p = malloc (BUFFER_SIZE); ++ /* Allocate a pbuf from the pool. ++ * Note that pbuf will be unchained (pbuf->next==NULL) as allocated size ++ * fits in a single pool buffer. ++ */ ++ struct pbuf * p = pbuf_alloc (PBUF_RAW, BUFFER_SIZE, PBUF_POOL); + UASSERT (p != NULL, EMEM); + + dwmac1000->rx[ix].des0 = xDES0_OWN; + dwmac1000->rx[ix].des1 = RDES1_RCH | BUFFER_SIZE; +- dwmac1000->rx[ix].buff = p; ++ dwmac1000->rx[ix].buff = p->payload; + dwmac1000->rx[ix].next = &dwmac1000->rx[ix + 1]; ++ dwmac1000->rx[ix].pbuf = p; + } + dwmac1000->rx[ix - 1].next = &dwmac1000->rx[0]; /* Point to first */ + dwmac1000->pRx = &dwmac1000->rx[0]; /* Driver index */ +@@ -373,6 +424,7 @@ static void dwmac1000_hw_init (struct netif *netif, const dwmac1000_cfg_t * cfg) + dwmac1000->tx[ix].des0 = TDES0_TCH; + dwmac1000->tx[ix].buff = p; + dwmac1000->tx[ix].next = &dwmac1000->tx[ix + 1]; ++ dwmac1000->tx[ix].pbuf = NULL; + } + dwmac1000->tx[ix - 1].next = &dwmac1000->tx[0]; /* Point to first */ + dwmac1000->pTx = &dwmac1000->tx[0]; /* Driver index */ +@@ -420,6 +472,9 @@ static err_t dwmac1000_hw_transmit_frame (struct netif *netif, struct pbuf *p) + return ERR_OK; + } + ++ /* Lock TX handling */ ++ mtx_lock (dwmac1000->mtx_tx); ++ + #if ETH_PAD_SIZE + pbuf_header (p, -ETH_PAD_SIZE); /* drop the padding word */ + #endif +@@ -478,6 +533,7 @@ static err_t dwmac1000_hw_transmit_frame (struct netif *netif, struct pbuf *p) + pbuf_header (p, ETH_PAD_SIZE); /* reclaim the padding word */ + #endif + ++ mtx_unlock (dwmac1000->mtx_tx); + return ERR_OK; + } + +@@ -513,7 +569,7 @@ static struct pbuf *dwmac1000_hw_get_received_frame (struct netif * netif) + struct pbuf *p = NULL; + uint16_t length; + volatile dma_descriptor_t * pRx; +- uint8_t * buffer; ++ struct pbuf * q; + uint32_t des0; + + pRx = dwmac1000->pRx; +@@ -527,39 +583,33 @@ static struct pbuf *dwmac1000_hw_get_received_frame (struct netif * netif) + if ((des0 & (RDES0_FS | RDES0_LS)) != (RDES0_FS | RDES0_LS)) + goto done; + +- buffer = pRx->buff; ++ /* Fetch received pbuf */ ++ p = pRx->pbuf; ++ p->payload = pRx->buff; + length = (des0 >> 16) & 0x3FFF; +- length -= 4; /* Drop CRC */ ++ length -= 4; /* Drop CRC */ ++ p->len = p->tot_len = length; ++ ++ DPRINT ("in (%d):\n", p->len); ++#ifdef DEBUG_DATA ++ dwmac1000_pbuf_dump (q); ++#endif /* DEBUG_DATA */ + + #if ETH_PAD_SIZE + #error "ETH_PAD_SIZE not supported" + #endif + +- /* We allocate a pbuf chain of pbufs from the pool. */ +- p = pbuf_alloc (PBUF_RAW, length, PBUF_POOL); ++ /* Allocate a pbuf from the pool to replace the one currently use by DMA. ++ * Note that pbuf will be unchained (pbuf->next==NULL) as allocated size ++ * fits in a single pool buffer. ++ */ ++ q = pbuf_alloc (PBUF_RAW, BUFFER_SIZE, PBUF_POOL); + +- if (p != NULL) ++ if (q != NULL) + { +- struct pbuf *q; +- +- /* We iterate over the pbuf chain until we have read the entire +- * packet into the pbuf. +- */ +- for (q = p; q != NULL; q = q->next) +- { +- /* Read enough bytes to fill this pbuf in the chain. The +- * available data in the pbuf is given by the q->len +- * variable. +- */ +- +- memcpy (q->payload, buffer, q->len); +- buffer += q->len; +- +- DPRINT ("in (%d):\n", q->len); +-#ifdef DEBUG_DATA +- dwmac1000_pbuf_dump (q); +-#endif /* DEBUG_DATA */ +- } ++ ASSERT (q->len == q->tot_len); ++ pRx->buff = q->payload; ++ pRx->pbuf = q; + + MIB2_STATS_NETIF_ADD (netif, ifinoctets, p->tot_len); + if (IS_UNICAST (p)) +@@ -575,6 +625,7 @@ static struct pbuf *dwmac1000_hw_get_received_frame (struct netif * netif) + } + else + { ++ p = NULL; + LINK_STATS_INC (link.memerr); + LINK_STATS_INC (link.drop); + MIB2_STATS_NETIF_INC (netif, ifindiscards); +@@ -694,16 +745,63 @@ static dev_state_t dwmac1000_probe (drv_t * drv) + + link_state = dwmac1000->phy->ops->get_link_state (dwmac1000->phy); + ++ /* Save link state for IOCTL_ETH_GET_STATUS */ ++ dwmac1000->last_link_state = link_state; ++ ++ /* Set ifSpeed in SNMP MIB-II ifTable */ ++#if MIB2_STATS ++ dwmac1000->netif->link_speed = dwmac1000_snmp_link_speed (link_state); ++#endif /* MIB2_STATS */ ++ + return (link_state & PHY_LINK_OK) ? StateAttached : StateDetached; + } + ++static void dwmac1000_get_status ( ++ dwmac1000_t * dwmac1000, ++ struct netif * netif, ++ ioctl_eth_status_t * link) ++{ ++ if (dwmac1000->phy->loopback_mode) ++ { ++ link->is_autonegotiation_supported = false; ++ link->is_autonegotiation_enabled = false; ++ link->capabilities = 0x0000; ++ } ++ else ++ { ++ link->is_autonegotiation_supported = true; ++ link->is_autonegotiation_enabled = true; ++ link->capabilities = dwmac1000->link_capabilities; ++ } ++ ++ link->state = dwmac1000->last_link_state; ++ link->is_operational = netif_is_up (netif) && netif_is_link_up (netif); ++} ++ ++static int dwmac1000_ioctl (drv_t * drv, void * arg, int req, void * param) ++{ ++ dwmac1000_t * dwmac1000 = (dwmac1000_t *)drv; ++ int status = EARG; ++ ++ switch (req) ++ { ++ case IOCTL_ETH_GET_STATUS: ++ dwmac1000_get_status (dwmac1000, arg, param); ++ return 0; ++ default: ++ UASSERT (0, EARG); ++ } ++ ++ return status; ++} ++ + static const drv_ops_t dwmac1000_ops = + { + .open = NULL, + .read = NULL, + .write = NULL, + .close = NULL, +- .ioctl = NULL, ++ .ioctl = dwmac1000_ioctl, + .hotplug = dwmac1000_hotplug, + }; + +@@ -714,6 +812,7 @@ drv_t * dwmac1000_init (const char * name, const dwmac1000_cfg_t * cfg, + + UASSERT (cfg->hclk >= 20 * 1000 * 1000, EARG); + UASSERT (cfg->rx_buffers <= RX_MAX_BUFFERS, EARG); ++ UASSERT (cfg->rx_buffers < PBUF_POOL_SIZE, EARG); + UASSERT (cfg->tx_buffers <= TX_MAX_BUFFERS, EARG); + + dwmac1000 = malloc (sizeof (dwmac1000_t)); +@@ -735,9 +834,12 @@ drv_t * dwmac1000_init (const char * name, const dwmac1000_cfg_t * cfg, + /* Initialise driver state */ + dwmac1000->phy = phy; + dwmac1000->irq = cfg->irq; ++ dwmac1000->mtx_tx = mtx_create(); + dwmac1000->mac = (eth_mac_t *)cfg->base; + dwmac1000->mmc = (eth_mmc_t *)(cfg->base + 0x100); + dwmac1000->dma = (eth_dma_t *)(cfg->base + 0x1000); ++ dwmac1000->last_link_state = 0x00; ++ dwmac1000->netif = netif; + + /* Create receive task */ + dwmac1000->tRcv = task_spawn ("EthRcv", +diff --git a/drivers/eth/phy/mii.c b/drivers/eth/phy/mii.c +index 2cbaef613..7eb31315b 100644 +--- a/drivers/eth/phy/mii.c ++++ b/drivers/eth/phy/mii.c +@@ -305,11 +305,18 @@ uint8_t mii_get_link_state (phy_t * phy) + return link_state; + } + ++uint16_t mii_get_link_capabilities (struct phy * phy) ++{ ++ uint16_t anar = phy->read (phy->arg, phy->address, MII_ANAR); ++ return anar; ++} ++ + static phy_ops_t mii_ops = + { +- .reset = mii_reset, +- .start = mii_start, +- .get_link_state = mii_get_link_state ++ .reset = mii_reset, ++ .start = mii_start, ++ .get_link_state = mii_get_link_state, ++ .get_link_capabilities = mii_get_link_capabilities, + }; + + phy_t * mii_init (const phy_cfg_t * cfg) +diff --git a/include/drivers/eth/phy/mii.h b/include/drivers/eth/phy/mii.h +index 57364bdc6..dd69ceaee 100644 +--- a/include/drivers/eth/phy/mii.h ++++ b/include/drivers/eth/phy/mii.h +@@ -19,6 +19,7 @@ phy_t * mii_init (const phy_cfg_t * cfg); + void mii_reset (phy_t * phy); + void mii_start (phy_t * phy); + uint8_t mii_get_link_state (phy_t * phy); ++uint16_t mii_get_link_capabilities (phy_t * phy); + + /** \internal Exposed for testing purposes only + * +diff --git a/include/drivers/eth/phy/phy.h b/include/drivers/eth/phy/phy.h +index 22aea4ccf..a34a0a616 100644 +--- a/include/drivers/eth/phy/phy.h ++++ b/include/drivers/eth/phy/phy.h +@@ -19,6 +19,14 @@ + #define PHY_LINK_1000MBIT BIT (3) + #define PHY_LINK_FULL_DUPLEX BIT (4) + ++/* Capabilities advertised to link partner during auto-negotiation. ++ * Bit patterns are the same as the Auto-Negotiation Advertisement Register. ++ */ ++#define PHY_CAPABILITY_100_FD BIT (8) /* Can do 100BASE-TX full duplex */ ++#define PHY_CAPABILITY_100 BIT (7) /* Can do 100BASE-TX */ ++#define PHY_CAPABILITY_10_FD BIT (6) /* Can do 10BASE-T full duplex */ ++#define PHY_CAPABILITY_10 BIT (5) /* Can do 10BASE-T */ ++ + struct phy; + + typedef struct phy_ops +@@ -26,6 +34,7 @@ typedef struct phy_ops + void (*reset) (struct phy * phy); + void (*start) (struct phy * phy); + uint8_t (*get_link_state) (struct phy * phy); ++ uint16_t (*get_link_capabilities) (struct phy * phy); + } phy_ops_t; + + typedef struct phy +diff --git a/include/drivers/ioctl.h b/include/drivers/ioctl.h +index 4471b4ae6..d7be0d261 100644 +--- a/include/drivers/ioctl.h ++++ b/include/drivers/ioctl.h +@@ -142,6 +142,57 @@ typedef struct ioctl_i2c_access + + #define IOCTL_CAN_LOOPBACK 0x701 /**< Set CAN module in loopback state */ + ++/** ++ * Status of Ethernet link ++ */ ++typedef struct ioctl_eth_status ++{ ++ /** Capabilities advertised to link partner during auto-negotiation ++ * ++ * This forms a bit pattern. Valid bit values (see driver/eth/phy/phy.h): ++ * - PHY_CAPABILITY_100_FD, ++ * - PHY_CAPABILITY_100, ++ * - PHY_CAPABILITY_10_FD, ++ * - PHY_CAPABILITY_10. ++ */ ++ uint16_t capabilities; ++ ++ /** Is auto-negotiation process supported by physical layer? */ ++ bool is_autonegotiation_supported; ++ ++ /** Is auto-negotiation process enabled for physical layer? */ ++ bool is_autonegotiation_enabled; ++ ++ /** ++ * Is link operational? ++ * ++ * True if link is up and network interface is administratively up. ++ * False if any of those are down. ++ */ ++ bool is_operational; ++ ++ /** ++ * Link state ++ * ++ * Bit pattern of the following: ++ * - PHY_LINK_OK is set if link is up, ++ * - PHY_LINK_10MBIT, PHY_LINK_100MBIT or PHY_LINK_1000MBIT is the speed, ++ * - PHY_LINK_FULL_DUPLEX is set if link is full-duplex and cleared if ++ * it is half-duplex. ++ */ ++ uint8_t state; ++} ioctl_eth_status_t; ++ ++/** ++ * Get status of network interface ++ * ++ * \code ++ * ioctl_eth_status_t status; ++ * int error = ioctl (fd, IOCTL_ETH_GET_STATUS, &status); ++ * \endcode ++ */ ++#define IOCTL_ETH_GET_STATUS 0x801 ++ + /* ioctl is not defined by newlib */ + int ioctl (int fd, int request, ...); + +diff --git a/kern/arch/xmc4/hal.h b/kern/arch/xmc4/hal.h +index 20694b6bb..58fd606d9 100644 +--- a/kern/arch/xmc4/hal.h ++++ b/kern/arch/xmc4/hal.h +@@ -18,7 +18,7 @@ + + /* Set the maximum allocation size in bytes per malloc. + * Maximum size is 2^HAL_MAXIMUM_ALLOCATION_SIZE_POWER bytes */ +-#define HAL_MAXIMUM_ALLOCATION_SIZE_POWER 17 /* 128 kB */ ++#define HAL_MAXIMUM_ALLOCATION_SIZE_POWER 18 /* 256 kB */ + + /* This macro enables interrupts globally. */ + #define HAL_INTERRUPT_UNLOCK() \ +diff --git a/lwip/CMakeLists.txt b/lwip/CMakeLists.txt +index 1e5a3d2bd..122fde5fb 100644 +--- a/lwip/CMakeLists.txt ++++ b/lwip/CMakeLists.txt +@@ -16,6 +16,7 @@ file(GLOB SOURCES CONFIGURE_DEPENDS + src/core/*.c + src/core/ipv4/*.c + src/netif/*.c ++ src/apps/snmp/*.c + ) + add_library(lwip ${SOURCES}) + +diff --git a/lwip/src/apps/Makefile b/lwip/src/apps/Makefile +new file mode 100644 +index 000000000..e92bb4eed +--- /dev/null ++++ b/lwip/src/apps/Makefile +@@ -0,0 +1,12 @@ ++#******************************************************************** ++# _ _ _ ++# _ __ | |_ _ | | __ _ | |__ ___ ++# | '__|| __|(_)| | / _` || '_ \ / __| ++# | | | |_ _ | || (_| || |_) |\__ \ ++# |_| \__|(_)|_| \__,_||_.__/ |___/ ++# ++# http://www.rt-labs.com ++# Copyright 2013 rt-labs AB, Sweden. ++# See LICENSE file in the project root for full license information. ++#*******************************************************************/ ++include $(PRJ_ROOT)/make/subdir.mk +diff --git a/lwip/src/apps/snmp/Makefile b/lwip/src/apps/snmp/Makefile +new file mode 100644 +index 000000000..e1383bfe1 +--- /dev/null ++++ b/lwip/src/apps/snmp/Makefile +@@ -0,0 +1,16 @@ ++#******************************************************************** ++# _ _ _ ++# _ __ | |_ _ | | __ _ | |__ ___ ++# | '__|| __|(_)| | / _` || '_ \ / __| ++# | | | |_ _ | || (_| || |_) |\__ \ ++# |_| \__|(_)|_| \__,_||_.__/ |___/ ++# ++# http://www.rt-labs.com ++# Copyright 2013 rt-labs AB, Sweden. ++# See LICENSE file in the project root for full license information. ++#*******************************************************************/ ++OBJDIR = ../../../obj/$(ARCH)/$(VARIANT)/$(CPU) ++ ++override EXTRA_CFLAGS += -Wno-enum-compare ++ ++include $(PRJ_ROOT)/make/subdir.mk +diff --git a/lwip/src/apps/snmp/snmp_mib2_interfaces.c b/lwip/src/apps/snmp/snmp_mib2_interfaces.c +index 5f12dd552..85e80f9b0 100644 +--- a/lwip/src/apps/snmp/snmp_mib2_interfaces.c ++++ b/lwip/src/apps/snmp/snmp_mib2_interfaces.c +@@ -159,6 +159,7 @@ interfaces_Table_get_value(struct snmp_node_instance *instance, void *value) + struct netif *netif = (struct netif *)instance->reference.ptr; + u32_t *value_u32 = (u32_t *)value; + s32_t *value_s32 = (s32_t *)value; ++ char *value_string = (char *)value; + u16_t value_len; + + switch (SNMP_TABLE_GET_COLUMN_FROM_OID(instance->instance_oid.id)) { +@@ -167,8 +168,9 @@ interfaces_Table_get_value(struct snmp_node_instance *instance, void *value) + value_len = sizeof(*value_s32); + break; + case 2: /* ifDescr */ +- value_len = sizeof(netif->name); +- MEMCPY(value, netif->name, value_len); ++ value_len = sizeof(netif->name) + 1; ++ MEMCPY(value_string, netif->name, sizeof(netif->name)); ++ value_string[sizeof(netif->name)] = '0' + netif->num; + break; + case 3: /* ifType */ + *value_s32 = netif->link_type; +diff --git a/lwip/src/apps/snmp/snmp_mib2_system.c b/lwip/src/apps/snmp/snmp_mib2_system.c +index 71c1c29c0..9331b6080 100644 +--- a/lwip/src/apps/snmp/snmp_mib2_system.c ++++ b/lwip/src/apps/snmp/snmp_mib2_system.c +@@ -56,309 +56,57 @@ + + /* --- system .1.3.6.1.2.1.1 ----------------------------------------------------- */ + +-/** mib-2.system.sysDescr */ +-static const u8_t sysdescr_default[] = SNMP_LWIP_MIB2_SYSDESC; +-static const u8_t *sysdescr = sysdescr_default; +-static const u16_t *sysdescr_len = NULL; /* use strlen for determining len */ +- +-/** mib-2.system.sysContact */ +-static const u8_t syscontact_default[] = SNMP_LWIP_MIB2_SYSCONTACT; +-static const u8_t *syscontact = syscontact_default; +-static const u16_t *syscontact_len = NULL; /* use strlen for determining len */ +-static u8_t *syscontact_wr = NULL; /* if writable, points to the same buffer as syscontact (required for correct constness) */ +-static u16_t *syscontact_wr_len = NULL; /* if writable, points to the same buffer as syscontact_len (required for correct constness) */ +-static u16_t syscontact_bufsize = 0; /* 0=not writable */ +- +-/** mib-2.system.sysName */ +-static const u8_t sysname_default[] = SNMP_LWIP_MIB2_SYSNAME; +-static const u8_t *sysname = sysname_default; +-static const u16_t *sysname_len = NULL; /* use strlen for determining len */ +-static u8_t *sysname_wr = NULL; /* if writable, points to the same buffer as sysname (required for correct constness) */ +-static u16_t *sysname_wr_len = NULL; /* if writable, points to the same buffer as sysname_len (required for correct constness) */ +-static u16_t sysname_bufsize = 0; /* 0=not writable */ +- +-/** mib-2.system.sysLocation */ +-static const u8_t syslocation_default[] = SNMP_LWIP_MIB2_SYSLOCATION; +-static const u8_t *syslocation = syslocation_default; +-static const u16_t *syslocation_len = NULL; /* use strlen for determining len */ +-static u8_t *syslocation_wr = NULL; /* if writable, points to the same buffer as syslocation (required for correct constness) */ +-static u16_t *syslocation_wr_len = NULL; /* if writable, points to the same buffer as syslocation_len (required for correct constness) */ +-static u16_t syslocation_bufsize = 0; /* 0=not writable */ +- +-/** +- * @ingroup snmp_mib2 +- * Initializes sysDescr pointers. +- * +- * @param str if non-NULL then copy str pointer +- * @param len points to string length, excluding zero terminator +- */ +-void +-snmp_mib2_set_sysdescr(const u8_t *str, const u16_t *len) +-{ +- if (str != NULL) { +- sysdescr = str; +- sysdescr_len = len; +- } +-} +- +-/** +- * @ingroup snmp_mib2 +- * Initializes sysContact pointers +- * +- * @param ocstr if non-NULL then copy str pointer +- * @param ocstrlen points to string length, excluding zero terminator. +- * if set to NULL it is assumed that ocstr is NULL-terminated. +- * @param bufsize size of the buffer in bytes. +- * (this is required because the buffer can be overwritten by snmp-set) +- * if ocstrlen is NULL buffer needs space for terminating 0 byte. +- * otherwise complete buffer is used for string. +- * if bufsize is set to 0, the value is regarded as read-only. +- */ +-void +-snmp_mib2_set_syscontact(u8_t *ocstr, u16_t *ocstrlen, u16_t bufsize) +-{ +- if (ocstr != NULL) { +- syscontact = ocstr; +- syscontact_wr = ocstr; +- syscontact_len = ocstrlen; +- syscontact_wr_len = ocstrlen; +- syscontact_bufsize = bufsize; +- } +-} +- +-/** +- * @ingroup snmp_mib2 +- * see \ref snmp_mib2_set_syscontact but set pointer to readonly memory +- */ +-void +-snmp_mib2_set_syscontact_readonly(const u8_t *ocstr, const u16_t *ocstrlen) ++static snmp_mib2_get_callback_fct snmp_mib2_system_get; ++static snmp_mib2_test_set_callback_fct snmp_mib2_system_test_set; ++static snmp_mib2_set_callback_fct snmp_mib2_system_set; ++ ++void snmp_mib2_system_set_callbacks( ++ snmp_mib2_get_callback_fct get, ++ snmp_mib2_test_set_callback_fct test_set, ++ snmp_mib2_set_callback_fct set) + { +- if (ocstr != NULL) { +- syscontact = ocstr; +- syscontact_len = ocstrlen; +- syscontact_wr = NULL; +- syscontact_wr_len = NULL; +- syscontact_bufsize = 0; +- } ++ snmp_mib2_system_get = get; ++ snmp_mib2_system_test_set = test_set; ++ snmp_mib2_system_set = set; + } + +- +-/** +- * @ingroup snmp_mib2 +- * Initializes sysName pointers +- * +- * @param ocstr if non-NULL then copy str pointer +- * @param ocstrlen points to string length, excluding zero terminator. +- * if set to NULL it is assumed that ocstr is NULL-terminated. +- * @param bufsize size of the buffer in bytes. +- * (this is required because the buffer can be overwritten by snmp-set) +- * if ocstrlen is NULL buffer needs space for terminating 0 byte. +- * otherwise complete buffer is used for string. +- * if bufsize is set to 0, the value is regarded as read-only. +- */ +-void +-snmp_mib2_set_sysname(u8_t *ocstr, u16_t *ocstrlen, u16_t bufsize) +-{ +- if (ocstr != NULL) { +- sysname = ocstr; +- sysname_wr = ocstr; +- sysname_len = ocstrlen; +- sysname_wr_len = ocstrlen; +- sysname_bufsize = bufsize; +- } +-} +- +-/** +- * @ingroup snmp_mib2 +- * see \ref snmp_mib2_set_sysname but set pointer to readonly memory +- */ +-void +-snmp_mib2_set_sysname_readonly(const u8_t *ocstr, const u16_t *ocstrlen) +-{ +- if (ocstr != NULL) { +- sysname = ocstr; +- sysname_len = ocstrlen; +- sysname_wr = NULL; +- sysname_wr_len = NULL; +- sysname_bufsize = 0; +- } +-} +- +-/** +- * @ingroup snmp_mib2 +- * Initializes sysLocation pointers +- * +- * @param ocstr if non-NULL then copy str pointer +- * @param ocstrlen points to string length, excluding zero terminator. +- * if set to NULL it is assumed that ocstr is NULL-terminated. +- * @param bufsize size of the buffer in bytes. +- * (this is required because the buffer can be overwritten by snmp-set) +- * if ocstrlen is NULL buffer needs space for terminating 0 byte. +- * otherwise complete buffer is used for string. +- * if bufsize is set to 0, the value is regarded as read-only. +- */ +-void +-snmp_mib2_set_syslocation(u8_t *ocstr, u16_t *ocstrlen, u16_t bufsize) +-{ +- if (ocstr != NULL) { +- syslocation = ocstr; +- syslocation_wr = ocstr; +- syslocation_len = ocstrlen; +- syslocation_wr_len = ocstrlen; +- syslocation_bufsize = bufsize; +- } +-} +- +-/** +- * @ingroup snmp_mib2 +- * see \ref snmp_mib2_set_syslocation but set pointer to readonly memory +- */ +-void +-snmp_mib2_set_syslocation_readonly(const u8_t *ocstr, const u16_t *ocstrlen) +-{ +- if (ocstr != NULL) { +- syslocation = ocstr; +- syslocation_len = ocstrlen; +- syslocation_wr = NULL; +- syslocation_wr_len = NULL; +- syslocation_bufsize = 0; +- } +-} +- +- + static s16_t + system_get_value(const struct snmp_scalar_array_node_def *node, void *value) + { +- const u8_t *var = NULL; +- const s16_t *var_len; +- u16_t result; +- +- switch (node->oid) { +- case 1: /* sysDescr */ +- var = sysdescr; +- var_len = (const s16_t *)sysdescr_len; +- break; +- case 2: { /* sysObjectID */ +- const struct snmp_obj_id *dev_enterprise_oid = snmp_get_device_enterprise_oid(); +- MEMCPY(value, dev_enterprise_oid->id, dev_enterprise_oid->len * sizeof(u32_t)); +- return dev_enterprise_oid->len * sizeof(u32_t); +- } +- case 3: /* sysUpTime */ +- MIB2_COPY_SYSUPTIME_TO((u32_t *)value); +- return sizeof(u32_t); +- case 4: /* sysContact */ +- var = syscontact; +- var_len = (const s16_t *)syscontact_len; +- break; +- case 5: /* sysName */ +- var = sysname; +- var_len = (const s16_t *)sysname_len; +- break; +- case 6: /* sysLocation */ +- var = syslocation; +- var_len = (const s16_t *)syslocation_len; +- break; +- case 7: /* sysServices */ +- *(s32_t *)value = SNMP_SYSSERVICES; +- return sizeof(s32_t); +- default: +- LWIP_DEBUGF(SNMP_MIB_DEBUG, ("system_get_value(): unknown id: %"S32_F"\n", node->oid)); +- return 0; ++ if (snmp_mib2_system_get == NULL) ++ { ++ return SNMP_ERR_GENERROR; + } +- +- /* handle string values (OID 1,4,5 and 6) */ +- LWIP_ASSERT("", (value != NULL)); +- if (var_len == NULL) { +- result = (s16_t)strlen((const char *)var); +- } else { +- result = *var_len; ++ else ++ { ++ return snmp_mib2_system_get(node->oid, value, SNMP_MAX_VALUE_SIZE); + } +- MEMCPY(value, var, result); +- return result; + } + + static snmp_err_t + system_set_test(const struct snmp_scalar_array_node_def *node, u16_t len, void *value) + { +- snmp_err_t ret = SNMP_ERR_WRONGVALUE; +- const u16_t *var_bufsize = NULL; +- const u16_t *var_wr_len; +- +- LWIP_UNUSED_ARG(value); +- +- switch (node->oid) { +- case 4: /* sysContact */ +- var_bufsize = &syscontact_bufsize; +- var_wr_len = syscontact_wr_len; +- break; +- case 5: /* sysName */ +- var_bufsize = &sysname_bufsize; +- var_wr_len = sysname_wr_len; +- break; +- case 6: /* sysLocation */ +- var_bufsize = &syslocation_bufsize; +- var_wr_len = syslocation_wr_len; +- break; +- default: +- LWIP_DEBUGF(SNMP_MIB_DEBUG, ("system_set_test(): unknown id: %"S32_F"\n", node->oid)); +- return ret; ++ if (snmp_mib2_system_test_set == NULL) ++ { ++ return SNMP_ERR_GENERROR; + } +- +- /* check if value is writable at all */ +- if (*var_bufsize > 0) { +- if (var_wr_len == NULL) { +- /* we have to take the terminating 0 into account */ +- if (len < *var_bufsize) { +- ret = SNMP_ERR_NOERROR; +- } +- } else { +- if (len <= *var_bufsize) { +- ret = SNMP_ERR_NOERROR; +- } +- } +- } else { +- ret = SNMP_ERR_NOTWRITABLE; ++ else ++ { ++ return snmp_mib2_system_test_set(node->oid, value, len); + } +- +- return ret; + } + + static snmp_err_t + system_set_value(const struct snmp_scalar_array_node_def *node, u16_t len, void *value) + { +- u8_t *var_wr = NULL; +- u16_t *var_wr_len; +- +- switch (node->oid) { +- case 4: /* sysContact */ +- var_wr = syscontact_wr; +- var_wr_len = syscontact_wr_len; +- break; +- case 5: /* sysName */ +- var_wr = sysname_wr; +- var_wr_len = sysname_wr_len; +- break; +- case 6: /* sysLocation */ +- var_wr = syslocation_wr; +- var_wr_len = syslocation_wr_len; +- break; +- default: +- LWIP_DEBUGF(SNMP_MIB_DEBUG, ("system_set_value(): unknown id: %"S32_F"\n", node->oid)); +- return SNMP_ERR_GENERROR; ++ if (snmp_mib2_system_set == NULL) ++ { ++ return SNMP_ERR_GENERROR; + } +- +- /* no need to check size of target buffer, this was already done in set_test method */ +- LWIP_ASSERT("", var_wr != NULL); +- MEMCPY(var_wr, value, len); +- +- if (var_wr_len == NULL) { +- /* add terminating 0 */ +- var_wr[len] = 0; +- } else { +- *var_wr_len = len; ++ else ++ { ++ return snmp_mib2_system_set(node->oid, value, len); + } +- +- return SNMP_ERR_NOERROR; + } + + static const struct snmp_scalar_array_node_def system_nodes[] = { +diff --git a/lwip/src/core/lwip_hooks.c b/lwip/src/core/lwip_hooks.c +new file mode 100644 +index 000000000..ad6229f82 +--- /dev/null ++++ b/lwip/src/core/lwip_hooks.c +@@ -0,0 +1,23 @@ ++#include "lwip/lwip_hooks.h" ++ ++#ifdef LWIP_HOOK_UNKNOWN_ETH_PROTOCOL ++static netif_input_fn lwip_hook_for_unknown_eth_protocol; ++ ++err_enum_t lwip_hook_unknown_eth_protocol(struct pbuf *pbuf, struct netif *netif) ++{ ++ if(lwip_hook_for_unknown_eth_protocol == NULL) ++ { ++ /* Not handled. User needs to free pbuf */ ++ return ERR_IF; ++ } ++ else ++ { ++ return lwip_hook_for_unknown_eth_protocol(pbuf, netif); ++ } ++} ++ ++void lwip_set_hook_for_unknown_eth_protocol(struct netif *netif, netif_input_fn hook) ++{ ++ lwip_hook_for_unknown_eth_protocol = hook; ++} ++#endif /* LWIP_HOOK_UNKNOWN_ETH_PROTOCOL */ +diff --git a/lwip/src/core/udp.c b/lwip/src/core/udp.c +index 0b609d333..05166989c 100644 +--- a/lwip/src/core/udp.c ++++ b/lwip/src/core/udp.c +@@ -372,6 +372,18 @@ udp_input(struct pbuf *p, struct netif *inp) + goto end; + } + ++ /* rtlaka: discard invalid UDP packets to pass PNIO tester */ ++ if ((ntohs(udphdr->len) - UDP_HLEN) != p->len) { ++ /* drop invalid packets */ ++ LWIP_DEBUGF(UDP_DEBUG, ++ ("udp_input: invalid UDP length (%"U16_F" bytes) discarded\n", p->tot_len)); ++ UDP_STATS_INC(udp.lenerr); ++ UDP_STATS_INC(udp.drop); ++ MIB2_STATS_INC(mib2.udpinerrors); ++ pbuf_free(p); ++ goto end; ++ } ++ + if (pcb != NULL) { + MIB2_STATS_INC(mib2.udpindatagrams); + #if SO_REUSE && SO_REUSE_RXTOALL +diff --git a/lwip/src/include/lwip/apps/snmp_mib2.h b/lwip/src/include/lwip/apps/snmp_mib2.h +index 2f4a68935..1df6bb0dc 100644 +--- a/lwip/src/include/lwip/apps/snmp_mib2.h ++++ b/lwip/src/include/lwip/apps/snmp_mib2.h +@@ -60,13 +60,23 @@ extern struct snmp_threadsync_instance snmp_mib2_lwip_locks; + #define SNMP_SYSSERVICES ((1 << 6) | (1 << 3) | ((IP_FORWARD) << 2)) + #endif + +-void snmp_mib2_set_sysdescr(const u8_t* str, const u16_t* len); /* read-only be defintion */ +-void snmp_mib2_set_syscontact(u8_t *ocstr, u16_t *ocstrlen, u16_t bufsize); +-void snmp_mib2_set_syscontact_readonly(const u8_t *ocstr, const u16_t *ocstrlen); +-void snmp_mib2_set_sysname(u8_t *ocstr, u16_t *ocstrlen, u16_t bufsize); +-void snmp_mib2_set_sysname_readonly(const u8_t *ocstr, const u16_t *ocstrlen); +-void snmp_mib2_set_syslocation(u8_t *ocstr, u16_t *ocstrlen, u16_t bufsize); +-void snmp_mib2_set_syslocation_readonly(const u8_t *ocstr, const u16_t *ocstrlen); ++typedef s16_t(*snmp_mib2_get_callback_fct)( ++ u32_t column, ++ void *value, ++ size_t size); ++typedef snmp_err_t(*snmp_mib2_test_set_callback_fct)( ++ u32_t column, ++ const void *value, ++ size_t size); ++typedef snmp_err_t(*snmp_mib2_set_callback_fct)( ++ u32_t column, ++ const void *value, ++ size_t size); ++ ++void snmp_mib2_system_set_callbacks( ++ snmp_mib2_get_callback_fct get, ++ snmp_mib2_test_set_callback_fct test_set, ++ snmp_mib2_set_callback_fct set); + + #endif /* SNMP_LWIP_MIB2 */ + #endif /* LWIP_SNMP */ +diff --git a/lwip/src/include/lwip/lwip_hooks.h b/lwip/src/include/lwip/lwip_hooks.h +new file mode 100644 +index 000000000..c48f1c57b +--- /dev/null ++++ b/lwip/src/include/lwip/lwip_hooks.h +@@ -0,0 +1,41 @@ ++/** ++ * Hook functions ++ * ++ * Declares hook functions called by lwip. ++ * Also declared API for configuring hook functions. ++ * ++ * The name of this file is specified as LWIP_HOOK_FILENAME in lwipopts.h. ++ */ ++ ++#ifndef LWIP_HOOKS_H ++#define LWIP_HOOKS_H ++ ++#include "lwip/netif.h" ++ ++/** ++ * LWIP_HOOK_UNKNOWN_ETH_PROTOCOL ++ * ++ * Called from ethernet_input() when an unknown eth type is encountered. ++ * ++ * By default, this will do nothing and return ERR_IF. ++ * If a hook function has been set in lwip_set_hook_for_unknown_eth_protocol(), ++ * then that function will be called. ++ * ++ * \param pbuf Payload points to ethernet header! ++ * \param netif Network interface. ++ * \return ERR_OK if packet is accepted and freed, ++ * ERR_IF otherwise. ++ */ ++err_enum_t lwip_hook_unknown_eth_protocol(struct pbuf *pbuf, struct netif *netif); ++ ++/** ++ * Configure function to be called by lwip_hook_unknown_eth_protocol() ++ * ++ *\param netif Network interface. ++ *\param hook Hook function to be called when frame with unknown eth type ++ * is encountered. Should return ERR_OK for accepted and freed ++ * frames, ERR_IF otherwise. ++ */ ++void lwip_set_hook_for_unknown_eth_protocol(struct netif *netif, netif_input_fn hook); ++ ++#endif /* LWIP_HOOKS_H */ +diff --git a/lwip/src/include/lwip/lwipopts.h b/lwip/src/include/lwip/lwipopts.h +index 34d44ecc0..480045bc3 100644 +--- a/lwip/src/include/lwip/lwipopts.h ++++ b/lwip/src/include/lwip/lwipopts.h +@@ -44,12 +44,33 @@ + #define LWIP_NETIF_LINK_CALLBACK 1 + #define LWIP_NETIF_STATUS_CALLBACK 1 + #define LWIP_NETIF_LOOPBACK 1 ++#define LWIP_TCPIP_CORE_LOCKING_INPUT 1 + #define LWIP_SOCKET 1 + #define LWIP_IGMP 1 + #define LWIP_TCP_KEEPALIVE 1 + #define LWIP_SO_RCVTIMEO 1 + #define LWIP_SO_SNDTIMEO 1 ++#define LWIP_SNMP 1 ++#define SNMP_USE_NETCONN 1 ++#define SNMP_USE_RAW 0 ++#define MIB2_STATS 1 + #define SO_REUSE 1 ++#define ETHARP_SUPPORT_VLAN 1 ++ ++/** ++ * LWIP_HOOK_FILENAME: Custom filename to #include in files that provide hooks. ++ * Declare your hook function prototypes in there, you may also #include all headers ++ * providing data types that are need in this file. ++ */ ++#define LWIP_HOOK_FILENAME "lwip/lwip_hooks.h" ++ ++/** ++ * LWIP_HOOK_UNKNOWN_ETH_PROTOCOL(pbuf, netif): ++ * Called from ethernet_input() when an unknown eth type is encountered. ++ * Return ERR_OK if packet is accepted, any error code otherwise. ++ * Payload points to ethernet header! ++ */ ++#define LWIP_HOOK_UNKNOWN_ETH_PROTOCOL lwip_hook_unknown_eth_protocol + + /** + * LWIP_DHCP_AUTOIP_COOP_TRIES: Set to the number of DHCP DISCOVER probes +@@ -60,20 +81,33 @@ + */ + #define LWIP_DHCP_AUTOIP_COOP_TRIES 2 + ++/* Supported services. Should be 78 for Profinet. See IETF RFC 3418. */ ++#define SNMP_SYSSERVICES ((1 << 6) | (1 << 3) | (1 << 2) | (1 << 1)) ++ + /* Various memory size options */ + #define MEM_SIZE (6 * 1024) + #define MEMP_NUM_UDP_PCB 6 +-#define PBUF_POOL_SIZE 8 ++#define PBUF_POOL_SIZE (8 + 3) + #define TCP_MSS 1460 /* Should be 1460 for Ethernet IPv4 */ + #define TCP_SND_BUF (2 * TCP_MSS) ++#define SNMP_MAX_OBJ_ID_LEN 32 ++ ++/* Max size + VLAN tag + extra PNET buffer */ ++#define PROFINET_INFO_SIZE 0 ++#define PBUF_FCS_SIZE 4 ++#define PBUF_POOL_BUFSIZE LWIP_MEM_ALIGN_SIZE(1518 + PBUF_FCS_SIZE + PROFINET_INFO_SIZE) + + /* Stack sizes */ + #define DEFAULT_THREAD_STACKSIZE 1024 + #define TCPIP_THREAD_STACKSIZE 1768 ++#define SNMP_STACK_SIZE 4000 + + /* TCPIP thread priority */ + #define TCPIP_THREAD_PRIO 5 + ++/* SNMP server thread priority */ ++#define SNMP_THREAD_PRIO 3 ++ + /* Mailbox sizes */ + #define TCPIP_MBOX_SIZE 128 + #define DEFAULT_RAW_RECVMBOX_SIZE 5 +@@ -106,6 +140,7 @@ + * LWIP_DBG_OFF + * LWIP_DBG_ON + */ ++#define PBUF_DEBUG LWIP_DBG_OFF + #define IP_DEBUG LWIP_DBG_OFF + #define IGMP_DEBUG LWIP_DBG_OFF + #define TCPIP_DEBUG LWIP_DBG_OFF +@@ -120,5 +155,7 @@ + #define TCP_FR_DEBUG LWIP_DBG_OFF + #define TCP_QLEN_DEBUG LWIP_DBG_OFF + #define TCP_RST_DEBUG LWIP_DBG_OFF ++#define SNMP_DEBUG LWIP_DBG_OFF ++#define SNMP_MIB_DEBUG LWIP_DBG_OFF + + #endif /* __LWIPOPTS_H__ */ +-- +2.25.1 + diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.c b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.c new file mode 100644 index 0000000..4836d10 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.c @@ -0,0 +1,532 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "lldp-ext-dot3-mib.h" + +#include "options.h" +#include "pnet_api.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal.h" +#include "pf_types.h" +#include "pf_snmp.h" +#include "pnal_snmp.h" +#include "rowindex.h" + +#include + +/* +Generated by LwipMibCompiler +*/ + +#include "lwip/apps/snmp_opts.h" +#if LWIP_SNMP + +#include "lldp-ext-dot3-mib.h" +#include "lwip/apps/snmp.h" +#include "lwip/apps/snmp_core.h" +#include "lwip/apps/snmp_scalar.h" +#include "lwip/apps/snmp_table.h" + +/* --- lldpXdot3LocalData 1.0.8802.1.1.2.1.5.4623.1.2 + * ----------------------------------------------------- */ +static snmp_err_t lldpxdot3locporttable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldpxdot3locporttable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldpxdot3locporttable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldpxdot3locporttable_columns[] = { + {1, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3LocPortAutoNegSupported + */ + {2, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3LocPortAutoNegEnabled + */ + {3, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3LocPortAutoNegAdvertisedCap + */ + {4, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3LocPortOperMauType + */ +}; +static const struct snmp_table_node lldpxdot3locporttable = SNMP_TABLE_CREATE ( + 1, + lldpxdot3locporttable_columns, + lldpxdot3locporttable_get_instance, + lldpxdot3locporttable_get_next_instance, + lldpxdot3locporttable_get_value, + NULL, + NULL); + +static const struct snmp_node * const lldpxdot3localdata_subnodes[] = { + &lldpxdot3locporttable.node.node, +}; +static const struct snmp_tree_node lldpxdot3localdata_treenode = + SNMP_CREATE_TREE_NODE (2, lldpxdot3localdata_subnodes); + +/* --- lldpXdot3RemoteData 1.0.8802.1.1.2.1.5.4623.1.3 + * ----------------------------------------------------- */ +static snmp_err_t lldpxdot3remporttable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldpxdot3remporttable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldpxdot3remporttable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldpxdot3remporttable_columns[] = { + {1, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3RemPortAutoNegSupported + */ + {2, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3RemPortAutoNegEnabled + */ + {3, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3RemPortAutoNegAdvertisedCap + */ + {4, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXdot3RemPortOperMauType + */ +}; +static const struct snmp_table_node lldpxdot3remporttable = SNMP_TABLE_CREATE ( + 1, + lldpxdot3remporttable_columns, + lldpxdot3remporttable_get_instance, + lldpxdot3remporttable_get_next_instance, + lldpxdot3remporttable_get_value, + NULL, + NULL); + +static const struct snmp_node * const lldpxdot3remotedata_subnodes[] = { + &lldpxdot3remporttable.node.node, +}; +static const struct snmp_tree_node lldpxdot3remotedata_treenode = + SNMP_CREATE_TREE_NODE (3, lldpxdot3remotedata_subnodes); + +/* --- lldpXdot3Objects 1.0.8802.1.1.2.1.5.4623.1 + * ----------------------------------------------------- */ +static const struct snmp_node * const lldpxdot3objects_subnodes[] = { + &lldpxdot3localdata_treenode.node, + &lldpxdot3remotedata_treenode.node}; +static const struct snmp_tree_node lldpxdot3objects_treenode = + SNMP_CREATE_TREE_NODE (1, lldpxdot3objects_subnodes); + +/* --- lldpXdot3MIB ----------------------------------------------------- */ +static const struct snmp_node * const lldpxdot3mib_subnodes[] = { + &lldpxdot3objects_treenode.node}; +static const struct snmp_tree_node lldpxdot3mib_root = + SNMP_CREATE_TREE_NODE (4623, lldpxdot3mib_subnodes); +static const u32_t lldpxdot3mib_base_oid[] = {1, 0, 8802, 1, 1, 2, 1, 5, 4623}; +const struct snmp_mib lldpxdot3mib = { + lldpxdot3mib_base_oid, + LWIP_ARRAYSIZE (lldpxdot3mib_base_oid), + &lldpxdot3mib_root.node}; + +/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +LWIP MIB generator - preserved section begin +Code below is preserved on regeneration. Remove these comment lines to +regenerate code. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +*/ + +/* --- lldpXdot3LocalData 1.0.8802.1.1.2.1.5.4623.1.2 + * ----------------------------------------------------- */ + +/** + * Get cell in table lldpXdot3LocPortTable. + * + * Called when an SNMP Get request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxdot3locporttable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int port = rowindex_match_with_local_port (row_oid, row_oid_len); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + cell_instance->reference.s32 = port; + return SNMP_ERR_NOERROR; + } +} + +/** + * Get next cell in table lldpXdot3LocPortTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxdot3locporttable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int port = rowindex_update_with_next_local_port (row_oid); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + cell_instance->reference.s32 = port; + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value at cell in table lldpXdot3LocPortTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if error occurred (server will report GenError). + */ +static s16_t lldpxdot3locporttable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len; + int port = cell_instance->reference.s32; + u32_t column = + SNMP_TABLE_GET_COLUMN_FROM_OID (cell_instance->instance_oid.id); + + switch (column) + { + case 1: + { + /* lldpXdot3LocPortAutoNegSupported */ + s32_t * v = (s32_t *)value; + pf_snmp_link_status_t link_status; + + pf_snmp_get_link_status (pnal_snmp.net, port, &link_status); + value_len = sizeof (s32_t); + *v = link_status.auto_neg_supported; + } + break; + case 2: + { + /* lldpXdot3LocPortAutoNegEnabled */ + s32_t * v = (s32_t *)value; + pf_snmp_link_status_t link_status; + + pf_snmp_get_link_status (pnal_snmp.net, port, &link_status); + value_len = sizeof (s32_t); + *v = link_status.auto_neg_enabled; + } + break; + case 3: + { + /* lldpXdot3LocPortAutoNegAdvertisedCap */ + u8_t * v = (u8_t *)value; + pf_snmp_link_status_t link_status; + + pf_snmp_get_link_status (pnal_snmp.net, port, &link_status); + value_len = 2; + memcpy (v, link_status.auto_neg_advertised_cap, 2); + } + break; + case 4: + { + /* lldpXdot3LocPortOperMauType */ + s32_t * v = (s32_t *)value; + pf_snmp_link_status_t link_status; + + pf_snmp_get_link_status (pnal_snmp.net, port, &link_status); + value_len = sizeof (s32_t); + *v = link_status.oper_mau_type; + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-EXT-DOT3-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/* --- lldpXdot3RemoteData 1.0.8802.1.1.2.1.5.4623.1.3 + * ----------------------------------------------------- */ + +static s16_t lldpxdot3remporttable_get_value_from_remote_device ( + pnal_snmp_response_t * value, + int port, + u32_t column); + +/** + * Get cell in table lldpXdot3RemPortTable. + * + * Called when an SNMP Get request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxdot3remporttable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_match_with_remote_device (row_oid, row_oid_len); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpxdot3remporttable_get_value_from_remote_device ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get next cell in table lldpXdot3RemPortTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxdot3remporttable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_update_with_next_remote_device (row_oid); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpxdot3remporttable_get_value_from_remote_device ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get value at cell in table lldpXdot3RemPortTable. + * + * @param value Out: Value to be returned in response. + * @param port In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * @param column In: Column index for the cell. + * @return Size of returned value, in bytes. + * -1 if no valid value could be returned. + */ +static s16_t lldpxdot3remporttable_get_value_from_remote_device ( + pnal_snmp_response_t * value, + int port, + u32_t column) +{ + s16_t value_len; + int error; + + switch (column) + { + case 1: + { + /* lldpXdot3RemPortAutoNegSupported */ + pf_snmp_link_status_t link_status; + + error = pf_snmp_get_peer_link_status (pnal_snmp.net, port, &link_status); + if (error) + { + return error; + } + + value_len = sizeof (s32_t); + value->s32 = link_status.auto_neg_supported; + } + break; + case 2: + { + /* lldpXdot3RemPortAutoNegEnabled */ + pf_snmp_link_status_t link_status; + + error = pf_snmp_get_peer_link_status (pnal_snmp.net, port, &link_status); + if (error) + { + return error; + } + + value_len = sizeof (s32_t); + value->s32 = link_status.auto_neg_enabled; + } + break; + case 3: + { + /* lldpXdot3RemPortAutoNegAdvertisedCap */ + pf_snmp_link_status_t link_status; + + error = pf_snmp_get_peer_link_status (pnal_snmp.net, port, &link_status); + if (error) + { + return error; + } + + value_len = 2; + memcpy (value->buffer, link_status.auto_neg_advertised_cap, 2); + } + break; + case 4: + { + /* lldpXdot3RemPortOperMauType */ + pf_snmp_link_status_t link_status; + + error = pf_snmp_get_peer_link_status (pnal_snmp.net, port, &link_status); + if (error) + { + return error; + } + + value_len = sizeof (s32_t); + value->s32 = link_status.oper_mau_type; + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-EXT-DOT3-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/** + * Get value at cell in table lldpXdot3RemPortTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). Its value was also read and saved in + * cell_instance->reference. + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if value was too large (server will report GenError). + */ +static s16_t lldpxdot3remporttable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len = cell_instance->reference_len; + pnal_snmp_response_t * saved_value = cell_instance->reference.ptr; + + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-EXT-DOT3-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + + memcpy (value, saved_value, value_len); + return value_len; +} + +/* --- lldpXdot3Objects 1.0.8802.1.1.2.1.5.4623.1 + * ----------------------------------------------------- */ +/* --- lldpXdot3MIB ----------------------------------------------------- */ +#endif /* LWIP_SNMP */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.h b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.h new file mode 100644 index 0000000..d699c73 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-dot3-mib.h @@ -0,0 +1,44 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief The LLDP-EXT-DOT3-MIB used by SNMP server + */ + +/* +Generated by LwipMibCompiler +*/ + +#ifndef LLDP_EXT_DOT3_MIB_H +#define LLDP_EXT_DOT3_MIB_H LLDP_EXT_DOT3_MIB_H + +#include "lwip/apps/snmp_opts.h" +#if LWIP_SNMP + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "lwip/apps/snmp_core.h" + +extern const struct snmp_mib lldpxdot3mib; + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* LWIP_SNMP */ +#endif /* LLDP_EXT_DOT3_MIB_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.c b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.c new file mode 100644 index 0000000..f4f32b4 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.c @@ -0,0 +1,545 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "lldp-ext-pno-mib.h" + +#include "options.h" +#include "pnet_api.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal.h" +#include "pf_types.h" +#include "pf_snmp.h" +#include "pnal_snmp.h" +#include "rowindex.h" + +#include + +/* +Generated by LwipMibCompiler +*/ + +#include "lwip/apps/snmp_opts.h" +#if LWIP_SNMP + +#include "lldp-ext-pno-mib.h" +#include "lwip/apps/snmp.h" +#include "lwip/apps/snmp_core.h" +#include "lwip/apps/snmp_scalar.h" +#include "lwip/apps/snmp_table.h" + +/* --- lldpXPnoLocalData 1.0.8802.1.1.2.1.5.3791.1.2 + * ----------------------------------------------------- */ +static snmp_err_t lldpxpnoloctable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldpxpnoloctable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldpxpnoloctable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldpxpnoloctable_columns[] = { + {1, + SNMP_ASN1_TYPE_GAUGE, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoLocLPDValue + */ + {2, + SNMP_ASN1_TYPE_GAUGE, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoLocPortTxDValue + */ + {3, + SNMP_ASN1_TYPE_GAUGE, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoLocPortRxDValue + */ + {6, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoLocPortNoS + */ +}; +static const struct snmp_table_node lldpxpnoloctable = SNMP_TABLE_CREATE ( + 1, + lldpxpnoloctable_columns, + lldpxpnoloctable_get_instance, + lldpxpnoloctable_get_next_instance, + lldpxpnoloctable_get_value, + NULL, + NULL); + +static const struct snmp_node * const lldpxpnolocaldata_subnodes[] = { + &lldpxpnoloctable.node.node, +}; +static const struct snmp_tree_node lldpxpnolocaldata_treenode = + SNMP_CREATE_TREE_NODE (2, lldpxpnolocaldata_subnodes); + +/* --- lldpXPnoRemoteData 1.0.8802.1.1.2.1.5.3791.1.3 + * ----------------------------------------------------- */ +static snmp_err_t lldpxpnoremtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldpxpnoremtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldpxpnoremtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldpxpnoremtable_columns[] = { + {1, + SNMP_ASN1_TYPE_GAUGE, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoRemLPDValue + */ + {2, + SNMP_ASN1_TYPE_GAUGE, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoRemPortTxDValue + */ + {3, + SNMP_ASN1_TYPE_GAUGE, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoRemPortRxDValue + */ + {6, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpXPnoRemPortNoS + */ +}; +static const struct snmp_table_node lldpxpnoremtable = SNMP_TABLE_CREATE ( + 1, + lldpxpnoremtable_columns, + lldpxpnoremtable_get_instance, + lldpxpnoremtable_get_next_instance, + lldpxpnoremtable_get_value, + NULL, + NULL); + +static const struct snmp_node * const lldpxpnoremotedata_subnodes[] = { + &lldpxpnoremtable.node.node, +}; +static const struct snmp_tree_node lldpxpnoremotedata_treenode = + SNMP_CREATE_TREE_NODE (3, lldpxpnoremotedata_subnodes); + +/* --- lldpXPnoObjects 1.0.8802.1.1.2.1.5.3791.1 + * ----------------------------------------------------- */ +static const struct snmp_node * const lldpxpnoobjects_subnodes[] = { + &lldpxpnolocaldata_treenode.node, + &lldpxpnoremotedata_treenode.node}; +static const struct snmp_tree_node lldpxpnoobjects_treenode = + SNMP_CREATE_TREE_NODE (1, lldpxpnoobjects_subnodes); + +/* --- lldpXPnoMIB ----------------------------------------------------- */ +static const struct snmp_node * const lldpxpnomib_subnodes[] = { + &lldpxpnoobjects_treenode.node}; +static const struct snmp_tree_node lldpxpnomib_root = + SNMP_CREATE_TREE_NODE (3791, lldpxpnomib_subnodes); +static const u32_t lldpxpnomib_base_oid[] = {1, 0, 8802, 1, 1, 2, 1, 5, 3791}; +const struct snmp_mib lldpxpnomib = { + lldpxpnomib_base_oid, + LWIP_ARRAYSIZE (lldpxpnomib_base_oid), + &lldpxpnomib_root.node}; + +/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +LWIP MIB generator - preserved section begin +Code below is preserved on regeneration. Remove these comment lines to +regenerate code. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +*/ + +/* --- lldpXPnoLocalData 1.0.8802.1.1.2.1.5.3791.1.2 + * ----------------------------------------------------- */ + +/** + * Get cell in table lldpXPnoLocTable. + * + * Called when an SNMP Get request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxpnoloctable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int port = rowindex_match_with_local_port (row_oid, row_oid_len); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + cell_instance->reference.s32 = port; + return SNMP_ERR_NOERROR; + } +} + +/** + * Get next cell in table lldpXPnoLocTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxpnoloctable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int port = rowindex_update_with_next_local_port (row_oid); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + cell_instance->reference.s32 = port; + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value at cell in table lldpXPnoLocTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if error occurred (server will report GenError). + */ +static s16_t lldpxpnoloctable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len; + int port = cell_instance->reference.s32; + u32_t column = + SNMP_TABLE_GET_COLUMN_FROM_OID (cell_instance->instance_oid.id); + + switch (column) + { + case 1: + { + /* lldpXPnoLocLPDValue */ + u32_t * v = (u32_t *)value; + pf_snmp_signal_delay_t delays; + + pf_snmp_get_signal_delays (pnal_snmp.net, port, &delays); + *v = delays.line_propagation_delay_ns; + value_len = sizeof (u32_t); + } + break; + case 2: + { + /* lldpXPnoLocPortTxDValue */ + u32_t * v = (u32_t *)value; + pf_snmp_signal_delay_t delays; + + pf_snmp_get_signal_delays (pnal_snmp.net, port, &delays); + *v = delays.port_tx_delay_ns; + value_len = sizeof (u32_t); + } + break; + case 3: + { + /* lldpXPnoLocPortRxDValue */ + u32_t * v = (u32_t *)value; + pf_snmp_signal_delay_t delays; + + pf_snmp_get_signal_delays (pnal_snmp.net, port, &delays); + *v = delays.port_rx_delay_ns; + value_len = sizeof (u32_t); + } + break; + case 6: + { + /* lldpXPnoLocPortNoS */ + u8_t * v = (u8_t *)value; + pf_lldp_station_name_t station_name; + + pf_snmp_get_station_name (pnal_snmp.net, &station_name); + value_len = station_name.len; + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-EXT-PNO-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + memcpy (v, station_name.string, value_len); + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-EXT-PNO-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/* --- lldpXPnoRemoteData 1.0.8802.1.1.2.1.5.3791.1.3 + * ----------------------------------------------------- */ + +static s16_t lldpxpnoremtable_get_value_from_remote_device ( + pnal_snmp_response_t * value, + int port, + u32_t column); + +/** + * Get cell in table lldpXPnoRemTable. + * + * Called when an SNMP Get request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxpnoremtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_match_with_remote_device (row_oid, row_oid_len); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpxpnoremtable_get_value_from_remote_device ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get next cell in table lldpXPnoRemTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpxpnoremtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_update_with_next_remote_device (row_oid); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpxpnoremtable_get_value_from_remote_device ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get value at cell in table lldpXPnoRemTable. + * + * @param value Out: Value to be returned in response. + * @param port In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * @param column In: Column index for the cell. + * @return Size of returned value, in bytes. + * -1 if no valid value could be returned. + */ +static s16_t lldpxpnoremtable_get_value_from_remote_device ( + pnal_snmp_response_t * value, + int port, + u32_t column) +{ + s16_t value_len; + int error; + + switch (column) + { + case 1: + { + /* lldpXPnoRemLPDValue */ + pf_snmp_signal_delay_t delays; + + error = pf_snmp_get_peer_signal_delays (pnal_snmp.net, port, &delays); + if (error) + { + return error; + } + + value_len = sizeof (u32_t); + value->u32 = delays.line_propagation_delay_ns; + } + break; + case 2: + { + /* lldpXPnoRemPortTxDValue */ + pf_snmp_signal_delay_t delays; + + error = pf_snmp_get_peer_signal_delays (pnal_snmp.net, port, &delays); + if (error) + { + return error; + } + + value_len = sizeof (u32_t); + value->u32 = delays.port_tx_delay_ns; + } + break; + case 3: + { + /* lldpXPnoRemPortRxDValue */ + pf_snmp_signal_delay_t delays; + + error = pf_snmp_get_peer_signal_delays (pnal_snmp.net, port, &delays); + if (error) + { + return error; + } + + value_len = sizeof (u32_t); + value->u32 = delays.port_rx_delay_ns; + } + break; + case 6: + { + /* lldpXPnoRemPortNoS */ + pf_lldp_station_name_t station_name; + + error = + pf_snmp_get_peer_station_name (pnal_snmp.net, port, &station_name); + if (error) + { + return error; + } + + value_len = station_name.len; + if ((size_t)value_len <= SNMP_MAX_VALUE_SIZE) + { + memcpy (value->buffer, station_name.string, value_len); + } + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-EXT-PNO-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/** + * Get value at cell in table lldpXPnoRemTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). Its value was also read and saved in + * cell_instance->reference. + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if value was too large (server will report GenError). + */ +static s16_t lldpxpnoremtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len = cell_instance->reference_len; + pnal_snmp_response_t * saved_value = cell_instance->reference.ptr; + + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-EXT-PNO-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + + memcpy (value, saved_value, value_len); + return value_len; +} + +/* --- lldpXPnoObjects 1.0.8802.1.1.2.1.5.3791.1 + * ----------------------------------------------------- */ +/* --- lldpXPnoMIB ----------------------------------------------------- */ +#endif /* LWIP_SNMP */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.h b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.h new file mode 100644 index 0000000..42e2e8d --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-ext-pno-mib.h @@ -0,0 +1,44 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief The LLDP-EXT-PNO-MIB used by SNMP server + */ + +/* +Generated by LwipMibCompiler +*/ + +#ifndef LLDP_EXT_PNO_MIB_H +#define LLDP_EXT_PNO_MIB_H LLDP_EXT_PNO_MIB_H + +#include "lwip/apps/snmp_opts.h" +#if LWIP_SNMP + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "lwip/apps/snmp_core.h" + +extern const struct snmp_mib lldpxpnomib; + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* LWIP_SNMP */ +#endif /* LLDP_EXT_PNO_MIB_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.c b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.c new file mode 100644 index 0000000..019f13a --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.c @@ -0,0 +1,1174 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "lldp-mib.h" + +#include "options.h" +#include "pnet_api.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal.h" +#include "pf_types.h" +#include "pf_snmp.h" +#include "pnal_snmp.h" +#include "rowindex.h" + +#include + +/* +Generated by LwipMibCompiler +*/ + +#include "lwip/apps/snmp_opts.h" +#if LWIP_SNMP + +#include "lldp-mib.h" +#include "lwip/apps/snmp.h" +#include "lwip/apps/snmp_core.h" +#include "lwip/apps/snmp_scalar.h" +#include "lwip/apps/snmp_table.h" + +/* --- lldpConfiguration 1.0.8802.1.1.2.1.1 + * ----------------------------------------------------- */ + +static snmp_err_t lldpconfigmanaddrtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldpconfigmanaddrtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldpconfigmanaddrtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldpconfigmanaddrtable_columns[] = { + {1, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpConfigManAddrPortsTxEnable + */ +}; +static const struct snmp_table_node lldpconfigmanaddrtable = SNMP_TABLE_CREATE ( + 7, + lldpconfigmanaddrtable_columns, + lldpconfigmanaddrtable_get_instance, + lldpconfigmanaddrtable_get_next_instance, + lldpconfigmanaddrtable_get_value, + NULL, + NULL); + +static const struct snmp_node * const lldpconfiguration_subnodes[] = { + &lldpconfigmanaddrtable.node.node}; +static const struct snmp_tree_node lldpconfiguration_treenode = + SNMP_CREATE_TREE_NODE (1, lldpconfiguration_subnodes); + +/* --- lldpLocalSystemData 1.0.8802.1.1.2.1.3 + * ----------------------------------------------------- */ +static s16_t lldplocalsystemdata_treenode_get_value ( + struct snmp_node_instance * instance, + void * value); +static const struct snmp_scalar_node lldplocchassisidsubtype_scalar = + SNMP_SCALAR_CREATE_NODE_READONLY ( + 1, + SNMP_ASN1_TYPE_INTEGER, + lldplocalsystemdata_treenode_get_value); + +static const struct snmp_scalar_node lldplocchassisid_scalar = + SNMP_SCALAR_CREATE_NODE_READONLY ( + 2, + SNMP_ASN1_TYPE_OCTET_STRING, + lldplocalsystemdata_treenode_get_value); + +static snmp_err_t lldplocporttable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldplocporttable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldplocporttable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldplocporttable_columns[] = { + {2, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpLocPortIdSubtype + */ + {3, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpLocPortId + */ + {4, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpLocPortDesc + */ +}; +static const struct snmp_table_node lldplocporttable = SNMP_TABLE_CREATE ( + 7, + lldplocporttable_columns, + lldplocporttable_get_instance, + lldplocporttable_get_next_instance, + lldplocporttable_get_value, + NULL, + NULL); + +static snmp_err_t lldplocmanaddrtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldplocmanaddrtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldplocmanaddrtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldplocmanaddrtable_columns[] = { + {3, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpLocManAddrLen + */ + {4, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpLocManAddrIfSubtype + */ + {5, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpLocManAddrIfId + */ +}; +static const struct snmp_table_node lldplocmanaddrtable = SNMP_TABLE_CREATE ( + 8, + lldplocmanaddrtable_columns, + lldplocmanaddrtable_get_instance, + lldplocmanaddrtable_get_next_instance, + lldplocmanaddrtable_get_value, + NULL, + NULL); + +static const struct snmp_node * const lldplocalsystemdata_subnodes[] = { + &lldplocchassisidsubtype_scalar.node.node, + &lldplocchassisid_scalar.node.node, + &lldplocporttable.node.node, + &lldplocmanaddrtable.node.node}; +static const struct snmp_tree_node lldplocalsystemdata_treenode = + SNMP_CREATE_TREE_NODE (3, lldplocalsystemdata_subnodes); + +/* --- lldpRemoteSystemsData 1.0.8802.1.1.2.1.4 + * ----------------------------------------------------- */ +static snmp_err_t lldpremtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldpremtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldpremtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldpremtable_columns[] = { + {4, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpRemChassisIdSubtype + */ + {5, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpRemChassisId + */ + {6, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpRemPortIdSubtype + */ + {7, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpRemPortId + */ + {8, + SNMP_ASN1_TYPE_OCTET_STRING, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpRemPortDesc + */ +}; +static const struct snmp_table_node lldpremtable = SNMP_TABLE_CREATE ( + 1, + lldpremtable_columns, + lldpremtable_get_instance, + lldpremtable_get_next_instance, + lldpremtable_get_value, + NULL, + NULL); + +static snmp_err_t lldpremmanaddrtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance); +static snmp_err_t lldpremmanaddrtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance); +static s16_t lldpremmanaddrtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value); +static const struct snmp_table_col_def lldpremmanaddrtable_columns[] = { + {3, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpRemManAddrIfSubtype + */ + {4, + SNMP_ASN1_TYPE_INTEGER, + SNMP_NODE_INSTANCE_READ_ONLY}, /* lldpRemManAddrIfId + */ +}; +static const struct snmp_table_node lldpremmanaddrtable = SNMP_TABLE_CREATE ( + 2, + lldpremmanaddrtable_columns, + lldpremmanaddrtable_get_instance, + lldpremmanaddrtable_get_next_instance, + lldpremmanaddrtable_get_value, + NULL, + NULL); + +static const struct snmp_node * const lldpremotesystemsdata_subnodes[] = { + &lldpremtable.node.node, + &lldpremmanaddrtable.node.node}; +static const struct snmp_tree_node lldpremotesystemsdata_treenode = + SNMP_CREATE_TREE_NODE (4, lldpremotesystemsdata_subnodes); + +/* --- lldpObjects 1.0.8802.1.1.2.1 + * ----------------------------------------------------- */ +static const struct snmp_node * const lldpobjects_subnodes[] = { + &lldpconfiguration_treenode.node, + &lldplocalsystemdata_treenode.node, + &lldpremotesystemsdata_treenode.node}; +static const struct snmp_tree_node lldpobjects_treenode = + SNMP_CREATE_TREE_NODE (1, lldpobjects_subnodes); + +/* --- lldpMIB ----------------------------------------------------- */ +static const struct snmp_node * const lldpmib_subnodes[] = { + &lldpobjects_treenode.node}; +static const struct snmp_tree_node lldpmib_root = + SNMP_CREATE_TREE_NODE (2, lldpmib_subnodes); +static const u32_t lldpmib_base_oid[] = {1, 0, 8802, 1, 1, 2}; +const struct snmp_mib lldpmib = { + lldpmib_base_oid, + LWIP_ARRAYSIZE (lldpmib_base_oid), + &lldpmib_root.node}; + +/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +LWIP MIB generator - preserved section begin +Code below is preserved on regeneration. Remove these comment lines to +regenerate code. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +*/ + +/* --- lldpConfiguration 1.0.8802.1.1.2.1.1 + * ----------------------------------------------------- */ + +/** + * Get cell in table lldpConfigManAddrTable. + * + * Called when an SNMP Get request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpconfigmanaddrtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int interface = rowindex_match_with_local_interface (row_oid, row_oid_len); + if (interface == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Get next cell in table lldpConfigManAddrTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpconfigmanaddrtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int interface = rowindex_update_with_next_local_interface (row_oid); + if (interface == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value at cell in table lldpConfigManAddrTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if error occurred (server will report GenError). + */ +static s16_t lldpconfigmanaddrtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len; + u32_t column = + SNMP_TABLE_GET_COLUMN_FROM_OID (cell_instance->instance_oid.id); + + switch (column) + { + case 1: + { + /* lldpConfigManAddrPortsTxEnable */ + pf_lldp_port_list_t port_list; + + pf_snmp_get_port_list (pnal_snmp.net, &port_list); + memcpy (value, port_list.ports, sizeof (port_list.ports)); + value_len = sizeof (port_list.ports); + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/* --- lldpLocalSystemData 1.0.8802.1.1.2.1.3 + * ----------------------------------------------------- */ + +/** + * Get cell in table lldpLocPortTable. + * + * Called when an SNMP Get request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldplocporttable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int port = rowindex_match_with_local_port (row_oid, row_oid_len); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + cell_instance->reference.s32 = port; + return SNMP_ERR_NOERROR; + } +} + +/** + * Get next cell in table lldpLocPortTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldplocporttable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int port = rowindex_update_with_next_local_port (row_oid); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + cell_instance->reference.s32 = port; + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value at cell in table lldpLocPortTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if error occurred (server will report GenError). + */ +static s16_t lldplocporttable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len; + int port = cell_instance->reference.s32; + u32_t column = + SNMP_TABLE_GET_COLUMN_FROM_OID (cell_instance->instance_oid.id); + + switch (column) + { + case 2: + { + /* lldpLocPortIdSubtype */ + s32_t * v = (s32_t *)value; + pf_lldp_port_id_t port_id; + + pf_snmp_get_port_id (pnal_snmp.net, port, &port_id); + *v = port_id.subtype; + value_len = sizeof (s32_t); + } + break; + case 3: + { + /* lldpLocPortId */ + char * v = (char *)value; + pf_lldp_port_id_t port_id; + + pf_snmp_get_port_id (pnal_snmp.net, port, &port_id); + value_len = port_id.len; + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + memcpy (v, port_id.string, value_len); + } + break; + case 4: + { + /* lldpLocPortDesc */ + char * v = (char *)value; + pf_lldp_port_description_t port_desc; + + pf_snmp_get_port_description (pnal_snmp.net, port, &port_desc); + value_len = port_desc.len; + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + memcpy (v, port_desc.string, value_len); + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/** + * Get cell in table lldpLocManAddrTable. + * + * Called when an SNMP Get request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldplocmanaddrtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int interface = rowindex_match_with_local_interface (row_oid, row_oid_len); + if (interface == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Get next cell in table lldpLocManAddrTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell is found, the SNMP stack may call the corresponding get_value() + * function below to retrieve the actual value contained in the cell. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldplocmanaddrtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int interface = rowindex_update_with_next_local_interface (row_oid); + if (interface == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value at cell in table lldpLocManAddrTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if error occurred (server will report GenError). + */ +static s16_t lldplocmanaddrtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len; + u32_t column = + SNMP_TABLE_GET_COLUMN_FROM_OID (cell_instance->instance_oid.id); + + switch (column) + { + case 3: + { + /* lldpLocManAddrLen */ + s32_t * v = (s32_t *)value; + pf_snmp_management_address_t address; + + pf_snmp_get_management_address (pnal_snmp.net, &address); + value_len = sizeof (s32_t); + *v = address.len; + } + break; + case 4: + { + /* lldpLocManAddrIfSubtype */ + s32_t * v = (s32_t *)value; + pf_lldp_interface_number_t port_index; + + pf_snmp_get_management_port_index (pnal_snmp.net, &port_index); + value_len = sizeof (s32_t); + *v = port_index.subtype; + } + break; + case 5: + { + /* lldpLocManAddrIfId */ + s32_t * v = (s32_t *)value; + pf_lldp_interface_number_t port_index; + + pf_snmp_get_management_port_index (pnal_snmp.net, &port_index); + + value_len = sizeof (s32_t); + *v = port_index.value; + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/** + * Get value at top level in object lldpLocalSystemData. + * + * Called when an SNMP Get or GetNext request is received for this object. + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if error occurred (server will report GenError). + */ +static s16_t lldplocalsystemdata_treenode_get_value ( + struct snmp_node_instance * instance, + void * value) +{ + s16_t value_len; + u32_t column = instance->node->oid; + + switch (column) + { + case 1: + { + /* lldpLocChassisIdSubtype */ + s32_t * v = (s32_t *)value; + pf_lldp_chassis_id_t chassis_id; + + pf_snmp_get_chassis_id (pnal_snmp.net, &chassis_id); + value_len = sizeof (s32_t); + *v = chassis_id.subtype; + } + break; + case 2: + { + /* lldpLocChassisId */ + u8_t * v = (u8_t *)value; + pf_lldp_chassis_id_t chassis_id; + + pf_snmp_get_chassis_id (pnal_snmp.net, &chassis_id); + value_len = chassis_id.len; + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + memcpy (v, chassis_id.string, value_len); + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/* --- lldpRemoteSystemsData 1.0.8802.1.1.2.1.4 + * ----------------------------------------------------- */ + +static s16_t lldpremtable_get_value_from_remote_device ( + pnal_snmp_response_t * value, + int port, + u32_t column); + +/** + * Get cell in table lldpRemTable. + * + * Called when an SNMP Get request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpremtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_match_with_remote_device (row_oid, row_oid_len); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpremtable_get_value_from_remote_device ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get next cell in table lldpRemTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpremtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_update_with_next_remote_device (row_oid); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpremtable_get_value_from_remote_device ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get value at cell in table lldpRemTable. + * + * @param value Out: Value to be returned in response. + * @param port In: Local port number for port directly + * connected to the remote device. + * Valid range: 1 .. num_physical_ports + * @param column In: Column index for the cell. + * @return Size of returned value, in bytes. + * -1 if no valid value could be returned. + */ +static s16_t lldpremtable_get_value_from_remote_device ( + pnal_snmp_response_t * value, + int port, + u32_t column) +{ + s16_t value_len; + int error; + + switch (column) + { + case 4: + { + /* lldpRemChassisIdSubtype */ + pf_lldp_chassis_id_t chassis_id; + + error = pf_snmp_get_peer_chassis_id (pnal_snmp.net, port, &chassis_id); + if (error) + { + return error; + } + + value_len = sizeof (s32_t); + value->s32 = chassis_id.subtype; + } + break; + case 5: + { + /* lldpRemChassisId */ + pf_lldp_chassis_id_t chassis_id; + + error = pf_snmp_get_peer_chassis_id (pnal_snmp.net, port, &chassis_id); + if (error) + { + return error; + } + + value_len = chassis_id.len; + if ((size_t)value_len <= SNMP_MAX_VALUE_SIZE) + { + memcpy (value->buffer, chassis_id.string, value_len); + } + } + break; + case 6: + { + /* lldpRemPortIdSubtype */ + pf_lldp_port_id_t port_id; + + error = pf_snmp_get_peer_port_id (pnal_snmp.net, port, &port_id); + if (error) + { + return error; + } + + value_len = sizeof (s32_t); + value->s32 = port_id.subtype; + } + break; + case 7: + { + /* lldpRemPortId */ + pf_lldp_port_id_t port_id; + + error = pf_snmp_get_peer_port_id (pnal_snmp.net, port, &port_id); + if (error) + { + return error; + } + + value_len = port_id.len; + if ((size_t)value_len <= SNMP_MAX_VALUE_SIZE) + { + memcpy (value->buffer, port_id.string, value_len); + } + } + break; + case 8: + { + /* lldpRemPortDesc */ + pf_lldp_port_description_t port_desc; + + error = + pf_snmp_get_peer_port_description (pnal_snmp.net, port, &port_desc); + if (error) + { + return error; + } + + value_len = port_desc.len; + if ((size_t)value_len <= SNMP_MAX_VALUE_SIZE) + { + memcpy (value->buffer, port_desc.string, value_len); + } + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/** + * Get value at cell in table lldpRemTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). Its value was also read and saved in + * cell_instance->reference. + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if value was too large (server will report GenError). + */ +static s16_t lldpremtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len = cell_instance->reference_len; + pnal_snmp_response_t * saved_value = cell_instance->reference.ptr; + + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + + memcpy (value, saved_value, value_len); + return value_len; +} + +static s16_t lldpremmanaddrtable_get_value_from_remote_interface ( + pnal_snmp_response_t * value, + int port, + u32_t column); + +/** + * Get cell in table lldpRemManAddrTable. + * + * Called when an SNMP Get request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid In: Row index (array) for the cell. + * @param row_oid_len In: The number of elements in the row index array. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpremmanaddrtable_get_instance ( + const u32_t * column, + const u32_t * row_oid, + u8_t row_oid_len, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_match_with_remote_interface (row_oid, row_oid_len); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpremmanaddrtable_get_value_from_remote_interface ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get next cell in table lldpRemManAddrTable. + * + * Called when an SNMP GetNext request is received for this table. + * If cell with valid value is found, the value will be saved for later + * retrieval by the SNMP stack in call to the corresponding get_value() + * function. + * + * @param column In: Column index for the cell. + * @param row_oid InOut: Row index for the cell. + * @param cell_instance InOut: Cell instance (containing meta-data). + * @return SNMP_ERR_NOERROR if cell was found, + * SNMP_ERR_NOSUCHINSTANCE otherwise. + */ +static snmp_err_t lldpremmanaddrtable_get_next_instance ( + const u32_t * column, + struct snmp_obj_id * row_oid, + struct snmp_node_instance * cell_instance) +{ + int port; + s16_t response_len; + + port = rowindex_update_with_next_remote_interface (row_oid); + if (port == 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + response_len = lldpremmanaddrtable_get_value_from_remote_interface ( + &pnal_snmp.response, + port, + *column); + if (response_len < 0) + { + return SNMP_ERR_NOSUCHINSTANCE; + } + + cell_instance->reference.ptr = &pnal_snmp.response; + cell_instance->reference_len = response_len; + return SNMP_ERR_NOERROR; +} + +/** + * Get value at cell in table lldpRemManAddrTable. + * + * @param value Out: Value to be returned in response. + * @param port In: Local port number for port directly + * connected to the remote interface. + * Valid range: 1 .. num_physical_ports + * @param column In: Column index for the cell. + * @return Size of returned value, in bytes. + * -1 if no valid value could be returned. + */ +static s16_t lldpremmanaddrtable_get_value_from_remote_interface ( + pnal_snmp_response_t * value, + int port, + u32_t column) +{ + s16_t value_len; + int error; + + switch (column) + { + case 3: + { + /* lldpRemManAddrIfSubtype */ + pf_lldp_interface_number_t port_index; + + error = pf_snmp_get_peer_management_port_index ( + pnal_snmp.net, + port, + &port_index); + if (error) + { + return error; + } + + value_len = sizeof (s32_t); + value->s32 = port_index.subtype; + } + break; + case 4: + { + /* lldpRemManAddrIfId */ + pf_lldp_interface_number_t port_index; + + error = pf_snmp_get_peer_management_port_index ( + pnal_snmp.net, + port, + &port_index); + if (error) + { + return error; + } + + value_len = sizeof (s32_t); + value->s32 = port_index.value; + } + break; + default: + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Unknown table column: %" PRIu32 ".\n", + __LINE__, + column); + return -1; + } + + return value_len; +} + +/** + * Get value at cell in table lldpRemManAddrTable. + * + * Called when an SNMP Get or GetNext request is received for this table. + * The cell was previously identified in a call to get_instance() or + * get_next_instance(). Its value was also read and saved in + * cell_instance->reference. + * + * @param cell_instance In: Cell instance (containing meta-data). + * @param value Out: Value to be returned in response. + * @return Size of returned value, in bytes. + * -1 if value was too large (server will report GenError). + */ +static s16_t lldpremmanaddrtable_get_value ( + struct snmp_node_instance * cell_instance, + void * value) +{ + s16_t value_len = cell_instance->reference_len; + pnal_snmp_response_t * saved_value = cell_instance->reference.ptr; + + if ((size_t)value_len > SNMP_MAX_VALUE_SIZE) + { + LOG_ERROR ( + PF_SNMP_LOG, + "LLDP-MIB(%d): Value is too large: %" PRId16 "\n", + __LINE__, + value_len); + return -1; + } + + memcpy (value, saved_value, value_len); + return value_len; +} + +/* --- lldpObjects 1.0.8802.1.1.2.1 + * ----------------------------------------------------- */ +/* --- lldpMIB ----------------------------------------------------- */ +#endif /* LWIP_SNMP */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.h b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.h new file mode 100644 index 0000000..c8b5fda --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/lldp-mib.h @@ -0,0 +1,44 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief The LLDP-MIB used by SNMP server + */ + +/* +Generated by LwipMibCompiler +*/ + +#ifndef LLDP_MIB_H +#define LLDP_MIB_H LLDP_MIB_H + +#include "lwip/apps/snmp_opts.h" +#if LWIP_SNMP + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "lwip/apps/snmp_core.h" + +extern const struct snmp_mib lldpmib; + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* LWIP_SNMP */ +#endif /* LLDP_MIB_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.c b/profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.c new file mode 100644 index 0000000..62c5d4c --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.c @@ -0,0 +1,418 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Functions for reading and writing SNMP MIB-II system variables + */ + +#include "mib2_system.h" + +#include "options.h" +#include "pnet_api.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal.h" +#include "pf_types.h" +#include "pf_snmp.h" +#include "pnal_snmp.h" + +#include + +/** + * Get value of sysDescr variable + * + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value (in bytes) if successful, + * SNMP_ERR_GENERROR if buffer was too small. + */ +static s16_t system_get_description (void * value, size_t max_size) +{ + size_t size; + pf_snmp_system_description_t description; + + pf_snmp_get_system_description (pnal_snmp.net, &description); + + size = strlen (description.string); + if (size > max_size) + { + return SNMP_ERR_GENERROR; + } + + memcpy (value, description.string, size); + return size; +} + +/** + * Get value of sysObjectID variable + * + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value (in bytes) if successful, + * SNMP_ERR_GENERROR if buffer was too small. + */ +static s16_t system_get_object_id (void * value, size_t max_size) +{ + size_t size; + const struct snmp_obj_id * dev_enterprise_oid = + snmp_get_device_enterprise_oid(); + + size = dev_enterprise_oid->len * sizeof (u32_t); + if (size > max_size) + { + return SNMP_ERR_GENERROR; + } + + memcpy (value, dev_enterprise_oid->id, size); + return size; +} + +/** + * Get value of sysUpTime variable + * + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value (in bytes) if successful, + * SNMP_ERR_GENERROR if buffer was too small. + */ +static s16_t system_get_uptime (void * value, size_t max_size) +{ + u32_t * uptime = value; + + *uptime = pnal_get_system_uptime_10ms(); + return sizeof (*uptime); +} + +/** + * Get value of sysContact variable + * + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value (in bytes) if successful, + * SNMP_ERR_GENERROR if buffer was too small. + */ +static s16_t system_get_contact (void * value, size_t max_size) +{ + size_t size; + pf_snmp_system_contact_t contact; + + pf_snmp_get_system_contact (pnal_snmp.net, &contact); + + size = strlen (contact.string); + if (size > max_size) + { + return SNMP_ERR_GENERROR; + } + + memcpy (value, contact.string, size); + return size; +} + +/** + * Test if value could be set for sysContact variable + * + * @param value In: Buffer containing new value. + * Not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_WRONGVALUE if \a size is too large. + */ +static snmp_err_t system_test_contact (const void * value, size_t size) +{ + pf_snmp_system_contact_t contact; + + if (size >= sizeof (contact.string)) + { + return SNMP_ERR_WRONGVALUE; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Set new value for sysContact variable + * + * Note that it has already been verified that \a size is not too large. + * + * @param value In: Buffer containing new value. + * Not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_GENERROR on error. + */ +static snmp_err_t system_set_contact (const void * value, size_t size) +{ + pf_snmp_system_contact_t contact; + int error; + + memset (contact.string, '\0', sizeof (contact.string)); + memcpy (contact.string, value, size); + + error = pf_snmp_set_system_contact (pnal_snmp.net, &contact); + if (error) + { + return SNMP_ERR_GENERROR; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value of sysName variable + * + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value (in bytes) if successful, + * SNMP_ERR_GENERROR if buffer was too small. + */ +static s16_t system_get_name (void * value, size_t max_size) +{ + size_t size; + pf_snmp_system_name_t name; + + pf_snmp_get_system_name (pnal_snmp.net, &name); + + size = strlen (name.string); + if (size > max_size) + { + return SNMP_ERR_GENERROR; + } + + memcpy (value, name.string, size); + return size; +} + +/** + * Test if value could be set for sysName variable + * + * @param value In: Buffer containing new value. + * Not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_WRONGVALUE if \a size is too large. + */ +static snmp_err_t system_test_name (const void * value, size_t size) +{ + pf_snmp_system_name_t name; + + if (size >= sizeof (name.string)) + { + return SNMP_ERR_WRONGVALUE; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Set new value for sysName variable + * + * Note that it has already been verified that \a size is not too large. + * + * @param value In: Buffer containing new value. + * Not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_GENERROR on error. + */ +static snmp_err_t system_set_name (const void * value, size_t size) +{ + pf_snmp_system_name_t name; + int error; + + memset (name.string, '\0', sizeof (name.string)); + memcpy (name.string, value, size); + + error = pf_snmp_set_system_name (pnal_snmp.net, &name); + if (error) + { + return SNMP_ERR_GENERROR; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value of sysLocation variable + * + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value (in bytes) if successful, + * SNMP_ERR_GENERROR if buffer was too small. + */ +static s16_t system_get_location (void * value, size_t max_size) +{ + size_t size; + pf_snmp_system_location_t location; + + pf_snmp_get_system_location (pnal_snmp.net, &location); + + size = strlen (location.string); + if (size > max_size) + { + return SNMP_ERR_GENERROR; + } + + memcpy (value, location.string, size); + return size; +} + +/** + * Test if value could be set for sysLocation variable + * + * @param value In: Buffer containing new value. + * Not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_WRONGVALUE if \a size is too large. + */ +static snmp_err_t system_test_location (const void * value, size_t size) +{ + pf_snmp_system_location_t location; + + if (size >= sizeof (location.string)) + { + return SNMP_ERR_WRONGVALUE; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Set new value for sysLocation variable + * + * Note that it has already been verified that \a size is not too large. + * + * @param value In: Buffer containing new value. + * Not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_GENERROR on error. + */ +static snmp_err_t system_set_location (const void * value, size_t size) +{ + pf_snmp_system_location_t location; + int error; + + memset (location.string, '\0', sizeof (location.string)); + memcpy (location.string, value, size); + + error = pf_snmp_set_system_location (pnal_snmp.net, &location); + if (error) + { + return SNMP_ERR_GENERROR; + } + else + { + return SNMP_ERR_NOERROR; + } +} + +/** + * Get value of sysServices variable + * + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value (in bytes) if successful, + * SNMP_ERR_GENERROR if buffer was too small. + */ +static s16_t system_get_services (void * value, size_t max_size) +{ + s32_t * services = value; + + *services = SNMP_SYSSERVICES; + return sizeof (*services); +} + +s16_t mib2_system_get_value (u32_t column, void * value, size_t max_size) +{ + switch (column) + { + case 1: /* sysDescr */ + return system_get_description (value, max_size); + case 2: /* sysObjectID */ + return system_get_object_id (value, max_size); + case 3: /* sysUpTime */ + return system_get_uptime (value, max_size); + case 4: /* sysContact */ + return system_get_contact (value, max_size); + case 5: /* sysName */ + return system_get_name (value, max_size); + case 6: /* sysLocation */ + return system_get_location (value, max_size); + case 7: /* sysServices */ + return system_get_services (value, max_size); + default: + LOG_ERROR ( + PF_SNMP_LOG, + "MIB2.system(%d): Unknown column: %" PRIu32 ".\n", + __LINE__, + column); + return SNMP_ERR_GENERROR; + } +} + +snmp_err_t mib2_system_test_set_value ( + u32_t column, + const void * value, + size_t size) +{ + switch (column) + { + case 4: /* sysContact */ + return system_test_contact (value, size); + case 5: /* sysName */ + return system_test_name (value, size); + case 6: /* sysLocation */ + return system_test_location (value, size); + default: + LOG_ERROR ( + PF_SNMP_LOG, + "MIB2.system(%d): Unknown column: %" PRIu32 ".\n", + __LINE__, + column); + return SNMP_ERR_WRONGVALUE; + } +} + +snmp_err_t mib2_system_set_value (u32_t column, const void * value, size_t size) +{ + switch (column) + { + case 4: /* sysContact */ + return system_set_contact (value, size); + case 5: /* sysName */ + return system_set_name (value, size); + case 6: /* sysLocation */ + return system_set_location (value, size); + default: + LOG_ERROR ( + PF_SNMP_LOG, + "MIB2.system(%d): Unknown column: %" PRIu32 ".\n", + __LINE__, + column); + return SNMP_ERR_GENERROR; + } +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.h b/profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.h new file mode 100644 index 0000000..b407619 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/mib2_system.h @@ -0,0 +1,91 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Functions for reading and writing SNMP MIB-II system variables + * + * These functions should be used as callback functions to be called from the + * lwip stack when SNMP Get/GetNext or Set requests are received for variables + * in the system group (OID 1.3.6.1.2.1.1). The appropriate functions in + * the p-net stack will be called as needed. For configuringlwip to use these, + * functions, call snmp_mib2_system_set_callbacks(). + * + * Note that it is assumed that a patched version of lwip is used. + * The original version of lwip does not provide callback functions to read + * variables in SNMP Get/GetNext requests. + */ + +#ifndef MIB2_SYSTEM_H +#define MIB2_SYSTEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * Get value of system variable + * + * This function should be called when an SNMP Get or GetNext request + * is received. + * + * @param column In: Column index for system variable. + * @param value Out: Buffer where value will be stored. + * @param max_size In: Size of buffer in bytes. + * @return Size of retrieved value if successful, + * SNMP_ERR_GENERROR if an error occurred. + */ +s16_t mib2_system_get_value (u32_t column, void * value, size_t max_size); + +/** + * Test if value could be set for system variable + * + * This function should be called when an SNMP Set request is received. + * + * @param column In: Column index for system variable. + * @param value In: Buffer containing the new value. + * Is not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_WRONGVALUE if \a len is too large. + */ +snmp_err_t mib2_system_test_set_value ( + u32_t column, + const void * value, + size_t size); + +/** + * Set new value for system variable + * + * This function should be called when an SNMP Set request is received + * after first calling mib2_system_test_set_value() to verify + * that \a size is not too large. + * + * @param column In: Column index for system variable. + * @param value In: Buffer containing the new value. + * Is not null-terminated. + * @param size In: Size of \a value in bytes. + * @return SNMP_ERR_NOERROR if successful, + * SNMP_ERR_GENERROR on error. + */ +snmp_err_t mib2_system_set_value (u32_t column, const void * value, size_t size); + +#ifdef __cplusplus +} +#endif + +#endif /* MIB2_SYSTEM_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.c b/profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.c new file mode 100644 index 0000000..03b1136 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.c @@ -0,0 +1,351 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "rowindex.h" + +#include "options.h" +#include "pnet_api.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal.h" +#include "pf_types.h" +#include "pf_snmp.h" +#include "pnal_snmp.h" + +#include + +static void rowindex_construct_for_local_port ( + struct snmp_obj_id * row_index, + int port) +{ + row_index->id[0] = port; /* lldpLocPortNum */ + row_index->len = 1; +} + +static void rowindex_construct_for_local_interface ( + struct snmp_obj_id * row_index) +{ + pf_snmp_management_address_t address; + size_t i; + + pf_snmp_get_management_address (pnal_snmp.net, &address); + row_index->id[0] = address.subtype; /* lldpLocManAddrSubtype */ + for (i = 0; i < address.len; i++) + { + row_index->id[1 + i] = address.value[i]; /* lldpLocManAddr */ + } + row_index->len = 1 + address.len; +} + +static int rowindex_construct_for_remote_device ( + struct snmp_obj_id * row_index, + int port) +{ + uint32_t timestamp; + int error; + + error = pf_snmp_get_peer_timestamp (pnal_snmp.net, port, ×tamp); + if (error) + { + return error; + } + + row_index->id[0] = timestamp; /* lldpRemTimeMark */ + row_index->id[1] = port; /* lldpRemLocalPortNum */ + row_index->id[2] = port; /* lldpRemIndex */ + row_index->len = 3; + + return error; +} + +static int rowindex_construct_for_remote_interface ( + struct snmp_obj_id * row_index, + int port) +{ + uint32_t timestamp; + pf_snmp_management_address_t address; + int error; + size_t i; + + error = pf_snmp_get_peer_timestamp (pnal_snmp.net, port, ×tamp); + if (error) + { + return error; + } + error = pf_snmp_get_peer_management_address (pnal_snmp.net, port, &address); + if (error) + { + return error; + } + + row_index->id[0] = timestamp; /* lldpRemTimeMark */ + row_index->id[1] = port; /* lldpRemLocalPortNum */ + row_index->id[2] = port; /* lldpRemIndex */ + row_index->id[3] = address.subtype; /* lldpRemManAddrSubtype */ + for (i = 0; i < address.len; i++) + { + row_index->id[4 + i] = address.value[i]; /* lldpRemManAddr */ + } + row_index->len = 4 + address.len; + + return error; +} + +int rowindex_match_with_local_port (const u32_t * row_oid, u8_t row_oid_len) +{ + pf_port_iterator_t port_iterator; + int port; + + pf_snmp_init_port_iterator (pnal_snmp.net, &port_iterator); + port = pf_snmp_get_next_port (&port_iterator); + while (port != 0) + { + struct snmp_obj_id port_oid; + + rowindex_construct_for_local_port (&port_oid, port); + if (snmp_oid_equal (row_oid, row_oid_len, port_oid.id, port_oid.len)) + { + return port; + } + + port = pf_snmp_get_next_port (&port_iterator); + } + + return 0; +} + +int rowindex_update_with_next_local_port (struct snmp_obj_id * row_oid) +{ + struct snmp_next_oid_state state; + u32_t next_oid[SNMP_MAX_OBJ_ID_LEN]; + pf_port_iterator_t port_iterator; + int port; + + snmp_next_oid_init ( + &state, + row_oid->id, + row_oid->len, + next_oid, + SNMP_MAX_OBJ_ID_LEN); + pf_snmp_init_port_iterator (pnal_snmp.net, &port_iterator); + port = pf_snmp_get_next_port (&port_iterator); + while (port != 0) + { + struct snmp_obj_id port_oid; + + rowindex_construct_for_local_port (&port_oid, port); + snmp_next_oid_check (&state, port_oid.id, port_oid.len, (void *)port); + + port = pf_snmp_get_next_port (&port_iterator); + } + + if (state.status == SNMP_NEXT_OID_STATUS_SUCCESS) + { + snmp_oid_assign (row_oid, state.next_oid, state.next_oid_len); + port = (int)state.reference; + } + else + { + port = 0; + } + + return port; +} + +int rowindex_match_with_local_interface (const u32_t * row_oid, u8_t row_oid_len) +{ + struct snmp_obj_id interface_oid; + int interface; + + rowindex_construct_for_local_interface (&interface_oid); + if (snmp_oid_equal (row_oid, row_oid_len, interface_oid.id, interface_oid.len)) + { + interface = 1; + } + else + { + interface = 0; + } + + return interface; +} + +snmp_err_t rowindex_update_with_next_local_interface ( + struct snmp_obj_id * row_oid) +{ + struct snmp_next_oid_state state; + u32_t next_oid[SNMP_MAX_OBJ_ID_LEN]; + struct snmp_obj_id interface_oid; + int interface; + + snmp_next_oid_init ( + &state, + row_oid->id, + row_oid->len, + next_oid, + SNMP_MAX_OBJ_ID_LEN); + rowindex_construct_for_local_interface (&interface_oid); + snmp_next_oid_check (&state, interface_oid.id, interface_oid.len, NULL); + + if (state.status == SNMP_NEXT_OID_STATUS_SUCCESS) + { + snmp_oid_assign (row_oid, state.next_oid, state.next_oid_len); + interface = 1; + } + else + { + interface = 0; + } + + return interface; +} + +int rowindex_match_with_remote_device (const u32_t * row_oid, u8_t row_oid_len) +{ + pf_port_iterator_t port_iterator; + int port; + + pf_snmp_init_port_iterator (pnal_snmp.net, &port_iterator); + port = pf_snmp_get_next_port (&port_iterator); + while (port != 0) + { + struct snmp_obj_id port_oid; + int error; + + error = rowindex_construct_for_remote_device (&port_oid, port); + if ( + !error && + snmp_oid_equal (row_oid, row_oid_len, port_oid.id, port_oid.len)) + { + return port; + } + + port = pf_snmp_get_next_port (&port_iterator); + } + + return 0; +} + +int rowindex_update_with_next_remote_device (struct snmp_obj_id * row_oid) +{ + struct snmp_next_oid_state state; + u32_t next_oid[SNMP_MAX_OBJ_ID_LEN]; + pf_port_iterator_t port_iterator; + int port; + + snmp_next_oid_init ( + &state, + row_oid->id, + row_oid->len, + next_oid, + SNMP_MAX_OBJ_ID_LEN); + pf_snmp_init_port_iterator (pnal_snmp.net, &port_iterator); + port = pf_snmp_get_next_port (&port_iterator); + while (port != 0) + { + int error; + struct snmp_obj_id port_oid; + + error = rowindex_construct_for_remote_device (&port_oid, port); + if (!error) + { + snmp_next_oid_check (&state, port_oid.id, port_oid.len, (void *)port); + } + + port = pf_snmp_get_next_port (&port_iterator); + } + + if (state.status == SNMP_NEXT_OID_STATUS_SUCCESS) + { + snmp_oid_assign (row_oid, state.next_oid, state.next_oid_len); + port = (int)state.reference; + } + else + { + port = 0; + } + + return port; +} + +int rowindex_match_with_remote_interface ( + const u32_t * row_oid, + u8_t row_oid_len) +{ + pf_port_iterator_t port_iterator; + int port; + + pf_snmp_init_port_iterator (pnal_snmp.net, &port_iterator); + port = pf_snmp_get_next_port (&port_iterator); + while (port != 0) + { + int error; + struct snmp_obj_id port_oid; + + error = rowindex_construct_for_remote_interface (&port_oid, port); + if ( + !error && + snmp_oid_equal (row_oid, row_oid_len, port_oid.id, port_oid.len)) + { + return port; + } + + port = pf_snmp_get_next_port (&port_iterator); + } + + return 0; +} + +int rowindex_update_with_next_remote_interface (struct snmp_obj_id * row_oid) +{ + struct snmp_next_oid_state state; + u32_t next_oid[SNMP_MAX_OBJ_ID_LEN]; + pf_port_iterator_t port_iterator; + int port; + + snmp_next_oid_init ( + &state, + row_oid->id, + row_oid->len, + next_oid, + SNMP_MAX_OBJ_ID_LEN); + pf_snmp_init_port_iterator (pnal_snmp.net, &port_iterator); + port = pf_snmp_get_next_port (&port_iterator); + while (port != 0) + { + int error; + struct snmp_obj_id port_oid; + + error = rowindex_construct_for_remote_interface (&port_oid, port); + if (!error) + { + snmp_next_oid_check (&state, port_oid.id, port_oid.len, (void *)port); + } + + port = pf_snmp_get_next_port (&port_iterator); + } + + if (state.status == SNMP_NEXT_OID_STATUS_SUCCESS) + { + snmp_oid_assign (row_oid, state.next_oid, state.next_oid_len); + port = (int)state.reference; + } + else + { + port = 0; + } + + return port; +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.h b/profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.h new file mode 100644 index 0000000..d742352 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/mib/rowindex.h @@ -0,0 +1,123 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief SNMP table row index utilities + * + * Contains functions to: + * - Check if a row index matches existing row index in table. + * - Update a row index with next existing row index in table. + */ + +#ifndef ROWINDEX_H +#define ROWINDEX_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * Match content of table row index with a local port. + * + * @param row_oid In: Table row index (array). + * @param row_oid_len In: Number of elements in array. + * @return Local port number if found, + * 0 otherwise. + */ +int rowindex_match_with_local_port (const u32_t * row_oid, u8_t row_oid_len); + +/** + * Update content of table row index with next matching local port. + * + * @param row_oid InOut: Table row index. + * @return Local port number if found, + * 0 otherwise. + */ +int rowindex_update_with_next_local_port (struct snmp_obj_id * row_oid); + +/** + * Match content of table row index with a local interface. + * + * Note that we only support a single local interface. + * + * @param row_oid In: Table row index (array). + * @param row_oid_len In: Number of elements in array. + * @return 1 if found, + * 0 otherwise. + */ +int rowindex_match_with_local_interface ( + const u32_t * row_oid, + u8_t row_oid_len); + +/** + * Update content of table row index with next matching local interface. + * + * Note that we only support a single local interface. + * + * @param row_oid InOut: Table row index. + * @return 1 if found, + * 0 otherwise. + */ +snmp_err_t rowindex_update_with_next_local_interface ( + struct snmp_obj_id * row_oid); + +/** + * Match content of table row index with a remote device. + * + * @param row_oid In: Table row index (array). + * @param row_oid_len In: Number of elements in array. + * @return Port number for local port connected to the remote device if found, + * 0 otherwise. + */ +int rowindex_match_with_remote_device (const u32_t * row_oid, u8_t row_oid_len); + +/** + * Update content of table row index with next matching remote device. + * + * @param row_oid InOut: Table row index. + * @return Port number for local port connected to the remote device if found, + * 0 otherwise. + */ +int rowindex_update_with_next_remote_device (struct snmp_obj_id * row_oid); + +/** + * Match content of table row index with a remote interface. + * + * @param row_oid In: Table row index (array). + * @param row_oid_len In: Number of elements in array. + * @return Port number for local port connected to the remote interface + * if found, 0 otherwise. + */ +int rowindex_match_with_remote_interface ( + const u32_t * row_oid, + u8_t row_oid_len); + +/** + * Update content of table row index with next matching remote interface. + * + * @param row_oid InOut: Table row index. + * @return Port number for local port connected to the remote interface + * if found, 0 otherwise. + */ +int rowindex_update_with_next_remote_interface (struct snmp_obj_id * row_oid); + +#ifdef __cplusplus +} +#endif + +#endif /* ROWINDEX_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/pnal.c b/profinet_stack/p-net/src/ports/rt-kernel/pnal.c new file mode 100644 index 0000000..df25444 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/pnal.c @@ -0,0 +1,372 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pnal.h" + +#include "options.h" +#include "osal.h" +#include "osal_log.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +int pnal_set_ip_suite ( + const char * interface_name, + const pnal_ipaddr_t * p_ipaddr, + const pnal_ipaddr_t * p_netmask, + const pnal_ipaddr_t * p_gw, + const char * hostname, + bool permanent) +{ + ip_addr_t ip_addr; + ip_addr_t ip_mask; + ip_addr_t ip_gw; + + ip_addr.addr = htonl (*p_ipaddr); + ip_mask.addr = htonl (*p_netmask); + ip_gw.addr = htonl (*p_gw); + net_configure (NULL, &ip_addr, &ip_mask, &ip_gw, NULL, hostname); + + return 0; +} + +int pnal_get_macaddress (const char * interface_name, pnal_ethaddr_t * mac_addr) +{ + memcpy (mac_addr, netif_default->hwaddr, sizeof (pnal_ethaddr_t)); + return 0; +} + +pnal_ipaddr_t pnal_get_ip_address (const char * interface_name) +{ + return htonl (netif_default->ip_addr.addr); +} + +pnal_ipaddr_t pnal_get_netmask (const char * interface_name) +{ + return htonl (netif_default->netmask.addr); +} + +pnal_ipaddr_t pnal_get_gateway (const char * interface_name) +{ + /* TODO Read the actual default gateway */ + + pnal_ipaddr_t ip; + pnal_ipaddr_t gateway; + + ip = pnal_get_ip_address (interface_name); + gateway = (ip & 0xFFFFFF00) | 0x00000001; + + return gateway; +} + +int pnal_get_hostname (char * hostname) +{ + strcpy (hostname, netif_default->hostname); + return 0; +} + +int pnal_get_ip_suite ( + const char * interface_name, + pnal_ipaddr_t * p_ipaddr, + pnal_ipaddr_t * p_netmask, + pnal_ipaddr_t * p_gw, + char * hostname) +{ + int ret = -1; + + *p_ipaddr = pnal_get_ip_address (interface_name); + *p_netmask = pnal_get_netmask (interface_name); + *p_gw = pnal_get_gateway (interface_name); + ret = pnal_get_hostname (hostname); + + return ret; +} + +int pnal_get_port_statistics ( + const char * interface_name, + pnal_port_stats_t * port_stats) +{ + port_stats->if_in_octets = netif_default->mib2_counters.ifinoctets; + port_stats->if_in_errors = netif_default->mib2_counters.ifinerrors; + port_stats->if_in_discards = netif_default->mib2_counters.ifindiscards; + port_stats->if_out_octets = netif_default->mib2_counters.ifoutoctets; + port_stats->if_out_errors = netif_default->mib2_counters.ifouterrors; + port_stats->if_out_discards = netif_default->mib2_counters.ifoutdiscards; + + return 0; +} + +int pnal_get_interface_index (const char * interface_name) +{ + u8_t index; + struct netif * netif = netif_find (interface_name); + + if (netif == NULL) + { + return 0; + } + + index = netif_to_num (netif); + + return index; +} + +/** + * Calculate MAU type + * + * @param link_state In: Link state. + * @return The MAU type + */ +static pnal_eth_mau_t calculate_mau_type (uint8_t link_state) +{ + switch (link_state) + { + case PHY_LINK_OK | PHY_LINK_10MBIT: + return PNAL_ETH_MAU_COPPER_10BaseT; + case PHY_LINK_OK | PHY_LINK_100MBIT: + return PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX; + case PHY_LINK_OK | PHY_LINK_1000MBIT: + return PNAL_ETH_MAU_COPPER_1000BaseT_HALF_DUPLEX; + case PHY_LINK_OK | PHY_LINK_10MBIT | PHY_LINK_FULL_DUPLEX: + return PNAL_ETH_MAU_COPPER_10BaseT; + case PHY_LINK_OK | PHY_LINK_100MBIT | PHY_LINK_FULL_DUPLEX: + return PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + case PHY_LINK_OK | PHY_LINK_1000MBIT | PHY_LINK_FULL_DUPLEX: + return PNAL_ETH_MAU_COPPER_1000BaseT_FULL_DUPLEX; + default: + return PNAL_ETH_MAU_UNKNOWN; + } +} + +/** + * Calculate advertised capabilities + * + * @param capabilities In: rt-kernel advertised capabilities as a + * bitfield. + * @return Profinet advertised capabilities as a bitfield. + */ +static uint16_t calculate_capabilities (uint16_t capabilities) +{ + uint16_t out = 0; + + if (capabilities & PHY_CAPABILITY_10) + { + out |= PNAL_ETH_AUTONEG_CAP_10BaseT_HALF_DUPLEX; + } + if (capabilities & PHY_CAPABILITY_10_FD) + { + out |= PNAL_ETH_AUTONEG_CAP_10BaseT_FULL_DUPLEX; + } + if (capabilities & PHY_CAPABILITY_100) + { + out |= PNAL_ETH_AUTONEG_CAP_100BaseTX_HALF_DUPLEX; + } + if (capabilities & PHY_CAPABILITY_100_FD) + { + out |= PNAL_ETH_AUTONEG_CAP_100BaseTX_FULL_DUPLEX; + } + + if (out == 0) + { + out |= PNAL_ETH_AUTONEG_CAP_UNKNOWN; + } + + return out; +} + +int pnal_eth_get_status (const char * interface_name, pnal_eth_status_t * status) +{ + struct netif * netif; + drv_t * drv; + ioctl_eth_status_t link; + int error; + + netif = netif_find (interface_name); + ASSERT (netif != NULL); + + drv = netif->state; + ASSERT (drv != NULL); + error = drv->ops->ioctl (drv, netif, IOCTL_ETH_GET_STATUS, &link); + if (error) + { + return -1; + } + + status->is_autonegotiation_supported = link.is_autonegotiation_supported; + status->is_autonegotiation_enabled = link.is_autonegotiation_enabled; + status->autonegotiation_advertised_capabilities = + calculate_capabilities (link.capabilities); + + status->operational_mau_type = calculate_mau_type (link.state); + status->running = link.is_operational; + + return 0; +} + +int os_snprintf (char * str, size_t size, const char * fmt, ...) +{ + int ret; + va_list list; + + va_start (list, fmt); + ret = snprintf (str, size, fmt, list); + va_end (list); + return ret; +} + +int pnal_save_file ( + const char * fullpath, + const void * object_1, + size_t size_1, + const void * object_2, + size_t size_2) +{ + int outputfile; + int ret = 0; /* Assume everything goes well */ + + /* Open file */ + outputfile = open (fullpath, O_WRONLY | O_CREAT); + if (outputfile < 0) + { + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Could not open file %s\n", + __LINE__, + fullpath); + return -1; + } + + /* Write file contents */ + if (size_1 > 0) + { + if (write (outputfile, object_1, size_1) != (int)size_1) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to write file %s\n", + __LINE__, + fullpath); + } + } + if (size_2 > 0 && ret == 0) + { + if (write (outputfile, object_2, size_2) != (int)size_2) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to write file %s (second buffer)\n", + __LINE__, + fullpath); + } + } + + /* Close file */ + close (outputfile); + return ret; +} + +void pnal_clear_file (const char * fullpath) +{ + LOG_DEBUG (PF_PNAL_LOG, "PNAL(%d): Clearing file %s\n", __LINE__, fullpath); + (void)remove (fullpath); +} + +int pnal_load_file ( + const char * fullpath, + void * object_1, + size_t size_1, + void * object_2, + size_t size_2) +{ + int inputfile; + int ret = 0; /* Assume everything goes well */ + + /* Open file */ + inputfile = open (fullpath, O_RDONLY); + if (inputfile < 0) + { + LOG_DEBUG ( + PF_PNAL_LOG, + "PNAL(%d): Could not yet open file %s\n", + __LINE__, + fullpath); + return -1; + } + + /* Read file contents */ + if (size_1 > 0) + { + if (read (inputfile, object_1, size_1) != (int)size_1) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to read file %s\n", + __LINE__, + fullpath); + } + } + if (size_2 > 0 && ret == 0) + { + if (read (inputfile, object_2, size_2) != (int)size_2) + { + ret = -1; + LOG_ERROR ( + PF_PNAL_LOG, + "PNAL(%d): Failed to read file %s (second buffer)\n", + __LINE__, + fullpath); + } + } + + /* Close file */ + close (inputfile); + + return ret; +} + +uint32_t pnal_get_system_uptime_10ms (void) +{ + uint32_t uptime = 0; + + MIB2_COPY_SYSUPTIME_TO (&uptime); + return uptime; +} + +pnal_buf_t * pnal_buf_alloc (uint16_t length) +{ + return pbuf_alloc (PBUF_RAW, length, PBUF_POOL); +} + +void pnal_buf_free (pnal_buf_t * p) +{ + ASSERT (pbuf_free (p) == 1); +} + +uint8_t pnal_buf_header (pnal_buf_t * p, int16_t header_size_increment) +{ + return pbuf_header (p, header_size_increment); +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/pnal_config.h b/profinet_stack/p-net/src/ports/rt-kernel/pnal_config.h new file mode 100644 index 0000000..3eed49e --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/pnal_config.h @@ -0,0 +1,49 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2021 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief PNAL-specific configuration + * + * This file contains definitions of configuration settings for the + * PNAL layer. + */ + +#ifndef PNAL_CONFIG_H +#define PNAL_CONFIG_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct pnal_thread_cfg +{ + uint32_t prio; + size_t stack_size; +} pnal_thread_cfg_t; + +typedef struct pnal_cfg +{ + pnal_thread_cfg_t bg_worker_thread; +} pnal_cfg_t; + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_CONFIG_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/pnal_eth.c b/profinet_stack/p-net/src/ports/rt-kernel/pnal_eth.c new file mode 100644 index 0000000..675f580 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/pnal_eth.c @@ -0,0 +1,172 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief rt-kernel Ethernet related functions that use \a pnal_eth_handle_t + */ + +#include "pnal.h" +#include "osal_log.h" + +#include +#include +#include +#include + +#define MAX_NUMBER_OF_IF 1 + +struct pnal_eth_handle +{ + struct netif * netif; + pnal_eth_callback_t * eth_rx_callback; + void * arg; +}; + +static pnal_eth_handle_t interface[MAX_NUMBER_OF_IF]; +static int nic_index = 0; + +/** + * Find PNAL network interface handle + * + * @param netif In: lwip network interface. + * @return PNAL network interface handle corresponding to \a netif, + * NULL otherwise. + */ +static pnal_eth_handle_t * pnal_eth_find_handle (struct netif * netif) +{ + pnal_eth_handle_t * handle; + int i; + + for (i = 0; i < MAX_NUMBER_OF_IF; i++) + { + handle = &interface[i]; + if (handle->netif == netif) + { + return handle; + } + } + + return NULL; +} + +/** + * Allocate PNAL network interface handle + * + * Handles are allocated from a static array and need never be freed. + * + * @return PNAL network interface handle if available, + * NULL if too many handles were allocated. + */ +static pnal_eth_handle_t * pnal_eth_allocate_handle (void) +{ + pnal_eth_handle_t * handle; + + if (nic_index < MAX_NUMBER_OF_IF) + { + handle = &interface[nic_index]; + nic_index++; + return handle; + } + else + { + return NULL; + } +} + +/** + * Process received Ethernet frame + * + * Called from lwip when an Ethernet frame is received with an EtherType + * lwip is not aware of (e.g. Profinet and LLDP). + * + * @param p_buf InOut: Packet buffer containing Ethernet frame. + * @param netif InOut: Network interface receiving the frame. + * @return ERR_OK if frame was processed and freed, + * ERR_IF if it was ignored. + */ +static err_t pnal_eth_sys_recv (struct pbuf * p_buf, struct netif * netif) +{ + int processed; + pnal_eth_handle_t * handle; + + handle = pnal_eth_find_handle (netif); + ASSERT (handle != NULL); + + processed = handle->eth_rx_callback (handle, handle->arg, p_buf); + if (processed) + { + /* Frame handled and freed */ + return ERR_OK; + } + else + { + /* Frame not handled */ + return ERR_IF; + } +} + +pnal_eth_handle_t * pnal_eth_init ( + const char * if_name, + pnal_ethertype_t receive_type, + const pnal_cfg_t * pnal_cfg, + pnal_eth_callback_t * callback, + void * arg) +{ + pnal_eth_handle_t * handle; + struct netif * netif; + + (void)receive_type; /* Ignore, for now all frames will be received. */ + + netif = netif_find (if_name); + if (netif == NULL) + { + os_log (LOG_LEVEL_ERROR, "Network interface \"%s\" not found!\n", if_name); + return NULL; + } + + handle = pnal_eth_allocate_handle(); + if (handle == NULL) + { + os_log (LOG_LEVEL_ERROR, "Too many network interfaces\n"); + return NULL; + } + + handle->arg = arg; + handle->eth_rx_callback = callback; + handle->netif = netif; + + lwip_set_hook_for_unknown_eth_protocol (netif, pnal_eth_sys_recv); + + return handle; +} + +int pnal_eth_send (pnal_eth_handle_t * handle, pnal_buf_t * buf) +{ + int ret = -1; + + ASSERT (handle->netif->linkoutput != NULL); + + /* TODO: Determine if buf could ever be NULL here */ + if (buf != NULL) + { + /* TODO: remove tot_len from os_buff */ + buf->tot_len = buf->len; + + handle->netif->linkoutput (handle->netif, buf); + ret = buf->len; + } + return ret; +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.c b/profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.c new file mode 100644 index 0000000..77dc037 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.c @@ -0,0 +1,75 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "options.h" +#include "pnet_api.h" +#include "osal.h" +#include "osal_log.h" +#include "pnal.h" +#include "pf_types.h" +#include "pnal_snmp.h" + +#include "mib/mib2_system.h" +#include "mib/lldp-mib.h" +#include "mib/lldp-ext-pno-mib.h" +#include "mib/lldp-ext-dot3-mib.h" +#include +#include +#include + +/* Global variable for SNMP server state */ +pnal_snmp_t pnal_snmp; + +/* Configure standard SNMP variables (MIB-II) + * + * NOTE: This uses a patched version of lwip where callback functions are + * called when the MIB-II system variables are read or written. + */ +static void pnal_snmp_configure_mib2 (void) +{ + static const struct snmp_obj_id enterprise_oid = { + .len = SNMP_DEVICE_ENTERPRISE_OID_LEN, + .id = {1, 3, 6, 1, 4, 1, 24686}, /* Profinet */ + }; + + snmp_threadsync_init (&snmp_mib2_lwip_locks, snmp_mib2_lwip_synchronizer); + snmp_set_device_enterprise_oid (&enterprise_oid); + snmp_mib2_system_set_callbacks ( + mib2_system_get_value, + mib2_system_test_set_value, + mib2_system_set_value); +} + +int pnal_snmp_init (pnet_t * net, const pnal_cfg_t * pnal_cfg) +{ + static const struct snmp_mib * mibs[] = { + &mib2, + &lldpmib, + &lldpxpnomib, + &lldpxdot3mib, + }; + + /* Store reference to Profinet device instance */ + pnal_snmp.net = net; + + pnal_snmp_configure_mib2(); + snmp_set_mibs (mibs, LWIP_ARRAYSIZE (mibs)); + + /* Start the SNMP server task */ + snmp_init(); + + /* Success */ + return 0; +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.h b/profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.h new file mode 100644 index 0000000..ec3ebb3 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/pnal_snmp.h @@ -0,0 +1,75 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief SNMP server for rt-kernel + * + * Uses (a patched version of) the SNMP server implementation supplied by lwip. + * Access to Profinet stack is done through the platform-independent internal + * API, pf_snmp.h. Supported MIBs: + * - MIB-II, + * - LLDP-MIB, + * - LLDP-EXT-DOT3-MIB, + * - LLDP-EXT-PNO-MIB. + * + * Note that the rt-kernel tree needs to be patched to support SNMP etc. + * See supplied patch file. + */ + +#ifndef PNAL_SNMP_H +#define PNAL_SNMP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pf_snmp.h" +#include + +/** + * Response buffer + * + * Note that this is a union, not a struct. + */ +typedef union pnal_snmp_response +{ + uint32_t u32; + int32_t s32; + uint8_t buffer[SNMP_MAX_VALUE_SIZE]; +} pnal_snmp_response_t; + +/** + * SNMP server state + */ +typedef struct pnal_snmp +{ + /* The p-net stack instance. + * + * Used for accessing variables in the stack, such as LLDP variables. + */ + pnet_t * net; + + pnal_snmp_response_t response; +} pnal_snmp_t; + +/** Global variable containing SNMP server state */ +extern pnal_snmp_t pnal_snmp; + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_SNMP_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/pnal_sys.h b/profinet_stack/p-net/src/ports/rt-kernel/pnal_sys.h new file mode 100644 index 0000000..76246ed --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/pnal_sys.h @@ -0,0 +1,35 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef PNAL_SYS_H +#define PNAL_SYS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include /* For htonl etc */ +#include + +#define PNAL_BUF_MAX_SIZE PBUF_POOL_BUFSIZE + +/* Re-use lwIP pbuf for rt-kernel */ +typedef struct pbuf pnal_buf_t; + +#ifdef __cplusplus +} +#endif + +#endif /* PNAL_SYS_H */ diff --git a/profinet_stack/p-net/src/ports/rt-kernel/pnal_udp.c b/profinet_stack/p-net/src/ports/rt-kernel/pnal_udp.c new file mode 100644 index 0000000..4c6ed8d --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/pnal_udp.c @@ -0,0 +1,110 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "pnal.h" +#include "osal_log.h" + +#include +#include + +int pnal_udp_open (pnal_ipaddr_t addr, pnal_ipport_t port) +{ + struct sockaddr_in local; + int id; + const int enable = 1; + + id = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (id == -1) + { + return -1; + } + + if (setsockopt(id, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) != 0) + { + goto error; + } + + /* set IP and port number */ + local = (struct sockaddr_in){ + .sin_family = AF_INET, + .sin_addr.s_addr = htonl (addr), + .sin_port = htons (port), + }; + + if (bind (id, (struct sockaddr *)&local, sizeof (local)) != 0) + { + goto error; + } + + return id; + +error: + close (id); + return -1; +} + +int pnal_udp_sendto ( + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size) +{ + struct sockaddr_in remote; + int len; + + remote = (struct sockaddr_in){ + .sin_family = AF_INET, + .sin_addr.s_addr = htonl (dst_addr), + .sin_port = htons (dst_port), + }; + len = + sendto (id, data, size, 0, (struct sockaddr *)&remote, sizeof (remote)); + + return len; +} + +int pnal_udp_recvfrom ( + uint32_t id, + pnal_ipaddr_t * src_addr, + pnal_ipport_t * src_port, + uint8_t * data, + int size) +{ + struct sockaddr_in remote; + socklen_t addr_len = sizeof (remote); + int len; + + memset (&remote, 0, sizeof (remote)); + len = recvfrom ( + id, + data, + size, + MSG_DONTWAIT, + (struct sockaddr *)&remote, + &addr_len); + if (len > 0) + { + *src_addr = ntohl (remote.sin_addr.s_addr); + *src_port = ntohs (remote.sin_port); + } + + return len; +} + +void pnal_udp_close (uint32_t id) +{ + close (id); +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_bsp.c b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_bsp.c new file mode 100644 index 0000000..3468807 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_bsp.c @@ -0,0 +1,25 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" + +void app_set_led (uint16_t id, bool led_state) +{ +} + +bool app_get_button (uint16_t id) +{ + return false; +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_f769disco.c b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_f769disco.c new file mode 100644 index 0000000..5f60a87 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_f769disco.c @@ -0,0 +1,44 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" +#include + +/************************* Utilities ******************************************/ + +void app_set_led (uint16_t id, bool led_state) +{ + if (id == APP_DATA_LED_ID) + { + gpio_set (GPIO_LED_GREEN, led_state ? 1 : 0); + } + else if (id == APP_PROFINET_SIGNAL_LED_ID) + { + gpio_set (GPIO_LED_RED, led_state ? 1 : 0); + } +} + +bool app_get_button (uint16_t id) +{ + if (id == 0) + { + return (gpio_get (GPIO_BUTTON) == 1); + } + else if (id == 1) + { + /* No more buttons on board */ + } + return false; +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_main.c b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_main.c new file mode 100644 index 0000000..9338c5f --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_main.c @@ -0,0 +1,200 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" +#include "app_utils.h" +#include "app_log.h" +#include "app_gsdml.h" + +#include "osal_log.h" +#include "osal.h" +#include + +#include +#include +#include + +#include + +#define APP_DEFAULT_ETHERNET_INTERFACE "en1" +#define APP_DEFAULT_FILE_DIRECTORY "/disk1" +#define APP_LOG_LEVEL APP_LOG_LEVEL_INFO + +#define APP_BG_WORKER_THREAD_PRIORITY 2 +#define APP_BG_WORKER_THREAD_STACKSIZE 4096 /* bytes */ + +/********************************** Globals ***********************************/ + +static app_data_t * sample_app = NULL; +static pnet_cfg_t pnet_cfg = {0}; +app_args_t app_args = {0}; + +/************************* Shell commands *************************************/ +/* Press enter in the terminal to get to the built-in shell. */ +/* Type help to see available commands. */ + +static int _cmd_pnio_show (int argc, char * argv[]) +{ + unsigned long level = 0; + char * end = NULL; + extern char * os_kernel_revision; + + if (argc == 1) + { + level = 0x2010; /* See documentation for pnet_show() */ + } + else if (argc == 2) + { + level = strtoul (argv[1], &end, 0); + if (end == argv[1] || level == 0) + { + shell_usage (argv[0], "level must be an integer > 0"); + return -1; + } + } + else + { + shell_usage (argv[0], "wrong number of arguments"); + return -1; + } + + pnet_show (app_get_pnet_instance (sample_app), level); + printf ("\n"); + printf ("p-net revision: " PNET_VERSION "\n"); + printf ("rt-kernel revision: %s\n", os_kernel_revision); + + return 0; +} + +const shell_cmd_t cmd_pnio_show = { + .cmd = _cmd_pnio_show, + .name = "pnio_show", + .help_short = "Show pnio [level]", + .help_long = "Show pnio [level]. Defaults to 8208. Use 65535 to show all."}; +SHELL_CMD (cmd_pnio_show); + +static int _cmd_pnio_factory_reset (int argc, char * argv[]) +{ + printf ("Factory reset\n"); + (void)pnet_factory_reset (app_get_pnet_instance (sample_app)); + + return 0; +} + +const shell_cmd_t cmd_pnio_factory_reset = { + .cmd = _cmd_pnio_factory_reset, + .name = "pnio_factory_reset", + .help_short = "Perform factory reset, and save to file", + .help_long = "Perform factory reset, and save to file"}; +SHELL_CMD (cmd_pnio_factory_reset); + +static int _cmd_pnio_remove_files (int argc, char * argv[]) +{ + printf ("Deleting data files\n"); + (void)pnet_remove_data_files (APP_DEFAULT_FILE_DIRECTORY); + + return 0; +} + +const shell_cmd_t cmd_pnio_remove_files = { + .cmd = _cmd_pnio_remove_files, + .name = "pnio_remove_files", + .help_short = "Remove data files", + .help_long = "Remove data files"}; +SHELL_CMD (cmd_pnio_remove_files); + +/** + * Suppress RT-Kernel banner during startup + */ +void shell_banner (void) +{ + /* Do not print message since it is run in another context + * and is not aligned with application log + */ +} + +/****************************** Main ******************************************/ + +int main (void) +{ + int ret; + app_utils_netif_namelist_t netif_name_list; + pnet_if_cfg_t netif_cfg = {0}; + uint16_t number_of_ports; + + strcpy (app_args.eth_interfaces, APP_DEFAULT_ETHERNET_INTERFACE); + strcpy (app_args.station_name, APP_GSDML_DEFAULT_STATION_NAME); + app_log_set_log_level (APP_LOG_LEVEL); + + APP_LOG_INFO ("\n** Starting P-Net sample application " PNET_VERSION + " **\n"); + APP_LOG_INFO ("\nType help to get a list of supported shell commands.\n" + "Type help to get a command description.\n" + "For example: help pnio_show\n\n"); + APP_LOG_INFO ( + "Number of slots: %u (incl slot for DAP module)\n", + PNET_MAX_SLOTS); + APP_LOG_INFO ("P-net log level: %u (DEBUG=0, FATAL=4)\n", LOG_LEVEL); + APP_LOG_INFO ("App log level: %u (DEBUG=0, FATAL=4)\n", APP_LOG_LEVEL); + APP_LOG_INFO ("Max number of ports: %u\n", PNET_MAX_PHYSICAL_PORTS); + APP_LOG_INFO ("Network interfaces: %s\n", app_args.eth_interfaces); + APP_LOG_INFO ("Default station name: %s\n", app_args.station_name); + + app_pnet_cfg_init_default (&pnet_cfg); + + /* Note: station name is defined by app_gsdml.h */ + + strcpy (pnet_cfg.file_directory, APP_DEFAULT_FILE_DIRECTORY); + + ret = app_utils_pnet_cfg_init_netifs ( + app_args.eth_interfaces, + &netif_name_list, + &number_of_ports, + &netif_cfg); + if (ret != 0) + { + return -1; + } + + pnet_cfg.if_cfg = netif_cfg; + pnet_cfg.num_physical_ports = number_of_ports; + + app_utils_print_network_config (&netif_cfg, number_of_ports); + + pnet_cfg.pnal_cfg.bg_worker_thread.prio = APP_BG_WORKER_THREAD_PRIORITY; + pnet_cfg.pnal_cfg.bg_worker_thread.stack_size = + APP_BG_WORKER_THREAD_STACKSIZE; + + /* Initialize profinet stack */ + sample_app = app_init (&pnet_cfg); + if (sample_app == NULL) + { + printf ("Failed to initialize P-Net.\n"); + printf ("Aborting application\n"); + return -1; + } + + /* Start main loop */ + if (app_start (sample_app, RUN_IN_MAIN_THREAD) != 0) + { + printf ("Failed to start\n"); + printf ("Aborting application\n"); + return -1; + } + + app_loop_forever (sample_app); + + return 0; +} diff --git a/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_xmc48relax.c b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_xmc48relax.c new file mode 100644 index 0000000..269ea48 --- /dev/null +++ b/profinet_stack/p-net/src/ports/rt-kernel/sampleapp_xmc48relax.c @@ -0,0 +1,45 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "sampleapp_common.h" +#include +#include + +/************************* Utilities ******************************************/ + +void app_set_led (uint16_t id, bool led_state) +{ + if (id == APP_DATA_LED_ID) + { + gpio_set (GPIO_LED1, led_state ? 1 : 0); /* "LED1" on circuit board */ + } + else if (id == APP_PROFINET_SIGNAL_LED_ID) + { + gpio_set (GPIO_LED2, led_state ? 1 : 0); /* "LED2" on circuit board */ + } +} + +bool app_get_button (uint16_t id) +{ + if (id == 0) + { + return (gpio_get (GPIO_BUTTON1) == 0); + } + else if (id == 1) + { + return (gpio_get (GPIO_BUTTON2) == 0); + } + return false; +} diff --git a/profinet_stack/p-net/test/CMakeLists.txt b/profinet_stack/p-net/test/CMakeLists.txt new file mode 100644 index 0000000..31c2400 --- /dev/null +++ b/profinet_stack/p-net/test/CMakeLists.txt @@ -0,0 +1,135 @@ +#******************************************************************** +# _ _ _ +# _ __ | |_ _ | | __ _ | |__ ___ +# | '__|| __|(_)| | / _` || '_ \ / __| +# | | | |_ _ | || (_| || |_) |\__ \ +# |_| \__|(_)|_| \__,_||_.__/ |___/ +# +# www.rt-labs.com +# Copyright 2018 rt-labs AB, Sweden. +# +# This software is dual-licensed under GPLv3 and a commercial +# license. See the file LICENSE.md distributed with this software for +# full license information. +#*******************************************************************/ + +option(TEST_DEBUG "Add trace output from test case code" OFF) + +set_target_properties (pf_test + PROPERTIES + C_STANDARD 99 + CXX_STANDARD 11 + ) + +target_sources(pf_test PRIVATE + # Unit tests + test_alarm.cpp + test_block_reader.cpp + test_cmdev.cpp + test_cmdmc.cpp + test_cmina.cpp + test_cmio.cpp + test_cmpbe.cpp + test_cmrdr.cpp + test_cmrpc.cpp + test_cmrs.cpp + test_cmsm.cpp + test_cmsu.cpp + test_cmwdr.cpp + test_cpm.cpp + test_dcp.cpp + test_diag.cpp + test_eth.cpp + test_file.cpp + test_fspm.cpp + test_lldp.cpp + test_pnetapi.cpp + test_port.cpp + test_ppm.cpp + test_ptcp.cpp + test_scheduler.cpp + $<$:${PROFINET_SOURCE_DIR}/test/test_snmp.cpp> + utils_for_testing.h + utils_for_testing.cpp + + # Mocks + mocks.h + mocks.cpp + + # Testrunner + pf_test.cpp + ) + +# Rebuild units to be tested with UNIT_TEST flag set. This is used to +# mock external dependencies. +target_sources(pf_test PRIVATE + # Units to be tested + ${PROFINET_SOURCE_DIR}/src/device/pf_block_reader.c + ${PROFINET_SOURCE_DIR}/src/device/pf_block_writer.c + ${PROFINET_SOURCE_DIR}/src/device/pf_fspm.c + ${PROFINET_SOURCE_DIR}/src/device/pf_diag.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmdev.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmdmc.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmina.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmio.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmpbe.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmrdr.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmrpc.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmrpc_helpers.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmrs.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmsm.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmsu.c + ${PROFINET_SOURCE_DIR}/src/device/pf_cmwrr.c + ${PROFINET_SOURCE_DIR}/src/device/pf_pdport.c + ${PROFINET_SOURCE_DIR}/src/device/pnet_api.c + ${PROFINET_SOURCE_DIR}/src/common/pf_alarm.c + ${PROFINET_SOURCE_DIR}/src/common/pf_bg_worker.c + ${PROFINET_SOURCE_DIR}/src/common/pf_cpm_driver_sw.c + ${PROFINET_SOURCE_DIR}/src/common/pf_dcp.c + ${PROFINET_SOURCE_DIR}/src/common/pf_eth.c + ${PROFINET_SOURCE_DIR}/src/common/pf_lldp.c + ${PROFINET_SOURCE_DIR}/src/common/pf_file.c + ${PROFINET_SOURCE_DIR}/src/common/pf_ppm.c + ${PROFINET_SOURCE_DIR}/src/common/pf_ppm_driver_sw.c + ${PROFINET_SOURCE_DIR}/src/common/pf_ptcp.c + ${PROFINET_SOURCE_DIR}/src/common/pf_scheduler.c + $<$:${PROFINET_SOURCE_DIR}/src/common/pf_snmp.c> + ${PROFINET_SOURCE_DIR}/src/common/pf_udp.c + ) + +# Set logging from the stack in tests +get_target_property(PROFINET_OPTIONS profinet COMPILE_OPTIONS) +target_compile_options(pf_test PRIVATE + -DUNIT_TEST + $<$:-DTEST_DEBUG> + $<$>:-DPF_ETH_LOG=LOG_STATE_OFF> + $<$>:-DPF_CPM_LOG=LOG_STATE_OFF> + $<$>:-DPF_PPM_LOG=LOG_STATE_OFF> + $<$>:-DPF_DCP_LOG=LOG_STATE_OFF> + $<$>:-DPF_RPC_LOG=LOG_STATE_OFF> + $<$>:-DPF_ALARM_LOG=LOG_STATE_OFF> + $<$>:-DPF_AL_BUF_LOG=LOG_STATE_OFF> + $<$>:-DPF_LLDP_LOG=LOG_STATE_OFF> + $<$>:-DPF_SNMP_LOG=LOG_STATE_OFF> + $<$>:-DPF_PNAL_LOG=LOG_STATE_OFF> + $<$>:-DPNET_LOG=LOG_STATE_OFF> + ${PROFINET_OPTIONS} + ) + +target_include_directories(pf_test + PRIVATE + ${PROFINET_SOURCE_DIR}/src + ${PROFINET_SOURCE_DIR}/src/common + ${PROFINET_SOURCE_DIR}/src/device + ${PROFINET_BINARY_DIR}/src + ) + +# Link against profinet target to get the cmake properties for that +# target, such as include paths and libraries. +target_link_libraries(pf_test + PRIVATE + profinet + ) + +# No need for gmock +set(BUILD_GMOCK OFF CACHE BOOL "") diff --git a/profinet_stack/p-net/test/mocks.cpp b/profinet_stack/p-net/test/mocks.cpp new file mode 100644 index 0000000..0097789 --- /dev/null +++ b/profinet_stack/p-net/test/mocks.cpp @@ -0,0 +1,512 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +// TODO: Must be included first due to redefinition of atomics in +// pf_types.h +#include + +#include "mocks.h" + +#include + +struct pnal_eth_handle +{ + const char * if_name; + pnal_eth_callback_t * callback; + void * arg; +}; + +uint8_t pnet_log_level; + +os_mutex_t * mock_mutex; +mock_os_data_t mock_os_data; +mock_lldp_data_t mock_lldp_data; +mock_file_data_t mock_file_data; +mock_fspm_data_t mock_fspm_data; +pnal_eth_handle_t mock_eth_handle; + +void mock_clear (void) +{ + memset (&mock_os_data, 0, sizeof (mock_os_data)); + memset (&mock_lldp_data, 0, sizeof (mock_lldp_data)); + memset (&mock_file_data, 0, sizeof (mock_file_data)); + memset (&mock_fspm_data, 0, sizeof (mock_fspm_data)); + mock_os_data.eth_status[1].operational_mau_type = + PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + mock_os_data.eth_status[1].running = true; +} + +void mock_init (void) +{ + mock_mutex = os_mutex_create(); + mock_clear(); +} + +uint32_t mock_os_get_current_time_us (void) +{ + return mock_os_data.current_time_us; +} + +uint32_t mock_pnal_get_system_uptime_10ms (void) +{ + return mock_os_data.system_uptime_10ms; +} + +pnal_eth_handle_t * mock_pnal_eth_init ( + const char * if_name, + const pnal_cfg_t * pnal_cfg, + pnal_eth_callback_t * callback, + void * arg) +{ + pnal_eth_handle_t * handle; + + handle = &mock_eth_handle; + + handle->if_name = if_name; + handle->arg = arg; + handle->callback = callback; + + mock_os_data.eth_if_handle = handle; + + return handle; +} + +int mock_pnal_eth_get_status ( + const char * interface_name, + pnal_eth_status_t * status) +{ + /* TODO get loc_port_num from interface_name */ + *status = mock_os_data.eth_status[1]; + + return 0; +} + +int mock_pnal_get_port_statistics ( + const char * interface_name, + pnal_port_stats_t * port_stats) +{ + /* TODO get loc_port_num from interface_name */ + *port_stats = mock_os_data.port_statistics[1]; + + return 0; +} + +int mock_pnal_get_hostname (char * hostname) +{ + hostname[0] = 'A'; + hostname[1] = '\0'; + + return 0; +} + +int mock_pnal_get_ip_suite ( + const char * interface_name, + pnal_ipaddr_t * p_ipaddr, + pnal_ipaddr_t * p_netmask, + pnal_ipaddr_t * p_gw, + const char ** p_device_name) +{ + return 0; +} + +int mock_pnal_set_ip_suite ( + const char * interface_name, + const pnal_ipaddr_t * p_ipaddr, + const pnal_ipaddr_t * p_netmask, + const pnal_ipaddr_t * p_gw, + const char * hostname, + bool permanent) +{ + mock_os_data.set_ip_suite_count++; + return 0; +} + +int mock_pnal_eth_send (pnal_eth_handle_t * handle, pnal_buf_t * p_buf) +{ + memcpy (mock_os_data.eth_send_copy, p_buf->payload, p_buf->len); + mock_os_data.eth_send_len = p_buf->len; + mock_os_data.eth_send_count++; + + return p_buf->len; +} + +int mock_pnal_get_macaddress ( + const char * interface_name, + pnal_ethaddr_t * p_mac) +{ + p_mac->addr[0] = 0x12; + p_mac->addr[1] = 0x34; + p_mac->addr[2] = 0x00; + p_mac->addr[3] = 0x78; + p_mac->addr[4] = 0x90; + p_mac->addr[5] = 0xab; + return 0; +} + +int mock_pnal_udp_open (pnal_ipaddr_t addr, pnal_ipport_t port) +{ + int ret = 2; + return ret; +} + +int mock_pnal_udp_sendto ( + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size) +{ + mock_os_data.udp_sendto_len = size; + mock_os_data.udp_sendto_count++; + + return size; +} + +void mock_set_pnal_udp_recvfrom_buffer (uint8_t * p_src, uint16_t len) +{ + os_mutex_lock (mock_mutex); + + memcpy (mock_os_data.udp_recvfrom_buffer, p_src, len); + mock_os_data.udp_recvfrom_length = len; + mock_os_data.udp_recvfrom_count++; + + os_mutex_unlock (mock_mutex); +} + +int mock_pnal_udp_recvfrom ( + uint32_t id, + pnal_ipaddr_t * p_dst_addr, + pnal_ipport_t * p_dst_port, + uint8_t * data, + int size) +{ + int len; + + os_mutex_lock (mock_mutex); + + memcpy ( + data, + mock_os_data.udp_recvfrom_buffer, + mock_os_data.udp_recvfrom_length); + len = mock_os_data.udp_recvfrom_length; + mock_os_data.udp_recvfrom_length = 0; + + os_mutex_unlock (mock_mutex); + + return len; +} + +void mock_pnal_udp_close (uint32_t id) +{ +} + +int mock_pnal_get_interface_index (const char * interface_name) +{ + return mock_os_data.interface_index; +} + +int mock_pnal_save_file ( + const char * fullpath, + const void * object_1, + size_t size_1, + const void * object_2, + size_t size_2) +{ + int pos = 0; + + if (size_1 + size_2 > sizeof (mock_os_data.file_content)) + { + return -1; + } + if (strlen (fullpath) > sizeof (mock_os_data.file_fullpath)) + { + return -1; + } + + strcpy (mock_os_data.file_fullpath, fullpath); + mock_os_data.file_size = size_1 + size_2; + + if (size_1 > 0) + { + memcpy (&mock_os_data.file_content[pos], object_1, size_1); + pos += size_1; + } + if (size_2 > 0) + { + memcpy (&mock_os_data.file_content[pos], object_2, size_2); + } + + return 0; +} + +void mock_pnal_clear_file (const char * fullpath) +{ + if (strcmp (fullpath, mock_os_data.file_fullpath) == 0) + { + strcpy (mock_os_data.file_fullpath, ""); + mock_os_data.file_size = 0; + } +} + +int mock_pnal_load_file ( + const char * fullpath, + void * object_1, + size_t size_1, + void * object_2, + size_t size_2) +{ + int pos = 0; + + if (size_1 + size_2 > mock_os_data.file_size) + { + return -1; + } + + if (strcmp (fullpath, mock_os_data.file_fullpath) != 0) + { + return -1; + } + + if (size_1 > 0) + { + memcpy (object_1, &mock_os_data.file_content[pos], size_1); + pos += size_1; + } + if (size_2 > 0) + { + memcpy (object_2, &mock_os_data.file_content[pos], size_2); + } + + return 0; +} + +int mock_pf_alarm_send_diagnosis ( + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_diag_item_t * p_item) +{ + return 0; +} + +void mock_pf_generate_uuid ( + uint32_t timestamp, + uint32_t session_number, + pnet_ethaddr_t mac_address, + pf_uuid_t * p_uuid) +{ + p_uuid->data1 = session_number; + p_uuid->data2 = 0x1234; + p_uuid->data3 = 0x5678; + p_uuid->data4[0] = 0x01; + p_uuid->data4[1] = 0x02; + p_uuid->data4[2] = 0x03; + p_uuid->data4[3] = 0x04; + p_uuid->data4[4] = 0x05; + p_uuid->data4[5] = 0x06; + p_uuid->data4[6] = 0x07; + p_uuid->data4[7] = 0x08; +} + +void mock_pf_lldp_get_management_address ( + pnet_t * net, + pf_lldp_management_address_t * p_man_address) +{ + *p_man_address = mock_lldp_data.management_address; +} + +int mock_pf_lldp_get_peer_management_address ( + pnet_t * net, + int loc_port_num, + pf_lldp_management_address_t * p_man_address) +{ + *p_man_address = mock_lldp_data.peer_management_address; + return mock_lldp_data.error; +} + +void mock_pf_lldp_get_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status) +{ + *p_link_status = mock_lldp_data.link_status; +} + +int mock_pf_lldp_get_peer_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status) +{ + *p_link_status = mock_lldp_data.peer_link_status; + return mock_lldp_data.error; +} + +int mock_pnal_snmp_init (pnet_t * pnet, const pnal_cfg_t * pnal_cfg) +{ + return 0; +} + +int mock_pf_file_save_if_modified ( + const char * directory, + const char * filename, + const void * p_object, + void * p_tempobject, + size_t size) +{ + int result; + + /* Exit if any argument is invalid */ + if (size > sizeof (mock_file_data.object)) + { + return -1; + } + if (filename == NULL || p_object == NULL || p_tempobject == NULL) + { + return -1; + } + if (strlen (filename) >= sizeof (mock_file_data.filename)) + { + return -1; + } + + /* Inject error? */ + if (mock_file_data.is_save_failing) + { + return -1; + } + + /* Calculate return value */ + if (strcmp (filename, mock_file_data.filename) != 0) + { + result = 2; /* First saving of file */ + } + else if ( + size == mock_file_data.size && + memcmp (p_object, mock_file_data.object, size) == 0) + { + result = 0; /* No update required */ + } + else + { + result = 1; /* Updating file */ + } + + /* Store to fake file */ + strcpy (mock_file_data.filename, filename); + mock_file_data.size = size; + memcpy (mock_file_data.object, p_object, size); + return result; +} + +int mock_pf_file_save ( + const char * directory, + const char * filename, + const void * p_object, + size_t size) +{ + /* Exit if any argument is invalid */ + if (size > sizeof (mock_file_data.object)) + { + return -1; + } + if (filename == NULL || p_object == NULL) + { + return -1; + } + if (strlen (filename) >= sizeof (mock_file_data.filename)) + { + return -1; + } + + /* Inject error? */ + if (mock_file_data.is_save_failing) + { + return -1; + } + + /* Store to fake file */ + strcpy (mock_file_data.filename, filename); + mock_file_data.size = size; + memcpy (mock_file_data.object, p_object, size); + return 0; +} + +void mock_pf_file_clear (const char * directory, const char * filename) +{ + if (strcmp (filename, mock_file_data.filename) == 0) + { + strcpy (mock_file_data.filename, ""); + mock_file_data.size = 0; + } +} + +int mock_pf_file_load ( + const char * directory, + const char * filename, + void * p_object, + size_t size) +{ + /* Exit if any argument is invalid */ + if (size != mock_file_data.size) + { + return -1; + } + if (filename == NULL || p_object == NULL) + { + return -1; + } + if (strcmp (filename, mock_file_data.filename) != 0) + { + return -1; + } + + /* Inject error? */ + if (mock_file_data.is_load_failing) + { + return -1; + } + + memcpy (p_object, mock_file_data.object, size); + return 0; +} + +void mock_pf_fspm_get_im_location (pnet_t * net, char * location) +{ + snprintf ( + location, + PNET_LOCATION_MAX_SIZE, + "%-22s", + mock_fspm_data.im_location); +} + +void mock_pf_fspm_save_im_location (pnet_t * net, const char * location) +{ + snprintf ( + mock_fspm_data.im_location, + sizeof (mock_fspm_data.im_location), + "%-22s", + location); +} + +void mock_pf_bg_worker_init (pnet_t * net) +{ + return; +} + +int mock_pf_bg_worker_start_job (pnet_t * net, pf_bg_job_t job_id) +{ + return 0; +} diff --git a/profinet_stack/p-net/test/mocks.h b/profinet_stack/p-net/test/mocks.h new file mode 100644 index 0000000..403f59c --- /dev/null +++ b/profinet_stack/p-net/test/mocks.h @@ -0,0 +1,224 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef MOCKS_H +#define MOCKS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "options.h" +#include "pnet_api.h" +#include "pf_includes.h" +#include "osal.h" + +typedef struct mock_os_data_obj +{ + uint8_t eth_send_copy[PF_FRAME_BUFFER_SIZE]; + uint16_t eth_send_len; + uint16_t eth_send_count; + + /* Per port Ethernet link status. + * Note that port numbers start at 1. To simplify test cases, we add a + * dummy array element at index 0. + */ + pnal_eth_status_t eth_status[PNET_MAX_PHYSICAL_PORTS + 1]; + pnal_port_stats_t port_statistics[PNET_MAX_PHYSICAL_PORTS + 1]; + + uint16_t udp_sendto_len; + uint16_t udp_sendto_count; + + uint8_t udp_recvfrom_buffer[PF_FRAME_BUFFER_SIZE]; + uint16_t udp_recvfrom_length; + uint16_t udp_recvfrom_count; + + uint16_t set_ip_suite_count; + + uint32_t current_time_us; + uint32_t system_uptime_10ms; + + int interface_index; + pnal_eth_handle_t * eth_if_handle; + + char file_fullpath[100]; /* Full file path at latest save operation */ + uint16_t file_size; + uint8_t file_content[2000]; + +} mock_os_data_t; + +typedef struct mock_lldp_data +{ + pf_lldp_management_address_t management_address; + pf_lldp_management_address_t peer_management_address; + pf_lldp_link_status_t link_status; + pf_lldp_link_status_t peer_link_status; + int error; + +} mock_lldp_data_t; + +typedef struct mock_file_data +{ + char filename[PNET_MAX_FILENAME_SIZE]; + uint8_t object[300]; + size_t size; + bool is_save_failing; /* Used for injecting error */ + bool is_load_failing; /* Used for injecting error */ + +} mock_file_data_t; + +typedef struct mock_fspm_data +{ + char im_location[PNET_LOCATION_MAX_SIZE]; +} mock_fspm_data_t; + +extern mock_os_data_t mock_os_data; +extern mock_lldp_data_t mock_lldp_data; +extern mock_file_data_t mock_file_data; +extern mock_fspm_data_t mock_fspm_data; + +uint32_t mock_os_get_current_time_us (void); +uint32_t mock_pnal_get_system_uptime_10ms (void); + +void mock_init (void); +void mock_clear (void); +void mock_set_pnal_udp_recvfrom_buffer (uint8_t * p_src, uint16_t len); + +pnal_eth_handle_t * mock_pnal_eth_init ( + const char * if_name, + const pnal_cfg_t * pnal_cfg, + pnal_eth_callback_t * callback, + void * arg); +int mock_pnal_eth_send (pnal_eth_handle_t * handle, pnal_buf_t * buf); +int mock_pnal_get_macaddress ( + const char * interface_name, + pnal_ethaddr_t * p_mac); +int mock_pnal_eth_get_status ( + const char * interface_name, + pnal_eth_status_t * status); +int mock_pnal_get_port_statistics ( + const char * interface_name, + pnal_port_stats_t * port_stats); +int mock_pnal_get_hostname (char * hostname); +void mock_os_cpy_mac_addr (uint8_t * mac_addr); +int mock_pnal_udp_open (pnal_ipaddr_t addr, pnal_ipport_t port); +int mock_pnal_udp_sendto ( + uint32_t id, + pnal_ipaddr_t dst_addr, + pnal_ipport_t dst_port, + const uint8_t * data, + int size); +int mock_pnal_udp_recvfrom ( + uint32_t id, + pnal_ipaddr_t * dst_addr, + pnal_ipport_t * dst_port, + uint8_t * data, + int size); +void mock_pnal_udp_close (uint32_t id); +int mock_pnal_set_ip_suite ( + const char * interface_name, + const pnal_ipaddr_t * p_ipaddr, + const pnal_ipaddr_t * p_netmask, + const pnal_ipaddr_t * p_gw, + const char * hostname, + bool permanent); +int mock_pnal_get_interface_index (const char * interface_name); + +int mock_pnal_save_file ( + const char * fullpath, + const void * object_1, + size_t size_1, + const void * object_2, + size_t size_2); + +void mock_pnal_clear_file (const char * fullpath); + +int mock_pnal_load_file ( + const char * fullpath, + void * object_1, + size_t size_1, + void * object_2, + size_t size_2); + +int mock_pf_alarm_send_diagnosis ( + pf_ar_t * p_ar, + uint32_t api_id, + uint16_t slot_nbr, + uint16_t subslot_nbr, + pf_diag_item_t * p_item); + +void mock_pf_generate_uuid ( + uint32_t timestamp, + uint32_t session_number, + pnet_ethaddr_t mac_address, + pf_uuid_t * p_uuid); + +void mock_pf_lldp_get_management_address ( + pnet_t * net, + pf_lldp_management_address_t * p_man_address); + +int mock_pf_lldp_get_peer_management_address ( + pnet_t * net, + int loc_port_num, + pf_lldp_management_address_t * p_man_address); + +void mock_pf_lldp_get_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status); + +int mock_pf_lldp_get_peer_link_status ( + pnet_t * net, + int loc_port_num, + pf_lldp_link_status_t * p_link_status); + +int mock_pnal_snmp_init (pnet_t * pnet, const pnal_cfg_t * pnal_cfg); + +int mock_pf_file_save_if_modified ( + const char * directory, + const char * filename, + const void * p_object, + void * p_tempobject, + size_t size); + +int mock_pf_file_save ( + const char * directory, + const char * filename, + const void * p_object, + size_t size); + +void mock_pf_file_clear (const char * directory, const char * filename); + +int mock_pf_file_load ( + const char * directory, + const char * filename, + void * p_object, + size_t size); + +void mock_pf_fspm_get_im_location (pnet_t * net, char * location); + +void mock_pf_fspm_save_im_location (pnet_t * net, const char * location); + +void mock_pf_bg_worker_init (pnet_t * net); + +int mock_pf_bg_worker_start_job (pnet_t * net, pf_bg_job_t job_id); + +#ifdef __cplusplus +} +#endif + +#endif /* MOCKS_H */ diff --git a/profinet_stack/p-net/test/pf_test.cpp b/profinet_stack/p-net/test/pf_test.cpp new file mode 100644 index 0000000..1320281 --- /dev/null +++ b/profinet_stack/p-net/test/pf_test.cpp @@ -0,0 +1,28 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include +#include "osal.h" + +OS_MAIN (int argc, char * argv[]) +{ + if (argc > 0) + ::testing::InitGoogleTest (&argc, argv); + else + ::testing::InitGoogleTest(); + + int result = RUN_ALL_TESTS(); + return result; +} diff --git a/profinet_stack/p-net/test/test_alarm.cpp b/profinet_stack/p-net/test/test_alarm.cpp new file mode 100644 index 0000000..e9c670d --- /dev/null +++ b/profinet_stack/p-net/test/test_alarm.cpp @@ -0,0 +1,488 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class AlarmTest : public PnetIntegrationTest +{ +}; + +class AlarmUnitTest : public PnetUnitTest +{ +}; + +TEST_F (AlarmUnitTest, AlarmCheckAddDiagItemToSummary) +{ + pnet_alarm_spec_t alarm_specifier; + uint32_t maint_status = 0; + pf_diag_item_t diag_item; + pf_subslot_t subslot; + pf_ar_t ar; + + /* Prepare default input data */ + memset (&ar, 0, sizeof (ar)); + memset (&subslot, 0, sizeof (subslot)); + subslot.p_ar = &ar; + + memset (&diag_item, 0, sizeof (diag_item)); + diag_item.usi = PF_USI_EXTENDED_CHANNEL_DIAGNOSIS; + PF_DIAG_CH_PROP_SPEC_SET ( + diag_item.fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_APPEARS); + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_FAULT); + + /* Happy case */ + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, 0UL); + + /* Other AR */ + subslot.p_ar = NULL; + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, 0UL); + subslot.p_ar = &ar; + + /* Manufacturer specific (in USI format) */ + diag_item.usi = TEST_DIAG_USI_CUSTOM; + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_FALSE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, 0UL); + diag_item.usi = PF_USI_EXTENDED_CHANNEL_DIAGNOSIS; + + /* Disappearing diagnosis */ + PF_DIAG_CH_PROP_SPEC_SET ( + diag_item.fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_DISAPPEARS); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_FALSE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, 0UL); + PF_DIAG_CH_PROP_SPEC_SET ( + diag_item.fmt.std.ch_properties, + PF_DIAG_CH_PROP_SPEC_APPEARS); + + /* Maintenance required */ + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_REQUIRED); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, BIT (0)); + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_FAULT); + + /* Maintenance required, via qualified channel diagnosis (qual 7..16) */ + diag_item.usi = PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS; + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_QUALIFIED); + diag_item.fmt.std.qual_ch_qualifier = BIT (7); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, (BIT (7) | BIT (0))); + + diag_item.fmt.std.qual_ch_qualifier = BIT (16); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, (BIT (16) | BIT (0))); + diag_item.usi = PF_USI_EXTENDED_CHANNEL_DIAGNOSIS; + diag_item.fmt.std.qual_ch_qualifier = 0; + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_FAULT); + + /* Maintenance demanded */ + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_DEMANDED); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, BIT (1)); + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_FAULT); + + /* Maintenance demanded, via qualified channel diagnosis (qual 17..26) */ + diag_item.usi = PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS; + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_QUALIFIED); + diag_item.fmt.std.qual_ch_qualifier = BIT (17); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, (BIT (17) | BIT (1))); + + diag_item.fmt.std.qual_ch_qualifier = BIT (26); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, (BIT (26) | BIT (1))); + diag_item.usi = PF_USI_EXTENDED_CHANNEL_DIAGNOSIS; + diag_item.fmt.std.qual_ch_qualifier = 0; + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_FAULT); + + /* Fault, via qualified channel diagnosis (qual 27..31) */ + diag_item.usi = PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS; + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_QUALIFIED); + diag_item.fmt.std.qual_ch_qualifier = BIT (27); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, BIT (27)); + + diag_item.fmt.std.qual_ch_qualifier = BIT (31); + + memset (&alarm_specifier, 0, sizeof (alarm_specifier)); + maint_status = 0; + pf_alarm_add_diag_item_to_summary ( + &ar, + &subslot, + &diag_item, + &alarm_specifier, + &maint_status); + EXPECT_TRUE ((bool)alarm_specifier.channel_diagnosis); + EXPECT_FALSE ((bool)alarm_specifier.manufacturer_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.submodule_diagnosis); + EXPECT_TRUE ((bool)alarm_specifier.ar_diagnosis); + EXPECT_EQ (maint_status, BIT (31)); + diag_item.usi = PF_USI_EXTENDED_CHANNEL_DIAGNOSIS; + diag_item.fmt.std.qual_ch_qualifier = 0; + PF_DIAG_CH_PROP_MAINT_SET ( + diag_item.fmt.std.ch_properties, + PNET_DIAG_CH_PROP_MAINT_FAULT); +} + +TEST_F (AlarmUnitTest, AlarmCheckSendQueueHandling) +{ + pf_alarm_send_queue_t queue; + pf_alarm_data_t post_message; + pf_alarm_data_t fetch_message; + int ix = 0; + int err = 0; + const uint16_t SEQUENCE_START_NUMBER = 100; + + memset (&post_message, 0, sizeof (post_message)); + memset (&fetch_message, 0, sizeof (fetch_message)); + memset (&queue, 0, sizeof (queue)); + + /* Set up queue */ + EXPECT_FALSE (pf_alarm_queue_is_available(&queue.accountant)); + pf_alarm_queue_mutex_create (&queue.accountant); + pf_alarm_queue_mutex_create (&queue.accountant); /* No-op */ + EXPECT_TRUE (pf_alarm_queue_is_available(&queue.accountant)); + pf_alarm_send_queue_reset (&queue); + EXPECT_EQ (queue.accountant.count, 0); /* Do not bother with mutex + (this test runs in single thread) */ + + /* Fill the queue */ + for (ix = 0; ix < PNET_MAX_ALARMS; ix++) + { + post_message.sequence_number = SEQUENCE_START_NUMBER + ix; + err = pf_alarm_send_queue_post (&queue, &post_message); + EXPECT_EQ (err, 0); + } + EXPECT_EQ (queue.accountant.count, PNET_MAX_ALARMS); + + /* Add to full queue */ + post_message.sequence_number += 1; + err = pf_alarm_send_queue_post (&queue, &post_message); + EXPECT_EQ (err, -1); + EXPECT_EQ (queue.accountant.count, PNET_MAX_ALARMS); + + /* Fetch from the queue */ + for (ix = 0; ix < PNET_MAX_ALARMS; ix++) + { + err = pf_alarm_send_queue_fetch (&queue, &fetch_message); + EXPECT_EQ (err, 0); + EXPECT_EQ (fetch_message.sequence_number, SEQUENCE_START_NUMBER + ix); + } + EXPECT_EQ (queue.accountant.count, 0); + + /* Fetch from empty queue */ + err = pf_alarm_send_queue_fetch (&queue, &fetch_message); + EXPECT_EQ (err, -1); + EXPECT_EQ (queue.accountant.count, 0); + + /* Reset the queue */ + err = pf_alarm_send_queue_post (&queue, &post_message); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, 1); + + pf_alarm_send_queue_reset (&queue); + EXPECT_EQ (queue.accountant.count, 0); + + /* Wrap read_index and write_index, by adding and fetching a lot */ + for (ix = 0; ix < PNET_MAX_ALARMS * 5; ix++) + { + post_message.sequence_number = SEQUENCE_START_NUMBER + ix; + err = pf_alarm_send_queue_post (&queue, &post_message); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, 1); + + err = pf_alarm_send_queue_fetch (&queue, &fetch_message); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, 0); + EXPECT_EQ (fetch_message.sequence_number, SEQUENCE_START_NUMBER + ix); + } + + /* Close down queue */ + pf_alarm_send_queue_reset (&queue); + EXPECT_EQ (queue.accountant.count, 0); + EXPECT_TRUE (pf_alarm_queue_is_available(&queue.accountant)); + pf_alarm_queue_mutex_destroy (&queue.accountant); + pf_alarm_queue_mutex_destroy (&queue.accountant); /* Should be safe */ + EXPECT_FALSE (pf_alarm_queue_is_available(&queue.accountant)); + + /* Post to closed queue */ + post_message.sequence_number = SEQUENCE_START_NUMBER; + err = pf_alarm_send_queue_post (&queue, &post_message); + EXPECT_EQ (err, -1); + + /* Fetch from closed queue */ + err = pf_alarm_send_queue_fetch (&queue, &fetch_message); + EXPECT_EQ (err, -1); +} + +TEST_F (AlarmUnitTest, AlarmCheckReceiveQueueHandling) +{ + pf_alarm_receive_queue_t queue; + pf_apmr_msg_t post_frame; + pf_apmr_msg_t fetch_frame; + int ix = 0; + int err = 0; + const uint16_t POSITION_START_NUMBER = 100; + const uintptr_t POINTER_START_NUMBER = 1000; + + memset (&post_frame, 0, sizeof (post_frame)); + memset (&fetch_frame, 0, sizeof (fetch_frame)); + memset (&queue, 0, sizeof (queue)); + + /* Set up queue */ + EXPECT_FALSE (pf_alarm_queue_is_available(&queue.accountant)); + pf_alarm_queue_mutex_create (&queue.accountant); + pf_alarm_queue_mutex_create (&queue.accountant); /* No-op */ + EXPECT_TRUE (pf_alarm_queue_is_available(&queue.accountant)); + pf_alarm_receive_queue_reset (&queue); + EXPECT_EQ (queue.accountant.count, 0); /* Do not bother with mutex + (this test runs in single thread) */ + + /* Fill the queue */ + for (ix = 0; ix < PNET_MAX_ALARMS; ix++) + { + post_frame.frame_id_pos = POSITION_START_NUMBER + ix; + post_frame.p_buf = (pnal_buf_t *)(POINTER_START_NUMBER + ix); + err = pf_alarm_receive_queue_post (&queue, &post_frame); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, ix + 1); + } + EXPECT_EQ (queue.accountant.count, PNET_MAX_ALARMS); + + /* Add to full queue */ + post_frame.frame_id_pos += 1; + post_frame.p_buf -= 1; + err = pf_alarm_receive_queue_post (&queue, &post_frame); + EXPECT_EQ (err, -1); + EXPECT_EQ (queue.accountant.count, PNET_MAX_ALARMS); + + /* Fetch from the queue */ + for (ix = 0; ix < PNET_MAX_ALARMS; ix++) + { + err = pf_alarm_receive_queue_fetch (&queue, &fetch_frame); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, PNET_MAX_ALARMS - ix - 1); + EXPECT_EQ (fetch_frame.frame_id_pos, POSITION_START_NUMBER + ix); + EXPECT_EQ ((uintptr_t)fetch_frame.p_buf, POINTER_START_NUMBER + ix); + } + EXPECT_EQ (queue.accountant.count, 0); + + /* Fetch from empty queue */ + err = pf_alarm_receive_queue_fetch (&queue, &fetch_frame); + EXPECT_EQ (err, -1); + EXPECT_EQ (queue.accountant.count, 0); + + /* Reset the queue (will free allocated buffers) */ + post_frame.frame_id_pos = 42; + post_frame.p_buf = pnal_buf_alloc (PNAL_BUF_MAX_SIZE); + err = pf_alarm_receive_queue_post (&queue, &post_frame); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, 1); + + pf_alarm_receive_queue_reset (&queue); + EXPECT_EQ (queue.accountant.count, 0); + + /* Wrap read_index and write_index, by adding and fetching a lot */ + for (ix = 0; ix < PNET_MAX_ALARMS * 5; ix++) + { + post_frame.frame_id_pos = POSITION_START_NUMBER + ix; + post_frame.p_buf = (pnal_buf_t *)(POINTER_START_NUMBER + ix); + err = pf_alarm_receive_queue_post (&queue, &post_frame); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, 1); + + err = pf_alarm_receive_queue_fetch (&queue, &fetch_frame); + EXPECT_EQ (err, 0); + EXPECT_EQ (queue.accountant.count, 0); + EXPECT_EQ (fetch_frame.frame_id_pos, POSITION_START_NUMBER + ix); + EXPECT_EQ ((uintptr_t)fetch_frame.p_buf, POINTER_START_NUMBER + ix); + } + + /* Close down queue */ + pf_alarm_receive_queue_reset (&queue); + EXPECT_EQ (queue.accountant.count, 0); + EXPECT_TRUE (pf_alarm_queue_is_available(&queue.accountant)); + pf_alarm_queue_mutex_destroy (&queue.accountant); + pf_alarm_queue_mutex_destroy (&queue.accountant); /* Should be safe */ + EXPECT_FALSE (pf_alarm_queue_is_available(&queue.accountant)); + + /* Post to closed queue */ + post_frame.frame_id_pos = POSITION_START_NUMBER; + post_frame.p_buf = (pnal_buf_t *)(POINTER_START_NUMBER); + err = pf_alarm_receive_queue_post (&queue, &post_frame); + EXPECT_EQ (err, -1); + + /* Fetch from closed queue */ + err = pf_alarm_receive_queue_fetch (&queue, &fetch_frame); + EXPECT_EQ (err, -1); +} diff --git a/profinet_stack/p-net/test/test_block_reader.cpp b/profinet_stack/p-net/test/test_block_reader.cpp new file mode 100644 index 0000000..0740475 --- /dev/null +++ b/profinet_stack/p-net/test/test_block_reader.cpp @@ -0,0 +1,113 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_block_reader.h" +#include "pf_includes.h" + +#include + +class BlockReaderUnitTest : public PnetUnitTest +{ +}; + +TEST_F (BlockReaderUnitTest, BlockReaderTestGetBits) +{ + + /* 11111111 11111111 11111111 11111111 */ + EXPECT_EQ (0x00000000ul, pf_get_bits (0xFFFFFFFF, 0, 0)); + EXPECT_EQ (0x00000001ul, pf_get_bits (0xFFFFFFFF, 0, 1)); + EXPECT_EQ (0x00000003ul, pf_get_bits (0xFFFFFFFF, 0, 2)); + EXPECT_EQ (0x00000007ul, pf_get_bits (0xFFFFFFFF, 0, 3)); + EXPECT_EQ (0x0000000Ful, pf_get_bits (0xFFFFFFFF, 0, 4)); + EXPECT_EQ (0x0000001Ful, pf_get_bits (0xFFFFFFFF, 0, 5)); + EXPECT_EQ (0x0000003Ful, pf_get_bits (0xFFFFFFFF, 0, 6)); + EXPECT_EQ (0x0000007Ful, pf_get_bits (0xFFFFFFFF, 0, 7)); + EXPECT_EQ (0x000000FFul, pf_get_bits (0xFFFFFFFF, 0, 8)); + EXPECT_EQ (0x7FFFFFFFul, pf_get_bits (0xFFFFFFFF, 0, 31)); + EXPECT_EQ (0xFFFFFFFFul, pf_get_bits (0xFFFFFFFF, 0, 32)); + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 0, 33)); /* Illegal length */ + + EXPECT_EQ (0x00000000ul, pf_get_bits (0xFFFFFFFF, 1, 0)); + EXPECT_EQ (0x00000001ul, pf_get_bits (0xFFFFFFFF, 1, 1)); + EXPECT_EQ (0x00000003ul, pf_get_bits (0xFFFFFFFF, 1, 2)); + EXPECT_EQ (0x00000007ul, pf_get_bits (0xFFFFFFFF, 1, 3)); + EXPECT_EQ (0x000000FFul, pf_get_bits (0xFFFFFFFF, 1, 8)); + EXPECT_EQ (0x7FFFFFFFul, pf_get_bits (0xFFFFFFFF, 1, 31)); + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 1, 32)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 1, 33)); /* Illegal length */ + + EXPECT_EQ (0x00000000ul, pf_get_bits (0xFFFFFFFF, 31, 0)); + EXPECT_EQ (0x00000001ul, pf_get_bits (0xFFFFFFFF, 31, 1)); + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 31, 2)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 31, 32)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 31, 33)); /* Illegal length */ + + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 32, 0)); /* Illegal position */ + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 32, 1)); /* Illegal position */ + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 32, 32)); /* Illegal position */ + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 32, 33)); /* Illegal position and + illegal length */ + + EXPECT_EQ (0ul, pf_get_bits (0xFFFFFFFF, 0xFF, 0xFF)); /* Illegal position + and illegal length + */ + + /* 00000000 00000000 00000000 00010000 */ + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000010, 0, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000010, 1, 3)); + EXPECT_EQ (0x00000004ul, pf_get_bits (0x00000010, 2, 3)); + EXPECT_EQ (0x00000002ul, pf_get_bits (0x00000010, 3, 3)); + EXPECT_EQ (0x00000001ul, pf_get_bits (0x00000010, 4, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000010, 5, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000010, 28, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000010, 29, 3)); + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 30, 3)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 31, 3)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 32, 3)); /* Illegal position */ + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 33, 3)); /* Illegal position */ + + /* 00000000 00000000 00000000 00000001 */ + EXPECT_EQ (0x00000001ul, pf_get_bits (0x00000001, 0, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000001, 1, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000001, 2, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000001, 3, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x00000001, 4, 3)); + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 30, 3)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 31, 3)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 32, 3)); /* Illegal position */ + EXPECT_EQ (0ul, pf_get_bits (0x00000010, 33, 3)); /* Illegal position */ + + /* 10000000 00000000 00000000 00000000 */ + EXPECT_EQ (0x00000000ul, pf_get_bits (0x80000000, 0, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x80000000, 1, 3)); + EXPECT_EQ (0x00000000ul, pf_get_bits (0x80000000, 28, 3)); + EXPECT_EQ (0x00000004ul, pf_get_bits (0x80000000, 29, 3)); + EXPECT_EQ (0ul, pf_get_bits (0x80000000, 30, 3)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0x80000000, 31, 3)); /* Illegal length and + position combination*/ + EXPECT_EQ (0ul, pf_get_bits (0x80000000, 32, 3)); /* Illegal position */ + EXPECT_EQ (0ul, pf_get_bits (0x80000000, 33, 3)); /* Illegal position */ +} diff --git a/profinet_stack/p-net/test/test_cmdev.cpp b/profinet_stack/p-net/test/test_cmdev.cpp new file mode 100644 index 0000000..44f0d3b --- /dev/null +++ b/profinet_stack/p-net/test/test_cmdev.cpp @@ -0,0 +1,408 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Unit tests of features from the CMDEV state machine. + * + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmdevUnitTest : public PnetUnitTest +{ +}; + +TEST_F (CmdevUnitTest, CmdevCalculateDatadirectionInDescriptor) +{ + pf_data_direction_values_t resulting_direction; + int ret; + + /* Module with no cyclic data */ + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_NO_IO, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (-1, ret); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_NO_IO, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_INPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_NO_IO, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_INPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_NO_IO, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (-1, ret); + + /* Input module */ + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_INPUT, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (-1, ret); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_INPUT, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_INPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_INPUT, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_INPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_INPUT, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (-1, ret); + + /* Output module */ + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_OUTPUT, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_OUTPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_OUTPUT, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (-1, ret); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_OUTPUT, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (-1, ret); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_OUTPUT, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_OUTPUT, resulting_direction); + + /* Input and output module */ + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_IO, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_OUTPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_IO, + PF_DIRECTION_INPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_INPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_IO, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOCS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_INPUT, resulting_direction); + + ret = pf_cmdev_calculate_exp_sub_data_descriptor_direction ( + PNET_DIR_IO, + PF_DIRECTION_OUTPUT, + PF_DEV_STATUS_TYPE_IOPS, + &resulting_direction); + EXPECT_EQ (0, ret); + EXPECT_EQ (PF_DIRECTION_OUTPUT, resulting_direction); +} + +TEST_F (CmdevUnitTest, CmdevCheckZero) +{ + int ret; + int ix; + + /* Shortest buffer */ + uint8_t buffer_1[1] = {0}; + ret = pf_cmdev_check_zero (buffer_1, 1); + EXPECT_EQ (0, ret); + for (ix = 1; ix < 0x100; ix++) + { + buffer_1[0] = ix; + ret = pf_cmdev_check_zero (buffer_1, 1); + EXPECT_EQ (-1, ret); + } + + /* Small buffer */ + uint8_t buffer_5[5] = {0}; + ret = pf_cmdev_check_zero (buffer_5, 5); + EXPECT_EQ (0, ret); + for (ix = 1; ix < 0x100; ix++) + { + buffer_5[3] = ix; + ret = pf_cmdev_check_zero (buffer_5, 5); + EXPECT_EQ (-1, ret); + } + + /* Buffer size larger than uint8_t */ + uint8_t buffer_500[500] = {0}; + ret = pf_cmdev_check_zero (buffer_500, 500); + EXPECT_EQ (0, ret); + for (ix = 1; ix < 0x100; ix++) + { + buffer_500[260] = ix; + ret = pf_cmdev_check_zero (buffer_500, 500); + EXPECT_EQ (-1, ret); + } + memset (buffer_500, 0, sizeof (buffer_500)); + ret = pf_cmdev_check_zero (buffer_500, 500); + EXPECT_EQ (0, ret); + memset (buffer_500, 9, sizeof (buffer_500)); + ret = pf_cmdev_check_zero (buffer_500, 500); + EXPECT_EQ (-1, ret); + + /* Pass in length zero */ + ret = pf_cmdev_check_zero (buffer_500, 0); + EXPECT_EQ (0, ret); +} + +TEST_F (CmdevUnitTest, CmdevCheckVisibleString) +{ + int ix; + int ret; + + ret = pf_cmdev_check_visible_string (""); + EXPECT_EQ (-1, ret); + + ret = pf_cmdev_check_visible_string ("abc"); + EXPECT_EQ (0, ret); + + /* Short string */ + char buffer_20[20] = {0}; + ret = pf_cmdev_check_visible_string (buffer_20); + EXPECT_EQ (-1, ret); + + buffer_20[0] = "A"[0]; + ret = pf_cmdev_check_visible_string (buffer_20); + EXPECT_EQ (0, ret); + + memset (buffer_20, "A"[0], sizeof (buffer_20) - 1); + ret = pf_cmdev_check_visible_string (buffer_20); + EXPECT_EQ (0, ret); + + /* Test all character values */ + for (ix = 0x01; ix <= 0x1F; ix++) + { + buffer_20[10] = ix; + ret = pf_cmdev_check_visible_string (buffer_20); + EXPECT_EQ (-1, ret); + } + for (ix = 0x20; ix <= 0x7E; ix++) + { + buffer_20[10] = ix; + ret = pf_cmdev_check_visible_string (buffer_20); + EXPECT_EQ (0, ret); + } + for (ix = 0x7F; ix <= 0xFF; ix++) + { + buffer_20[10] = ix; + ret = pf_cmdev_check_visible_string (buffer_20); + EXPECT_EQ (-1, ret); + } + + /* String length larger than uint8_t */ + char buffer_500[500] = {0}; + memset (buffer_500, "A"[0], sizeof (buffer_500) - 1); + + ret = pf_cmdev_check_visible_string (buffer_500); + EXPECT_EQ (0, ret); + + buffer_500[300] = 0x07; + ret = pf_cmdev_check_visible_string (buffer_500); + EXPECT_EQ (-1, ret); +} + +TEST_F (CmdevUnitTest, CmdevCheckNoStraddle) +{ + uint16_t start_a; + uint16_t length_a; + uint16_t start_b; + uint16_t length_b; + + /* x Area A */ + /* 0 1 2 3 4 5 6 7 8 9 */ + /* x Area B, initial position */ + start_a = 0; + length_a = 1; + length_b = 1; + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 0, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 1, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 2, length_b)); + + /* Area A */ + /* 0 1 2 3 4 5 6 7 8 9 */ + /* x Area B, initial position */ + start_a = 0; + length_a = 0; + length_b = 1; + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 0, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 1, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 2, length_b)); + + /* Area A */ + /* 0 1 2 3 4 5 6 7 8 9 */ + /* Area B, initial position */ + start_a = 0; + length_a = 0; + length_b = 0; + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 0, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 1, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 2, length_b)); + + /* x Area A */ + /* 0 1 2 3 4 5 6 7 8 9 */ + /* x Area B, initial position */ + start_a = 2; + length_a = 1; + length_b = 1; + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 0, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 1, length_b)); + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 2, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 3, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 4, length_b)); + + /* x x Area A */ + /* 0 1 2 3 4 5 6 7 8 9 */ + /* x Area B, initial position */ + start_a = 2; + length_a = 2; + length_b = 1; + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 0, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 1, length_b)); + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 2, length_b)); + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 3, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 4, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 5, length_b)); + + /* x x x Area A */ + /* 0 1 2 3 4 5 6 7 8 9 */ + /* x x Area B, initial position */ + start_a = 4; + length_a = 3; + length_b = 2; + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 0, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 1, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 2, length_b)); + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 3, length_b)); + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 4, length_b)); + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 5, length_b)); + EXPECT_EQ (-1, pf_cmdev_check_no_straddle (start_a, length_a, 6, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 7, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 8, length_b)); + EXPECT_EQ (0, pf_cmdev_check_no_straddle (start_a, length_a, 9, length_b)); + + /* Values larger than uint8_t */ + start_a = 1000; + length_a = 500; + length_b = 300; + for (start_b = 0; start_b <= 700; start_b++) + { + EXPECT_EQ ( + 0, + pf_cmdev_check_no_straddle (start_a, length_a, start_b, length_b)); + EXPECT_EQ ( + 0, + pf_cmdev_check_no_straddle (start_b, length_b, start_a, length_a)); + } + for (start_b = 701; start_b <= 1499; start_b++) + { + EXPECT_EQ ( + -1, + pf_cmdev_check_no_straddle (start_a, length_a, start_b, length_b)); + EXPECT_EQ ( + -1, + pf_cmdev_check_no_straddle (start_b, length_b, start_a, length_a)); + } + for (start_b = 1500; start_b < 0xFFFF; start_b++) + { + EXPECT_EQ ( + 0, + pf_cmdev_check_no_straddle (start_a, length_a, start_b, length_b)); + EXPECT_EQ ( + 0, + pf_cmdev_check_no_straddle (start_b, length_b, start_a, length_a)); + } + EXPECT_EQ ( + 0, + pf_cmdev_check_no_straddle (start_a, length_a, 0xFFFF, length_b)); + EXPECT_EQ ( + 0, + pf_cmdev_check_no_straddle (0xFFFF, length_b, start_a, length_a)); +} + +TEST_F (CmdevUnitTest, CmdevCheckArType) +{ + int ret; + + for (uint16_t ar_type = 0; ar_type < 0xFF; ar_type++) + { + ret = pf_cmdev_check_ar_type (ar_type); + + if (ar_type == PF_ART_IOCAR_SINGLE) + { + EXPECT_EQ (0, ret); + } + else + { + EXPECT_EQ (-1, ret); + } + } + + ret = pf_cmdev_check_ar_type (0xFFFF); + EXPECT_EQ (-1, ret); +} diff --git a/profinet_stack/p-net/test/test_cmdmc.cpp b/profinet_stack/p-net/test/test_cmdmc.cpp new file mode 100644 index 0000000..8d46ab1 --- /dev/null +++ b/profinet_stack/p-net/test/test_cmdmc.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmdmcTest : public PnetIntegrationTest +{ +}; + +TEST_F (CmdmcTest, CmdmcRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_cmina.cpp b/profinet_stack/p-net/test/test_cmina.cpp new file mode 100644 index 0000000..4310ed1 --- /dev/null +++ b/profinet_stack/p-net/test/test_cmina.cpp @@ -0,0 +1,265 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CminaUnitTest : public PnetUnitTest +{ +}; + +TEST_F (CminaUnitTest, CminaCheckIsStationNameValid) +{ + EXPECT_TRUE (pf_cmina_is_stationname_valid ("", 0)); + EXPECT_TRUE (pf_cmina_is_stationname_valid ("abc", 3)); + EXPECT_TRUE (pf_cmina_is_stationname_valid ("a1.2.3.4", 8)); + EXPECT_TRUE ( + pf_cmina_is_stationname_valid ("device-1.machine-1.plant-1.vendor", 33)); + EXPECT_TRUE (pf_cmina_is_stationname_valid ( + "xn--mhle1-kva.xn--lmhle1-vxa4c.plant.com", + 40)); + + EXPECT_TRUE (pf_cmina_is_stationname_valid ("port-xyz", 8)); + EXPECT_TRUE (pf_cmina_is_stationname_valid ("port-xyz-abcde", 14)); + EXPECT_TRUE (pf_cmina_is_stationname_valid ( + "abcdefghijklmnopqrstuvwxyz-abcdefghijklmnopqrstuvwxyz1234567890", + 63)); + EXPECT_TRUE (pf_cmina_is_stationname_valid ( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb." + "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc." + "dddddddddddddddddddddddddddddddddddddddddddddddd", + 240)); + + EXPECT_FALSE (pf_cmina_is_stationname_valid ( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb." + "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc." + "dddddddddddddddddddddddddddddddddddddddddddddddd", + 240)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb." + "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc." + "ddddddddddddddddddddddddddddddddddddddddddddddddd", + 241)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("name_1", 6)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("name/1", 6)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("-name", 5)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("name-", 5)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("name.-name", 10)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("name.name-", 10)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ( + "looooooooooooooooooooooooooooooooooooooooooooooooooooooonglabelname", + 67)); + + EXPECT_FALSE (pf_cmina_is_stationname_valid ("1.2.3.4", 7)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("port-123", 8)); + EXPECT_FALSE (pf_cmina_is_stationname_valid ("port-123-98765", 14)); +} + +TEST_F (CminaUnitTest, CminaCheckIsNetmaskValid) +{ + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (0, 0, 0, 0))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 255))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 254))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 252))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 248))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 240))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 224))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 192))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 128))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 255, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 254, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 252, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 248, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 240, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 224, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 192, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 128, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 255, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 254, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 252, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 248, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 240, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 224, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 192, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 128, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 000, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (254, 000, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (252, 000, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (248, 000, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (240, 000, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (224, 000, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (192, 000, 000, 000))); + EXPECT_TRUE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (128, 000, 000, 000))); + + EXPECT_FALSE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 0, 255, 255))); + EXPECT_FALSE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (0, 255, 255, 255))); + EXPECT_FALSE (pf_cmina_is_netmask_valid (PNAL_MAKEU32 (255, 254, 255, 0))); +} + +TEST_F (CminaUnitTest, CminaCheckIsIPaddressValid) +{ + /* 0.0.0.0 /32 Mandatory 0.0.0.0 up to 0.0.0.0 + Special case: No IPsuite assigned in conjunction with + SubnetMask and StandardGateway set to zero */ + EXPECT_TRUE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (0, 0, 0, 0), + PNAL_MAKEU32 (0, 0, 0, 0))); + + /* 0.0.0.0 /8 Invalid 0.0.0.0 up to 0.255.255.255 + Reserved according to IETF RFC 6890 */ + EXPECT_FALSE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (255, 0, 0, 0), + PNAL_MAKEU32 (0, 255, 1, 1))); + + /* 127.0.0.0 /8 Invalid 127.0.0.0 up to 127.255.255.255 + Reserved according to IETF RFC 6890 loopback address */ + EXPECT_FALSE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (255, 0, 0, 0), + PNAL_MAKEU32 (127, 0, 0, 1))); + + /* 224.0.0.0 /4 Invalid 224.0.0.0 up to 239.255.255.255 + Reserved according to IETF RFC 6890; IPv4 multicast address assignments */ + EXPECT_FALSE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (0xF0, 0, 0, 0), + PNAL_MAKEU32 (224, 0, 0, 34))); + + /* 240.0.0.0 /4 Invalid 240.0.0.0 up to 255.255.255.255 + Reserved according to IETF RFC 6890; reserved for future addressing */ + EXPECT_FALSE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (0xF0, 0, 0, 0), + PNAL_MAKEU32 (240, 0, 0, 34))); + + /* Invalid — Subnet part of the IPAddress is “0” */ + EXPECT_FALSE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (255, 255, 0, 0), + PNAL_MAKEU32 (0, 0, 1, 10))); + + /* Invalid Host part of the IPAddress is a series of consecutive “1” + (subnet broadcast address) + IPAddress may be accepted but should be invalid.*/ + EXPECT_FALSE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (255, 255, 0, 0), + PNAL_MAKEU32 (192, 168, 255, 255))); + + /* Invalid — Host part of the IPAddress is a series of consecutive “0” + (subnet address) + IPAddress may be accepted but should be invalid. */ + EXPECT_FALSE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (255, 255, 0, 0), + PNAL_MAKEU32 (192, 168, 0, 0))); + + /* Other Mandatory — IP address assigned */ + EXPECT_TRUE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (255, 255, 0, 0), + PNAL_MAKEU32 (192, 168, 1, 1))); + + EXPECT_TRUE (pf_cmina_is_ipaddress_valid ( + PNAL_MAKEU32 (255, 255, 255, 0), + PNAL_MAKEU32 (10, 10, 0, 35))); +} + +TEST_F (CminaUnitTest, CminaCheckIsGatewayValid) +{ + EXPECT_TRUE (pf_cmina_is_gateway_valid ( + PNAL_MAKEU32 (192, 168, 1, 4), + PNAL_MAKEU32 (255, 255, 255, 0), + PNAL_MAKEU32 (192, 168, 1, 1))); + + EXPECT_TRUE (pf_cmina_is_gateway_valid ( + PNAL_MAKEU32 (192, 168, 1, 4), + PNAL_MAKEU32 (255, 255, 255, 0), + PNAL_MAKEU32 (0, 0, 0, 0))); + + EXPECT_FALSE (pf_cmina_is_gateway_valid ( + PNAL_MAKEU32 (192, 168, 1, 4), + PNAL_MAKEU32 (255, 255, 255, 0), + PNAL_MAKEU32 (192, 169, 1, 1))); + + EXPECT_FALSE (pf_cmina_is_gateway_valid ( + PNAL_MAKEU32 (192, 168, 1, 4), + PNAL_MAKEU32 (255, 255, 255, 0), + PNAL_MAKEU32 (192, 168, 0, 1))); +} + +TEST_F (CminaUnitTest, CminaCheckHasTimedOut) +{ + // Interval length 1 ms (= 32), factor 1 + EXPECT_FALSE (pf_cmina_has_timed_out (0, 0, 32, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (999, 0, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1000, 0, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1001, 0, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (UINT32_MAX, 0, 32, 1)); + + EXPECT_TRUE (pf_cmina_has_timed_out (0, 1000, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (999, 1000, 32, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (1000, 1000, 32, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (1001, 1000, 32, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (1999, 1000, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (2000, 1000, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (2001, 1000, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (3000, 1000, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (4000, 1000, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (UINT32_MAX, 1000, 32, 1)); + + EXPECT_FALSE (pf_cmina_has_timed_out (0, UINT32_MAX, 32, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (998, UINT32_MAX, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (999, UINT32_MAX, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1000, UINT32_MAX, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1001, UINT32_MAX, 32, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (UINT32_MAX - 1, UINT32_MAX, 32, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (UINT32_MAX, UINT32_MAX, 32, 1)); + + // Interval length 1 ms, factor 3 + EXPECT_TRUE (pf_cmina_has_timed_out (0, 1000, 32, 3)); + EXPECT_TRUE (pf_cmina_has_timed_out (999, 1000, 32, 3)); + EXPECT_FALSE (pf_cmina_has_timed_out (1000, 1000, 32, 3)); + EXPECT_FALSE (pf_cmina_has_timed_out (1001, 1000, 32, 3)); + EXPECT_FALSE (pf_cmina_has_timed_out (3999, 1000, 32, 3)); + EXPECT_TRUE (pf_cmina_has_timed_out (4000, 1000, 32, 3)); + EXPECT_TRUE (pf_cmina_has_timed_out (4001, 1000, 32, 3)); + EXPECT_TRUE (pf_cmina_has_timed_out (5000, 1000, 32, 3)); + EXPECT_TRUE (pf_cmina_has_timed_out (UINT32_MAX, 1000, 32, 3)); + + // Interval length 31.25 microseconds, factor 1 + EXPECT_TRUE (pf_cmina_has_timed_out (0, 1000, 1, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (999, 1000, 1, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (1000, 1000, 1, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (1001, 1000, 1, 1)); + EXPECT_FALSE (pf_cmina_has_timed_out (1030, 1000, 1, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1031, 1000, 1, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1032, 1000, 1, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (2000, 1000, 1, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (UINT32_MAX, 1000, 1, 1)); + + // No interval + EXPECT_TRUE (pf_cmina_has_timed_out (0, 1000, 0, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (999, 1000, 0, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1000, 1000, 0, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (1001, 1000, 0, 1)); + EXPECT_TRUE (pf_cmina_has_timed_out (UINT32_MAX, 1000, 0, 1)); + + EXPECT_TRUE (pf_cmina_has_timed_out (0, 1000, 32, 0)); + EXPECT_TRUE (pf_cmina_has_timed_out (999, 1000, 32, 0)); + EXPECT_TRUE (pf_cmina_has_timed_out (1000, 1000, 32, 0)); + EXPECT_TRUE (pf_cmina_has_timed_out (1001, 1000, 32, 0)); + EXPECT_TRUE (pf_cmina_has_timed_out (UINT32_MAX, 1000, 32, 0)); +} diff --git a/profinet_stack/p-net/test/test_cmio.cpp b/profinet_stack/p-net/test/test_cmio.cpp new file mode 100644 index 0000000..22ddfdb --- /dev/null +++ b/profinet_stack/p-net/test/test_cmio.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmioTest : public PnetIntegrationTest +{ +}; + +TEST_F (CmioTest, CmioRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_cmpbe.cpp b/profinet_stack/p-net/test/test_cmpbe.cpp new file mode 100644 index 0000000..e76d22d --- /dev/null +++ b/profinet_stack/p-net/test/test_cmpbe.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmpbeTest : public PnetIntegrationTest +{ +}; + +TEST_F (CmpbeTest, CmpbeRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_cmrdr.cpp b/profinet_stack/p-net/test/test_cmrdr.cpp new file mode 100644 index 0000000..6f30b7b --- /dev/null +++ b/profinet_stack/p-net/test/test_cmrdr.cpp @@ -0,0 +1,493 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Integration testing of CMRDR. + * + * Using for example: + * pnet_application_ready() + * pnet_output_get_data_and_iops() + * pnet_input_get_iocs() + * pnet_input_set_data_and_iops() + * pnet_output_set_iocs() + * pnet_create_log_book_entry() + * pnet_diag_add() + * + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +// clang-format off + +static uint8_t connect_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x72, 0x01, + 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x42, 0x01, 0x00, 0x00, 0x01, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0xde, 0xa0, + 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x01, 0x40, 0x00, + 0x00, 0x11, 0x02, 0x58, 0x88, 0x92, 0x00, 0x0c, 0x72, 0x74, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2d, + 0x64, 0x65, 0x6d, 0x6f, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x05, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x02, + 0x00, 0x02, 0x88, 0x92, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x00, 0x00, 0x20, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, 0x01, 0x04, 0x00, 0x3c, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x04, 0x00, 0x26, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, + 0x00, 0x02, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x02, 0x00, 0xc8, 0xc0, 0x00, 0xa0, 0x00 +}; + +static uint8_t release_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x14, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 +}; + +static uint8_t prm_end_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x10, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 +}; + +static uint8_t appl_rdy_rsp[] = +{ + 0x04, 0x02, 0x0a, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x00, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0x79, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x81, 0x12, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00 +}; + +static uint8_t data_packet_good_iops_good_iocs[] = +{ + 0x1e, 0x30, 0x6c, 0xa2, 0x45, 0x5e, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0x88, 0x92, 0x80, 0x00, + 0x80, 0x80, 0x80, 0x80, 0x23, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0x35, 0x00 +}; + +static uint16_t test_mod_diff[] = +{ + 0xe002 +}; + +static uint16_t test_reads[] = +{ + 0x8000, 0x8001, + 0x800a, 0x800b, 0x800c, + 0x8010, 0x8011, 0x8012, 0x8013, + 0x801e, + 0x8020, + 0x8027, 0x8028, 0x8029, + 0x802a, 0x802b, + 0x802c, 0x802d, + 0x802f, + 0x8030, 0x8031, + 0x8050, 0x8051, 0x8052, 0x8053, 0x8054, 0x8055, 0x8056, 0x8057, + 0x8060, 0x8061, 0x8062, + 0x8070, 0x8071, 0x8072, + 0x8080, + 0x8090, + 0x80af, + 0x80b0, + 0x80cf, + 0xaff0, 0xaff1, 0xaff2, 0xaff3, 0xaff4, 0xaff5, 0xaff6, 0xaff7, 0xaff8, 0xaff9, 0xaffa, 0xaffb, 0xaffc, 0xaffd, 0xaffe, 0xafff, + 0xc000, 0xc001, + 0xc00a, 0xc00b, 0xc00c, + 0xc010, 0xc011, 0xc012, 0xc013, + 0xe000, 0xe001, 0xe002, + 0xe00a, 0xe00b, 0xe00c, + 0xe010, 0xe011, 0xe012, 0xe013, + 0xe030, 0xe031, + 0xe040, + 0xe050, + 0xe060, 0xe061, + 0xf000, + 0xf00a, 0xf00b, 0xf00c, + 0xf010, 0xf011, 0xf012, 0xf013, + 0xf020, + 0xf80c, + 0xf820, 0xf821, + 0xf830, 0xf831, + 0xf840, 0xf841, 0xf842, + 0xf850, + 0xf860, + 0xf870, 0xf871, + 0xf880, 0xf881, 0xf882, 0xf883, 0xf884, 0xf885, 0xf886, 0xf887, 0xf888, 0xf889, + 0xfbff, +}; + +// clang-format on + +static pf_iod_read_request_t read_request; +static pnet_result_t read_status; +static uint16_t seq_nbr = 0; + +class CmrdrTest : public PnetIntegrationTest +{ + protected: + void test_read (uint16_t idx) + { + pf_ar_t * p_ar; + uint8_t buffer[PF_FRAME_BUFFER_SIZE]; + uint16_t pos = 0; + + /* Send data to prevent timeout */ + send_data ( + data_packet_good_iops_good_iocs, + sizeof (data_packet_good_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + + memset (&read_status, 0, sizeof (read_status)); + memset (&read_request, 0, sizeof (read_request)); + pf_ar_find_by_arep (net, appdata.main_arep, &p_ar); + + read_request.sequence_number = seq_nbr++; + /* read_request.ar_uuid = NIL */ + read_request.api = 0; + read_request.slot_number = 1; + read_request.subslot_number = 1; + read_request.index = idx; + read_request.record_data_length = 0; + // read_request.target_ar_uuid; /* Only used if implicit AR */ + + pf_cmrdr_rm_read_ind ( + net, + p_ar, + &read_request, + &read_status, + sizeof (buffer), + buffer, + &pos); + + if (read_status.pnio_status.error_code != PNET_ERROR_CODE_NOERROR) + { + TEST_TRACE ("Read failed for idx %#x\n", (unsigned)idx); + appdata.read_fails++; + } + } +}; + +TEST_F (CmrdrTest, CmrdrRunTest) +{ + int ret; + pnet_pnio_status_t pnio_status = {1, 2, 3, 4}; + bool new_flag = false; + uint8_t in_data[10]; + uint16_t in_len = sizeof (in_data); + uint8_t out_data[] = { + 0x33, /* Slot 1, subslot 1 Data */ + }; + uint8_t iops = PNET_IOXS_BAD; + uint8_t iocs = PNET_IOXS_BAD; + uint32_t ix; + pnet_diag_source_t diag_source = { + .api = TEST_API_IDENT, + .slot = TEST_SLOT_IDENT, + .subslot = TEST_SUBSLOT_IDENT, + .ch = TEST_CHANNEL_IDENT, + .ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL, + .ch_direction = TEST_CHANNEL_DIRECTION}; + + TEST_TRACE ("\nGenerating mock connection request\n"); + mock_set_pnal_udp_recvfrom_buffer (connect_req, sizeof (connect_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_GT (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer (prm_end_req, sizeof (prm_end_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nSimulate application calling APPL_RDY\n"); + TEST_TRACE ("Line %d\n", __LINE__); + ret = pnet_application_ready (net, appdata.main_arep); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerating mock application ready response\n"); + mock_set_pnal_udp_recvfrom_buffer (appl_rdy_rsp, sizeof (appl_rdy_rsp)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nSend a couple of data packets and verify reception\n"); + for (ix = 0; ix < 100; ix++) + { + send_data ( + data_packet_good_iops_good_iocs, + sizeof (data_packet_good_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + } + + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (in_data[0], 0x23); + EXPECT_EQ (iops, PNET_IOXS_GOOD); + + iocs = 77; /* Something non-valid */ + ret = pnet_input_get_iocs ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + &iocs); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iocs, PNET_IOXS_GOOD); + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + TEST_TRACE ("\nSend some data to the controller\n"); + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + out_data, + sizeof (out_data), + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nAcknowledge the reception of controller data\n"); + ret = pnet_output_set_iocs ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + /* Send data to avoid timeout */ + send_data ( + data_packet_good_iops_good_iocs, + sizeof (data_packet_good_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + + TEST_TRACE ("\nCreate a logbook entry\n"); + pnet_create_log_book_entry (net, appdata.main_arep, &pnio_status, 0x13245768); + + TEST_TRACE ("\nCreate a diag and an alarm.\n"); + pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + + TEST_TRACE ("Number of tests: %u\n", (unsigned)NELEMENTS (test_reads)); + + TEST_TRACE ("\nNow read all the records\n"); + for (ix = 0; ix < NELEMENTS (test_reads); ix++) + { + test_read (test_reads[ix]); + } + EXPECT_EQ (appdata.read_fails, 62); // Currently expected number of fails. + + TEST_TRACE ("\nGenerating mock release request\n"); + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 1); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} + +TEST_F (CmrdrTest, CmrdrModDiffTest) +{ + int ret; + pnet_pnio_status_t pnio_status = {1, 2, 3, 4}; + bool new_flag = false; + uint8_t in_data[10]; + uint16_t in_len = sizeof (in_data); + uint8_t out_data[] = { + 0x33, /* Slot 1, subslot 1 Data */ + }; + uint8_t iops = PNET_IOXS_BAD; + uint8_t iocs = PNET_IOXS_BAD; + uint32_t ix; + const uint16_t slot = 1; + const uint16_t subslot = 1; + + // TODO This test used to have an offset for the submodule identifier value + // in the on_submodule callback. However it does not make any difference + // for the test result + + TEST_TRACE ("\nGenerating mock connection request\n"); + mock_set_pnal_udp_recvfrom_buffer (connect_req, sizeof (connect_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_GT (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer (prm_end_req, sizeof (prm_end_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nSimulate application calling APPL_RDY\n"); + ret = pnet_application_ready (net, appdata.main_arep); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerating mock application ready response\n"); + mock_set_pnal_udp_recvfrom_buffer (appl_rdy_rsp, sizeof (appl_rdy_rsp)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nSend a couple of data packets and verify reception\n"); + for (ix = 0; ix < 100; ix++) + { + send_data ( + data_packet_good_iops_good_iocs, + sizeof (data_packet_good_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + } + + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (in_data[0], 0x23); + EXPECT_EQ (iops, PNET_IOXS_GOOD); + + iocs = 77; /* Something non-valid */ + ret = pnet_input_get_iocs (net, TEST_API_IDENT, slot, subslot, &iocs); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iocs, PNET_IOXS_GOOD); + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + /* Send some data to the controller */ + TEST_TRACE ("Line %d\n", __LINE__); + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + out_data, + sizeof (out_data), + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + + /* Acknowledge the reception of controller data */ + ret = + pnet_output_set_iocs (net, TEST_API_IDENT, slot, subslot, PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + /* Setup some record for the reader */ + + TEST_TRACE ("Line %d\n", __LINE__); + /* Create a logbook entry */ + pnet_create_log_book_entry (net, appdata.main_arep, &pnio_status, 0x13245768); + + TEST_TRACE ("Number of tests: %u\n", (unsigned)NELEMENTS (test_mod_diff)); + + /* Now read all the mod diff record */ + for (ix = 0; ix < NELEMENTS (test_mod_diff); ix++) + { + test_read (test_mod_diff[ix]); + } + EXPECT_EQ (appdata.read_fails, 0); + + TEST_TRACE ("\nGenerating mock release request\n"); + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 1); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} diff --git a/profinet_stack/p-net/test/test_cmrpc.cpp b/profinet_stack/p-net/test/test_cmrpc.cpp new file mode 100644 index 0000000..cf7f3ab --- /dev/null +++ b/profinet_stack/p-net/test/test_cmrpc.cpp @@ -0,0 +1,704 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Integration testing of CMRPC. + * + * For example + * Connect + * Read IM0 + * Release connection + * Timeout in CPM + * Fragmented connect + * ConnectReleaseIOSAR_DA ? + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmrpcUnitTest : public PnetUnitTest +{ +}; +class CmrpcTest : public PnetIntegrationTest +{ +}; + +// clang-format off + +/** + * Connect request data + * This is UDP payload, so the first 42 bytes (in dec) have been removed from + * the Wireshark output. + */ +static uint8_t connect_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x72, 0x01, + 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x42, 0x01, 0x00, 0x00, 0x01, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0xde, 0xa0, + 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x01, 0x40, 0x00, + 0x00, 0x11, 0x02, 0x58, 0x88, 0x92, 0x00, 0x0c, 0x72, 0x74, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2d, + 0x64, 0x65, 0x6d, 0x6f, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x05, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x02, + 0x00, 0x02, 0x88, 0x92, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x00, 0x00, 0x20, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, 0x01, 0x04, 0x00, 0x3c, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x04, 0x00, 0x26, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, + 0x00, 0x02, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x02, 0x00, 0xc8, 0xc0, 0x00, 0xa0, 0x00 +}; + +static uint8_t release_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x14, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 +}; + +static uint8_t prm_end_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x10, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 +}; + +static uint8_t appl_rdy_rsp[] = +{ + 0x04, 0x02, 0x0a, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x00, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0x79, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x81, 0x12, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00 +}; + +static uint8_t write_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x3c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x7c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0xa0, + 0xbe, 0xda +}; + +static uint8_t read_im0_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xde, 0xa0, 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0xde, 0xa0, 0x00, 0x01, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x09, + 0x00, 0x3c, 0x01, 0x00, 0x00, 0x02, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0xaf, 0xf0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + * In order to create a fragmented connect using the connect request above: + * 1) Copy the connect request to both fragments. + * 2) Strip away the ETH header, the IP header and the UDP header (0x2a bytes) from both packets (already done!!!) + * 3) In fragment 1 change byte [0x2c] from 0x28 to 0x2c. (means fragment) + * 4) In fragment 2 change byte [0x2c] from 0x28 to 0x2e. (means last fragment) + * 5) Modify fragment length member of both frames: + * a) Fragment 1: bytes [0x74..0x75] from {0x86 0x01} to [0x00 0x01] (means 0x100 bytes). + * b) Fragment 2: bytes [0x74..0x75] from {0x86 0x01} to [0x86 0x00] (means 0x86 bytes). + * 6) Modify fragment number of fragment 2: + * Fragment 2: bytes [0x76..0x77] from {0x00 0x00} to {0x01 0x00} (means fragment 1). + * 7) Modify serial_low member of fragment 2: + * Fragment 2: byte [0x79] from 0x00 to 0x01. + * 7) Cut fragment 1 at 256 bytes of connect request (i.e., at position 0x16a). + * 8) Remove the first 256 bytes of the connect request from fragment 2 (i.e. bytes 0x7a to 0x17a) + */ +static uint8_t connect_frag_1_req[] = +{ + /* 20 */ /* 2a */0x04, 0x00, 0x2c, 0x00, 0x10, 0x00, + /* 30 */0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + /* 40 */0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + /* 50 */0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + /* 60 */0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 70 */0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x72, 0x01, + /* 80 */0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x01, 0x01, + /* 90 */0x00, 0x42, 0x01, 0x00, 0x00, 0x01, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + /* a0 */0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0xde, 0xa0, + /* b0 */0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x01, 0x40, 0x00, + /* c0 */0x00, 0x11, 0x02, 0x58, 0x88, 0x92, 0x00, 0x0c, 0x72, 0x74, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2d, + /* d0 */0x64, 0x65, 0x6d, 0x6f, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x88, 0x92, + /* e0 */0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + /* f0 */0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 100 */0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + /* 110 */0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, + /* 120 */0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x05, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x02, + /* 130 */0x00, 0x02, 0x88, 0x92, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x00, 0x00, 0x20, 0x00, 0x01, + /* 140 */0x00, 0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, + /* 150 */0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + /* 160 */0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, + /* 170 */0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01 +}; + +static uint8_t connect_frag_2_req[] = +{ + /* 20 */ /* 2a */0x04, 0x00, 0x2e, 0x00, 0x10, 0x00, + /* 30 */0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + /* 40 */0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + /* 50 */0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + /* 60 */0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 70 */0xff, 0xff, 0xff, 0xff, 0x86, 0x00, 0x01, 0x00, 0x00, 0x01, + /* 80 */ + /* 90 */ + /* a0 */ + /* b0 */ + /* c0 */ + /* d0 */ + /* e0 */ + /* f0 */ + /* 100 */ + /* 110 */ + /* 120 */ + /* 130 */ + /* 140 */ + /* 150 */ + /* 160 */ + /* 170 */ 0x00, 0x03, 0x01, 0x04, 0x00, 0x3c, + /* 180 */0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + /* 190 */0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, + /* 1a0 */0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x80, 0x01, + /* 1b0 */0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x04, 0x00, 0x26, + /* 1c0 */0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, + /* 1d0 */0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, + /* 1e0 */0x00, 0x02, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x01, 0x88, 0x92, + /* 1f0 */0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x02, 0x00, 0xc8, 0xc0, 0x00, 0xa0, 0x00 +}; + +static uint8_t connect_req_iosar_da[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x42, 0x00, + 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x3e, 0x01, 0x00, 0x00, 0x06, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0xde, 0xa0, + 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x01, 0x40, 0x00, + 0x01, 0x11, 0x02, 0x58, 0x88, 0x92, 0x00, 0x08, 0x72, 0x74, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x31 +}; + +static uint8_t release_req_iosar_da[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x14, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 +}; + +static uint8_t connect_req_siemens[] = +{ + 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, +0x00, 0x00, 0xde, 0xa0, 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, +0xfe, 0xed, 0xde, 0xa0, 0x00, 0x01, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, +0xdf, 0x7d, 0x0a, 0x07, 0x10, 0x32, 0x1b, 0xd0, 0x1f, 0x1f, 0x8a, 0x8c, 0xe0, 0xdc, 0xa0, 0x8a, +0xba, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0xff, 0xff, 0xff, 0xff, 0x01, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, +0x01, 0x70, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x70, 0x01, 0x01, +0x00, 0x40, 0x01, 0x00, 0x00, 0x01, 0xf1, 0x07, 0x05, 0x4c, 0x1e, 0x41, 0x43, 0x4f, 0x8b, 0xc8, +0xcc, 0x5f, 0x1c, 0x76, 0x93, 0xf8, 0x00, 0x01, 0xe0, 0xdc, 0xa0, 0x8a, 0xba, 0x33, 0xde, 0xa0, +0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x64, 0x01, 0x0d, 0x00, 0x2a, 0x40, 0x00, +0x00, 0x11, 0x00, 0xc8, 0x88, 0x92, 0x00, 0x0a, 0x70, 0x6c, 0x63, 0x78, 0x62, 0x31, 0x64, 0x30, +0x65, 0x64, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x88, 0x92, 0x00, 0x00, +0x00, 0x02, 0x00, 0x28, 0x80, 0x00, 0x00, 0x20, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0xff, 0xff, +0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, +0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, +0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, +0x00, 0x01, 0x00, 0x01, 0x00, 0x05, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x02, 0x00, 0x02, +0x88, 0x92, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0xff, 0xff, 0x00, 0x20, 0x00, 0x02, 0x00, 0x01, +0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, +0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, +0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, 0x01, 0x04, 0x00, 0x3c, 0x01, 0x00, +0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, +0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x80, 0x00, +0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x80, 0x01, 0x00, 0x00, +0x80, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x04, 0x00, 0x26, 0x01, 0x00, +0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x01, +0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x02, +0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x01, 0x88, 0x92, 0x00, 0x00, +0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x01, 0x00, 0xc0, 0x00, 0xa0, 0x00 +}; + +static uint8_t write_req_siemens[] = +{ + 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, +0x00, 0x00, 0xde, 0xa0, 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, +0xfe, 0xed, 0xde, 0xa0, 0x00, 0x01, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, +0xdf, 0x7d, 0x0a, 0x07, 0x10, 0x32, 0x1b, 0xd0, 0x1f, 0x1f, 0x8a, 0x8c, 0xe0, 0xdc, 0xa0, 0x8a, +0xba, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, +0xff, 0xff, 0xff, 0xff, 0x01, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, +0x01, 0x14, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x08, +0x00, 0x3c, 0x01, 0x00, 0x00, 0x00, 0xf1, 0x07, 0x05, 0x4c, 0x1e, 0x41, 0x43, 0x4f, 0x8b, 0xc8, +0xcc, 0x5f, 0x1c, 0x76, 0x93, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, +0xe0, 0x40, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, +0x00, 0x3c, 0x01, 0x00, 0x00, 0x01, 0xf1, 0x07, 0x05, 0x4c, 0x1e, 0x41, 0x43, 0x4f, 0x8b, 0xc8, +0xcc, 0x5f, 0x1c, 0x76, 0x93, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, +0x80, 0x71, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x50, +0x00, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x3c, 0x01, 0x00, +0x00, 0x02, 0xf1, 0x07, 0x05, 0x4c, 0x1e, 0x41, 0x43, 0x4f, 0x8b, 0xc8, 0xcc, 0x5f, 0x1c, 0x76, +0x93, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, +0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, +0x00, 0x3c, 0x01, 0x00, 0x00, 0x03, 0xf1, 0x07, 0x05, 0x4c, 0x1e, 0x41, 0x43, 0x4f, 0x8b, 0xc8, +0xcc, 0x5f, 0x1c, 0x76, 0x93, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, +0x00, 0x7c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00 +}; + +static uint8_t prm_end_req_siemens[] = +{ + 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, +0x00, 0x00, 0xde, 0xa0, 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, +0xfe, 0xed, 0xde, 0xa0, 0x00, 0x01, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, +0xdf, 0x7d, 0x0a, 0x07, 0x10, 0x32, 0x1b, 0xd0, 0x1f, 0x1f, 0x8a, 0x8c, 0xe0, 0xdc, 0xa0, 0x8a, +0xba, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, +0xff, 0xff, 0xff, 0xff, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, +0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x10, +0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0xf1, 0x07, 0x05, 0x4c, 0x1e, 0x41, 0x43, 0x4f, 0x8b, 0xc8, +0xcc, 0x5f, 0x1c, 0x76, 0x93, 0xf8, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 +}; + +/* This is a raw Ethernet packet (not UDP payload) */ +static uint8_t data_packet[] = +{ + 0x1e, 0x30, 0x6c, 0xa2, 0x45, 0x5e, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0x88, 0x92, 0x80, 0x00, + 0x80, 0x80, 0x80, 0x20, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf3, 0x35, 0x00 +}; + +// clang-format on + +TEST_F (CmrpcTest, CmrpcConnectReleaseTest) +{ + int ret; + uint32_t ix; + EXPECT_EQ (mock_os_data.udp_sendto_len, 0); + + TEST_TRACE ("\nGenerating mock connection request\n"); + mock_set_pnal_udp_recvfrom_buffer (connect_req, sizeof (connect_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_GT (mock_os_data.eth_send_count, 0); + EXPECT_EQ (mock_os_data.udp_sendto_count, 1); + EXPECT_EQ (mock_os_data.udp_sendto_len, 178); + + TEST_TRACE ("\nGenerating mock write request\n"); + mock_set_pnal_udp_recvfrom_buffer (write_req, sizeof (write_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.write_calls, 1); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_EQ (mock_os_data.udp_sendto_count, 2); + EXPECT_EQ (mock_os_data.udp_sendto_len, 164); + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer (prm_end_req, sizeof (prm_end_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_EQ (appdata.call_counters.write_calls, 1); + EXPECT_EQ (mock_os_data.udp_sendto_count, 3); + EXPECT_EQ (mock_os_data.udp_sendto_len, 132); + + TEST_TRACE ("\nSimulate application calling APPL_RDY\n"); + ret = pnet_application_ready (net, appdata.main_arep); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + EXPECT_EQ (mock_os_data.udp_sendto_count, 4); + EXPECT_EQ (mock_os_data.udp_sendto_len, 132); + + TEST_TRACE ("\nGenerating mock application ready response\n"); + mock_set_pnal_udp_recvfrom_buffer (appl_rdy_rsp, sizeof (appl_rdy_rsp)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + EXPECT_EQ (mock_os_data.udp_sendto_count, 4); + + TEST_TRACE ("\nGenerating I&M0 request\n"); + mock_set_pnal_udp_recvfrom_buffer (read_im0_req, sizeof (read_im0_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + EXPECT_EQ (mock_os_data.udp_sendto_count, 5); + EXPECT_EQ (mock_os_data.udp_sendto_len, 224); + + TEST_TRACE ("\nGenerating cyclic data\n"); + for (ix = 0; ix < 100; ix++) + { + send_data (data_packet, sizeof (data_packet)); + run_stack (TEST_DATA_DELAY); + } + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + EXPECT_EQ (mock_os_data.udp_sendto_count, 5); + + TEST_TRACE ("\nGenerating mock release request\n"); + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 1); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); + EXPECT_EQ (mock_os_data.udp_sendto_count, 6); + EXPECT_EQ (mock_os_data.udp_sendto_len, 132); +} + +TEST_F (CmrpcTest, CmrpcConnectionTimeoutTest) +{ + int ret; + bool new_flag = false; + uint8_t in_data[10]; + uint16_t in_len = sizeof (in_data); + uint8_t out_data[] = { + 0x33, /* Slot 1, subslot 1 Data */ + }; + uint8_t iops = PNET_IOXS_BAD; + uint32_t ix; + + TEST_TRACE ("\nGenerating mock connection request\n"); + mock_set_pnal_udp_recvfrom_buffer (connect_req, sizeof (connect_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_GT (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock write request\n"); + mock_set_pnal_udp_recvfrom_buffer (write_req, sizeof (write_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer (prm_end_req, sizeof (prm_end_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nSimulate application calling APPL_RDY\n"); + ret = pnet_application_ready (net, appdata.main_arep); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerating mock application ready response\n"); + mock_set_pnal_udp_recvfrom_buffer (appl_rdy_rsp, sizeof (appl_rdy_rsp)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerate a couple of data packets to move pf_cpm to RUN " + "state\n"); + for (ix = 0; ix < 100; ix++) + { + send_data (data_packet, sizeof (data_packet)); + run_stack (TEST_DATA_DELAY); + } + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + TEST_TRACE ("\nRead data just to see that it works\n"); + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + 1, + 1, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iops, PNET_IOXS_GOOD); + + TEST_TRACE ("\nGenerate new data to move PPM to RUN state\n"); + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + 1, + 1, + out_data, + sizeof (out_data), + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nWait for timeout in CPM\n"); + run_stack (TEST_TIMEOUT_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); + + TEST_TRACE ("\nGenerating mock release request\n"); + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} + +/** + * Replicate startup with a Siemens S7-1200 controller + */ +TEST_F (CmrpcTest, CmrpcSiemensConnectTest) +{ + EXPECT_EQ (mock_os_data.udp_sendto_len, 0); + + TEST_TRACE ("\nGenerating mock connection request\n"); + mock_set_pnal_udp_recvfrom_buffer ( + connect_req_siemens, + sizeof (connect_req_siemens)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_EQ (appdata.call_counters.write_calls, 0); + EXPECT_EQ (appdata.call_counters.read_calls, 0); + EXPECT_EQ (appdata.call_counters.ccontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.dcontrol_calls, 0); + EXPECT_GT (mock_os_data.eth_send_count, 0); + EXPECT_EQ (mock_os_data.udp_sendto_count, 1); + EXPECT_EQ (mock_os_data.udp_sendto_len, 178); // Actual value. OK in + // Wireshark (232 bytes) + + TEST_TRACE ("\nGenerating mock write request\n"); + mock_set_pnal_udp_recvfrom_buffer ( + write_req_siemens, + sizeof (write_req_siemens)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_EQ (appdata.call_counters.write_calls, 2); + EXPECT_EQ (appdata.call_counters.read_calls, 0); + EXPECT_EQ (appdata.call_counters.ccontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.dcontrol_calls, 0); + EXPECT_EQ (mock_os_data.udp_sendto_count, 2); + EXPECT_EQ (mock_os_data.udp_sendto_len, 356); // Actual value. OK in + // Wireshark (398 bytes) + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer ( + prm_end_req_siemens, + sizeof (prm_end_req_siemens)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_EQ (appdata.call_counters.write_calls, 2); + EXPECT_EQ (appdata.call_counters.read_calls, 0); + EXPECT_EQ (appdata.call_counters.ccontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.dcontrol_calls, 1); + EXPECT_EQ (mock_os_data.udp_sendto_count, 3); + EXPECT_EQ (mock_os_data.udp_sendto_len, 132); // 20 bytes longer than sent + // from faulty application +} + +TEST_F (CmrpcTest, CmrpcConnectFragmentTest) +{ + int ret; + uint32_t ix; + + TEST_TRACE ("\nGenerating mock connection request, fragment 1\n"); + mock_set_pnal_udp_recvfrom_buffer ( + connect_frag_1_req, + sizeof (connect_frag_1_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 0); + EXPECT_EQ (appdata.call_counters.connect_calls, 0); + EXPECT_EQ (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock connection request, fragment 2\n"); + mock_set_pnal_udp_recvfrom_buffer ( + connect_frag_2_req, + sizeof (connect_frag_2_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_GT (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock connection write request\n"); + mock_set_pnal_udp_recvfrom_buffer (write_req, sizeof (write_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.write_calls, 1); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer (prm_end_req, sizeof (prm_end_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nSimulate application calling APPL_RDY\n"); + ret = pnet_application_ready (net, appdata.main_arep); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerating mock application ready response\n"); + mock_set_pnal_udp_recvfrom_buffer (appl_rdy_rsp, sizeof (appl_rdy_rsp)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nRead I&M0\n"); + mock_set_pnal_udp_recvfrom_buffer (read_im0_req, sizeof (read_im0_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nSending data\n"); + for (ix = 0; ix < 100; ix++) + { + send_data (data_packet, sizeof (data_packet)); + run_stack (TEST_DATA_DELAY); + } + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + TEST_TRACE ("Sending mock release request\n"); + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 1); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} + +TEST_F (CmrpcTest, CmrpcConnectReleaseIOSAR_DA) +{ + // Device-access AR is not yet supported + TEST_TRACE ("\nGenerating mock connection request IOSAR_DA\n"); + mock_set_pnal_udp_recvfrom_buffer ( + connect_req_iosar_da, + sizeof (connect_req_iosar_da)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 0); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); + EXPECT_EQ (appdata.call_counters.connect_calls, 0); + EXPECT_EQ (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock release request IOSAR_DA\n"); + mock_set_pnal_udp_recvfrom_buffer ( + release_req_iosar_da, + sizeof (release_req_iosar_da)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 0); + EXPECT_EQ (appdata.call_counters.release_calls, 0); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} + +TEST_F (CmrpcUnitTest, CmrpcCheckGenerateUuid) +{ + uint32_t timestamp; + uint32_t session_number; + pnet_ethaddr_t mac_address; + pf_uuid_t uuid; + + timestamp = 0xC1C2C3C4; + session_number = 0xB1B2B3B4; + mac_address.addr[0] = 0xA1; + mac_address.addr[1] = 0xA2; + mac_address.addr[2] = 0xA3; + mac_address.addr[3] = 0xA4; + mac_address.addr[4] = 0xA5; + mac_address.addr[5] = 0xA6; + + pf_generate_uuid (timestamp, session_number, mac_address, &uuid); + + EXPECT_EQ (uuid.data1, timestamp); + EXPECT_EQ (uuid.data2, 0xB1B2); + EXPECT_EQ (uuid.data3, 0x10B3); + EXPECT_EQ (uuid.data4[0], 0x80); + EXPECT_EQ (uuid.data4[1], 0xB4); + EXPECT_EQ (uuid.data4[2], 0xA1); + EXPECT_EQ (uuid.data4[3], 0xA2); + EXPECT_EQ (uuid.data4[4], 0xA3); + EXPECT_EQ (uuid.data4[5], 0xA4); + EXPECT_EQ (uuid.data4[6], 0xA5); + EXPECT_EQ (uuid.data4[7], 0xA6); +} diff --git a/profinet_stack/p-net/test/test_cmrs.cpp b/profinet_stack/p-net/test/test_cmrs.cpp new file mode 100644 index 0000000..774a14d --- /dev/null +++ b/profinet_stack/p-net/test/test_cmrs.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmrsTest : public PnetIntegrationTest +{ +}; + +TEST_F (CmrsTest, CmrsRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_cmsm.cpp b/profinet_stack/p-net/test/test_cmsm.cpp new file mode 100644 index 0000000..75e9c05 --- /dev/null +++ b/profinet_stack/p-net/test/test_cmsm.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmsmTest : public PnetIntegrationTest +{ +}; + +TEST_F (CmsmTest, CmsmRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_cmsu.cpp b/profinet_stack/p-net/test/test_cmsu.cpp new file mode 100644 index 0000000..1641716 --- /dev/null +++ b/profinet_stack/p-net/test/test_cmsu.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmsuTest : public PnetIntegrationTest +{ +}; + +TEST_F (CmsuTest, CmsuRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_cmwdr.cpp b/profinet_stack/p-net/test/test_cmwdr.cpp new file mode 100644 index 0000000..a56378a --- /dev/null +++ b/profinet_stack/p-net/test/test_cmwdr.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CmwdrTest : public PnetIntegrationTest +{ +}; + +TEST_F (CmwdrTest, CmwdrRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_cpm.cpp b/profinet_stack/p-net/test/test_cpm.cpp new file mode 100644 index 0000000..b32e0c7 --- /dev/null +++ b/profinet_stack/p-net/test/test_cpm.cpp @@ -0,0 +1,106 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class CpmUnitTest : public PnetUnitTest +{ +}; + +TEST_F (CpmUnitTest, CpmCheckCycle) +{ + EXPECT_EQ (-1, pf_cpm_check_cycle (1, 0xFFFF)); + EXPECT_EQ (-1, pf_cpm_check_cycle (1, 0)); + EXPECT_EQ (-1, pf_cpm_check_cycle (1, 1)); + EXPECT_EQ (0, pf_cpm_check_cycle (1, 2)); + EXPECT_EQ (0, pf_cpm_check_cycle (1, 3)); + + /* Previous counter value was 0x4000 */ + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0x0000)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0x0001)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0x2FFE)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0x2FFF)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0x3000)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x4000, 0x3001)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x4000, 0x3002)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x4000, 0x3FFD)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x4000, 0x3FFE)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x4000, 0x3FFF)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x4000, 0x4000)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0x4001)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0x4002)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x4000, 0xFFFF)); + + /* Previous counter value was 0xFFFF (Forbidden zone ends at 0xFFFF) */ + EXPECT_EQ (0, pf_cpm_check_cycle (0xFFFF, 0xEFFE)); + EXPECT_EQ (0, pf_cpm_check_cycle (0xFFFF, 0xEFFF)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0xFFFF, 0xF000)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0xFFFF, 0xF001)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0xFFFF, 0xFFFE)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0xFFFF, 0xFFFF)); + EXPECT_EQ (0, pf_cpm_check_cycle (0xFFFF, 0x0000)); + EXPECT_EQ (0, pf_cpm_check_cycle (0xFFFF, 0x0001)); + EXPECT_EQ (0, pf_cpm_check_cycle (0xFFFF, 0x0002)); + + /* Previous counter value was 0x0000 (Forbidden zone ends at 0x0000) */ + EXPECT_EQ (0, pf_cpm_check_cycle (0x0000, 0xEFFE)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0000, 0xEFFF)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0000, 0xF000)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0000, 0xF001)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0000, 0xFFFE)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0000, 0xFFFF)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0000, 0x0000)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0000, 0x0001)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0000, 0x0002)); + + /* Previous counter value was 0x0FFE (Forbidden zone starts at 0xFFFF) */ + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFE, 0xFFFE)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFE, 0xFFFF)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFE, 0x0000)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFE, 0x0001)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFE, 0x0FFE)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFE, 0x0FFF)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFE, 0x1000)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFE, 0x1001)); + + /* Previous counter value was 0x0FFF (Forbidden zone starts at zero) */ + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFF, 0xFFFE)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFF, 0xFFFF)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFF, 0x0000)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFF, 0x0001)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFF, 0x0FFE)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0FFF, 0x0FFF)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFF, 0x1000)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0FFF, 0x1001)); + + /* Previous counter value was 0x0010 (Forbidden zone overlaps zero) */ + EXPECT_EQ (0, pf_cpm_check_cycle (0x0010, 0xF00F)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0010, 0xF010)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0xF011)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0xFFFE)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0xFFFF)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0x0000)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0x0001)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0x000E)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0x000F)); + EXPECT_EQ (-1, pf_cpm_check_cycle (0x0010, 0x0010)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0010, 0x0011)); + EXPECT_EQ (0, pf_cpm_check_cycle (0x0010, 0x0012)); +} diff --git a/profinet_stack/p-net/test/test_dcp.cpp b/profinet_stack/p-net/test/test_dcp.cpp new file mode 100644 index 0000000..2119c4b --- /dev/null +++ b/profinet_stack/p-net/test/test_dcp.cpp @@ -0,0 +1,380 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Testing of DCP aspects. + * + * For example + * Sending hello frame + * Handling of get name request + * Read station name + * Set station name + * Set IP address + * Do factory reset + * + * Checks only function return codes. No checking of the sent Ethernet frames is + * done. + * + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class DcpTest : public PnetIntegrationTest +{ +}; + +class DcpUnitTest : public PnetUnitTest +{ +}; + +static uint8_t get_wrong_length_req[] = { + // clang-format off + + // Destination MAC address. Pos 0 + 0x12, 0x34, 0x00, 0x78, 0x90, 0xab, + + // Source MAC address. Pos 6 + 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, + + // Ethernet type = Profinet RT. Pos 12 + 0x88, 0x92, + + // DCP set/get. Pos 14 + 0xfe, 0xfd, + + // Get, request. Pos 16 + 0x03, 0x00, + + // XID. Pos 20 + 0x00, 0x00, 0x00, 0x01, + + // Reserved. Pos 22 + 0x04, 0x01, + + // DCP data length. Wrong value. Pos 24 + 0x04, 0x00, + + // Options. Pos 26 + 0x00, 0x00 + + // clang-format on +}; + +static uint8_t get_name_req[] = { + 0x12, 0x34, 0x00, 0x78, 0x90, 0xab, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, + 0x88, 0x92, 0xfe, 0xfd, 0x03, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x06, 0x02, 0x02, 0x02, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static uint8_t ident_req[] = { + 0x01, 0x0e, 0xcf, 0x00, 0x00, 0x00, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, + 0x88, 0x92, 0xfe, 0xfe, 0x05, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, + 0x00, 0x10, 0x02, 0x02, 0x00, 0x0c, 0x72, 0x74, 0x2d, 0x6c, 0x61, 0x62, + 0x73, 0x2d, 0x64, 0x65, 0x6d, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static uint8_t set_name_req[] = { + 0x12, 0x34, 0x00, 0x78, 0x90, 0xab, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, + 0x88, 0x92, 0xfe, 0xfd, 0x04, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x12, 0x02, 0x02, 0x00, 0x0e, 0x00, 0x00, 0x72, 0x74, 0x2d, 0x6c, + 0x61, 0x62, 0x73, 0x2d, 0x64, 0x65, 0x6d, 0x6f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static uint8_t set_ip_req[] = { + 0x12, 0x34, 0x00, 0x78, 0x90, 0xab, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, + 0x88, 0x92, 0xfe, 0xfd, 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x18, 0x01, 0x02, 0x00, 0x0e, 0x00, 0x00, 0xc0, 0xa8, 0x01, 0xab, + 0xff, 0xff, 0xff, 0x00, 0xc0, 0xa8, 0x01, 0x01, 0x05, 0x02, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static uint8_t factory_reset_req[] = { + 0x12, 0x34, 0x00, 0x78, 0x90, 0xab, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, + 0x88, 0x92, 0xfe, 0xfd, 0x04, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x06, 0x05, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static uint8_t signal_req[] = { + 0x12, 0x34, 0x00, 0x78, 0x90, 0xab, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, + 0x88, 0x92, 0xfe, 0xfd, 0x04, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x06, 0x05, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +TEST_F (DcpTest, DcpHelloTest) +{ + pnal_buf_t * p_buf; + int ret; + + mock_clear(); + + ret = pf_dcp_hello_req (net); + EXPECT_EQ (ret, 0); + + mock_clear(); + + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + memcpy (p_buf->payload, get_name_req, sizeof (get_name_req)); + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + + EXPECT_EQ (ret, 1); // Incoming frame is handled + // LLDP frames and one additional frame + EXPECT_EQ (mock_os_data.eth_send_count, PNET_MAX_PHYSICAL_PORTS + 1); + + EXPECT_EQ (appdata.call_counters.led_off_calls, 1); + EXPECT_EQ (appdata.call_counters.led_on_calls, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 0); + EXPECT_EQ (appdata.call_counters.connect_calls, 0); + EXPECT_EQ (appdata.call_counters.release_calls, 0); + EXPECT_EQ (appdata.call_counters.dcontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.ccontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.read_calls, 0); + EXPECT_EQ (appdata.call_counters.write_calls, 0); +} + +TEST_F (DcpTest, DcpGetWrongLengthTest) +{ + pnal_buf_t * p_buf; + int ret; + + mock_clear(); + + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + memcpy (p_buf->payload, get_wrong_length_req, sizeof (get_wrong_length_req)); + p_buf->len = sizeof (get_wrong_length_req); + + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + + EXPECT_EQ (ret, 1); // Incoming frame is handled + EXPECT_EQ (mock_os_data.eth_send_count, 0); // Drop malformed frame + + EXPECT_EQ (appdata.call_counters.led_off_calls, 1); + EXPECT_EQ (appdata.call_counters.led_on_calls, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 0); + EXPECT_EQ (appdata.call_counters.connect_calls, 0); + EXPECT_EQ (appdata.call_counters.release_calls, 0); + EXPECT_EQ (appdata.call_counters.dcontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.ccontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.read_calls, 0); + EXPECT_EQ (appdata.call_counters.write_calls, 0); +} + +TEST_F (DcpTest, DcpRunTest) +{ + pnal_buf_t * p_buf; + int ret; + + TEST_TRACE ("\nGenerating mock set name request\n"); + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + memcpy (p_buf->payload, set_name_req, sizeof (set_name_req)); + p_buf->len = sizeof (set_name_req); + + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + EXPECT_EQ (ret, 1); + + TEST_TRACE ("\nGenerating mock set IP request\n"); + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + memcpy (p_buf->payload, set_ip_req, sizeof (set_ip_req)); + p_buf->len = sizeof (set_ip_req); + + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + EXPECT_EQ (ret, 1); + + TEST_TRACE ("\nGenerating mock set ident request\n"); + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + memcpy (p_buf->payload, ident_req, sizeof (ident_req)); + p_buf->len = sizeof (ident_req); + + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + EXPECT_EQ (ret, 1); + + TEST_TRACE ("\nGenerating mock factory reset request\n"); + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + memcpy (p_buf->payload, factory_reset_req, sizeof (factory_reset_req)); + p_buf->len = sizeof (factory_reset_req); + + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + EXPECT_EQ (ret, 1); + + TEST_TRACE ("\nGenerating mock flash LED request\n"); + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + memcpy (p_buf->payload, signal_req, sizeof (signal_req)); + p_buf->len = sizeof (signal_req); + + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + EXPECT_EQ (ret, 1); + /* Wait for LED to flash three times at 1 Hz */ + run_stack (4 * 1000 * 1000); + + EXPECT_EQ ( + mock_os_data.eth_send_count, + 9 + (PNET_MAX_PHYSICAL_PORTS - 1) * 4); + EXPECT_EQ (mock_os_data.set_ip_suite_count, 2); + + EXPECT_EQ (appdata.call_counters.led_on_calls, 3); + EXPECT_EQ (appdata.call_counters.led_off_calls, 4); + EXPECT_EQ (appdata.call_counters.state_calls, 0); + EXPECT_EQ (appdata.call_counters.connect_calls, 0); + EXPECT_EQ (appdata.call_counters.release_calls, 0); + EXPECT_EQ (appdata.call_counters.dcontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.ccontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.read_calls, 0); + EXPECT_EQ (appdata.call_counters.write_calls, 0); +} + +TEST_F (DcpUnitTest, DcpCalculateDelay) +{ + pnet_ethaddr_t mac_address = {0}; + const uint16_t step = 10000; /* Output resolution in microseconds */ + + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6400), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); + + mac_address.addr[5] = 0x01; + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6400), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); + + mac_address.addr[5] = 0x02; + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 2U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 2U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 2U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6400), 2U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); + + mac_address.addr[5] = 0xC7; /* 199 */ + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 99U * step); + + mac_address.addr[5] = 0xFF; /* 255 */ + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 5U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 55U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 252), 3U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 253), 2U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 254), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 255), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 256), 255U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 257), 255U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 258), 255U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 255U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6400), 255U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); + mac_address.addr[5] = 0x00; + + mac_address.addr[4] = 0x01; /* 256 */ + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 6U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 56U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 253), 3U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 254), 2U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 255), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 256), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 257), 256U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 258), 256U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 256U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 256U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6400), 256U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); + + mac_address.addr[4] = 0x02; /* 512 */ + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 2U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 12U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 512U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6400), 512U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); + + mac_address.addr[4] = 0xFF; /* 65280 */ + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 80U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 280U * step); + ASSERT_EQ ( + pf_dcp_calculate_response_delay (&mac_address, 6400), + 1280U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); + + mac_address.addr[4] = 0xFF; + mac_address.addr[5] = 0xFF; /* 65535 */ + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 2), 1U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 10), 5U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 100), 35U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 1000), 535U * step); + ASSERT_EQ ( + pf_dcp_calculate_response_delay (&mac_address, 6400), + 1535U * step); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 6401), 0U); + ASSERT_EQ (pf_dcp_calculate_response_delay (&mac_address, 0xFFFF), 0U); +} + +TEST_F (DcpUnitTest, DcpCheckDestinationAddress) +{ + pnet_ethaddr_t local_mac_address = {0x12, 0x34, 0x00, 0x78, 0x90, 0xab}; + pnet_ethaddr_t unknown_mac_address = {0x12, 0x34, 0x00, 0x78, 0x95, 0xab}; + pnet_ethaddr_t multicast_mac_address = {0x01, 0x0e, 0xcf, 0x00, 0x00, 0x00}; + + ASSERT_EQ ( + pf_dcp_check_destination_address (&local_mac_address, &local_mac_address), + true); + ASSERT_EQ ( + pf_dcp_check_destination_address ( + &local_mac_address, + &unknown_mac_address), + false); + ASSERT_EQ ( + pf_dcp_check_destination_address ( + &local_mac_address, + &multicast_mac_address), + false); +} diff --git a/profinet_stack/p-net/test/test_diag.cpp b/profinet_stack/p-net/test/test_diag.cpp new file mode 100644 index 0000000..9c86171 --- /dev/null +++ b/profinet_stack/p-net/test/test_diag.cpp @@ -0,0 +1,1075 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Integration testing of diagnostics. + * + * For example: + * - pnet_diag_std_add() + * - pnet_diag_std_update() + * - pnet_diag_std_remove() + * - pnet_diag_usi_add() + * - pnet_diag_usi_update() + * - pnet_diag_usi_remove() + * + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class DiagTest : public PnetIntegrationTest +{ +}; + +// clang-format off + +static uint8_t connect_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x72, 0x01, + 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x42, 0x01, 0x00, 0x00, 0x01, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0xde, 0xa0, + 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x01, 0x40, 0x00, + 0x00, 0x11, 0x02, 0x58, 0x88, 0x92, 0x00, 0x0c, 0x72, 0x74, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2d, + 0x64, 0x65, 0x6d, 0x6f, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x05, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x02, + 0x00, 0x02, 0x88, 0x92, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x00, 0x00, 0x20, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, 0x01, 0x04, 0x00, 0x3c, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x04, 0x00, 0x26, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, + 0x00, 0x02, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x02, 0x00, 0xc8, 0xc0, 0x00, 0xa0, 0x00 +}; + +static uint8_t release_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x14, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 +}; + +static uint8_t prm_end_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x10, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 +}; + +static uint8_t appl_rdy_rsp[] = +{ + 0x04, 0x02, 0x0a, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x00, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0x79, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x81, 0x12, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00 +}; + +static uint8_t data_packet_good_iops_good_iocs[] = +{ + 0x1e, 0x30, 0x6c, 0xa2, 0x45, 0x5e, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0x88, 0x92, 0x80, 0x00, + 0x80, 0x80, 0x80, 0x80, 0x23, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0x35, 0x00 +}; + +// clang-format on + +TEST_F (DiagTest, DiagRunTest) +{ + int ret; + bool new_flag = false; + uint8_t in_data[10]; + uint16_t in_len = sizeof (in_data); + uint8_t out_data[] = { + 0x33, /* Slot 1, subslot 1 Data */ + }; + uint8_t iops = PNET_IOXS_BAD; + uint8_t iocs = PNET_IOXS_BAD; + uint32_t ix; + pnet_diag_source_t diag_source = { + .api = TEST_API_IDENT, + .slot = TEST_SLOT_IDENT, + .subslot = TEST_SUBSLOT_IDENT, + .ch = TEST_CHANNEL_IDENT, + .ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL, + .ch_direction = TEST_CHANNEL_DIRECTION}; + + TEST_TRACE ("\nGenerating mock connection request\n"); + mock_set_pnal_udp_recvfrom_buffer (connect_req, sizeof (connect_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_GT (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer (prm_end_req, sizeof (prm_end_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nSimulate application calling APPL_RDY\n"); + ret = pnet_application_ready (net, appdata.main_arep); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerating mock application ready response\n"); + mock_set_pnal_udp_recvfrom_buffer (appl_rdy_rsp, sizeof (appl_rdy_rsp)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerating cyclic data\n"); + for (ix = 0; ix < 100; ix++) + { + send_data ( + data_packet_good_iops_good_iocs, + sizeof (data_packet_good_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + } + + TEST_TRACE ("\nTesting pnet_output_get_data_and_iops()\n"); + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (in_data[0], 0x23); + EXPECT_EQ (iops, PNET_IOXS_GOOD); + + TEST_TRACE ("\nTesting pnet_input_get_iocs()\n"); + iocs = 77; /* Something non-valid */ + ret = pnet_input_get_iocs ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + &iocs); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iocs, PNET_IOXS_GOOD); + + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + TEST_TRACE ("\nSend some data to the controller\n"); + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + out_data, + sizeof (out_data), + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nAcknowledge the reception of controller data\n"); + ret = pnet_output_set_iocs ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + /* Send data to avoid timeout */ + send_data ( + data_packet_good_iops_good_iocs, + sizeof (data_packet_good_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + + TEST_TRACE ("\nCreate a standard diag entry. Then update it and finally " + "remove it.\n"); + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE_B); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nCreate several different severity STD diag entries. Then " + "remove them.\n"); + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_REQUIRED, + TEST_CHANNEL_ERRORTYPE_B, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_DEMANDED, + TEST_CHANNEL_ERRORTYPE_C, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_QUALIFIED, + TEST_CHANNEL_ERRORTYPE_D, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE_B, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE_C, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE_D, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Try to remove it again\n"); + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("Remove non-existing errortype\n"); + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE_NONEXIST, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("Try to add two of the same kind\n"); + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_REQUIRED, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + /* This is expected to succeed as the same entry will be re-used and + * overwritten. */ + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_REQUIRED, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Update with the same value\n"); + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_REQUIRED, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Try to update a diag entry that does not exist\n"); + ret = pnet_diag_std_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_std_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE_NONEXIST, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("Add diag to non-existing sub-slot\n"); + diag_source.slot = TEST_SUBSLOT_NONEXIST_IDENT; + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_REQUIRED, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, -1); + diag_source.slot = TEST_SUBSLOT_IDENT; + + TEST_TRACE ("Try adding, updating and remove diag for illegal channel\n"); + diag_source.ch = TEST_CHANNEL_ILLEGAL; + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_REQUIRED, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_std_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + diag_source.ch = TEST_CHANNEL_IDENT; + + TEST_TRACE ("\nMake sure that all parts of the diag_source are correct " + "before removing the diagnosis.\n"); + ret = pnet_diag_std_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET); + EXPECT_EQ (ret, 0); + + diag_source.api = TEST_API_NONEXIST_IDENT; + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + diag_source.api = TEST_API_IDENT; + + diag_source.slot = TEST_SLOT_NONEXIST_IDENT; + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + diag_source.slot = TEST_SLOT_IDENT; + + diag_source.subslot = TEST_SUBSLOT_NONEXIST_IDENT; + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + diag_source.subslot = TEST_SUBSLOT_IDENT; + + diag_source.ch = TEST_CHANNEL_NONEXIST_IDENT; + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + diag_source.ch = TEST_CHANNEL_IDENT; + + diag_source.ch_grouping = PNET_DIAG_CH_CHANNEL_GROUP; + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + diag_source.ch_grouping = PNET_DIAG_CH_INDIVIDUAL_CHANNEL; + + diag_source.ch_direction = PNET_DIAG_CH_PROP_DIR_INPUT; + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + diag_source.ch_direction = TEST_CHANNEL_DIRECTION; + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE_NONEXIST, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE_NONEXIST); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_std_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Test with a manufacturer specific USI\n"); + ret = pnet_diag_usi_add ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_CUSTOM, + 5, /* Should fit in PNET_MAX_DIAG_MANUF_DATA_SIZE */ + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Update data for a manufacturer specific USI, via add()\n"); + ret = pnet_diag_usi_add ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABC1"); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Update data for a manufacturer specific USI, via update()\n"); + ret = pnet_diag_usi_update ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABC2"); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Update for wrong USI and wrong subslot\n"); + ret = pnet_diag_usi_update ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_NONEXIST, + 5, + (uint8_t *)"ABC2"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_usi_update ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_NONEXIST_IDENT, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABC2"); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("Remove wrong USI\n"); + ret = pnet_diag_usi_remove ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_NONEXIST); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("Remove correct USI\n"); + ret = pnet_diag_usi_remove ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_CUSTOM); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("Add for invalid subslot\n"); + ret = pnet_diag_usi_add ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_NONEXIST_IDENT, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("Add, update and remove diagnosis for invalid USI\n"); + ret = pnet_diag_usi_add ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_INVALID, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_usi_update ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_INVALID, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_usi_remove ( + net, + TEST_API_IDENT, + TEST_SLOT_IDENT, + TEST_SUBSLOT_IDENT, + TEST_DIAG_USI_INVALID); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nUse low-level functionality. Create a standard diag entry, " + "with QUALIFIED and FAULT. Then update it and finally " + "remove it.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET, + 0x8003, /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE_B, + 0x8003, /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + 0x8003 /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + ); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nUse low-level functionality. Create a standard diag entry, " + "with QUALIFIED. Then update it and finally " + "remove it.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_QUALIFIED, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER, + 0x8003, /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE_B, + 0x8003, /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + 0x8003 /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + ); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nUse low-level functionality. Create a standard diag entry, " + "with EXTENDED. Then update it and finally " + "remove it.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET, + 0x8002, /* PF_USI_EXTENDED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE_B, + 0x8002, /* PF_USI_EXTENDED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + 0x8002 /* PF_USI_EXTENDED_CHANNEL_DIAGNOSIS */ + ); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nUse low-level functionality. Create a USI diag entry. Then " + "update it and finally " + "remove it.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + 0, /* Channel error type */ + 0, /* Ext channel error type */ + 0, /* Add value */ + 0, /* Qualifier */ + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_update ( + net, + &diag_source, + 0, /* Channel error type */ + 0, /* Ext channel error type */ + 0, /* Add value */ + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABC2"); + EXPECT_EQ (ret, 0); + + ret = pnet_diag_remove ( + net, + &diag_source, + 0, /* Channel error type */ + 0, /* Ext channel error type */ + TEST_DIAG_USI_CUSTOM); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nError checking for low-level functionality. Invalid USI.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET, + 0x8001, /* Invalid */ + 0, + NULL); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET, + 0x8004, /* Invalid */ + 0, + NULL); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE_B, + 0x8001, /* Invalid */ + 0, + NULL); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + 0x8001 /* Invalid */ + ); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE_B, + 0x8004, /* Invalid */ + 0, + NULL); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + 0x8004 /* Invalid */ + ); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nError checking for low-level functionality. Channel error " + "type given for custom USI.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + 0, /* Ext channel error type */ + 0, /* Add value */ + TEST_DIAG_QUALIFIER_NOTSET, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + 0, /* Extended hannel error type */ + 0, /* Add value */ + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_remove ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + 0, /* Ext channel error type */ + TEST_DIAG_USI_CUSTOM); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nError checking for low-level functionality. Ext channel " + "error type given for custom USI.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + 0, /* Channel error type */ + TEST_DIAG_EXT_ERRTYPE, + 0, /* Add value */ + TEST_DIAG_QUALIFIER_NOTSET, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_update ( + net, + &diag_source, + 0, /* Channel error type */ + TEST_DIAG_EXT_ERRTYPE, + 0, /* Add value */ + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABC2"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_remove ( + net, + &diag_source, + 0, /* Channel error type */ + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_USI_CUSTOM); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nError checking for low-level functionality. Channel " + "additional value given for custom USI.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + 0, /* Channel error type */ + 0, /* Ext channel error type */ + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_update ( + net, + &diag_source, + 0, /* Channel error type */ + 0, /* Ext channel error type */ + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABC2"); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nError checking for low-level functionality. Qualifier given " + "for custom USI.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + PNET_DIAG_CH_PROP_TYPE_UNSPECIFIED, + PNET_DIAG_CH_PROP_MAINT_FAULT, + 0, /* Channel error type */ + 0, /* Ext channel error type */ + 0, /* Add value */ + TEST_DIAG_QUALIFIER, + TEST_DIAG_USI_CUSTOM, + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nError checking for low-level functionality. Manufacturer " + "data given for standard USI.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER_NOTSET, + 0x8003, /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + ret = pnet_diag_update ( + net, + &diag_source, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE_B, + 0x8003, /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + 5, + (uint8_t *)"ABCD"); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nError checking for low-level functionality. Qualifier given " + "for non-qualifyer USI.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER, + 0x8002, /* PF_USI_EXTENDED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nError checking for low-level functionality. Qualifier given " + "for wrong severity.\n"); + ret = pnet_diag_add ( + net, + &diag_source, + TEST_CHANNEL_NUMBER_OF_BITS, + PNET_DIAG_CH_PROP_MAINT_FAULT, + TEST_CHANNEL_ERRORTYPE, + TEST_DIAG_EXT_ERRTYPE, + TEST_DIAG_EXT_ADDVALUE, + TEST_DIAG_QUALIFIER, + 0x8003, /* PF_USI_QUALIFIED_CHANNEL_DIAGNOSIS */ + 0, + NULL); + EXPECT_EQ (ret, -1); + + TEST_TRACE ("\nGenerating mock release request\n"); + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 1); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} diff --git a/profinet_stack/p-net/test/test_eth.cpp b/profinet_stack/p-net/test/test_eth.cpp new file mode 100644 index 0000000..6de9792 --- /dev/null +++ b/profinet_stack/p-net/test/test_eth.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class EthTest : public PnetIntegrationTest +{ +}; + +TEST_F (EthTest, EthRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_file.cpp b/profinet_stack/p-net/test/test_file.cpp new file mode 100644 index 0000000..43328cd --- /dev/null +++ b/profinet_stack/p-net/test/test_file.cpp @@ -0,0 +1,410 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +#define TEST_FILE_DATA_SIZE 10 +#define TEST_FILE_FILENAME "my_filename.bin" +#define TEST_FILE_DIRECTORY "/my_directory/" +#define TEST_FILE_OUTPUTSTRING_SIZE 40 + +class FileUnitTest : public PnetUnitTest +{ +}; + +TEST_F (FileUnitTest, FileJoinDirectoryFilename) +{ + char outputstring[TEST_FILE_OUTPUTSTRING_SIZE] = {0}; /** Terminated */ + int res = 0; + + /* Happy case */ + res = pf_file_join_directory_filename ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, TEST_FILE_DIRECTORY TEST_FILE_FILENAME), 0); + + /* Missing filename or outputbuffer */ + res = pf_file_join_directory_filename ( + TEST_FILE_DIRECTORY, + NULL, + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, -1); + res = pf_file_join_directory_filename ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + NULL, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, -1); + + /* Too small outputbuffer */ + res = pf_file_join_directory_filename ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + outputstring, + 0); + EXPECT_EQ (res, -1); + res = pf_file_join_directory_filename ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + outputstring, + 4); + EXPECT_EQ (res, -1); + + /* Too short filename */ + res = pf_file_join_directory_filename ( + TEST_FILE_DIRECTORY, + "", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, -1); + + /* Joining directory and filename */ + res = pf_file_join_directory_filename ( + "abc", + "def", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "abc/def"), 0); + + res = pf_file_join_directory_filename ( + "abc/", + "def", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "abc/def"), 0); + + res = pf_file_join_directory_filename ( + "abc/", + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "abc/def.ghi"), 0); + + res = pf_file_join_directory_filename ( + "/abc/", + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "/abc/def.ghi"), 0); + + res = pf_file_join_directory_filename ( + "/", + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "/def.ghi"), 0); + + res = pf_file_join_directory_filename ( + "a", + "d", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "a/d"), 0); + + res = pf_file_join_directory_filename ( + "a", + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "a/def.ghi"), 0); + + res = pf_file_join_directory_filename ( + ".", + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "./def.ghi"), 0); + + res = pf_file_join_directory_filename ( + "..", + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "../def.ghi"), 0); + + res = pf_file_join_directory_filename ( + "/", + "d", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "/d"), 0); + + /* No directory given (NULL or empty string) */ + res = pf_file_join_directory_filename ( + NULL, + "d", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "d"), 0); + + res = pf_file_join_directory_filename ( + "", + "d", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "d"), 0); + + res = pf_file_join_directory_filename ( + NULL, + "def", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "def"), 0); + + res = pf_file_join_directory_filename ( + "", + "def", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "def"), 0); + + res = pf_file_join_directory_filename ( + NULL, + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "def.ghi"), 0); + + res = pf_file_join_directory_filename ( + "", + "def.ghi", + outputstring, + TEST_FILE_OUTPUTSTRING_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (strcmp (outputstring, "def.ghi"), 0); +} + +TEST_F (FileUnitTest, FileCheckMagicAndVersion) +{ + uint8_t testdata[TEST_FILE_DATA_SIZE] = + {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}; + uint8_t retrieved[TEST_FILE_DATA_SIZE] = {0}; + uint8_t temporary_byte; + int res = 0; + int i; + + /* Save data together with file version information */ + res = pf_file_save ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &testdata, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (mock_os_data.file_size, TEST_FILE_DATA_SIZE + 8); /* Implementation + detail, but + verifies mock + functionality + */ + + /* Save: Validate mock (protection of max mocked file size) */ + res = + pf_file_save (TEST_FILE_DIRECTORY, TEST_FILE_FILENAME, &testdata, 10000); + EXPECT_EQ (res, -1); + + /* Load: Non-existent file */ + res = pf_file_load ( + TEST_FILE_DIRECTORY, + "unknown_filename", + &retrieved, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, -1); + + /* Retrieve data */ + res = pf_file_load ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &retrieved, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + for (i = 0; i < TEST_FILE_DATA_SIZE; i++) + { + EXPECT_EQ (testdata[i], retrieved[i]); + } + + /* Invalid magic bytes in simulated file */ + temporary_byte = mock_os_data.file_content[0]; + mock_os_data.file_content[0] = 'A'; + res = pf_file_load ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &retrieved, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, -1); + mock_os_data.file_content[0] = temporary_byte; /* Reset to initial value */ + + /* Invalid file version in simulated file */ + temporary_byte = mock_os_data.file_content[4]; + mock_os_data.file_content[4] = 200; + res = pf_file_load ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &retrieved, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, -1); + mock_os_data.file_content[4] = temporary_byte; /* Reset to initial value */ + + /* Verify that mock does not delete file for other name */ + pf_file_clear (TEST_FILE_DIRECTORY, "nonexistent file"); + EXPECT_GT (mock_os_data.file_size, 1); + + /* Clear: Invalid directory */ + pf_file_clear (NULL, TEST_FILE_FILENAME); + pf_file_clear ("", TEST_FILE_FILENAME); + + /* Check that it is OK when we use correct sample data again */ + res = pf_file_load ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &retrieved, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + + /* Verify that the mock can delete file entry */ + pf_file_clear (TEST_FILE_DIRECTORY, TEST_FILE_FILENAME); + EXPECT_EQ (mock_os_data.file_size, 0); /* Verifies mock functionality */ + res = pf_file_load ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &retrieved, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, -1); + + /* Save to current directory */ + res = + pf_file_save (NULL, TEST_FILE_FILENAME, &testdata, TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + EXPECT_EQ (mock_os_data.file_size, TEST_FILE_DATA_SIZE + 8); /* Implementation + detail, but + verifies mock + functionality + */ + + /* Retrieve data from current directory */ + res = + pf_file_load (NULL, TEST_FILE_FILENAME, &retrieved, TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + for (i = 0; i < TEST_FILE_DATA_SIZE; i++) + { + EXPECT_EQ (testdata[i], retrieved[i]); + } + + res = pf_file_load ("", TEST_FILE_FILENAME, &retrieved, TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + for (i = 0; i < TEST_FILE_DATA_SIZE; i++) + { + EXPECT_EQ (testdata[i], retrieved[i]); + } + + /* Delete file in current directory */ + pf_file_clear (NULL, TEST_FILE_FILENAME); + EXPECT_EQ (mock_os_data.file_size, 0); /* Verifies mock functionality */ + res = + pf_file_load (NULL, TEST_FILE_FILENAME, &retrieved, TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, -1); +} + +TEST_F (FileUnitTest, FileCheckSaveIfModified) +{ + uint8_t testdata[TEST_FILE_DATA_SIZE] = + {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}; + uint8_t tempobject[TEST_FILE_DATA_SIZE]; /* Not initialized on purpose */ + uint8_t retrieved[TEST_FILE_DATA_SIZE] = {0}; + int res = 0; + int i; + + pf_file_clear (TEST_FILE_DIRECTORY, TEST_FILE_FILENAME); + + /* First saving */ + res = pf_file_save_if_modified ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &testdata, + &tempobject, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 2); + + /* No update */ + res = pf_file_save_if_modified ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &testdata, + &tempobject, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + + res = pf_file_save_if_modified ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &testdata, + &tempobject, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + + /* Updated data */ + testdata[0] = 's'; + res = pf_file_save_if_modified ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &testdata, + &tempobject, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 1); + + res = pf_file_save_if_modified ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &testdata, + &tempobject, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + + /* Verify updated data */ + res = pf_file_load ( + TEST_FILE_DIRECTORY, + TEST_FILE_FILENAME, + &retrieved, + TEST_FILE_DATA_SIZE); + EXPECT_EQ (res, 0); + + EXPECT_EQ (retrieved[0], 's'); + for (i = 1; i < TEST_FILE_DATA_SIZE; i++) + { + EXPECT_EQ (retrieved[i], testdata[i]); + } +} diff --git a/profinet_stack/p-net/test/test_fspm.cpp b/profinet_stack/p-net/test/test_fspm.cpp new file mode 100644 index 0000000..cc8c237 --- /dev/null +++ b/profinet_stack/p-net/test/test_fspm.cpp @@ -0,0 +1,180 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class FspmTest : public PnetIntegrationTest +{ +}; + +class FspmUnitTest : public PnetUnitTest +{ +}; + +TEST_F (FspmUnitTest, FspmCheckValidateConfiguration) +{ + pnet_cfg_t cfg; + + /* Known good values */ + memset (&cfg, 0, sizeof (pnet_cfg_t)); + cfg.tick_us = 1000; + cfg.min_device_interval = 1; + cfg.im_0_data.im_supported = 0; + cfg.if_cfg.main_netif_name = "eth0"; + cfg.num_physical_ports = PNET_MAX_PHYSICAL_PORTS; + + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + /* Check pointer validity */ + EXPECT_EQ (pf_fspm_validate_configuration (NULL), -1); + + /* Check number of ports */ + cfg.num_physical_ports = PNET_MAX_PHYSICAL_PORTS + 1; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + + cfg.num_physical_ports = 0; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + + cfg.num_physical_ports = 1; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + cfg.num_physical_ports = PNET_MAX_PHYSICAL_PORTS; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + /* Check minimum stack update interval */ + cfg.min_device_interval = 0; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + + cfg.min_device_interval = 0x1000; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + cfg.min_device_interval = 0x1001; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + + cfg.min_device_interval = 0xFFFF; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + cfg.min_device_interval = 1; + + /* Check supported I&M fields */ + cfg.im_0_data.im_supported = PNET_SUPPORTED_IM1; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + cfg.im_0_data.im_supported = PNET_SUPPORTED_IM1 | PNET_SUPPORTED_IM2; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + cfg.im_0_data.im_supported = PNET_SUPPORTED_IM1 | PNET_SUPPORTED_IM2 | + PNET_SUPPORTED_IM3 | PNET_SUPPORTED_IM4; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + cfg.im_0_data.im_supported = PNET_SUPPORTED_IM4; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), 0); + + cfg.im_0_data.im_supported = 1; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + + cfg.im_0_data.im_supported = 0x0020; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + + cfg.im_0_data.im_supported = 0x8000; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + + cfg.im_0_data.im_supported = 0xFFFF; + EXPECT_EQ (pf_fspm_validate_configuration (&cfg), -1); + cfg.im_0_data.im_supported = 0; +} + +TEST_F (FspmTest, FspmGetImLocation) +{ + const char expected[PNET_LOCATION_MAX_SIZE] = " "; + char actual[PNET_LOCATION_MAX_SIZE]; + + memset (actual, 0xff, sizeof (actual)); + + /* Note that as loading from file failed in pf_fspm_init(), location was set + * to its default value, which is all spaces. + */ + pf_fspm_get_im_location (net, actual); + EXPECT_STREQ (actual, expected); + EXPECT_EQ (strlen (actual), 22u); +} + +TEST_F (FspmTest, FspmSaveImLocation) +{ + const char written[PNET_LOCATION_MAX_SIZE] = "location of device"; + const char expected[PNET_LOCATION_MAX_SIZE] = "location of device "; + char actual[PNET_LOCATION_MAX_SIZE]; + + memset (actual, 0xff, sizeof (actual)); + + /* Note that extra spaces are added at the end */ + pf_fspm_save_im_location (net, written); + + /* The file save operation below is performed by the background worker + * during normal execution of the stack. + */ + pf_fspm_save_im (net); + + pf_fspm_get_im_location (net, actual); + EXPECT_STREQ (actual, expected); + EXPECT_EQ (strlen (actual), 22u); + EXPECT_STREQ (mock_file_data.filename, PF_FILENAME_IM); +} + +TEST_F (FspmTest, FspmSaveImLocationShouldAddTermination) +{ + char not_terminated[PNET_LOCATION_MAX_SIZE]; + char actual[PNET_LOCATION_MAX_SIZE]; + + memset (actual, 0xff, sizeof (actual)); + memset (not_terminated, 'n', sizeof (not_terminated)); + + pf_fspm_save_im_location (net, not_terminated); + + /* The file save operation below is performed by the background worker + * during normal execution of the stack. + */ + pf_fspm_save_im (net); + + pf_fspm_get_im_location (net, actual); + EXPECT_EQ (actual[sizeof (actual) - 1], '\0'); + EXPECT_EQ (strlen (actual), 22u); + EXPECT_STREQ (mock_file_data.filename, PF_FILENAME_IM); +} + +TEST_F (FspmTest, FspmSaveImLocationShouldTruncateLargeStrings) +{ + const char large[] = "123456789012345678901234567890"; + const char expected[] = "1234567890123456789012"; + char actual[PNET_LOCATION_MAX_SIZE]; + + memset (actual, 0xff, sizeof (actual)); + + pf_fspm_save_im_location (net, large); + + /* The file save operation below is performed by the background worker + * during normal execution of the stack. + */ + pf_fspm_save_im (net); + + pf_fspm_get_im_location (net, actual); + EXPECT_STREQ (actual, expected); + EXPECT_EQ (strlen (actual), 22u); + EXPECT_STREQ (mock_file_data.filename, PF_FILENAME_IM); +} diff --git a/profinet_stack/p-net/test/test_lldp.cpp b/profinet_stack/p-net/test/test_lldp.cpp new file mode 100644 index 0000000..af4406c --- /dev/null +++ b/profinet_stack/p-net/test/test_lldp.cpp @@ -0,0 +1,1154 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Integration test that a LLDP frame is sent at init. + * + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +#define LOCAL_PORT 1 + +#define MAX_ETH_FRAME_SIZE 1514u +#define MAX_ETH_PAYLOAD_SIZE 1500u + +#define LLDP_TYPE_END 0 +#define LLDP_TYPE_CHASSIS_ID 1 +#define LLDP_TYPE_PORT_ID 2 +#define LLDP_TYPE_TTL 3 +#define LLDP_TYPE_PORT_DESCRIPTION 4 +#define LLDP_TYPE_MANAGEMENT_ADDRESS 8 +#define LLDP_TYPE_ORG_SPEC 127 + +#define TLV_BYTE_0(len, type) (((len) >> 8) | ((type) << 1)) +#define TLV_BYTE_1(len) (((len) >> 0) & 0xff) +#define TLV_TYPE(tlv) ((tlv)[0] >> 1) +#define TLV_LEN(tlv) ((size_t) (((tlv)[0] << 8) + (tlv)[1]) & 0x1FF) + +static const char * chassis_id_test_sample_1 = + "\x53\x43\x41\x4c\x41\x4e\x43\x45\x20\x58\x2d\x32\x30\x30\x20" + "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x36\x47\x4b\x35\x20" + "\x32\x30\x34\x2d\x30\x42\x41\x30\x30\x2d\x32\x42\x41\x33\x20\x20" + "\x53\x56\x50\x4c\x36\x31\x34\x37\x39\x32\x30\x20\x20\x20\x20\x20" + "\x20\x20\x20\x20\x20\x39\x20\x56\x20\x20\x35\x20\x20\x34\x20\x20" + "\x32"; + +static const char * port_id_test_sample_1 = + "\x70\x6f\x72\x74\x2d\x30\x30\x33\x2e\x61\x62\x63\x64\x65\x66\x67" + "\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77" + "\x78\x79\x7a\x2d\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c" + "\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x31\x32" + "\x33\x34\x35\x36\x37\x38\x39\x30\x2e\x61\x62\x63\x64\x65\x66\x67" + "\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77" + "\x78\x79\x7a\x2d\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c" + "\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x31\x32" + "\x2e\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f" + "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x2d\x61\x62\x63\x64" + "\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74" + "\x75\x76\x77\x78\x79\x7a\x31\x32\x33\x2e\x61\x62\x63\x64\x65\x66" + "\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76" + "\x77\x78\x79\x7a\x2d\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b" + "\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x31" + "\x32\x33\x34\x35\x36\x37\x38\x39\x30"; + +static pf_lldp_chassis_id_t fake_chassis_id (void) +{ + pf_lldp_chassis_id_t chassis_id; + + memset (&chassis_id, 0, sizeof (chassis_id)); + snprintf ( + chassis_id.string, + sizeof (chassis_id.string), + "%s", + "Chassis ID received from remote device"); + chassis_id.len = strlen (chassis_id.string); + chassis_id.subtype = PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED; + chassis_id.is_valid = true; + return chassis_id; +} + +static pf_lldp_port_id_t fake_port_id (void) +{ + pf_lldp_port_id_t port_id; + + memset (&port_id, 0, sizeof (port_id)); + snprintf ( + port_id.string, + sizeof (port_id.string), + "%s", + "Port ID received from remote device"); + port_id.len = strlen (port_id.string); + port_id.subtype = PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED; + port_id.is_valid = true; + return port_id; +} + +static uint16_t fake_ttl (void) +{ + return 12345; +} + +static pf_lldp_port_description_t fake_port_description (void) +{ + pf_lldp_port_description_t port_description; + + memset (&port_description, 0, sizeof (port_description)); + snprintf ( + port_description.string, + sizeof (port_description.string), + "%s", + "Port Description"); + port_description.len = strlen (port_description.string); + port_description.is_valid = true; + return port_description; +} + +static pf_lldp_management_address_t fake_ipv4_management_address (void) +{ + pf_lldp_management_address_t management_address; + + memset (&management_address, 0, sizeof (management_address)); + management_address.subtype = 1; /* IPv4 */ + management_address.value[0] = 192; + management_address.value[1] = 168; + management_address.value[2] = 10; + management_address.value[3] = 102; + management_address.len = 4; + management_address.interface_number.subtype = 2; /* ifIndex */ + management_address.interface_number.value = 1; + management_address.is_valid = true; + return management_address; +} + +static pf_lldp_management_address_t fake_ipv6_management_address (void) +{ + pf_lldp_management_address_t management_address; + + memset (&management_address, 0, sizeof (management_address)); + management_address.subtype = 2; /* IPv6 */ + management_address.value[0] = 0x20; + management_address.value[1] = 0x01; + management_address.value[2] = 0x0d; + management_address.value[3] = 0xd8; + management_address.value[4] = 0x85; + management_address.value[5] = 0xa3; + management_address.value[6] = 0x00; + management_address.value[7] = 0x00; + management_address.value[8] = 0x00; + management_address.value[9] = 0x00; + management_address.value[10] = 0x8a; + management_address.value[11] = 0x2e; + management_address.value[12] = 0x03; + management_address.value[13] = 0x70; + management_address.value[14] = 0x73; + management_address.value[15] = 0x34; + management_address.len = 16; + management_address.interface_number.subtype = 2; /* ifIndex */ + management_address.interface_number.value = 1; + management_address.is_valid = true; + return management_address; +} + +static pf_lldp_link_status_t fake_phy_config (void) +{ + pf_lldp_link_status_t phy_config; + + memset (&phy_config, 0, sizeof (phy_config)); + phy_config.is_autonegotiation_supported = true; + phy_config.is_autonegotiation_enabled = true; + phy_config.autonegotiation_advertised_capabilities = + PNAL_ETH_AUTONEG_CAP_100BaseTX_HALF_DUPLEX | + PNAL_ETH_AUTONEG_CAP_100BaseTX_FULL_DUPLEX; + phy_config.operational_mau_type = PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + phy_config.is_valid = true; + return phy_config; +} + +static pnet_ethaddr_t fake_mac_address (void) +{ + pnet_ethaddr_t mac_address; + + memset (&mac_address, 0, sizeof (mac_address)); + mac_address.addr[0] = 0xab; + mac_address.addr[0] = 0xcd; + mac_address.addr[0] = 0xef; + mac_address.addr[0] = 0x01; + mac_address.addr[0] = 0x23; + mac_address.addr[0] = 0x45; + return mac_address; +} + +static pf_lldp_signal_delay_t fake_port_delay (void) +{ + pf_lldp_signal_delay_t port_delay; + + memset (&port_delay, 0, sizeof (port_delay)); + port_delay.cable_delay_local = 1; + port_delay.rx_delay_local = 2; + port_delay.tx_delay_local = 3; + port_delay.rx_delay_remote = 4; + port_delay.tx_delay_remote = 5; + port_delay.is_valid = true; + return port_delay; +} + +static pf_lldp_peer_info_t fake_peer_info (void) +{ + pf_lldp_peer_info_t peer; + + memset (&peer, 0, sizeof (peer)); + peer.chassis_id = fake_chassis_id(); + peer.port_id = fake_port_id(); + peer.ttl = fake_ttl(); + peer.port_description = fake_port_description(); + peer.management_address = fake_ipv4_management_address(); + peer.phy_config = fake_phy_config(); + peer.mac_address = fake_mac_address(); + peer.port_delay = fake_port_delay(); + return peer; +} + +static size_t construct_packet_chassis_id ( + uint8_t packet[], + const pf_lldp_chassis_id_t * id) +{ + size_t size = 0; + + if (id->is_valid) + { + packet[0] = TLV_BYTE_0 (1 + id->len, LLDP_TYPE_CHASSIS_ID); + packet[1] = TLV_BYTE_1 (1 + id->len); + packet[2] = id->subtype; + memcpy (&packet[3], id->string, id->len); + size = 3 + id->len; + } + + return size; +} + +static size_t construct_packet_port_id ( + uint8_t packet[], + const pf_lldp_port_id_t * id) +{ + size_t size = 0; + + if (id->is_valid) + { + packet[0] = TLV_BYTE_0 (1 + id->len, LLDP_TYPE_PORT_ID); + packet[1] = TLV_BYTE_1 (1 + id->len); + packet[2] = id->subtype; + memcpy (&packet[3], id->string, id->len); + size = 3 + id->len; + } + + return size; +} + +static size_t construct_packet_ttl (uint8_t packet[], uint16_t ttl) +{ + size_t size = 0; + + packet[0] = TLV_BYTE_0 (sizeof (ttl), LLDP_TYPE_TTL); + packet[1] = TLV_BYTE_1 (sizeof (ttl)); + packet[2] = ttl >> 8; + packet[3] = ttl >> 0; + size = 4; + + return size; +} + +static size_t construct_packet_port_description ( + uint8_t packet[], + const pf_lldp_port_description_t * description) +{ + size_t size = 0; + + if (description->is_valid) + { + packet[0] = TLV_BYTE_0 (description->len, LLDP_TYPE_PORT_DESCRIPTION); + packet[1] = TLV_BYTE_1 (description->len); + memcpy (&packet[2], description->string, description->len); + size = 2 + description->len; + } + + return size; +} + +static size_t construct_packet_management_address ( + uint8_t packet[], + const pf_lldp_management_address_t * address) +{ + size_t size = 0; + + if (address->is_valid) + { + uint32_t big_endian; + + packet[0] = + TLV_BYTE_0 (2 + address->len + 6, LLDP_TYPE_MANAGEMENT_ADDRESS); + packet[1] = TLV_BYTE_1 (2 + address->len + 6); + packet[2] = 1 + address->len; + packet[3] = address->subtype; + memcpy (&packet[4], address->value, address->len); + size += 4 + address->len; + + big_endian = CC_TO_BE32 (address->interface_number.value); + packet[0 + size] = address->interface_number.subtype; + memcpy (&packet[1 + size], &big_endian, 4); + packet[5 + size] = 0; /* OID string length */ + size += 6; + } + + return size; +} + +static size_t construct_packet_link_status ( + uint8_t packet[], + const pf_lldp_link_status_t * status) +{ + size_t size = 0; + + if (status->is_valid) + { + packet[0] = TLV_BYTE_0 (9, LLDP_TYPE_ORG_SPEC); + packet[1] = TLV_BYTE_1 (9); + packet[2] = 0x00; /* 802.3 OUI */ + packet[3] = 0x12; /* 802.3 OUI */ + packet[4] = 0x0F; /* 802.3 OUI */ + packet[5] = 1; /* subtype */ + packet[6] = ((uint8_t)status->is_autonegotiation_supported << 0) | + ((uint8_t)status->is_autonegotiation_enabled << 1); + packet[7] = status->autonegotiation_advertised_capabilities >> 8; + packet[8] = status->autonegotiation_advertised_capabilities >> 0; + packet[9] = status->operational_mau_type >> 8; + packet[10] = status->operational_mau_type >> 0; + size = 11; + } + + return size; +} + +static size_t construct_packet_signal_delay ( + uint8_t packet[], + const pf_lldp_signal_delay_t * delay) +{ + size_t size = 0; + uint32_t bigendian; + + if (delay->is_valid) + { + packet[0] = TLV_BYTE_0 (24, LLDP_TYPE_ORG_SPEC); + packet[1] = TLV_BYTE_1 (24); + packet[2] = 0x00; /* Profinet OUI */ + packet[3] = 0x0E; /* Profinet OUI */ + packet[4] = 0xCF; /* Profinet OUI */ + packet[5] = 1; /* subtype */ + bigendian = CC_TO_BE32 (delay->rx_delay_local); + memcpy (&packet[6], &bigendian, 4); + bigendian = CC_TO_BE32 (delay->rx_delay_remote); + memcpy (&packet[10], &bigendian, 4); + bigendian = CC_TO_BE32 (delay->tx_delay_local); + memcpy (&packet[14], &bigendian, 4); + bigendian = CC_TO_BE32 (delay->tx_delay_remote); + memcpy (&packet[18], &bigendian, 4); + bigendian = CC_TO_BE32 (delay->cable_delay_local); + memcpy (&packet[22], &bigendian, 4); + size = 26; + } + + return size; +} + +static size_t construct_packet_chassis_mac ( + uint8_t packet[], + const pnet_ethaddr_t * mac_address) +{ + size_t size = 0; + + packet[0] = TLV_BYTE_0 (10, LLDP_TYPE_ORG_SPEC); + packet[1] = TLV_BYTE_1 (10); + packet[2] = 0x00; /* Profinet OUI */ + packet[3] = 0x0E; /* Profinet OUI */ + packet[4] = 0xCF; /* Profinet OUI */ + packet[5] = 5; /* subtype */ + memcpy (&packet[6], mac_address->addr, 6); + size = 12; + + return size; +} + +static size_t construct_packet_end_marker (uint8_t packet[]) +{ + size_t size; + + packet[0] = TLV_BYTE_0 (0, LLDP_TYPE_END); + packet[1] = TLV_BYTE_1 (0); + size = 2; + + return size; +} + +static size_t construct_packet ( + uint8_t packet[MAX_ETH_PAYLOAD_SIZE], + const pf_lldp_peer_info_t * data) +{ + memset (packet, 0xff, MAX_ETH_PAYLOAD_SIZE); + size_t size = 0; + + size += construct_packet_chassis_id (&packet[size], &data->chassis_id); + size += construct_packet_port_id (&packet[size], &data->port_id); + size += construct_packet_ttl (&packet[size], data->ttl); + size += construct_packet_port_description ( + &packet[size], + &data->port_description); + size += construct_packet_management_address ( + &packet[size], + &data->management_address); + size += construct_packet_link_status (&packet[size], &data->phy_config); + size += construct_packet_signal_delay (&packet[size], &data->port_delay); + size += construct_packet_chassis_mac (&packet[size], &data->mac_address); + size += construct_packet_end_marker (&packet[size]); + + return size; +} + +class LldpTest : public PnetIntegrationTestBase +{ + protected: + virtual void SetUp() override + { + mock_init(); + cfg_init(); + appdata_init(); + available_modules_and_submodules_init(); + + callcounter_reset(); + + pnet_init_only (net, &pnet_default_cfg); + + /* Do not clear mock or callcounters here - we need to verify send at init + * from LLDP */ + }; +}; + +TEST_F (LldpTest, LldpInitTest) +{ + /* Verify that LLDP has sent a frame at init */ + EXPECT_EQ (appdata.call_counters.state_calls, 0); + EXPECT_EQ (appdata.call_counters.connect_calls, 0); + EXPECT_EQ (appdata.call_counters.release_calls, 0); + EXPECT_EQ (appdata.call_counters.dcontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.ccontrol_calls, 0); + EXPECT_EQ (appdata.call_counters.read_calls, 0); + EXPECT_EQ (appdata.call_counters.write_calls, 0); + EXPECT_EQ (mock_os_data.eth_send_count, PNET_MAX_PHYSICAL_PORTS); +} + +TEST_F (LldpTest, LldpGenerateAliasName) +{ + char alias[256]; /** Terminated string */ + int err; + + err = pf_lldp_generate_alias_name (NULL, "dut", alias, 96); + EXPECT_EQ (err, -1); + + err = pf_lldp_generate_alias_name ("port-001", NULL, alias, 96); + EXPECT_EQ (err, -1); + + err = pf_lldp_generate_alias_name ("port-001", "dut", NULL, 96); + EXPECT_EQ (err, -1); + + /* Test legacy PN v2.2 + * PortId string does not include a "." + * Alias = PortId.StationName + */ + err = pf_lldp_generate_alias_name ("port-001", "dut", alias, 13); + EXPECT_EQ (err, 0); + EXPECT_EQ (strcmp (alias, "port-001.dut"), 0); + memset (alias, 0, sizeof (alias)); + + err = pf_lldp_generate_alias_name ("port-001", "dut", alias, 12); + EXPECT_EQ (err, -1); + + /* Test PN v2.3+ + * PortId string does include a "." + * Alias = PortId + */ + err = pf_lldp_generate_alias_name ("port-001.dut", "tester", alias, 13); + EXPECT_EQ (err, 0); + EXPECT_EQ (strcmp (alias, "port-001.dut"), 0); + memset (alias, 0, sizeof (alias)); + + err = pf_lldp_generate_alias_name ("port-001.dut", "tester", alias, 12); + EXPECT_EQ (err, -1); + + /* Test data from rt-tester v2.3 style */ + err = pf_lldp_generate_alias_name ( + port_id_test_sample_1, + chassis_id_test_sample_1, + alias, + 256); + EXPECT_EQ (err, 0); + EXPECT_EQ (strcmp (alias, port_id_test_sample_1), 0); + memset (alias, 0, sizeof (alias)); +} + +TEST_F (LldpTest, LldpParsePacket) +{ + uint8_t packet[MAX_ETH_PAYLOAD_SIZE]; + int error; + size_t size; + pf_lldp_peer_info_t actual; + const pf_lldp_peer_info_t expected = fake_peer_info(); + + size = construct_packet (packet, &expected); + memset (&actual, 0xff, sizeof (actual)); + + error = pf_lldp_parse_packet (packet, size, &actual); + + EXPECT_EQ (error, 0); + + EXPECT_EQ (actual.chassis_id.is_valid, expected.chassis_id.is_valid); + EXPECT_EQ (actual.chassis_id.subtype, expected.chassis_id.subtype); + EXPECT_EQ (actual.chassis_id.len, expected.chassis_id.len); + EXPECT_STREQ (actual.chassis_id.string, expected.chassis_id.string); + + EXPECT_EQ (actual.port_id.is_valid, expected.port_id.is_valid); + EXPECT_EQ (actual.port_id.subtype, expected.port_id.subtype); + EXPECT_EQ (actual.port_id.len, expected.port_id.len); + EXPECT_STREQ (actual.port_id.string, expected.port_id.string); + + EXPECT_EQ (actual.ttl, expected.ttl); + + EXPECT_EQ ( + actual.port_description.is_valid, + expected.port_description.is_valid); + EXPECT_EQ (actual.port_description.len, expected.port_description.len); + EXPECT_STREQ ( + actual.port_description.string, + expected.port_description.string); + + EXPECT_EQ ( + actual.management_address.is_valid, + expected.management_address.is_valid); + EXPECT_EQ ( + actual.management_address.subtype, + expected.management_address.subtype); + EXPECT_EQ (actual.management_address.len, expected.management_address.len); + EXPECT_EQ ( + memcmp ( + actual.management_address.value, + expected.management_address.value, + 4), + 0); + EXPECT_EQ ( + actual.management_address.interface_number.subtype, + expected.management_address.interface_number.subtype); + EXPECT_EQ ( + actual.management_address.interface_number.value, + expected.management_address.interface_number.value); + + EXPECT_EQ (actual.phy_config.is_valid, expected.phy_config.is_valid); + EXPECT_EQ ( + actual.phy_config.is_autonegotiation_supported, + expected.phy_config.is_autonegotiation_supported); + EXPECT_EQ ( + actual.phy_config.is_autonegotiation_enabled, + expected.phy_config.is_autonegotiation_enabled); + EXPECT_EQ ( + actual.phy_config.autonegotiation_advertised_capabilities, + expected.phy_config.autonegotiation_advertised_capabilities); + EXPECT_EQ ( + actual.phy_config.operational_mau_type, + expected.phy_config.operational_mau_type); + + EXPECT_EQ (actual.port_delay.is_valid, expected.port_delay.is_valid); + EXPECT_EQ ( + actual.port_delay.rx_delay_local, + expected.port_delay.rx_delay_local); + EXPECT_EQ ( + actual.port_delay.rx_delay_remote, + expected.port_delay.rx_delay_remote); + EXPECT_EQ ( + actual.port_delay.tx_delay_local, + expected.port_delay.tx_delay_local); + EXPECT_EQ ( + actual.port_delay.tx_delay_remote, + expected.port_delay.tx_delay_remote); + EXPECT_EQ ( + actual.port_delay.cable_delay_local, + expected.port_delay.cable_delay_local); + + EXPECT_EQ (memcmp (actual.mac_address.addr, expected.mac_address.addr, 6), 0); + + /* TODO: Add check for port status */ + + /* Finally check that everything is as expected. + * While this is the only check really needed, the checks above will make + * it easier to find out exactly what went wrong. + */ + EXPECT_EQ (memcmp (&actual, &expected, sizeof (expected)), 0); +} + +TEST_F (LldpTest, LldpParsePacketWithTooBigChassisId) +{ + uint8_t packet[MAX_ETH_PAYLOAD_SIZE]; + int error; + size_t size; + pf_lldp_peer_info_t actual; + pf_lldp_peer_info_t peer = fake_peer_info(); + const size_t max_len = sizeof (peer.chassis_id.string) - 1; + + peer.chassis_id.len = max_len + 1; + size = construct_packet (packet, &peer); + memset (&actual, 0xff, sizeof (actual)); + + error = pf_lldp_parse_packet (packet, size, &actual); + + EXPECT_EQ (error, 0); + EXPECT_EQ (actual.chassis_id.is_valid, false); +} + +TEST_F (LldpTest, LldpParsePacketWithTooBigPortId) +{ + uint8_t packet[MAX_ETH_PAYLOAD_SIZE]; + int error; + size_t size; + pf_lldp_peer_info_t actual; + pf_lldp_peer_info_t peer = fake_peer_info(); + const size_t max_len = sizeof (peer.port_id.string) - 1; + + peer.port_id.len = max_len + 1; + size = construct_packet (packet, &peer); + memset (&actual, 0xff, sizeof (actual)); + + error = pf_lldp_parse_packet (packet, size, &actual); + + EXPECT_EQ (error, 0); + EXPECT_EQ (actual.port_id.is_valid, false); +} + +TEST_F (LldpTest, LldpParsePacketWithTooBigPortDescription) +{ + uint8_t packet[MAX_ETH_PAYLOAD_SIZE]; + int error; + size_t size; + pf_lldp_peer_info_t actual; + pf_lldp_peer_info_t peer = fake_peer_info(); + const size_t max_len = sizeof (peer.port_description.string) - 1; + + peer.port_description.len = max_len + 1; + size = construct_packet (packet, &peer); + memset (&actual, 0xff, sizeof (actual)); + + error = pf_lldp_parse_packet (packet, size, &actual); + + EXPECT_EQ (error, 0); + EXPECT_EQ (actual.port_description.is_valid, false); +} + +TEST_F (LldpTest, LldpParsePacketWithTooBigManagementAddress) +{ + uint8_t packet[MAX_ETH_PAYLOAD_SIZE]; + int error; + size_t size; + pf_lldp_peer_info_t actual; + pf_lldp_peer_info_t peer = fake_peer_info(); + const size_t max_len = sizeof (peer.management_address.value); + + peer.management_address.len = max_len + 1; + size = construct_packet (packet, &peer); + memset (&actual, 0xff, sizeof (actual)); + + error = pf_lldp_parse_packet (packet, size, &actual); + + EXPECT_EQ (error, 0); + EXPECT_EQ (actual.management_address.is_valid, false); +} + +TEST_F (LldpTest, LldpParsePacketWithMultipleManagementAddresses) +{ + uint8_t packet[MAX_ETH_PAYLOAD_SIZE]; + int error; + size_t size; + pf_lldp_peer_info_t actual; + const pf_lldp_chassis_id_t chassis_id = fake_chassis_id(); + const pf_lldp_port_id_t port_id = fake_port_id(); + const uint16_t ttl = fake_ttl(); + const pf_lldp_management_address_t ipv4 = fake_ipv4_management_address(); + const pf_lldp_management_address_t ipv6 = fake_ipv6_management_address(); + + /* Construct LLDP packet with IPv4 and IPv6 addresses (in that order) */ + memset (packet, 0xff, MAX_ETH_PAYLOAD_SIZE); + size = 0; + size += construct_packet_chassis_id (&packet[size], &chassis_id); + size += construct_packet_port_id (&packet[size], &port_id); + size += construct_packet_ttl (&packet[size], ttl); + size += construct_packet_management_address (&packet[size], &ipv4); + size += construct_packet_management_address (&packet[size], &ipv6); + size += construct_packet_end_marker (&packet[size]); + + memset (&actual, 0xff, sizeof (actual)); + + error = pf_lldp_parse_packet (packet, size, &actual); + + /* Note that only the IPv4 address should be saved, as IPv6 addresses are + * of no use for the p-net stack. + */ + EXPECT_EQ (error, 0); + EXPECT_EQ (actual.management_address.is_valid, true); + EXPECT_EQ (actual.management_address.subtype, ipv4.subtype); + EXPECT_EQ (actual.management_address.len, 4u); +} + +TEST_F (LldpTest, LldpParsePacketWithInvalidManagementAddresses) +{ + uint8_t packet[MAX_ETH_PAYLOAD_SIZE]; + int error; + size_t size; + pf_lldp_peer_info_t actual; + const pf_lldp_chassis_id_t chassis_id = fake_chassis_id(); + const pf_lldp_port_id_t port_id = fake_port_id(); + const uint16_t ttl = fake_ttl(); + pf_lldp_management_address_t address; + + address = fake_ipv4_management_address(); + address.len = 5; + + /* Construct LLDP packet */ + memset (packet, 0xff, MAX_ETH_PAYLOAD_SIZE); + size = 0; + size += construct_packet_chassis_id (&packet[size], &chassis_id); + size += construct_packet_port_id (&packet[size], &port_id); + size += construct_packet_ttl (&packet[size], ttl); + size += construct_packet_management_address (&packet[size], &address); + size += construct_packet_end_marker (&packet[size]); + + memset (&actual, 0xff, sizeof (actual)); + + error = pf_lldp_parse_packet (packet, size, &actual); + + EXPECT_EQ (error, 0); + EXPECT_EQ (actual.management_address.is_valid, false); +} + +TEST_F (LldpTest, LldpGetChassisId) +{ + pf_lldp_chassis_id_t chassis_id; + + memset (&chassis_id, 0xff, sizeof (chassis_id)); + + pf_lldp_get_chassis_id (net, &chassis_id); + EXPECT_EQ (chassis_id.subtype, PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED); + EXPECT_STREQ ( + chassis_id.string, + "PNET unit tests 1 P " + " 0 0 0"); + EXPECT_EQ ( + chassis_id.len, + strlen ("PNET unit tests " + " 1 P 0 0 0")); +} + +TEST_F (LldpTest, LldpGetPortId) +{ + pf_lldp_port_id_t port_id; + + memset (&port_id, 0xff, sizeof (port_id)); + + pf_lldp_get_port_id (net, LOCAL_PORT, &port_id); + EXPECT_EQ (port_id.subtype, PF_LLDP_SUBTYPE_LOCALLY_ASSIGNED); + EXPECT_STREQ (port_id.string, "port-001.12-34-00-78-90-AB"); + EXPECT_EQ (port_id.len, strlen ("port-001.12-34-00-78-90-AB")); +} + +TEST_F (LldpTest, LldpGetPortDescription) +{ + pf_lldp_port_description_t port_desc; + + memset (&port_desc, 0xff, sizeof (port_desc)); + + pf_lldp_get_port_description (net, LOCAL_PORT, &port_desc); + EXPECT_STREQ (port_desc.string, TEST_INTERFACE_NAME); + EXPECT_EQ (port_desc.len, strlen (TEST_INTERFACE_NAME)); +} + +TEST_F (LldpTest, LldpGetManagementAddress) +{ + pf_lldp_management_address_t man_address; + + mock_os_data.interface_index = 3; + memset (&man_address, 0xff, sizeof (man_address)); + + pf_lldp_get_management_address (net, &man_address); + EXPECT_EQ (man_address.subtype, 1); /* IPv4 */ + EXPECT_EQ (man_address.value[0], 192); + EXPECT_EQ (man_address.value[1], 168); + EXPECT_EQ (man_address.value[2], 1); + EXPECT_EQ (man_address.value[3], 171); + EXPECT_EQ (man_address.len, 4u); + EXPECT_EQ (man_address.interface_number.subtype, 2); /* ifIndex */ + EXPECT_EQ (man_address.interface_number.value, 3); +} + +TEST_F (LldpTest, LldpGetSignalDelays) +{ + pf_lldp_signal_delay_t delays; + + memset (&delays, 0xff, sizeof (delays)); + + pf_lldp_get_signal_delays (net, LOCAL_PORT, &delays); + EXPECT_EQ (delays.tx_delay_local, 0u); /* Not measured */ + EXPECT_EQ (delays.rx_delay_local, 0u); /* Not measured */ + EXPECT_EQ (delays.cable_delay_local, 0u); /* Not measured */ +} + +TEST_F (LldpTest, LldpGetLinkStatus) +{ + pf_lldp_link_status_t link_status; + + mock_os_data.eth_status[LOCAL_PORT].is_autonegotiation_supported = true; + mock_os_data.eth_status[LOCAL_PORT].is_autonegotiation_enabled = true; + mock_os_data.eth_status[LOCAL_PORT].autonegotiation_advertised_capabilities = + PNAL_ETH_AUTONEG_CAP_100BaseTX_HALF_DUPLEX | + PNAL_ETH_AUTONEG_CAP_100BaseTX_FULL_DUPLEX; + mock_os_data.eth_status[LOCAL_PORT].operational_mau_type = + PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + memset (&link_status, 0xff, sizeof (link_status)); + + /* The ethernet status is updated by the background worker */ + pf_pdport_update_eth_status (net); + + pf_lldp_get_link_status (net, LOCAL_PORT, &link_status); + EXPECT_EQ (link_status.is_autonegotiation_supported, true); + EXPECT_EQ (link_status.is_autonegotiation_enabled, true); + EXPECT_EQ ( + link_status.autonegotiation_advertised_capabilities, + PNAL_ETH_AUTONEG_CAP_100BaseTX_HALF_DUPLEX | + PNAL_ETH_AUTONEG_CAP_100BaseTX_FULL_DUPLEX); + EXPECT_EQ ( + link_status.operational_mau_type, + PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX); +} + +TEST_F (LldpTest, LldpGetLinkStatusGivenAutonegotiationDisabled) +{ + pf_lldp_link_status_t link_status; + + mock_os_data.eth_status[LOCAL_PORT].is_autonegotiation_supported = false; + mock_os_data.eth_status[LOCAL_PORT].is_autonegotiation_enabled = false; + mock_os_data.eth_status[LOCAL_PORT].autonegotiation_advertised_capabilities = + 0x0000; + mock_os_data.eth_status[LOCAL_PORT].operational_mau_type = + PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX; + memset (&link_status, 0xff, sizeof (link_status)); + + pf_pdport_update_eth_status (net); + + pf_lldp_get_link_status (net, LOCAL_PORT, &link_status); + EXPECT_EQ (link_status.is_autonegotiation_supported, false); + EXPECT_EQ (link_status.is_autonegotiation_enabled, false); + EXPECT_EQ (link_status.autonegotiation_advertised_capabilities, 0x0000); + EXPECT_EQ ( + link_status.operational_mau_type, + PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX); +} + +TEST_F (LldpTest, LldpGetPeerTimestamp) +{ + uint32_t actual; + const uint32_t expected = 0x87654321; + int error; + pf_lldp_peer_info_t received_data; + + /* Nothing received */ + error = pf_lldp_get_peer_timestamp (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, -1); + + /* Receive fake data at fake timestamp */ + mock_os_data.system_uptime_10ms = expected; + received_data = fake_peer_info(); + pf_lldp_store_peer_info (net, LOCAL_PORT, &received_data); + + error = pf_lldp_get_peer_timestamp (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, 0); + EXPECT_EQ (actual, expected); +} + +TEST_F (LldpTest, LldpGetPeerChassisId) +{ + const pf_lldp_peer_info_t received = fake_peer_info(); + const pf_lldp_chassis_id_t expected = received.chassis_id; + pf_lldp_chassis_id_t actual; + int error; + + /* Nothing received */ + error = pf_lldp_get_peer_chassis_id (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, -1); + + /* Receive fake data */ + pf_lldp_store_peer_info (net, LOCAL_PORT, &received); + + error = pf_lldp_get_peer_chassis_id (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, 0); + EXPECT_EQ (memcmp (&actual, &expected, sizeof (actual)), 0); +} + +TEST_F (LldpTest, LldpGetPeerPortId) +{ + const pf_lldp_peer_info_t received = fake_peer_info(); + const pf_lldp_port_id_t expected = received.port_id; + pf_lldp_port_id_t actual; + int error; + + /* Nothing received */ + error = pf_lldp_get_peer_port_id (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, -1); + + /* Receive fake data */ + pf_lldp_store_peer_info (net, LOCAL_PORT, &received); + + error = pf_lldp_get_peer_port_id (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, 0); + EXPECT_EQ (memcmp (&actual, &expected, sizeof (actual)), 0); +} + +TEST_F (LldpTest, LldpGetPeerPortDescription) +{ + const pf_lldp_peer_info_t received = fake_peer_info(); + const pf_lldp_port_description_t expected = received.port_description; + pf_lldp_port_description_t actual; + int error; + + /* Nothing received */ + error = pf_lldp_get_peer_port_description (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, -1); + + /* Receive fake data */ + pf_lldp_store_peer_info (net, LOCAL_PORT, &received); + + error = pf_lldp_get_peer_port_description (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, 0); + EXPECT_EQ (memcmp (&actual, &expected, sizeof (actual)), 0); +} + +TEST_F (LldpTest, LldpGetPeerManagementAddress) +{ + const pf_lldp_peer_info_t received = fake_peer_info(); + const pf_lldp_management_address_t expected = received.management_address; + pf_lldp_management_address_t actual; + int error; + + /* Nothing received */ + error = pf_lldp_get_peer_management_address (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, -1); + + /* Receive fake data */ + pf_lldp_store_peer_info (net, LOCAL_PORT, &received); + + error = pf_lldp_get_peer_management_address (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, 0); + EXPECT_EQ (memcmp (&actual, &expected, sizeof (actual)), 0); +} + +TEST_F (LldpTest, LldpGetPeerStationName) +{ + pf_lldp_station_name_t station_name; + int error; + + /* Nothing received */ + error = pf_lldp_get_peer_station_name (net, LOCAL_PORT, &station_name); + EXPECT_EQ (error, -1); + + /* TODO: Add test case for scenario where LLDP packet has been received */ +} + +TEST_F (LldpTest, LldpGetPeerSignalDelays) +{ + const pf_lldp_peer_info_t received = fake_peer_info(); + const pf_lldp_signal_delay_t expected = received.port_delay; + pf_lldp_signal_delay_t actual; + int error; + + /* Nothing received */ + error = pf_lldp_get_peer_signal_delays (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, -1); + + /* Receive fake data */ + pf_lldp_store_peer_info (net, LOCAL_PORT, &received); + + error = pf_lldp_get_peer_signal_delays (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, 0); + EXPECT_EQ (memcmp (&actual, &expected, sizeof (actual)), 0); +} + +TEST_F (LldpTest, LldpGetPeerLinkStatus) +{ + const pf_lldp_peer_info_t received = fake_peer_info(); + const pf_lldp_link_status_t expected = received.phy_config; + pf_lldp_link_status_t actual; + int error; + + /* Nothing received */ + error = pf_lldp_get_peer_link_status (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, -1); + + /* Receive fake data */ + pf_lldp_store_peer_info (net, LOCAL_PORT, &received); + + error = pf_lldp_get_peer_link_status (net, LOCAL_PORT, &actual); + EXPECT_EQ (error, 0); + EXPECT_EQ (memcmp (&actual, &expected, sizeof (actual)), 0); +} + +TEST_F (LldpTest, LldpConstructFrame) +{ + uint8_t frame[MAX_ETH_FRAME_SIZE]; + const uint8_t * p; + const uint8_t * tlv; + size_t returned_size; + size_t size; + + mock_os_data.interface_index = 3; + + returned_size = pf_lldp_construct_frame (net, LOCAL_PORT, frame); + + EXPECT_GT (returned_size, 14u); + EXPECT_LE (returned_size, MAX_ETH_FRAME_SIZE); + + size = 0; + + /* Check destination MAC address (from LLDP standard) */ + p = &frame[size]; + EXPECT_EQ (p[0], 0x01); + EXPECT_EQ (p[1], 0x80); + EXPECT_EQ (p[2], 0xC2); + EXPECT_EQ (p[3], 0x00); + EXPECT_EQ (p[4], 0x00); + EXPECT_EQ (p[5], 0x0E); + size += 6; + + /* Check source MAC address (from app configuration) */ + p = &frame[size]; + EXPECT_EQ (p[0], 0x12); + EXPECT_EQ (p[1], 0x34); + EXPECT_EQ (p[2], 0x00); + EXPECT_EQ (p[3], 0x78); + EXPECT_EQ (p[4], 0x90); + EXPECT_EQ (p[5], 0xab); + size += 6; + + /* Check EtherType (from LLDP standard) */ + p = &frame[size]; + EXPECT_EQ (p[0], 0x88); + EXPECT_EQ (p[1], 0xCC); + size += 2; + + /* Check Chassis ID TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_CHASSIS_ID); + EXPECT_GE (TLV_LEN (tlv), 2u); + EXPECT_LE (TLV_LEN (tlv), 256u); + size += 2 + TLV_LEN (tlv); + + /* Check Port ID TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_PORT_ID); + EXPECT_GE (TLV_LEN (tlv), 2u); + EXPECT_LE (TLV_LEN (tlv), 256u); + EXPECT_EQ (tlv[2], 7u); /* Subtype */ + size += 2 + TLV_LEN (tlv); + + /* Check Time To Live TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_TTL); + EXPECT_EQ (TLV_LEN (tlv), 2u); + EXPECT_EQ (tlv[2], 0u); /* MSB of TTL */ + EXPECT_EQ (tlv[3], 20u); /* LSB of TTL */ + size += 2 + TLV_LEN (tlv); + + /* Note that the order in which TLVs below are placed in the packet + * is not specified by LLDP or Profinet (except for the End TTL). + */ + + /* Check LLDP_PNIO_PORTSTATUS TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_ORG_SPEC); + EXPECT_EQ (TLV_LEN (tlv), 3 + 1 + 4u); + EXPECT_EQ (tlv[2], 0x00); /* Profinet OUI */ + EXPECT_EQ (tlv[3], 0x0E); /* Profinet OUI */ + EXPECT_EQ (tlv[4], 0xCF); /* Profinet OUI */ + EXPECT_EQ (tlv[5], 0x02); /* Subtype */ + size += 2 + TLV_LEN (tlv); + + /* Check LLDP_PNIO_CHASSIS_MAC TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_ORG_SPEC); + EXPECT_EQ (TLV_LEN (tlv), 3 + 1 + 6u); + EXPECT_EQ (tlv[2], 0x00); /* Profinet OUI */ + EXPECT_EQ (tlv[3], 0x0E); /* Profinet OUI */ + EXPECT_EQ (tlv[4], 0xCF); /* Profinet OUI */ + EXPECT_EQ (tlv[5], 0x05); /* Subtype */ + EXPECT_EQ (tlv[6], 0x12); /* MAC address */ + EXPECT_EQ (tlv[7], 0x34); /* MAC address */ + EXPECT_EQ (tlv[8], 0x00); /* MAC address */ + EXPECT_EQ (tlv[9], 0x78); /* MAC address */ + EXPECT_EQ (tlv[10], 0x90); /* MAC address */ + EXPECT_EQ (tlv[11], 0xab); /* MAC address */ + size += 2 + TLV_LEN (tlv); + + /* Check 802.3 MAC/PHY Configuration/Status TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_ORG_SPEC); + EXPECT_EQ (TLV_LEN (tlv), 3 + 1 + 5u); + EXPECT_EQ (tlv[2], 0x00); /* 802.3 OUI */ + EXPECT_EQ (tlv[3], 0x12); /* 802.3 OUI */ + EXPECT_EQ (tlv[4], 0x0F); /* 802.3 OUI */ + EXPECT_EQ (tlv[5], 0x01); /* Subtype */ + size += 2 + TLV_LEN (tlv); + + /* Check Management Address TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_MANAGEMENT_ADDRESS); + EXPECT_EQ (TLV_LEN (tlv), 1 + 5 + 5 + 1u); + EXPECT_EQ (tlv[2], 1u + 4u); /* Size of Subtype+Address */ + EXPECT_EQ (tlv[3], 1u); /* Subtype IPv4 */ + EXPECT_EQ (tlv[4], 192u); + EXPECT_EQ (tlv[5], 168u); + EXPECT_EQ (tlv[6], 1u); + EXPECT_EQ (tlv[7], 171u); + EXPECT_EQ (tlv[8], 2u); /* Subtype ifIndex */ + EXPECT_EQ (tlv[9], 0u); /* ifIndex MSB */ + EXPECT_EQ (tlv[10], 0u); /* ifIndex */ + EXPECT_EQ (tlv[11], 0u); /* ifIndex */ + EXPECT_EQ (tlv[12], 3u); /* ifIndex LSB */ + EXPECT_EQ (tlv[13], 0u); /* Size of OID */ + size += 2 + TLV_LEN (tlv); + + /* Check End Of LLDPPDU TLV */ + tlv = &frame[size]; + EXPECT_EQ (TLV_TYPE (tlv), LLDP_TYPE_END); + EXPECT_EQ (TLV_LEN (tlv), 0u); + size += 2 + TLV_LEN (tlv); + + EXPECT_EQ (size, returned_size); +} diff --git a/profinet_stack/p-net/test/test_pnetapi.cpp b/profinet_stack/p-net/test/test_pnetapi.cpp new file mode 100644 index 0000000..13db969 --- /dev/null +++ b/profinet_stack/p-net/test/test_pnetapi.cpp @@ -0,0 +1,429 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Integration tests of API functions + * + * For example + * pnet_output_get_data_and_iops() + * pnet_input_get_iocs() + * pnet_output_set_iocs() + * pnet_create_log_book_entry() + * pnet_show() + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class PnetapiTest : public PnetIntegrationTest +{ +}; + +// clang-format off + +static uint8_t connect_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x72, 0x01, + 0x00, 0x00, 0x24, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x42, 0x01, 0x00, 0x00, 0x01, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0xde, 0xa0, + 0x00, 0x00, 0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x01, 0x40, 0x00, + 0x00, 0x11, 0x02, 0x58, 0x88, 0x92, 0x00, 0x0c, 0x72, 0x74, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2d, + 0x64, 0x65, 0x6d, 0x6f, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x05, 0x01, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x02, + 0x00, 0x02, 0x88, 0x92, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x80, 0x00, 0x00, 0x20, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, 0x01, 0x04, 0x00, 0x3c, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x04, 0x00, 0x26, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, + 0x00, 0x02, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x01, 0x88, 0x92, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x02, 0x00, 0xc8, 0xc0, 0x00, 0xa0, 0x00 +}; + +static uint8_t release_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x14, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 +}; + +static uint8_t prm_end_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x10, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 +}; + +static uint8_t appl_rdy_rsp[] = +{ + 0x04, 0x02, 0x0a, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x00, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0x79, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x81, 0x12, + 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00 +}; + +static uint8_t write_req[] = +{ + 0x04, 0x00, 0x28, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0x01, 0xbe, 0xef, + 0xfe, 0xed, 0x01, 0x00, 0xa0, 0xde, 0x97, 0x6c, 0xd1, 0x11, 0x82, 0x71, 0x00, 0xa0, 0x24, 0x42, + 0xdf, 0x7d, 0xbb, 0xac, 0x97, 0xe2, 0x76, 0x54, 0x9f, 0x47, 0xa5, 0xbd, 0xa5, 0xe3, 0x7d, 0x98, + 0xe5, 0xda, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x3c, 0x01, 0x00, 0x00, 0x00, 0x30, 0xab, 0xa9, 0xa3, 0xf7, 0x64, 0xb7, 0x44, 0xb3, 0xb6, + 0x7e, 0xe2, 0x8a, 0x1a, 0x02, 0xcb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x7c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0xa0, + 0xbe, 0xda +}; + +static uint8_t data_packet1_bad_iops_bad_iocs[] = +{ + 0x1e, 0x30, 0x6c, 0xa2, 0x45, 0x5e, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0x88, 0x92, 0x80, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x20, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf3, 0x35, 0x00 +}; + +static uint8_t data_packet2_bad_iops_good_iocs[] = +{ + 0x1e, 0x30, 0x6c, 0xa2, 0x45, 0x5e, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0x88, 0x92, 0x80, 0x00, + 0x80, 0x80, 0x80, 0x80, 0x21, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf4, 0x35, 0x00 +}; + +static uint8_t data_packet3_good_iops_bad_iocs[] = +{ + 0x1e, 0x30, 0x6c, 0xa2, 0x45, 0x5e, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0x88, 0x92, 0x80, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x22, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf5, 0x35, 0x00 +}; + +static uint8_t data_packet4_good_iops_good_iocs[] = +{ + 0x1e, 0x30, 0x6c, 0xa2, 0x45, 0x5e, 0xc8, 0x5b, 0x76, 0xe6, 0x89, 0xdf, 0x88, 0x92, 0x80, 0x00, + 0x80, 0x80, 0x80, 0x80, 0x23, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0x35, 0x00 +}; + +// clang-format on + +TEST_F (PnetapiTest, PnetapiRunTest) +{ + int ret; + pnet_pnio_status_t pnio_status = {1, 2, 3, 4}; + bool new_flag = false; + uint8_t in_data[10]; + uint16_t in_len = sizeof (in_data); + uint8_t out_data[] = { + 0x33, /* Slot 1, subslot 1 Data */ + }; + uint8_t iops = PNET_IOXS_BAD; + uint8_t iocs = PNET_IOXS_BAD; + uint32_t ix; + const uint16_t slot = 1; + const uint16_t subslot = 1; + + TEST_TRACE ("\nGenerating mock connection request\n"); + mock_set_pnal_udp_recvfrom_buffer (connect_req, sizeof (connect_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_GT (mock_os_data.eth_send_count, 0); + + TEST_TRACE ("\nGenerating mock write request\n"); + mock_set_pnal_udp_recvfrom_buffer (write_req, sizeof (write_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nGenerating mock parameter end request\n"); + mock_set_pnal_udp_recvfrom_buffer (prm_end_req, sizeof (prm_end_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_PRMEND); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + + TEST_TRACE ("\nSimulate application calling APPL_RDY\n"); + ret = pnet_application_ready (net, appdata.main_arep); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nGenerating mock application ready response\n"); + mock_set_pnal_udp_recvfrom_buffer (appl_rdy_rsp, sizeof (appl_rdy_rsp)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 3); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_APPLRDY); + + TEST_TRACE ("\nTry receiving data before any received\n"); + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, -1); + EXPECT_EQ (new_flag, false); + EXPECT_EQ (in_len, 0); + EXPECT_EQ (iops, PNET_IOXS_BAD); + + TEST_TRACE ("\nTest data with bad IOPS and bad IOCS\n"); + for (ix = 0; ix < 100; ix++) + { + send_data ( + data_packet1_bad_iops_bad_iocs, + sizeof (data_packet1_bad_iops_bad_iocs)); + run_stack (TEST_DATA_DELAY); + } + + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (in_data[0], 0x20); + EXPECT_EQ (iops, 0x05); + + iocs = 77; /* Something non-valid */ + ret = pnet_input_get_iocs (net, TEST_API_IDENT, slot, subslot, &iocs); + EXPECT_EQ (ret, 0); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iocs, 0x04); + + TEST_TRACE ("\nTest data with bad IOPS and good IOCS\n"); + for (ix = 0; ix < 100; ix++) + { + send_data ( + data_packet2_bad_iops_good_iocs, + sizeof (data_packet2_bad_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + } + + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (in_data[0], 0x21); + EXPECT_EQ (iops, 0x05); + + iocs = 77; /* Something non-valid */ + ret = pnet_input_get_iocs (net, TEST_API_IDENT, slot, subslot, &iocs); + EXPECT_EQ (ret, 0); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iocs, PNET_IOXS_GOOD); + + TEST_TRACE ("\nTest data with good IOPS and bad IOCS\n"); + for (ix = 0; ix < 100; ix++) + { + send_data ( + data_packet3_good_iops_bad_iocs, + sizeof (data_packet3_good_iops_bad_iocs)); + run_stack (TEST_DATA_DELAY); + } + + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (in_data[0], 0x22); + EXPECT_EQ (iops, PNET_IOXS_GOOD); + + iocs = 77; /* Something non-valid */ + ret = pnet_input_get_iocs (net, TEST_API_IDENT, slot, subslot, &iocs); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iocs, 0x04); + + TEST_TRACE ("\nTest data with good IOPS and good IOCS\n"); + for (ix = 0; ix < 100; ix++) + { + send_data ( + data_packet4_good_iops_good_iocs, + sizeof (data_packet4_good_iops_good_iocs)); + run_stack (TEST_DATA_DELAY); + } + + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (in_data[0], 0x23); + EXPECT_EQ (iops, PNET_IOXS_GOOD); + + iocs = 77; /* Something non-valid */ + ret = pnet_input_get_iocs (net, TEST_API_IDENT, slot, subslot, &iocs); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, true); + EXPECT_EQ (in_len, 1); + EXPECT_EQ (iocs, PNET_IOXS_GOOD); + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + TEST_TRACE ("\nRead more data when no new data received\n"); + iops = 88; /* Something non-valid */ + in_len = sizeof (in_data); + in_data[0] = 0; + new_flag = (bool)0x33; + ret = pnet_output_get_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + &new_flag, + in_data, + &in_len, + &iops); + EXPECT_EQ (ret, 0); + EXPECT_EQ (new_flag, false); + EXPECT_EQ (in_len, 1); /* Received one byte (same as before) */ + EXPECT_EQ (in_data[0], 0x23); /* Same as before */ + EXPECT_EQ (iops, PNET_IOXS_GOOD); /* Same as before */ + + TEST_TRACE ("\nSend some data to the controller\n"); + TEST_TRACE ("Line %d\n", __LINE__); + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + slot, + subslot, + out_data, + sizeof (out_data), + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + + TEST_TRACE ("\nAcknowledge the reception of controller data\n"); + ret = + pnet_output_set_iocs (net, TEST_API_IDENT, slot, subslot, PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + EXPECT_EQ (appdata.call_counters.state_calls, 4); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_DATA); + + TEST_TRACE ("\nCreate a logbook entry\n"); + pnet_create_log_book_entry (net, appdata.main_arep, &pnio_status, 0x13245768); + + TEST_TRACE ("\nGenerating mock release request\n"); + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.release_calls, 1); + EXPECT_EQ (appdata.call_counters.state_calls, 5); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} + +TEST_F (PnetapiTest, PnetapiShowTest) +{ + mock_set_pnal_udp_recvfrom_buffer (connect_req, sizeof (connect_req)); + run_stack (TEST_UDP_DELAY); + EXPECT_EQ (appdata.call_counters.state_calls, 1); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_STARTUP); + EXPECT_EQ (appdata.call_counters.connect_calls, 1); + EXPECT_GT (mock_os_data.eth_send_count, 0); + + pnet_show (net, 0x7fffffff); + + mock_set_pnal_udp_recvfrom_buffer (release_req, sizeof (release_req)); + run_stack (TEST_UDP_DELAY); + + EXPECT_EQ (appdata.call_counters.release_calls, 1); + EXPECT_EQ (appdata.call_counters.state_calls, 2); + EXPECT_EQ (appdata.cmdev_state, PNET_EVENT_ABORT); +} diff --git a/profinet_stack/p-net/test/test_port.cpp b/profinet_stack/p-net/test/test_port.cpp new file mode 100644 index 0000000..63ba5be --- /dev/null +++ b/profinet_stack/p-net/test/test_port.cpp @@ -0,0 +1,249 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class PortTest : public PnetIntegrationTest +{ +}; + +class PortUnitTest : public PnetUnitTest +{ + protected: + pnet_t dummystack = {}; +}; + +TEST_F (PortUnitTest, PortGetPortList) +{ + pf_lldp_port_list_t port_list; + + memset (&port_list, 0xff, sizeof (port_list)); + + dummystack.fspm_cfg.num_physical_ports = 1; + pf_port_get_list_of_ports (&dummystack, &port_list); + EXPECT_EQ (port_list.ports[0], 0x80); + EXPECT_EQ (port_list.ports[1], 0x00); + + dummystack.fspm_cfg.num_physical_ports = 2; + pf_port_get_list_of_ports (&dummystack, &port_list); + EXPECT_EQ (port_list.ports[0], 0xC0); + EXPECT_EQ (port_list.ports[1], 0x00); + + dummystack.fspm_cfg.num_physical_ports = 4; + pf_port_get_list_of_ports (&dummystack, &port_list); + EXPECT_EQ (port_list.ports[0], 0xF0); + EXPECT_EQ (port_list.ports[1], 0x00); + + dummystack.fspm_cfg.num_physical_ports = 8; + pf_port_get_list_of_ports (&dummystack, &port_list); + EXPECT_EQ (port_list.ports[0], 0xFF); + EXPECT_EQ (port_list.ports[1], 0x00); + + dummystack.fspm_cfg.num_physical_ports = 9; + pf_port_get_list_of_ports (&dummystack, &port_list); + EXPECT_EQ (port_list.ports[0], 0xFF); + EXPECT_EQ (port_list.ports[1], 0x80); + + dummystack.fspm_cfg.num_physical_ports = 12; + pf_port_get_list_of_ports (&dummystack, &port_list); + EXPECT_EQ (port_list.ports[0], 0xFF); + EXPECT_EQ (port_list.ports[1], 0xF0); + + dummystack.fspm_cfg.num_physical_ports = 16; + pf_port_get_list_of_ports (&dummystack, &port_list); + EXPECT_EQ (port_list.ports[0], 0xFF); + EXPECT_EQ (port_list.ports[1], 0xFF); +} + +TEST_F (PortUnitTest, PortCheckIterator) +{ + pf_port_iterator_t port_iterator; + + dummystack.fspm_cfg.num_physical_ports = 6; + + pf_port_init_iterator_over_ports (&dummystack, &port_iterator); + + EXPECT_EQ (pf_port_get_next (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next (&port_iterator), 2); + EXPECT_EQ (pf_port_get_next (&port_iterator), 3); + EXPECT_EQ (pf_port_get_next (&port_iterator), 4); + EXPECT_EQ (pf_port_get_next (&port_iterator), 5); + EXPECT_EQ (pf_port_get_next (&port_iterator), 6); + + /* Verify that we return 0 when we are done, and no auto restart */ + EXPECT_EQ (pf_port_get_next (&port_iterator), 0); + EXPECT_EQ (pf_port_get_next (&port_iterator), 0); + EXPECT_EQ (pf_port_get_next (&port_iterator), 0); + + /* Verify behaviour if the iterator for unknown reason has + * value that is out of port range */ + port_iterator.next_port = 10; + EXPECT_EQ (pf_port_get_next (&port_iterator), 0); + EXPECT_EQ (pf_port_get_next (&port_iterator), 0); + + /* Restart the iterator */ + pf_port_init_iterator_over_ports (&dummystack, &port_iterator); + + EXPECT_EQ (pf_port_get_next (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next (&port_iterator), 2); + EXPECT_EQ (pf_port_get_next (&port_iterator), 3); + EXPECT_EQ (pf_port_get_next (&port_iterator), 4); + EXPECT_EQ (pf_port_get_next (&port_iterator), 5); + EXPECT_EQ (pf_port_get_next (&port_iterator), 6); + + /* Different number of ports */ + dummystack.fspm_cfg.num_physical_ports = 1; + pf_port_init_iterator_over_ports (&dummystack, &port_iterator); + + EXPECT_EQ (pf_port_get_next (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next (&port_iterator), 0); + EXPECT_EQ (pf_port_get_next (&port_iterator), 0); +} + +TEST_F (PortUnitTest, PortCheckIteratorRepeatCyclic) +{ + pf_port_iterator_t port_iterator; + + dummystack.fspm_cfg.num_physical_ports = 6; + + pf_port_init_iterator_over_ports (&dummystack, &port_iterator); + + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 2); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 3); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 4); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 5); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 6); + + /* Verify auto restart */ + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 2); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 3); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 4); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 5); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 6); + + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 2); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 3); + + /* Verify behaviour if the iterator for unknown reason has + * value that is out of port range */ + port_iterator.next_port = 10; + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 2); + + /* Restart the iterator from first port */ + pf_port_init_iterator_over_ports (&dummystack, &port_iterator); + + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 2); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 3); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 4); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 5); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 6); + + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 2); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 3); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 4); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 5); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 6); + + /* Different number of ports */ + dummystack.fspm_cfg.num_physical_ports = 1; + pf_port_init_iterator_over_ports (&dummystack, &port_iterator); + + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); + EXPECT_EQ (pf_port_get_next_repeat_cyclic (&port_iterator), 1); +} + +TEST_F (PortUnitTest, PortNumToSubslot) +{ + dummystack.fspm_cfg.num_physical_ports = 16; + + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 1), 0x8001); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 2), 0x8002); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 3), 0x8003); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 4), 0x8004); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 5), 0x8005); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 6), 0x8006); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 7), 0x8007); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 8), 0x8008); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 9), 0x8009); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 10), 0x800A); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 11), 0x800B); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 12), 0x800C); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 13), 0x800D); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 14), 0x800E); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 15), 0x800F); + EXPECT_EQ (pf_port_loc_port_num_to_dap_subslot (&dummystack, 16), 0x8010); +} + +TEST_F (PortUnitTest, PortSubslotToLocalPort) +{ + dummystack.fspm_cfg.num_physical_ports = 4; + + EXPECT_EQ (pf_port_dap_subslot_to_local_port (&dummystack, 0x8001), 1); + EXPECT_EQ (pf_port_dap_subslot_to_local_port (&dummystack, 0x8002), 2); + EXPECT_EQ (pf_port_dap_subslot_to_local_port (&dummystack, 0x8003), 3); + EXPECT_EQ (pf_port_dap_subslot_to_local_port (&dummystack, 0x8004), 4); + + /* Out of range */ + EXPECT_EQ (pf_port_dap_subslot_to_local_port (&dummystack, 0x8000), 0); + EXPECT_EQ (pf_port_dap_subslot_to_local_port (&dummystack, 0x8005), 0); +} + +TEST_F (PortUnitTest, PortIsSubslotDap) +{ + dummystack.fspm_cfg.num_physical_ports = 4; + + EXPECT_EQ (pf_port_subslot_is_dap_port_id (&dummystack, 0x8001), true); + EXPECT_EQ (pf_port_subslot_is_dap_port_id (&dummystack, 0x8002), true); + EXPECT_EQ (pf_port_subslot_is_dap_port_id (&dummystack, 0x8003), true); + EXPECT_EQ (pf_port_subslot_is_dap_port_id (&dummystack, 0x8004), true); + + /* Out of range */ + EXPECT_EQ (pf_port_subslot_is_dap_port_id (&dummystack, 0x8000), false); + EXPECT_EQ (pf_port_subslot_is_dap_port_id (&dummystack, 0x8005), false); +} + +TEST_F (PortUnitTest, PortIsValid) +{ + dummystack.fspm_cfg.num_physical_ports = 4; + + EXPECT_EQ (pf_port_is_valid (&dummystack, 1), true); + EXPECT_EQ (pf_port_is_valid (&dummystack, 2), true); + EXPECT_EQ (pf_port_is_valid (&dummystack, 3), true); + EXPECT_EQ (pf_port_is_valid (&dummystack, 4), true); + + /* Out of range */ + EXPECT_EQ (pf_port_is_valid (&dummystack, 0), false); + EXPECT_EQ (pf_port_is_valid (&dummystack, 5), false); +} + +TEST_F (PortUnitTest, PortGetNumberOfPorts) +{ + dummystack.fspm_cfg.num_physical_ports = 4; + + EXPECT_EQ (pf_port_get_number_of_ports (&dummystack), 4); +} diff --git a/profinet_stack/p-net/test/test_ppm.cpp b/profinet_stack/p-net/test/test_ppm.cpp new file mode 100644 index 0000000..0c0c718 --- /dev/null +++ b/profinet_stack/p-net/test/test_ppm.cpp @@ -0,0 +1,137 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class PpmTest : public PnetIntegrationTest +{ +}; + +class PpmUnitTest : public PnetUnitTest +{ +}; + +TEST_F (PpmTest, PpmTestCalculateCycle) +{ + /* Cycle length 31.25 us, transmission period 31.25 us */ + EXPECT_EQ (pf_ppm_calculate_cyclecounter (0, 1, 1), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (20, 1, 1), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (40, 1, 1), 1); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (70, 1, 1), 2); + + /* Cycle length 62.5 us, transmission period 62.5 us */ + EXPECT_EQ (pf_ppm_calculate_cyclecounter (0, 2, 1), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (10, 2, 1), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (50, 2, 1), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (80, 2, 1), 2); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (100, 2, 1), 2); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (140, 2, 1), 4); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (160, 2, 1), 4); + + /* Cycle length 62.5 us, transmission period 500 us */ + EXPECT_EQ (pf_ppm_calculate_cyclecounter (0, 2, 8), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (250, 2, 8), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (750, 2, 8), 16); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (1250, 2, 8), 32); + + /* Cycle length 125 us, transmission period 500 us */ + EXPECT_EQ (pf_ppm_calculate_cyclecounter (0, 4, 4), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (250, 4, 4), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (750, 4, 4), 16); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (1250, 4, 4), 32); + + /* Cycle length 1 ms, transmission period 4 ms */ + EXPECT_EQ (pf_ppm_calculate_cyclecounter (0, 32, 4), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (3000, 32, 4), 0); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (5000, 32, 4), 128); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (7000, 32, 4), 128); + EXPECT_EQ (pf_ppm_calculate_cyclecounter (9000, 32, 4), 256); +} + +TEST_F (PpmTest, PpmTestCalculateNextCycle) +{ + /* Cycle length 31.25 us, transmission period 31.25 us */ + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0, 1, 1), 1); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (1, 1, 1), 2); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (2, 1, 1), 3); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (3, 1, 1), 4); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (4, 1, 1), 5); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (5, 1, 1), 6); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (6, 1, 1), 7); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFF, 1, 1), 0); + + /* Cycle length 62.5 us, transmission period 62.5 us */ + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0, 2, 1), 2); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (1, 2, 1), 2); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (2, 2, 1), 4); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (3, 2, 1), 4); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (4, 2, 1), 6); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (5, 2, 1), 6); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (6, 2, 1), 8); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (7, 2, 1), 8); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (8, 2, 1), 10); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (9, 2, 1), 10); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (10, 2, 1), 12); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFD, 2, 1), 0xFFFE); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFE, 2, 1), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFF, 2, 1), 0); + + /* Cycle length 62.5 us, transmission period 250 us */ + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0, 2, 4), 8); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (7, 2, 4), 8); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (8, 2, 4), 16); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (9, 2, 4), 16); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFE, 2, 4), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFF, 2, 4), 0); + + /* Cycle length 1 ms, transmission period 4 ms */ + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0, 32, 4), 128); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (1, 32, 4), 128); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (2, 32, 4), 128); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (3, 32, 4), 128); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (127, 32, 4), 128); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (128, 32, 4), 256); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (129, 32, 4), 256); + + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0x00, 32, 4), 0x080); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0x7F, 32, 4), 0x080); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0x80, 32, 4), 0x100); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0x81, 32, 4), 0x100); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFE, 32, 4), 0x100); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFF, 32, 4), 0x100); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0x100, 32, 4), 0x180); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0x101, 32, 4), 0x180); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFEFF, 32, 4), 0xFF00); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFF00, 32, 4), 0xFF80); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFF01, 32, 4), 0xFF80); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFF7E, 32, 4), 0xFF80); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFF7F, 32, 4), 0xFF80); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFF80, 32, 4), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFF81, 32, 4), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFE, 32, 4), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFF, 32, 4), 0); + + /* Cycle length 4 ms, transmission period 2.048 s */ + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0, 128, 512), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (1, 128, 512), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (2, 128, 512), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFE, 128, 512), 0); + EXPECT_EQ (pf_ppm_calculate_next_cyclecounter (0xFFFF, 128, 512), 0); +} diff --git a/profinet_stack/p-net/test/test_ptcp.cpp b/profinet_stack/p-net/test/test_ptcp.cpp new file mode 100644 index 0000000..b18aa5d --- /dev/null +++ b/profinet_stack/p-net/test/test_ptcp.cpp @@ -0,0 +1,29 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class PtcpTest : public PnetIntegrationTest +{ +}; + +TEST_F (PtcpTest, PtcpRunTest) +{ +} diff --git a/profinet_stack/p-net/test/test_scheduler.cpp b/profinet_stack/p-net/test/test_scheduler.cpp new file mode 100644 index 0000000..d21fccc --- /dev/null +++ b/profinet_stack/p-net/test/test_scheduler.cpp @@ -0,0 +1,534 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +class SchedulerTest : public PnetIntegrationTest +{ +}; + +class SchedulerUnitTest : public PnetUnitTest +{ +}; + +void test_scheduler_callback_a (pnet_t * net, void * arg, uint32_t current_time) +{ + app_data_for_testing_t * appdata = (app_data_for_testing_t *)arg; + + appdata->call_counters.scheduler_callback_a_calls += 1; + pf_scheduler_reset_handle (&appdata->scheduler_handle_a); +} + +void test_scheduler_callback_b (pnet_t * net, void * arg, uint32_t current_time) +{ + app_data_for_testing_t * appdata = (app_data_for_testing_t *)arg; + + appdata->call_counters.scheduler_callback_b_calls += 1; + pf_scheduler_reset_handle (&appdata->scheduler_handle_b); +} + +TEST_F (SchedulerUnitTest, SchedulerSanitizeDelayTest) +{ + const uint32_t cycle_len = 1000; + const uint32_t margin = 10u; + uint32_t result = 0; + uint32_t input_delay = 0; + + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 1000, margin); + + input_delay = 500; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 1000, margin); + + input_delay = 1000; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 1000, margin); + + input_delay = 1400; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 1000, margin); + + input_delay = 1600; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 1500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 2000, margin); + + input_delay = 2000; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 1500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 2000, margin); + + input_delay = 2400; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 1500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 2000, margin); + + input_delay = 2600; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 2500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 3000, margin); + + input_delay = 3000; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 2500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 3000, margin); + + input_delay = 3400; + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 2500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 3000, margin); + + input_delay = 1000000; /* 1 second */ + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 999500u, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 1000000, margin); + + input_delay = 10000000; /* 10 seconds */ + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 9999500u, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 10000000, margin); + + input_delay = 1000000000; /* Unrealisticly far in the future (1000 s) */ + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 1000, margin); + + input_delay = (uint32_t)(-4); /* Next execution has already passed */ + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, true); + ASSERT_NEAR (result, 500, margin); + result = pf_scheduler_sanitize_delay (input_delay, cycle_len, false); + ASSERT_NEAR (result, 1000, margin); +} + +TEST_F (SchedulerTest, SchedulerAddRemove) +{ + int ret; + pf_scheduler_handle_t * p_a = &appdata.scheduler_handle_a; + pf_scheduler_handle_t * p_b = &appdata.scheduler_handle_b; + const char scheduler_name_a[] = "testhandle_a"; + const char scheduler_name_b[] = "testhandle_b"; + const char * resulting_name = NULL; + uint32_t value; + bool is_scheduled; + + pf_scheduler_init (net, TEST_TICK_INTERVAL_US); + run_stack (TEST_SCHEDULER_RUNTIME); + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 0); + + /* Initialize and verify handle */ + pf_scheduler_init_handle (p_a, scheduler_name_a); + pf_scheduler_init_handle (p_b, scheduler_name_b); + + resulting_name = pf_scheduler_get_name (p_a); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_a), 0); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + /* Reset non-running handle */ + pf_scheduler_reset_handle (p_a); + + resulting_name = pf_scheduler_get_name (p_a); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_a), 0); + + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + /* Schedule callback A */ + ret = pf_scheduler_add ( + net, + TEST_SCHEDULER_CALLBACK_DELAY, + test_scheduler_callback_a, + &appdata, + p_a); + EXPECT_EQ (ret, 0); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 0); + resulting_name = pf_scheduler_get_name (p_a); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_a), 0); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 1UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 0); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + /* Verify that it not is triggered */ + run_stack (TEST_TICK_INTERVAL_US); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 0); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 1UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 0); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + /* Verify that callback A is triggered */ + run_stack (TEST_SCHEDULER_RUNTIME); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 1); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 0); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + /* Schedule both callbacks */ + ret = pf_scheduler_add ( + net, + TEST_SCHEDULER_CALLBACK_DELAY, + test_scheduler_callback_a, + &appdata, + p_a); + EXPECT_EQ (ret, 0); + ret = pf_scheduler_add ( + net, + TEST_SCHEDULER_RUNTIME + TEST_SCHEDULER_CALLBACK_DELAY, + test_scheduler_callback_b, + &appdata, + p_b); + EXPECT_EQ (ret, 0); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 1); + resulting_name = pf_scheduler_get_name (p_a); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_a), 0); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 1UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 0); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_TRUE (is_scheduled); + + /* Verify that they not are triggered */ + run_stack (TEST_TICK_INTERVAL_US); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 1); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 1UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 0); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_TRUE (is_scheduled); + + /* Verify that callback A (but not B) is triggered */ + run_stack (TEST_SCHEDULER_RUNTIME); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 0); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_TRUE (is_scheduled); + + /* Verify that both callbacks have been triggered */ + run_stack (TEST_SCHEDULER_RUNTIME); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + resulting_name = pf_scheduler_get_name (p_b); + EXPECT_EQ (strcmp (resulting_name, scheduler_name_b), 0); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + /* Remove non-scheduled event */ + run_stack (TEST_SCHEDULER_RUNTIME); + + pf_scheduler_remove_if_running (net, p_a); + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + pf_scheduler_remove (net, p_a); /* Will log error message */ + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + /* Remove scheduled event */ + run_stack (TEST_SCHEDULER_RUNTIME); + + ret = pf_scheduler_add ( + net, + TEST_SCHEDULER_CALLBACK_DELAY, + test_scheduler_callback_a, + &appdata, + p_a); + EXPECT_EQ (ret, 0); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + run_stack (TEST_TICK_INTERVAL_US); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + pf_scheduler_remove (net, p_a); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + run_stack (TEST_SCHEDULER_RUNTIME); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + /* Remove scheduled event, using utility function */ + run_stack (TEST_SCHEDULER_RUNTIME); + + ret = pf_scheduler_add ( + net, + TEST_SCHEDULER_CALLBACK_DELAY, + test_scheduler_callback_a, + &appdata, + p_a); + EXPECT_EQ (ret, 0); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 2UL); /* Implementation detail */ // TODO + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + run_stack (TEST_TICK_INTERVAL_US); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + pf_scheduler_remove_if_running (net, p_a); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + run_stack (TEST_SCHEDULER_RUNTIME); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + value = pf_scheduler_get_value (p_b); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + /* Schedule and restart */ + ret = pf_scheduler_add ( + net, + TEST_SCHEDULER_CALLBACK_DELAY, + test_scheduler_callback_a, + &appdata, + p_a); + EXPECT_EQ (ret, 0); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + run_stack (TEST_TICK_INTERVAL_US); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + ret = pf_scheduler_restart ( + net, + TEST_SCHEDULER_RUNTIME + TEST_SCHEDULER_CALLBACK_DELAY, + test_scheduler_callback_a, + &appdata, + p_a); + + run_stack (TEST_SCHEDULER_RUNTIME); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 2); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, 2UL); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_TRUE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); + + run_stack (TEST_SCHEDULER_RUNTIME); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_a_calls, 3); + value = pf_scheduler_get_value (p_a); + EXPECT_EQ (value, UINT32_MAX); /* Implementation detail */ + is_scheduled = pf_scheduler_is_running (p_a); + EXPECT_FALSE (is_scheduled); + + EXPECT_EQ (appdata.call_counters.scheduler_callback_b_calls, 1); + is_scheduled = pf_scheduler_is_running (p_b); + EXPECT_FALSE (is_scheduled); +} diff --git a/profinet_stack/p-net/test/test_snmp.cpp b/profinet_stack/p-net/test/test_snmp.cpp new file mode 100644 index 0000000..1c3f20f --- /dev/null +++ b/profinet_stack/p-net/test/test_snmp.cpp @@ -0,0 +1,263 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2020 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +/** + * @file + * @brief Unit test for SNMP helper functions in pf_snmp.h. + * + */ + +#include "utils_for_testing.h" +#include "mocks.h" + +#include "pf_includes.h" + +#include + +#define LOCAL_PORT 1 + +class SnmpTest : public PnetIntegrationTest +{ +}; + +/* See https://tools.ietf.org/html/rfc2578#section-7.7 clause 3, + * for encoding of OCTET STRING field ManAddress. + */ +TEST_F (SnmpTest, SnmpGetManagementAddress) +{ + pf_snmp_management_address_t address; + + memset (&address, 0xff, sizeof (address)); + mock_lldp_data.management_address.subtype = 1; + mock_lldp_data.management_address.value[0] = 192; + mock_lldp_data.management_address.value[1] = 168; + mock_lldp_data.management_address.value[2] = 1; + mock_lldp_data.management_address.value[3] = 100; + mock_lldp_data.management_address.len = 4; + + pf_snmp_get_management_address (net, &address); + EXPECT_EQ (address.subtype, 1); + EXPECT_EQ (address.value[0], 4); /* len */ + EXPECT_EQ (address.value[1], 192); + EXPECT_EQ (address.value[2], 168); + EXPECT_EQ (address.value[3], 1); + EXPECT_EQ (address.value[4], 100); + EXPECT_EQ (address.len, 5u); +} + +TEST_F (SnmpTest, SnmpGetPeerManagementAddress) +{ + pf_snmp_management_address_t address; + int error; + + memset (&address, 0xff, sizeof (address)); + mock_lldp_data.peer_management_address.subtype = 1; + mock_lldp_data.peer_management_address.value[0] = 192; + mock_lldp_data.peer_management_address.value[1] = 168; + mock_lldp_data.peer_management_address.value[2] = 1; + mock_lldp_data.peer_management_address.value[3] = 101; + mock_lldp_data.peer_management_address.len = 4; + mock_lldp_data.error = 0; + + error = pf_snmp_get_peer_management_address (net, LOCAL_PORT, &address); + EXPECT_EQ (error, 0); + EXPECT_EQ (address.subtype, 1); + EXPECT_EQ (address.value[0], 4); /* len */ + EXPECT_EQ (address.value[1], 192); + EXPECT_EQ (address.value[2], 168); + EXPECT_EQ (address.value[3], 1); + EXPECT_EQ (address.value[4], 101); + EXPECT_EQ (address.len, 5u); + + mock_lldp_data.error = -1; + error = pf_snmp_get_peer_management_address (net, LOCAL_PORT, &address); + EXPECT_EQ (error, -1); +} + +/* See https://tools.ietf.org/html/rfc1906 for encoding of + * BITS field AutoNegAdvertisedCap. + * See https://tools.ietf.org/html/rfc2579 for encoding of + * TruthValue fields AutoNegSupported and AutoNegEnabled. + */ +TEST_F (SnmpTest, SnmpGetLinkStatus) +{ + pf_snmp_link_status_t status; + + memset (&status, 0xff, sizeof (status)); + mock_lldp_data.link_status.is_autonegotiation_supported = true; + mock_lldp_data.link_status.is_autonegotiation_enabled = true; + mock_lldp_data.link_status.autonegotiation_advertised_capabilities = 0xF00F; + mock_lldp_data.link_status.operational_mau_type = + PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + + pf_snmp_get_link_status (net, LOCAL_PORT, &status); + EXPECT_EQ (status.auto_neg_supported, 1); /* true */ + EXPECT_EQ (status.auto_neg_enabled, 1); /* true */ + EXPECT_EQ (status.auto_neg_advertised_cap[0], 0xF0); + EXPECT_EQ (status.auto_neg_advertised_cap[1], 0x0F); + EXPECT_EQ (status.oper_mau_type, PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX); + + memset (&status, 0xff, sizeof (status)); + mock_lldp_data.link_status.is_autonegotiation_supported = true; + mock_lldp_data.link_status.is_autonegotiation_enabled = false; + mock_lldp_data.link_status.autonegotiation_advertised_capabilities = + BIT (5 + 0) | BIT (3 + 0) | BIT (6 + 8) | BIT (0 + 8); + mock_lldp_data.link_status.operational_mau_type = + PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX; + + pf_snmp_get_link_status (net, LOCAL_PORT, &status); + EXPECT_EQ (status.auto_neg_supported, 1); /* true */ + EXPECT_EQ (status.auto_neg_enabled, 2); /* false */ + EXPECT_EQ (status.auto_neg_advertised_cap[0], BIT (2) | BIT (4)); + EXPECT_EQ (status.auto_neg_advertised_cap[1], BIT (1) | BIT (7)); + EXPECT_EQ (status.oper_mau_type, PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX); +} + +TEST_F (SnmpTest, SnmpGetPeerLinkStatus) +{ + pf_snmp_link_status_t status; + int error; + + memset (&status, 0xff, sizeof (status)); + mock_lldp_data.peer_link_status.is_autonegotiation_supported = true; + mock_lldp_data.peer_link_status.is_autonegotiation_enabled = true; + mock_lldp_data.peer_link_status.autonegotiation_advertised_capabilities = + 0xF00F; + mock_lldp_data.peer_link_status.operational_mau_type = + PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX; + mock_lldp_data.error = 0; + + error = pf_snmp_get_peer_link_status (net, LOCAL_PORT, &status); + EXPECT_EQ (error, 0); + EXPECT_EQ (status.auto_neg_supported, 1); /* true */ + EXPECT_EQ (status.auto_neg_enabled, 1); /* true */ + EXPECT_EQ (status.auto_neg_advertised_cap[0], 0xF0); + EXPECT_EQ (status.auto_neg_advertised_cap[1], 0x0F); + EXPECT_EQ (status.oper_mau_type, PNAL_ETH_MAU_COPPER_100BaseTX_FULL_DUPLEX); + + memset (&status, 0xff, sizeof (status)); + mock_lldp_data.peer_link_status.is_autonegotiation_supported = true; + mock_lldp_data.peer_link_status.is_autonegotiation_enabled = false; + mock_lldp_data.peer_link_status.autonegotiation_advertised_capabilities = + BIT (5 + 0) | BIT (3 + 0) | BIT (6 + 8) | BIT (0 + 8); + mock_lldp_data.peer_link_status.operational_mau_type = + PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX; + mock_lldp_data.error = 0; + + error = pf_snmp_get_peer_link_status (net, LOCAL_PORT, &status); + EXPECT_EQ (error, 0); + EXPECT_EQ (status.auto_neg_supported, 1); /* true */ + EXPECT_EQ (status.auto_neg_enabled, 2); /* false */ + EXPECT_EQ (status.auto_neg_advertised_cap[0], BIT (2) | BIT (4)); + EXPECT_EQ (status.auto_neg_advertised_cap[1], BIT (1) | BIT (7)); + EXPECT_EQ (status.oper_mau_type, PNAL_ETH_MAU_COPPER_100BaseTX_HALF_DUPLEX); + + mock_lldp_data.error = -1; + error = pf_snmp_get_peer_link_status (net, LOCAL_PORT, &status); + EXPECT_EQ (error, -1); +} + +TEST_F (SnmpTest, SnmpGetLocationGivenLargeString) +{ + const pf_snmp_system_location_t stored = {"1234567890123456789012 large"}; + const char * expected = stored.string; + pf_snmp_system_location_t actual; + + memset (&actual, 0xff, sizeof (actual)); + mock_pf_file_save (NULL, PF_FILENAME_SNMP_SYSLOCATION, &stored, sizeof (stored)); + pf_snmp_data_init (net); + + pf_snmp_get_system_location (net, &actual); + + EXPECT_STREQ (actual.string, expected); +} + +TEST_F (SnmpTest, SnmpGetLocationGivenSmallString) +{ + const pf_snmp_system_location_t stored = {"small"}; + const char * expected = stored.string; + pf_snmp_system_location_t actual; + + memset (&actual, 0xcd, sizeof (actual)); + mock_pf_file_save (NULL, PF_FILENAME_SNMP_SYSLOCATION, &stored, sizeof (stored)); + pf_snmp_data_init (net); + + pf_snmp_get_system_location (net, &actual); + + EXPECT_STREQ (actual.string, expected); +} + +/* Note that the 22 bytes device location in I&M1 will be used if loading of + * sysLocation file fails (e.g. if does not exit). + */ +TEST_F (SnmpTest, SnmpGetLocationGivenError) +{ + const char expected[] = "IM_Tag_Location in I&M"; + pf_snmp_system_location_t actual; + + mock_file_data.is_load_failing = true; + memset (&actual, 0xff, sizeof (actual)); + mock_pf_fspm_save_im_location (net, expected); + pf_snmp_data_init (net); + + pf_snmp_get_system_location (net, &actual); + + EXPECT_STREQ (actual.string, expected); + EXPECT_EQ (strlen (actual.string), 22u); +} + +/* Note that the first 22 bytes is also written to device location in I&M1 */ +TEST_F (SnmpTest, SnmpSetLocationGivenLargeString) +{ + const pf_snmp_system_location_t written = {"1234567890123456789012345"}; + int error; + + error = pf_snmp_set_system_location (net, &written); + + EXPECT_EQ (error, 0); + EXPECT_STREQ (mock_file_data.filename, PF_FILENAME_SNMP_SYSLOCATION); + EXPECT_EQ (mock_file_data.size, sizeof (written)); + EXPECT_EQ (memcmp (mock_file_data.object, &written, sizeof (written)), 0); + EXPECT_STREQ (mock_fspm_data.im_location, "1234567890123456789012"); +} + +/* Note that the first 22 bytes is also written to device location in I&M1 */ +TEST_F (SnmpTest, SnmpSetLocationGivenSmallString) +{ + const pf_snmp_system_location_t written = {"small"}; + int error; + + error = pf_snmp_set_system_location (net, &written); + + EXPECT_EQ (error, 0); + EXPECT_STREQ (mock_file_data.filename, PF_FILENAME_SNMP_SYSLOCATION); + EXPECT_EQ (mock_file_data.size, sizeof (written)); + EXPECT_EQ (memcmp (mock_file_data.object, &written, sizeof (written)), 0); + EXPECT_STREQ (mock_fspm_data.im_location, "small "); +} + +/* Note that the first 22 bytes is also written to device location in I&M1 */ +TEST_F (SnmpTest, SnmpSetLocationGivenError) +{ + const pf_snmp_system_location_t written = {"1234567890123456789012345"}; + int error; + + mock_file_data.is_save_failing = true; + + error = pf_snmp_set_system_location (net, &written); + + EXPECT_EQ (error, -1); + EXPECT_STREQ (mock_fspm_data.im_location, "1234567890123456789012"); +} diff --git a/profinet_stack/p-net/test/utils_for_testing.cpp b/profinet_stack/p-net/test/utils_for_testing.cpp new file mode 100644 index 0000000..1a07959 --- /dev/null +++ b/profinet_stack/p-net/test/utils_for_testing.cpp @@ -0,0 +1,617 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#include "utils_for_testing.h" + +#include + +#include + +/******************** Callbacks defined by p-net *****************************/ + +int my_connect_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + + p_appdata->call_counters.connect_calls++; + return 0; +} + +int my_release_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + + p_appdata->call_counters.release_calls++; + return 0; +} + +int my_dcontrol_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_control_command_t control_command, + pnet_result_t * p_result) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + + p_appdata->call_counters.dcontrol_calls++; + return 0; +} + +int my_ccontrol_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + + p_appdata->call_counters.ccontrol_calls++; + return 0; +} + +static int my_signal_led_ind (pnet_t * net, void * arg, bool led_state) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + + TEST_TRACE ("Callback on set LED state: %u\n", led_state); + if (led_state == 1) + { + p_appdata->call_counters.led_on_calls++; + } + else + { + p_appdata->call_counters.led_off_calls++; + } + + return 0; +} + +int my_read_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t idx, + uint16_t sequence_number, + uint8_t ** pp_read_data, /* Out: A pointer to the data */ + uint16_t * p_read_length, /* Out: Size of data */ + pnet_result_t * p_result) /* Error status if returning != 0 */ +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + + TEST_TRACE ("Callback on read\n"); + TEST_TRACE ( + " API: %" PRIu32 " Slot: %" PRIu16 " Subslot: %" PRIu16 + " Index: %" PRIu16 " Sequence: %" PRIu16 "\n", + api, + slot, + subslot, + idx, + sequence_number); + p_appdata->call_counters.read_calls++; + return 0; +} + +int my_write_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint16_t idx, + uint16_t sequence_number, + uint16_t write_length, + const uint8_t * p_write_data, + pnet_result_t * p_result) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + + TEST_TRACE ("Callback on write\n"); + TEST_TRACE ( + " API: %" PRIu32 " Slot: %" PRIu16 " Subslot: %" PRIu16 + " Index: %" PRIu16 " Sequence: %" PRIu16 " Len: %" PRIu16 "\n", + api, + slot, + subslot, + idx, + sequence_number, + write_length); + p_appdata->call_counters.write_calls++; + return 0; +} + +int my_new_data_status_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t crep, + uint8_t changes, + uint8_t data_status) +{ + TEST_TRACE ("Callback on new data\n"); + return 0; +} + +int my_alarm_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_arg, + uint16_t data_len, + uint16_t data_usi, + const uint8_t * p_data) +{ + TEST_TRACE ("Callback on alarm\n"); + return 0; +} + +int my_alarm_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_pnio_status_t * p_pnio_status) +{ + TEST_TRACE ("Callback on alarm confirmation\n"); + return 0; +} + +int my_state_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_event_values_t state) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + int ret; + uint16_t slot = 0; + uint16_t err_cls = 0; + uint16_t err_code = 0; + + p_appdata->call_counters.state_calls++; + p_appdata->main_arep = arep; + p_appdata->cmdev_state = state; + + if (state == PNET_EVENT_PRMEND) + { + /* Set IOPS for DAP slot (has same numbering as the module identifiers) */ + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_IDENT, + NULL, + 0, + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_IDENT, + NULL, + 0, + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + PNET_SLOT_DAP_IDENT, + PNET_SUBSLOT_DAP_INTERFACE_1_PORT_1_IDENT, + NULL, + 0, + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + + /* Set initial data and IOPS for custom input modules, and IOCS for custom + * output modules */ + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + if (p_appdata->custom_input_slots[slot] == true) + { + ret = pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + slot, + TEST_SUBMOD_CUSTOM_IDENT, + p_appdata->inputdata, + TEST_DATASIZE_INPUT, + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + } + if (p_appdata->custom_output_slots[slot] == true) + { + ret = pnet_output_set_iocs ( + net, + TEST_API_IDENT, + slot, + TEST_SUBMOD_CUSTOM_IDENT, + PNET_IOXS_GOOD); + EXPECT_EQ (ret, 0); + } + } + + ret = pnet_set_provider_state (net, true); + EXPECT_EQ (ret, 0); + } + else if (state == PNET_EVENT_ABORT) + { + ret = pnet_get_ar_error_codes (net, arep, &err_cls, &err_code); + EXPECT_EQ (ret, 0); + TEST_TRACE ( + "ABORT err_cls 0x%02" PRIx16 " err_code 0x%02" PRIx16 "\n", + err_cls, + err_code); + } + + return 0; +} + +int my_exp_module_ind ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint32_t module_ident) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + int ret = -1; /* Not supported in specified slot */ + bool found = false; + uint16_t ix; + + TEST_TRACE ("Callback on module\n"); + + /* Find it in the list of supported modules */ + ix = 0; + while (ix < TEST_MAX_NUMBER_AVAILABLE_MODULE_TYPES) + { + if (p_appdata->available_module_types[ix] == module_ident) + { + found = true; + break; + } + ix++; + } + + if (found == true) + { + /* For now support any module in any slot */ + TEST_TRACE ( + " Plug module. API: %" PRIu32 " Slot: %" PRIu16 " Module ID: " + "%" PRIu32 " Index in list of supported modules: %" PRIu16 "\n", + api, + slot, + module_ident, + ix); + ret = pnet_plug_module (net, api, slot, module_ident); + EXPECT_EQ (ret, 0); + + /* Remember what is plugged in each slot */ + if (module_ident == TEST_MOD_8_0_IDENT || module_ident == TEST_MOD_8_8_IDENT) + { + p_appdata->custom_input_slots[slot] = true; + } + if (module_ident == TEST_MOD_8_8_IDENT || module_ident == TEST_MOD_0_8_IDENT) + { + p_appdata->custom_output_slots[slot] = true; + } + } + else + { + TEST_TRACE (" Module ident %08" PRIx32 " not found\n", module_ident); + EXPECT_TRUE (false); // Fail the test + } + + return ret; +} + +int my_exp_submodule_ind ( + pnet_t * net, + void * arg, + uint32_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_ident, + uint32_t submodule_ident, + const pnet_data_cfg_t * p_exp_data) +{ + app_data_for_testing_t * p_appdata = (app_data_for_testing_t *)arg; + int ret = -1; + bool found = false; + uint16_t ix = 0; + + TEST_TRACE ("Callback on submodule\n"); + + /* Find it in the list of supported submodules */ + ix = 0; + while (ix < TEST_MAX_NUMBER_AVAILABLE_SUBMODULE_TYPES) + { + if ( + p_appdata->available_submodule_types[ix].module_ident_number == + module_ident && + p_appdata->available_submodule_types[ix].submodule_ident_number == + submodule_ident) + { + found = true; + break; + } + ix++; + } + + if (found == true) + { + TEST_TRACE ( + " Plug submodule. API: %" PRIu32 " Slot: %" PRIu16 " Subslot: " + "%" PRIu16 " Module ID: %" PRIu32 " Submodule ID: %" PRIu32 + " (Index in " + "available " + "submodules: %" PRIu16 ") Direction: %u Len in: %" PRIu16 + " out: %" PRIu16 "\n", + api, + slot, + subslot, + module_ident, + submodule_ident, + ix, + p_appdata->available_submodule_types[ix].direction, + p_appdata->available_submodule_types[ix].input_length, + p_appdata->available_submodule_types[ix].output_length); + ret = pnet_plug_submodule ( + net, + api, + slot, + subslot, + module_ident, + submodule_ident, + p_appdata->available_submodule_types[ix].direction, + p_appdata->available_submodule_types[ix].input_length, + p_appdata->available_submodule_types[ix].output_length); + EXPECT_EQ (ret, 0); + } + else + { + TEST_TRACE ( + " Sub-module ident %08" PRIx32 " not found\n", + submodule_ident); + EXPECT_TRUE (false); // Fail the test + } + + return ret; +} + +/*********************** Base classes for tests *****************************/ + +void PnetIntegrationTestBase::appdata_init() +{ + memset (&appdata, 0, sizeof (appdata)); +} + +void PnetIntegrationTestBase::callcounter_reset() +{ + memset (&appdata.call_counters, 0, sizeof (call_counters_t)); +} + +void PnetIntegrationTestBase::available_modules_and_submodules_init() +{ + appdata.available_module_types[0] = PNET_MOD_DAP_IDENT; + appdata.available_module_types[1] = TEST_MOD_8_8_IDENT; + appdata.available_module_types[2] = TEST_MOD_8_0_IDENT; + + appdata.available_submodule_types[0].module_ident_number = + PNET_MOD_DAP_IDENT; + appdata.available_submodule_types[0].submodule_ident_number = + PNET_SUBMOD_DAP_IDENT; + appdata.available_submodule_types[0].direction = PNET_DIR_NO_IO; + appdata.available_submodule_types[0].input_length = 0; + appdata.available_submodule_types[0].output_length = 0; + + appdata.available_submodule_types[1].module_ident_number = + PNET_MOD_DAP_IDENT; + appdata.available_submodule_types[1].submodule_ident_number = + PNET_SUBMOD_DAP_INTERFACE_1_IDENT; + appdata.available_submodule_types[1].direction = PNET_DIR_NO_IO; + appdata.available_submodule_types[1].input_length = 0; + appdata.available_submodule_types[1].output_length = 0; + + appdata.available_submodule_types[2].module_ident_number = + PNET_MOD_DAP_IDENT; + appdata.available_submodule_types[2].submodule_ident_number = + PNET_SUBMOD_DAP_INTERFACE_1_PORT_1_IDENT; + appdata.available_submodule_types[2].direction = PNET_DIR_NO_IO; + appdata.available_submodule_types[2].input_length = 0; + appdata.available_submodule_types[2].output_length = 0; + + appdata.available_submodule_types[3].module_ident_number = + TEST_MOD_8_8_IDENT; + appdata.available_submodule_types[3].submodule_ident_number = + TEST_SUBMOD_CUSTOM_IDENT; + appdata.available_submodule_types[3].direction = PNET_DIR_IO; + appdata.available_submodule_types[3].input_length = TEST_DATASIZE_INPUT; + appdata.available_submodule_types[3].output_length = TEST_DATASIZE_OUTPUT; + + appdata.available_submodule_types[4].module_ident_number = + TEST_MOD_8_0_IDENT; + appdata.available_submodule_types[4].submodule_ident_number = + TEST_SUBMOD_CUSTOM_IDENT; + appdata.available_submodule_types[4].direction = PNET_DIR_OUTPUT; + appdata.available_submodule_types[4].input_length = 0; + appdata.available_submodule_types[4].output_length = TEST_DATASIZE_OUTPUT; +} + +void PnetIntegrationTestBase::cfg_init() +{ + pnet_default_cfg.tick_us = TEST_TICK_INTERVAL_US; + pnet_default_cfg.state_cb = my_state_ind; + pnet_default_cfg.connect_cb = my_connect_ind; + pnet_default_cfg.release_cb = my_release_ind; + pnet_default_cfg.dcontrol_cb = my_dcontrol_ind; + pnet_default_cfg.ccontrol_cb = my_ccontrol_cnf; + pnet_default_cfg.read_cb = my_read_ind; + pnet_default_cfg.write_cb = my_write_ind; + pnet_default_cfg.exp_module_cb = my_exp_module_ind; + pnet_default_cfg.exp_submodule_cb = my_exp_submodule_ind; + pnet_default_cfg.new_data_status_cb = my_new_data_status_ind; + pnet_default_cfg.alarm_ind_cb = my_alarm_ind; + pnet_default_cfg.alarm_cnf_cb = my_alarm_cnf; + pnet_default_cfg.signal_led_cb = my_signal_led_ind; + pnet_default_cfg.reset_cb = NULL; + pnet_default_cfg.cb_arg = &appdata; + + /* Device configuration */ + pnet_default_cfg.device_id.vendor_id_hi = 0xfe; + pnet_default_cfg.device_id.vendor_id_lo = 0xed; + pnet_default_cfg.device_id.device_id_hi = 0xbe; + pnet_default_cfg.device_id.device_id_lo = 0xef; + pnet_default_cfg.oem_device_id.vendor_id_hi = 0xfe; + pnet_default_cfg.oem_device_id.vendor_id_lo = 0xed; + pnet_default_cfg.oem_device_id.device_id_hi = 0xbe; + pnet_default_cfg.oem_device_id.device_id_lo = 0xef; + + strcpy (pnet_default_cfg.station_name, ""); + strcpy (pnet_default_cfg.product_name, "PNET unit tests"); + + pnet_default_cfg.if_cfg.physical_ports[0].netif_name = TEST_INTERFACE_NAME; + + /* Timing */ + pnet_default_cfg.min_device_interval = 32; /* Corresponds to 1 ms */ + + /* Network configuration */ + pnet_default_cfg.send_hello = 1; /* Send HELLO */ + pnet_default_cfg.if_cfg.ip_cfg.dhcp_enable = 0; + pnet_default_cfg.if_cfg.main_netif_name = TEST_INTERFACE_NAME; + pnet_default_cfg.if_cfg.ip_cfg.ip_addr.a = 192; + pnet_default_cfg.if_cfg.ip_cfg.ip_addr.b = 168; + pnet_default_cfg.if_cfg.ip_cfg.ip_addr.c = 1; + pnet_default_cfg.if_cfg.ip_cfg.ip_addr.d = 171; + pnet_default_cfg.if_cfg.ip_cfg.ip_mask.a = 255; + pnet_default_cfg.if_cfg.ip_cfg.ip_mask.b = 255; + pnet_default_cfg.if_cfg.ip_cfg.ip_mask.c = 255; + pnet_default_cfg.if_cfg.ip_cfg.ip_mask.d = 255; + pnet_default_cfg.if_cfg.ip_cfg.ip_gateway.a = 192; + pnet_default_cfg.if_cfg.ip_cfg.ip_gateway.b = 168; + pnet_default_cfg.if_cfg.ip_cfg.ip_gateway.c = 1; + pnet_default_cfg.if_cfg.ip_cfg.ip_gateway.d = 1; + + pnet_default_cfg.im_0_data.im_vendor_id_hi = 0x00; + pnet_default_cfg.im_0_data.im_vendor_id_lo = 0x01; + strcpy (pnet_default_cfg.im_0_data.im_order_id, " "); + strcpy (pnet_default_cfg.im_0_data.im_serial_number, " "); + pnet_default_cfg.im_0_data.im_hardware_revision = 1; + pnet_default_cfg.im_0_data.im_sw_revision_prefix = 'P'; /* 'V', 'R', 'P', + 'U', or 'T' */ + pnet_default_cfg.im_0_data.im_sw_revision_functional_enhancement = 0; + pnet_default_cfg.im_0_data.im_sw_revision_bug_fix = 0; + pnet_default_cfg.im_0_data.im_sw_revision_internal_change = 0; + pnet_default_cfg.im_0_data.im_revision_counter = 0; + pnet_default_cfg.im_0_data.im_profile_id = 0x1234; + pnet_default_cfg.im_0_data.im_profile_specific_type = 0x5678; + pnet_default_cfg.im_0_data.im_version_major = 0; + pnet_default_cfg.im_0_data.im_version_minor = 1; + pnet_default_cfg.im_0_data.im_supported = + PNET_SUPPORTED_IM1 | PNET_SUPPORTED_IM2 | PNET_SUPPORTED_IM3 | + PNET_SUPPORTED_IM4; + strcpy (pnet_default_cfg.im_1_data.im_tag_function, ""); + strcpy (pnet_default_cfg.im_1_data.im_tag_location, ""); + strcpy (pnet_default_cfg.im_2_data.im_date, ""); + strcpy (pnet_default_cfg.im_3_data.im_descriptor, ""); + strcpy (pnet_default_cfg.im_4_data.im_signature, ""); + + /* Storage */ + strcpy (pnet_default_cfg.file_directory, "/disk1"); + + /* Diagnosis */ + pnet_default_cfg.use_qualified_diagnosis = true; + + pnet_default_cfg.num_physical_ports = PNET_MAX_PHYSICAL_PORTS; +} + +void PnetIntegrationTestBase::run_stack (int us) +{ + uint16_t slot = 0; + + for (int tmr = 0; tmr < us / TEST_TICK_INTERVAL_US; tmr++) + { + /* Set new output data every 10 ticks */ + appdata.tick_ctr++; + if ((appdata.main_arep != 0) && (appdata.tick_ctr > 10)) + { + appdata.tick_ctr = 0; + appdata.inputdata[0] = appdata.counter_data++; + + /* Set data for custom input modules, if any */ + for (slot = 0; slot < PNET_MAX_SLOTS; slot++) + { + if (appdata.custom_input_slots[slot] == true) + { + (void)pnet_input_set_data_and_iops ( + net, + TEST_API_IDENT, + slot, + TEST_SUBMOD_CUSTOM_IDENT, + appdata.inputdata, + TEST_DATASIZE_INPUT, + PNET_IOXS_GOOD); + } + } + } + + /* Run stack functionality every tick */ + pnet_handle_periodic (net); + mock_os_data.current_time_us += TEST_TICK_INTERVAL_US; + } +} + +void PnetIntegrationTestBase::send_data (uint8_t * data_packet, uint16_t len) +{ + int ret; + pnal_buf_t * p_buf; + uint8_t * p_ctr; + + p_buf = pnal_buf_alloc (PF_FRAME_BUFFER_SIZE); + if (p_buf == NULL) + { + TEST_TRACE ("(%d): Out of memory in send_data\n", __LINE__); + } + else + { + memcpy (p_buf->payload, data_packet, len); + + /* Insert frame time, store in big-endian */ + appdata.data_cycle_ctr++; + p_ctr = &((uint8_t *)(p_buf->payload))[len - 4]; + *(p_ctr + 0) = (appdata.data_cycle_ctr >> 8) & 0xff; + *(p_ctr + 1) = appdata.data_cycle_ctr & 0xff; + + p_buf->len = len; + ret = pf_eth_recv (mock_os_data.eth_if_handle, net, p_buf); + EXPECT_EQ (ret, 1); + if (ret == 0) + { + TEST_TRACE ("(%d): Unhandled p_buf\n", __LINE__); + pnal_buf_free (p_buf); + } + } +} diff --git a/profinet_stack/p-net/test/utils_for_testing.h b/profinet_stack/p-net/test/utils_for_testing.h new file mode 100644 index 0000000..cb5848a --- /dev/null +++ b/profinet_stack/p-net/test/utils_for_testing.h @@ -0,0 +1,317 @@ +/********************************************************************* + * _ _ _ + * _ __ | |_ _ | | __ _ | |__ ___ + * | '__|| __|(_)| | / _` || '_ \ / __| + * | | | |_ _ | || (_| || |_) |\__ \ + * |_| \__|(_)|_| \__,_||_.__/ |___/ + * + * www.rt-labs.com + * Copyright 2018 rt-labs AB, Sweden. + * + * This software is dual-licensed under GPLv3 and a commercial + * license. See the file LICENSE.md distributed with this software for + * full license information. + ********************************************************************/ + +#ifndef UTILS_FOR_TESTING_H +#define UTILS_FOR_TESTING_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#include "pf_includes.h" +#include "mocks.h" + +#if defined(TEST_DEBUG) +#define TEST_TRACE(...) printf (__VA_ARGS__) +#else +#define TEST_TRACE(...) +#endif + +#define TEST_UDP_DELAY (500 * 1000) /* us */ +#define TEST_TICK_INTERVAL_US 1000 /* us */ +#define TEST_DATA_DELAY (2 * TEST_TICK_INTERVAL_US) /* us */ +#define TEST_TIMEOUT_DELAY (3 * 1000 * 1000) /* us */ +#define TEST_SCHEDULER_CALLBACK_DELAY (4 * TEST_TICK_INTERVAL_US) /* us */ +#define TEST_SCHEDULER_RUNTIME (10 * TEST_TICK_INTERVAL_US) /* us */ + +#define TEST_API_IDENT 0 +#define TEST_API_NONEXIST_IDENT 2 +#define TEST_SLOT_IDENT 1 +#define TEST_SLOT_NONEXIST_IDENT 2 +#define TEST_SUBSLOT_IDENT 1 +#define TEST_SUBSLOT_NONEXIST_IDENT 2 +#define TEST_CHANNEL_IDENT 0 +#define TEST_CHANNEL_NONEXIST_IDENT 1 +#define TEST_CHANNEL_ILLEGAL 0x8001 +#define TEST_CHANNEL_DIRECTION PNET_DIAG_CH_PROP_DIR_OUTPUT +#define TEST_CHANNEL_NUMBER_OF_BITS PNET_DIAG_CH_PROP_TYPE_8_BIT +#define TEST_CHANNEL_ERRORTYPE 0x0001 +#define TEST_CHANNEL_ERRORTYPE_B 0x0002 +#define TEST_CHANNEL_ERRORTYPE_C 0x0003 +#define TEST_CHANNEL_ERRORTYPE_D 0x0004 +#define TEST_CHANNEL_ERRORTYPE_NONEXIST 0x0007 +#define TEST_DIAG_USI_CUSTOM 0x1234 +#define TEST_DIAG_USI_NONEXIST 0x1235 +#define TEST_DIAG_USI_INVALID 0x8888 +#define TEST_DIAG_EXT_ERRTYPE 0x0002 +#define TEST_DIAG_EXT_ERRTYPE_NONEXIST 0x0009 +#define TEST_DIAG_EXT_ADDVALUE 0x00030004 +#define TEST_DIAG_EXT_ADDVALUE_B 0x00030005 +#define TEST_DIAG_QUALIFIER 0x00010000 +#define TEST_DIAG_QUALIFIER_NOTSET 0x00000000 +#define TEST_INTERFACE_NAME "en1" +#define TEST_MAX_NUMBER_AVAILABLE_MODULE_TYPES 20 +#define TEST_MAX_NUMBER_AVAILABLE_SUBMODULE_TYPES 20 + +/* + * I/O Modules. These modules and their sub-modules must be plugged by the + * application after the call to pnet_init. + * + * Assume that all modules only have a single submodule, with same number. + */ +#define TEST_MOD_8_0_IDENT 0x00000030 /* 8 bit input */ +#define TEST_MOD_0_8_IDENT 0x00000031 /* 8 bit output */ +#define TEST_MOD_8_8_IDENT 0x00000032 /* 8 bit input, 8 bit output */ +#define TEST_SUBMOD_CUSTOM_IDENT 0x00000001 +#define TEST_DATASIZE_INPUT 1 /* bytes, for digital inputs data */ +#define TEST_DATASIZE_OUTPUT 1 /* bytes, for digital outputs data */ + +typedef struct cfg_submodules +{ + uint32_t module_ident_number; + uint32_t submodule_ident_number; + pnet_submodule_dir_t direction; + uint16_t input_length; + uint16_t output_length; +} cfg_submodules_t; + +typedef struct call_counters_obj +{ + uint16_t state_calls; + uint16_t connect_calls; + uint16_t release_calls; + uint16_t dcontrol_calls; + uint16_t ccontrol_calls; + uint16_t read_calls; + uint16_t write_calls; + uint16_t led_on_calls; + uint16_t led_off_calls; + uint16_t scheduler_callback_a_calls; + uint16_t scheduler_callback_b_calls; +} call_counters_t; + +typedef struct app_data_for_testing_obj +{ + uint32_t tick_ctr; + os_timer_t * periodic_timer; + pnet_event_values_t cmdev_state; + uint16_t data_cycle_ctr; + uint32_t counter_data; + os_event_t * main_events; + uint32_t main_arep; + bool alarm_allowed; + uint32_t app_param_1; + uint32_t app_param_2; + uint8_t inputdata[TEST_DATASIZE_INPUT]; + uint8_t custom_input_slots[PNET_MAX_SLOTS]; + uint8_t custom_output_slots[PNET_MAX_SLOTS]; + uint32_t available_module_types[TEST_MAX_NUMBER_AVAILABLE_MODULE_TYPES]; + cfg_submodules_t + available_submodule_types[TEST_MAX_NUMBER_AVAILABLE_SUBMODULE_TYPES]; + bool init_done; + uint16_t read_fails; + call_counters_t call_counters; + pf_scheduler_handle_t scheduler_handle_a; + pf_scheduler_handle_t scheduler_handle_b; +} app_data_for_testing_t; + +/******************** Callbacks defined by p-net *****************************/ + +int my_connect_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result); + +int my_release_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result); + +int my_dcontrol_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_control_command_t control_command, + pnet_result_t * p_result); + +int my_ccontrol_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_result_t * p_result); + +int my_read_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint16_t api, + uint16_t slot, + uint16_t subslot, + uint16_t idx, + uint16_t sequence_number, + uint8_t ** pp_read_data, /* Out: A pointer to the data */ + uint16_t * p_read_length, /* Out: Size of data */ + pnet_result_t * p_result); /* Error status if returning != 0 */ + +int my_write_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint16_t api, + uint16_t slot, + uint16_t subslot, + uint16_t idx, + uint16_t sequence_number, + uint16_t write_length, + uint8_t * p_write_data, + pnet_result_t * p_result); + +int my_new_data_status_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + uint32_t crep, + uint8_t changes, + uint8_t data_status); + +int my_alarm_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + const pnet_alarm_argument_t * p_alarm_arg, + uint16_t data_len, + uint16_t data_usi, + uint8_t * p_data); + +int my_alarm_cnf ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_pnio_status_t * p_pnio_status); + +int my_state_ind ( + pnet_t * net, + void * arg, + uint32_t arep, + pnet_event_values_t state); + +int my_exp_module_ind ( + pnet_t * net, + void * arg, + uint16_t api, + uint16_t slot, + uint32_t module_ident); + +int my_exp_submodule_ind ( + pnet_t * net, + void * arg, + uint16_t api, + uint16_t slot, + uint16_t subslot, + uint32_t module_ident, + uint32_t submodule_ident); + +#ifdef __cplusplus +} +#endif + +/*********************** Base classes for tests *****************************/ + +class PnetUnitTest : public ::testing::Test +{ + protected: + virtual void SetUp(){}; +}; + +class PnetIntegrationTestBase : public ::testing::Test +{ + protected: + pnet_cfg_t pnet_default_cfg; + app_data_for_testing_t appdata; + pnet_t the_net; + pnet_t * net = &the_net; + + /** Initialize appdata, including clearing available modules etc. */ + virtual void appdata_init(); + + virtual void callcounter_reset(); + + virtual void available_modules_and_submodules_init(); + + virtual void cfg_init(); + + /** Simulate sleep + * + * This function updates the app state and calls the periodic + * maintenance functions while simulating sleeping. + * + * @param us In: time to sleep (in us) + */ + virtual void run_stack (int us); + + /** Send raw Ethernet test data + * + * @param data_packet In: Data packet + * @param len In: Length of data packet + */ + virtual void send_data (uint8_t * data_packet, uint16_t len); +}; + +class PnetIntegrationTest : public PnetIntegrationTestBase +{ + protected: + virtual void SetUp() override + { + mock_init(); + cfg_init(); + appdata_init(); + available_modules_and_submodules_init(); + + callcounter_reset(); + + pnet_init_only (net, &pnet_default_cfg); + + pf_pdport_update_eth_status (net); + + mock_clear(); /* lldp sends a frame at init */ + }; +}; + +/*************************** Assertion helpers ******************************/ + +template +::testing::AssertionResult ArraysMatch ( + const T (&expected)[size], + const T (&actual)[size]) +{ + for (size_t i (0); i < size; ++i) + { + if (expected[i] != actual[i]) + { + return ::testing::AssertionFailure() + << std::hex << std::showbase << "array[" << i << "] (" + << static_cast (actual[i]) << ") != expected[" << i + << "] (" << static_cast (expected[i]) << ")"; + } + } + + return ::testing::AssertionSuccess(); +} + +#endif /* UTILS_FOR_TESTING_H */

I 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%0DgStV}%45S*Y#=C8kDHmxwoM{z zLkBW;KUA`0#<*Vw6aE^)kUM3X1JJaikw_!&#RtP0Be85u{JIo>qhKD}%80u+?~R^v z>z&~;z0VL76W>J7g-ei$mZ<_LERj1gZf$BZ`SXpLrFB8+A&ZzmhR}YR_3n!o1L6aqoSw4evO)*zF-3Q@Z8;2 zjn7%buC7So$BI}}3O^E*fG#W)dxw3HdR>AF+c(0~E(K0dZESO{(4gRFHTy4b*@9oNse)cZw!(%tL-JbbEicDsooVGK|)$Gx% zLKlC@%w#-!BdquL*x161mRrY#F}15bv zF93JAc+a5!XT6i+lPm>ZedK~U>Dy4Y@5=)Kg$bO^nIH#tHEJ^vyRUn<-(PFmJ2+H+ zEd`YyrKTR6J4YHTm(+1eEd?GFGIu~p6_Up0EdcR;WGyJSE^j_ZQLoSpp4#5r3}-T& z*rbdg=0r(c8fgs-KYe&YCEyE)C6}IDnGy%$YTz9LE+7AEX)>G-@4f>8N_oZ^y~Q=ACEu#v2oBNg=nT~(5=Zs^#SB#ew3EK=}k z%aDiYr1F+jDS55;tJSLj(}k zsPK)zu=*QcJML%@(UaG@+;bD!ftFxL+GS^P@Z(AAPfRI09*IC zQURP=S9ar-j#7Jz5==A9OhFi_H|m*?nks%Zp>ckGj?3zaOTcA~SPUO26}gbcm34k` zk#u(K(bMDDRxM4OHab+%CJIJErq2_nW<#Q0%@U>onRqZN6|-oI!exM%753yp2 z%v_VrfKPjMOMCmQQ>$j{o3tN*6{!IjT*I0>y{_jInq1(2pOpCHd&3j;_Ulx%wSj#i ziK!0{w_kMe4Q$*iSTWkMj@~%zkj2Zzkcd{y#ipkEtCtv^w8P!L5-M-B)S%LCd{w62 zpI0-c|J;?yqvjnDnaW)#%I@<+ghb}^h)PZs&C|oX!UdXQwZ&7Fl5cI!jJ<(@4BGAg zDQGrS87{CLH$$C4PN=;7%ve$QiK9UHnn%u!FNHZ)CRxx&NHN&oVN~c1P^Y7N<#-7H zb)c=Sy-=x~`E?u5n*+7r>)RQZ4?yyc8~1ycKj%p3lKfZ2zw^Mq)a}zgD`kMk*)( zg>4j^Y3O{i6r7z4*Cg-Tsi!x0+}0>)jZQ?j>-+E=Ftu+8lsF!3CV(A`!;>Y*WKnQA zY*|3wDXu@tXzo}rf{^nSAS}u(fBYoFlR5S+fmjUqC3)Nci{{ku_&NDI(7fK}tI7h^ z6T$5c=iNJHhuZhA-!iBZG&S+wKDMJmKlhwmu$zvPwBO!apq#%kwKS)Q_glP2q>{(& zj}4O><$bVpK9WUx$BB=|XjKLpO_nkZi<1)+13pGJA3z8kHeT>ABuA!VBx>j==T|dg znR<7Sf~KjZwc$*P)9s^Bn&Jm;Ef>ZIbV7>v*n7vvqPZ&2GdGn#?_Ar6Iq*3*WPT=L zLJJNm$T5+tEobBmRcXPBSPAOTaabfl& zvEPN*Vdv` zTZ|K4msn*W^{Lr``{Dx=U@Eb4RIeqwELZA@RI^lRLi=)-ogN@}wmZ}z`prj`+2WO7 z%P6pA3$$30+a9jta_7F+U#hBn>-_N&&eYnPluiP)9SN$q%`?~6f!$gxZftGp^zoe1 zyblrVxI9_H?>^7WA36hZ4LR=sCi`Q#)f3pAKzUzI(-P3(mdQiK<4DqJK?{XvYPnPe zA-%a}SN2Zj5K%b9af7P$n+G@7dpH0oND^M!gJBwL3ty%fKD57?7uOe)nJ7Ilnl6va z9Yvk_Okf2D&HY^>VHv-iwIY+R$cv6KX+>)$?+!?tqV)Igq&YO>ZXS9+WXZHB`8BVmZq#cf{QL8hQC|6TAAep(G^YBgp<>aFb@xHPeH$&Rz%rnx!Tt|mWzZT3Xa z$^4M4h9VpB$cK} zZQn!~D$xZT+@Ob(2W()UQ40%4i;##^boXU=A%z5V1I_NzUC@s82zO^^3p7R9Si}h> zL8DhTk5HoW=1qGYj{_@UE2-@$6{~@l_l2_{@5A3k&kdo?X@8U*KwsqNvu$2Uy9ySz zJ%Pi}KV-8TygdU{66f9f0`~9(rrq^yae%idRL6@r_Z;~M(W#jl!kh2i71MdJ#7&Ui zTiDDl5DK_r+W#~0#lDLYe~{0RU&t0WV+)w;|AW`KDv=^od{^D-~P-n^;gPy2_K5@z26@1$Obpj$dZ?`E~_@_I3pI*wwK z3}8k7k$n;{+4~BN%iGUUblkuLAeWYsofp*PoDU^RCRm^=E_Tf;%l6R7+S+>C&aDet zI)=RV_LqTT1JBiLAVxO)`z2iCso%x7CU-Ra$b?UQ(S?;+KY%(6{P5oF?%=zf2MCz@ zJS{gzlsG7?Sw22_s+c%5?82-pXn*aRY5A zjqUp@`(&Kdl~LeV1w%TXQg|=kvv@D_08ap9v9oaDfIxq9jRyHR!+??WF>S`rDp&;eHiHss?=VQp=} zW!EPsAohNW(f!?u@FsT9aGtGMrf?=p6=WM!!_>%nEvtac(WC#0-{r`-CuGBQh4?25 zFTlkpDCe;;VkI-=VL)aMbk#CkoGvb1s*LsP68n3A57bY>vJ#WwJ(WEtcW~j2>yHD# z<&qHHO2-i1x=<1-*x6I*aV0G21tncTA{-6sqxxi>)Uu5$EG#TWmJ;5N6aZyYPJFUh zn!*SIaAHOO_=N0t(U(_fky>8c??B!V-iPEjfPw~3K?=98y#E`3R~Q)q5od?IoUnuP zrj*FkhDwt)Z@@wg9tWtQ7a*a4l`Jnuhft`HCQ~>N06f4WXLk2#8gGr}hzW9OHs`}6 z)JnSkqqDcDfk0c9E>@ddoe;GH`8|;zhHL|kaY_XZ4T(v(-PSJuC8qjI&{4+Av)ZnU zNJSIA&Xaxd6gDN4K@$eXwJPl%z|P~@;>qkbFRdHis6;rdHpR_1y1fCU@chEwu2~D* zZvmS*`al(W7(&ldqpLY7t!fJGdVH0q`g+3_9>{JXSlz|Heo^6~X3rEYxU9cG3HA~)A&nMTb$Of|$)56vqKi?-LWtDG%@>IF^rfBcO<2Sm4zW@E(wNnWkzWu{PL!SlG zz9_t2AW;8H&=g6Q*7ET>p-6F-#PT{7aF+mOqkb!d>uNW zK*&&fJ%k`NWx}iukZDK60`-rd9N0OXoWN{I<6g_~B|*MCnZtJUHoS7LXDZV z84xBg!(Th5M5M9Vy=rn7l$neGWc==gi0L&ZT4vJnnGd3B`}rWfmR;{VE$0QJ1&d&G zBK@1=l~(XY*!Dw%)smE%87(+MY52X;my(%QunHib%ocbtCJZ6*a9;$eH<&@p`Jat$ z!sU+WLGo=5?%7Xnd1smS`D}23wUIxDiqSE_0=(vyTbDmDD8`Bp6dECdDYSGV}joR}$>?lN4q>2!{L_8z84b z`A65Ia63y+e70I;cUkOvkO7*SFrWVF;gRe($_q?x^Mk+p9Lax12C@sv6C?yWK^z2A ziGt_hkewA@fo4nY5S<@P;~V2)5b8w;2WN5bDt4K&vH5I>xevb&LM0vs^0`4xp+(e@ zuxktm-2rQRz5({)6O~fI%DAYtHIuUkGkRg%W2?YpHi=F}@blAsVQ%dPW^B@rxr6-H z>afvKMbH<))hyK3)j2Gh6=FpN#LDLNKnB+%GU7`>`&B+ z$De@-cI(5htKK(lrJwTDY0b|ocF^Lo66y?jl0-RH%`()zISAW} z*qOy}HM&j+vh9@99Efd&%$SRo%&;0?no6NU`ar(gU$HQE%1|QY^)T$r16;YhhX+!k zt~cJt1g3x82o7m*UPD#PKpgu_EiA|HRdaI-9$?tg2M1tGQ!q1Qh=$mmq`RDgO&2U| zct774pqB#y5(~ZXU@WCf-BSnB)YQbI#q=n|yySAIf8DTlJ~eqsO6Zq_NSo^?;^hVB zo*vcxF21(5?$c*&1v?d1F^0h=lgxqh&lbpmeyGGeApjYwf}B%MZ+LNOxoEx`oBq7= zFLQH{;i#>1VY8fPsgF;nc3wg3iy|88qWnlhPoJAz92nOZl|Q4GRbL-PP3-aW?=N$p zrt`Y)vCw}h%WkR*%phjJ<88ua+eV>EcS3Y7#7Q+KIpiNQTvKSw$8rXJ%nW`5s%@f&*M%%+2!|hW#ZU2;GnT!6N^6r@al; zYv2cl*x_jB|=}^88uWB{PJOgRxa`@Yq~g96m7gEG`WJ{7n`{_IZXA5IZbX_^-|O?-G^TewI|UzG_*C8km-} z>+t5_;>L>X<+06*Fe*_$zx(a&yvE9yp+d#&VBQPmUq6v^ zM3h`G1dZ9bS8N4A0F^L3LxRnBY9H}6O+1R490GAgq8>@l_9y22LtEbP_spdD;o!Pt zUkYVXYu5_JZ~FUqOLROxeXRuYS`@A#U9vODrX5zKoB|21;o#QA-X6!>umCJFRt`Qi zaYXc-;yNJwM`B`EwGjSH{E{_y5F$n9n9A7taIKltEI;q=5h{^$2^o(b*iHWkhIqI2 z?!86D_Y+^Zt=+Bh`DHP>_(7?rBWD7QGW22zF`y1nW%9pHU5T1WK~MRo65-bc$x!%J zE%o^N8|`u60|h%U(9Ah{Xn*JapnMw`zghL|zUeKXw9J7kxMbZF3U(v_-3B4ECiyV2 zY!E2h!6;PRvX!J%P5x;}C#e+ywdDhz8%X#79zOUNpQ&w2A4pJr1tXf3^M_gtTr2L^TEZcq;#U(Z*n5e4W zd4A!{XIe~;q7>pjz?)pd1@A(D5+l9ZHQ+gNj>H-<7COnX%ce3Eot~ ze!qkl1^B>zGHJrh_>&?!pj0jiIOcc+k+%Q2^il>99%5Wn@O}%be}V=P#4fi-UND znSK9gEU@gr_sr^O?kNAq;b1#m6#JF&z5L1wSg}m_d@y_d%+EgG;sOEoWaiq~eqn|f zpm{PBTU%QoiXIaB3cEkLfBbvalI2dQG{pE|_NcLqIN)o9T0y~Q3jFM*e9ocy*nuh0 zcPcplh(?ZzFL8J>NP0B>85<)9!o{po9^@tmkYfRCW;eDCWW!Hv(6~z=VqqXR5++4af7(5_B{TGm;DeFHp@NC z0v%5*X^VsX{o!&3HyCCmNo$2|O_nr`B z2LN)Q<>25TO_T$tYjtlp#>i0c9t^YdK9tq~Z78p)E`A6&>IwDgjvAnVF$#*C0?(lR zP}>60-zm*}K$Clz-vSY@v|FBD0GA-glnrXoOhImQfRiVj>qaLu?4+fQlBr|!{NvRq^MeefpEG_49tbgvdJ2^is!9!>?9kK!R1oq>tDm6%% z!S(V#d9J|fW3WG|KhlI52b-RV56H;58QwdB@1w#z1>Sb-5QW+LX+i2g0isYo=?5U7 zjg7bs53sZA3}2NQGuVyEV)CkotP+)A0KSYEQn9K;3i2z}ydn}BXLt|EUca_7qa!0H zm!puPq6XE&5Hil!`^oYH^8fAjCKJ95Yk_&3c(qsTq+RfvDBi^%-0PIROji$QGCM_@ zf4`@Oy$2?K(SfxaT#m{J;FAR=B)pH1B1aES?+%W(ot+*Kc;1@o$x*5IZYFBd5)~jW z^Z`bbphoXW$H_l^!*!WBCGPI66&FFrb93V2qQ<|&CxjNRX}unJD7BTT)J6vtc=aqrUo{KD_> z!3lz1=noKe?u@Caxphu%E?mBxtUxT-qZ8i9RTA2X(zNchZkSr0?h#(|$~4tPEm;_z zwOV4?yQ-z^$#JP@Kco8mSsD-Mk{u^&O5$E{F3a}pK0aR}@@l?WY1Ml2HrO<$f?S~A z!U6|Zn55EdSyO;b`&|MY62>`r<8tN*dNze(X&_cBgR^Bzf6#VpPshKPa!nvtUB!*v zLVrU7#DGrw1$yPqYrFnp!ZZSlJm_r6ro&+T09n(v5NE(Uw=3?{HDp5YF~VnpzU!E{FV+Gna?YQ+$GQ zh+nIghK3j+zccV@;n+eX4NwW#l9Ys2q81D{fBX6aJ*-lAKOHwxs&V%j=@xu=3k@gS z#O{|^fBzmN^LubXysFX4q{M+7VAFo}e$DF!ZhxPw__1$oHTzdV%5beCtA0gQD7#do z-a+Bg?fC&LyNq$PD#ZX0YJB`uQ)_eM@hmoQ1~s#K>N{Z20v)Pn-%+0}wdhxQPIguD zWi5{RuabX8H6Hgd>V15Is+JbRh>B)|mz=gX5qS`r(Zg=014vjnYA^zJP5xQB>f5kq ziv#tY+ugGF92Lr&^SkXiT|2%*i7u>RV{G8BI zGr6!tFRev8bYg`hDd2669h3aPPNu&E)drzdd*S(9Oyvu;}^Ndh0czrMwRngvM?E=pGu&LSf15t}a6D&0lkk(^lQ`s#`sgdK)6 zJ&rr1F*9BjlGDOU0v(!>yN{Qsj~_ZrVJ243L;_dCx=CyXRAT}+0!ji;s9s3%Qyn&t z>mwLkyhc`DW61W$MBDXs(J1w~nE2$*{{kdCYr}E*|k;EjX4uKnkCtY#b8pFLTfqV6yApHGkzbV_>$i6>=!<+q? z-c*#BS&c;@DF*$GW>CZo_d}Ic*E>mj2m8IlfSeq$(R9H#z;LZH=;v|RgfDOZYhSAV zrkEBCYEHJs)->JgV@gKOcp$}Hd)^3Q_bX+6k!1}=hxDCbR5}jm>rc=CHHkXIJ4m`g z;J1;*+&X9to?FOI4>!9qpE-pp5=}DQ{~It-G+Zjov>PqO^Ss*UlBdG<(}-L!>@joE zt4;!L4%zyJDp>6qJ0N9beAlEU3ls8cw3*W8IdtxdMc&^xLnNJ$S>=$`pfpO~ta6&%36S zz~Klct^&kdE8YZUWo0;9yH!RPZFvRoESkg1wRUhHKQPK^6n=@eZTnvqVA2dNxM}}B zGR>~fG$$4rjQEu`A89#~-y?H2h9keiX&7j5UK2zj#>+`k;8SvPMk1XoMF9D%J=?d+ zpihqtzqqW-XeR=1*>%M|icm0YXuoIMQTg-d-ofpmqEedOrSuk4wBUiUF@wFBY_RgC zqN>#e!j0#B{3n>F4-2j%oZyR7wEo}45&cW!Oc|s^y@@;1EZgt@T8s)=;B)JCV1+Np zTUgLPJzVAI=SxzQ++S~ZZGWh=7;^yW{Ay_l&B7q zUzS(}w;o%n3Jnfwc_~GPX>O^m4<8>N0Eypc0_mHMf6+JXmh;a$9*h7~%*W3Uf)F%Y z_LHigZk>0n1+2NJ3KS(V)o{_=+vn@!fhrTFPDGhRfP?*vK_tdAIe4D`(GAvi(`3KG zC*%gxZq>oS+sDI2Lj-X1U)Xxh2L|U)PY^BAh&vO21p;hC1x_3EEjecWM!ZK3{uG+lKxp6~zXqoz%FO-*;l#N@`NySuw% zW0-FC$f;qvrWwYgO;5+8yE}fD-#OnuImgCxU-vs+bs7B8Q7cl_GJ5urFjaYQ^ zX`Nh4>v5XOw1d9DMOyr z`KjMrs&eLR{_){?U0m>+$r8Pr#51?DFyV%PuoRgR)x0U|Vjvk9DB$sNA4CAUGIGgs z20$@f3@Mnlw~H;iY*?hZ4iltJ%#KOYCK#XhCD=cltJQGxxHM%xyOWw1w2N; zQ-rIQtRIRv=C`$l_#Fj(BI7UHx+%&ry$5$b2b09X&Tju*-8sF>;D(e|XhXqZKrCX( zo8$NtJ#>y?>fo@yP*2vfI0D0VSUr*Zb$o6X2QO&9-QrAO@6%Y=Ns2aT>B zWx$OBM;(|N-sX>e(9#S4{t@QdTs6PndbKJC#syO#skG(Io2ecSn=)NZnK9g&E3J4Q zPsglztr7V!V=#xZ#4=C>mZe=j=6>tV-5ze?k5X0-&fEt~NsiJ9CpAqg6EJTOS{wh{2aE?6s^u22Huv@qo09 z5M8lQupt$j)$DP>ty-u288e?@6W{`pGk}8kt-g|zKe>B*bk@rN91IG7VAdYG_WWdT zLqpY5)6RxRvcMemE?w|*^f@o?0Yzh`^0T@2$Gc2$cPve|ygR4Uh!CQ`q3_k40Q|gsSA%363}srPiI9Y2uxiKT z=}6xes@K@fI8ruVIcl=&DbiAchPmo42u4_jl`Au@3@~|!K5V~QH@6tWwe4B0I_tkX zQk^R$^0>GFp9HXWIRfBLKWx|%23CUm(>-zKL6no^oWw}z5kMbxec{r6pBeU?Kb1kpsJ-M$H^G( zE&5p(=eRMm^?JeRMMGU81fr}|oC5HQNHp|_Up?`IzpZ(}jQA1#Kfp-?Rq@wXa3?er}sJ8GKhE-*T+9K}pbv?gE!bznXO0U^a{s#;K{R|2D&30@*~;e~YJaRt?xJj;Zve>x9vguV_T10JNAZp1^De;j#o^$d20Jd2Vo1dwTwwuF{ z?%s=;_^*t|J^^6Zj((%~$&8QBdWsY*Y+$d!+dvqUuWJDCZFeJA{Al5vd}m$Vm%rAN;LQa_pq+^pffRy`$w|s?+v@Rg+^(%Fmj{nM5`KGP zFyvmh8jeqlUwh6dJGPm;x>~M9&F}oGT3C?ZMo~E<=kXphP2W?4pgLbjwr8 z`1R#9Eg8VK)RWWP`g@2@4@L~|i3p!}(2s6qVaA?VAW$W%#f!&e|6f7U=WO8oc82w0 zwjPg{KF=H>$e48GsAsFo%eh9&;*AE5$md5&et8s<^v0aqg*Tk!F{&TMzqa`WS)AOU zU$2!`g1Tr1gpGAQ^MGOpRtww+Pkk%^>qespI9UR6>AvPl3w(pWB@3iGP>m4tMK%}) zkYpLEh>l4L&P4)0(W%mmYv)=|zq^7j@CeTTaw-BlrsRh;cR>;nPu5lWFRjK{Y`HkP z<)kf(lG0`f%0)>HMt-5e_xjJ{AA1pAz3%rydKnE5zEGqaU>#auC&G-muyz*)kPTX4 zVnD|M<0~=D+S%xCM`;GFcwC%U#?K|mL16jMmufRE;&Vhd*GA(LQ?OQ092|iC;`G!N zoR>s0jKr+0VXx&UCSRfp=GDqk@$TnVdABM@m@N>eEZjdN&&b1*^mEsi(tDeRzoH}c z?r8SU0D9`_Gz*8@RI^e96b8yY<$^!6RnjivvT3kfz>X6!`CpSG!-oX3i5YIU_GE z&#$RT%m5~kekkQCIG&u_m;l)nr4;bM#MBSC*(hr?M(ucd6hT12T%$S)_^q*5nXjdQ zikuH$+cQ-6I;mQ_tS+n@tmsU?v{Ud^mKV&u(Imsz68C-oxU8P1{ZVil0Jpl}v&Tk!F03n6ZeCDSZ8t-i zsf@y&mIQp5Q>GgY4|hAul^oz}YhFb=K`Ax2uZe54a16@CqX0Zn^0j5gDkTUp96dbh zFdGX-TIjfOtZ3P^jg}QtZV3@_Y=SUS&UP{8I6_StYM69*=Sabe&1N$v`dgca-0Z>+a zR9F9^TfQ0n@s*J4Ig@xLQO68)^(XW0?(Vn1Y2KQenm?`WNsK)H+c<%b795UgQDUVxce`xkz_?C-q`lWJWVXvsb+ zm>t17>`g04y<=?33;E+xIiqHvChe)oQ14NHzBixdOebZl$Q<)NMlj3tPt3&XN1Kyv*wmoG;$(ULD+=`0Wp=gZ2>s1teWg_9MSZ6yHP_9xxuxx}3 z{BvEl1oSX?;mc*CI;n;CkV1rrF_5KlH5C;jh5WCX=;`T2{htB=Itl*sroCT!pl7N-?^PO=P;&W5Lz;ygqO8J%Z#n?M(+TXvy?neRYm1Ef=YPe}{b zojYJoXMesU2MCFr@7@a>Z>OSYyFUKc8%_Bh(2s+utUsJ`mh7LdUt!21@JDS(tgR}C zKanczT=*8!QX3Ti{E1us_9^NTs1|U}y4)qOGB4Ag*g-d9W1|6x8o&YA!QzbQR#Wn` zg5i}|7W9{>yr(9iVO%&+;3p`d1%bs=n7lyy`3Iy8jwLnTAXk=uRlz}1W&^0mH49upC4PBb{jW(_((FGc?? z>%EDjIFNqjj{zrmk1ZF%in}0if&4KgY&dG&uw)(_w85+&uXLgU?w{vwnVcgBj35BS zfg|@M1IG;z-#B(rQAmEx8W$|AOy5hIBKFHz(@DwtMn;gIhktEW2F1ufND4SI!K#J0 z+5uysqIZArKo8s;O7H(uZC0(#dfh4x^hz##xktFC32 z#NcyDY}h|jk?3~-xjqDG6=1=M0Ix;vhp(rfEuN>*-E>l3QcQ3FJ33BCfzrEn_KPBY zSdFcR@DRf*xU;Ph*!YwwO^&fAKj=~m##n&x1zU|NKLq?$X7##4-YX#9|MmFY-<>+T z#8vg18=o@$U&%{tk?_t!6rgy4owIKcQ*lXjR!0Z)om4i1E{cYQcOBvZT##tcI?&M3 zzslx|K20VK3Er`Sm8wd2Xnh0e=Aq$%7{eDJ%xW5H(npE7CoK&Oos2is5dmD{Idi6NQr(8U zXU_B;xdfIu_qi|ycT?u%B>uT>d80L|Em)iX?p*){e6+J**RhFolXnkV`(jhnEYBa& zLV$=Bs=|L--oleMf5!lXf>7H%XJMH^ym$KiklZ1 zu=FFqi{LD>I4Lj*18$8~ND;(!6LHDs%%e=R^n8(xkOw>~KB96u057#yCoE)=Bd1f}Y+QWyVK# zAdq56s&=^Z9J1sYB;@!}Tbp!XYcFl=cHi*y=u0*ReAxu*z02y!C8)oxiT*GKdm zEf0liZ5%LRitT%#_>q#z&~H9;bc8H1B_`{Z6Xth*lFHrYYDj-?3Dhi>NhHAIbx|!m zo(LNNYs^daI{Gzp^8&XSiiT>JhkEVAC<9ANWa0AERq62Ko>&UR`#@PNvR(+R_xR5% z?CN9DYTR<={I?&_wTCQ8(`B%~1B! zs&v6M!nhA;94JRED4~jyzmKfubkrKHd$_^7TnJNcp@;#lSrfU6566e|L9j5)y{3(@ zm`rdhWt;Cs7r37-ss96W3aHg!3(+AVi6-+y2CWuk*nC$aeWR8{84J$ zSWcHNK~ZHQ+wxejkg)5qX&#gc`+oUF2QlML@9Q7Il{PSN1iE3zhNy12SYuAo#?@v! z9oI*xyaXnIYKXvhqI@x(7aj~Wbkk~me`L%>{6 zUuMvT1|BE%i%cFn3CvULC&tVb%% z)EF-{>HGK7mv!9Q0@q5!B_gwAgno%s{uE9`@3HR!kkRgYibRbX^`#D9t)2OTlWWFH z>J8fqMLIrhF_uD=T!?+!RcAAP5Y@SBzjI zL=a3*;DLdFG`Ew;l;R`;kquWN!dj9n0Fs*iC@Fz_8kLg^Z$#F2J?Y65a>M|o`Suo> zn}-LkYS#WmFj}Y-(9iKU@Zufc!ja^DC?%H4w1zr9hg>RPBR>D*J2p1ni;nC~+q8(1 z->ax|076uC@ep)+dN#`4SQEUNN?4)W5NlV_n7P)~MK?x5J2@A*XxX#psRWZ?V+-9c zdlwlM32N{Qus%!vHfaF1;Qqm3@JfXIOSJ3lzFLOj#fb(}7yo@>3uNvB%T)<9AuPqE zm2w}LKb9a0i~O-Yp=tJzWQscBPs_OBhz!1uGFoh16 zHld&-k3lrG=dNAOKS_$I8v{HE!@KBOa8;fc%*Y-rj!eZ<)`;*a-@9z9abjiv?z+EV zriTOk;w!n*zW#wO)9QHKW1hMiDP5(&+Z!u@b1I-B%D+eDbQmzdz1WMQE(Vz-jft+2 z-6mVdWBG{9<@xBF!y7}WFwjhaFs@%mGJpEJDl?d`&A3SvloZ}taUP$5Al*Xz2>yC% zxW6nn_n2!HeT;Gc!at zU-AR046TVmDjz9EF(LstUXlod-$Y$#pIN5a#GZaDO<)87I-AC%hZ%c*m;RqoADJ}( zlkHklQ>DXJjL!_n23twv{u^{+K)C>D4?qHvA51{t9mC8C&_e~@ZV;)CtAf8B@Y!_G z8@j`G7{9(UipK>eAn2<7`*Fbn@PV$Ic`6t!%P>xs?faCii!g!6(H3pywA2(iUnj#JGZLx*cIR zL`IfCatwyKLUB-1nO?K^?wx>C(W-C5pkjY8P#f+u>7IQYLwT1a`V7Y09o@%#1m?X; zFRCHgSGm)g0J$k>JFWf4W+}Ap=m=Gertedr;ILecT{?5B5?tT(srCDzG|c zqGtT`1ulFS^sBD@Yij?92G8n101O5|875|???0O(>q|9VE(rF(>Q5YlT+!}N0pAg) z&8-UTeAHl1(lg1rXiQ*tssDJf|mI&z6qG1MG?<4{|DNGl^D8~Lki6Oa|ruUL&@FDIqY zj4T@X=p(>OaERVvAlzQ%1G&81)m?l)S4K~r14Giwdfgc4##)V%X8y$9M|4{-EbuA% zrThh&1ci?@!^3R<%J@ms)4@jwc$Y*&Yb>DTpFO$IGPg8eaTc^j4W-Nax8bg;rx!PK zkh-Md1!d@19v#cq04{%CTRxFr0bS^$UujO5Q_zaKC8ecbX-3iSVBpGD&cJ_A*Y2w} ztIv~`mKE-JxD>GfPttu%ikW5L#HZ)xC-rNi=0$d}<)C0uCLea&*5}Raya?^_V8cgC z$iVvp^8{#cv=C%zO7yVditgM+?phPUN}h@e0?j4`pWYX^gjfI|F> z-LmL+pe-whC&|+d+?G~+Z>)lYE1C$^#j%cpen7X??S7fxnnPhzrPz@0GweLv=5{ACLV*r*_@yMuC(2Oc)So)X100zc3L`fj`%EiGaPJG!z zS%e;=`CgAz*1|^!b2z4vT=^ri5*;ULPrH4s?wIY{2M!UID6@L#i%rCv+gg>6rAVxV zQFLTK5ee9aXhDf$NyPytT7^U>s}8XVW%Cg8s7m&(wy3_qbE8o>#L8h0?yeE-h(MM%_tzoVP4`D zA0HCH?`bI+#l*6JOHB11o|KI2q|pzzN;aPc;-r*n%wAY(Q%XQ3=Osh7-_J>xKfOe>WfWu1;k_W@y4`Y1xs$G8D4@GIaFGI`SS zNMD&`z%kd<1+sd4PEzYv>yZa{GE21LeAkw@R{pp9} zx|mCds1VIUvMl@|OSg)C=H>%#SvvL+&GQPIQF1(oVe?#B+V|ZlakYyyBy;N_6C|IAmcGq26AR=4j{1V61LH8#p zBqSvM)n+I;N7sqNUW+~hZeZzi8-*bKJ^)ziFO`N`*9-db1>nX1 z%3@<}zy9-LCLv+yGmq6Y2H5N+ZJs`ZE(7pz-^{(U+jvF-;LnNFr?Kj4ti~;w4~>kJ z>|0+GrF6thTDx)q1D{~T8>GIGfr9E12w3ZdbaeJ5j>Bc}Lj;#G$vNR-bVD?Y#CH5~ zIyQ2<<-mNE2sd4+lB4!3obaQnC7f$RV-kb?(A*^FATw?p4+1@j*{~T{uQl%) zh)wFr(`F?Ele}&gVWBEM0=_Ff0PQ!F;)>)^Ev@h(dO&#Pg8xGp$amJOjBK(cBhC2Q zW2((&iq#oA;yE7bVDI%mEr6OD7rh!LKnc!~#T=^nfiMza38zytPa(QEjVH|){-?8^ zhiylJ5!RQDV()VJHEwh2>eK-gDh<#XHHNHyIlYF&)Wm^&Jxb%tg|mVati0TO9>|Nu z9p~e26^Ux=RsT#Z6wpjUrD%MGWb<%em*T3Mt|rRkH%Wc&vF|#%!i%a0lQu9!tgaCZ z_6-b}8aDWNoEG=9CwK6l2JX=qgB zV4-4S59}6-iv~hx&0jt=>Bsh%=C-v_lxq&g54b*K^FUgc-0*nGZ`0pI`KCmbrnm=3 zqD?s*MEjZkb@|V3{p;$2hHgEe1At_&O0Q29~(HW|19l+>|G6{j$4s*xIWj?`q~ zPaG19y*EUx`4tt}fV5KdyD%Mx;Q3tT9Id(w1MvWu88r>7l0m$~w0@wFl}eWD_?c(< ztH*WSovVl?rdbF?^zv^ATtPv>cbmaf)JczyD{jzv6e~ z?CX~-#EoZ>F>~~^B#h}#rUl>|xVa?OL9ge^v>MFTAd8*Vunwnkz8l!2n(o?B&uqky-bo`jw)ZO|ux|75S3a!m@6V<{~)1NvVz!^KQyaixZ( zvddnWy@gpraUH0~Wyov$WuDpt`Ui%7n{&IsdiOq*BGu`-WV4g~%{eW|OiN8yY~%|b zvyPXi8tXeO6vE%NTqod!#IX!eGy8fidMd?v0Uk60iU%Svj<`QN5S8}lPdy}k6c58wRywl4-zj& zv&6_^zW2gTo+Wszp3UA{IZuDg@sEOZxG6M`4BG_V*O?RxBZs2hZHBN|$=>9(QKaa{ zO0gG}6hacD!&lFIPv|M+x)M0W8^QrYVshlvp?GvTqUY4J<9@t!NA+E^ltkS91sMF4 z_M8r z)anZ9Y9M@F_hy<@^whM(hWy@fud$Zo4z}o)Et`fty?*hZf(tPY)W?rD0VAKtq+SQ~ zBrv)DT^J7@-8yi=_H4;QC*;_y2h2mQS}Viv7_th`Z~Oe5I_QUIfedDeeJP{cQ7&O9 zBt0o5)X-@qt<7&|d;9m%)mUb-iJ5~#LE#S^C%adSA|g8*#Hp>L&G}%7p4Q~bqa_}q zVPw?Pb)(=X3#OJIp(2gZ&0HpMB^;56>$(?xGo(@cfk{C@!33ISlrRxP?kP-e&6n3| z4&FCMo1K%k8Xg`dQ7+L3hO6d+c8DWBL9ii89-Hv1tvJ%p-+Q zC@%mM3p_f2`7t}poCTv7*uc${h3PKX*$PWaB8)l1cMA}-v?$PkaYZ-3_vk8j%KBsJ z6nJ}l+bBb&63*j^idi{|8mXy5B;zJbimIz$`sE~DI8*~Vf zF$=2B21k7kVgzh#ZG{z}NW|)zVg2T7g<3{Erd&}8S&AB3ZB&kphr(}I>n0W!SWn)( z{tO35EOqxiHv<6jbvf3$0byP=rBHA7zh0V?B8l{@>&PQ!mt_SrqrzcPw<7=>yyCBtCjWW>FZ;U!_788$k$aw#^3`_#8hGE_ zSixUkA|jQD95x?JCtZp)Z+xbl4;dKn)n1N@LVL}QOH}+CkCCDB6`Hq-_!%#J&QpQi zkD|hpHKS~xJ5{7EDfu+J%hUawae0Tk;poRVj8p1~2Tg|u|} zS$oR+?_ZMwkGo4?98z8h!<@--XNv**LV~=w-b)qH(|GrNck><{H3qloWLOG93yYT* z(70sv`^{L_0n<)bd3iso5x#yN8d>l(^~=D`0(!Zm^QAqkEJGI5!dk0$R+rH`t(U!e zj0s|kxwz^~iAN)rwPSO0b7riciuHBMH6!$QnK^UEtO494|3EJD_G7u`LgQC)IfE7>)e|$njFCq> zD*=MEV>%+CjH!XYG}6knOz@xsD9>Hxn1Z1a2$SwNZiu`e$~zJu2_pd+ho;UO9ahN+ zfF4L@#B(zM>kF$;P-l*zbn3nv>BUK)2Q2Jjyy1YhS=b`0Z-Y^9e24+>0F_JTP}_lx zIYkk7qy<3UPM1}a9;=yi<(7A~1i57ea#-uJ=deB%yOMG@VyAS0jK`Tru?k}H~p**7< z)IJhG61C)a$7as0gY8;_Uh)}Ez@Q_+$?LE{#m4E>HDsa6q|JR{!+m^m^2^LC=6427 zMF6{X4Sk0z3%8C&VyiJ~MGo8rj4i`fPiVkqC__v0BSk(@J5Hi5G^q8CGQm8Tr=I%n z7|0|3UQ}Ea52ad4^k=lW|MlbrbZeaSs7C+_1yHGS^W%WU;5SZB*3L}wiq?Hq%lA34 zK!~wdr8f>04`c+j2e`AW)^61eHF|O_*u@G^tn7 z%l&00a$ELgtS7LneV&9X`HatQ*mPgFwdl5WP*6zDNH(R5X$d>2?k#wf53=PPA5 zeQb5(B8elT1wUe&Qan^)(6*cW#AohY$a|#AN=Q5Q;=%W7HA}!SX%6_mb!~0qz%crO z38)KLjhgX+SRaryD;EOGT^r7za{xeukWTEU17Vc;_8m_vgzmV-Y%yKwal@xgNy*01 zF%|LB>xp5r$q&dF@0%;FJdQIemj9WxXt`8>L+i0bFjq~6`!s7UQ;3UhdvP=o4%X~2 zbxr3ju`5N{z94Y75Kv-l4WZ%zKKh(p%#kz0>yZ4^%`KtdT+Cb`Z33uzSAi!exsNe{ z?x>_L=S8S0d{&meM-!8h*xAAvK-W%BWoV@fRZoWS;>MPi0>_&G>S;FF_Y$9O-V>UV zW`Oo}Ox3}G3-BYW&MZKd3St7i5BS7>zFrQxGT2|L4L>7f0|{Br)Bnytl6=`qK>rRr|29E*~wRr=Zj4EeHPs2S$U@EDG)$0 znp}DLfuI4{WrlLO0B}=|=}71PG3OpD)sFr*X3dwl5Hv1?hNM^+EZ=xQcVm8AR|`j+ zseZqFdnb|zE|Q}RJcj?}I3ET*I5ep`m?^@3xIG8-1}ALaCOU(6gKll1cp_i1!f$W7 z%)~*}YLE6efg*jdVo$4(n{TGqUd)VAhXyme#rU0Oq3T6^u1CLqA9VgF9n6quBwaMr z#jH2;bPeTW9KS3Aeow_V{{vIP;M+~0C(MoS|E#R4rq*QKZ#vRpOp--GaUCF@H^!+M zMGz_lp$l6FLIZFF$E?qjvy%bF;kD$A88`plQdanRK7?Y(;{99~HRUBP5Zc%q{dLtS zYwO;mcSz2l{+sTs$2x!BO-bD4y_v=P&= zM9U#SgwsEtxo@wasoi|-ze8S6EkDX>HO&h4=blXz%6*ehhrvDevJ7dT-!WuOgaUaJ z(r>?`c9q{pMTLc-+N_bvjJwp}-Ol#8|7c@?y=e|Q`?A8*#}DzN`y>-!=>b(=SNYhP z58Nw}_Yv&kg@LR|5qo+>2=%vJ=!KUKC;abZ@qC&jEk`pYB_+iX?yjuHznthd-s0|( zlD=V=#eq+hEK_Q@yK{ilX!jE)fG@GH_EaWB7UID>Yd}4CYGwY%B z{#jtFr#HAI(k|$@kP`7=gq3KRm6|H0tBdw3k-QraK8%Z_p8_BHMsQ~s^CdZAGCnPj zz8y?ekd3Q*4zBOr1j${az&_M^Mx<0wuxOcIC^Q)|DRI&Sg2D?razG4wTO_()olA>; z|G*A(<1Q+d!wS5fnP0&(6TjaQ8NVzSFytr~BFrGsP#Wbj;oSx+qw4vK-(5Fn00}5+ zU4FuU;!CWlt4p3S3d$N}Z;9@Byr@JBkzDdWdmouNu>SPp0Tw4Q@|MVe&PR3R8&^on zQTv@lyB6eK9qvz+4^l?=gFH_s^)2~jK@v;eJC)J0-pRf? zP@u>8Bl*jFH3tu~sOzh_IM$~ILmx=L)u>=O-1!ruW~IKQj(}7xIylJ->uZzrzddvB z-rRR|ZRtsfo2W2y&z97_$`tWqNRU5&+V5xyhXg(L%fYoit;#XsL!X}R2+5SvsI0k&g^n0mgopyU z(UR{|)=cb7D?4&j;s;OJXqHJ4K-~-&73uQnOkQPpE>_wS>p@V9T#@2Ly*u3ffhUjk z8zXZqh;nhDYJXS3wdj{#x@E(FLueHLYuS(K;@8Q@|0i8TGc~^@XuVlX7LkD;#~tdlm~>zRTzZ z`q0-m0{y#q5gi!5XGqGre1HQM`lJ&xC}-fl3z9=uqszDxfUNE4 zXhY9z>viYw4op8Mk6`uhA;~jDAC)wgOxY?J-+2998l!m$Yx7GRw+JFAujH~eFV&Cr_zK=4&P`$4qV?>UcaYW_<7vX%NZP)DizWaGI@Y9;%PU{Wi`mSy6}^ zlG(I1WC&dde z_SJA^7ENkFG}5Al|KpSvC{&xsadPP(1w{De1iXjV`ZJO)HvZ+l!_jv~Wd+JnI{HiSn++FE<+lB9pBB^ftNoJy>r|#hZpfvZ#vR^W{*h&VLEx zc&hB*#F^Qa6Kb5^c^2~SU=?-84*J~kVf^_TrS|p%JnJ`+w7?=Azm{Vw5L6u3N7$s# z{?;uMYo@YeQzZ_;{_ePqa^5o|W^lS}6I{@MvAuj$SC1PRg?0ngbIPpi@axDFb8DX5 z8UR^-V_ir?5I*V1$?bi7u?B#Y1m=M7U1+e+R-{dEVza%UUvTWm2qjJHqDynr2AFms zKELGcy%MsDq<~B3+3yO*Bu-nIQNE%(Qs7rEoRoWd&5+}xZv}C=z*)W#)ur~{n+oqLOR;{bKL8o0wonm_RN1gh-Qu3T!dIL8NV;G3g# z1%pu$8)1^@w-!#T%OT2BWg>Qiw)F8tT6;~FG{c%^C~&>8?>bU^pUQe{(_wthX06%|JUxUVqi<{H;ZrEkyy@cYb55bG~#N1+jrXWC>a}R zTwH1znv`}G0tMz0MdlLq2z9vYk%r2+0H^Oka20#V z(OYbK{96n}a2gg@3l&AdIkjIIe!8-X=bSC26g-#~_;?tpaQI7P8z-vk6C z3ahW-LwF6pVsPY3v_ybvoqzcm+aHeV`Zvnlqv&_TuX~jpl%V|t99l&OT$CC)+>n^1 zwLPl(>+{e|PXw-&J7EsQz@;o&TP!XvQcE%8HM_oY$F}eJ+~3r#n#+tz>vcdOv&jBzWd@q@TO+Mr68F#F@-2dX?hH8|4LDNbaVU2wu=t1J%fV z0FU31Um(R%fJ)!_{Xe-!mJ+%H~>7U~*!XjEc_BP*< z-ap-lbc9?zHj^G%Nj#K%^VG<`xVq}Tdie29(QFWsR%#Jl9Sg4r*9d};PP*sYJJ!yg zRD*{p>IIB080`O5!K)njlJ<}ju1;ySp_1LAaB_0So7z?WB0%pMs@ZP|bk;-P{KCAb zR1+E#K(}VRsgwA&fqcHs2G3LU+;~36iH~D+zL}uXzsINZ{xyLTy!VX8epf2nYWk@c z)gAFV_p*vvrO!p_30X771wzp2 z=jUTGIu_@!^(!buTfPby)WIucu6AL(j_^EaJMs}&%J>)?c#*O6Wugd0ZKJDl;x<#@ zkhtrp^~@B1zkbinrVc*;_swYQ1I54e7wP#&%y9QqPZtd{_V3{SGuU&OXa5~VL5IfP zbZxqjqV{8H$MDf;=`8!-XwZFl(7#Ksjjwyzespone`FWaJ&fQSr(7{o!_@DPA#MbYX?uXPp%G;K1tA_;a0G!OTZ*@B3BS9NCP0<~u)x>6v zPD`z*fxT_>buM`NGo4|WlIAszR%5(wYT4fFbsJ@*r%IC{OKn#}Eu zzf)18z3Cs$5a>Q@9TIH7W5G|Qaw20%QGESle@e?Bbjwo=emU)%zPs~kDqnBpW)WlY z?9-!`031cYV~a|5?!0G~!4%g*qgnibN8IKBmYARGuZ=B<^Rc+`wej(vv8%IfqZ1PM zD{zagZr{XR=1p7!s@Kev*?70Nt*v100g^e52RQGCW_IuzC}STE`uhG8b6MBl_j0oc z7r#CTpvcM}Ken#kJ0GVCesJ+>Np7b7%t}POqncycD>&75siwVU@1Icf>IqF^ z;`VM%W7v<8tik(*%KN#Nour)GB8#{&?QQ~Ie!YB{nG!FTkp02i90kdo1-=GtEoQZRa}vBAhi13q9forAN9~N)K3l79=ly!v;|Z|x z8CcpuVQt4#k;()lAz_+I|A&~Hf0#Is2rvHHV-@pqO~uOUH-gQ~`P1w?wV6=8A8K>I zBTqOTjE!L%JE@+!5YdAqCvI+T2Io|^JmHJg1TkMUHSbM?+)Z;BmI#SwrF-wpT(;y; z6gI<9z$c)wlk3rn?QdGlS1>|EZ6P50d#v(#^*gW!2*bKD19yk4}^VVA1@x0>h`vWn&UoywKj1{pj`F~ zZuehynRCnY@NdDr+srSr8I8E#a~GA&X}!Q{YX80nEiwxd5#d%GJ+h-z!paenc6#i$ z>-Wfj#+479Az>;T{LsbG5fwI?sSt2EEaAhpczkT) zyZ@W+C5Sc2VS<6!E*^G5g92(+yuV;zCPS6J?pX?VsdD1Wze3l z@9sA5`ymT6wD$P*UtcQ;(^C7+Qi&$W{Cuw{eb=j@+)tkV{t@EtuqYPcz{yk2zIvtM zM-JNwCCni$O)cv4f8HeE#6+5zDgA4=Dg!UvS=?sw zC11hImLqW&N~|26KW(FNt2doKf`+$Q1{`{k&P#^JL1b~gGXt(-@%wCUz4V7l#3W*iVux1L zyvPk2&e9JXdKn09y|;TJCbzJ%bbgzmjMRG!O?^w7kr4l?mn~Ym2E$O+IIg_Pv44Kuf^JhQrS9z~~@+V=3AkL0n7V$c9#p(}^odciTirajg$+kX0K97c| zn<@XMK;^F+nPUF&$5Zr>Gw%ZyKbI30Uzg!M0oZSdzr>h!(V44hJC@P8GRHQm1__Z6*QpSW z7e+qEyqB1-7gk~yUwQBTo2xAEF?|_yeb*80cNq&a9%**jQ?oh=^@|k{i-nZ3ROd8b zrW2Qn*b43Hlsa;RB*Xnk6LyA+s7H$QFs)$ZYFqy#@4+8f5>1!;VWr9ed08dSY%ABT zF;MRAJC50>eP@P_4d_y2a1W=b#@kLyF6X}smjTje-o`nu>X=_=cSX(IF5vVD6My@h zSuxS&s#hIDJKSF{Id{TQ4mn5XYgUo;ZjQq_cBd3Ko%DFNubJlVnjY==fKM#sD`(64 z+8=HD>mk))x#5q?B0`3eCB{8`7vKJ~Ho6-S*FU=*jz<*rbS6|`pO@5t2Cnt-{vfPb z4zCnmbQYTHN>LO6z#O()pCHI*AS67DBOH8 z1$g;t87ELvr+#I4V!<-v3a+q?a71%H9B~TO=x6A-{?)5-_P2k1dDqNn>@xjpX+bdt z&i`8E&P?(g+TmqwFtRxG2rUnH9$i8O=S&;iM)LR|?S1)YUx_)P8ka1iG{0f6D^$;K zr7`eFYB?-2HpwSzZe0o?JS^=))vVHg>@o-Y?wB*}f#|l`akHmN2;YRWzr=rebR>(K zYCLlGY>wq$*Ml#MbszNa328vPn+HFOcXqG`8D$kJ_IsH_{ANlg%Q&$99qwnYvYVwe zi@UJRlT;=CRRXf+-)vz$=_vEMi{5Ou{8q;nzLfG5X8Hjq$gL*xZ}i$7i_T7!t}_2r z|DjCfi}f<}q2#|v`KwVy!`<6V*(=ET*TKxZ9l7@}zP>@i$Fmo=pnPi?3Vt&39lUuX zV>NwEU2Z?|iXF05`}BPcOPbSKO~W_T{ZUCZhlhg)pUpnz2}UZ+4b1!NM1XWr7Gra+VBM*&m!@s;2tv!S+neTikLW_b^g*mp4uiO zmod2JD8gMXg+fbMRqLPh$^mp+!E0jLSp(abc;6{pFd8T zs%#iTj2jSXn(P#rV))|`QIToKsqK+oO};ALfFA`~QKaolf3C&0!G zvVgCk+fYwIYc5?udieIuDQBaVP!{8xf$d7`XWU5sQFF7(@T{zo)f%k}7{`)_PW(C7 zQzYT%iN8(E_ED9Qcm}M+e#fAnu7Nqb{Mvi49zu_DrorfS@Yq>VD=5--=~iUy^3J@9 zT$H9$sLJqU;yf|reOu^c_Ly_ZuZj-k5v}@F=@*C!tVZ(c6jY{JBTi+TFZ_37Uv-Yg zvbb#Od(5Iz&{+4t^^~`atcggF8jf+L+Nq+zO*La1{pTUKmddzxXJV)pJyi#;K2F0G zqu--Pd0iSC2lly1a;oTZs(5TTFpjT|A*g*TZTz>O^(VER))H^(9iP{O_P>cp_wRQwy;8!*AkCy(w)t(>0a}1=)!+|KXihqh=d+sK-i3~38jlr9$v$sg$Y|A z%2WwbpqwT4TFP+oSsIue-BjuO`upc|TvuB8D>vWGW1Mlu@1ZzXvhqr9)=zDnS&p zH;Wf8-UWQ<-h9y^NT|fLgb0%oEf3myTubYw2zSUy{TjA+*@S5CRuS}J6c`CNGNq~} zQ?W5alg>Ax2*eq~mxC$%?*6cNRTK2V`hFi2qgH|8T?z4zy<=nNNws^OToXGt?%-Khn z|67@xkQRMNlrZc=1LrWY3SvPjWzJYh>(RGMRFdF{7h8||07687>jF<}Ikz=Smr5S~ zg*~Z@JPZnM`dVKC4ufHrW!6=Z)vK(yq~f!7UO*iwgyAu4UI{DK9a1$-w zA?t^NF^XP=W!)yOC1c6%4YOiu%NDNL{A&k4k=oL^fp>H5MA}6P$Cztl=yeCYY{N z7ZP($4ke@{hxITmYqx2(`5(U4cXkhqZG>9|n`Rk`Q&)}x`1@#Y%T{GPgS zY`1c*#6SC~gGd&Hp1bLDe@*+V2mNZ~@D5;TP}N=iwDfqggUo@GZ|2h!Pz=sP_thUd z4c^~khC&Q+`egb~5`=5na9w(fc5;9MXWbSNRlQTG57HAc_S;nstdFk#gLr<` zar6t$c60ReGJ>@uMU5F+ma;gO&mPs4zb-9`4wY)w)kceZ#YG(>aw@tE-Op(#n#b+9@jm_zgN_LB?sI81lJjK z3wVZo79X4r3%ImZi-<$>(H^Yj55{+8T|)k8dj&JcB^>@N$pAnFMlJ*5K!>^XsPY&X zXa>hgQVb`@s>-c`P)vs{agq?5_DN#F0J_laY))6wbqOQyk2FF_YW{%y&1QdoqIx6G^gG`#)uiIoMrRE@}U z(znEyigt0;>CqIK)h_E7W*Et|z=6{&g%eWf=@}d{Z!EPa;Qc%c(0!FvKsLTvFX6+?p)~FMWR-gtdk=i4rg258#b->gg z+F*!Kicj&oK0gOB=D!_K2}e)z{QTjR@?rLyLl@k^+cKAEN!PV?Yr8KhIEh5lvia1q zNnEgDJYhcQfKneNd)PtW|j$3++ETnWM=PXf4nHA;aBT&;|U zey+^qf+r_tx4cWe4Uu)l=$o4>Q6}-O79eapIefj>yYG<*tXJ`XzlzZviMF51+6dl~85 z>u^8IHnF8_wLD$98XHfr-vghZ|PXiORd3jvCA?M4J__YYKP^~k=GlrQJrxxl$46BfbsCK z!6`GkT6h#8qq43$R<-l-)`$SB^6(N{OI>u-!tc=wP8n9cX!!<1f2mW-5Ye4bczNKJ z+-f#LZ{)mb8XSH4aQ&Hz2kXLN2FS53j2nfS(FRDTfe!>^Uy8f1nqoP&v?Iyds=|5} z=5f+@d3NNGI&a7~5^J#JNQfCmBMfImeN9jPdkZ(?`{9v*sq;FA9#)k*lRj6o!<#3N zJSN$&A;8u3wxHb(gv+;x=MgpBn-*0%K02?B+#rIbX4!CAY-7w!AV^qC`O8{$CNmq4 zDHn{GU(V}0@bqe-z;Kw9CxNr6ZCe}wbzgL`_y+-I`{#`aO(#ni>vBIs?oAC!-Xfa1 z2cw*1m@!MUr;=m}5M>v#C<;1DoK2Mk9FFl>*u&Z_Cms-bRHX`SnoJ^EKuM!p~ z4$Jo|8crdw857|nnEC+&VcRCulb)hwYs7H4s}4w7+&p~4ZQ#^1^{zJMxl(Y?#@wt7 zJ0fj=TKYgF{YG6o6J|6aR?m+R*vsZaP#@+A{u#@H89q4R`R_7`+ftgvF++J@Vg14L zZ$$&xt%Gw_T&OshQ2PyrS?vBlfJlQfcfw@b_2P>j1y!cJu;&@NwEl3yg$&0eY0>q$ zDGjYAq$;&onQwHkwd3rhlagX2@^{u`U2>l0f=O|S+r~q7%iivmQ}!aG+BoIK74MwX zQ*PVcz@KgIy*_VZfm;J{m1JUIZD=WB^3?_q$H4K`ppGATRM7f0y4F1Y_8Vyg*g{R} zQ>h308Jmr1%~lMZQF&*`3(8-HvUfh{g(jbNEPZ=FB&XaFAoyL_O%&FqQKrj$K&Nht$+U3Hs@xoMXDo<@|fZPRd%l8eloiU|X7DhbJ|)npYT zG7%fO!mf4$>?$G@UOI_v1P4~aVDu`Onm|0|@sb6ZCvQ;^6mSuRC&AUn!16!HACU5c zG?r7sLuEy$LP;hUo@^@0hG@$L*JC90KbPI2v8|c>`F*O7i>Sh$Xy1}nDvx{4!NA80 zyGaB<`m%JGNALSd(Apz}3YayH(i(iy6B{ueIldywIy%BDl_Y!#%mI@g@}>-&*?Z%P z(pY-Nyw#wqd@)CpUOwHb;`>?`rEYyqdGRR(C+mjn_lVf!uA828-V>`2s(UxbkQaFq z^JtbN`y!1kzcLB<8y6b6It|Kr1Oe=^PWK>77dEO_*u2>ea_0Y!3SpYxNJT+1|Kh9&#U8xU%1$7Je0!& zl>UtxkazJ-C+mf0KwO4C3Z0_IWA-EJm{7Ss`0C0GybvD`sM4O$8uU}2XHXQk# z$)QDJj*$>pV@%_;#cnWP7f@N zh5@eNUb`&?F$NV~$8%YS89ku|ap4~PD`Hn0OFc}JI30T*E9jSNOvj8H4jrpRV9YQX z=Mx#6AMS4sEy@`Ofq<+8I;^caMb9&_0WCfEUoJq$q6f&mX4cv2--z-+N!HqVRh2wt!OB#sL@MA+KhxP&j)9MdKmE@uXu6Ds6bsjF+ru%Q zpxMSCRn~-m3H0hqYTgze_ z)*%hG)0Tu06gQd}I1Gh9VE%x;q3|U-9JQMvEhVYTseH4fp!PM z~Jz;!qOx^>C=>>UOLY&v@;U*VSwP8G3j@-oC24Jk5B~fFGNyD@a$F? z%_Gv@XTmzl$XlxkU9c_&efs;$NIK8$PW=ZKxK1MV0|;&5JDl}R-X<)x)iQ%*_!42U zVU!x|UP3OuW-ov`+f#9Ie8{+!h!DZK_7abuziQHBh!5F-m^hNeGOdaI3`U)p?_Pa0W-q|-CisA8EOaNW z3{7Ya5z)CVk<4AB?WudgFuIe-v6Qt*$g0+@PaTG+z?K{%RlbEI$r&8{^!uZHELr}@ z4F*MA@U%Q%xgeWYNCL?O6qsbb&sdYbLyzDLPoLLnW7i>#_qrPkHmd2CLhgp>Qc$Vr ze&|h^KfJY{yq{MYxMT>kl?ka@h=aBs_P(Zt+3&I;qW;}OAU7ad zCqDDsgc@5p%S$~OEMF-fb)5nhN*y>w!ADnh1OeBJ$HBF7us!Y);!$Vner9HEpZsAX zGwR8z0UIACt>)$CwOl}@r^9C6+S3jF-o_qIrF@D`bq=0s9MwNWyGLSZXFJ2<=X0&7 zXSog&jOHKUFH+Z=podOpFY`%TapZJ>cHv;Doh7jTq0QpA-8&OZlPgx@qiUKLds~9K z8rYXsj*rs@V{5OnZMAfuL@BL|8S=10VSEzaFqI>Fb#nHRBd zUs<~)z?(`(mq=XHFkTf7s)6yjyLg)t*!hGcdK~&^^+T#(-wRryL!*2S*d=+k-#sW0 z;h)ds^4eX7>P$NlKD!i-$b*4FJDHw{e;F%4kyFJnbYgf563+}Vverga4{EC!;7_+I(ny$ zREm2Y5YycYqJPIA9E~3#Lb(mbaRgp&Lk|%YDf39S(iL`%0;%qzluXx!bu%K^*(3=+ zmpXEIgH|?HnlBs_vEl=8JJ70-l(fz}I6U2Tcw;@8+%jT#EHs@NCf7LkauEApVcKiY@u{2^%ndl2e^0_7L5#7Bu`^oONCYVj>3Hq0(RjctMuqsVhR5Zo-TuRdUY z8}dN;mA5%@9p5P$fCMFpBymd`XD4#KJwF{h5|G){KM9#50dNXs9sOy4<>xXeWRF#R zzF@}FT7T*UtXXTPkYiv-u-dG{J|m9lM&+LM|rN`7(62K{0K&6BSUOwHUC zN+j-M-yLJTfPxNPxb~IKf}gYn)YWx7+@^=8HnL9}Z>|UY3}D$>pl+;`69@fl zL)nAOMjtp$nZhBrQ?Y&(AO5nHZN|bRIVRbzFNr5@Es0B#XW=oGqMxc>awhew7j=cb znd_^oz=;b3Edl3C3J-w&W;Y)VUHbv4D&VO+NaJugBttNR2K7t@bwN>p5dDTK)C1v> zK;Y>e5yx!Vd<6N^jhD*>XXzyxisJ+}ll%M4JhGA5Q|<+e_>V&Ps$SgI%r5!*_>Xfu ziMTl5`iTT3CAmi+Iyqs_$UX&r->-3tgmALk9}vr*iM&aq;VPL}UyY(1yWKq~6WhBq ztbYb5wKC6>Yh4v2Xeau4PtBxZl!k`B;*T!ET;>9~f^Uwb_FhJ0?|aiNL1-%k;&v;w z&YwwTUF{3|@4C6)M{i>BtfSPwVXLd@HT`~NQ`g8MK&g?i{!S~1tjsUdeq^mLt31U- zVggRcSNaXOkX!y>)e3=_n7rhtRq<=Ph$t%g@7fx3`#0d$f$$DiIuu1U-`>?2)+Hm>M)odQtn!`Y1%#CnhMA(q)~sZ>0f~N^p_Xl_y$>n8F(}URL%&!7-h9*#W?Zd&TR0U%KJ|u`Z z_a$U6S3ss4^42CTWuUR^kO_PjE_?+cdGN*8cU(>Y3Ro(iyIfE;a_{t&!B~jDqn>{= z@2X6v?eLr>VXU1` zGPqE3vB}*7wo&90j99r(_lwnhBr60?C0-bf64N2$LK63W*1ezl<&XZ!SMsd#p-Mlg z8TZDq)WiR20mS&@pkf{UN^0qSJd4A7;UqSwoCbC8-@9yGEhFu58l<4jF9*zj*Vp^< zb@fq@pAZ=kQVkq+HvGD#*DU~0o2hfw_s;p~(+?}c*SrxIcMKN7s`lAiSXhJ6Y>EjA zOg0(xa%Ui<3SpE@$QUMJ86EUe5jUA58G2C4v5zy-k(U>FY#D!)Q8JX5){&#N=@3oY~Rs_QG9>2n|u zKu}G0(JMbZTo>S&wRzrp2iY?|7pohbb^F!5z7D~tMHJ%p2TD^PS>}GK^=va*B-a>H zBd^=n!dtO03?`@X=2EWGOm<#>fiI1F!v=F4OsNv|rl?svuuudA8Fh9Yp{mYpdIYv4 zwXX7=ECM4`qxhFYDta9(2dpu71~{tF--QQ|ZY@=hVY>?$`Qt^OY zORfn`Zv+Sw?i%^=6N-cq#S%eNC*o{^E3X|6b%@w_ac{2|DJCmB3|k~y4++^?CiXW~ zoG&B>am*UJh`F(tro%1znA(I!aSQM;pydJKmUv-Zmk(>wtWQ6TdO*XD{LTD z2#}#IGTBxY+T{JEH>{c%v1gC;_WCnNLQG$MC_YUJJmcFOQQX%qYtFN!jSV3N2l1da z$e>79TU<@hCrn@X4q~gq$Fi6A7~G0n062> zl7AUZ?RTRKeP5Y}qU=O2w-dj=hT{DFV%~>v7xY_hn0rp7SLaca#UL<4&rQF#V#cl2 zFlvAr|0eiey`PijxXQt}23#Ng2I9s6bFYz#vwbzHWp&h5DBiobx*{$fvOcJ!j{K@@ zyy3Hc#XDpL=ExLC%5O&i>;iDKi%s~ASLjKEEkPXXqFaO-zJ$6K`hR^%C(N*Okgs6N zi4mL~8DVYH5i3M)5c7l9O;l;gZYTxLf-ts)wGk7`1}np2YSVdB=!Uw*L&kWthgGf~ z=zHaB#LD{ba&gTphbZA7X;AU!ZxJN+IOz^ak>tHlC@uKDRGSnX22=dh8!$zm(b6BtMZ@6Iiw#yK#awQ; z0ks@{=@M#?t?mlV1ItR9>(iWEwXw8fbtc@?3IkrAqh*J``G6X!J^_5>Jm@^0w zI(=Inh?Ek7$yB$Lqf0sjA*TF-UCM{2No9?*^5sX!feFieyvD&Su0@(*8QIrW(l}}9 z+=?Z)gEZLme@^QEF#Po0Z6=nh7pkxkM!>S(q~rPKPDeaSb=itBSh`rE}kh@%Hb$#-MY0mb?H9LDlErhzz}Z<&x4b2oca zfB~Ca7j>ZpDlbJzobsi|5uf*h$2Q`h*NfSX(n^QP+wL?PMicLLa%DIV{?n@oFo2Q4 zTF}Q1aBgYAA`b^+{BNPJmvQ<5ilfAZRG#s5D_O!R%`dLE{TEmat@UgW(|?Vp@5Fc3 zr`_bRSy#mUQy_KStR}A76#cCyGN7~LOBj3J)cXS3ADxch$kPtcgNi6GuaTxTWkz5q zQ)*#&;Ei~_#-=m)MNP+xGFpXqvqOdQ4DrcT-EM9ED$u~3yvAtJYxKQ1@LD8YkW&* zN_}&C7X-ajnjHvw;i2bIZZ=426Q{Z=SLp!<0sS z#ogI9hTWoldA+DuDpbknTlA9dVYf?ld)xVd`?db3Z@SeXy4`-YyX^Yu$`&M;%@%j& z-0&<;g;Y$WQ-7aD%#!~$NY|(uB{vF5Hj!)3c329|=C}*$`+2%GFY}UsF@9hgMz=|< zIi=Fta0AwRpoR!Pny)X~jc%sEh7Vxhiqd&+sqrdClP=2Wmo3q32WvvBH9adh%{zZy z$20Wf%4+LBkA={)Tcu-9rK7jt9s!B1LB6}V@&Tq1;8C6ZE$KiOH?#c=)vwIuM@9EC z>32;Sa1$r}Tz0No9o=NsYI*D9q_1@zspX2nq+yyUs0{NXeb8!A zn~RXV^s1~9{#1Mmp=n7^5PJM!NEXmUj0KabBkQ;c2!~*vow#X=1HR!{GsS1eveCJh zmD+`pZZ*fqhoMQ?GB@SqR; zqIg~!{E9(bnkC+G-fCQb@DIddnFHTkvt3@ti`59vhPK2hf zoaXHfy3uO;UntemLz5JmYi%*;hpl5+4@abm{_EThi<&5I?B0 zzTDu`dGsoJm*bfSvOZ~NWlG_w0F`cen;k72%~0{ooHXP2T7JLwlPF4ZvoRS?b(SnF zd%#d_E`^w%=$e<35V56pq=(jLW&xH3i(e0Xt*oG@zG6i)4(G z%uoy1zbEa|i~wDax_G_|?FnCT+7l`ol&i1(=Dbn1W_u;4k_qur3Fn`Ho41{_63w*BZS2Z1agmIls{KLU5Z+0z$lLloOa}4kkw8Oe6Cc&s>hEfkJWO$O>E5Ty9Xs!bnx4ajZ95B}^C z|3_0)C~2B?w}B;>_0+M7^N`vD?mu-3z8bs&UnDlRH#Qdut@6T%KUSS#WqkEn_2ucTT zrXb!WW0dE~r#6Rs2e$l@&9D78=X;P-OFmdo!_F@$hgw;7$K{t}%-vsFED*A@DoL*= zL+1*maHl!~e$Jh=dW zL117`XU#aKs!2&|hIkz2rbhzoH&zXzncB^5$FZ3lh&^bw=nRAATj&0QHg;B!TnuM# zzXm+crHLoWZF@Oe%1-rd*tF1f$4pH<0tcb3(eCS)-3}iCR}-9MYhhCb7EFl_s(iaI zfm<`1sX7Gwk2FT%<)=&logCb!!K$)^V$IFF3p^fZb9}VtImQbJSvN_OgszfK1Ny6H z#x&ZD(+}~Oc$nk5nBKa6mC#*>q^C_^CEq^un-bPwLg%56Z?Iw;Ir39b8ji(309_We zK8l8ULi?!)&X=MkgQ(}m5<_15Fsd0C*zEs3mE0n!F%jh!q+VbvcqMro)c5WsBPZf3(ZaWoNT1LHw?kr}Xf-H6c66bQQt-q6`Oh6Wj@vgdMDSBG?Ig@X);#9}kTvdHY~6w~_O|J`cW{GcJ?2;56HCJ{$yeN&s#*fVUTT#18E z#pzhN@>Gu0Y3uPx7p8Is9FPBc?5Ny9Ws{W*(;IMvAVJ1SM0IQ~818C9@%K4;k>#pb)VJi#F3b8e&X7K^u9UYS%B zF^y4j-00M^C|Da(oy?UgmCU$+B8;ySbd~!3COoq{SGUQMe$=Z8#qT?#m7ZRG z5y@iXIttq6W38{%p66jouv$l?7H#Ms3ZUF=?8**5-s4ja<)Sh>v}f4r#W?pW6WzfJ zJMz~358AnWN#V3mAXdZFh}zM|S2mSZJAb25%%l=3(!nB${@r3p51il<)>bJ0{GlcX zqS|IsNs*P^9ZCaO65^*enLf>7wV#dpo_iUWWbtnJyw9|F==`Df^b;z21v(oUF~m^!jQXTlu3qp=Gzr*384T{fFZ-uvASM-5D9C$m``K1AqD z=Rt|SC>@wcz_3n`hkbmbIy!ouy_jpOx@#k^so@r+bHpo=a5DHzdDQ%hco(M*qUI>y zOH@;plroKt)4#X|8t9*5O4Cyk(*bemR){-RhAxrWfZSIjjAz7Te0Zxew-fqrAa@?8-KCc-K;)Yz~Sr4-M(OZe8pQNsJ)l13{?-i#_+iz0(BuY995HJ&L ziNjb9R_?f{Nfd}d-5T>(YqW|M;#w1n57>7}qaWY!J7KD3{DSn4U&W(X$q|styZ{KM z%%f0-yzlv$F;s8HI=DFB({1V?;SQD5`q8~gfdleXhu-jK($~phcdaZ$MLb!eXeH$i zG**|go9|yhjS8?CAQq49I-21i70J;Irv;v;;Cfw$nM`}A%2+dF;nB8+r+Mk*LlLz} zTREA8=Y?G|eWeI_O_=BpI3?xGfxx0DR)mSuOMg#;{)A?|*;% zVe)ufXl34JgL%U^W=mBPf?5UiTP%f_3<+0g?=GW%%upn5fYitJ4>2d<6g3c3i9Kr z!k`=za|ZzD$R8IMm(XBHF-~|?(%)Q1i4?Tpx#5dn_pH2beE@Gk%98uil*xD!|9}?S z789{LyhYE4nB6N3V|&I*$%6So->i*BCIfU1%z1}ubtRPrP~>^h6G*0GeiFNfIN`sD zOBDV3+2HMZ1Ne`)a7;`rujli~)(5&k5;jDP{72XucnR=v z;T!FjklQ!Nup#?N8T=-M(RNN_p&EhvH1M-LIotA*R+T+7?Qa8--H0^F<{{1Hldp{N z0~IGwgIv-RLh@bzo=wC z$Ko?jXm9Q~{WZ!W{nR`Z>E*2^s1sHSFpur&u!770K0q+I!VVOi7O> zlSQyF*4#gO-|jZ4NairU&&4tlk_zV&nZ7g3;l~Lqpgu9P^;-%SNml1<&?@ir2TgLD zdqzfASI*u#;E8+w&~fV);LaGQRt%You0fta*-JPOP>TA}{X;bPTj$rPSx$18;ue{k zEb|G{GaH0g-O0&aPKghnez9gF{ruS#8afrq%Q70e?X@^a0@OrG2 zN8fY#z*1l)@RN&VDF=P4Cu~_H1MMF&<=)@5u-&(k|cyMZx zX(kDqYqyFAAvMJTgeXpoFtUMgL*MGp&g_PYM1jdY$F{<6LC@FAWNmQDruBu25Z=9D zn#@so$z?047_nKG#1fMik3ZIt9wG5DtoOg59;BVw4x0}clGQ`-gdSPBYNeGnh{8=t zw(dqyzwoNIBfBx(Uy2aFm=glqCA)0Q%E;gI{o|^Vk5F6v*j#$#Jd_d`tz?ze{D|0N zwbSs%4-L}gCxohmz;dPPa8UX0O|$OKO!%>mUqw`cotkqq2zV@xP@Ll^dUZbfuIzd2 zv*TXY63P{1FNiM(JdozUxXU36RX!d~q@Mi(?@-CxJHL+XPyQ6tIsC5ybPQX4B*-w~ zE1L0q_ww18up`UKbH{43F-%Y*f$dlq{1FZxU0MF`)SuQu4$kQYRU)%cl{as`W=`LB zL&&~o6M`fnhQrk=1k*S@U<-$LoK_2LJ$wU1c9+t6^?@m!T+*+*)5=wQ$R9CG zjLH5Xw>O3T_nvH2LunwXmc6 z=V<9O;D3K7QmX&-LMK8){ZXrm9_(aiX{wE*AQ0M^xvAJ@k|%ZqTGf^T?wzWaTOJ&Z zj2DdX&27D8`?)7c`DSdU1E^-E{GN2Kx9qP3x^i$l${||SOzZg{#RjU>>n~$x9aO6W z!nSao^=NR!;Je1}G?2fh@+-7(e`T#%IMc#GbJrptQ%hIwBR|%IUY2#+yw5V2=+Qa| z=$QyIRuocc{OEl)GUC5{O|Q3YT+0y;oP`zj4(5-`&ASQjn81K~$9xsex73k#a%3?? zunJdk-Eigd<*Z8kyxa*D4Y)z31!OTotcG)o)`ak=Xf+DSdL^)o$!uW(rUZ(_$kriB zT^8D%W}m)Mm*SHVrh8YE>YPI&EUDm2>NbqFW~mw0F#UMdM1CcOu&ZAxuNIG?k)uuc zU&|4E#*{`N(eRaAz~-8qTfnCmfBM$nyooe4WawuhgtP|3{D)xupetQ-_cKcPX!Fp*RpKxA4F?;~Wt zfDn3pD5OKC^tZLexv^g_)l>-47gHGqLlh=nccaC^+vOYpNwJ(eC2zGQI`;O(D6B0l z3GcSE6ZSic0Q1L!(U|pm(t9A%ERrFc%V|=s-YE|l&Au9n;{_Xww@{~JxVjrFqb*b= zpA;ZGyh2h?V{smeQ~p0KfK5Y6bY5(PEp%04<{SM!g&4a)nk|$sW$%}xpmsn_lJ&s0 zjpl)<{7Pp;?fI-4OU5BL^Iee}Be=Sau=P7Fj77(F>Dsq$1y`Sjn4@aJ+WLMk_j&Nb zGK6i5EepfJu1J%#*A(ZEsdbHyaOeR9TF#TgwifY>LtI|r=n1&&bMBYJ*qy2u(v}DL zEalD&4F*bEPGS&YnH@jqcI1Uv!0(Xt-dwcaN9(B0#K{%oU@3(#v5BM!f#HYs-u#!)9Yg3dxfAyj^`1=4;gC-PHVrtWe4;s(;xz&ZWTy={QZ)^c~x)coZC!x-^z(19x)FjtSZ`EKUq*! zmn!a%LpuX>k)5g=HcSMtUsV>GUEs^BUQqw@$ z#j!qM9BeA4wcse|>>juegM#ryNN171O`H4^lpGbE84}C(?$?BT z_i*XQeO>G%tuNMgDi~SZ5dCDjJ2GJneVy6yJVIN*jd+((6_XgqxsQ5$`Oc2 zi$tVcsE#eNF*w!Y&OnhAg_-5K^6ME38S54Bt*o*lEpzZ%lK5D6bRb(C=q!c$g(R1Q zR)#^}nw6j~i;UvJ84v%GfH87gH0W}R?HQ9D0n36E@;yN)WAx0rq0WKQo()NW+RDEeQbLzu9GBV8Y1@d8$Hyoqsw1s$kcCI zgBop1La4UIJQWj^R)^s=Par8aY~B81t+}IH9zl>ma}@;S)g9kojNZRz z_Ttpg(DU$;Zf1es85l5!zDNQSeHBLAm{#xMEVm>k*?nXt*7Fs$kp(hltKCyK|ADXg zBr^L?(BjF}_rP6{sB%_O;MW08k6VvG|9HX(=ZDegbRpeiZAJw&U{ei;EfQ1%=EW& zq_To7gg2c*8z&5TQo&CptFM2x?eSMRJS8U1F1xO_o4@0LUg(6gt;0=fi?hNU;9JC) zPMb6z4`0y^1v;BbBYht;>x{3^rL|P5s$_9KzyNuodRQZUvu(NjN-Kz;X#|%r*3dW< znC>yt8uH$lvxGm-6q$z(!`QJpPF${)Mh^DBNnU@{GF2@b!&< zu0OUNO;aCb*k9s)qq9jmVRVZ0WPr&QRhx0>HsTTCHk;5^us51PsfsZwSySxu(Vf@G zNJD5b^qzP@Df%pqw3ec96c{bL3OKJ^-!+SgR8@RB0b6GwLjYF&DtL>Nfj&$d&XgN>zEgAlLae)^6&>cN_Ni+-sh*E!YN3eYS6U zeEU^4AuMf$r(BRuLJ8B)^?3%WZgcKBC3KqEYR<99pO+D2al2^d@53E>$_0$WMV3lAPqDK0&Vd?gN0Lwr$zjl)zB1$27 zhcp)Y8J34RgF}ita5w#ezyY7^Ou8x7{kMm((hr^5ytYP2a#<<4%7I|z{ zwL_kh*CP2*Qc`U_f&2jZMnylO_^jl8N%uNw>f$^Z3Og0AtvwoYZ#l#rs9_P$Sptb^ zGque8Q_5pf(z-o!ZGtj%d`L9+ z8glp!a)oI{2h5Sh-kyI0(40_nuATm!z@v;k6*bSlk^#%~MprlKz5c&nzJMqAaFxUE z)Rhtb`A43Ghp?ej>2i}nE~ATqbjQ&lF^QA0F{)rRCEIr#UBCyv^Y&1!QvmAPqo1pN zJA(reFe5)u#q6Wzx}Ov^##0+4N%Mkq2M=3QaG7N{&jJzq27m$$2eoJ8ob7Ri-AQ#* z)koC<0vuHC-OIZr-SWILqk;i{*EqL2!oT?4Z-&IW-)e4D)V8ylTGbB2p24-vY51-| z&tO{C&=YX)z4|wx<=!iuZ-qQag8jjK?~H#rEf=`(|6d={;tG;Ph{U z_cZ^`?xcow4u^($sID*cbu+l__&kx}Ti*L`j4Do2;_nodMV{;6L9|yFMLHMaJhayD zJC7wHGA8k))GB}gM$^3L@;a<)JM+H9`lPZRuBO2EgP(di>@nec*=nY;l&BY#ZS0$p zthGHhWjyrQMLhJ_Wx0tx@9}0vSFxpwN$L6gN||ReA8YIOY;ZJhcpsY@IPZdNRxU>+ z-_;)S{Py(n?N~5csnGCb-q=sND!(&NZq^>xwx)Cj>;srowF`_EiGO5@5QspG90kSf z$XT#YR88_@N;E~b=!0TKlHA7MIv?UifQp^cWHU<+y8u#V-Ig`-z3tDJ^J_o3H(IY$ zepSH1rraBQvUwen+{1b-4ztK!6L3Mm1m_O~G|Iky$MFRer44z*`gVac*AMPzdox3T zv99Y#NBV3>MU4-B`jyx9I)nk(1=^?aP*-D{f?8Cuc`)Vtn29PL+{*QaVe`c!=4~#pq;? zDa{2s{*e?rT*8WV$JH%AAjfy(wo1U=(l9ucR z?)ZAP#}!r*CZ)CPg&g8NkCF>p-ZhD8Drroc4k|!5Ekop0)(nO#EzB!mRMj|rZ8QTO zhy8=s+V!0xJU?Q0>L?wDLRs0^+?Mx>G*&BXhm5TSs@kEf`2A^mR<-pFm76dr%bJuu z-~GTb+}JJYy)cvV>gKFE@AVjOfRq6K-Vff3x7~SY#=}hzebMx5O*6?m6%cpLaZw+m)pM(+tA0mx0#tBEJWn5x#5n$gKW*RH) zlV=_`aT|j4$aZ}3@;1J4ngI1S2#^jbQgkH@BE^wz?9|YrS)ulhgP?tO)r^6)g?@$$ zYhzTk$MKc^-hTve&@=zT6$miSG<9m1YVf3NGR~}v=J^{tWso>j%%t2cN(wB5#hSeL zc`U;&7Q>y#hBHYsb%fLG%^E+Cf>?#pdTzjUFpx51&{X8aGyZA3Hk?P0GIS0P)SenT z^)h|aRYC84(j}H9N(}636|3XF_D8ScYo|6aDrzkB%+0Y)6wvN+d0@G>nPpiCW}!fV z<}W-n-%qco$U9X)_(9J!fc3I+oj_<=J1h<|Y>%rjM-DIK;fssXaVKmQym0{Xs!h98 zH~fn#M}e{OK&w%Y?_fm9c;?(DPF>xNz%0)V@%IA6>ux?{H9Z3=_7qy%E-)_05=Cr8 z#-$5;;5?5@>mtaWYhsZY{(iX!>pa_R(0%O%YKMeM3X;ISAf*&j#US4Dv!NmqpD%No zF3kg8GnRS|2mQbRY{vjK7e54ul7@0JOxV}wC8rU*ljaSt$Jh_(h+i)rPB9LYnB&nG z*6{NWJ%97x?GcLN;x8KgxjBxUvtLY0N+9>zPr4!=?>%<*C{6&gvCv%a$2lsX+0P?D zVtQ<@o9AGfmxx?A1qxF}XZsqSzp#a$f9Qp|-%-`gB+5GkIjZ$^c8Q$WJK)e_KG$Qr z&7@3$CXYyvDA~_2-N$M^Q}$$ugDs?(NI?Ga9rh2&b!dgqjoUf|HC)L>3+p5BCi(YX za&L)cJqQ#uTnKfJJSWpwxXrX(=o@;cT#RwVErnQI?A4*3a!nr!jiv=^MoBa9w!2nv zWT_Wfku5MxMyjUFG6StN6CVh)OKoS2Vg(v2>#UQrC>GYzJxrcu8*8CFm(~WUduVRA zi^?=#n%8R+Y#=up4hq)UKs;%aW`sp?&YRbKJv=@=j^QL#UU2{^9nA-$6$PXGOxJe`O2+kk0AjytEkHH~!gdL!!A7HU z;+Kh;K)F3Ktx#Kw#eRm>?J4fLrMshdd)#x&B95%gXIv@;^fGCjL-+CcE9>~`%j?a1 zc0~6-zdpjr!-IGTTQdKiR4ydUmIF{psBWga_wq2usjIO;k2H>=L(#F-K=i*=pi14a zmmR3EA<_e3E0f7{^Kg%{6AjX zamF*TtQ~Tru)bS{{id=GMNLJSf+d~wnAnUTz|TMQ0=CAK;7V~Xd?qUuJdp*kxmyP2 znokkF)_C}-Yr%|up4?lO?)-)?%`TN%DLnM}MSSDbMo1ENCN-7^CNdgbx2KkMRNx`I z3+nJGa&!5XX=S;w6#Z;38wBwxPHhMF1Hql6CUgx}sfp+Ko*<>0We-R(OeAwOUp*#8 zb3J=Y?@m0PvQ)zMB?d`)TNv`~NV>CuZjd4+j#yQdug}{jY zU#MJ%u&=E1IJ}tSj$^}pD)4pCHveU$) z)pPD@v`DR+dxKb2?N!te)j*IPdr>7ee=qI5pta)Wn)F;WQ~*f50-RGJ4y-N3P)c>j z1`<05RyG%61AP47 zYX?9|!_L5evtFXG^`n`-iBhjERkhs+#kRWS&@S zL*bqLQgy>KmKwi`6g(kv@ca+WcAO2@A=W5)Qn6Hwr{qSzPG#cR@dii4+ossnf zA0S>phnISI_gjwx9u0Z1(1m3V)Lap%Y8#};m6gw8IVunk7OLQIr(o4BM-`?)%|QTF z#Vnj`^ypSSNkAp$u|`MvHC=B6P326DARZ-AWlc$CKhp`zg07XGjmD*Q?f|MZoF%qd zWgYtY!2kz&ZqhtJ9#0cnp@k&Lp><@|{N4vo;%#@Yw3w}3o|I@XW{82+eJ$-EBL!zB zB}p42lTO2EJrgX#$HQ6Vk!4K)q_weG0|30`jumRmBy7Z_3TJo@ZH!9t{h8EkV9bWX zY2y~(q;}IaOTwqa{DY6|xMRybtPFG13GlYiBLHPw)F^B6mD5UvgjPlh6bg0TXoFvW z_*IMxs$}g>ZS+JztQsjr{&d!YI}AG%rH!63paezUD={m$`lG0VBkX5MP$0iNVJ^b6_vx*xN0P!#Kv~GzF8nMk)2ank6s2$rk38L zqUZ16l0@X-Ov*zGIkv~tEqyet@!x;s8BB_ryW4}Alsa~hdkPOd zezEgjsiPu;kf8V94!T0H#R>oVPT2t0WXRfe9_VKVruaXl<3Mt6Pp0ne_m;jh@0-E*g^fuA%#}nWfRwGo>{Cx%!jFB|tuu*4 zRXYrNCYaERY=q_ta1|qhaj<6(luX!^Iq*8%=4HmB*lO!r8qusj!lCwx7?WqyUy0*QgLTC^Qr6z##D0 zw>d#t2qkv!e)CHK@Vq7|_|LTo@dPzz=mwO8-Pyh;VH#A`G!z0Str$Swo%jxx*0ns` zqWw)LdoX#?9oD3;d34!UC>>>U;p-JbA{o1>)5|MbUTz2eZjWGaFCbAY*n%xnx^J|o z+`i`Z!ovQ-H&^kipM4bo&6R0s_hj52)RR&yc=j9O4ZRk zE;yI2c>r9n#MKvrpwCJ6=rA^<49^H~Cf zH5zX+@U+){;lR(-6FB9;;6`g7ZWLz0Ov*sm0F*K~y4(vzDA^XR^SI?uA1lM&oc!E} zSX3z$Y``doqw9a}!WO>x-0IAA0K~KukW%721ybY8WvFv8&^m|vZ(YXWg&su7CPhsp z&)ZXsoM{c06b-$fRU86{tWXcwx{kJYiY)7TMq`vVt5dZ9URobxx2VIsmcp84o4bqa z6FmIXm6^RdHW4EwA*sd?@;{Olx6a!1`7_8|JBoElW$Htd*EC+O8GOtX^<(fH$2gCm z4vM*KQWWw;*dlv04}(PRjoCt`{7rjJdQZS4*?H(lKJaYm)GBPD`%wR2b z96P8drFa0P^y0RJ8@z!VLk*1p;7zwI;PxZUs%;mIT`2{@`-uT@a3*EC$)yIn^tqS` z%7nRPISD6_lBye-;VOOd`TxZay&1h+2O!&M6)N|d<>cGr3ddIl7)`4nAx%5xQ4q@& zIKI}h@&JQO8CP`3Gq2^vrd61?05Iqo)Q&odRUEKV^4Ux#|JC1rCyp$|jXn5HI*%#K z7k4HV2Ldjz4JA>zv@xb?4BHA!*~}QAM9_Kinejf#D*@W%g}IPcAcb+P6eVAagA70P z?YD&WEx-T5#clk?pPX)*_j|y#7-azPnJ2H{+!|N2-QQJY2<|m94qTRo z8H&@BTur6>xS2f*i04FT(8L9yE|8Lf2#?*O#$qqu6C9YiL^W$w za8S8-FEcaWmAzSGxssf&2>I*Tg$-vXGK2WQvD7-VK9(J!sTFf&>-J!1d> zAOJ~3K~xwOHAFT_02BpQm9-%_1?ZVwTpMGUo7gSgr3p3pUwY>Qt5JDG=Bw`x7JvJT z=TSRuOy|Uiy;-~iz=e=voy|BK&y7CV!gz#~O_k>S7Foz__5rfSyF+;$xa7Mbl6 z!&PgFLp&7kkx6X9d$R-fWm1ZKP!hYnm~{Zyqb~8J_S%E?%LB;0nY9tXT2DYIz)VU+#!-k*X22qeBja^utg*9P|p8`?hevi2kq zRhZc?fEc?wM;(tly|qJZAAc5`MF3}4Czup<@b?C|`zx;l3p%_~Kuq?rcHF7_9z~Yq z`z7}PKm~P}fA?VB-&Qg)sC2VLa zTsP-P3d(6EOiIp80BG%TVRg*Rzj%$cb@<)~j^R7*Jrvf^H0jt(Af=E%$dlY~K~@0R zbV;`3)P=rbHezh-A~s9&*qcV1@Gjo{mSZ@++(XS46_uk0pp^=2LCOEqIn!lt0s#Ho z;K))=h3VYDD21hkoZi2uxZ#&S{z~#pqpgw1wQFZXF-m+OuWU?k>gs6LT90FBDaueW zTL*A6m4ACrOu@$c=R8?Dse4a3zET889$xH)I6@l9>`W^v(ekwrp+fQ|6|OlYW=Wx5 z&XKU!tM2Ab^zNI*L+2S#wVo81it;S4M5*H&^OW8-t!26nIyZ!IYn{j6de2>W_ghau z`i%UIci$ZftHO}qWLuZrX@%v547VK}piT;g4hThIIeR59eixwGUNiz2hkIlb^sX_($kGa5e0~)Vef1*A z`4aXUZHCQ&$a4-QY{!0PFfJ{6nKb!vjcU9Mg?MmC6;2xGjEfrmTwye&u46t)Vhji3 z^o}DSHLWcBtI%AoSU#P84}id2l0ahEBevVor5xk34zeUk3c7|I$euyNY32AZATw#} zV}m2|fQg5XRPLr_Z1O^fQKbEgpP{pK4Fujs`n<@!<(U`EcGOy7XIux?r}x0tn1Iur zag8TW-N5zjBFqijT1bz-x*j_bI*+4EeQfMj5wOuZcn?0Q!YqH1qe~hrQ+3|M*leItPgPDRMmO zAn0b217j2}tc~#LCoe;5g?mpfb_V6%1J03Lv)9uH>MkDdJz6OMEFW~uWY%bn|Lymm zqy1LV8eSeAT<-nPKk_Wrw+dMA@z~32_|(@f-_-T>bA#My{M>Ip6B4xa`hNB&9>gFg zR?(v`tl{ftHX6p}B`3^T04bZmRZ&^)EZ_WjRXYs(1|w$YM!O9N`0NW8Wva=1758Zvs9bR2>}}|Wd?g%r--Dq=I5mqCD3~f zNyMaqj}2?e1^?!Gg^4u&2>0ZrVCyjKXT;p`9^+}#_mKW1`a(%(QyJt%h>&>9+-n-A zSqF_-w&ySW;fuJmHo*%Qcksbazr5#j2%kGAX;Vt!%<3+F^9yJA`xDTJug|f>Zdx@P zPONX2b7hdtd-BMY;{C&gA~AqayzOc%1Kl7vabuuYwy+$Up_jD$TM6t(8 zp~@yCf^gry0knBYjM{FT6O(D9(aDaNk8QAr5+)_!QCf?B7GnXiMghe0jIKgHLiVw? zf=3F$T_crA$FoueCZ&)hDog5_gB0sRY#=d4h*=NjQTkd&m)v_lKUwQMdU=NHTLu2$ zE8Xghn;oH$(gBXATW>o*sU0(y_5t7KjV>UV$OQA5lmc{k0H?3*;LPd>%fk%oJ99wF z);x}d2wDTZ%s_kqo%dMmXBZcCC!?x)9jb9AlGTDoW(+EB0M_;pY%{N|^Z4(6>*+>} zP%(Q>7-wBwpc_m|4HOlxd+VV!6eG^svS35_aTSMM$Y7oJUKUM z2cI1RYQA!17a#o8E8&?dCbUS=&3jMmaQ0N6VPY#$dNCt5Q*13m^RAas(^Mhq{DdAEQ^87FS@s-SWW0yYYBAfVt$AlCRh zxfcFjYWhKmzH>3bNDp|~93%)*07gZHes02HD+!8J?;6r~N1&H=#7a7-CuPN6Ows7M zUHO%+VRWt2szxs~ZJBLk)CP-;R)Jk<3~fStC8G^yhf%n+KE=7U5q8EEmKPExC7a(i z0wOQIX>_f-d`tuQJqb|>_l(`G$gBjd!<^5$MJ5Vbq0W02;fHGxu;0Bgnc z_u64vxBxPEPiz&DdQ_IGVnXAm>YXG}bnjFQ0JPTp+~ob~@cuo6nZi{4)YmTIt1qo{ zGUL$hrl|m&zi=rWj>lH|*yPS;^6Vq4x*M!Zhc*hg9vNWJGbn3+V7d1}N2WFT=pr_h zFk0g)FRfu`Vxcwdc}gi9UFxHDdr)YUUI)Q zGTUsY!cLyAafmiWhw9W@F+q5{dvXJBK_0i1Sm3cVtS<*SAQPc#e-*@XGoKxZNb|*EKhFPqj`|f0& z&STw3IKvs|s!-cnPLNg(ft`!?eN)eTnV3-g1hsEfG%=Y2p|M-!5Ps< zi>|B5R1Dvh`Z#)zx~?dGLY#X9bT{X+Bzuw3O}=;Y8s|6%YBsQk(jfi(;7O?oD6A@q z&(nW5T5&@@&xv4%CuPeE9heUmdykM!d*1(Lc8bKWwmvjMd((-9ec6x_8-%l5_W6$Y zarwp=PoM2>u5vS@>-M_VuGR|IH>cE~(RuW9jh8QPw^hKec{p@;&7LWist6j$9_bO) zQozm0z0)&eJhk|>hhGizshHzQX_DqM+Gtdj$B9GzP7hUp8R5xoNv{PV8Dxf8kclF2 zX^>-gI@^e=onJcdLmVkqXj=P*En0f7?LBI9R0UfCfaYLTQ>-dSgGW?bgzHG&i&iSA zy(k_b44{`$0p#z#_nvt6^79UPObcYItVch&M$dqwVd=3kw132Ny_qKrauWzmBC8kD zh3;5K0N8&z?S3t+bIlE9Rq(2mJtS)Z+w&Aq0C3MOi&)z#P?Z%-CLZvH&Z$fXyu&+> z+m9^-@R<}H+=GDCF~U@rMqrXFukV!j%FAn^U_;j?a&Lu=of6NS+ib)k-p@XdbZ++8 z=Ua2EDaF>H9^kns>qY$No64tXFG71F00rE6bU=B#k8Dc-GgcmPzR`Q07BiCZ)x&N7iz3eWKjRnfX(T z%D|?@1s!Y+!gaCohn~l`22Okfqy&&>yV9XdtBlAcEOuBU0ZGlY?TGt zsCc&)xD4?Jm3u6$og0A7cv7+th4;jiy6x~F{96Fb5}?{Us^m$fq@Qan^mFcpMc2AK zG|lhyAc{Xr-{kNVCZ%&83xf=!sf9LDy@(r#0Dy8x{mvt2apu}?s4Pl{Jd%6QGAV6R zHG!hJpJxhF4xUANm&(-BA#?Zp-g*>E!yIKrQqBqcOxoH(85My7MU8B~B(94?6PS#(6=^6S%qtm{b?6y`ie(R@)-*347_MV#9o5ar+U#{-Y z8CNy_l}WPEShj8(^7pK%VCW5;+8^yCkl&h3+Qn zsHp5rnN3Uz7nqdJ6Tq2%e=k!Zj;7*^Z?Hn;y|mea^EwdA z-}vOe1H{rK`8zdau+E{hB=^3$Io+2{85&!NCuLwh&92=iUfIMaZbAhxZ`9nR3Ls|C z|NgNTW{PeA&52Kc{W8Az+*%+_jBdhjTE@q|dJ(_z`O`B$_Z(NZ*^o0#s02u^nPlNT zpb}5;*V5hF7=>wVaqjvU(%hv?VwN_8vE` zLA@oayQM)kBl%|BJZu++i2}mZ6fj;Z>F(Xpjk#s#u{AD(HHNSzgIBea4$!kq)igI$ z0H!scnc{b=BEjaJ_Z|v!I27q6H%_MYlczUuZgrfX069rTmp*|@M+=o|h_ z7r<7Mp;Im)&*JD(FR^K$=i6XXYR^r4Nm2W)7;k`-ppLlGRV=N$5KB-uKJ^C5>LJa!m!2WJUWy9eu z&)Rv9&pmw&|M%xk(dMP2XJeTx(1TQmc#90bUt2eqdC(3rR)$%SG_7tGjg37rDN|Vw z{ig)83%v}NH^*E#7kO#(I~k)4borD?sW^#W8f3^b>huZ31NY8jLs+m!4krMs+a<blZNiOs`2#LA$-RG>qjd@!v?Cs@}t2%zu*9DKb-W}?Y;Z5_V- z?!)NkRA|`PnL;atUS=>YErxxPwoWUHK_0Jzyi-*$qmHbq7?2m0!-Z9PM)QK5zjFUk zJaF4G0Pcd$2dwpRZR8wSYelBqsoc9LEvB{QK)2Z?xT-z!T(QRtfhMyFHA&V19S++Y zyCuf$KpF_r_F-;grDToy}2&FTJ?d`S(&;4fpCje@^qA^)nsr+aday0tk(w(k_N_ z?ZTo=e@#gD)~E=?wOY8@O>{Xnm)^|Mbys#&^qdCFeX|y0ncizqAhWyEXV%KlL31Zq@{V zzS05(rMZ+e&Z%4Pz;H1*J=Zr!B|iCt034BhK(g3b`LIXCGR3k20A9GbgAaEA4!|rE zTZY!)^(-+4fx9WNReSn(Y7#{E^6|$n;q}%$E1`vLVjnSFBg4vjID*OyPw=!X<(TPN-`(q-Dn2#75^SU zvEih&c=Y+W57KoZTvxb0FLA00jdR*SNXiFfC3(V!A7N4&t?>SL-r6`L z4l=ezCAP-1V4n9)pHLaI?0k9_=T=8Jdu=!5i}C#6DQ>Gd*Vp;2*|{%v`fVW-tNsvVO#cqku1#i}Myjx4x{ zG|*;Jw(TJ{M-?@#WB*?A1h>R!) z&Xau?BIquFtk#O<-zwOu1W!sgOUR;mPH~EOQpP?aT})=h{WOR<=$vcFz0;x?k#h*7 z?B^Qy+_C`gn=Jb@22F?e%<#7k9ty8q-67DGS&7;Z+te{0;JxG`v+5q_MeRcQy7LId zq;L(6_5xwm4ss%;rHv@ZWsQYi#&K(Gj7YIG%k!;6FE_ZfF-?@M1l*<;LJBdS-_Pdk zwT?GHO3({0Yx4OU7d6HV;t4CDA@^pAEK#sMKL6}>JpRfCP97e>d5@>h+`x^UD*PQ| zDxs44l^56WwNo2`r6j;q@E=5X6;zl}Hn5t+M+1z4&!|)ZBn>aLCbox34hQRC0A`@g z_oVD+2Gg2A45Jk$MIDkFsoq5<3myJ>M)y4InFFzr0C0jFSqQQ)d%4D>uo1-da^OZL zWrM}&XPf58?+Cc3wFIZ#*U}EmOY6eH*KVAnJtgn$V9}TeA}$RxjHe`<7Kuk{Knnmz zmwF*Nm%~IVa&p+_T48;s0Na;^vASKrvtNhdL%ysmfotKwjS@k0@=RmOaw;IsOT$w0NoiRU zp#VkTrc~Y(SEK6M!Ydu5TWJ+pD|%1fC6Y5oY3!t^;T-URcijr-ffp}r=~U)e0e z-0I~zR9~ermJE5e(ot6C%3|MylK3;{Hu0f{U&V75w(yB3F3p`6G9r^w!}=Hy+;?&* z1b2r0Rm$2iNCgxn?RiS6M%V3{gYudgAkxJ<1sSNA;~I>cVb9>hpFfSyKXWzw9swCF ziD3(Z+Il>HaT}k0^2&kakedHb*3m-pJSphqDtGNEsAZqX{n(U6ZDF)wfPtP%Sp&cJ z@TnjX3qVO1%T)lYMEc&vI_>4;#aUaAdv952TpM))RSf`Mzzy-E1b~%=JajpyzoU0_ zY^mR1QgTj!&^$uXUU%!^A)Jq_P7M`;GXNMB77IOMjMa|Vg+BRr%Qgp+&`5$I3?XVh zNAsKJbCwi8=uORI?|DR96{R9bX}D0BDLP}XZBB99ks!L?uc;|WaSAFK(Cf0XKyl`58MXj4B~=~iAbAZK}g$xs{|>Ezj6-zxCM z=dSa6rQiF5FJHh57q-KCO$RY-#6wU8+ZpS-eM{wUIt!dip{{L+A7E)dq}D1jHx4i4 z!L?_KmPin>P6Xl82iyx{*n574MAHFVp1pNb%5FW1-^RWN4O zqT!L}SMlo)$AoxBx+>?UBiVcWo$f>ph~QwDV+SbCObQp6d6alI3+V(2`|wVTCIl7m)vMB z)&hU&&O;4`P>4}1gO6O135*~wRzL}0;7A5KkJ0KA#*eUSrHKUrF_DRV^6+5aB8OZj zft0FIC``eSqH+XYshbDnx(3DK^xxOF3yKeGH&Y&CmVJ z*RSGZU%e3a&aSEykmb-5R~B*dwztjMa)5s^y(N$Z=RL66J_;*!`Py?ALSC{;y$A;6qmbDYmY9Em_7 zka%udDsoh<#yOS?h7qfg=dZ)H1tzoed~Z>);e7;3<$d68VI%+mAOJ~3K~xY~i1LX~ zKXElO3i)?ZpGRRirsFsN>?~Gy3I;P{ys)-i0`Rb&CHZ_liJz(zj?;81$h}?c`QSZS z97rL}sHo_X`|af_upb5ZpA~+^j}1HL z(Vus`0a7A14N*{d;^hteo6k{|J+iHm1|>>0f>Xty;^uBiNdlwp5>RSRz=%o79ugcJ zKq2@mO5!Gru=b!NC}lNCqm#&hkahb9U%8NUnx$$%X2_gAGqD4tFc4Be5=etCS}1)^ zbo#jt4QqPL)~lU^Nq|*h?e%l2$PW7%?mV_I_u1drE%DU38~EJQS6M!ng@9HTW-rqO zBKa6dW=5l^eCUSQ+%&tZL+Ot)tvWrmWV8>~+~|#WKB=R+>Or}7of@jO_*?KxKlM%+&>=zz zDXpnNmIZ*Jrm|4*C$Wxz6vcoK$s+_ z^~x>|E%tD2a|%FVY2`2-Abg)c{r>y$kKcbk-?v8XYycPb1M6fg4>SC|AGqg0zk4$y z<78FVM5gJGmY)ll*5@WW)_P1zhhZMG3Z(ixnp%uYJ2%*ok!QSi zGu`y{Qoxms3B6ZD*~spc0I6|lS@xnr0SOSfw+;;k(yDTN78;fc>>)TEa7W7&Z~>Uz z#UQ6JNJd4C;lOlv1m}?kiK+B((0HDT-vbp##wFd`K|soqyQoB+$NfVx`AHZu9+00gXVpxL#9s>>qsj0=|Cw20rrWd8}@is5rq(v?`B2pkUpuvd4}UH)ML+wEEBIeN`W(PXkmclYXIgXfvZh3N zZKuR~0))2ENbufm>0 zaW*kZKmOPyT)i=2CQ<~nocHMUdtB5+XciV@;W#m3;>@vybr|%r&_t~LJjj!BPU9R$ zF^kMQkH~O0TJ!%kK~iDb$sZWrf8|%a^Fe_!!=&6}e*5Hig{hR@Z|yvGCN=pJ=leI2 zYpX08jCsD;KY8{Ve)*Fxp$+!E*1@3%0Af;7lOH>?hX`=8Gp-sj{XszTbhOv+%a^yY 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