Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d4f92cf
Add initial extension
Apr 18, 2024
b4aeccc
Add configurable AMS Net Id and refresh rate
Apr 18, 2024
9e6a655
Add configurable AMS Net Id and refresh rate
Apr 18, 2024
7ccc8b5
Merge remote-tracking branch 'origin/main'
Apr 18, 2024
895fdad
Update some comments
Apr 18, 2024
916771c
Add pyads dependency
Joshpolansky Apr 22, 2024
6f43c0f
Add todo
Joshpolansky Apr 22, 2024
2269514
Add a launch.json
Joshpolansky Apr 22, 2024
af20e32
Fix data size
Joshpolansky Apr 22, 2024
a5da970
Add setting management
Joshpolansky Apr 23, 2024
4083ab1
Cleanup refresh function
Joshpolansky Apr 23, 2024
1fd4c53
Add support for lists of variables
Joshpolansky Apr 23, 2024
7d6a03a
build out api
Joshpolansky Apr 23, 2024
8d15bf6
Add basic API for apps to use
Joshpolansky Apr 23, 2024
63ce80b
Remove omni from the extension name
Joshpolansky Apr 23, 2024
214ecc6
return empty dict if no variables are given
Joshpolansky Apr 23, 2024
3f4ce52
Fix length check
Joshpolansky Apr 24, 2024
3a964ee
Update todo's
Joshpolansky Apr 24, 2024
dbca51a
Add images and readme
Joshpolansky Apr 24, 2024
1056f15
Add a README with usage instructions
Joshpolansky Apr 24, 2024
e948bb0
Add write_req unsubscribe
Joshpolansky Apr 24, 2024
4299b3f
Improve status message by showing an error for at least 1 second befo…
Joshpolansky Apr 24, 2024
94f584e
Remove event sender ID
Joshpolansky Apr 24, 2024
9c3929a
Update description
Joshpolansky Apr 24, 2024
1ec4eab
Update dev readme
Joshpolansky Apr 24, 2024
2fcc7e9
Add docs to Api
Joshpolansky Apr 24, 2024
a6623a4
Update API name
Joshpolansky Apr 24, 2024
d525710
Add docstring to AdsDriver
Joshpolansky Apr 24, 2024
55ad2da
Update API docs
Joshpolansky Apr 24, 2024
53fe69c
Merge branch 'feature/release_prep'
Joshpolansky Apr 24, 2024
65e9b0e
Fix the readme link
Joshpolansky Apr 24, 2024
365e00b
Apply suggestions from code review
Joshpolansky Apr 25, 2024
8e39865
Add readme and license files
agrayzel May 1, 2024
0999edc
Add file headers
agrayzel May 1, 2024
2a94f44
Add link to pyads
agrayzel May 2, 2024
7447284
Merge pull request #3 from loupeteam/feature/oss-release
agrayzel May 2, 2024
958fdb3
Remove isaac dependencies
May 15, 2024
ab65d60
Update extension menu text
May 16, 2024
c76d647
Add support for detecting when a connection is live
May 16, 2024
130e03d
Update the READMEs
May 16, 2024
21f6f66
Remove remaining references to IsaacSim
May 16, 2024
5b450bb
Update extension configuration
May 16, 2024
611ff97
Update repo structure to adhere to standard format
May 16, 2024
c055b44
Update README.md
AndrewMusser May 16, 2024
9d23a3a
Add intermediate 'simulation' folder to adhere to standard
May 16, 2024
c9d5ee1
Update README
May 16, 2024
bc98ff3
Add a "PLC" keyword
May 16, 2024
78832b2
Merge pull request #4 from loupeteam/release/prep
AndrewMusser May 16, 2024
602a08a
Add tests adapted from our other PLC extension
shanereetz Jul 24, 2024
d84386a
Add [[tests]] to extension TOML
shanereetz Jul 24, 2024
43a4f24
Integrate Dave Wiens' parsing algorithm into this project
shanereetz Jul 24, 2024
4f29128
Update unit tests with new parsing function name
shanereetz Jul 24, 2024
11d6979
Merge pull request #5 from loupeteam/feature/AddParseUnitTest
shanereetz Sep 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Beckhoff Bridge Extension for IsaacSim
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose we either remove this file, or include a link inside it to the actual README.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think it should be removed. This readme would be for the developers of the extension, where the inner one is for the users and gets packaged with the extension. This readme should probably have instructions for development.

16 changes: 16 additions & 0 deletions loupe.beckhoff_bridge/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Attach",
"type": "python",
"request": "attach",
"port": 3000,
"host": "127.0.0.1",
"justMyCode": false
},
]
}
29 changes: 29 additions & 0 deletions loupe.beckhoff_bridge/config/extension.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[core]
reloadable = true
order = 0

