4343
4444**Note:** The data type you send must match the expected type of the target OPC UA variable.
4545
46+ ### Node Lookup Mode
47+ The block supports two methods for locating OPC UA nodes via the `node_lookup_mode` parameter:
48+
49+ - **`hierarchical` (default)**: Uses standard OPC UA hierarchical path navigation. The block navigates
50+ through the address space using `get_child()`. Each component in the `object_name` path is
51+ automatically prefixed with the namespace index.
52+ - **Example**: `object_name="Roboflow/Crane_11"` → path `0:Objects/2:Roboflow/2:Crane_11/2:Variable`
53+ - **Best for**: Traditional OPC UA servers with hierarchical address spaces
54+
55+ - **`direct`**: Uses direct NodeId string access. The block constructs a NodeId as
56+ `ns={namespace};s={object_name}/{variable_name}` and accesses it directly via `get_node()`.
57+ - **Example**: `object_name="[Sample_Tags]/Ramp"` → NodeId `ns=2;s=[Sample_Tags]/Ramp/South_Person_Count`
58+ - **Best for**: Ignition SCADA systems and other servers using string-based NodeId identifiers
59+
4660### Cooldown
4761To prevent excessive traffic to the OPC UA server, the block includes a `cooldown_seconds` parameter,
4862which defaults to **5 seconds**. During the cooldown period:
@@ -190,6 +204,15 @@ class BlockManifest(WorkflowBlockManifest):
190204 },
191205 examples = [10 , "$inputs.cooldown_seconds" ],
192206 )
207+ node_lookup_mode : Union [
208+ Selector (kind = [STRING_KIND ]),
209+ Literal ["hierarchical" , "direct" ],
210+ ] = Field (
211+ default = "hierarchical" ,
212+ description = "Method to locate the OPC UA node: 'hierarchical' uses path navigation, "
213+ "'direct' uses NodeId strings (for Ignition-style string-based tags)." ,
214+ examples = ["hierarchical" , "direct" ],
215+ )
193216
194217 @classmethod
195218 def describe_outputs (cls ) -> List [OutputDefinition ]:
@@ -238,8 +261,10 @@ def run(
238261 fire_and_forget : bool = True ,
239262 disable_sink : bool = False ,
240263 cooldown_seconds : int = 5 ,
264+ node_lookup_mode : Literal ["hierarchical" , "direct" ] = "hierarchical" ,
241265 ) -> BlockResult :
242266 if disable_sink :
267+ logging .debug ("OPC Writer disabled by disable_sink parameter" )
243268 return {
244269 "disabled" : True ,
245270 "throttling_status" : False ,
@@ -261,6 +286,7 @@ def run(
261286 }
262287
263288 value_str = str (value )
289+ logging .debug (f"OPC Writer converting value '{ value_str } ' to type { value_type } " )
264290 try :
265291 if value_type in [BOOLEAN_KIND , "Boolean" ]:
266292 decoded_value = value_str .strip ().lower () in ("true" , "1" )
@@ -272,7 +298,9 @@ def run(
272298 decoded_value = value_str
273299 else :
274300 raise ValueError (f"Unsupported value type: { value_type } " )
301+ logging .debug (f"OPC Writer successfully converted value to { decoded_value } " )
275302 except ValueError as exc :
303+ logging .error (f"OPC Writer failed to convert value: { exc } " )
276304 return {
277305 "disabled" : False ,
278306 "error_status" : True ,
@@ -290,9 +318,11 @@ def run(
290318 variable_name = variable_name ,
291319 value = decoded_value ,
292320 timeout = timeout ,
321+ node_lookup_mode = node_lookup_mode ,
293322 )
294323 self ._last_notification_fired = datetime .now ()
295324 if fire_and_forget and self ._background_tasks :
325+ logging .debug ("OPC Writer submitting write task to background tasks" )
296326 self ._background_tasks .add_task (opc_writer_handler )
297327 return {
298328 "disabled" : False ,
@@ -301,14 +331,19 @@ def run(
301331 "message" : "Writing to the OPC UA server in the background task" ,
302332 }
303333 if fire_and_forget and self ._thread_pool_executor :
334+ logging .debug ("OPC Writer submitting write task to thread pool executor" )
304335 self ._thread_pool_executor .submit (opc_writer_handler )
305336 return {
306337 "disabled" : False ,
307338 "error_status" : False ,
308339 "throttling_status" : False ,
309340 "message" : "Writing to the OPC UA server in the background task" ,
310341 }
342+ logging .debug ("OPC Writer executing synchronous write" )
311343 error_status , message = opc_writer_handler ()
344+ logging .debug (
345+ f"OPC Writer write completed: error_status={ error_status } , message={ message } "
346+ )
312347 return {
313348 "disabled" : False ,
314349 "error_status" : error_status ,
@@ -317,6 +352,30 @@ def run(
317352 }
318353
319354
355+ def get_available_namespaces (client : Client ) -> List [str ]:
356+ """
357+ Get list of available namespaces from OPC UA server.
358+ Returns empty list if unable to fetch namespaces.
359+ """
360+ try :
361+ get_namespace_array = sync_async_client_method (AsyncClient .get_namespace_array )(
362+ client
363+ )
364+ return get_namespace_array ()
365+ except Exception as exc :
366+ logging .info (f"Failed to get namespace array (non-fatal): { exc } " )
367+ return ["<unable to fetch namespaces>" ]
368+
369+
370+ def safe_disconnect (client : Client ) -> None :
371+ """Safely disconnect from OPC UA server, swallowing any errors"""
372+ try :
373+ logging .debug ("OPC Writer disconnecting from server" )
374+ client .disconnect ()
375+ except Exception as exc :
376+ logging .debug (f"OPC Writer disconnect error (non-fatal): { exc } " )
377+
378+
320379def opc_connect_and_write_value (
321380 url : str ,
322381 namespace : str ,
@@ -326,7 +385,11 @@ def opc_connect_and_write_value(
326385 variable_name : str ,
327386 value : Union [bool , float , int , str ],
328387 timeout : int ,
388+ node_lookup_mode : Literal ["hierarchical" , "direct" ] = "hierarchical" ,
329389) -> Tuple [bool , str ]:
390+ logging .debug (
391+ f"OPC Writer attempting to connect and write value={ value } to { url } /{ object_name } /{ variable_name } "
392+ )
330393 try :
331394 _opc_connect_and_write_value (
332395 url = url ,
@@ -337,9 +400,14 @@ def opc_connect_and_write_value(
337400 variable_name = variable_name ,
338401 value = value ,
339402 timeout = timeout ,
403+ node_lookup_mode = node_lookup_mode ,
404+ )
405+ logging .debug (
406+ f"OPC Writer successfully wrote value to { url } /{ object_name } /{ variable_name } "
340407 )
341408 return False , "Value set successfully"
342409 except Exception as exc :
410+ logging .error (f"OPC Writer failed to write value: { exc } " )
343411 return (
344412 True ,
345413 f"Failed to write { value } to { object_name } :{ variable_name } in { url } . Internal error details: { exc } ." ,
@@ -355,21 +423,28 @@ def _opc_connect_and_write_value(
355423 variable_name : str ,
356424 value : Union [bool , float , int , str ],
357425 timeout : int ,
426+ node_lookup_mode : Literal ["hierarchical" , "direct" ] = "hierarchical" ,
358427):
428+ logging .debug (f"OPC Writer creating client for { url } with timeout={ timeout } " )
359429 client = Client (url = url , sync_wrapper_timeout = timeout )
360430 if user_name and password :
361431 client .set_user (user_name )
362432 client .set_password (password )
363433 try :
434+ logging .debug (f"OPC Writer connecting to { url } " )
364435 client .connect ()
436+ logging .debug ("OPC Writer successfully connected to server" )
365437 except BadUserAccessDenied as exc :
366- client .disconnect ()
438+ logging .error (f"OPC Writer authentication failed: { exc } " )
439+ safe_disconnect (client )
367440 raise Exception (f"AUTH ERROR: { exc } " )
368441 except OSError as exc :
369- client .disconnect ()
442+ logging .error (f"OPC Writer network error during connection: { exc } " )
443+ safe_disconnect (client )
370444 raise Exception (f"NETWORK ERROR: { exc } " )
371445 except Exception as exc :
372- client .disconnect ()
446+ logging .error (f"OPC Writer unhandled connection error: { type (exc )} { exc } " )
447+ safe_disconnect (client )
373448 raise Exception (f"UNHANDLED ERROR: { type (exc )} { exc } " )
374449 get_namespace_index = sync_async_client_method (AsyncClient .get_namespace_index )(
375450 client
@@ -378,33 +453,79 @@ def _opc_connect_and_write_value(
378453 try :
379454 if namespace .isdigit ():
380455 nsidx = int (namespace )
456+ logging .debug (f"OPC Writer using numeric namespace index: { nsidx } " )
381457 else :
382458 nsidx = get_namespace_index (namespace )
383459 except ValueError as exc :
384- client .disconnect ()
385- raise Exception (f"WRONG NAMESPACE ERROR: { exc } " )
386- except Exception as exc :
387- client .disconnect ()
388- raise Exception (f"UNHANDLED ERROR: { type (exc )} { exc } " )
389-
390- try :
391- var = client .nodes .root .get_child (
392- f"0:Objects/{ nsidx } :{ object_name } /{ nsidx } :{ variable_name } "
460+ namespaces = get_available_namespaces (client )
461+ logging .error (f"OPC Writer invalid namespace: { exc } " )
462+ logging .error (f"Available namespaces: { namespaces } " )
463+ safe_disconnect (client )
464+ raise Exception (
465+ f"WRONG NAMESPACE ERROR: { exc } . Available namespaces: { namespaces } "
393466 )
394- except BadNoMatch as exc :
395- client .disconnect ()
396- raise Exception (f"WRONG OBJECT OR PROPERTY ERROR: { exc } " )
397467 except Exception as exc :
398- client .disconnect ()
399- raise Exception (f"UNHANDLED ERROR: { type (exc )} { exc } " )
468+ namespaces = get_available_namespaces (client )
469+ logging .error (f"OPC Writer unhandled namespace error: { type (exc )} { exc } " )
470+ logging .error (f"Available namespaces: { namespaces } " )
471+ safe_disconnect (client )
472+ raise Exception (
473+ f"UNHANDLED ERROR: { type (exc )} { exc } . Available namespaces: { namespaces } "
474+ )
475+
476+ if node_lookup_mode == "direct" :
477+ # Direct NodeId access for Ignition-style string identifiers
478+ try :
479+ node_id = f"ns={ nsidx } ;s={ object_name } /{ variable_name } "
480+ logging .debug (f"OPC Writer using direct NodeId access: { node_id } " )
481+ var = client .get_node (node_id )
482+ # Verify the node exists by reading its attributes
483+ var .read_browse_name ()
484+ logging .debug (
485+ f"OPC Writer successfully found variable node using direct NodeId"
486+ )
487+ except Exception as exc :
488+ logging .error (f"OPC Writer direct NodeId access failed: { exc } " )
489+ safe_disconnect (client )
490+ raise Exception (
491+ f"WRONG OBJECT OR PROPERTY ERROR: Could not find node with direct NodeId '{ node_id } '. Error: { exc } "
492+ )
493+ else :
494+ # Hierarchical path navigation (standard OPC UA)
495+ try :
496+ # Split object_name on "/" and prepend namespace index to each component
497+ object_components = object_name .split ("/" )
498+ object_path = "/" .join ([f"{ nsidx } :{ comp } " for comp in object_components ])
499+ node_path = f"0:Objects/{ object_path } /{ nsidx } :{ variable_name } "
500+ logging .debug (f"OPC Writer using hierarchical path: { node_path } " )
501+ var = client .nodes .root .get_child (node_path )
502+ logging .debug (
503+ f"OPC Writer successfully found variable node using hierarchical path"
504+ )
505+ except BadNoMatch as exc :
506+ logging .error (f"OPC Writer hierarchical path not found: { exc } " )
507+ safe_disconnect (client )
508+ raise Exception (
509+ f"WRONG OBJECT OR PROPERTY ERROR: Could not find node at hierarchical path '{ node_path } '. Error: { exc } "
510+ )
511+ except Exception as exc :
512+ logging .error (f"OPC Writer unhandled node lookup error: { type (exc )} { exc } " )
513+ safe_disconnect (client )
514+ raise Exception (f"UNHANDLED ERROR: { type (exc )} { exc } " )
400515
401516 try :
517+ logging .debug (f"OPC Writer writing value '{ value } ' to variable" )
402518 var .write_value (value )
519+ logging .info (
520+ f"OPC Writer successfully wrote '{ value } ' to variable at { object_name } /{ variable_name } "
521+ )
403522 except BadTypeMismatch as exc :
404- client .disconnect ()
523+ logging .error (f"OPC Writer type mismatch error: { exc } " )
524+ safe_disconnect (client )
405525 raise Exception (f"WRONG TYPE ERROR: { exc } " )
406526 except Exception as exc :
407- client .disconnect ()
527+ logging .error (f"OPC Writer unhandled write error: { type (exc )} { exc } " )
528+ safe_disconnect (client )
408529 raise Exception (f"UNHANDLED ERROR: { type (exc )} { exc } " )
409530
410- client . disconnect ( )
531+ safe_disconnect ( client )
0 commit comments