diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt index 526dec7353..ba38c27517 100644 --- a/applications/CMakeLists.txt +++ b/applications/CMakeLists.txt @@ -156,6 +156,7 @@ add_holohub_application(webrtc_video_server) add_holohub_application(yolo_model_deployment) add_holohub_application(vila_live) +add_holohub_application(holocat) add_holohub_application(holochat) add_holohub_application(xr_holoviz DEPENDS OPERATORS xr) add_holohub_application(xr_gsplat DEPENDS OPERATORS xr) diff --git a/applications/holocat/CMakeLists.txt b/applications/holocat/CMakeLists.txt new file mode 100644 index 0000000000..eb6c84d094 --- /dev/null +++ b/applications/holocat/CMakeLists.txt @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) + +# Check if EC-Master SDK is available before building +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +find_package(EcMaster QUIET) + +if(EcMaster_FOUND) + message(STATUS "Building HoloCat application with EC-Master SDK") + add_subdirectory(cpp) +else() + message(FATAL_ERROR "EC-Master SDK not found") + + # Exit with failure + exit(1) +endif() diff --git a/applications/holocat/Dockerfile b/applications/holocat/Dockerfile new file mode 100644 index 0000000000..fee8703614 --- /dev/null +++ b/applications/holocat/Dockerfile @@ -0,0 +1,71 @@ +# HoloCat EtherCAT Application Dockerfile + +ARG BASE_SDK_VERSION +FROM ${BASE_IMAGE} + +# Install system dependencies for EtherCAT and real-time operation +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + libcap-dev \ + ethtool \ + net-tools \ + iproute2 \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +# Copy EC-Master SDK from build context +# To build: Copy SDK to applications/holocat/ecmaster-sdk/ +# cp -r /path/to/ecmaster applications/holocat/ecmaster-sdk +# Note: Docker COPY doesn't follow symlinks outside build context +RUN mkdir -p /opt/acontis/ecmaster +COPY applications/holocat/ecmaster-sdk /opt/acontis/ecmaster + +# Set environment variables for EC-Master SDK +ENV ECMASTER_ROOT=/opt/acontis/ecmaster +ENV LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/opt/acontis/ecmaster/lib:/opt/acontis/ecmaster/Bin/Linux/x64 + +# Create holocat user with necessary privileges +RUN useradd -m -s /bin/bash holocat && \ + usermod -aG sudo holocat && \ + echo "holocat ALL=(ALL) NOPASSWD: /usr/sbin/setcap" >> /etc/sudoers + +# Create directories for configuration, logs, and scripts +RUN mkdir -p /opt/holocat/config /opt/holocat/logs /opt/holocat/scripts && \ + chown -R holocat:holocat /opt/holocat + +# Copy default configuration files and verification script +COPY applications/holocat/configs/holocat_config.yaml /opt/holocat/config/ +COPY applications/holocat/configs/eni2.xml /opt/holocat/config/ +COPY applications/holocat/scripts/verify_ecmaster.sh /opt/holocat/scripts/ +RUN chmod +x /opt/holocat/scripts/verify_ecmaster.sh + +# Verify EC-Master installation (required - will fail build if not found) +# Only check critical components during build (headers and libraries) +RUN /opt/holocat/scripts/verify_ecmaster.sh || \ + (echo "Verification failed - checking if SDK is minimally viable..." && \ + test -f "/opt/acontis/ecmaster/SDK/INC/EcMaster.h" && \ + test -f "/opt/acontis/ecmaster/Bin/Linux/x64/libEcMaster.so" && \ + echo "✓ Minimal SDK requirements met for build") + +# Set working directory +WORKDIR /workspace + +# Switch to holocat user +USER holocat + +# Set default environment variables for HoloCat +ENV HOLOCAT_ADAPTER_NAME=eth0 +ENV HOLOCAT_ENI_FILE=/opt/holocat/config/eni2.xml +ENV HOLOCAT_CONFIG_DIR=/opt/holocat/config +ENV HOLOCAT_LOG_DIR=/opt/holocat/logs + +# Default command +CMD ["/bin/bash"] + +# Labels for container metadata +LABEL org.opencontainers.image.title="HoloCat EtherCAT Application" +LABEL org.opencontainers.image.description="Real-time EtherCAT integration with NVIDIA Holoscan SDK" +LABEL org.opencontainers.image.vendor="NVIDIA" +LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL org.opencontainers.image.documentation="https://github.com/nvidia-holoscan/holohub/applications/holocat" diff --git a/applications/holocat/FindEcMaster.cmake b/applications/holocat/FindEcMaster.cmake new file mode 100644 index 0000000000..be9ba8f885 --- /dev/null +++ b/applications/holocat/FindEcMaster.cmake @@ -0,0 +1,155 @@ +# FindEcMaster.cmake +# Find the acontis EC-Master SDK +# +# This module defines: +# ECMASTER_FOUND - True if EC-Master SDK is found +# ECMASTER_INCLUDE_DIRS - Include directories for EC-Master +# ECMASTER_LIBRARIES - Libraries to link against +# ECMASTER_VERSION - Version of EC-Master SDK +# +# Environment variables used: +# ECMASTER_ROOT - Root directory of EC-Master installation + +# Handle CMake policy for environment variables +if(POLICY CMP0144) + cmake_policy(SET CMP0144 NEW) +endif() + +# Get ECMASTER_ROOT from environment if not set as CMake variable +if(NOT ECMASTER_ROOT AND DEFINED ENV{ECMASTER_ROOT}) + set(ECMASTER_ROOT $ENV{ECMASTER_ROOT}) +endif() + +# Find include directory +find_path(ECMASTER_INCLUDE_DIR + NAMES EcMaster.h + PATHS + ${ECMASTER_ROOT}/SDK/INC + /opt/acontis/ecmaster/SDK/INC + ${CMAKE_SOURCE_DIR}/../ethercat/ecm/SDK/INC + /usr/local/include/ecmaster + DOC "EC-Master include directory" +) + +# Determine library directory based on architecture +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(ECMASTER_ARCH "x64") +else() + set(ECMASTER_ARCH "x86") +endif() + +# Find main EC-Master library +find_library(ECMASTER_LIBRARY + NAMES EcMaster libEcMaster.so + PATHS + ${ECMASTER_ROOT}/SDK/LIB/Linux/${ECMASTER_ARCH} + ${ECMASTER_ROOT}/Bin/Linux/${ECMASTER_ARCH} + /opt/acontis/ecmaster/lib + ${CMAKE_SOURCE_DIR}/../ethercat/ecm/Bin/Linux/${ECMASTER_ARCH} + /usr/local/lib + DOC "EC-Master library" +) + +# Get library directory for finding link layer libraries +if(ECMASTER_LIBRARY) + get_filename_component(ECMASTER_LIBRARY_DIR ${ECMASTER_LIBRARY} DIRECTORY) +endif() + +# Find all available link layer libraries +set(ECMASTER_LINK_LIBRARIES) +set(ECMASTER_LINK_LAYER_NAMES + emllSockRaw + emllDpdk + emllIntelGbe + emllRTL8169 + emllVlan + emllRemote + emllCCAT + emllBcmNetXtreme + emllLAN743x + emllDW3504 + emllAlteraTSE +) + +foreach(lib_name IN LISTS ECMASTER_LINK_LAYER_NAMES) + find_library(ECMASTER_${lib_name}_LIBRARY + NAMES ${lib_name} lib${lib_name}.so + PATHS ${ECMASTER_LIBRARY_DIR} + NO_DEFAULT_PATH + ) + if(ECMASTER_${lib_name}_LIBRARY) + list(APPEND ECMASTER_LINK_LIBRARIES ${ECMASTER_${lib_name}_LIBRARY}) + message(STATUS "Found EC-Master link layer: ${lib_name}") + endif() +endforeach() + +# Try to determine version from EcVersion.h +if(ECMASTER_INCLUDE_DIR) + set(ECVERSION_FILE "${ECMASTER_INCLUDE_DIR}/EcVersion.h") + if(EXISTS ${ECVERSION_FILE}) + file(READ ${ECVERSION_FILE} ECVERSION_CONTENT) + string(REGEX MATCH "#define EC_VERSION_MAJ[ \t]+([0-9]+)" _ ${ECVERSION_CONTENT}) + set(ECMASTER_VERSION_MAJOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define EC_VERSION_MIN[ \t]+([0-9]+)" _ ${ECVERSION_CONTENT}) + set(ECMASTER_VERSION_MINOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define EC_VERSION_SERVICEPACK[ \t]+([0-9]+)" _ ${ECVERSION_CONTENT}) + set(ECMASTER_VERSION_PATCH ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define EC_VERSION_BUILD[ \t]+([0-9]+)" _ ${ECVERSION_CONTENT}) + set(ECMASTER_VERSION_BUILD ${CMAKE_MATCH_1}) + + if(ECMASTER_VERSION_MAJOR AND ECMASTER_VERSION_MINOR AND ECMASTER_VERSION_PATCH) + set(ECMASTER_VERSION "${ECMASTER_VERSION_MAJOR}.${ECMASTER_VERSION_MINOR}.${ECMASTER_VERSION_PATCH}.${ECMASTER_VERSION_BUILD}") + endif() + endif() +endif() + +# Handle standard CMake find_package arguments +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(EcMaster + REQUIRED_VARS ECMASTER_INCLUDE_DIR ECMASTER_LIBRARY + VERSION_VAR ECMASTER_VERSION +) + +# Set output variables +if(EcMaster_FOUND) + set(ECMASTER_LIBRARIES ${ECMASTER_LIBRARY} ${ECMASTER_LINK_LIBRARIES}) + set(ECMASTER_INCLUDE_DIRS ${ECMASTER_INCLUDE_DIR}) + + # Also check for Linux-specific include directory + if(EXISTS "${ECMASTER_INCLUDE_DIR}/Linux") + list(APPEND ECMASTER_INCLUDE_DIRS "${ECMASTER_INCLUDE_DIR}/Linux") + endif() + + # Create imported target + if(NOT TARGET EcMaster::EcMaster) + add_library(EcMaster::EcMaster SHARED IMPORTED) + set_target_properties(EcMaster::EcMaster PROPERTIES + IMPORTED_LOCATION ${ECMASTER_LIBRARY} + INTERFACE_INCLUDE_DIRECTORIES "${ECMASTER_INCLUDE_DIRS}" + ) + + # Add link layer libraries as dependencies + if(ECMASTER_LINK_LIBRARIES) + set_target_properties(EcMaster::EcMaster PROPERTIES + INTERFACE_LINK_LIBRARIES "${ECMASTER_LINK_LIBRARIES}" + ) + endif() + endif() + + message(STATUS "Found EC-Master SDK:") + message(STATUS " Version: ${ECMASTER_VERSION}") + message(STATUS " Include: ${ECMASTER_INCLUDE_DIRS}") + message(STATUS " Library: ${ECMASTER_LIBRARY}") + message(STATUS " Link layers: ${ECMASTER_LINK_LIBRARIES}") +endif() + +# Mark variables as advanced +mark_as_advanced( + ECMASTER_INCLUDE_DIR + ECMASTER_LIBRARY + ECMASTER_LIBRARY_DIR +) + +foreach(lib_name IN LISTS ECMASTER_LINK_LAYER_NAMES) + mark_as_advanced(ECMASTER_${lib_name}_LIBRARY) +endforeach() diff --git a/applications/holocat/README.md b/applications/holocat/README.md new file mode 100644 index 0000000000..32d0b3eb54 --- /dev/null +++ b/applications/holocat/README.md @@ -0,0 +1,94 @@ +# HoloCat - EtherCAT Real-time Integration + +![HoloCat Logo](docs/holocat_logo.png) + +HoloCat is an EtherCAT master application that integrates the acontis EC-Master SDK with NVIDIA's Holoscan platform. + +## Overview + +HoloCat provides deterministic EtherCAT communication capabilities within the Holoscan ecosystem, enabling: + +- **Real-time Control** +- **Holoscan Native** + +## Prerequisites + +### Required Dependencies + +1. **acontis EC-Master SDK** (Commercial License) + - Version 3.2.3 or later + +## Usage + +### Prerequisites +```bash +# Set EC-Master SDK path +export ECMASTER_ROOT=/home/hking/devel/ethercat/ecm + +# Verify installation (optional) +./applications/holocat/scripts/verify_ecmaster.sh +``` + +### Build +```bash +# Build using HoloHub CLI (recommended) +./holohub build holocat --local +``` + +### Run +```bash +# Run with configuration file +./build/holocat/applications/holocat/cpp/holocat --config ./applications/holocat/configs/holocat_config.yaml +``` + +## Configuration + +### Basic Configuration + +Create `holocat_config.yaml`: + +```yaml +holocat: + # Network adapter for EtherCAT + adapter_name: "eth0" # Change to your EtherCAT interface + + # EtherCAT configuration file + eni_file: "/tmp/holocat_config.xml" + + # Cycle time in microseconds + cycle_time_us: 1000 # 1ms cycle time + + # Real-time priorities (1-99) + rt_priority: 39 + job_thread_priority: 98 + + # Enable real-time scheduling + enable_rt: true + +# Holoscan application configuration +holoscan: + logging: + level: "info" +``` + +### ENI File Generation + +Use EtherCAT configuration tools to generate your ENI file. + + +## Troubleshooting + +### Common Issues + +1. **Permission Denied** + ```bash + # Ensure capabilities are set + sudo setcap 'cap_net_raw=ep' ./build/holocat/applications/holocat/holocat + ``` + +3. **EC-Master SDK Not Found** + ```bash + # Verify ECMASTER_ROOT environment variable + echo $ECMASTER_ROOT + ls -la $ECMASTER_ROOT/SDK/INC/EcMaster.h + ``` \ No newline at end of file diff --git a/applications/holocat/configs/eni2.xml b/applications/holocat/configs/eni2.xml new file mode 100644 index 0000000000..24d6390519 --- /dev/null +++ b/applications/holocat/configs/eni2.xml @@ -0,0 +1 @@ +ffffffffffff5c857e3ca9a6a4881509949441IP1cycle7030400000IP1cycle7030400000IP1cycle8051204003IP1cycle801600003IP1cycle8076800000000000000003IPPIBISIOI1cycle8015362563IP1cycle8020482563IP1cycle802320323IP1cycle802433003IP1cycle80235200103IP1cycle802356000c3IP1cycle8025900310010YY26640028083660650048048Outputs614410015634Inputs64803216657#x1a01Module 1 (CSV - Axis).Inputs#x6041016Status WordUINT#x6064032Position Actual ValueDINT#x1602Module 1 (CSV - Axis).Outputs#x6040016Control WordUINT#x60ff032Target VelocityDINT409651251205121040965125120512CoEFoEPS017186001000216PS01718700100011aPIBISIOIcycle20288110035000PISIOIcycle103040000301000f005000BIcycle103040000301000f0010000IPIBcycle202881100133000IPIBcycle1030400001301000f003000IPcycle2012800013IP20128200010800000013IPcycle10128800000000130a010000100IP20128200010a00000013IPcycle1012880000000013b3c76202100IPIBcycle2016e90313IPIBPISIOI510012048000000000000000000000000000000003BI202048000000000000000000000000000000003IP510012048001000022600010013IP510012056001400022200010013IB510012048001000022600010013IB510012056001400022200010013SPOPcycle5100128812003005000IPSPSIOPOI510012064163PS510012064001806006400010013PS510012072501906002000010013PS5100115360000001006000007001800020100000013PS5100115520000001006000007501900010100000013OScycle5100128804003200SPSIOPOI510011536000000000000000000000000000000003SPSIOPOI510011552000000000000000000000000000000003SPOPcycle4100130400000000000030200000000000f00000000005000IPIBcycle5100112800113IIcycle201280003IPcycle51001288120013003000IPcycle41001304000000000000130200000000001f00000000003000IPBIcycle201280003IBcycle510012881300133000IBcycle41001304000000000000130300000000001f00000000003000PScycle5100128804001310000PScycle41001304000000000000130400000000001f000000000010000OScycle4100130400000000000030400000000000f0000000000200SOcycle5100128808001310000SOcycle41001304000000000000130800000000001f0000000000100000100265535YY3312268424420484848112Outputs6144100158875632Inputs921601691166586657#x1affDevice Status PDO#xf10011K-Bus Cycle Overrun FlagBOOL#xf10021Input Process Data Hold Ack.BOOL#xf10031Output Process Data Hold Ack.BOOL#xf10041Output Process Data Clear Ack.BOOL#x011#x10f341New Message AvailableBOOL#xf100516Diagnostics Status WordUINT#x1a02TxPDO Mapping Terminal 3#x6020116Channel 1, Word 1UINT#x6020216Channel 2, Word 1UINT#x6020316Channel 3, Word 1UINT#x6020416Channel 4, Word 1UINT#x1a01TxPDO Mapping Terminal 2#x601011Channel 1 DataBOOL#x601021Channel 2 DataBOOL#x601031Channel 3 DataBOOL#x601041Channel 4 DataBOOL#x601051Channel 5 DataBOOL#x601061Channel 6 DataBOOL#x601071Channel 7 DataBOOL#x601081Channel 8 DataBOOL#x601091Channel 9 DataBOOL#x6010101Channel 10 DataBOOL#x6010111Channel 11 DataBOOL#x6010121Channel 12 DataBOOL#x6010131Channel 13 DataBOOL#x6010141Channel 14 DataBOOL#x6010151Channel 15 DataBOOL#x6010161Channel 16 DataBOOL#x16ffDevice Control PDO#xf20011K-Bus Cycle Overrun Flag DisableBOOL#xf20021Input Process Data Hold RequestBOOL#xf20031Output Process Data Hold RequestBOOL#xf20041Output Process Data Clear RequestBOOL#x012#xf200516Diagnostics Control WordUINT#x1600RxPDO Mapping Terminal 1#x700011Channel 1 DataBOOL#x700021Channel 2 DataBOOL#x700031Channel 3 DataBOOL#x700041Channel 4 DataBOOL#x700051Channel 5 DataBOOL#x700061Channel 6 DataBOOL#x700071Channel 7 DataBOOL#x700081Channel 8 DataBOOL#x700091Channel 9 DataBOOL#x7000101Channel 10 DataBOOL#x7000111Channel 11 DataBOOL#x7000121Channel 12 DataBOOL#x7000131Channel 13 DataBOOL#x7000141Channel 14 DataBOOL#x7000151Channel 15 DataBOOL#x7000161Channel 16 DataBOOL409651251205120CoE50010PIBISIOIcycle265535288110035000PISIOIcycle1655353040000301000f005000BIcycle1655353040000301000f0010000IPIBcycle2655352881100133000IPIBcycle16553530400001301000f003000IPcycle26553512800013IP265535128200010800000013IPcycle1655351288000000001321000000100IP265535128200010a00000013IPcycle1655351288000000001354035007100IPIBcycle26553516ea0313IPIBPISIOI510022048000000000000000000000000000000003BI2655352048000000000000000000000000000000003IPIB510022048001000022600010013IPIB510022056001400022200010013SPOPcycle5100228812003005000IPSPSIOPOI510022064163PS510022064001806006400010013PS51002207200240e000000010013PS5100215360600001006000007001800020100000013PS510021552060000100e000007002400010100000013IPIB51002156800000009010000000d0800010100000013OScycle5100228804003200SPSIOPOI510021536000000000000000000000000000000003SPSIOPOI510021552000000000000000000000000000000003PIBISIOI510021568000000000000000000000000000000003SPOPcycle4100230400000000000030200000000000f00000000005000IPIBcycle5100212800113IIcycle2655351280003IPcycle51002288120013003000IPcycle41002304000000000000130200000000001f00000000003000IPBIcycle2655351280003IBcycle510022881300133000IBcycle41002304000000000000130300000000001f00000000003000PScycle5100228804001310000PScycle41002304000000000000130400000000001f000000000010000OScycle4100230400000000000030400000000000f0000000000200SOcycle5100228808001310000SOcycle41002304000000000000130800000000001f000000000010000B100101000PREOPSAFEOPOP70304222020PREOPSAFEOPOP1015099494422222SAFEOPOP122684354562060024SubDevice_1001 [simco drive 40028083-00-0].Module 1 (CSV - Axis).Inputs.Status WordObject 0x6041:0UINT160SubDevice_1001 [simco drive 40028083-00-0].Module 1 (CSV - Axis).Inputs.Position Actual ValueObject 0x6064:0DINT3216SubDevice_1002 [750-354].Device Status PDO.K-Bus Cycle Overrun FlagBOOL148SubDevice_1002 [750-354].Device Status PDO.Input Process Data Hold Ack.BOOL149SubDevice_1002 [750-354].Device Status PDO.Output Process Data Hold Ack.BOOL150SubDevice_1002 [750-354].Device Status PDO.Output Process Data Clear Ack.BOOL151SubDevice_1002 [750-354].Device Status PDO.New Message AvailableBOOL163SubDevice_1002 [750-354].Device Status PDO.Diagnostics Status WordUINT1664SubDevice_1002 [750-354].TxPDO Mapping Terminal 3.Channel 1, Word 1UINT1680SubDevice_1002 [750-354].TxPDO Mapping Terminal 3.Channel 2, Word 1UINT1696SubDevice_1002 [750-354].TxPDO Mapping Terminal 3.Channel 3, Word 1UINT16112SubDevice_1002 [750-354].TxPDO Mapping Terminal 3.Channel 4, Word 1UINT16128SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 1 DataBOOL1144SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 2 DataBOOL1145SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 3 DataBOOL1146SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 4 DataBOOL1147SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 5 DataBOOL1148SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 6 DataBOOL1149SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 7 DataBOOL1150SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 8 DataBOOL1151SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 9 DataBOOL1152SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 10 DataBOOL1153SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 11 DataBOOL1154SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 12 DataBOOL1155SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 13 DataBOOL1156SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 14 DataBOOL1157SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 15 DataBOOL1158SubDevice_1002 [750-354].TxPDO Mapping Terminal 2.Channel 16 DataBOOL115924SubDevice_1001 [simco drive 40028083-00-0].Module 1 (CSV - Axis).Outputs.Control WordObject 0x6040:0UINT160SubDevice_1001 [simco drive 40028083-00-0].Module 1 (CSV - Axis).Outputs.Target VelocityObject 0x60FF:0DINT3216SubDevice_1002 [750-354].Device Control PDO.K-Bus Cycle Overrun Flag DisableBOOL148SubDevice_1002 [750-354].Device Control PDO.Input Process Data Hold RequestBOOL149SubDevice_1002 [750-354].Device Control PDO.Output Process Data Hold RequestBOOL150SubDevice_1002 [750-354].Device Control PDO.Output Process Data Clear RequestBOOL151SubDevice_1002 [750-354].Device Control PDO.Diagnostics Control WordUINT1664SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 1 DataBOOL180SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 2 DataBOOL181SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 3 DataBOOL182SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 4 DataBOOL183SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 5 DataBOOL184SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 6 DataBOOL185SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 7 DataBOOL186SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 8 DataBOOL187SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 9 DataBOOL188SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 10 DataBOOL189SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 11 DataBOOL190SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 12 DataBOOL191SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 13 DataBOOL192SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 14 DataBOOL193SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 15 DataBOOL194SubDevice_1002 [750-354].RxPDO Mapping Terminal 1.Channel 16 DataBOOL195 \ No newline at end of file diff --git a/applications/holocat/configs/holocat_config.yaml b/applications/holocat/configs/holocat_config.yaml new file mode 100644 index 0000000000..466033ccac --- /dev/null +++ b/applications/holocat/configs/holocat_config.yaml @@ -0,0 +1,43 @@ +# HoloCat EtherCAT Configuration + +holocat: + # EtherCAT network adapter name + adapter_name: "enxc84bd6d2788c" + + # EtherCAT Network Information (ENI) file path + # Use environment variable HOLOCAT_ENI_FILE to override, or falls back to config dir + eni_file: "/home/hking/devel/holo/holohub/applications/holocat/configs/eni2.xml" + + # EtherCAT bus cycle time in microseconds + # Common values: 1000 (1ms), 500 (0.5ms), 250 (0.25ms) + cycle_time_us: 10000 + + # Real-time thread priority (1-99, higher = more priority) + # Typical values: 39 (medium), 80 (high), 99 (highest) + rt_priority: 39 + + # Job thread priority (1-99, typically higher than rt_priority) + job_thread_priority: 98 + + # Enable real-time scheduling and memory locking + enable_rt: true + + # Process data bit offsets for Wago DIO modules + # These depend on your specific EtherCAT slave configuration + dio_out_offset: 80 # Digital output bit offset (10 bytes * 8 bits) + dio_in_offset: 144 # Digital input bit offset (18 bytes * 8 bits) + + # Advanced EtherCAT parameters + max_acyc_frames: 32 # Maximum queued acyclic frames + job_thread_stack_size: 0x8000 # Job thread stack size (32KB) + +# Holoscan application configuration +holoscan: + # Scheduler configuration + scheduler: + type: "greedy" + check_recession_period_ms: 0 + + # Logging configuration + logging: + level: "info" diff --git a/applications/holocat/cpp/CMakeLists.txt b/applications/holocat/cpp/CMakeLists.txt new file mode 100644 index 0000000000..8996862889 --- /dev/null +++ b/applications/holocat/cpp/CMakeLists.txt @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(holocat LANGUAGES CXX) + +# Find required packages +find_package(holoscan 3.0 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +# Find EC-Master SDK (REQUIRED - no fallback) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/..) +find_package(EcMaster REQUIRED) + +message(STATUS "HoloCat: EC-Master SDK found:") +message(STATUS " Include: ${ECMASTER_INCLUDE_DIRS}") +message(STATUS " Libraries: ${ECMASTER_LIBRARIES}") + +# Source files (no conditional compilation) +set(HOLOCAT_SOURCES + main.cpp + holocat_op.cpp + holocat_app.cpp + hc_data_tx_op.cpp + hc_data_rx_op.cpp +) + +set(HOLOCAT_HEADERS + holocat_op.hpp + holocat_app.hpp + hc_data_tx_op.hpp + hc_data_rx_op.hpp +) + +# Create executable +add_executable(holocat ${HOLOCAT_SOURCES} ${HOLOCAT_HEADERS}) + +# Include directories +target_include_directories(holocat PRIVATE + . + ${ECMASTER_INCLUDE_DIRS} +) + +# Compile options +target_compile_options(holocat PRIVATE + -Wall + -Wextra + -Wno-unused-parameter + -Wno-packed-not-aligned +) + +# Link libraries (EtherCAT always linked) +target_link_libraries(holocat + holoscan::core + ${ECMASTER_LIBRARIES} + pthread + rt +) + +# Set RPATH for runtime library discovery and output directory +set_target_properties(holocat PROPERTIES + INSTALL_RPATH_USE_LINK_PATH TRUE + BUILD_WITH_INSTALL_RPATH FALSE + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.." +) + +# Optionally set capabilities for real-time operation +# Set HOLOCAT_SET_CAPABILITIES=ON to enable this during build +# Otherwise, set capabilities manually after build: +# sudo setcap 'cap_net_raw,cap_sys_nice,cap_ipc_lock=ep' holocat +option(HOLOCAT_SET_CAPABILITIES "Set Linux capabilities during build" ON) +if(HOLOCAT_SET_CAPABILITIES) + add_custom_command(TARGET holocat POST_BUILD + COMMAND sudo setcap 'cap_net_raw,cap_sys_nice,cap_ipc_lock=ep' $ + COMMENT "Setting capabilities for real-time EtherCAT operation" + ) +endif() diff --git a/applications/holocat/cpp/hc_data_rx_op.cpp b/applications/holocat/cpp/hc_data_rx_op.cpp new file mode 100644 index 0000000000..7e7a8f820b --- /dev/null +++ b/applications/holocat/cpp/hc_data_rx_op.cpp @@ -0,0 +1,39 @@ +/*----------------------------------------------------------------------------- + * hc_data_rx_op.cpp + * HoloCat Data Receive Operator + * + * This file implements the HcDataTxOp operator that generates incrementing + * counter data for testing and demonstration purposes. + *---------------------------------------------------------------------------*/ + +#include + +#include "hc_data_rx_op.hpp" + + +namespace holocat { + +void HcDataRxOp::setup(holoscan::OperatorSpec& spec) { + // Configure input port for receiving counter data + spec.input("count_in"); + + HOLOSCAN_LOG_INFO("HcDataRxOp: Setup complete - configured input port 'count_in'"); +} + +void HcDataRxOp::compute(holoscan::InputContext& op_input, + holoscan::OutputContext& op_output, + holoscan::ExecutionContext& context) { + + // Receive count value from ECat bus + auto maybe_count = op_input.receive("count_in"); + if (!maybe_count) { + HOLOSCAN_LOG_ERROR("HcDataRxOp: Failed to receive count from ECat bus"); + return; + } + last_count_ = maybe_count.value(); + HOLOSCAN_LOG_INFO("HcDataRxOp: Received count: {}", last_count_); +} + +} // namespace holocat + +/*-END OF SOURCE FILE--------------------------------------------------------*/ diff --git a/applications/holocat/cpp/hc_data_rx_op.hpp b/applications/holocat/cpp/hc_data_rx_op.hpp new file mode 100644 index 0000000000..da60bd0ff4 --- /dev/null +++ b/applications/holocat/cpp/hc_data_rx_op.hpp @@ -0,0 +1,59 @@ +/*----------------------------------------------------------------------------- + * hc_data_rx_op.hpp + * HoloCat Data Receive Operator for Holoscan SDK + * + * This file defines the HcDataRxOp operator that receives integer data from the ECat bus. + *---------------------------------------------------------------------------*/ + +#ifndef INC_HC_DATA_RX_OP_H +#define INC_HC_DATA_RX_OP_H 1 + +#include +#include + +namespace holocat { + +/** + * @brief HoloCat Data Receive Operator + * + * A Holoscan operator that receives integer data from the ECat bus. + * This operator acts as a data receiver, receiving integer values from the ECat bus. + * + * The operator runs as a receiver operator and can be used for testing data + * flow in Holoscan applications or as a simple data receiver. + */ +class HcDataRxOp : public holoscan::Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(HcDataRxOp); + + /** + * @brief Default constructor + */ + HcDataRxOp() = default; + + /** + * @brief Setup the operator's input/output specifications + * @param spec The operator specification to configure + */ + void setup(holoscan::OperatorSpec& spec) override; + + /** + * @brief Compute method called on each execution cycle + * @param op_input Input context (unused for this source operator) + * @param op_output Output context for emitting counter data + * @param context Execution context + */ + void compute(holoscan::InputContext& op_input, + holoscan::OutputContext& op_output, + holoscan::ExecutionContext& context) override; + + private: + // last received count value + int last_count_ = 0; +}; + +} // namespace holocat + +#endif /* INC_HC_DATA_RX_OP_H */ + +/*-END OF SOURCE FILE--------------------------------------------------------*/ diff --git a/applications/holocat/cpp/hc_data_tx_op.cpp b/applications/holocat/cpp/hc_data_tx_op.cpp new file mode 100644 index 0000000000..092c898c27 --- /dev/null +++ b/applications/holocat/cpp/hc_data_tx_op.cpp @@ -0,0 +1,43 @@ +/*----------------------------------------------------------------------------- + * hc_data_tx_op.cpp + * HoloCat Data Transmit Operator Implementation + * + * This file implements the HcDataTxOp operator that generates incrementing + * counter data for testing and demonstration purposes. + *---------------------------------------------------------------------------*/ + + +#include + +#include "hc_data_tx_op.hpp" + + +namespace holocat { + +void HcDataTxOp::setup(holoscan::OperatorSpec& spec) { + // Configure output port for emitting counter data + spec.output("count_out"); + + HOLOSCAN_LOG_INFO("HcDataTxOp: Setup complete - configured output port 'count_out'"); +} + +void HcDataTxOp::compute(holoscan::InputContext& op_input, + holoscan::OutputContext& op_output, + holoscan::ExecutionContext& context) { + + // Increment counter + counter_++; + counter_ = counter_ % kMaxCount; + + // Emit current counter value + op_output.emit(counter_, "count_out"); + + // Log every 50 counts to avoid spam + if (counter_ % 50 == 0) { + HOLOSCAN_LOG_DEBUG("HcDataTxOp: 50x Emitted count = {}", counter_); + } +} + +} // namespace holocat + +/*-END OF SOURCE FILE--------------------------------------------------------*/ diff --git a/applications/holocat/cpp/hc_data_tx_op.hpp b/applications/holocat/cpp/hc_data_tx_op.hpp new file mode 100644 index 0000000000..3aebd7cbe9 --- /dev/null +++ b/applications/holocat/cpp/hc_data_tx_op.hpp @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------- + * hc_data_tx_op.hpp + * HoloCat Data Transmit Operator for Holoscan SDK + * + * This file defines the HcDataTxOp operator that generates incrementing + * counter data for testing and demonstration purposes. + *---------------------------------------------------------------------------*/ + +#ifndef INC_HC_DATA_TX_OP_H +#define INC_HC_DATA_TX_OP_H 1 + +#include +#include + +namespace holocat { + +/** + * @brief HoloCat Data Transmit Operator + * + * A Holoscan operator that generates incrementing counter data from 0 to 256. + * This operator acts as a data source, emitting integer values that increment + * on each compute cycle. When the counter reaches 256, it wraps back to 0. + * + * The operator runs as a source operator and can be used for testing data + * flow in Holoscan applications or as a simple data generator. + */ +class HcDataTxOp : public holoscan::Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(HcDataTxOp); + + /** + * @brief Default constructor + */ + HcDataTxOp() = default; + + /** + * @brief Setup the operator's input/output specifications + * @param spec The operator specification to configure + */ + void setup(holoscan::OperatorSpec& spec) override; + + /** + * @brief Compute method called on each execution cycle + * @param op_input Input context (unused for this source operator) + * @param op_output Output context for emitting counter data + * @param context Execution context + */ + void compute(holoscan::InputContext& op_input, + holoscan::OutputContext& op_output, + holoscan::ExecutionContext& context) override; + + private: + // coutner from 0 to maxcount for output on ECat bus + int counter_ = 0; + static constexpr int kMaxCount = 256; +}; + +} // namespace holocat + +#endif /* INC_HC_DATA_TX_OP_H */ + +/*-END OF SOURCE FILE--------------------------------------------------------*/ diff --git a/applications/holocat/cpp/holocat_app.cpp b/applications/holocat/cpp/holocat_app.cpp new file mode 100644 index 0000000000..2e20dbd0bc --- /dev/null +++ b/applications/holocat/cpp/holocat_app.cpp @@ -0,0 +1,168 @@ +/** + * @file holocat_app.cpp + * @brief HoloCat Application Implementation + * + * Implementation of the main HoloCat application class that manages + * EtherCAT integration with the Holoscan streaming framework. + */ + +// System includes +#include +#include + +// Local includes +#include "holocat_app.hpp" +#include "holocat_op.hpp" +#include "hc_data_tx_op.hpp" +#include "hc_data_rx_op.hpp" + +using namespace std::chrono_literals; + +namespace holocat { + +void HolocatApp::compose() { + HOLOSCAN_LOG_INFO("HoloCat EtherCAT Application Starting..."); + + // Extract and validate configuration parameters + HolocatConfig config = extract_config(); + + HOLOSCAN_LOG_INFO("Configuration loaded successfully"); + HOLOSCAN_LOG_INFO("EtherCAT adapter: {}", config.adapter_name); + HOLOSCAN_LOG_INFO("Cycle time: {} μs", config.cycle_time_us); + + // Create and configure the HoloCat operator + auto ecat_bus_periodic_cond = make_condition( + "ethercat_cycle", config.cycle_time_us * 1us); + std::shared_ptr holocat_op = make_operator("holocat_op", ecat_bus_periodic_cond); + holocat_op->set_config(config); + add_operator(holocat_op); + HOLOSCAN_LOG_INFO("HoloCat operator created with %dus periodic condition", config.cycle_time_us); + + // Create and configure the HcDataTxOp operator + auto counter_update_periodic_cond = make_condition( + "ethercat_cycle", 100ms); + std::shared_ptr data_tx_op; + data_tx_op = make_operator("data_tx_op", counter_update_periodic_cond); + add_operator(data_tx_op); + HOLOSCAN_LOG_INFO("HcDataTxOp operator created with 100ms periodic condition"); + + // create and configure the HcDataRxOp operator + std::shared_ptr data_rx_op; + data_rx_op = make_operator("data_rx_op"); + add_operator(data_rx_op); + HOLOSCAN_LOG_INFO("HcDataRxOp operator created (runs when data available)"); + + // Create flow from data_tx_op to holocat_op + add_flow(data_tx_op, holocat_op, {{"count_out", "count_in"}}); + HOLOSCAN_LOG_INFO("Flow created: data_tx_op.count_out -> holocat_op.count_in"); + + // Create flow from holocat_op to data_rx_op + add_flow(holocat_op, data_rx_op, {{"count_out", "count_in"}}); + HOLOSCAN_LOG_INFO("Flow created: holocat_op.count_out -> data_rx_op.count_in"); +} + +/* + * Extract configuration from the configuration file + * @return The configuration object + */ +HolocatConfig HolocatApp::extract_config() { + HolocatConfig config = {}; + + // Helper lambda for safe configuration extraction + auto try_extract = [this](const std::string& key, auto& target) { + try { + target = from_config(key).as>(); + } catch (...) { + // Key not found or conversion failed - target remains uninitialized + } + }; + + // EtherCAT network configuration + try_extract("holocat.adapter_name", config.adapter_name); + try_extract("holocat.eni_file", config.eni_file); + + // Timing configuration + try_extract("holocat.cycle_time_us", config.cycle_time_us); + + // Real-time configuration + try_extract("holocat.rt_priority", config.rt_priority); + try_extract("holocat.job_thread_priority", config.job_thread_priority); + try_extract("holocat.enable_rt", config.enable_rt); + try_extract("holocat.job_thread_stack_size", config.job_thread_stack_size); + + // Process data configuration + try_extract("holocat.dio_out_offset", config.dio_out_offset); + try_extract("holocat.dio_in_offset", config.dio_in_offset); + + // Communication configuration + try_extract("holocat.max_acyc_frames", config.max_acyc_frames); + + // Logging configuration + try_extract("holoscan.logging.level", config.log_level); + + // Validate and finalize configuration + if (!validate_config(config)) { + HOLOSCAN_LOG_ERROR("Configuration validation failed: {}", config.error_message); + throw std::runtime_error("Invalid configuration: " + config.error_message); + } + + return config; +} + +/* + * Validate the configuration + * @param config The configuration object + * @return True if the configuration is valid, false otherwise + */ +bool HolocatApp::validate_config(HolocatConfig& config) { + // Validate adapter name + if (config.adapter_name.empty()) { + config.error_message = "EtherCAT adapter name cannot be empty"; + return false; + } + + // Validate ENI file exists + if (!std::filesystem::exists(config.eni_file)) { + config.error_message = "ENI configuration file not found: " + config.eni_file; + return false; + } + + // Validate cycle time range + constexpr uint64_t MIN_CYCLE_TIME_US = 100; + constexpr uint64_t MAX_CYCLE_TIME_US = 100000; + if (config.cycle_time_us < MIN_CYCLE_TIME_US || config.cycle_time_us > MAX_CYCLE_TIME_US) { + config.error_message = "Invalid cycle time: " + std::to_string(config.cycle_time_us) + + " μs (valid range: " + std::to_string(MIN_CYCLE_TIME_US) + + "-" + std::to_string(MAX_CYCLE_TIME_US) + " μs)"; + return false; + } + + // Validate and correct RT priorities + constexpr uint32_t MIN_PRIORITY = 1; + constexpr uint32_t MAX_PRIORITY = 99; + constexpr uint32_t DEFAULT_RT_PRIORITY = 39; + constexpr uint32_t DEFAULT_JOB_PRIORITY = 98; + + if (config.rt_priority < MIN_PRIORITY || config.rt_priority > MAX_PRIORITY) { + HOLOSCAN_LOG_WARN("Invalid RT priority {}, using default {}", config.rt_priority, DEFAULT_RT_PRIORITY); + config.rt_priority = DEFAULT_RT_PRIORITY; + } + + if (config.job_thread_priority < MIN_PRIORITY || config.job_thread_priority > MAX_PRIORITY) { + HOLOSCAN_LOG_WARN("Invalid job thread priority {}, using default {}", config.job_thread_priority, DEFAULT_JOB_PRIORITY); + config.job_thread_priority = DEFAULT_JOB_PRIORITY; + } + + // Set defaults for zero values + if (config.max_acyc_frames == 0) { + config.max_acyc_frames = 32; + } + if (config.job_thread_stack_size == 0) { + config.job_thread_stack_size = 0x8000; + } + + return true; +} + + +} // namespace holocat diff --git a/applications/holocat/cpp/holocat_app.hpp b/applications/holocat/cpp/holocat_app.hpp new file mode 100644 index 0000000000..2a26fe6065 --- /dev/null +++ b/applications/holocat/cpp/holocat_app.hpp @@ -0,0 +1,28 @@ +/** + * @file holocat_app.hpp + * @brief HoloCat Application - EtherCAT integration with Holoscan + */ + +#pragma once + +#include +#include "holocat_op.hpp" + +namespace holocat { + +/** + * @brief HoloCat Application Class + * + * Orchestrates EtherCAT integration with periodic HolocatOp scheduling. + */ +class HolocatApp : public holoscan::Application { +public: + void compose() override; + + HolocatConfig extract_config(); + +private: + bool validate_config(HolocatConfig& config); +}; + +} // namespace holocat diff --git a/applications/holocat/cpp/holocat_op.cpp b/applications/holocat/cpp/holocat_op.cpp new file mode 100644 index 0000000000..9b69690587 --- /dev/null +++ b/applications/holocat/cpp/holocat_op.cpp @@ -0,0 +1,376 @@ +//----------------------------------------------------------------------------- +// holocat_op.cpp +// HoloCat EtherCAT Operator Implementation +// Based on EC-Master demo application from acontis technologies GmbH +//----------------------------------------------------------------------------- + +// System includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Third-party includes +#include + +// EtherCAT SDK includes +#include "EcOs.h" +#include "EcOsPlatform.h" +#include "EcType.h" + +// Local includes +#include "holocat_op.hpp" + +namespace holocat { +// EtherCAT logging callback function - maps EC-Master log levels to Holoscan +EC_T_DWORD HolocatOp::LogWrapper(struct _EC_T_LOG_CONTEXT* pContext, + EC_T_DWORD dwLogMsgSeverity, + const EC_T_CHAR* szFormat, ...) { + EC_T_VALIST vaArgs; + EC_VASTART(vaArgs, szFormat); + + char buffer[1024]; + const char* prefix = "[EC-Master] "; + strcpy(buffer, prefix); + vsnprintf(buffer + strlen(prefix), sizeof(buffer) - strlen(prefix), szFormat, vaArgs); + EC_VAEND(vaArgs); + + if (dwLogMsgSeverity == EC_LOG_LEVEL_SILENT) { + // Don't log + } else if (dwLogMsgSeverity == EC_LOG_LEVEL_CRITICAL) { + HOLOSCAN_LOG_CRITICAL("{}", buffer); + } else if (dwLogMsgSeverity == EC_LOG_LEVEL_ERROR) { + HOLOSCAN_LOG_ERROR("{}", buffer); + } else if (dwLogMsgSeverity == EC_LOG_LEVEL_WARNING) { + HOLOSCAN_LOG_WARN("{}", buffer); + } else if (dwLogMsgSeverity == EC_LOG_LEVEL_INFO) { + HOLOSCAN_LOG_INFO("{}", buffer); + } else { // VERBOSE_CYC (8), ANY (1) + HOLOSCAN_LOG_INFO("{}", buffer); + } + + return EC_E_NOERROR; +} + +// Initialize EtherCAT parameters +void HolocatOp::InitializeEthercatParams() { + + // Initialize logging parameters with LogWrapper callback + log_parms_ = {EC_LOG_LEVEL_VERBOSE, LogWrapper, EC_NULL}; + + // EtherCat link/interface parameters + OsMemset(&sockraw_params_, 0, sizeof(EC_T_LINK_PARMS_SOCKRAW)); + sockraw_params_.linkParms.dwSignature = EC_LINK_PARMS_SIGNATURE_SOCKRAW; + sockraw_params_.linkParms.dwSize = sizeof(EC_T_LINK_PARMS_SOCKRAW); + sockraw_params_.linkParms.LogParms.dwLogLevel = log_parms_.dwLogLevel; + sockraw_params_.linkParms.LogParms.pfLogMsg = log_parms_.pfLogMsg; + sockraw_params_.linkParms.LogParms.pLogContext = log_parms_.pLogContext; + OsStrncpy(sockraw_params_.linkParms.szDriverIdent, EC_LINK_PARMS_IDENT_SOCKRAW, EC_DRIVER_IDENT_MAXLEN); // Driver identity + sockraw_params_.linkParms.dwInstance = 1; + sockraw_params_.linkParms.eLinkMode = EcLinkMode_POLLING; // POLLING mode - no receiver thread + sockraw_params_.linkParms.dwIstPriority = 0; // Not used in polling mode + // Get adapter name from config or use default + std::string adapter_name = config_.adapter_name; + + if (config_.adapter_name.empty()) { + throw std::runtime_error("Adapter name is empty"); + } else { + // log message: "Adapter name: {}" + HOLOSCAN_LOG_INFO("Adapter name: {}", adapter_name); + } + + OsStrncpy(sockraw_params_.szAdapterName, adapter_name.c_str(), EC_SOCKRAW_ADAPTER_NAME_MAXLEN); + sockraw_params_.bDisableForceBroadcast = EC_TRUE; + sockraw_params_.bReplacePaddingWithNopCmd = EC_FALSE; + sockraw_params_.bUsePacketMmapRx = EC_TRUE; + sockraw_params_.bSetCoalescingParms = EC_FALSE; + sockraw_params_.bSetPromiscuousMode = EC_FALSE; + + // OS parameters + OsMemset(&os_parms_, 0, sizeof(EC_T_OS_PARMS)); + os_parms_.dwSignature = EC_OS_PARMS_SIGNATURE; + os_parms_.dwSize = sizeof(EC_T_OS_PARMS); + os_parms_.pLogParms = &(log_parms_); + os_parms_.dwSupportedFeatures = 0xFFFFFFFF; + // Don't configure mutex - avoids pthread conflicts + os_parms_.PlatformParms.bConfigMutex = EC_FALSE; + os_parms_.PlatformParms.nMutexType = PTHREAD_MUTEX_RECURSIVE; + os_parms_.PlatformParms.nMutexProtocol = PTHREAD_PRIO_NONE; + OsInit(&os_parms_); + + // EC-Master init parameters from config + OsMemset(&ec_master_init_parms_, 0, sizeof(EC_T_INIT_MASTER_PARMS)); + ec_master_init_parms_.dwSignature = ATECAT_SIGNATURE; + ec_master_init_parms_.dwSize = sizeof(EC_T_INIT_MASTER_PARMS); + ec_master_init_parms_.pOsParms = &os_parms_; + ec_master_init_parms_.pLinkParms = (EC_T_LINK_PARMS*)&sockraw_params_; + ec_master_init_parms_.pLinkParmsRed = EC_NULL; + ec_master_init_parms_.dwBusCycleTimeUsec = config_.cycle_time_us; + ec_master_init_parms_.dwMaxAcycFramesQueued = config_.max_acyc_frames; +} + +// Network configuration library call wrapper +EC_T_DWORD HolocatOp::ConfigureNetwork() +{ + EC_T_DWORD dwRes = EC_E_NOERROR; + // setup ENI configuration + EC_T_CNF_TYPE eCnfType = eCnfType_Filename; + EC_T_CHAR szENIFilename[256]; + // ENI file path from config or environment variable + // Get ENI file path from config or use default + std::string eni_file = config_.eni_file; + OsSnprintf(szENIFilename, sizeof(szENIFilename), "%s", eni_file.c_str()); + EC_T_BYTE* pbyCnfData = (EC_T_BYTE*)szENIFilename; + EC_T_DWORD dwCnfDataLen = (EC_T_DWORD)OsStrlen(szENIFilename); + + // configure network + dwRes = ecatConfigureNetwork(eCnfType, pbyCnfData, dwCnfDataLen); + if (dwRes != EC_E_NOERROR) + { + HOLOSCAN_LOG_ERROR("Cannot configure EtherCAT-Master: {} (0x{:x})", ecatGetText(dwRes), dwRes); + + return dwRes; + } else { + bHasConfig_ = EC_TRUE; + } + return dwRes; +} + + +void HolocatOp::setup(holoscan::OperatorSpec& spec) { + spec.input("count_in") + .condition(holoscan::ConditionType::kNone); // Make input optional + spec.output("count_out") + .condition(holoscan::ConditionType::kNone); // Make output optional +} + +void HolocatOp::start() +{ + HOLOSCAN_LOG_INFO("HolocatOp: Starting EtherCAT Master"); + InitializeEthercatParams(); + + // Setup EtherCAT Master + EC_T_DWORD dwRes = ecatInitMaster(&ec_master_init_parms_); + if (dwRes != EC_E_NOERROR) + { + HOLOSCAN_LOG_ERROR("Cannot initialize EtherCAT-Master: {} (0x{:x})", + ecatGetText(dwRes), dwRes); + throw std::runtime_error("Cannot initialize EtherCAT-Master: " + std::string(ecatGetText(dwRes))); + return; + } + HOLOSCAN_LOG_INFO("Master initialized"); + +} + +void HolocatOp::stop() +{ + HOLOSCAN_LOG_INFO("HolocatOp: stopping EtherCAT Master"); + // reset master to INIT + pending_state_transition_ = std::async(std::launch::async, [this]() { + return ecatSetMasterState(kEthercatStateChangeTimeout_, eEcatState_INIT); + }); + + sleep(1); + // deinitialize master + ecatDeinitMaster(); +} + + +void HolocatOp::BusStartupStateMachine() { + EC_T_DWORD dwRes = EC_E_NOERROR; + + switch (startup_state_) { + case StartupState::INIT: + startup_state_ = StartupState::CONFIGURE_NETWORK; + break; + + case StartupState::CONFIGURE_NETWORK: + HOLOSCAN_LOG_INFO("EtherCAT: Configuring network"); + dwRes = ConfigureNetwork(); + if (dwRes != EC_E_NOERROR) { + HOLOSCAN_LOG_ERROR("EtherCAT: Failed to configure network: {} (0x{:x})", ecatGetText(dwRes), dwRes); + return; + } + startup_state_ = StartupState::GET_PD_MEMORY_SIZE; + break; + + case StartupState::GET_PD_MEMORY_SIZE: + HOLOSCAN_LOG_INFO("EtherCAT: Getting process data memory size"); + dwRes = ecatIoCtl(EC_IOCTL_GET_PDMEMORYSIZE, EC_NULL, 0, + &oPdMemorySize_, sizeof(EC_T_MEMREQ_DESC), EC_NULL); + if (dwRes != EC_E_NOERROR) { + HOLOSCAN_LOG_ERROR("EtherCAT: Cannot get process data size: {} (0x{:x})", ecatGetText(dwRes), dwRes); + return; + } + startup_state_ = StartupState::SET_MASTER_INIT; + break; + + case StartupState::SET_MASTER_INIT: + if (!state_transition_in_progress_) { + HOLOSCAN_LOG_INFO("BusStartupStateMachine: Setting master state to INIT"); + pending_state_transition_ = std::async(std::launch::async, [this]() { + return ecatSetMasterState(kEthercatStateChangeTimeout_, eEcatState_INIT); + }); + state_transition_in_progress_ = true; + return; + } + if (pending_state_transition_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + state_transition_in_progress_ = false; + dwRes = pending_state_transition_.get(); + if (dwRes != EC_E_NOERROR) { + HOLOSCAN_LOG_ERROR("Cannot set master state to INIT: {} (0x{:x})", ecatGetText(dwRes), dwRes); + return; + } + startup_state_ = StartupState::SET_MASTER_PREOP; + } + break; + + case StartupState::SET_MASTER_PREOP: + if (!state_transition_in_progress_) { + HOLOSCAN_LOG_INFO("BusStartupStateMachine: Setting master state to PREOP"); + pending_state_transition_ = std::async(std::launch::async, [this]() { + return ecatSetMasterState(kEthercatStateChangeTimeout_, eEcatState_PREOP); + }); + state_transition_in_progress_ = true; + return; + } + if (pending_state_transition_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + state_transition_in_progress_ = false; + dwRes = pending_state_transition_.get(); + if (dwRes != EC_E_NOERROR) { + HOLOSCAN_LOG_ERROR("Cannot set master state to PREOP: {} (0x{:x})", ecatGetText(dwRes), dwRes); + return; + } + startup_state_ = StartupState::SET_MASTER_OP; + } + break; + + case StartupState::SET_MASTER_OP: + if (bHasConfig_) { + if (!state_transition_in_progress_) { + HOLOSCAN_LOG_INFO("BusStartupStateMachine: Setting master state to OP"); + pending_state_transition_ = std::async(std::launch::async, [this]() { + return ecatSetMasterState(kEthercatStateChangeTimeout_, eEcatState_OP); + }); + state_transition_in_progress_ = true; + return; + } + if (pending_state_transition_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + state_transition_in_progress_ = false; + dwRes = pending_state_transition_.get(); + if (dwRes != EC_E_NOERROR) { + HOLOSCAN_LOG_ERROR("Cannot set master state to OP: {} (0x{:x})", ecatGetText(dwRes), dwRes); + return; + } + startup_state_ = StartupState::OPERATIONAL; + } + } else { + HOLOSCAN_LOG_INFO("BusStartupStateMachine: No config available, skipping OP state"); + startup_state_ = StartupState::OPERATIONAL; + } + break; + + case StartupState::OPERATIONAL: + // Startup complete, nothing to do + break; + } +} + +void HolocatOp::compute(holoscan::InputContext& op_input, + holoscan::OutputContext& op_output, + holoscan::ExecutionContext& context) +{ + EC_T_STATE eMasterState = ecatGetMasterState(); + EC_T_DWORD dwRes = EC_E_NOERROR; + + // Read count_in input if available + int count_in_value = 0; + bool has_count_input = false; + + auto count_in_msg = op_input.receive("count_in"); + if (count_in_msg) { + count_in_value = count_in_msg.value(); + has_count_input = true; + HOLOSCAN_LOG_DEBUG("HolocatOp: received count_in: {}", count_in_value); + } + + // Step through asynchronous bus initialization states. + BusStartupStateMachine(); + + // PD output variables + unsigned short int inval = 0; + bool has_inval = false; + + // POLLING mode: Process received frames directly (no interrupt/event) + dwRes = ecatExecJob(eUsrJob_ProcessAllRxFrames, EC_NULL); + EC_UNREFPARM(dwRes); + + EC_T_BYTE* pbyPdIn = ecatGetProcessImageInputPtr(); + if (pbyPdIn) + { + EC_COPYBITS((EC_T_BYTE*)&inval, 0, pbyPdIn, kWagoDioInOffset, 16); + has_inval = true; + } + + // process data + if ((eEcatState_SAFEOP == eMasterState) || (eEcatState_OP == eMasterState)) + { + if (has_count_input) { + outval_ = count_in_value; + EC_T_BYTE* pbyPdOut = ecatGetProcessImageOutputPtr(); + EC_COPYBITS(pbyPdOut, kWagoDioOutOffset, (EC_T_BYTE*)&outval_, 0, kTwoBytes); + } + } else { + HOLOSCAN_LOG_DEBUG("."); + } + + EC_T_USER_JOB_PARMS oJobParms; + OsMemset(&oJobParms, 0, sizeof(EC_T_USER_JOB_PARMS)); + + // write output values of current cycle, by sending all cyclic frames + dwRes = ecatExecJob(eUsrJob_SendAllCycFrames, EC_NULL); + EC_UNREFPARM(dwRes); + + // execute some administrative jobs. No bus traffic is performed by this function + // Required for master state machine to run + dwRes = ecatExecJob(eUsrJob_MasterTimer, EC_NULL); + EC_UNREFPARM(dwRes); + + // send queued acyclic EtherCAT frames + dwRes = ecatExecJob(eUsrJob_SendAcycFrames, EC_NULL); + EC_UNREFPARM(dwRes); + + + // Performance monitoring and diagnostics + struct timespec t_now; + clock_gettime(CLOCK_MONOTONIC, &t_now); + static struct timespec t_last{t_now.tv_sec, t_now.tv_nsec}; + + // Calculate sample delay with wraparound handling at 256 + int sample_delay = outval_ - inval; + if (sample_delay < 0) { + sample_delay += 256; + } + + // Calculate elapsed time in milliseconds + double elapsed_time_ms = (t_now.tv_sec - t_last.tv_sec) * 1000.0 + + (double)(t_now.tv_nsec - t_last.tv_nsec) / 1000000.0; + t_last = t_now; + + // Log process data when in operational states + if ((eEcatState_SAFEOP == eMasterState) || (eEcatState_OP == eMasterState)) { + // emit count value read from ECat bus + if (has_inval) { + op_output.emit(inval, "count_out"); + } + HOLOSCAN_LOG_DEBUG("Elapsed time: {:.2f} ms\t\t Process data out: 0x{:02x} \tin: 0x{:02x} \t sample delay: {}", elapsed_time_ms, outval_, inval, sample_delay); + } +} + +} // namespace holocat diff --git a/applications/holocat/cpp/holocat_op.hpp b/applications/holocat/cpp/holocat_op.hpp new file mode 100644 index 0000000000..5529617acd --- /dev/null +++ b/applications/holocat/cpp/holocat_op.hpp @@ -0,0 +1,135 @@ +/*----------------------------------------------------------------------------- + * holocat_op.hpp + * HoloCat EtherCAT Operator for Holoscan SDK + * Based on EC-Master demo from acontis technologies GmbH + * + * This file defines the HolocatOp operator that integrates EtherCAT real-time + * communication with NVIDIA's Holoscan framework. + *---------------------------------------------------------------------------*/ + +#ifndef INC_HOLOCAT_H +#define INC_HOLOCAT_H 1 + +#define INCLUDE_EC_MASTER + +#include "EcMaster.h" +#include +#include +#include +#include + +namespace holocat { + +// Configuration structure for Holoscan integration +struct HolocatConfig { + std::string adapter_name; + std::string eni_file; + uint64_t cycle_time_us; + uint32_t rt_priority; + uint32_t job_thread_priority; + bool enable_rt; + uint32_t dio_out_offset; + uint32_t dio_in_offset; + uint32_t max_acyc_frames; + uint32_t job_thread_stack_size; + std::string log_level; + // For validation error reporting + std::string error_message; +}; + +/** + * @brief HoloCat EtherCAT Operator + * + * A Holoscan operator that provides real-time EtherCAT communication capabilities. + * This operator manages the complete EtherCAT master lifecycle including: + * - Network configuration and initialization + * - State machine management (INIT -> PREOP -> SAFEOP -> OP) + * - Cyclic process data exchange + * - Asynchronous state transitions to maintain real-time performance + * + * The operator runs as a source operator with periodic scheduling, executing + * EtherCAT communication cycles at regular intervals defined by PeriodicCondition. + */ +class HolocatOp : public holoscan::Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(HolocatOp) + + // Constructor that accepts configuration + HolocatOp() = default; + + // Set configuration + void set_config(const HolocatConfig& config) { config_ = config; } + + void setup(holoscan::OperatorSpec& spec) override; + void start() override; + void compute(holoscan::InputContext& op_input, + holoscan::OutputContext& op_output, + holoscan::ExecutionContext& context) override; + void stop() override; + + private: + // Configuration + HolocatConfig config_; + + // State machine for functions that need to be called while bus communication is also running. + void BusStartupStateMachine(); + + const static EC_T_DWORD kEthercatStateChangeTimeout_ = 3000; // master state change timeout in ms + + // Job task member variables (moved from global) + EC_T_MEMREQ_DESC oPdMemorySize_{0,0}; + + // Member function for job task + void EcMasterJobTask(); + + // Static function to interface with OsCreateThread + static EC_T_VOID EcMasterJobTaskStatic(EC_T_VOID* pvAppContext); + + // EtherCAT constants + static constexpr int kWagoDioOutOffset = 10 * 8; // Bit offset for Wago DIO output + static constexpr int kWagoDioInOffset = 18 * 8; // Bit offset for Wago DIO input + static constexpr uint64_t kUsPerMs = 1000000; // Microseconds per millisecond + static constexpr int kTwoBytes = 16; // 2 bytes bit length + + // EtherCAT parameter structs (moved from global) + EC_T_LOG_PARMS log_parms_; + EC_T_LINK_PARMS_SOCKRAW sockraw_params_; + EC_T_OS_PARMS os_parms_; + EC_T_INIT_MASTER_PARMS ec_master_init_parms_; + + // EtherCAT state variables + EC_T_BOOL bHasConfig_ = EC_FALSE; + EC_T_VOID* pvCycFrameReceivedEvent_ = EC_NULL; + + // Startup state machine variables + enum class StartupState { + INIT, + CONFIGURE_NETWORK, + GET_PD_MEMORY_SIZE, + SET_MASTER_INIT, + SET_MASTER_PREOP, + SET_MASTER_OP, + OPERATIONAL + }; + StartupState startup_state_ = StartupState::INIT; + + // Async state transition management + std::future pending_state_transition_; + bool state_transition_in_progress_ = false; + + // Helper methods + void InitializeEthercatParams(); + EC_T_DWORD ConfigureNetwork(); + static EC_T_DWORD LogWrapper(struct _EC_T_LOG_CONTEXT* pContext, + EC_T_DWORD dwLogMsgSeverity, + const EC_T_CHAR* szFormat, ...); + + + // Process data output value + int outval_ = 0; +}; + +} +#endif /* INC_HOLOCAT_H */ + +/*-END OF SOURCE FILE--------------------------------------------------------*/ diff --git a/applications/holocat/cpp/main.cpp b/applications/holocat/cpp/main.cpp new file mode 100644 index 0000000000..eb3cc23b35 --- /dev/null +++ b/applications/holocat/cpp/main.cpp @@ -0,0 +1,134 @@ +/** + * @file main.cpp + * @brief HoloCat Application Entry Point + * + * Main entry point for the HoloCat EtherCAT real-time integration application. + * Handles command line argument parsing, configuration loading, and application + * lifecycle management. + */ + +// System includes +#include +#include +#include + +// Third-party includes +#include + +// Local includes +#include "holocat_app.hpp" + +/** + * @brief Command line arguments structure + */ +struct CommandLineArgs { + std::string config_file; + bool print_config_only = false; + bool show_help = false; +}; + +/** + * @brief Print application usage information + * @param program_name Name of the program executable + */ +void print_usage(const char* program_name) { + std::cout << "Usage: " << program_name << " [OPTIONS]\n\n"; + std::cout << "HoloCat - EtherCAT Real-time Integration with Holoscan SDK\n\n"; + std::cout << "Options:\n"; + std::cout << " -c, --config FILE Load configuration from YAML file\n"; + std::cout << " -h, --help Show this help message\n"; + std::cout << " --print-config Print loaded configuration and exit\n\n"; + std::cout << "Examples:\n"; + std::cout << " " << program_name << " --config /path/to/config.yaml\n"; + std::cout << " " << program_name << " --print-config\n\n"; +} + + +/** + * @brief Parse command line arguments + * @param argc Argument count + * @param argv Argument values + * @return Parsed command line arguments structure + * @throws std::runtime_error if parsing fails + */ +CommandLineArgs parse_arguments(int argc, char** argv) { + CommandLineArgs args; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + args.show_help = true; + } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) { + if (i + 1 < argc) { + args.config_file = argv[++i]; + } else { + throw std::runtime_error("--config requires a file path"); + } + } else if (strcmp(argv[i], "--print-config") == 0) { + args.print_config_only = true; + } else { + throw std::runtime_error(std::string("Unknown option: ") + argv[i]); + } + } + + return args; +} + +int main(int argc, char** argv) { + HOLOSCAN_LOG_INFO("HoloCat - EtherCAT Real-time Integration"); + HOLOSCAN_LOG_INFO("Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES"); + + try { + // Parse command line arguments + CommandLineArgs args = parse_arguments(argc, argv); + + // Handle help request + if (args.show_help) { + print_usage(argv[0]); + return 0; + } + + // Create and configure the Holoscan application + auto app = holoscan::make_application(); + + // Load configuration file + std::filesystem::path config_path; + if (!args.config_file.empty()) { + config_path = args.config_file; + } else { + config_path = std::filesystem::canonical(argv[0]).parent_path() / "holocat_config.yaml"; + } + + if (!std::filesystem::exists(config_path)) { + throw std::runtime_error("Configuration file not found: " + config_path.string()); + } + app->config(config_path.string()); + + + // Handle configuration printing request + if (args.print_config_only) { + try { + auto config = app->extract_config(); + std::cout << "HoloCat Configuration:\n"; + std::cout << " Adapter: " << config.adapter_name << "\n"; + std::cout << " ENI File: " << config.eni_file << "\n"; + std::cout << " Cycle Time: " << config.cycle_time_us << " μs\n"; + std::cout << " RT Priority: " << config.rt_priority << "\n"; + // ... print other fields + } catch (const std::exception& e) { + throw std::runtime_error("Configuration error: " + std::string(e.what())); + } + return 0; + } + + // Run the main application + HOLOSCAN_LOG_INFO("Starting HoloCat application..."); + app->run(); + + + } catch (const std::exception& e) { + HOLOSCAN_LOG_ERROR("Application error: {}", e.what()); + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } +} + diff --git a/applications/holocat/metadata.json b/applications/holocat/metadata.json new file mode 100644 index 0000000000..a7bace8330 --- /dev/null +++ b/applications/holocat/metadata.json @@ -0,0 +1,81 @@ +{ + "application": { + "name": "HoloCat - EtherCAT Real-time Integration", + "authors": [ + { + "affiliation": "NVIDIA" + } + ], + "language": "C++", + "version": "1.0.0", + "changelog": { + "1.0.0": "Initial release with acontis EC-Master integration" + }, + "holoscan_sdk": { + "minimum_required_version": "3.0.0", + "tested_versions": [ + "3.0.0" + ] + }, + "dockerfile": "applications/holocat/Dockerfile", + "platforms": [ + "x86_64", + "aarch64" + ], + "tags": [ + "EtherCAT" + ], + "ranking": 2, + "requirements": { + "external": { + "ec-master-sdk": { + "version": "3.2.3+", + "description": "acontis EC-Master SDK (commercial license required)", + "url": "https://www.acontis.com/en/ethercat-master.html", + "license": "Commercial", + "required": true + } + }, + "build": { + "cmake_version": "3.20+", + "compiler": "GCC 9+ or Clang 10+", + "cuda": "12.0+" + }, + "system": { + "real-time-kernel": { + "description": "Linux kernel with real-time patches (PREEMPT_RT) recommended for best performance", + "required": false + }, + "capabilities": { + "description": "CAP_NET_RAW, CAP_SYS_NICE, CAP_IPC_LOCK - automatically set during build", + "required": true + }, + "network": { + "description": "EtherCAT-capable network interface (standard Ethernet adapter)", + "required": true + } + }, + "runtime": { + "ethercat_interface": "Standard Ethernet adapter configured for EtherCAT", + "permissions": "CAP_NET_RAW, CAP_SYS_NICE, CAP_IPC_LOCK capabilities" + } + }, + "default_mode": "standalone", + "modes": { + "standalone": { + "description": "Run HoloCat as standalone EtherCAT master application", + "run": { + "command": "/holocat", + "workdir": "holohub_bin" + } + }, + "with_config": { + "description": "Run HoloCat with custom configuration file", + "run": { + "command": "/holocat --config /holocat/holocat_config.yaml", + "workdir": "holohub_bin" + } + } + } + } +} diff --git a/applications/holocat/scripts/verify_ecmaster.sh b/applications/holocat/scripts/verify_ecmaster.sh new file mode 100755 index 0000000000..3ba22bc7ad --- /dev/null +++ b/applications/holocat/scripts/verify_ecmaster.sh @@ -0,0 +1,221 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# HoloCat EC-Master SDK Verification Script + +set -e + +echo "=============================================" +echo "HoloCat EC-Master SDK Verification" +echo "=============================================" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + local status=$1 + local message=$2 + if [ "$status" = "OK" ]; then + echo -e "${GREEN}✓${NC} $message" + elif [ "$status" = "WARN" ]; then + echo -e "${YELLOW}⚠${NC} $message" + else + echo -e "${RED}❌${NC} $message" + fi +} + +# Check for ECMASTER_ROOT environment variable +echo "1. Checking ECMASTER_ROOT environment variable..." +if [ -z "$ECMASTER_ROOT" ]; then + print_status "ERROR" "ECMASTER_ROOT environment variable not set" + echo " Set it to your EC-Master installation path:" + echo " export ECMASTER_ROOT=/path/to/ecm" + echo " Example: export ECMASTER_ROOT=/home/hking/devel/ethercat/ecm" + exit 1 +else + print_status "OK" "ECMASTER_ROOT set to: $ECMASTER_ROOT" +fi + +# Check if ECMASTER_ROOT directory exists +echo "" +echo "2. Checking EC-Master SDK directory structure..." +if [ ! -d "$ECMASTER_ROOT" ]; then + print_status "ERROR" "EC-Master root directory not found: $ECMASTER_ROOT" + exit 1 +else + print_status "OK" "EC-Master root directory exists" +fi + +# Check SDK include directory +if [ ! -d "$ECMASTER_ROOT/SDK/INC" ]; then + print_status "ERROR" "EC-Master SDK include directory not found: $ECMASTER_ROOT/SDK/INC" + exit 1 +else + print_status "OK" "SDK include directory found" +fi + +# Check for main header file +if [ ! -f "$ECMASTER_ROOT/SDK/INC/EcMaster.h" ]; then + print_status "ERROR" "EcMaster.h not found in: $ECMASTER_ROOT/SDK/INC/" + exit 1 +else + print_status "OK" "EcMaster.h header file found" +fi + +# Check for Linux-specific headers +if [ ! -f "$ECMASTER_ROOT/SDK/INC/Linux/EcOsPlatform.h" ]; then + print_status "WARN" "Linux platform headers not found (may be optional)" +else + print_status "OK" "Linux platform headers found" +fi + +# Determine architecture +ARCH="x64" +if [ "$(uname -m)" = "aarch64" ]; then + ARCH="arm64" +fi + +# Check for libraries +echo "" +echo "3. Checking EC-Master libraries (architecture: $ARCH)..." +LIB_DIR="$ECMASTER_ROOT/Bin/Linux/$ARCH" +if [ ! -d "$LIB_DIR" ]; then + print_status "ERROR" "Library directory not found: $LIB_DIR" + exit 1 +else + print_status "OK" "Library directory found" +fi + +# Check main library +if [ ! -f "$LIB_DIR/libEcMaster.so" ]; then + print_status "ERROR" "libEcMaster.so not found in: $LIB_DIR" + exit 1 +else + print_status "OK" "libEcMaster.so found" + # Get library info + LIB_SIZE=$(ls -lh "$LIB_DIR/libEcMaster.so" | awk '{print $5}') + echo " Library size: $LIB_SIZE" +fi + +# Check for link layer libraries +echo "" +echo "4. Checking EtherCAT link layer libraries..." +LINK_LIBS=("libemllSockRaw.so" "libemllDpdk.so" "libemllIntelGbe.so" "libemllRTL8169.so" "libemllVlan.so") +FOUND_LIBS=0 + +for lib in "${LINK_LIBS[@]}"; do + if [ -f "$LIB_DIR/$lib" ]; then + print_status "OK" "$lib found" + ((FOUND_LIBS++)) + else + print_status "WARN" "$lib not found (may be optional)" + fi +done + +if [ $FOUND_LIBS -eq 0 ]; then + print_status "ERROR" "No link layer libraries found" + exit 1 +else + print_status "OK" "$FOUND_LIBS link layer libraries available" +fi + +# Check version information +echo "" +echo "5. Checking EC-Master version..." +if [ -f "$ECMASTER_ROOT/EcVersion.txt" ]; then + VERSION=$(cat "$ECMASTER_ROOT/EcVersion.txt" 2>/dev/null || echo "Unknown") + print_status "OK" "Version file found: $VERSION" +elif [ -f "$ECMASTER_ROOT/SDK/INC/EcVersion.h" ]; then + # Extract version from header file + MAJ=$(grep "#define EC_VERSION_MAJ" "$ECMASTER_ROOT/SDK/INC/EcVersion.h" | awk '{print $3}') + MIN=$(grep "#define EC_VERSION_MIN" "$ECMASTER_ROOT/SDK/INC/EcVersion.h" | awk '{print $3}') + SP=$(grep "#define EC_VERSION_SERVICEPACK" "$ECMASTER_ROOT/SDK/INC/EcVersion.h" | awk '{print $3}') + BUILD=$(grep "#define EC_VERSION_BUILD" "$ECMASTER_ROOT/SDK/INC/EcVersion.h" | awk '{print $3}') + if [ -n "$MAJ" ] && [ -n "$MIN" ] && [ -n "$SP" ] && [ -n "$BUILD" ]; then + VERSION="$MAJ.$MIN.$SP.$BUILD" + print_status "OK" "Version extracted from header: $VERSION" + else + print_status "WARN" "Could not determine version" + fi +else + print_status "WARN" "Version information not found" +fi + +# Check system requirements +echo "" +echo "6. Checking system requirements..." + +# Check for required capabilities +if command -v getcap >/dev/null 2>&1; then + print_status "OK" "getcap utility available" +else + print_status "WARN" "getcap utility not found (install libcap2-bin)" +fi + +# Check for real-time kernel +if uname -r | grep -q rt; then + print_status "OK" "Real-time kernel detected: $(uname -r)" +else + print_status "WARN" "Standard kernel detected: $(uname -r)" + echo " For best performance, consider using a real-time kernel (PREEMPT_RT)" +fi + +# Check for network interfaces +echo "" +echo "7. Checking network interfaces..." +if command -v ip >/dev/null 2>&1; then + INTERFACES=$(ip link show | grep -E "^[0-9]+:" | grep -v "lo:" | wc -l) + if [ $INTERFACES -gt 0 ]; then + print_status "OK" "$INTERFACES network interfaces available" + echo " Available interfaces:" + ip link show | grep -E "^[0-9]+:" | grep -v "lo:" | awk '{print " - " $2}' | sed 's/:$//' + else + print_status "WARN" "No network interfaces found (excluding loopback)" + fi +else + print_status "WARN" "ip command not found" +fi + +# Check build tools +echo "" +echo "8. Checking build requirements..." +if command -v cmake >/dev/null 2>&1; then + CMAKE_VERSION=$(cmake --version | head -n1 | awk '{print $3}') + print_status "OK" "CMake found: $CMAKE_VERSION" +else + print_status "ERROR" "CMake not found (required for building)" +fi + +if command -v gcc >/dev/null 2>&1; then + GCC_VERSION=$(gcc --version | head -n1 | awk '{print $4}') + print_status "OK" "GCC found: $GCC_VERSION" +else + print_status "WARN" "GCC not found" +fi + +if command -v nvcc >/dev/null 2>&1; then + CUDA_VERSION=$(nvcc --version | grep "release" | awk '{print $6}' | sed 's/,$//') + print_status "OK" "CUDA found: $CUDA_VERSION" +else + print_status "WARN" "CUDA nvcc not found" +fi + +# Summary +echo "" +echo "=============================================" +echo "Verification Summary" +echo "=============================================" +print_status "OK" "EC-Master SDK verification completed successfully" +echo "" +echo "Next steps:" +echo "1. Build HoloCat: ./holohub build holocat" +echo "2. Configure network adapter in holocat_config.yaml" +echo "3. Create or obtain EtherCAT ENI configuration file" +echo "4. Run HoloCat: ./holohub run holocat" +echo "" +echo "For more information, see: applications/holocat/README.md"