[package]
version = "0.1.0"
category = "Connector"
title = "Beckhoff Bridge"
description = "A client for connecting to Beckhoff PLCs"
authors = ["Loupe"]
repository = "https://github.com/loupeteam/IsaacSim_Beckhoff_Bridge_Extension"
keywords = []
changelog = "docs/CHANGELOG.md"
readme = "docs/README.md"
preview_image = "data/preview.png"
icon = "data/icon.png"


[dependencies]
"omni.kit.uiapp" = {}
"omni.isaac.ui" = {}
"omni.isaac.core" = {}

[python.pipapi]
requirements = ['pyads']
use_online_index = true

[[python.module]]
name = "loupe.beckhoff_bridge"
31 changes: 31 additions & 0 deletions loupe.beckhoff_bridge/loupe/beckhoff_bridge/Api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import carb.events
import omni.kit.app

EXTENSION_EVENT_SENDER_ID = 500
EVENT_TYPE_DATA_INIT = carb.events.type_from_string("loupe.beckhoff_bridge.DATA_INIT")
EVENT_TYPE_DATA_READ = carb.events.type_from_string("loupe.beckhoff_bridge.DATA_READ")
EVENT_TYPE_DATA_READ_REQ = carb.events.type_from_string("loupe.beckhoff_bridge.DATA_READ_REQ")
EVENT_TYPE_DATA_WRITE_REQ = carb.events.type_from_string("loupe.beckhoff_bridge.DATA_WRITE_REQ")

class BeckhoffBridge:
def __init__( self ):
self._event_stream = omni.kit.app.get_app().get_message_bus_event_stream()
self._callbacks = []

def __del__(self):
for callback in self._callbacks:
self._event_stream.remove_subscription(callback)

def register_init_callback(self, callback):
self._callbacks.append(self._event_stream.create_subscription_to_push_by_type(EVENT_TYPE_DATA_INIT, callback))
callback( None )

def register_data_callback(self, callback):
self._callbacks.append(self._event_stream.create_subscription_to_push_by_type(EVENT_TYPE_DATA_READ, callback))

def add_cyclic_read_variables(self, variableList):
self._event_stream.push(event_type=EVENT_TYPE_DATA_READ_REQ, payload={'variables':variableList})

def write_variable(self, name, value):
payload = {"variables":[{'name': name, 'value': value}]}
self._event_stream.push(event_type=EVENT_TYPE_DATA_WRITE_REQ, payload=payload)
1 change: 1 addition & 0 deletions loupe.beckhoff_bridge/loupe/beckhoff_bridge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .extension import *
65 changes: 65 additions & 0 deletions loupe.beckhoff_bridge/loupe/beckhoff_bridge/ads_driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pyads
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neumannjim do you have any thoughts on what license would be appropriate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the other sample files from NVIDIA already had a license on it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MIT! I'll see if @agrayzel can help give this the Open Loupe treatment.


class AdsDriver():

def __init__(self, ams_net_id):

self.ams_net_id = ams_net_id
self._read_names = list()
self._read_struct_def = dict()

def add_read(self, name : str, structure_def = None):

if(name not in self._read_names):
self._read_names.append(name)

if structure_def is not None:
if name not in self._read_struct_def:
self._read_struct_def[name] = structure_def

def write_data(self, data : dict ):
self._connection.write_list_by_name( data )

def read_data(self):
# self._connection.
if self._read_names is not None:
data = self._connection.read_list_by_name( self._read_names, structure_defs=self._read_struct_def)
parsed_data = dict()
for name in data.keys():
parsed_data = self._parse_name(parsed_data, name, data[name])
else:
parsed_data = dict()
return parsed_data

def _parse_name(self, name_dict, name, value):
name_parts = name.split(".")
if len(name_parts) > 1:
if name_parts[0] not in name_dict:
name_dict[name_parts[0]] = dict()
if "[" in name_parts[1]:
array_name, index = name_parts[1].split("[")
index = int(index[:-1])
if array_name not in name_dict[name_parts[0]]:
name_dict[name_parts[0]][array_name] = []
if index >= len(name_dict[name_parts[0]][array_name]):
name_dict[name_parts[0]][array_name].extend([None] * (index - len(name_dict[name_parts[0]][array_name]) + 1))
name_dict[name_parts[0]][array_name][index] = self._parse_name(name_dict[name_parts[0]][array_name], "[" + str(index) + "]" + ".".join(name_parts[2:]), value)
else:
name_dict[name_parts[0]] = self._parse_name(name_dict[name_parts[0]], ".".join(name_parts[1:]), value)
else:
if "[" in name_parts[0]:
array_name, index = name_parts[0].split("[")
index = int(index[:-1])
if index >= len(name_dict):
name_dict.extend([None] * (index - len(name_dict) + 1))
name_dict[index] = value
return name_dict[index]
else:
name_dict[name_parts[0]] = value
return name_dict

