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 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"