def connect(self):
self._connection = pyads.Connection(self.ams_net_id, pyads.PORT_TC3PLC1)
self._connection.open()


140 changes: 140 additions & 0 deletions loupe.beckhoff_bridge/loupe/beckhoff_bridge/extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# This software contains source code provided by NVIDIA Corporation.
# Copyright (c) 2022-2023, NVIDIA CORPORATION. All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto. Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#

import weakref
import asyncio
import gc
import omni
import omni.ui as ui
import omni.usd
import omni.timeline
import omni.kit.commands
from omni.kit.menu.utils import add_menu_items, remove_menu_items
from omni.isaac.ui.menu import make_menu_item_description
from omni.usd import StageEventType
import omni.physx as _physx

from .global_variables import EXTENSION_TITLE, EXTENSION_DESCRIPTION
from .ui_builder import UIBuilder

"""
This file serves as a basic template for the standard boilerplate operations
that make a UI-based extension appear on the toolbar.

This implementation is meant to cover most use-cases without modification.
Various callbacks are hooked up to a seperate class UIBuilder in .ui_builder.py
Most users will be able to make their desired UI extension by interacting solely with
UIBuilder.

This class sets up standard useful callback functions in UIBuilder:
on_menu_callback: Called when extension is opened
on_timeline_event: Called when timeline is stopped, paused, or played
on_stage_event: Called when stage is opened or closed
cleanup: Called when resources such as physics subscriptions should be cleaned up
build_ui: User function that creates the UI they want.
"""


class TestExtension(omni.ext.IExt):
def on_startup(self, ext_id: str):
"""Initialize extension and UI elements"""

# Events
self._usd_context = omni.usd.get_context()

# Build Window
self._window = ui.Window(
title=EXTENSION_TITLE, width=600, height=500, visible=False, dockPreference=ui.DockPreference.LEFT_BOTTOM
)
self._window.set_visibility_changed_fn(self._on_window)

# UI
self._models = {}
self._ext_id = ext_id
self._menu_items = [
make_menu_item_description(ext_id, EXTENSION_TITLE, lambda a=weakref.proxy(self): a._menu_callback())
]

add_menu_items(self._menu_items, EXTENSION_TITLE)

# Filled in with User Functions
self.ui_builder = UIBuilder()

# Events
self._usd_context = omni.usd.get_context()
self._physxIFace = _physx.acquire_physx_interface()
self._physx_subscription = None
self._stage_event_sub = None
self._timeline = omni.timeline.get_timeline_interface()

def on_shutdown(self):
self._models = {}
remove_menu_items(self._menu_items, EXTENSION_TITLE)
if self._window:
self._window = None
self.ui_builder.cleanup()
gc.collect()

def _on_window(self, visible):
if self._window.visible:
# Subscribe to Stage and Timeline Events
self._usd_context = omni.usd.get_context()
events = self._usd_context.get_stage_event_stream()
self._stage_event_sub = events.create_subscription_to_pop(self._on_stage_event)
stream = self._timeline.get_timeline_event_stream()
self._timeline_event_sub = stream.create_subscription_to_pop(self._on_timeline_event)

self._build_ui()
else:
self._usd_context = None
self._stage_event_sub = None
self._timeline_event_sub = None

def _build_ui(self):
with self._window.frame:
with ui.VStack(spacing=5, height=0):
self._build_extension_ui()

async def dock_window():
await omni.kit.app.get_app().next_update_async()

def dock(space, name, location, pos=0.5):
window = omni.ui.Workspace.get_window(name)
if window and space:
window.dock_in(space, location, pos)
return window

tgt = ui.Workspace.get_window("Viewport")
dock(tgt, EXTENSION_TITLE, omni.ui.DockPosition.LEFT, 0.33)
await omni.kit.app.get_app().next_update_async()

self._task = asyncio.ensure_future(dock_window())

#################################################################
# Functions below this point call user functions
#################################################################

def _menu_callback(self):
self._window.visible = not self._window.visible
self.ui_builder.on_menu_callback()

def _on_timeline_event(self, event):
self.ui_builder.on_timeline_event(event)

def _on_stage_event(self, event):
if event.type == int(StageEventType.OPENED) or event.type == int(StageEventType.CLOSED):
# stage was opened or closed, cleanup
self._physx_subscription = None

self.ui_builder.on_stage_event(event)

def _build_extension_ui(self):
# Call user function for building UI
self.ui_builder.build_ui()
15 changes: 15 additions & 0 deletions loupe.beckhoff_bridge/loupe/beckhoff_bridge/global_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This software contains source code provided by NVIDIA Corporation.
# Copyright (c) 2022-2023, NVIDIA CORPORATION. All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto. Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#


EXTENSION_TITLE = "beckhoff_bridge"
EXTENSION_NAME = "loupe.beckhoff_bridge"
EXTENSION_DESCRIPTION = "Connector for Beckhoff PLCs"

Loading