From a7576ffb38290187c7801f758fd9b5f05e4b639c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 25 Mar 2026 05:55:27 -0400 Subject: [PATCH] =?UTF-8?q?Implement=20LmxOpcUa=20server=20=E2=80=94=20all?= =?UTF-8?q?=206=20phases=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full OPC UA server on .NET Framework 4.8 (x86) exposing AVEVA System Platform Galaxy tags via MXAccess. Mirrors Galaxy object hierarchy as OPC UA address space, translating contained-name browse paths to tag-name runtime references. Components implemented: - Configuration: AppConfiguration with 4 sections, validator - Domain: ConnectionState, Quality, Vtq, MxDataTypeMapper, error codes - MxAccess: StaComThread, MxAccessClient (partial classes), MxProxyAdapter using strongly-typed ArchestrA.MxAccess COM interop - Galaxy Repository: SQL queries (hierarchy, attributes, change detection), ChangeDetectionService with auto-rebuild on deploy - OPC UA Server: LmxNodeManager (CustomNodeManager2), LmxOpcUaServer, OpcUaServerHost with programmatic config, SecurityPolicy None - Status Dashboard: HTTP server with HTML/JSON/health endpoints - Integration: Full 14-step startup, graceful shutdown, component wiring 175 tests (174 unit + 1 integration), all passing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 29 + CLAUDE.md | 113 + ZB.MOM.WW.LmxOpcUa.slnx | 9 + dashboard.JPG | Bin 0 -> 69646 bytes docs/implementation-plan.md | 384 ++ docs/reqs/GalaxyRepositoryReqs.md | 121 + docs/reqs/HighLevelReqs.md | 47 + docs/reqs/MxAccessClientReqs.md | 172 + docs/reqs/OpcUaServerReqs.md | 229 ++ docs/reqs/ServiceHostReqs.md | 117 + docs/reqs/StatusDashboardReqs.md | 157 + gr/CLAUDE.md | 51 + gr/build_layout_plan.md | 84 + gr/connectioninfo.md | 26 + gr/data_type_mapping.md | 80 + gr/ddl/tables/ConversionQueue.sql | 13 + gr/ddl/tables/CurrentSessionContainedName.sql | 9 + gr/ddl/tables/ImportTransaction.sql | 7 + gr/ddl/tables/aa_sql_objects.sql | 8 + gr/ddl/tables/affected_overview_symbols.sql | 9 + gr/ddl/tables/alarm_message_defaults.sql | 8 + gr/ddl/tables/alarm_message_timestamps.sql | 8 + gr/ddl/tables/alarm_message_translations.sql | 12 + gr/ddl/tables/alarm_messages.sql | 13 + gr/ddl/tables/attribute_definition.sql | 24 + gr/ddl/tables/attribute_reference.sql | 26 + .../tables/attributes_translation_table.sql | 11 + gr/ddl/tables/autobind_device.sql | 11 + gr/ddl/tables/autobind_device_category.sql | 11 + gr/ddl/tables/autobind_device_template.sql | 11 + gr/ddl/tables/autobind_device_topic.sql | 13 + gr/ddl/tables/autobind_naming_rule.sql | 8 + gr/ddl/tables/autobind_naming_rule_spec.sql | 12 + gr/ddl/tables/autobind_translation_rule.sql | 10 + gr/ddl/tables/autobound_attribute.sql | 17 + gr/ddl/tables/client_control_class_link.sql | 9 + gr/ddl/tables/client_info.sql | 11 + gr/ddl/tables/control_index.sql | 16 + gr/ddl/tables/data_type.sql | 9 + gr/ddl/tables/deleted_gobject.sql | 8 + gr/ddl/tables/deleted_ids.sql | 9 + gr/ddl/tables/deleted_visual_element.sql | 8 + .../tables/deleted_visual_element_version.sql | 13 + gr/ddl/tables/deployed_file.sql | 19 + gr/ddl/tables/deployed_intouch_viewapp.sql | 8 + ...ouch_viewapp_visual_element_dependency.sql | 7 + gr/ddl/tables/dynamic_attribute.sql | 25 + .../tables/external_content_media_types.sql | 12 + gr/ddl/tables/feature.sql | 9 + gr/ddl/tables/feature_file_link.sql | 11 + gr/ddl/tables/file_browserinfo_link.sql | 13 + gr/ddl/tables/file_pending_update.sql | 11 + .../tables/file_primitive_definition_link.sql | 15 + gr/ddl/tables/file_table.sql | 13 + gr/ddl/tables/folder.sql | 14 + gr/ddl/tables/folder_gobject_link.sql | 13 + gr/ddl/tables/galaxy.sql | 18 + gr/ddl/tables/galaxy_data.sql | 7 + gr/ddl/tables/galaxy_settings.sql | 8 + gr/ddl/tables/gobject.sql | 35 + gr/ddl/tables/gobject_asset_order.sql | 11 + gr/ddl/tables/gobject_change_log.sql | 16 + .../tables/gobject_filter_info_timestamp.sql | 11 + gr/ddl/tables/gobject_friendly_name.sql | 11 + gr/ddl/tables/gobject_log_details.sql | 7 + gr/ddl/tables/gobject_protected.sql | 6 + gr/ddl/tables/instance.sql | 13 + .../intouchviewapptemplate_allsymbols.sql | 6 + gr/ddl/tables/lookup_category.sql | 8 + gr/ddl/tables/lookup_folder.sql | 7 + gr/ddl/tables/lookup_operation.sql | 9 + gr/ddl/tables/lookup_package_op_status.sql | 8 + gr/ddl/tables/lookup_status.sql | 8 + gr/ddl/tables/lookup_table_name.sql | 7 + gr/ddl/tables/namespace.sql | 8 + gr/ddl/tables/object_device_linkage.sql | 9 + .../tables/object_wizard_overview_symbols.sql | 9 + .../tables/object_wizard_symbol_override.sql | 12 + .../object_wizard_symbol_override_mapping.sql | 15 + gr/ddl/tables/old_checked_in_packages.sql | 9 + gr/ddl/tables/operation.sql | 13 + gr/ddl/tables/operation_message.sql | 13 + gr/ddl/tables/operation_status.sql | 11 + gr/ddl/tables/operation_status_look_up.sql | 8 + gr/ddl/tables/ow_group_def.sql | 16 + gr/ddl/tables/ow_group_id.sql | 7 + gr/ddl/tables/ow_group_override.sql | 13 + gr/ddl/tables/ow_instance_setting.sql | 17 + gr/ddl/tables/ow_link_def.sql | 19 + gr/ddl/tables/ow_link_id.sql | 11 + gr/ddl/tables/ow_lu_definition.sql | 17 + gr/ddl/tables/ow_lu_setting.sql | 18 + gr/ddl/tables/ow_opt_or_choice_def.sql | 21 + gr/ddl/tables/ow_opt_or_choice_id.sql | 11 + gr/ddl/tables/ow_opt_or_choice_override.sql | 14 + gr/ddl/tables/ow_setting_def.sql | 18 + gr/ddl/tables/ow_setting_id.sql | 11 + gr/ddl/tables/ow_setting_override.sql | 14 + gr/ddl/tables/ow_symbol_setting.sql | 17 + gr/ddl/tables/owned_visual_element.sql | 21 + gr/ddl/tables/package.sql | 21 + gr/ddl/tables/packages_to_be_deleted.sql | 8 + gr/ddl/tables/platform.sql | 21 + gr/ddl/tables/platform_license.sql | 11 + ...primitive_attribute_validation_results.sql | 15 + gr/ddl/tables/primitive_data_type.sql | 11 + gr/ddl/tables/primitive_definition.sql | 21 + gr/ddl/tables/primitive_instance.sql | 34 + .../primitive_instance_feature_link.sql | 15 + .../primitive_instance_file_table_link.sql | 16 + gr/ddl/tables/proxy_timestamp.sql | 9 + gr/ddl/tables/redundancy.sql | 8 + gr/ddl/tables/renamed_visual_element.sql | 14 + gr/ddl/tables/schema_version.sql | 9 + gr/ddl/tables/supported_locales.sql | 7 + gr/ddl/tables/template.sql | 11 + gr/ddl/tables/template_attribute.sql | 20 + gr/ddl/tables/template_definition.sql | 20 + gr/ddl/tables/template_idebehavior_link.sql | 22 + gr/ddl/tables/template_migration_policy.sql | 7 + gr/ddl/tables/timestamp_record.sql | 6 + gr/ddl/tables/toolset.sql | 8 + gr/ddl/tables/user_preferences.sql | 13 + gr/ddl/tables/user_profile.sql | 22 + gr/ddl/tables/visual_element.sql | 10 + ...ual_element_affected_by_undo_check_out.sql | 11 + gr/ddl/tables/visual_element_archive.sql | 12 + gr/ddl/tables/visual_element_id.sql | 6 + gr/ddl/tables/visual_element_reference.sql | 37 + gr/ddl/tables/visual_element_timestamp.sql | 11 + gr/ddl/tables/visual_element_version.sql | 17 + gr/ddl/tables/well_known_client_controls.sql | 8 + gr/ddl/views/internal_all_alarms_view.sql | 11 + gr/ddl/views/internal_all_view_app_view.sql | 10 + ...automation_object_model_hierarchy_view.sql | 9 + ...utomation_object_model_hierarchy_view2.sql | 9 + ...n_object_visual_element_hierarchy_view.sql | 9 + ...lement_hierarchy_view_includetemplates.sql | 9 + ...relative_visual_element_reference_view.sql | 14 + ...relative_visual_element_reference_view.sql | 14 + gr/ddl/views/internal_common_obj.sql | 13 + gr/ddl/views/internal_control_view.sql | 15 + ...internal_get_GRMblob_preview_user_view.sql | 8 + ...l_get_TemplateToolBox_Allelements_view.sql | 10 + ...ernal_get_TemplateToolBox_folders_view.sql | 15 + ...l_get_asset_graphics_preview_user_view.sql | 9 + ...t_attribute_definition_with_validation.sql | 12 + .../internal_get_device_scangroups_view.sql | 12 + ..._get_external_content_media_types_view.sql | 15 + .../internal_get_gtb_Allelements_view.sql | 14 + .../views/internal_get_gtb_elements_view.sql | 14 + .../views/internal_get_gtb_folders_view.sql | 13 + ...ual_element_references_deployment_view.sql | 11 + ...visual_element_references_preview_view.sql | 11 + ...get_gtb_visual_element_references_view.sql | 11 + ...d_symbol_override_mapping_preview_view.sql | 9 + ...ct_wizard_symbol_override_mapping_view.sql | 9 + ...ct_wizard_symbol_override_preview_view.sql | 9 + ...get_object_wizard_symbol_override_view.sql | 9 + ...ual_element_primitives_deployment_view.sql | 8 + ...nal_get_visual_element_primitives_view.sql | 11 + .../internal_gtb_symbols_hierarchy_view.sql | 12 + ...isual_element_definition_per_user_view.sql | 9 + .../internal_list_automation_objects_view.sql | 13 + .../internal_list_model_objects_view.sql | 18 + gr/ddl/views/internal_list_objects_view.sql | 9 + .../internal_list_unassigned_objects_view.sql | 16 + .../internal_localized_alarm_messages.sql | 7 + .../internal_model_hierarchy_asset_view.sql | 12 + gr/ddl/views/internal_package_status_view.sql | 16 + gr/ddl/views/internal_proxy_obj.sql | 12 + ...internal_reference_primitive_attribute.sql | 9 + .../internal_required_support_features.sql | 9 + gr/ddl/views/internal_runtime_attributes.sql | 13 + ...nternal_visible_packages_per_user_view.sql | 12 + ..._element_description_all_packages_view.sql | 13 + ...sual_element_description_per_user_view.sql | 9 + ...ternal_visual_element_description_view.sql | 13 + ...l_element_primitives_preview_user_view.sql | 8 + ...visual_element_reference_per_user_view.sql | 12 + ...internal_visual_element_reference_view.sql | 13 + ..._element_reference_warning_status_view.sql | 15 + ...visual_element_timestamp_per_user_view.sql | 15 + gr/ddl/views/public_gobject_definition.sql | 7 + gr/hierarchy.jpeg | Bin 0 -> 16788 bytes gr/layout.md | 78 + gr/parse_tables.py | 122 + gr/queries/attributes.sql | 63 + gr/queries/attributes_extended.sql | 134 + gr/queries/change_detection.sql | 8 + gr/queries/hierarchy.sql | 43 + gr/schema.md | 2204 ++++++++++ gr/tags.JPG | Bin 0 -> 28675 bytes lib/ArchestrA.MXAccess.dll | Bin 0 -> 34968 bytes mxaccess_documentation.md | 3619 +++++++++++++++++ session.dat | 1 + .../Configuration/AppConfiguration.cs | 13 + .../Configuration/ConfigurationValidator.cs | 69 + .../Configuration/DashboardConfiguration.cs | 12 + .../GalaxyRepositoryConfiguration.cs | 12 + .../Configuration/MxAccessConfiguration.cs | 19 + .../Configuration/OpcUaConfiguration.cs | 15 + .../Domain/ConnectionState.cs | 15 + .../Domain/ConnectionStateChangedEventArgs.cs | 21 + .../Domain/GalaxyAttributeInfo.cs | 17 + .../Domain/GalaxyObjectInfo.cs | 15 + .../Domain/IGalaxyRepository.cs | 20 + .../Domain/IMxAccessClient.cs | 29 + .../Domain/IMxProxy.cs | 41 + .../Domain/MxDataTypeMapper.cs | 81 + .../Domain/MxErrorCodes.cs | 43 + src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Quality.cs | 36 + .../Domain/QualityMapper.cs | 54 + src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Vtq.cs | 32 + .../ChangeDetectionService.cs | 96 + .../GalaxyRepositoryService.cs | 199 + .../GalaxyRepository/GalaxyRepositoryStats.cs | 17 + .../Metrics/PerformanceMetrics.cs | 181 + .../MxAccess/MxAccessClient.Connection.cs | 113 + .../MxAccess/MxAccessClient.EventHandlers.cs | 101 + .../MxAccess/MxAccessClient.Monitor.cs | 65 + .../MxAccess/MxAccessClient.ReadWrite.cs | 139 + .../MxAccess/MxAccessClient.Subscription.cs | 89 + .../MxAccess/MxAccessClient.cs | 92 + .../MxAccess/MxProxyAdapter.cs | 69 + .../MxAccess/StaComThread.cs | 226 + .../OpcUa/AddressSpaceBuilder.cs | 119 + .../OpcUa/DataValueConverter.cs | 80 + .../OpcUa/LmxNodeManager.cs | 369 ++ .../OpcUa/LmxOpcUaServer.cs | 59 + .../OpcUa/OpcUaQualityMapper.cs | 23 + .../OpcUa/OpcUaServerHost.cs | 165 + src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs | 285 ++ src/ZB.MOM.WW.LmxOpcUa.Host/Program.cs | 53 + .../Status/HealthCheckService.cs | 57 + .../Status/StatusData.cs | 54 + .../Status/StatusReportService.cs | 146 + .../Status/StatusWebServer.cs | 144 + .../ZB.MOM.WW.LmxOpcUa.Host.csproj | 54 + src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json | 32 + .../SampleIntegrationTest.cs | 14 + ...ZB.MOM.WW.LmxOpcUa.IntegrationTests.csproj | 38 + .../appsettings.test.json | 5 + .../xunit.runner.json | 4 + .../ConfigurationLoadingTests.cs | 100 + .../Domain/MxDataTypeMapperTests.cs | 71 + .../Domain/MxErrorCodesTests.cs | 46 + .../Domain/QualityMapperTests.cs | 101 + .../EndToEnd/FullDataFlowTest.cs | 103 + .../ChangeDetectionServiceTests.cs | 100 + .../Helpers/FakeGalaxyRepository.cs | 45 + .../Helpers/FakeMxAccessClient.cs | 76 + .../Helpers/FakeMxProxy.cs | 119 + .../Metrics/PerformanceMetricsTests.cs | 122 + .../MxAccess/MxAccessClientConnectionTests.cs | 110 + .../MxAccess/MxAccessClientMonitorTests.cs | 72 + .../MxAccess/MxAccessClientReadWriteTests.cs | 126 + .../MxAccessClientSubscriptionTests.cs | 105 + .../MxAccess/StaComThreadTests.cs | 74 + .../OpcUa/DataValueConverterTests.cs | 122 + .../OpcUa/LmxNodeManagerBuildTests.cs | 109 + .../OpcUa/LmxNodeManagerRebuildTests.cs | 72 + .../OpcUa/OpcUaQualityMapperTests.cs | 60 + tests/ZB.MOM.WW.LmxOpcUa.Tests/SampleTest.cs | 14 + .../Status/HealthCheckServiceTests.cs | 82 + .../Status/StatusReportServiceTests.cs | 127 + .../Status/StatusWebServerTests.cs | 94 + .../ChangeDetectionToRebuildWiringTest.cs | 52 + .../Wiring/MxAccessToNodeManagerWiringTest.cs | 49 + .../Wiring/OpcUaReadToMxAccessWiringTest.cs | 44 + .../Wiring/OpcUaWriteToMxAccessWiringTest.cs | 41 + .../Wiring/ServiceStartupSequenceTest.cs | 73 + .../Wiring/ShutdownCompletesTest.cs | 41 + .../ZB.MOM.WW.LmxOpcUa.Tests.csproj | 44 + .../opcuacli-dotnet/Commands/BrowseCommand.cs | 92 + .../Commands/ConnectCommand.cs | 22 + tools/opcuacli-dotnet/Commands/ReadCommand.cs | 31 + .../Commands/SubscribeCommand.cs | 61 + .../opcuacli-dotnet/Commands/WriteCommand.cs | 44 + tools/opcuacli-dotnet/OpcUaHelper.cs | 81 + tools/opcuacli-dotnet/Program.cs | 8 + tools/opcuacli-dotnet/README.md | 123 + tools/opcuacli-dotnet/opcuacli-dotnet.csproj | 16 + 283 files changed, 16493 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 ZB.MOM.WW.LmxOpcUa.slnx create mode 100644 dashboard.JPG create mode 100644 docs/implementation-plan.md create mode 100644 docs/reqs/GalaxyRepositoryReqs.md create mode 100644 docs/reqs/HighLevelReqs.md create mode 100644 docs/reqs/MxAccessClientReqs.md create mode 100644 docs/reqs/OpcUaServerReqs.md create mode 100644 docs/reqs/ServiceHostReqs.md create mode 100644 docs/reqs/StatusDashboardReqs.md create mode 100644 gr/CLAUDE.md create mode 100644 gr/build_layout_plan.md create mode 100644 gr/connectioninfo.md create mode 100644 gr/data_type_mapping.md create mode 100644 gr/ddl/tables/ConversionQueue.sql create mode 100644 gr/ddl/tables/CurrentSessionContainedName.sql create mode 100644 gr/ddl/tables/ImportTransaction.sql create mode 100644 gr/ddl/tables/aa_sql_objects.sql create mode 100644 gr/ddl/tables/affected_overview_symbols.sql create mode 100644 gr/ddl/tables/alarm_message_defaults.sql create mode 100644 gr/ddl/tables/alarm_message_timestamps.sql create mode 100644 gr/ddl/tables/alarm_message_translations.sql create mode 100644 gr/ddl/tables/alarm_messages.sql create mode 100644 gr/ddl/tables/attribute_definition.sql create mode 100644 gr/ddl/tables/attribute_reference.sql create mode 100644 gr/ddl/tables/attributes_translation_table.sql create mode 100644 gr/ddl/tables/autobind_device.sql create mode 100644 gr/ddl/tables/autobind_device_category.sql create mode 100644 gr/ddl/tables/autobind_device_template.sql create mode 100644 gr/ddl/tables/autobind_device_topic.sql create mode 100644 gr/ddl/tables/autobind_naming_rule.sql create mode 100644 gr/ddl/tables/autobind_naming_rule_spec.sql create mode 100644 gr/ddl/tables/autobind_translation_rule.sql create mode 100644 gr/ddl/tables/autobound_attribute.sql create mode 100644 gr/ddl/tables/client_control_class_link.sql create mode 100644 gr/ddl/tables/client_info.sql create mode 100644 gr/ddl/tables/control_index.sql create mode 100644 gr/ddl/tables/data_type.sql create mode 100644 gr/ddl/tables/deleted_gobject.sql create mode 100644 gr/ddl/tables/deleted_ids.sql create mode 100644 gr/ddl/tables/deleted_visual_element.sql create mode 100644 gr/ddl/tables/deleted_visual_element_version.sql create mode 100644 gr/ddl/tables/deployed_file.sql create mode 100644 gr/ddl/tables/deployed_intouch_viewapp.sql create mode 100644 gr/ddl/tables/deployed_intouch_viewapp_visual_element_dependency.sql create mode 100644 gr/ddl/tables/dynamic_attribute.sql create mode 100644 gr/ddl/tables/external_content_media_types.sql create mode 100644 gr/ddl/tables/feature.sql create mode 100644 gr/ddl/tables/feature_file_link.sql create mode 100644 gr/ddl/tables/file_browserinfo_link.sql create mode 100644 gr/ddl/tables/file_pending_update.sql create mode 100644 gr/ddl/tables/file_primitive_definition_link.sql create mode 100644 gr/ddl/tables/file_table.sql create mode 100644 gr/ddl/tables/folder.sql create mode 100644 gr/ddl/tables/folder_gobject_link.sql create mode 100644 gr/ddl/tables/galaxy.sql create mode 100644 gr/ddl/tables/galaxy_data.sql create mode 100644 gr/ddl/tables/galaxy_settings.sql create mode 100644 gr/ddl/tables/gobject.sql create mode 100644 gr/ddl/tables/gobject_asset_order.sql create mode 100644 gr/ddl/tables/gobject_change_log.sql create mode 100644 gr/ddl/tables/gobject_filter_info_timestamp.sql create mode 100644 gr/ddl/tables/gobject_friendly_name.sql create mode 100644 gr/ddl/tables/gobject_log_details.sql create mode 100644 gr/ddl/tables/gobject_protected.sql create mode 100644 gr/ddl/tables/instance.sql create mode 100644 gr/ddl/tables/intouchviewapptemplate_allsymbols.sql create mode 100644 gr/ddl/tables/lookup_category.sql create mode 100644 gr/ddl/tables/lookup_folder.sql create mode 100644 gr/ddl/tables/lookup_operation.sql create mode 100644 gr/ddl/tables/lookup_package_op_status.sql create mode 100644 gr/ddl/tables/lookup_status.sql create mode 100644 gr/ddl/tables/lookup_table_name.sql create mode 100644 gr/ddl/tables/namespace.sql create mode 100644 gr/ddl/tables/object_device_linkage.sql create mode 100644 gr/ddl/tables/object_wizard_overview_symbols.sql create mode 100644 gr/ddl/tables/object_wizard_symbol_override.sql create mode 100644 gr/ddl/tables/object_wizard_symbol_override_mapping.sql create mode 100644 gr/ddl/tables/old_checked_in_packages.sql create mode 100644 gr/ddl/tables/operation.sql create mode 100644 gr/ddl/tables/operation_message.sql create mode 100644 gr/ddl/tables/operation_status.sql create mode 100644 gr/ddl/tables/operation_status_look_up.sql create mode 100644 gr/ddl/tables/ow_group_def.sql create mode 100644 gr/ddl/tables/ow_group_id.sql create mode 100644 gr/ddl/tables/ow_group_override.sql create mode 100644 gr/ddl/tables/ow_instance_setting.sql create mode 100644 gr/ddl/tables/ow_link_def.sql create mode 100644 gr/ddl/tables/ow_link_id.sql create mode 100644 gr/ddl/tables/ow_lu_definition.sql create mode 100644 gr/ddl/tables/ow_lu_setting.sql create mode 100644 gr/ddl/tables/ow_opt_or_choice_def.sql create mode 100644 gr/ddl/tables/ow_opt_or_choice_id.sql create mode 100644 gr/ddl/tables/ow_opt_or_choice_override.sql create mode 100644 gr/ddl/tables/ow_setting_def.sql create mode 100644 gr/ddl/tables/ow_setting_id.sql create mode 100644 gr/ddl/tables/ow_setting_override.sql create mode 100644 gr/ddl/tables/ow_symbol_setting.sql create mode 100644 gr/ddl/tables/owned_visual_element.sql create mode 100644 gr/ddl/tables/package.sql create mode 100644 gr/ddl/tables/packages_to_be_deleted.sql create mode 100644 gr/ddl/tables/platform.sql create mode 100644 gr/ddl/tables/platform_license.sql create mode 100644 gr/ddl/tables/primitive_attribute_validation_results.sql create mode 100644 gr/ddl/tables/primitive_data_type.sql create mode 100644 gr/ddl/tables/primitive_definition.sql create mode 100644 gr/ddl/tables/primitive_instance.sql create mode 100644 gr/ddl/tables/primitive_instance_feature_link.sql create mode 100644 gr/ddl/tables/primitive_instance_file_table_link.sql create mode 100644 gr/ddl/tables/proxy_timestamp.sql create mode 100644 gr/ddl/tables/redundancy.sql create mode 100644 gr/ddl/tables/renamed_visual_element.sql create mode 100644 gr/ddl/tables/schema_version.sql create mode 100644 gr/ddl/tables/supported_locales.sql create mode 100644 gr/ddl/tables/template.sql create mode 100644 gr/ddl/tables/template_attribute.sql create mode 100644 gr/ddl/tables/template_definition.sql create mode 100644 gr/ddl/tables/template_idebehavior_link.sql create mode 100644 gr/ddl/tables/template_migration_policy.sql create mode 100644 gr/ddl/tables/timestamp_record.sql create mode 100644 gr/ddl/tables/toolset.sql create mode 100644 gr/ddl/tables/user_preferences.sql create mode 100644 gr/ddl/tables/user_profile.sql create mode 100644 gr/ddl/tables/visual_element.sql create mode 100644 gr/ddl/tables/visual_element_affected_by_undo_check_out.sql create mode 100644 gr/ddl/tables/visual_element_archive.sql create mode 100644 gr/ddl/tables/visual_element_id.sql create mode 100644 gr/ddl/tables/visual_element_reference.sql create mode 100644 gr/ddl/tables/visual_element_timestamp.sql create mode 100644 gr/ddl/tables/visual_element_version.sql create mode 100644 gr/ddl/tables/well_known_client_controls.sql create mode 100644 gr/ddl/views/internal_all_alarms_view.sql create mode 100644 gr/ddl/views/internal_all_view_app_view.sql create mode 100644 gr/ddl/views/internal_automation_object_model_hierarchy_view.sql create mode 100644 gr/ddl/views/internal_automation_object_model_hierarchy_view2.sql create mode 100644 gr/ddl/views/internal_automation_object_visual_element_hierarchy_view.sql create mode 100644 gr/ddl/views/internal_automation_object_visual_element_hierarchy_view_includetemplates.sql create mode 100644 gr/ddl/views/internal_checked_in_unbound_relative_visual_element_reference_view.sql create mode 100644 gr/ddl/views/internal_checked_out_unbound_relative_visual_element_reference_view.sql create mode 100644 gr/ddl/views/internal_common_obj.sql create mode 100644 gr/ddl/views/internal_control_view.sql create mode 100644 gr/ddl/views/internal_get_GRMblob_preview_user_view.sql create mode 100644 gr/ddl/views/internal_get_TemplateToolBox_Allelements_view.sql create mode 100644 gr/ddl/views/internal_get_TemplateToolBox_folders_view.sql create mode 100644 gr/ddl/views/internal_get_asset_graphics_preview_user_view.sql create mode 100644 gr/ddl/views/internal_get_attribute_definition_with_validation.sql create mode 100644 gr/ddl/views/internal_get_device_scangroups_view.sql create mode 100644 gr/ddl/views/internal_get_external_content_media_types_view.sql create mode 100644 gr/ddl/views/internal_get_gtb_Allelements_view.sql create mode 100644 gr/ddl/views/internal_get_gtb_elements_view.sql create mode 100644 gr/ddl/views/internal_get_gtb_folders_view.sql create mode 100644 gr/ddl/views/internal_get_gtb_visual_element_references_deployment_view.sql create mode 100644 gr/ddl/views/internal_get_gtb_visual_element_references_preview_view.sql create mode 100644 gr/ddl/views/internal_get_gtb_visual_element_references_view.sql create mode 100644 gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_preview_view.sql create mode 100644 gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_view.sql create mode 100644 gr/ddl/views/internal_get_object_wizard_symbol_override_preview_view.sql create mode 100644 gr/ddl/views/internal_get_object_wizard_symbol_override_view.sql create mode 100644 gr/ddl/views/internal_get_visual_element_primitives_deployment_view.sql create mode 100644 gr/ddl/views/internal_get_visual_element_primitives_view.sql create mode 100644 gr/ddl/views/internal_gtb_symbols_hierarchy_view.sql create mode 100644 gr/ddl/views/internal_linked_visual_element_definition_per_user_view.sql create mode 100644 gr/ddl/views/internal_list_automation_objects_view.sql create mode 100644 gr/ddl/views/internal_list_model_objects_view.sql create mode 100644 gr/ddl/views/internal_list_objects_view.sql create mode 100644 gr/ddl/views/internal_list_unassigned_objects_view.sql create mode 100644 gr/ddl/views/internal_localized_alarm_messages.sql create mode 100644 gr/ddl/views/internal_model_hierarchy_asset_view.sql create mode 100644 gr/ddl/views/internal_package_status_view.sql create mode 100644 gr/ddl/views/internal_proxy_obj.sql create mode 100644 gr/ddl/views/internal_reference_primitive_attribute.sql create mode 100644 gr/ddl/views/internal_required_support_features.sql create mode 100644 gr/ddl/views/internal_runtime_attributes.sql create mode 100644 gr/ddl/views/internal_visible_packages_per_user_view.sql create mode 100644 gr/ddl/views/internal_visual_element_description_all_packages_view.sql create mode 100644 gr/ddl/views/internal_visual_element_description_per_user_view.sql create mode 100644 gr/ddl/views/internal_visual_element_description_view.sql create mode 100644 gr/ddl/views/internal_visual_element_primitives_preview_user_view.sql create mode 100644 gr/ddl/views/internal_visual_element_reference_per_user_view.sql create mode 100644 gr/ddl/views/internal_visual_element_reference_view.sql create mode 100644 gr/ddl/views/internal_visual_element_reference_warning_status_view.sql create mode 100644 gr/ddl/views/internal_visual_element_timestamp_per_user_view.sql create mode 100644 gr/ddl/views/public_gobject_definition.sql create mode 100644 gr/hierarchy.jpeg create mode 100644 gr/layout.md create mode 100644 gr/parse_tables.py create mode 100644 gr/queries/attributes.sql create mode 100644 gr/queries/attributes_extended.sql create mode 100644 gr/queries/change_detection.sql create mode 100644 gr/queries/hierarchy.sql create mode 100644 gr/schema.md create mode 100644 gr/tags.JPG create mode 100644 lib/ArchestrA.MXAccess.dll create mode 100644 mxaccess_documentation.md create mode 100644 session.dat create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/AppConfiguration.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/ConfigurationValidator.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/DashboardConfiguration.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/GalaxyRepositoryConfiguration.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/MxAccessConfiguration.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionState.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionStateChangedEventArgs.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyAttributeInfo.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyObjectInfo.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IGalaxyRepository.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxAccessClient.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxProxy.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxDataTypeMapper.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxErrorCodes.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Quality.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/QualityMapper.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Vtq.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/ChangeDetectionService.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryService.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryStats.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Metrics/PerformanceMetrics.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Connection.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.EventHandlers.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Monitor.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.ReadWrite.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Subscription.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxProxyAdapter.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/StaComThread.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/AddressSpaceBuilder.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/DataValueConverter.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaQualityMapper.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Program.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Status/HealthCheckService.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusWebServer.cs create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/ZB.MOM.WW.LmxOpcUa.Host.csproj create mode 100644 src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/SampleIntegrationTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/ZB.MOM.WW.LmxOpcUa.IntegrationTests.csproj create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/appsettings.test.json create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/xunit.runner.json create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Configuration/ConfigurationLoadingTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxDataTypeMapperTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxErrorCodesTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/QualityMapperTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/EndToEnd/FullDataFlowTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/GalaxyRepository/ChangeDetectionServiceTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeGalaxyRepository.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxAccessClient.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxProxy.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Metrics/PerformanceMetricsTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientConnectionTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientMonitorTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientReadWriteTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientSubscriptionTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/StaComThreadTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/DataValueConverterTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerBuildTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerRebuildTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/OpcUaQualityMapperTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/SampleTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/HealthCheckServiceTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusWebServerTests.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ChangeDetectionToRebuildWiringTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/MxAccessToNodeManagerWiringTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaReadToMxAccessWiringTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaWriteToMxAccessWiringTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ServiceStartupSequenceTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ShutdownCompletesTest.cs create mode 100644 tests/ZB.MOM.WW.LmxOpcUa.Tests/ZB.MOM.WW.LmxOpcUa.Tests.csproj create mode 100644 tools/opcuacli-dotnet/Commands/BrowseCommand.cs create mode 100644 tools/opcuacli-dotnet/Commands/ConnectCommand.cs create mode 100644 tools/opcuacli-dotnet/Commands/ReadCommand.cs create mode 100644 tools/opcuacli-dotnet/Commands/SubscribeCommand.cs create mode 100644 tools/opcuacli-dotnet/Commands/WriteCommand.cs create mode 100644 tools/opcuacli-dotnet/OpcUaHelper.cs create mode 100644 tools/opcuacli-dotnet/Program.cs create mode 100644 tools/opcuacli-dotnet/README.md create mode 100644 tools/opcuacli-dotnet/opcuacli-dotnet.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58a0b78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Build outputs +bin/ +obj/ +publish/ + +# IDE +.vs/ +.idea/ +*.user +*.suo + +# Logs +logs/ + +# OS +Thumbs.db +desktop.ini +.DS_Store + +# NuGet +packages/ +*.nupkg + +# Certificates +*.pfx +*.pem + +# OPC sample server (large external repo) +tools/opcsampleserver/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3f9b3b5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,113 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Goal + +Build an OPC UA server on .NET Framework 4.8 (32-bit) that exposes AVEVA System Platform (Wonderware) Galaxy tags via the MXAccess toolkit. The server mirrors the Galaxy object hierarchy as an OPC UA address space, translating between contained-name browse paths and tag-name runtime references. + +## Architecture Overview + +### Data Flow + +1. **Galaxy Repository DB (ZB)** — SQL Server database holding the deployed object hierarchy and attribute definitions. Queried at startup and on change detection to build/rebuild the OPC UA address space. +2. **MXAccess COM API** — Runtime data access layer. Subscribes to Galaxy tag attributes for live read/write. Requires a dedicated STA thread with a Win32 message pump for COM callbacks. +3. **OPC UA Server** — Exposes the hierarchy as browse nodes and attributes as variable nodes. Clients browse via contained names but reads/writes are translated to `tag_name.AttributeName` format for MXAccess. + +### Key Concept: Contained Name vs Tag Name + +Galaxy objects have two names: +- **contained_name** — human-readable name scoped to parent (used for OPC UA browse tree) +- **tag_name** — globally unique system name (used for MXAccess read/write) + +Example: browsing `TestMachine_001/DelmiaReceiver/DownloadPath` translates to MXAccess reference `DelmiaReceiver_001.DownloadPath`. + +See `gr/layout.md` for the full mapping and target OPC UA structure. + +### Data Type Mapping + +Galaxy `mx_data_type` values map to OPC UA types (Boolean, Int32, Float, Double, String, DateTime, etc.). Array attributes use ValueRank=1 with ArrayDimensions from the Galaxy attribute definition. Full mapping in `gr/data_type_mapping.md`. + +### Change Detection + +Poll `galaxy.time_of_last_deploy` in the ZB database to detect redeployments, then rebuild the address space. See `gr/build_layout_plan.md` for the step-by-step plan. + +## Reference Implementation + +An existing MXAccess client implementation is at: +`C:\Users\dohertj2\Desktop\scadalink-design\lmxproxy\src\ZB.MOM.WW.LmxProxy.Host` + +Key patterns from that codebase: +- **StaComThread** — Dedicated STA thread with Win32 message pump (`GetMessage`/`DispatchMessage` loop). All MXAccess COM objects must be created and called on this thread. Uses `PostThreadMessage(WM_APP)` to marshal work items. +- **LMXProxyServer COM object** — `Register(clientName)` returns a connection handle. `AddItem(handle, address)` + `AdviseSupervisory(handle, itemHandle)` for subscriptions. `OnDataChange`/`OnWriteComplete` events for callbacks. +- **Reconnect** — Stored subscriptions are replayed after reconnect. A probe tag subscription monitors connection health. +- **COM cleanup** — `Marshal.ReleaseComObject()` on disconnect. Event handlers must be unwired before unregister. + +## MXAccess Documentation + +`mxaccess_documentation.md` in the project root contains the full ArchestrA MXAccess Toolkit User's Guide. Key API: `ArchestrA.MxAccess` namespace, `LMXProxyServer` class. The toolkit DLLs are in `Program Files (x86)\ArchestrA\Framework\bin`. + +## Galaxy Repository Database + +Connection: `sqlcmd -S localhost -d ZB -E` (Windows Auth). See `gr/connectioninfo.md`. + +The `gr/` folder contains: +- `queries/` — SQL for hierarchy extraction, attribute lookup, and change detection +- `ddl/tables/` and `ddl/views/` — Schema definitions +- `schema.md` — Full table/view reference +- `build_layout_plan.md` — Step-by-step plan for building the OPC UA address space from DB queries +- `gr/CLAUDE.md` — Detailed guidance for working within the `gr/` subfolder + +Key tables: `gobject` (hierarchy/deployment), `template_definition` (object categories), `dynamic_attribute` (user-defined attributes), `primitive_instance` (primitive-to-attribute links), `galaxy` (change detection). + +## Build Commands + +```bash +dotnet restore ZB.MOM.WW.LmxOpcUa.slnx +dotnet build ZB.MOM.WW.LmxOpcUa.slnx +dotnet test ZB.MOM.WW.LmxOpcUa.slnx # all tests +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests # unit tests only +dotnet test tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests # integration tests only +dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # single test +``` + +## Build & Runtime Constraints + +- Language: C#, .NET Framework 4.8, **x86 (32-bit)** platform target — required for MXAccess COM interop +- MXAccess requires a deployed ArchestrA Platform on the machine running the server +- COM apartment: MXAccess objects must live on an STA thread with a message pump + +## Library Preferences + +- **Logging**: Serilog with rolling daily file sink +- **Unit tests**: xUnit + Shouldly for assertions +- **Service hosting**: TopShelf (Windows service install/uninstall/run as console) +- **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server` + +## OPC UA .NET Standard Documentation + +Use the DeepWiki MCP (`mcp__deepwiki`) to query documentation for the OPC UA .NET Standard stack: `https://deepwiki.com/OPCFoundation/UA-.NETStandard`. Tools: `read_wiki_structure`, `read_wiki_contents`, and `ask_question` with repo `OPCFoundation/UA-.NETStandard`. + +## Testing + +Use the dotnet OPC UA CLI tool at `tools/opcuacli-dotnet/` for manual testing against the running OPC UA server. Supports connect, read, write, subscribe, and browse commands. See `tools/opcuacli-dotnet/README.md` for usage details. + +```bash +cd tools/opcuacli-dotnet +dotnet run -- connect -u opc.tcp://localhost:4840 +dotnet run -- browse -u opc.tcp://localhost:4840 -r -d 3 +dotnet run -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" +dotnet run -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500 +``` + +### OPC PLC Sample Server + +A test OPC UA server is available at `tools/opcsampleserver/` (Azure IoT OPC PLC). It generates simulated data nodes (slow, fast, anomaly, GUID) on `opc.tcp://localhost:50000`. Must run from the `publish` directory or use the provided batch scripts. Requires `--unsecuretransport` flag for the CLI tool to connect. See `tools/opcsampleserver/README.md` for full details. + +```bash +# Start the test server +cd tools/opcsampleserver/publish && dotnet opcplc.dll --pn=50000 --autoaccept --unsecuretransport --sn=5 --sr=10 --st=uint --fn=5 --fr=1 --ft=uint + +# Or use the batch script +tools/opcsampleserver/start-server.bat +``` diff --git a/ZB.MOM.WW.LmxOpcUa.slnx b/ZB.MOM.WW.LmxOpcUa.slnx new file mode 100644 index 0000000..c956722 --- /dev/null +++ b/ZB.MOM.WW.LmxOpcUa.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dashboard.JPG b/dashboard.JPG new file mode 100644 index 0000000000000000000000000000000000000000..a8a88f99e9a4a78c417e86efa26373b3eaa0f816 GIT binary patch literal 69646 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

MnuGfkQ;Db3pxF7nC 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 Ujfm`N-ye@&{};$g=Ihx1028Gm!vFvP literal 0 HcmV?d00001 diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md new file mode 100644 index 0000000..b2bdfb3 --- /dev/null +++ b/docs/implementation-plan.md @@ -0,0 +1,384 @@ +# Implementation Plan: LmxOpcUa Server — All 44 Requirements + +## Context + +The LmxOpcUa project is scaffolded (solution, projects, configs, requirements docs) but has no implementation beyond Program.cs and a stub OpcUaService.cs. This plan implements all 44 requirements across 6 phases, each with verification gates and wiring checks to ensure nothing is left unconnected. + +## Architecture + +Five major components wired together in OpcUaService.cs: + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Galaxy Repository│────>│ OPC UA Server │<───>│ OPC UA Clients │ +│ (SQL queries) │ │ (address space) │ │ │ +└─────────────────┘ └────────┬──────────┘ └─────────────────┘ + │ + ┌────────┴──────────┐ + │ MxAccessClient │ + │ (STA + COM) │ + └───────────────────┘ + │ + ┌────────┴──────────┐ + │ Status Dashboard │ + │ (HTTP + metrics) │ + └───────────────────┘ +``` + +Reference implementation: `C:\Users\dohertj2\Desktop\scadalink-design\lmxproxy\src\ZB.MOM.WW.LmxProxy.Host\` + +--- + +## PHASE 1: Foundation — Domain Models, Configuration, Interfaces + +**Reqs:** SVC-003, SVC-006 (partial), MXA-008 (interfaces), MXA-009, OPC-005, OPC-012 (partial), GR-005 (config) + +### Files to Create + +**Configuration/** +- `AppConfiguration.cs` — top-level holder for all config sections +- `OpcUaConfiguration.cs` — Port, EndpointPath, ServerName, GalaxyName, MaxSessions, SessionTimeoutMinutes +- `MxAccessConfiguration.cs` — ClientName, timeouts, concurrency, probe settings +- `GalaxyRepositoryConfiguration.cs` — ConnectionString, intervals, command timeout +- `DashboardConfiguration.cs` — Enabled, Port, RefreshIntervalSeconds +- `ConfigurationValidator.cs` — validate and log effective config at startup + +**Domain/** +- `ConnectionState.cs` — enum: Disconnected, Connecting, Connected, Disconnecting, Error, Reconnecting +- `ConnectionStateChangedEventArgs.cs` — PreviousState, CurrentState, Message +- `Vtq.cs` — Value/Timestamp/Quality struct with factory methods +- `Quality.cs` — enum with Bad/Uncertain/Good families matching OPC DA codes +- `QualityMapper.cs` — MapFromMxAccessQuality(int) and MapToOpcUaStatusCode(Quality) +- `MxDataTypeMapper.cs` — MapToOpcUaDataType(int mxDataType), MapToClrType(int). Unknown defaults to String +- `MxErrorCodes.cs` — translate 1008/1012/1013 to human messages +- `GalaxyObjectInfo.cs` — DTO matching hierarchy.sql columns +- `GalaxyAttributeInfo.cs` — DTO matching attributes.sql columns +- `IMxAccessClient.cs` — interface: Connect, Disconnect, Subscribe, Read, Write, OnTagValueChanged delegate +- `IGalaxyRepository.cs` — interface: GetHierarchy, GetAttributes, GetLastDeployTime, TestConnection, OnGalaxyChanged event +- `IMxProxy.cs` — abstraction over LMXProxyServer COM object (enables testing without DLL) + +**Metrics/** +- `PerformanceMetrics.cs` — ITimingScope, OperationMetrics (1000-entry rolling buffer), BeginOperation/GetStatistics. Adapt from reference. + +### Tests +- `ConfigurationLoadingTests.cs` — bind appsettings.json, verify defaults +- `MxDataTypeMapperTests.cs` — all 12 type mappings + unknown default +- `QualityMapperTests.cs` — boundary values (0, 63, 64, 191, 192) +- `MxErrorCodesTests.cs` — known codes + unknown +- `PerformanceMetricsTests.cs` — recording, P95, buffer eviction, empty state + +### Verification Gate 1 +- [ ] `dotnet build` — zero errors +- [ ] All Phase 1 tests pass +- [ ] Config binding loads all 4 sections from appsettings.json +- [ ] MxDataTypeMapper covers every row in `gr/data_type_mapping.md` +- [ ] Quality enum covers all reference impl values +- [ ] Builds WITHOUT ArchestrA.MxAccess.dll (interface-based, no COM refs in Phase 1) +- [ ] Every new file has doc-comment referencing requirement ID(s) +- [ ] IMxAccessClient has every method needed by OPC-007, OPC-008, OPC-009 +- [ ] IGalaxyRepository has every method needed by GR-001 through GR-004 + +--- + +## PHASE 2: MxAccessClient — STA Thread and COM Interop + +**Reqs:** MXA-001, MXA-002, MXA-003, MXA-004, MXA-005, MXA-006, MXA-007, MXA-008 (wiring) + +### Files to Create + +**MxAccess/** +- `StaComThread.cs` — adapt from reference. STA thread, Win32 message pump, RunAsync(Action)/RunAsync(Func), WM_APP dispatch +- `MxAccessClient.cs` — core partial class implementing IMxAccessClient. Fields: StaComThread, IMxProxy, handle, state, semaphores, maps +- `MxAccessClient.Connection.cs` — ConnectAsync (Register on STA), DisconnectAsync (cleanup per MXA-007), COM cleanup +- `MxAccessClient.Subscription.cs` — SubscribeAsync (AddItem+AdviseSupervisory), UnsubscribeAsync, ReplayStoredSubscriptions +- `MxAccessClient.ReadWrite.cs` — ReadAsync (subscribe-get-first-unsubscribe), WriteAsync (Write+OnWriteComplete), semaphore-limited, timeout, ITimingScope metrics +- `MxAccessClient.EventHandlers.cs` — OnDataChange (resolve handle→address, create Vtq, invoke callback, update probe), OnWriteComplete (complete TCS, translate errors) +- `MxAccessClient.Monitor.cs` — monitor loop (reconnect on disconnect, probe staleness→force reconnect), cancellable +- `MxProxyAdapter.cs` — wraps real LMXProxyServer COM object, forwards calls to IMxProxy interface + +**Test Helpers (in Tests project):** +- `FakeMxProxy.cs` — implements IMxProxy, simulates connections/data changes for testing + +### Design Decision: IMxProxy Abstraction +Code against `IMxProxy` interface (not `LMXProxyServer` directly). This allows testing without ArchestrA.MxAccess.dll. `MxProxyAdapter` wraps the real COM object at runtime. + +### Tests +- `StaComThreadTests.cs` — STA apartment verified, work item execution, dispose +- `MxAccessClientConnectionTests.cs` — state transitions, cleanup order +- `MxAccessClientSubscriptionTests.cs` — subscribe/unsubscribe, stored subscriptions, reconnect replay, OnDataChange→callback +- `MxAccessClientReadWriteTests.cs` — read returns value, read timeout, write completes on callback, write timeout, semaphore limiting +- `MxAccessClientMonitorTests.cs` — reconnect on disconnect, probe staleness + +### Verification Gate 2 +- [ ] Solution builds without ArchestrA.MxAccess.dll +- [ ] STA thread test proves work items execute on STA apartment +- [ ] Connection lifecycle: Disconnected→Connecting→Connected→Disconnecting→Disconnected +- [ ] Subscription replay: stored subscriptions replayed after simulated reconnect +- [ ] Read/Write: timeout behavior returns error within expected window +- [ ] Metrics: Read/Write record timing in PerformanceMetrics +- [ ] **WIRING CHECK:** OnDataChange callback reaches OnTagValueChanged delegate +- [ ] COM cleanup order: UnAdvise→RemoveItem→unwire events→Unregister→ReleaseComObject +- [ ] Error codes 1008/1012/1013 translate correctly in OnWriteComplete path + +--- + +## PHASE 3: Galaxy Repository — SQL Queries and Change Detection + +**Reqs:** GR-001, GR-002, GR-003, GR-004, GR-006, GR-007 + +### Files to Create + +**GalaxyRepository/** +- `GalaxyRepositoryService.cs` — implements IGalaxyRepository. SQL embedded as `const string` (from gr/queries/). ADO.NET SqlConnection per-query. GetHierarchyAsync, GetAttributesAsync, GetLastDeployTimeAsync, TestConnectionAsync +- `ChangeDetectionService.cs` — background Timer at configured interval. Polls GetLastDeployTimeAsync, compares to last known, fires OnGalaxyChanged on change. First poll always triggers. Failed poll logs Warning, retries next interval +- `GalaxyRepositoryStats.cs` — POCO for dashboard: GalaxyName, DbConnected, LastDeployTime, ObjectCount, AttributeCount, LastRebuildTime + +### Tests +- `ChangeDetectionServiceTests.cs` — first poll triggers, same timestamp skips, changed triggers, failed poll retries +- `GalaxyRepositoryServiceTests.cs` (integration, in IntegrationTests) — TestConnection, GetHierarchy returns rows, GetAttributes returns rows + +### Verification Gate 3 +- [ ] All SQL is `const string` — no concatenation, no parameters, no INSERT/UPDATE/DELETE (GR-006 code review) +- [ ] GetHierarchyAsync maps all columns: gobject_id, tag_name, contained_name, browse_name, parent_gobject_id, is_area +- [ ] GetAttributesAsync maps all columns including array_dimension +- [ ] Change detection: first poll fires, same timestamp skips, changed fires +- [ ] Failed query does NOT crash or trigger false rebuild +- [ ] GalaxyRepositoryStats populated for dashboard +- [ ] Zero rows from hierarchy logs Warning + +--- + +## PHASE 4: OPC UA Server — Address Space and Node Manager + +**Reqs:** OPC-001, OPC-002, OPC-003, OPC-004, OPC-005, OPC-006, OPC-007, OPC-008, OPC-009, OPC-010, OPC-011, OPC-012, OPC-013 + +### Files to Create + +**OpcUa/** +- `LmxOpcUaServer.cs` — inherits StandardServer. Creates custom node manager. SecurityPolicy None. Registers namespace `urn:{GalaxyName}:LmxOpcUa` +- `LmxNodeManager.cs` — inherits CustomNodeManager2. Core class: + - `BuildAddressSpace(hierarchy, attributes)` — creates folder/object/variable nodes from Galaxy data. NodeId: `ns=1;s={tag_name}` / `ns=1;s={tag_name}.{attr}`. Stores full_tag_reference lookup + - `RebuildAddressSpace(hierarchy, attributes)` — removes old nodes, rebuilds. Preserves sessions + - Read/Write overrides delegate to IMxAccessClient via stored full_tag_reference + - Subscription management: ref-counted shared MXAccess subscriptions +- `OpcUaServerHost.cs` — manages ApplicationInstance lifecycle. Programmatic config (no XML). Start/Stop. Exposes ActiveSessionCount +- `OpcUaQualityMapper.cs` — domain Quality → OPC UA StatusCodes +- `DataValueConverter.cs` — COM variant ↔ OPC UA DataValue. Handles all types from data_type_mapping.md. DateTime UTC. Arrays + +### Tests +- `DataValueConverterTests.cs` — all type conversions, arrays, DateTime UTC +- `LmxNodeManagerBuildTests.cs` — synthetic hierarchy matching gr/layout.md, verify node types, NodeIds, data types, ValueRank, ArrayDimensions +- `LmxNodeManagerRebuildTests.cs` — rebuild replaces nodes, old nodes gone, new nodes present +- `OpcUaQualityMapperTests.cs` — all quality families + +### Verification Gate 4 +- [ ] Endpoint URL: `opc.tcp://{hostname}:{port}/LmxOpcUa` +- [ ] Namespace: `urn:{GalaxyName}:LmxOpcUa` at index 1 +- [ ] Root ZB folder under Objects +- [ ] Areas → FolderType + Organizes reference +- [ ] Non-areas → BaseObjectType + HasComponent reference +- [ ] Variable nodes: correct DataType, ValueRank, ArrayDimensions per data_type_mapping.md +- [ ] **WIRING CHECK:** Read handler resolves NodeId → full_tag_reference → calls IMxAccessClient.ReadAsync +- [ ] **WIRING CHECK:** Write handler resolves NodeId → full_tag_reference → calls IMxAccessClient.WriteAsync +- [ ] Rebuild removes old nodes, creates new ones without crash +- [ ] SecurityPolicy is None +- [ ] MaxSessions/SessionTimeout configured from appsettings + +--- + +## PHASE 5: Status Dashboard — HTTP, HTML, JSON, Health + +**Reqs:** DASH-001 through DASH-009 + +### Files to Create + +**Status/** +- `StatusData.cs` — DTO: ConnectionInfo, HealthInfo, SubscriptionInfo, GalaxyInfo, OperationMetrics, Footer +- `HealthCheckService.cs` — rules: not connected→Unhealthy, success rate<50% w/>100 ops→Degraded, else Healthy +- `StatusReportService.cs` — aggregates from all components. GenerateHtml (self-contained, inline CSS, color-coded panels, meta-refresh). GenerateJson. IsHealthy +- `StatusWebServer.cs` — HttpListener. Routes: / → HTML, /api/status → JSON, /api/health → 200/503. GET only. no-cache headers. Disableable + +### Tests +- `HealthCheckServiceTests.cs` — three health rules, messages +- `StatusReportServiceTests.cs` — HTML contains all panels, JSON deserializes, meta-refresh tag +- `StatusWebServerTests.cs` — routing (200/405/404), cache headers, start/stop + +### Verification Gate 5 +- [ ] HTML contains all panels: Connection, Health, Subscriptions, Galaxy Info, Operations table, Footer +- [ ] Connection panel: green/red/yellow border per state +- [ ] Health panel: three states with correct colors +- [ ] Operations table: Read/Write/Subscribe/Browse with Count/SuccessRate/Avg/Min/Max/P95 +- [ ] Galaxy Info panel: galaxy name, DB status, last deploy, object/attribute counts, last rebuild +- [ ] Footer: timestamp + assembly version +- [ ] JSON API: all same data as HTML +- [ ] /api/health: 200 when healthy, 503 when unhealthy +- [ ] Meta-refresh tag with configured interval +- [ ] Port conflict does not prevent service startup +- [ ] Dashboard disabled via config skips HttpListener + +--- + +## PHASE 6: Integration Wiring and End-to-End Verification + +**Reqs:** SVC-004, SVC-005, SVC-006, ALL wiring verification + +### OpcUaService.cs — Full Implementation + +**Start() sequence (SVC-005):** +1. Load AppConfiguration via IConfiguration +2. ConfigurationValidator.ValidateAndLog() +3. Register AppDomain.UnhandledException handler (SVC-006) +4. Create PerformanceMetrics +5. Create MxAccessClient → ConnectAsync (failure = fatal, don't start) +6. Start MxAccessClient monitor loop +7. Create GalaxyRepositoryService → TestConnectionAsync (failure = warning, continue) +8. Create OpcUaServerHost + LmxNodeManager, inject IMxAccessClient +9. Query initial hierarchy + attributes → BuildAddressSpace +10. Start OPC UA server listener (failure = fatal) +11. Create ChangeDetectionService → **wire OnGalaxyChanged → nodeManager.RebuildAddressSpace** +12. Start change detection polling +13. Create HealthCheckService, StatusReportService, StatusWebServer → Start (failure = warning) +14. Log "LmxOpcUa service started successfully" + +**Critical wiring (GUARDRAILS):** +- `_mxAccessClient.OnTagValueChanged` → node manager subscription delivery +- `_changeDetectionService.OnGalaxyChanged` → `_nodeManager.RebuildAddressSpace` +- `_mxAccessClient.ConnectionStateChanged` → health check updates +- Node manager Read/Write → `_mxAccessClient.ReadAsync/WriteAsync` +- StatusReportService reads from: MxAccessClient, PerformanceMetrics, GalaxyRepositoryStats, OpcUaServerHost + +**Stop() sequence (SVC-004, reverse order, 30s max):** +1. Cancel CancellationTokenSource (stops all background loops) +2. Stop change detection +3. Stop OPC UA server +4. Disconnect MXAccess (full COM cleanup) +5. Stop StatusWebServer +6. Dispose PerformanceMetrics +7. Log "Service shutdown complete" + +### Wiring Verification Tests (GUARDRAILS) + +These tests prove components are connected end-to-end, not just implemented in isolation: + +- `Wiring/MxAccessToNodeManagerWiringTest.cs` — simulate OnDataChange on FakeMxProxy → verify data reaches node manager subscription delivery +- `Wiring/ChangeDetectionToRebuildWiringTest.cs` — mock GalaxyRepository returns changed timestamp → verify RebuildAddressSpace called +- `Wiring/OpcUaReadToMxAccessWiringTest.cs` — issue Read via NodeManager → verify FakeMxProxy receives correct full_tag_reference +- `Wiring/OpcUaWriteToMxAccessWiringTest.cs` — issue Write via NodeManager → verify FakeMxProxy receives correct tag + value +- `Wiring/ServiceStartupSequenceTest.cs` — create OpcUaService with fakes, call Start(), verify all components created and wired +- `Wiring/ShutdownCompletesTest.cs` — Start then Stop, verify completes within 30s +- `EndToEnd/FullDataFlowTest.cs` — **THE ULTIMATE SMOKE TEST**: full service with fakes, verify: (1) address space built, (2) MXAccess data change → OPC UA variable, (3) read → correct tag ref, (4) write → correct tag+value, (5) dashboard HTML has real data + +### Verification Gate 6 (FINAL) +- [ ] Startup: all 14 steps execute in order +- [ ] Shutdown: completes within 30s, all components disposed in reverse order +- [ ] **WIRING:** MXAccess OnDataChange → node manager subscription delivery +- [ ] **WIRING:** Galaxy change → address space rebuild +- [ ] **WIRING:** OPC UA Read → MXAccess ReadAsync with correct tag reference +- [ ] **WIRING:** OPC UA Write → MXAccess WriteAsync with correct tag+value +- [ ] **WIRING:** Dashboard aggregates data from all components +- [ ] **WIRING:** Health endpoint reflects actual connection state +- [ ] AppDomain.UnhandledException registered +- [ ] TopShelf recovery configured (restart, 60s delay) +- [ ] FullDataFlowTest passes end-to-end + +--- + +## Master Requirement Traceability (all 44) + +| Req | Phase | Verified By | +|-----|-------|-------------| +| SVC-001 | Done | Program.cs already configured | +| SVC-002 | Done | Program.cs already configured | +| SVC-003 | 1 | ConfigurationLoadingTests | +| SVC-004 | 6 | ShutdownCompletesTest | +| SVC-005 | 6 | ServiceStartupSequenceTest | +| SVC-006 | 6 | AppDomain handler registration test | +| MXA-001 | 2 | StaComThreadTests | +| MXA-002 | 2 | MxAccessClientConnectionTests | +| MXA-003 | 2 | MxAccessClientSubscriptionTests | +| MXA-004 | 2 | MxAccessClientReadWriteTests | +| MXA-005 | 2 | MxAccessClientMonitorTests | +| MXA-006 | 2 | MxAccessClientMonitorTests (probe) | +| MXA-007 | 2 | Cleanup order test | +| MXA-008 | 2 | Metrics integration in ReadWrite | +| MXA-009 | 1+2 | MxErrorCodesTests + write error path | +| GR-001 | 3 | GetHierarchyAsync maps all columns | +| GR-002 | 3 | GetAttributesAsync maps all columns | +| GR-003 | 3 | ChangeDetectionServiceTests | +| GR-004 | 3+6 | ChangeDetectionToRebuildWiringTest | +| GR-005 | 1+3 | Config tests + ADO.NET usage | +| GR-006 | 3 | Code review: const string SQL only | +| GR-007 | 3 | TestConnectionAsync test | +| OPC-001 | 4 | Endpoint URL test | +| OPC-002 | 4 | BuildTests: node types + references | +| OPC-003 | 4 | BuildTests: variable nodes | +| OPC-004 | 4+6 | ReadWiringTest: browse→tag_name | +| OPC-005 | 1+4 | MxDataTypeMapperTests + variable node DataType | +| OPC-006 | 4 | BuildTests: ValueRank + ArrayDimensions | +| OPC-007 | 4+6 | OpcUaReadToMxAccessWiringTest | +| OPC-008 | 4+6 | OpcUaWriteToMxAccessWiringTest | +| OPC-009 | 4+6 | MxAccessToNodeManagerWiringTest | +| OPC-010 | 4+6 | RebuildTests + ChangeDetectionToRebuildWiringTest | +| OPC-011 | 4 | ServerStatus node test | +| OPC-012 | 4 | Namespace URI test | +| OPC-013 | 4 | Session config test | +| DASH-001 | 5 | StatusWebServerTests routing | +| DASH-002 | 5 | HTML contains Connection panel | +| DASH-003 | 5 | HealthCheckServiceTests | +| DASH-004 | 5 | HTML contains Subscriptions panel | +| DASH-005 | 5 | HTML contains Operations table | +| DASH-006 | 5 | HTML contains Footer | +| DASH-007 | 5 | Meta-refresh tag test | +| DASH-008 | 5 | JSON API deserialization test | +| DASH-009 | 5 | HTML contains Galaxy Info panel | + +--- + +## Final Folder Structure + +``` +src/ZB.MOM.WW.LmxOpcUa.Host/ + Configuration/ (Phase 1) + Domain/ (Phase 1) + Metrics/ (Phase 1) + MxAccess/ (Phase 2) + GalaxyRepository/ (Phase 3) + OpcUa/ (Phase 4) + Status/ (Phase 5) + OpcUaService.cs (Phase 6 — full wiring) + Program.cs (existing) + appsettings.json (existing) +tests/ZB.MOM.WW.LmxOpcUa.Tests/ + Configuration/ (Phase 1) + Domain/ (Phase 1) + Metrics/ (Phase 1) + MxAccess/ (Phase 2) + GalaxyRepository/ (Phase 3) + OpcUa/ (Phase 4) + Status/ (Phase 5) + Wiring/ (Phase 6 — GUARDRAILS) + EndToEnd/ (Phase 6 — GUARDRAILS) + Helpers/FakeMxProxy.cs (Phase 2) +``` + +## Verification: How to Run + +```bash +# Build +dotnet build ZB.MOM.WW.LmxOpcUa.slnx + +# All tests +dotnet test ZB.MOM.WW.LmxOpcUa.slnx + +# Phase-specific (by namespace convention) +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~Configuration" +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~MxAccess" +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~GalaxyRepository" +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~OpcUa" +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~Status" +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~Wiring" +dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~EndToEnd" + +# Integration tests (requires ZB database) +dotnet test tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests +``` diff --git a/docs/reqs/GalaxyRepositoryReqs.md b/docs/reqs/GalaxyRepositoryReqs.md new file mode 100644 index 0000000..0d908f9 --- /dev/null +++ b/docs/reqs/GalaxyRepositoryReqs.md @@ -0,0 +1,121 @@ +# Galaxy Repository — Component Requirements + +Parent: [HLR-002](HighLevelReqs.md#hlr-002-galaxy-hierarchy-as-opc-ua-address-space), [HLR-005](HighLevelReqs.md#hlr-005-dynamic-address-space-rebuild) + +## GR-001: Hierarchy Extraction + +The system shall query the Galaxy Repository database to extract all deployed objects with their parent-child containment relationships, contained names, and tag names. + +### Acceptance Criteria + +- Executes `queries/hierarchy.sql` against the ZB database. +- Returns a list of objects with: `gobject_id`, `tag_name`, `contained_name`, `browse_name`, `parent_gobject_id`, `is_area`. +- Objects with `parent_gobject_id = 0` are children of the root ZB node. +- Only deployed, non-template objects matching the category filter (areas, engines, user-defined objects, etc.) are returned. +- Query completes within 10 seconds on a typical Galaxy (hundreds of objects). Log a Warning if it takes longer. + +### Details + +- Results are ordered by `parent_gobject_id, tag_name` for deterministic tree building. +- If the query returns zero rows, log a Warning (Galaxy may have no deployed objects, or the DB connection may be misconfigured). +- Orphan detection: if a row references a `parent_gobject_id` that does not exist in the result set and is not 0, log a Warning and skip that node. + +--- + +## GR-002: Attribute Extraction + +The system shall query user-defined (dynamic) attributes for deployed objects, including data type, array flag, and array dimensions. + +### Acceptance Criteria + +- Executes `queries/attributes.sql` using the template chain CTE to resolve inherited attributes. +- Returns: `gobject_id`, `tag_name`, `attribute_name`, `full_tag_reference`, `mx_data_type`, `is_array`, `array_dimension`, `security_classification`. +- Attributes starting with `_` are filtered out by the query. +- `array_dimension` is correctly extracted from the `mx_value` hex bytes (positions 13-16, little-endian uint16). + +### Details + +- CTE recursion depth is limited to 10 levels (per the query). This is sufficient for Galaxy template hierarchies. +- If `mx_data_type` is null or not in the known set (1-8, 13-16), default to String. +- If `gobject_id` from an attribute row does not match any hierarchy object, skip that attribute (object may not be deployed). + +--- + +## GR-003: Change Detection + +The system shall poll `galaxy.time_of_last_deploy` at a configurable interval to detect when a new deployment has occurred. + +### Acceptance Criteria + +- Polls `SELECT time_of_last_deploy FROM galaxy` at a configurable interval (`GalaxyRepository:ChangeDetectionIntervalSeconds`, default 30 seconds). +- Compares the returned timestamp to the last known value stored in memory. +- If different, triggers a rebuild (re-run hierarchy + attributes queries, notify OPC UA server). +- First poll after startup always triggers an initial build. +- If the query fails (SQL timeout, connection error), log Warning and retry at next interval. Do not trigger a rebuild on failure. + +### Details + +- Polling runs on a background timer thread, not blocking the STA thread. +- `time_of_last_deploy` is a datetime column. Compare using exact equality (not range). + +--- + +## GR-004: Rebuild on Change + +When a deployment change is detected, the system shall re-query hierarchy and attributes and provide the updated structure to the OPC UA server for address space rebuild. + +### Acceptance Criteria + +- On change detection, re-query both hierarchy and attributes. +- Provide the new data set to the OPC UA server component for address space replacement. +- Log at Information level: "Galaxy deployment change detected. Rebuilding address space. ({ObjectCount} objects, {AttributeCount} attributes)". +- Log total rebuild time at Information level. +- If the re-query fails, log Error and keep the existing address space (do not clear it). + +### Details + +- Rebuild is not atomic from the DB perspective — hierarchy and attributes are two separate queries. This is acceptable; deployment is an infrequent operation. +- Raise an event/callback that the OPC UA server subscribes to: `OnGalaxyChanged(hierarchyData, attributeData)`. + +--- + +## GR-005: Connection Configuration + +Database connection parameters shall be configurable via appsettings.json (connection string using Windows Authentication by default). + +### Acceptance Criteria + +- Connection string in `appsettings.json` under `GalaxyRepository:ConnectionString`. +- Default: `Server=localhost;Database=ZB;Integrated Security=true` (Windows Auth). +- ADO.NET `SqlConnection` used for queries (.NET Framework 4.8 built-in). +- Connection is opened per-query (not kept open). Connection pooling handles efficiency. +- If the initial connection test at startup fails, log Error with the connection string and continue attempting (change detection polls will keep retrying). + +### Details + +- Command timeout: configurable via `GalaxyRepository:CommandTimeoutSeconds`, default 30 seconds. +- No ORM. Raw ADO.NET with `SqlCommand` and `SqlDataReader`. SQL text is embedded as constants (not dynamically constructed). + +--- + +## GR-006: Query Safety + +All SQL queries shall be static read-only SELECT statements. No writes to the Galaxy Repository database. + +### Acceptance Criteria + +- All queries are hardcoded SQL strings with no string concatenation or user-supplied parameters. +- No INSERT, UPDATE, DELETE, or DDL statements are ever executed against the Galaxy database. +- Queries use only SELECT with read-only intent. + +--- + +## GR-007: Startup Validation + +On startup, the Galaxy Repository component shall validate database connectivity. + +### Acceptance Criteria + +- Execute a simple test query (`SELECT 1`) against the configured database. +- If the database is unreachable, log an Error but do not prevent service startup. +- The service runs in degraded mode (empty address space) until the database becomes available and the next change detection poll succeeds. diff --git a/docs/reqs/HighLevelReqs.md b/docs/reqs/HighLevelReqs.md new file mode 100644 index 0000000..a977567 --- /dev/null +++ b/docs/reqs/HighLevelReqs.md @@ -0,0 +1,47 @@ +# High-Level Requirements + +## HLR-001: OPC UA Server + +The system shall expose an OPC UA server endpoint that OPC UA clients can connect to for browsing, reading, and writing Galaxy tag data. + +## HLR-002: Galaxy Hierarchy as OPC UA Address Space + +The system shall build an OPC UA address space that mirrors the System Platform Galaxy object hierarchy, using contained names for browse structure and tag names for runtime data access. + +## HLR-003: MXAccess Runtime Data Access + +The system shall use the MXAccess toolkit to subscribe to, read, and write Galaxy tag attribute values at runtime on behalf of connected OPC UA clients. + +## HLR-004: Data Type Mapping + +The system shall map Galaxy attribute data types (mx_data_type) to appropriate OPC UA built-in types, including support for array attributes. + +## HLR-005: Dynamic Address Space Rebuild + +The system shall detect Galaxy deployment changes (via `galaxy.time_of_last_deploy`) and rebuild the OPC UA address space to reflect the current deployed state. + +## HLR-006: Windows Service Hosting + +The system shall run as a Windows service (via TopShelf) with support for install, uninstall, and interactive console modes. + +## HLR-007: Logging + +The system shall log operational events to rolling daily log files using Serilog. + +## HLR-008: Connection Resilience + +The system shall automatically reconnect to MXAccess after connection loss, replaying active subscriptions upon reconnect. + +## HLR-009: Status Dashboard + +The system shall host an embedded HTTP status dashboard (similar to the LmxProxy dashboard) providing at-a-glance operational visibility including connection state, health, subscription statistics, and operation metrics. + +## Component-Level Requirements + +Detailed requirements are broken out into the following documents: + +- [OPC UA Server Requirements](OpcUaServerReqs.md) +- [MXAccess Client Requirements](MxAccessClientReqs.md) +- [Galaxy Repository Requirements](GalaxyRepositoryReqs.md) +- [Service Host Requirements](ServiceHostReqs.md) +- [Status Dashboard Requirements](StatusDashboardReqs.md) diff --git a/docs/reqs/MxAccessClientReqs.md b/docs/reqs/MxAccessClientReqs.md new file mode 100644 index 0000000..6ea40ee --- /dev/null +++ b/docs/reqs/MxAccessClientReqs.md @@ -0,0 +1,172 @@ +# MXAccess Client — Component Requirements + +Parent: [HLR-003](HighLevelReqs.md#hlr-003-mxaccess-runtime-data-access), [HLR-008](HighLevelReqs.md#hlr-008-connection-resilience) + +## MXA-001: STA Thread with Message Pump + +All MXAccess COM objects shall be created and called on a dedicated STA thread running a Win32 message pump to ensure COM callbacks are delivered. + +### Acceptance Criteria + +- A dedicated thread is created with `ApartmentState.STA` before any MXAccess COM objects are instantiated. +- The thread runs a Win32 message pump using `GetMessage`/`TranslateMessage`/`DispatchMessage` loop. +- Work items are marshalled to the STA thread via `PostThreadMessage(WM_APP)` and a concurrent queue. +- The STA thread processes work items between message pump iterations. +- All COM object creation (`LMXProxyServer` constructor), method calls, and event callbacks happen on this thread. + +### Details + +- Thread name: `MxAccess-STA` (for diagnostics). +- If the STA thread dies unexpectedly, log Fatal and trigger service shutdown. Do not attempt to create a replacement thread (COM objects on the dead thread are unrecoverable). +- `RunAsync(Action)` method returns a `Task` that completes when the action executes on the STA thread. Callers can `await` it. + +--- + +## MXA-002: Connection Lifecycle + +The client shall support Register/Unregister lifecycle with the LMXProxyServer COM object, tracking the connection handle. + +### Acceptance Criteria + +- `Register(clientName)` is called on the STA thread and returns a positive connection handle on success. +- If Register returns handle <= 0, throw with descriptive error. +- `Unregister(handle)` is called during disconnect after all subscriptions are removed. +- Client name: configurable via `MxAccess:ClientName`, default `LmxOpcUa`. Must be unique per MXAccess registration. +- Connection state transitions: Disconnected → Connecting → Connected → Disconnecting → Disconnected (and Error from any state). + +### Details + +- `ConnectedSince` timestamp (UTC) is recorded after successful Register. +- `ReconnectCount` is tracked for diagnostics and dashboard display. +- State change events are raised for dashboard and health check consumption. + +--- + +## MXA-003: Tag Subscription + +The client shall support subscribing to tags via AddItem + AdviseSupervisory, receiving value updates through OnDataChange callbacks. + +### Acceptance Criteria + +- Subscribe sequence: `AddItem(handle, address)` returns item handle, then `AdviseSupervisory(handle, itemHandle)` starts the subscription. +- `OnDataChange` callback delivers value, quality (integer), timestamp, and MXSTATUS_PROXY array. +- Item address format: `tag_name.AttributeName` for scalars, `tag_name.AttributeName[]` for whole arrays. +- If AddItem fails (e.g., tag does not exist), log Warning and return failure to caller. +- Bidirectional maps of `address ↔ itemHandle` are maintained for callback resolution. + +### Details + +- Use `AdviseSupervisory` (not `Advise`) because this is a background service with no interactive user session. AdviseSupervisory allows secured/verified writes without user authentication. +- Stored subscriptions dictionary maps address to callback for reconnect replay. +- On reconnect, all entries in stored subscriptions are re-subscribed (AddItem + AdviseSupervisory with new handles). + +--- + +## MXA-004: Tag Read/Write + +The client shall support synchronous-style read and write operations, marshalled to the STA thread, with configurable timeouts. + +### Acceptance Criteria + +- Read: implemented as subscribe-get-first-value-unsubscribe pattern (AddItem → AdviseSupervisory → wait for OnDataChange → UnAdvise → RemoveItem). +- Write: AddItem → AdviseSupervisory → `Write()` → await `OnWriteComplete` callback → cleanup. +- Read timeout: configurable via `MxAccess:ReadTimeoutSeconds`, default 5 seconds. +- Write timeout: configurable via `MxAccess:WriteTimeoutSeconds`, default 5 seconds. On timeout, log Warning and return timeout error. +- Concurrent operation limit: configurable semaphore via `MxAccess:MaxConcurrentOperations`, default 10. +- All operations marshalled to the STA thread. + +### Details + +- Write uses security classification -1 (no security). Galaxy runtime handles security enforcement. +- `OnWriteComplete` callback: check MXSTATUS_PROXY `success` field. If 0, extract detail code and propagate error. +- COM exceptions (`COMException` with HRESULT) are caught and translated to meaningful error messages. + +--- + +## MXA-005: Auto-Reconnect + +The client shall monitor connection health and automatically reconnect on failure, replaying all stored subscriptions after reconnect. + +### Acceptance Criteria + +- Monitor loop runs on a background thread, checking connection health at configurable interval (`MxAccess:MonitorIntervalSeconds`, default 5 seconds). +- If disconnected, attempt reconnect. On success, replay all stored subscriptions. +- On reconnect failure, log Warning and retry at next interval (no exponential backoff — reconnect as quickly as possible on a plant-floor service). +- Reconnect count is incremented on each successful reconnect. +- Monitor loop is cancellable (for clean shutdown). + +### Details + +- Reconnect cleans up old COM objects before creating new ones. +- After reconnect, probe subscription is re-established first, then stored subscriptions. +- No max retry limit — keep trying indefinitely until service is stopped. + +--- + +## MXA-006: Probe-Based Health Monitoring + +The client shall optionally subscribe to a configurable probe tag and use OnDataChange callback staleness to detect silent connection failures. + +### Acceptance Criteria + +- Subscribe to a configurable probe tag (a known-good Galaxy attribute that changes periodically). +- Track `_lastProbeValueTime` (UTC) updated on each OnDataChange for the probe tag. +- If `DateTime.UtcNow - _lastProbeValueTime > staleThreshold`, force disconnect and reconnect. +- Probe tag address: configurable via `MxAccess:ProbeTag`. If not configured, probe monitoring is disabled. +- Stale threshold: configurable via `MxAccess:ProbeStaleThresholdSeconds`, default 60 seconds. + +### Details + +- The probe tag should be an attribute that the Galaxy runtime updates regularly (e.g., a platform heartbeat or area-level timestamp). The specific tag is site-dependent. +- After forced reconnect, reset `_lastProbeValueTime` to `DateTime.UtcNow` to give the new connection a full threshold window. + +--- + +## MXA-007: COM Cleanup + +On disconnect or disposal, the client shall unwire event handlers, unadvise/remove all items, unregister, and release COM objects via Marshal.ReleaseComObject. + +### Acceptance Criteria + +- Cleanup order: UnAdvise all active subscriptions → RemoveItem all items → unwire OnDataChange and OnWriteComplete event handlers → Unregister → `Marshal.ReleaseComObject`. +- On dispose: run disconnect if still connected, then dispose STA thread. +- Each cleanup step is wrapped in try/catch (cleanup must not throw). +- After cleanup: handle maps are cleared, pending write TCS entries are abandoned, COM reference is set to null. + +### Details + +- `_storedSubscriptions` is NOT cleared on disconnect (preserved for reconnect replay). Only cleared on Dispose. +- Event handlers must be unwired BEFORE Unregister, or callbacks may fire on a dead object. +- `Marshal.ReleaseComObject` in a finally block, always, even if earlier steps fail. + +--- + +## MXA-008: Operation Metrics + +The MXAccess client shall record timing and success/failure for Read, Write, and Subscribe operations. + +### Acceptance Criteria + +- Each operation records: duration (ms), success/failure. +- Metrics are available for the status dashboard: count, success rate, avg/min/max/P95 latency. +- Uses a rolling 1000-entry buffer for percentile calculation. +- Metrics are exposed via a queryable interface consumed by the status report service. + +### Details + +- Uses an `ITimingScope` pattern: `using (var scope = metrics.BeginOperation("read")) { ... }` for automatic timing and success tracking. +- Metrics are periodically logged at Debug level for diagnostics. + +--- + +## MXA-009: Error Code Translation + +The client shall translate known MXAccess error codes from MXSTATUS_PROXY.detail into human-readable messages for logging and OPC UA status propagation. + +### Acceptance Criteria + +- Error 1008 → "User lacks security permission" +- Error 1012 → "Secured write required (one signature)" +- Error 1013 → "Verified write required (two signatures)" +- Unknown error codes are logged with their numeric value. +- Translated messages are included in OPC UA StatusCode descriptions and log entries. diff --git a/docs/reqs/OpcUaServerReqs.md b/docs/reqs/OpcUaServerReqs.md new file mode 100644 index 0000000..b56a7ee --- /dev/null +++ b/docs/reqs/OpcUaServerReqs.md @@ -0,0 +1,229 @@ +# OPC UA Server — Component Requirements + +Parent: [HLR-001](HighLevelReqs.md#hlr-001-opc-ua-server), [HLR-002](HighLevelReqs.md#hlr-002-galaxy-hierarchy-as-opc-ua-address-space), [HLR-004](HighLevelReqs.md#hlr-004-data-type-mapping) + +## OPC-001: Server Endpoint + +The OPC UA server shall listen on a configurable TCP port (default 4840) using the OPC Foundation .NET Standard stack. + +### Acceptance Criteria + +- Server starts and accepts TCP connections on the configured port. +- Port is read from `appsettings.json` under `OpcUa:Port`; defaults to 4840 if absent. +- Endpoint URL format: `opc.tcp://:/LmxOpcUa`. +- If the port is in use at startup, log an Error and fail to start (do not silently pick another port). +- Security policy: None (no certificate validation). This is an internal plant-floor service. + +### Details + +- Configurable items: port (default 4840), endpoint path (default `/LmxOpcUa`), server application name (default `LmxOpcUa`). +- Server shall use the `OPCFoundation.NetStandard.Opc.Ua.Server` NuGet package. +- On startup, log the endpoint URL at Information level. + +--- + +## OPC-002: Address Space Structure + +The server shall create folder nodes for areas and object nodes for automation objects, organized in the same parent-child hierarchy as the Galaxy. + +### Acceptance Criteria + +- The root folder node has BrowseName `ZB` (hardcoded Galaxy name). +- Objects where `is_area = 1` are created as FolderType nodes (organizational). +- Objects where `is_area = 0` are created as BaseObjectType nodes. +- Parent-child relationships use Organizes references (for areas) and HasComponent references (for contained objects). +- A client browsing Root → Objects → ZB → DEV → TestArea → TestMachine_001 → DelmiaReceiver sees the same structure as `gr/layout.md`. + +### Details + +- NodeIds use a string-based identifier scheme: `ns=1;s=` for object nodes, `ns=1;s=.` for variable nodes. +- Infrastructure objects (AppEngines, Platforms) are included in the tree but may have no variable children. +- When `contained_name` is null or empty, fall back to `tag_name` as the BrowseName. + +--- + +## OPC-003: Variable Nodes for Attributes + +Each user-defined attribute on a deployed object shall be represented as an OPC UA variable node under its parent object node. + +### Acceptance Criteria + +- Each row from `attributes.sql` creates one variable node under the matching object node (matched by `gobject_id`). +- Variable node BrowseName and DisplayName are set to `attribute_name`. +- Variable node stores `full_tag_reference` as its runtime MXAccess address. +- Variable nodes have AccessLevel = CurrentRead | CurrentWrite (3) by default. +- Objects with no user-defined attributes still appear as object nodes with zero children. + +### Details + +- Security classification from the attributes query is noted but not enforced at the OPC UA level (Galaxy runtime handles security). +- Attributes whose names start with `_` are already filtered by the SQL query. + +--- + +## OPC-004: Browse Name Translation + +Browse names shall use contained names (human-readable, scoped to parent). The server shall internally translate browse paths to tag_name references for MXAccess operations. + +### Acceptance Criteria + +- A variable node browsed as `ZB/DEV/TestArea/TestMachine_001/DelmiaReceiver/DownloadPath` correctly translates to MXAccess reference `DelmiaReceiver_001.DownloadPath`. +- Translation uses the `tag_name` stored on the parent object node, not the browse path. +- No runtime path parsing — the mapping is baked into each node at build time. + +### Details + +- Each variable node stores its `full_tag_reference` (e.g., `DelmiaReceiver_001.DownloadPath`) at address-space build time. Read/write operations use this stored reference directly. + +--- + +## OPC-005: Data Type Mapping + +Variable nodes shall use OPC UA data types mapped from Galaxy mx_data_type values per the mapping in `gr/data_type_mapping.md`. + +### Acceptance Criteria + +- Every `mx_data_type` value in the mapping table produces the correct OPC UA DataType NodeId on the variable node. +- Unknown/unmapped `mx_data_type` values default to String (i=12). +- ElapsedTime (type 7) maps to Double representing seconds. + +### Details + +- Full mapping table in `gr/data_type_mapping.md`. +- DateTime conversion: Galaxy may store local time; convert to UTC for OPC UA. +- LocalizedText (type 15): use empty locale string with the text value. + +--- + +## OPC-006: Array Support + +Attributes marked as arrays shall have ValueRank=1 and ArrayDimensions set to the attribute's array_dimension value. + +### Acceptance Criteria + +- `is_array = 1` produces ValueRank = 1 (OneDimension) and ArrayDimensions = `[array_dimension]`. +- `is_array = 0` produces ValueRank = -1 (Scalar) and no ArrayDimensions. +- MXAccess reference for array attributes uses `tag_name.attribute[]` (whole array) format. + +### Details + +- Individual array element access (`tag_name.attribute[n]`) is not required for initial implementation. Whole-array read/write only. +- If `array_dimension` is null or 0 when `is_array = 1`, log a Warning and default to ArrayDimensions = [0] (variable-length). + +--- + +## OPC-007: Read Operations + +The server shall fulfill OPC UA Read requests by reading the corresponding tag value from MXAccess using the tag_name.AttributeName reference. + +### Acceptance Criteria + +- OPC UA Read request for a variable node results in a read via MXAccess using the node's stored `full_tag_reference`. +- Returned value is converted from the COM variant to the OPC UA data type specified on the node. +- OPC UA StatusCode reflects MXAccess quality: Good maps to Good, Bad/Uncertain map appropriately. +- If MXAccess is not connected, return StatusCode = Bad_NotConnected. +- Read timeout: configurable, default 5 seconds. On timeout, return Bad_Timeout. + +### Details + +- Prefer cached subscription-delivered values over on-demand reads to reduce COM round-trips. +- If no subscription is active for the tag, perform an on-demand read (AddItem, AdviseSupervisory, wait for first OnDataChange, then UnAdvise/RemoveItem). +- Concurrency: semaphore-limited to configurable max (default 10) concurrent MXAccess operations. + +--- + +## OPC-008: Write Operations + +The server shall fulfill OPC UA Write requests by writing to the corresponding tag via MXAccess. + +### Acceptance Criteria + +- OPC UA Write request results in an MXAccess `Write()` call with completion confirmed via `OnWriteComplete()` callback. +- Write timeout: configurable, default 5 seconds. On timeout, log Warning and return Bad_Timeout. +- MXSTATUS_PROXY with `success = 0` causes the OPC UA write to return Bad_InternalError with the detail message. +- MXAccess errors 1008 (no permission), 1012 (secured write), 1013 (verified write) return Bad_UserAccessDenied. +- Write to a non-existent tag returns Bad_NodeIdUnknown. +- The server shall attempt to convert the written value to the expected Galaxy data type before passing to Write(). + +### Details + +- Write uses security classification -1 (no security). Galaxy runtime handles security enforcement. +- Write sequence: uses existing subscription handle if available, otherwise AddItem + AdviseSupervisory + Write + await OnWriteComplete + cleanup. +- Concurrent write limit: same semaphore as reads (configurable, default 10). + +--- + +## OPC-009: Subscriptions + +The server shall support OPC UA subscriptions by mapping them to MXAccess advisory subscriptions and forwarding data change notifications. + +### Acceptance Criteria + +- OPC UA CreateMonitoredItems results in MXAccess `AdviseSupervisory()` subscriptions for the requested tags. +- Data changes from `OnDataChange` callback are forwarded as OPC UA notifications to all subscribed clients. +- Shared subscriptions: if two OPC UA clients subscribe to the same tag, only one MXAccess subscription exists (ref-counted). +- Last subscriber unsubscribing triggers UnAdvise/RemoveItem on the MXAccess side. +- After MXAccess reconnect, all active MXAccess subscriptions are re-established automatically. + +### Details + +- Publishing interval from the OPC UA subscription request is honored on the OPC UA side; MXAccess delivers changes as fast as it receives them. +- OPC UA quality mapping from MXAccess quality integers: 192+ = Good, 64-191 = Uncertain, 0-63 = Bad. +- OnDataChange with MXSTATUS_PROXY failure: deliver notification with Bad quality to subscribed clients. + +--- + +## OPC-010: Address Space Rebuild + +When a Galaxy deployment change is detected, the server shall rebuild the address space without dropping existing OPC UA client connections where possible. + +### Acceptance Criteria + +- When Galaxy Repository detects a deployment change, the OPC UA address space is rebuilt. +- Existing OPC UA client sessions are preserved — clients stay connected. +- Subscriptions for tags that still exist after rebuild continue to work. +- Subscriptions for tags that no longer exist receive a Bad_NodeIdUnknown status notification. +- Rebuild is logged at Information level with timing (duration). + +### Details + +- Rebuild is a full replace, not an incremental diff. Re-query hierarchy and attributes, build new tree, swap atomically. +- During rebuild, reads/writes against the old address space may fail briefly. This is acceptable. +- New MXAccess subscriptions for new tags are established; removed tags are unsubscribed. + +--- + +## OPC-011: Server Diagnostics Node + +The server shall expose a ServerStatus node under the standard OPC UA Server object with ServerState, CurrentTime, and StartTime. This is required by the OPC UA specification for compliant servers. + +### Acceptance Criteria + +- ServerState reports Running during normal operation. +- CurrentTime returns the server's current UTC time. +- StartTime returns the UTC time when the service started. + +--- + +## OPC-012: Namespace Configuration + +The server shall register a namespace URI at namespace index 1. All application-specific NodeIds shall use this namespace. + +### Acceptance Criteria + +- Namespace URI: `urn:ZB:LmxOpcUa` (Galaxy name is configurable). +- All object and variable NodeIds created from Galaxy data use namespace index 1. +- Standard OPC UA nodes remain in namespace 0. + +--- + +## OPC-013: Session Management + +The server shall support multiple concurrent OPC UA client sessions. + +### Acceptance Criteria + +- Maximum concurrent sessions: configurable, default 100. +- Session timeout: configurable, default 30 minutes of inactivity. +- Expired sessions are cleaned up and their subscriptions removed. +- Session count is reported to the status dashboard. diff --git a/docs/reqs/ServiceHostReqs.md b/docs/reqs/ServiceHostReqs.md new file mode 100644 index 0000000..5a7abbf --- /dev/null +++ b/docs/reqs/ServiceHostReqs.md @@ -0,0 +1,117 @@ +# Service Host — Component Requirements + +Parent: [HLR-006](HighLevelReqs.md#hlr-006-windows-service-hosting), [HLR-007](HighLevelReqs.md#hlr-007-logging) + +## SVC-001: TopShelf Hosting + +The application shall use TopShelf for Windows service lifecycle (install, uninstall, start, stop) and interactive console mode for development. + +### Acceptance Criteria + +- TopShelf HostFactory configures the service with name `LmxOpcUa`, display name `LMX OPC UA Server`. +- Service installs via command line: `ZB.MOM.WW.LmxOpcUa.Host.exe install`. +- Service uninstalls via: `ZB.MOM.WW.LmxOpcUa.Host.exe uninstall`. +- Service runs as LocalSystem account (needed for MXAccess COM access and Windows Auth to SQL Server). +- Interactive console mode (exe with no args) works for development/debugging. +- `StartAutomatically` is set for Windows service registration. + +### Details + +- Platform target: x86 (32-bit) — required for MXAccess COM interop. +- Service description: "OPC UA server exposing System Platform Galaxy tags via MXAccess." + +--- + +## SVC-002: Serilog Logging + +The application shall configure Serilog with a rolling daily file sink and console sink, with log files retained for a configurable number of days (default 31). + +### Acceptance Criteria + +- Console sink active (for interactive/debug mode). +- Rolling daily file sink writing to `logs/lmxopcua-YYYYMMDD.log`. +- Retained file count: configurable, default 31 days. +- Minimum log level: configurable, default Information. +- Log file path: configurable, default `logs/lmxopcua-.log`. +- Serilog is initialized before any other component (first thing in Main). +- `Log.CloseAndFlush()` called in finally block on exit. + +### Details + +- Structured logging with Serilog message templates (not string.Format). +- Log output includes timestamp, level, source context, message, and exception. +- Fatal exceptions are caught at the top level and logged before exit. + +--- + +## SVC-003: Configuration + +The application shall load configuration from appsettings.json with support for environment-specific overrides (appsettings.*.json) and environment variables. + +### Acceptance Criteria + +- `appsettings.json` is the primary configuration file. +- Environment-specific overrides via `appsettings.{environment}.json`. +- Configuration sections: `OpcUa`, `MxAccess`, `GalaxyRepository`, `Dashboard`. +- Missing optional configuration keys use documented defaults (service does not crash). +- Invalid configuration (e.g., port = -1) is detected at startup with a clear error message. + +### Details + +- Config is loaded once at startup. No hot-reload (service restart required for config changes). This is appropriate for an industrial service. +- All configurable values and their defaults are documented in `appsettings.json`. + +--- + +## SVC-004: Graceful Shutdown + +On service stop, the application shall gracefully shut down all components and flush logs before exiting. + +### Acceptance Criteria + +- TopShelf WhenStopped triggers orderly shutdown. +- Shutdown sequence: (1) stop change detection polling, (2) stop OPC UA server (stop accepting new sessions, complete pending operations), (3) disconnect MXAccess (cleanup all COM objects), (4) stop status dashboard HTTP listener, (5) flush Serilog. +- Shutdown completes within 30 seconds (Windows SCM timeout). +- All IDisposable components are disposed in reverse-creation order. + +### Details + +- `CancellationTokenSource` signals all background loops (monitor, change detection, HTTP listener) to stop. +- Log "Service shutdown complete" at Information level as the final log entry before flush. + +--- + +## SVC-005: Startup Sequence + +The service shall start components in a defined order, with failure handling at each step. + +### Acceptance Criteria + +- Startup sequence: + 1. Load configuration + 2. Initialize Serilog + 3. Start STA thread + 4. Connect to MXAccess + 5. Query Galaxy Repository for initial build + 6. Build OPC UA address space + 7. Start OPC UA server listener + 8. Start change detection polling + 9. Start status dashboard HTTP listener +- Failure in steps 1-4 prevents startup (service fails to start). +- Failure in steps 5-9 logs Error but allows the service to run in degraded mode. + +### Details + +- Degraded mode means the service is running but may have an empty address space (waiting for Galaxy DB) or no dashboard (port conflict). MXAccess connection is the minimum required for the service to be useful. + +--- + +## SVC-006: Unhandled Exception Handling + +The service shall handle unexpected crashes gracefully. + +### Acceptance Criteria + +- Register `AppDomain.CurrentDomain.UnhandledException` handler that logs Fatal before the process terminates. +- TopShelf service recovery is configured: restart on failure with 60-second delay. +- Fatal-level log entry includes the full exception details. diff --git a/docs/reqs/StatusDashboardReqs.md b/docs/reqs/StatusDashboardReqs.md new file mode 100644 index 0000000..6a76fd0 --- /dev/null +++ b/docs/reqs/StatusDashboardReqs.md @@ -0,0 +1,157 @@ +# Status Dashboard — Component Requirements + +Parent: [HLR-009](HighLevelReqs.md#hlr-009-status-dashboard) + +Reference: LmxProxy Status Dashboard (see `dashboard.JPG` in project root). + +## DASH-001: Embedded HTTP Endpoint + +The service shall host a lightweight HTTP listener on a configurable port serving a self-contained HTML status dashboard page (no external dependencies). + +### Acceptance Criteria + +- Uses `System.Net.HttpListener` on a configurable port (`Dashboard:Port`, default 8081). +- Routes: + - `GET /` → HTML dashboard + - `GET /api/status` → JSON status report + - `GET /api/health` → 200 OK if healthy, 503 if unhealthy +- Only GET requests accepted; other methods return 405. +- Unknown paths return 404. +- All responses include `Cache-Control: no-cache, no-store, must-revalidate` headers. +- Dashboard can be disabled via config (`Dashboard:Enabled`, default true). + +### Details + +- HTTP prefix: `http://+:{port}/` to bind to all interfaces. +- If HttpListener fails to start (port conflict, missing URL reservation), log Error and continue service startup without the dashboard. +- HTML page is self-contained: inline CSS, no external resources (no CDN, no JavaScript frameworks). + +--- + +## DASH-002: Connection Panel + +The dashboard shall display a Connection panel showing MXAccess connection state. + +### Acceptance Criteria + +- Shows: **Connected** (True/False), **State** (Connected/Disconnected/Reconnecting/Error), **Connected Since** (UTC timestamp). +- Green left border when Connected, red when Disconnected/Error, yellow when Reconnecting. +- "Connected Since" shows "N/A" when not connected. +- Data sourced from MXAccess client's connection state properties. + +### Details + +- Timestamp format: `yyyy-MM-dd HH:mm:ss UTC`. +- Panel title: "Connection". + +--- + +## DASH-003: Health Panel + +The dashboard shall display a Health panel showing overall service health. + +### Acceptance Criteria + +- Three states: **Healthy** (green text), **Degraded** (yellow text), **Unhealthy** (red text). +- Includes a health message string explaining the status. +- Health rules: + - Not connected to MXAccess → Unhealthy + - Success rate < 50% with > 100 total operations → Degraded + - Connected with acceptable success rate → Healthy + +### Details + +- Health message examples: "LmxOpcUa is healthy", "MXAccess client is not connected", "Average success rate is below 50%". +- Green left border for Healthy, yellow for Degraded, red for Unhealthy. + +--- + +## DASH-004: Subscriptions Panel + +The dashboard shall display a Subscriptions panel showing subscription statistics. + +### Acceptance Criteria + +- Shows: **Clients** (connected OPC UA client count), **Tags** (total variable nodes in address space), **Active** (active MXAccess subscriptions), **Delivered** (cumulative data change notifications delivered). +- Values update on each dashboard refresh. +- Zero values shown as "0", not blank. + +### Details + +- "Tags" is the count of variable nodes, not object/folder nodes. +- "Active" is the count of distinct MXAccess item subscriptions (after ref-counting — the number of actual AdviseSupervisory calls, not the number of OPC UA monitored items). +- "Delivered" is a running counter since service start (not reset on reconnect). + +--- + +## DASH-005: Operations Table + +The dashboard shall display an operations metrics table showing performance statistics. + +### Acceptance Criteria + +- Table with columns: **Operation**, **Count**, **Success Rate**, **Avg (ms)**, **Min (ms)**, **Max (ms)**, **P95 (ms)**. +- Rows: Read, Write, Subscribe, Browse. +- Empty cells show em-dash ("—") when no data available (count = 0). +- Success rate displayed as percentage (e.g., "99.8%"). +- Latency values rounded to 1 decimal place. + +### Details + +- Metrics sourced from the PerformanceMetrics component (1000-entry rolling buffer for percentile calculation). +- "Browse" row tracks OPC UA browse operations. +- "Subscribe" row tracks OPC UA CreateMonitoredItems operations. + +--- + +## DASH-006: Footer + +The dashboard shall display a footer with last-updated time and service identification. + +### Acceptance Criteria + +- Format: "Last updated: {timestamp} UTC | Service: ZB.MOM.WW.LmxOpcUa.Host v{version}". +- Timestamp is the server-side UTC time when the HTML was generated. +- Version is read from the assembly version (`Assembly.GetExecutingAssembly().GetName().Version`). + +--- + +## DASH-007: Auto-Refresh + +The dashboard page shall auto-refresh to show current status without manual reload. + +### Acceptance Criteria + +- HTML page includes `` for 10-second auto-refresh. +- No JavaScript required for refresh (pure HTML meta-refresh). +- Refresh interval: configurable via `Dashboard:RefreshIntervalSeconds`, default 10 seconds. + +--- + +## DASH-008: JSON Status API + +The `/api/status` endpoint shall return a JSON object with all dashboard data for programmatic consumption. + +### Acceptance Criteria + +- Response Content-Type: `application/json`. +- JSON structure includes: connection state, health status, subscription statistics, and operation metrics. +- Same data as the HTML dashboard, structured for machine consumption. +- Suitable for integration with external monitoring tools. + +--- + +## DASH-009: Galaxy Info Panel + +The dashboard shall display a Galaxy Info panel showing Galaxy Repository state. + +### Acceptance Criteria + +- Shows: **Galaxy Name** (e.g., ZB), **DB Status** (Connected/Disconnected), **Last Deploy** (timestamp from `galaxy.time_of_last_deploy`), **Objects** (count), **Attributes** (count), **Last Rebuild** (timestamp of last address space rebuild). +- Provides visibility into the Galaxy Repository component's state independently of MXAccess connection status. + +### Details + +- "DB Status" reflects whether the most recent change detection poll succeeded. +- "Last Deploy" shows the raw `time_of_last_deploy` value from the Galaxy database. +- "Objects" and "Attributes" show counts from the most recent successful hierarchy/attribute query. diff --git a/gr/CLAUDE.md b/gr/CLAUDE.md new file mode 100644 index 0000000..550643e --- /dev/null +++ b/gr/CLAUDE.md @@ -0,0 +1,51 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Purpose + +The goal of this project is to identify and develop SQL queries that extract the Galaxy object hierarchy from the **System Platform Galaxy Repository** database in order to build a tag structure for an OPC UA server. + +Specifically, we need to: +- Build the hierarchy of **areas** and **automation objects** (using contained names for human-readable browsing) +- Translate contained names to **tag_names** for read/write operations (e.g., `TestMachine_001.DelmiaReceiver` in the hierarchy becomes `DelmiaReceiver_001` when addressing tag values) + +See `layout.md` for details on the hierarchy vs tag name relationship. + +## Key Files + +### Documentation +- `connectioninfo.md` — Database connection details and sqlcmd usage +- `layout.md` — Galaxy object hierarchy, contained_name vs tag_name translation, and target OPC UA structure +- `build_layout_plan.md` — Step-by-step plan for extracting hierarchy, attaching attributes, and monitoring for changes +- `data_type_mapping.md` — Galaxy mx_data_type to OPC UA DataType mapping, including array handling (ValueRank, ArrayDimensions) + +### Queries +- `queries/hierarchy.sql` — Deployed object hierarchy with browse names and parent relationships +- `queries/attributes.sql` — User-defined (dynamic) attributes with data types and array dimensions +- `queries/attributes_extended.sql` — All attributes (system + user-defined) with data types and array dimensions +- `queries/change_detection.sql` — Poll `galaxy.time_of_last_deploy` to detect deployment changes + +### Schema Reference +- `schema.md` — Full schema reference for all tables and views in the ZB database +- `ddl/tables/` — Individual CREATE TABLE definitions +- `ddl/views/` — Individual view definitions + +## Working with the Galaxy Repository Database + +The Galaxy Repository is the backing SQL Server database for Wonderware/AVEVA System Platform (Galaxy: ZB, localhost, Windows Auth). Key tables used by the queries: + +- **gobject** — Object instances, hierarchy (contained_by_gobject_id, area_gobject_id), deployment state (deployed_package_id) +- **template_definition** — Object type categories (category_id distinguishes areas, engines, user-defined objects, etc.) +- **dynamic_attribute** — User-defined attributes on templates, inherited by instances via derived_from_gobject_id chain +- **attribute_definition** — System/primitive attributes +- **primitive_instance** — Links objects to their primitive components and attribute definitions +- **galaxy** — Single-row table with time_of_last_deploy for change detection + +Use `sqlcmd -S localhost -d ZB -E -Q "..."` to run queries. See `connectioninfo.md` for details. + +## Conventions + +- Store all connection parameters in `connectioninfo.md`, not scattered across scripts. +- Keep SQL query examples and extraction notes as Markdown files in this repo. +- If scripts are added (Python, PowerShell, etc.), document their usage and dependencies alongside them. diff --git a/gr/build_layout_plan.md b/gr/build_layout_plan.md new file mode 100644 index 0000000..ea5f916 --- /dev/null +++ b/gr/build_layout_plan.md @@ -0,0 +1,84 @@ +# OPC UA Server Layout — Build Plan + +## Overview + +Extract the Galaxy object hierarchy and tag definitions from the ZB (Galaxy Repository) database to construct an OPC UA server address space. The root node is hardcoded as **ZB**. + +## Step 1: Build the Browse Tree + +Run `queries/hierarchy.sql` to get all deployed automation objects and their parent-child relationships. + +For each row returned: +- `parent_gobject_id = 0` → child of the root ZB node +- `is_area = 1` → create as an OPC UA folder node (organizational) +- `is_area = 0` → create as an OPC UA object node (container for tags) +- Use `browse_name` as the OPC UA BrowseName/DisplayName +- Store `gobject_id` and `tag_name` for attribute lookup and tag reference translation + +Build the tree by matching each row's `parent_gobject_id` to another row's `gobject_id`. The result is: + +``` +ZB (root, hardcoded) +└── DEV (folder, is_area=1) + ├── DevAppEngine (object) + ├── DevPlatform (object) + └── TestArea (folder, is_area=1) + ├── DevTestObject (object) + └── TestMachine_001 (object) + ├── DelmiaReceiver (object, browse_name from contained_name) + └── MESReceiver (object, browse_name from contained_name) +``` + +## Step 2: Attach Attributes as Tag Nodes + +Run `queries/attributes.sql` to get all user-defined attributes for deployed objects. + +For each attribute row: +- Match to the browse tree via `gobject_id` +- Create an OPC UA variable node under the matching object node +- Use `attribute_name` as the BrowseName/DisplayName +- Use `full_tag_reference` as the runtime tag path for read/write operations +- Map `mx_data_type` to OPC UA built-in types: + +| mx_data_type | Description | OPC UA Type | +|--------------|-------------|-------------| +| 1 | Boolean | Boolean | +| 2 | Integer | Int32 | +| 3 | Float | Float | +| 4 | Double | Double | +| 5 | String | String | +| 6 | Time | DateTime | +| 7 | ElapsedTime | Double (seconds) or Duration | + +- If `is_array = 1`, create the variable as an array with rank 1 and dimension from `array_dimension` + +## Step 3: Monitor for Changes + +Poll `queries/change_detection.sql` on a regular interval (e.g., every 30 seconds). + +``` +SELECT time_of_last_deploy FROM galaxy; +``` + +Compare the returned `time_of_last_deploy` to the last known value: +- **No change** → do nothing +- **Changed** → a deployment occurred; re-run Steps 1 and 2 to rebuild the address space + +This handles objects being deployed, undeployed, added, or removed. + +## Connection Details + +See `connectioninfo.md` for database connection parameters and sqlcmd usage. + +``` +sqlcmd -S localhost -d ZB -E -Q "YOUR QUERY HERE" +``` + +## Query Files + +| File | Purpose | +|------|---------| +| `queries/hierarchy.sql` | Deployed object hierarchy with browse names and parent relationships | +| `queries/attributes.sql` | User-defined attributes with data types and array dimensions | +| `queries/attributes_extended.sql` | All attributes (system + user-defined) with data types and array dimensions | +| `queries/change_detection.sql` | Poll galaxy.time_of_last_deploy for deployment changes | diff --git a/gr/connectioninfo.md b/gr/connectioninfo.md new file mode 100644 index 0000000..a615514 --- /dev/null +++ b/gr/connectioninfo.md @@ -0,0 +1,26 @@ +# Galaxy Repository — Connection Information + +## Database Connection + +| Parameter | Value | +|-----------------|----------------| +| Server | localhost (default instance) | +| Database Name | ZB | +| Port | 1433 (default) | +| Authentication | Windows Auth | +| Username | dohertj2 | + +## sqlcmd Usage + +``` +sqlcmd -S localhost -d ZB -E -Q "YOUR QUERY HERE" +``` + +- `-S localhost` — default instance +- `-d ZB` — database name +- `-E` — Windows Authentication (dohertj2) + +## Notes + +- The Galaxy Repository is a SQL Server database created and managed by AVEVA System Platform (formerly Wonderware). +- Typically accessed via SQL Server Management Studio (SSMS), `sqlcmd`, or programmatically via ODBC/ADO.NET/pyodbc. diff --git a/gr/data_type_mapping.md b/gr/data_type_mapping.md new file mode 100644 index 0000000..43d7dc2 --- /dev/null +++ b/gr/data_type_mapping.md @@ -0,0 +1,80 @@ +# Data Type Mapping — Galaxy Repository to OPC UA + +## Scalar Type Mapping + +| mx_data_type | Galaxy Description | OPC UA DataType | OPC UA NodeId | Notes | +|--------------|--------------------|-----------------|---------------|-------| +| 1 | Boolean | Boolean | i=1 | Direct mapping | +| 2 | Integer (Int32) | Int32 | i=6 | Galaxy integers are 32-bit signed | +| 3 | Float (Single) | Float | i=10 | 32-bit IEEE 754 | +| 4 | Double | Double | i=11 | 64-bit IEEE 754 | +| 5 | String | String | i=12 | Unicode string | +| 6 | Time (DateTime) | DateTime | i=13 | Galaxy DateTime to OPC UA DateTime (100ns ticks since 1601-01-01) | +| 7 | ElapsedTime (TimeSpan) | Double | i=11 | No native OPC UA TimeSpan; map to Double representing seconds (or use Duration type alias, NodeId i=290) | +| 8 | (reference) | String | i=12 | Object reference; expose as string representation | +| 13 | (enumeration) | Int32 | i=6 | Enum backing value is integer | +| 14 | (custom) | String | i=12 | Fallback to string | +| 15 | InternationalizedString | LocalizedText | i=21 | OPC UA LocalizedText supports locale + text pairs | +| 16 | (custom) | String | i=12 | Fallback to string | + +## OPC UA Built-in Type Reference + +For context, the full set of OPC UA built-in types and their NodeIds: + +| NodeId | Type | Description | +|--------|------|-------------| +| i=1 | Boolean | True/false | +| i=2 | SByte | Signed 8-bit integer | +| i=3 | Byte | Unsigned 8-bit integer | +| i=4 | Int16 | Signed 16-bit integer | +| i=5 | UInt16 | Unsigned 16-bit integer | +| i=6 | Int32 | Signed 32-bit integer | +| i=7 | UInt32 | Unsigned 32-bit integer | +| i=8 | Int64 | Signed 64-bit integer | +| i=9 | UInt64 | Unsigned 64-bit integer | +| i=10 | Float | 32-bit IEEE 754 | +| i=11 | Double | 64-bit IEEE 754 | +| i=12 | String | Unicode string | +| i=13 | DateTime | Date and time (100ns ticks since 1601-01-01) | +| i=14 | Guid | 128-bit globally unique identifier | +| i=15 | ByteString | Sequence of bytes | +| i=21 | LocalizedText | Locale + text pair | + +## Array Handling + +When `is_array = 1` in the attributes query, the OPC UA variable node must be configured as an array. + +### ValueRank + +Set on the OPC UA variable node to indicate scalar vs array: + +| is_array | ValueRank | Meaning | +|----------|-----------|---------| +| 0 | -1 (Scalar) | Value is not an array | +| 1 | 1 (OneDimension) | Value is a one-dimensional array | + +### ArrayDimensions + +When `ValueRank = 1`, set the `ArrayDimensions` attribute to a single-element array containing the `array_dimension` value from the attributes query. + +Example for `MESReceiver_001.MoveInPartNumbers` (`is_array=1`, `array_dimension=50`): +- DataType: String (i=12) +- ValueRank: 1 +- ArrayDimensions: [50] + +Example for `TestMachine_001.MachineID` (`is_array=0`): +- DataType: String (i=12) +- ValueRank: -1 +- ArrayDimensions: (not set) + +## DateTime Conversion + +Galaxy `Time` (mx_data_type=6) stores DateTime values. OPC UA DateTime is defined as the number of 100-nanosecond intervals since January 1, 1601 (UTC). Ensure the conversion accounts for: +- Timezone: Galaxy may store local time; OPC UA expects UTC +- Epoch difference: adjust if Galaxy uses a different epoch (e.g., Unix epoch 1970-01-01) + +## ElapsedTime Handling + +Galaxy `ElapsedTime` (mx_data_type=7) represents a duration/timespan. OPC UA has no native TimeSpan type. Options: +- **Double (i=11)**: Store as seconds (recommended for simplicity) +- **Duration (i=290)**: OPC UA type alias for Double, semantically represents milliseconds — use if the OPC UA SDK supports it diff --git a/gr/ddl/tables/ConversionQueue.sql b/gr/ddl/tables/ConversionQueue.sql new file mode 100644 index 0000000..3044c68 --- /dev/null +++ b/gr/ddl/tables/ConversionQueue.sql @@ -0,0 +1,13 @@ +-- Table: ConversionQueue +CREATE TABLE [ConversionQueue] ( + [id] int NULL, + [Name] nvarchar(329) NULL, + [IsCheckedOut] bit NOT NULL, + [Status] bit NOT NULL DEFAULT ((0)), + [MetaData] nchar(256) NULL, + [OperationType] nchar(20) NOT NULL, + [timestamp_of_last_change] bigint NULL, + [change_type] int NULL +); +GO + diff --git a/gr/ddl/tables/CurrentSessionContainedName.sql b/gr/ddl/tables/CurrentSessionContainedName.sql new file mode 100644 index 0000000..7e258ea --- /dev/null +++ b/gr/ddl/tables/CurrentSessionContainedName.sql @@ -0,0 +1,9 @@ +-- Table: CurrentSessionContainedName +CREATE TABLE [CurrentSessionContainedName] ( + [Uniqeid] int NOT NULL, + [obj_id] int NULL, + [containedname] nvarchar(32) NULL, + CONSTRAINT [PK_CurrentSessionContainedName] PRIMARY KEY ([Uniqeid]) +); +GO + diff --git a/gr/ddl/tables/ImportTransaction.sql b/gr/ddl/tables/ImportTransaction.sql new file mode 100644 index 0000000..e8a1110 --- /dev/null +++ b/gr/ddl/tables/ImportTransaction.sql @@ -0,0 +1,7 @@ +-- Table: ImportTransaction +CREATE TABLE [ImportTransaction] ( + [ImportOperationId] nvarchar(329) NULL, + [Status] bit NOT NULL DEFAULT ((1)) +); +GO + diff --git a/gr/ddl/tables/aa_sql_objects.sql b/gr/ddl/tables/aa_sql_objects.sql new file mode 100644 index 0000000..33b5918 --- /dev/null +++ b/gr/ddl/tables/aa_sql_objects.sql @@ -0,0 +1,8 @@ +-- Table: aa_sql_objects +CREATE TABLE [aa_sql_objects] ( + [object_name] nvarchar(128) NOT NULL, + [object_type] nvarchar(10) NOT NULL, + CONSTRAINT [PK_aa_sql_objects] PRIMARY KEY ([object_name]) +); +GO + diff --git a/gr/ddl/tables/affected_overview_symbols.sql b/gr/ddl/tables/affected_overview_symbols.sql new file mode 100644 index 0000000..f47a607 --- /dev/null +++ b/gr/ddl/tables/affected_overview_symbols.sql @@ -0,0 +1,9 @@ +-- Table: affected_overview_symbols +CREATE TABLE [affected_overview_symbols] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_id] int NOT NULL +); +GO + diff --git a/gr/ddl/tables/alarm_message_defaults.sql b/gr/ddl/tables/alarm_message_defaults.sql new file mode 100644 index 0000000..dac2099 --- /dev/null +++ b/gr/ddl/tables/alarm_message_defaults.sql @@ -0,0 +1,8 @@ +-- Table: alarm_message_defaults +CREATE TABLE [alarm_message_defaults] ( + [phrase_id] int NOT NULL, + [default_message] nvarchar(1024) NOT NULL, + CONSTRAINT [PK_alarm_message_defaults] PRIMARY KEY ([phrase_id]) +); +GO + diff --git a/gr/ddl/tables/alarm_message_timestamps.sql b/gr/ddl/tables/alarm_message_timestamps.sql new file mode 100644 index 0000000..2eb44fe --- /dev/null +++ b/gr/ddl/tables/alarm_message_timestamps.sql @@ -0,0 +1,8 @@ +-- Table: alarm_message_timestamps +CREATE TABLE [alarm_message_timestamps] ( + [gobject_id] int NOT NULL, + [timestamp_of_populate] bigint NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_alarm_message_timestamps] PRIMARY KEY ([gobject_id]) +); +GO + diff --git a/gr/ddl/tables/alarm_message_translations.sql b/gr/ddl/tables/alarm_message_translations.sql new file mode 100644 index 0000000..c48fb43 --- /dev/null +++ b/gr/ddl/tables/alarm_message_translations.sql @@ -0,0 +1,12 @@ +-- Table: alarm_message_translations +CREATE TABLE [alarm_message_translations] ( + [phrase_id] int NOT NULL, + [locale_id] smallint NOT NULL, + [translated_message] nvarchar(1024) NOT NULL, + CONSTRAINT [PK_alarm_message_translations] PRIMARY KEY ([phrase_id], [locale_id], [phrase_id], [locale_id]) +); +GO + +ALTER TABLE [alarm_message_translations] ADD FOREIGN KEY ([locale_id]) REFERENCES [supported_locales] ([locale_id]); +GO + diff --git a/gr/ddl/tables/alarm_messages.sql b/gr/ddl/tables/alarm_messages.sql new file mode 100644 index 0000000..29f75a8 --- /dev/null +++ b/gr/ddl/tables/alarm_messages.sql @@ -0,0 +1,13 @@ +-- Table: alarm_messages +CREATE TABLE [alarm_messages] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [phrase_id] int NOT NULL, + CONSTRAINT [PK_alarm_messages] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [phrase_id], [gobject_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [alarm_messages] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/attribute_definition.sql b/gr/ddl/tables/attribute_definition.sql new file mode 100644 index 0000000..5caab0c --- /dev/null +++ b/gr/ddl/tables/attribute_definition.sql @@ -0,0 +1,24 @@ +-- Table: attribute_definition +CREATE TABLE [attribute_definition] ( + [attribute_definition_id] int NOT NULL, + [primitive_definition_id] int NOT NULL, + [attribute_name] nvarchar(329) NOT NULL, + [mx_attribute_id] smallint NOT NULL, + [has_config_set_handler] bit NOT NULL, + [mx_data_type] smallint NOT NULL, + [is_array] bit NOT NULL, + [security_classification] smallint NOT NULL, + [security_classification_needs_deployed] bit NOT NULL, + [mx_attribute_category] int NOT NULL, + [is_frequently_accessed] bit NOT NULL, + [is_locked] bit NOT NULL, + [is_locked_needs_deployed] bit NOT NULL, + [mx_value] text(2147483647) NOT NULL, + [mx_value_needs_deployed] bit NOT NULL, + CONSTRAINT [PK_attribute_definition] PRIMARY KEY ([primitive_definition_id], [mx_attribute_id], [primitive_definition_id]) +); +GO + +ALTER TABLE [attribute_definition] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]); +GO + diff --git a/gr/ddl/tables/attribute_reference.sql b/gr/ddl/tables/attribute_reference.sql new file mode 100644 index 0000000..156e221 --- /dev/null +++ b/gr/ddl/tables/attribute_reference.sql @@ -0,0 +1,26 @@ +-- Table: attribute_reference +CREATE TABLE [attribute_reference] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [referring_mx_primitive_id] smallint NOT NULL DEFAULT ((0)), + [referring_mx_attribute_id] smallint NOT NULL DEFAULT ((0)), + [element_index] smallint NOT NULL DEFAULT ((0)), + [resolved_gobject_id] int NOT NULL DEFAULT ((0)), + [reference_string] nvarchar(700) NOT NULL DEFAULT (''), + [context_string] nvarchar(329) NOT NULL DEFAULT (''), + [object_signature] int NOT NULL DEFAULT ((0)), + [resolved_mx_primitive_id] smallint NOT NULL DEFAULT ((0)), + [resolved_mx_attribute_id] smallint NOT NULL DEFAULT ((0)), + [resolved_mx_property_id] smallint NOT NULL DEFAULT ((0)), + [attribute_signature] int NOT NULL DEFAULT ((0)), + [lock_type] int NOT NULL DEFAULT ((0)), + [is_valid] bit NOT NULL DEFAULT ((0)), + [attr_res_status] int NOT NULL DEFAULT ((0)), + [attribute_index] smallint NULL DEFAULT ((-1)), + CONSTRAINT [PK_attribute_reference] PRIMARY KEY ([gobject_id], [package_id], [referring_mx_primitive_id], [referring_mx_attribute_id], [element_index], [gobject_id], [package_id], [referring_mx_primitive_id], [gobject_id], [package_id], [referring_mx_primitive_id], [gobject_id], [package_id], [referring_mx_primitive_id]) +); +GO + +ALTER TABLE [attribute_reference] ADD FOREIGN KEY ([referring_mx_primitive_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/attributes_translation_table.sql b/gr/ddl/tables/attributes_translation_table.sql new file mode 100644 index 0000000..bd23e89 --- /dev/null +++ b/gr/ddl/tables/attributes_translation_table.sql @@ -0,0 +1,11 @@ +-- Table: attributes_translation_table +CREATE TABLE [attributes_translation_table] ( + [gobject_id] int NULL, + [attribute_name] nvarchar(329) NOT NULL, + [new_primitive_id] int NULL, + [new_attribute_id] int NULL, + [old_primitive_id] int NULL, + [old_attribute_id] int NULL +); +GO + diff --git a/gr/ddl/tables/autobind_device.sql b/gr/ddl/tables/autobind_device.sql new file mode 100644 index 0000000..61e1e94 --- /dev/null +++ b/gr/ddl/tables/autobind_device.sql @@ -0,0 +1,11 @@ +-- Table: autobind_device +CREATE TABLE [autobind_device] ( + [dio_id] int NOT NULL, + [overridden_naming_rule_id] int NULL, + CONSTRAINT [PK_autobind_device] PRIMARY KEY ([dio_id], [overridden_naming_rule_id], [dio_id]) +); +GO + +ALTER TABLE [autobind_device] ADD FOREIGN KEY ([dio_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/autobind_device_category.sql b/gr/ddl/tables/autobind_device_category.sql new file mode 100644 index 0000000..148c1fc --- /dev/null +++ b/gr/ddl/tables/autobind_device_category.sql @@ -0,0 +1,11 @@ +-- Table: autobind_device_category +CREATE TABLE [autobind_device_category] ( + [category_id] smallint NOT NULL, + [rule_id] int NULL DEFAULT ((0)), + CONSTRAINT [PK_autobind_device_category] PRIMARY KEY ([category_id], [rule_id], [category_id]) +); +GO + +ALTER TABLE [autobind_device_category] ADD FOREIGN KEY ([category_id]) REFERENCES [lookup_category] ([category_id]); +GO + diff --git a/gr/ddl/tables/autobind_device_template.sql b/gr/ddl/tables/autobind_device_template.sql new file mode 100644 index 0000000..9cd1c5a --- /dev/null +++ b/gr/ddl/tables/autobind_device_template.sql @@ -0,0 +1,11 @@ +-- Table: autobind_device_template +CREATE TABLE [autobind_device_template] ( + [template_definition_id] int NOT NULL, + [rule_id] int NULL, + CONSTRAINT [PK_autobind_device_template] PRIMARY KEY ([template_definition_id], [rule_id], [template_definition_id]) +); +GO + +ALTER TABLE [autobind_device_template] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]); +GO + diff --git a/gr/ddl/tables/autobind_device_topic.sql b/gr/ddl/tables/autobind_device_topic.sql new file mode 100644 index 0000000..a5c4afc --- /dev/null +++ b/gr/ddl/tables/autobind_device_topic.sql @@ -0,0 +1,13 @@ +-- Table: autobind_device_topic +CREATE TABLE [autobind_device_topic] ( + [dio_id] int NOT NULL, + [sg_mx_primitive_id] smallint NOT NULL DEFAULT ((0)), + [overridden_naming_rule_id] int NULL, + [default_xlate_rule_id] int NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_autobind_device_topic] PRIMARY KEY ([dio_id], [sg_mx_primitive_id], [overridden_naming_rule_id], [dio_id]) +); +GO + +ALTER TABLE [autobind_device_topic] ADD FOREIGN KEY ([dio_id]) REFERENCES [autobind_device] ([dio_id]); +GO + diff --git a/gr/ddl/tables/autobind_naming_rule.sql b/gr/ddl/tables/autobind_naming_rule.sql new file mode 100644 index 0000000..940403f --- /dev/null +++ b/gr/ddl/tables/autobind_naming_rule.sql @@ -0,0 +1,8 @@ +-- Table: autobind_naming_rule +CREATE TABLE [autobind_naming_rule] ( + [rule_id] int NOT NULL, + [rule_name] nvarchar(329) NOT NULL, + CONSTRAINT [PK_autobind_naming_rule] PRIMARY KEY ([rule_id]) +); +GO + diff --git a/gr/ddl/tables/autobind_naming_rule_spec.sql b/gr/ddl/tables/autobind_naming_rule_spec.sql new file mode 100644 index 0000000..6ee7648 --- /dev/null +++ b/gr/ddl/tables/autobind_naming_rule_spec.sql @@ -0,0 +1,12 @@ +-- Table: autobind_naming_rule_spec +CREATE TABLE [autobind_naming_rule_spec] ( + [rule_id] int NOT NULL, + [io_type] nchar(1) NOT NULL, + [rule_spec] nvarchar(512) NOT NULL, + CONSTRAINT [PK_autobind_naming_rule_spec] PRIMARY KEY ([rule_id], [io_type], [rule_id]) +); +GO + +ALTER TABLE [autobind_naming_rule_spec] ADD FOREIGN KEY ([rule_id]) REFERENCES [autobind_naming_rule] ([rule_id]); +GO + diff --git a/gr/ddl/tables/autobind_translation_rule.sql b/gr/ddl/tables/autobind_translation_rule.sql new file mode 100644 index 0000000..5ebb145 --- /dev/null +++ b/gr/ddl/tables/autobind_translation_rule.sql @@ -0,0 +1,10 @@ +-- Table: autobind_translation_rule +CREATE TABLE [autobind_translation_rule] ( + [xlate_rule_id] int NOT NULL, + [xlate_rule_name] nvarchar(329) NOT NULL, + [xlate_rule_gsub_str] nvarchar(1000) NULL, + [xlate_rule_scope_global] bit NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_autobind_translation_rule] PRIMARY KEY ([xlate_rule_id]) +); +GO + diff --git a/gr/ddl/tables/autobound_attribute.sql b/gr/ddl/tables/autobound_attribute.sql new file mode 100644 index 0000000..02e126b --- /dev/null +++ b/gr/ddl/tables/autobound_attribute.sql @@ -0,0 +1,17 @@ +-- Table: autobound_attribute +CREATE TABLE [autobound_attribute] ( + [dio_id] int NOT NULL, + [sg_mx_primitive_id] smallint NOT NULL DEFAULT ((0)), + [gobject_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [mx_attribute_id] smallint NOT NULL, + [element_index] smallint NOT NULL DEFAULT ((0)), + [attr_alias] nvarchar(329) NULL, + [xlate_rule_id] int NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_autobound_attribute] PRIMARY KEY ([gobject_id], [mx_primitive_id], [mx_attribute_id], [element_index], [dio_id], [sg_mx_primitive_id], [dio_id], [sg_mx_primitive_id], [xlate_rule_id]) +); +GO + +ALTER TABLE [autobound_attribute] ADD FOREIGN KEY ([xlate_rule_id]) REFERENCES [autobind_translation_rule] ([xlate_rule_id]); +GO + diff --git a/gr/ddl/tables/client_control_class_link.sql b/gr/ddl/tables/client_control_class_link.sql new file mode 100644 index 0000000..06c0a73 --- /dev/null +++ b/gr/ddl/tables/client_control_class_link.sql @@ -0,0 +1,9 @@ +-- Table: client_control_class_link +CREATE TABLE [client_control_class_link] ( + [gobject_id] int NOT NULL, + [file_id] int NULL, + [class_name] nvarchar(1024) NOT NULL, + CONSTRAINT [PK_client_control_class_link] PRIMARY KEY ([gobject_id]) +); +GO + diff --git a/gr/ddl/tables/client_info.sql b/gr/ddl/tables/client_info.sql new file mode 100644 index 0000000..d7a740a --- /dev/null +++ b/gr/ddl/tables/client_info.sql @@ -0,0 +1,11 @@ +-- Table: client_info +CREATE TABLE [client_info] ( + [id] int NOT NULL, + [client_unique_identifier] nvarchar(4000) NOT NULL, + [client_name] nvarchar(64) NOT NULL, + [deployed_files_count] smallint NOT NULL, + [time_of_last_deployed_object_components] datetime NULL DEFAULT (getdate()), + [timestamp_of_last_synchronized] bigint NOT NULL DEFAULT ((0)) +); +GO + diff --git a/gr/ddl/tables/control_index.sql b/gr/ddl/tables/control_index.sql new file mode 100644 index 0000000..97ff795 --- /dev/null +++ b/gr/ddl/tables/control_index.sql @@ -0,0 +1,16 @@ +-- Table: control_index +CREATE TABLE [control_index] ( + [entity_id] int NOT NULL, + [gobject_id] int NOT NULL, + [control_id] nvarchar(329) NULL, + [control_name] nvarchar(329) NOT NULL, + [control_description] nvarchar(2000) NULL, + [properties] nvarchar(-1) NULL, + [thumbnail] nvarchar(-1) NULL, + CONSTRAINT [PK_control_index] PRIMARY KEY ([gobject_id], [control_name], [gobject_id]) +); +GO + +ALTER TABLE [control_index] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/data_type.sql b/gr/ddl/tables/data_type.sql new file mode 100644 index 0000000..580091a --- /dev/null +++ b/gr/ddl/tables/data_type.sql @@ -0,0 +1,9 @@ +-- Table: data_type +CREATE TABLE [data_type] ( + [mx_data_type] tinyint NOT NULL, + [description] varchar(30) NOT NULL, + [ow_data_type] varchar(10) NULL, + CONSTRAINT [PK_data_type] PRIMARY KEY ([mx_data_type]) +); +GO + diff --git a/gr/ddl/tables/deleted_gobject.sql b/gr/ddl/tables/deleted_gobject.sql new file mode 100644 index 0000000..b0de805 --- /dev/null +++ b/gr/ddl/tables/deleted_gobject.sql @@ -0,0 +1,8 @@ +-- Table: deleted_gobject +CREATE TABLE [deleted_gobject] ( + [gobject_id] int NOT NULL DEFAULT ((0)), + [timestamp_of_delete] timestamp NOT NULL, + CONSTRAINT [PK_deleted_gobject] PRIMARY KEY ([timestamp_of_delete]) +); +GO + diff --git a/gr/ddl/tables/deleted_ids.sql b/gr/ddl/tables/deleted_ids.sql new file mode 100644 index 0000000..159810d --- /dev/null +++ b/gr/ddl/tables/deleted_ids.sql @@ -0,0 +1,9 @@ +-- Table: deleted_ids +CREATE TABLE [deleted_ids] ( + [table_id] smallint NULL, + [deleted_id] int NOT NULL, + [deletion_timestamp] timestamp NOT NULL, + [deletion_time] datetime NULL +); +GO + diff --git a/gr/ddl/tables/deleted_visual_element.sql b/gr/ddl/tables/deleted_visual_element.sql new file mode 100644 index 0000000..eaf8866 --- /dev/null +++ b/gr/ddl/tables/deleted_visual_element.sql @@ -0,0 +1,8 @@ +-- Table: deleted_visual_element +CREATE TABLE [deleted_visual_element] ( + [visual_element_name] nvarchar(329) NULL, + [visual_element_type] nvarchar(32) NULL, + [timestamp_of_delete] timestamp NOT NULL +); +GO + diff --git a/gr/ddl/tables/deleted_visual_element_version.sql b/gr/ddl/tables/deleted_visual_element_version.sql new file mode 100644 index 0000000..ce672b9 --- /dev/null +++ b/gr/ddl/tables/deleted_visual_element_version.sql @@ -0,0 +1,13 @@ +-- Table: deleted_visual_element_version +CREATE TABLE [deleted_visual_element_version] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_name] nvarchar(329) NOT NULL, + [visual_element_type] nvarchar(32) NOT NULL, + [timestamp_of_delete] timestamp NOT NULL, + [visual_element_id] int NOT NULL, + CONSTRAINT [PK_deleted_visual_element_version] PRIMARY KEY ([gobject_id], [package_id], [timestamp_of_delete]) +); +GO + diff --git a/gr/ddl/tables/deployed_file.sql b/gr/ddl/tables/deployed_file.sql new file mode 100644 index 0000000..2da76c1 --- /dev/null +++ b/gr/ddl/tables/deployed_file.sql @@ -0,0 +1,19 @@ +-- Table: deployed_file +CREATE TABLE [deployed_file] ( + [deployed_file_id] int NOT NULL, + [file_id] int NOT NULL, + [node_name] nvarchar(256) NOT NULL, + [need_to_delete] int NOT NULL DEFAULT ((0)), + [is_package_deployed] bit NOT NULL, + [is_editor_deployed] bit NOT NULL, + [is_runtime_deployed] bit NOT NULL, + [is_browser_deployed] bit NOT NULL, + [file_version] nvarchar(50) NOT NULL DEFAULT (''), + [file_modified_time] nvarchar(50) NOT NULL DEFAULT (''), + CONSTRAINT [PK_deployed_file] PRIMARY KEY ([deployed_file_id], [file_id]) +); +GO + +ALTER TABLE [deployed_file] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]); +GO + diff --git a/gr/ddl/tables/deployed_intouch_viewapp.sql b/gr/ddl/tables/deployed_intouch_viewapp.sql new file mode 100644 index 0000000..a69d529 --- /dev/null +++ b/gr/ddl/tables/deployed_intouch_viewapp.sql @@ -0,0 +1,8 @@ +-- Table: deployed_intouch_viewapp +CREATE TABLE [deployed_intouch_viewapp] ( + [timestamp_of_deploy] bigint NOT NULL DEFAULT ((1)), + [gobject_id] int NOT NULL, + [deploy_file_transfering] bit NULL DEFAULT ((0)) +); +GO + diff --git a/gr/ddl/tables/deployed_intouch_viewapp_visual_element_dependency.sql b/gr/ddl/tables/deployed_intouch_viewapp_visual_element_dependency.sql new file mode 100644 index 0000000..708ac34 --- /dev/null +++ b/gr/ddl/tables/deployed_intouch_viewapp_visual_element_dependency.sql @@ -0,0 +1,7 @@ +-- Table: deployed_intouch_viewapp_visual_element_dependency +CREATE TABLE [deployed_intouch_viewapp_visual_element_dependency] ( + [gobject_id] int NULL, + [visual_element_name] nvarchar(2000) NULL +); +GO + diff --git a/gr/ddl/tables/dynamic_attribute.sql b/gr/ddl/tables/dynamic_attribute.sql new file mode 100644 index 0000000..0867681 --- /dev/null +++ b/gr/ddl/tables/dynamic_attribute.sql @@ -0,0 +1,25 @@ +-- Table: dynamic_attribute +CREATE TABLE [dynamic_attribute] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [mx_attribute_id] smallint NOT NULL, + [attribute_name] nvarchar(329) NOT NULL, + [mx_data_type] smallint NOT NULL, + [is_array] bit NOT NULL, + [security_classification] smallint NOT NULL, + [mx_attribute_category] int NOT NULL, + [lock_type] int NOT NULL, + [mx_value] text(2147483647) NOT NULL, + [owned_by_gobject_id] int NOT NULL DEFAULT ((0)), + [original_lock_type] int NOT NULL DEFAULT ((0)), + [dynamic_attribute_type] smallint NOT NULL DEFAULT ((0)), + [bitvalues] smallint NOT NULL DEFAULT ((0)), + [dynamic_attribute_id] bigint NOT NULL, + CONSTRAINT [PK_dynamic_attribute] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [mx_attribute_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [dynamic_attribute] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/external_content_media_types.sql b/gr/ddl/tables/external_content_media_types.sql new file mode 100644 index 0000000..aaa10c4 --- /dev/null +++ b/gr/ddl/tables/external_content_media_types.sql @@ -0,0 +1,12 @@ +-- Table: external_content_media_types +CREATE TABLE [external_content_media_types] ( + [entity_id] int NOT NULL, + [media_type] nvarchar(255) NOT NULL, + [control_entity_id] int NOT NULL, + [uri_property_name] nvarchar(1023) NULL, + [media_type_property_name] nvarchar(1023) NULL, + [is_default] bit NULL, + CONSTRAINT [PK_external_content_media_types] PRIMARY KEY ([entity_id]) +); +GO + diff --git a/gr/ddl/tables/feature.sql b/gr/ddl/tables/feature.sql new file mode 100644 index 0000000..e09c1af --- /dev/null +++ b/gr/ddl/tables/feature.sql @@ -0,0 +1,9 @@ +-- Table: feature +CREATE TABLE [feature] ( + [feature_id] int NOT NULL, + [feature_name] nvarchar(256) NOT NULL, + [feature_type] nvarchar(256) NOT NULL, + CONSTRAINT [PK_feature] PRIMARY KEY ([feature_id]) +); +GO + diff --git a/gr/ddl/tables/feature_file_link.sql b/gr/ddl/tables/feature_file_link.sql new file mode 100644 index 0000000..4b6626c --- /dev/null +++ b/gr/ddl/tables/feature_file_link.sql @@ -0,0 +1,11 @@ +-- Table: feature_file_link +CREATE TABLE [feature_file_link] ( + [feature_id] int NOT NULL, + [file_id] int NOT NULL, + CONSTRAINT [PK_feature_file_link] PRIMARY KEY ([feature_id], [file_id], [feature_id], [file_id]) +); +GO + +ALTER TABLE [feature_file_link] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]); +GO + diff --git a/gr/ddl/tables/file_browserinfo_link.sql b/gr/ddl/tables/file_browserinfo_link.sql new file mode 100644 index 0000000..6b0c1b2 --- /dev/null +++ b/gr/ddl/tables/file_browserinfo_link.sql @@ -0,0 +1,13 @@ +-- Table: file_browserinfo_link +CREATE TABLE [file_browserinfo_link] ( + [primitive_definition_id] int NOT NULL, + [file_id] int NOT NULL, + [assembly_strong_name] nvarchar(512) NOT NULL, + [assembly_type_name] nvarchar(256) NOT NULL, + CONSTRAINT [PK_file_browserinfo_link] PRIMARY KEY ([primitive_definition_id], [file_id], [file_id], [primitive_definition_id]) +); +GO + +ALTER TABLE [file_browserinfo_link] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]); +GO + diff --git a/gr/ddl/tables/file_pending_update.sql b/gr/ddl/tables/file_pending_update.sql new file mode 100644 index 0000000..fcbe6ad --- /dev/null +++ b/gr/ddl/tables/file_pending_update.sql @@ -0,0 +1,11 @@ +-- Table: file_pending_update +CREATE TABLE [file_pending_update] ( + [file_id] int NOT NULL, + [node_name] nvarchar(256) NOT NULL, + CONSTRAINT [PK_file_pending_update] PRIMARY KEY ([file_id]) +); +GO + +ALTER TABLE [file_pending_update] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]); +GO + diff --git a/gr/ddl/tables/file_primitive_definition_link.sql b/gr/ddl/tables/file_primitive_definition_link.sql new file mode 100644 index 0000000..6cbb3b1 --- /dev/null +++ b/gr/ddl/tables/file_primitive_definition_link.sql @@ -0,0 +1,15 @@ +-- Table: file_primitive_definition_link +CREATE TABLE [file_primitive_definition_link] ( + [primitive_definition_id] int NOT NULL, + [file_id] int NOT NULL, + [is_needed_for_package] bit NOT NULL DEFAULT ((0)), + [is_needed_for_runtime] bit NOT NULL DEFAULT ((0)), + [is_needed_for_editor] bit NOT NULL DEFAULT ((0)), + [is_needed_for_browser] bit NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_file_primitive_definition_link] PRIMARY KEY ([primitive_definition_id], [file_id], [file_id], [primitive_definition_id]) +); +GO + +ALTER TABLE [file_primitive_definition_link] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]); +GO + diff --git a/gr/ddl/tables/file_table.sql b/gr/ddl/tables/file_table.sql new file mode 100644 index 0000000..a5e97aa --- /dev/null +++ b/gr/ddl/tables/file_table.sql @@ -0,0 +1,13 @@ +-- Table: file_table +CREATE TABLE [file_table] ( + [file_id] int NOT NULL, + [file_name] nvarchar(256) NOT NULL, + [vendor_name] nvarchar(256) NOT NULL, + [registration_type] int NOT NULL, + [subfolder] nvarchar(256) NOT NULL DEFAULT (''), + [file_version] nvarchar(50) NOT NULL DEFAULT (''), + [file_modified_time] nvarchar(50) NOT NULL DEFAULT (''), + CONSTRAINT [PK_file_table] PRIMARY KEY ([file_id]) +); +GO + diff --git a/gr/ddl/tables/folder.sql b/gr/ddl/tables/folder.sql new file mode 100644 index 0000000..4c9f32c --- /dev/null +++ b/gr/ddl/tables/folder.sql @@ -0,0 +1,14 @@ +-- Table: folder +CREATE TABLE [folder] ( + [folder_id] int NOT NULL, + [folder_type] smallint NOT NULL, + [folder_name] nvarchar(64) NOT NULL, + [parent_folder_id] int NOT NULL, + [depth] int NOT NULL, + [has_objects] bit NOT NULL, + [has_folders] bit NOT NULL, + [timestamp_of_last_change] timestamp NOT NULL, + CONSTRAINT [PK_folder] PRIMARY KEY ([folder_id]) +); +GO + diff --git a/gr/ddl/tables/folder_gobject_link.sql b/gr/ddl/tables/folder_gobject_link.sql new file mode 100644 index 0000000..0fd8b41 --- /dev/null +++ b/gr/ddl/tables/folder_gobject_link.sql @@ -0,0 +1,13 @@ +-- Table: folder_gobject_link +CREATE TABLE [folder_gobject_link] ( + [folder_id] int NOT NULL, + [folder_type] smallint NOT NULL, + [gobject_id] int NOT NULL, + [timestamp_of_last_change] timestamp NOT NULL, + CONSTRAINT [PK_folder_gobject_link] PRIMARY KEY ([folder_id], [gobject_id], [gobject_id]) +); +GO + +ALTER TABLE [folder_gobject_link] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/galaxy.sql b/gr/ddl/tables/galaxy.sql new file mode 100644 index 0000000..6767e8e --- /dev/null +++ b/gr/ddl/tables/galaxy.sql @@ -0,0 +1,18 @@ +-- Table: galaxy +CREATE TABLE [galaxy] ( + [time_of_last_deploy] datetime NULL DEFAULT (getdate()), + [time_of_last_config_change] datetime NULL DEFAULT (getdate()), + [is_galaxy_installed] bit NOT NULL DEFAULT ((1)), + [time_of_last_reference_binding] datetime NULL DEFAULT (getdate()), + [timestamp_of_last_cascade] bigint NOT NULL DEFAULT ((1)), + [timestamp_of_last_visual_element_reference_bind] bigint NOT NULL DEFAULT ((0)), + [max_proxy_timestamp] bigint NOT NULL DEFAULT (CONVERT([bigint],@@dbts)), + [max_visual_element_timestamp] bigint NOT NULL DEFAULT (CONVERT([bigint],@@dbts)), + [is_migration_in_progress] bit NOT NULL DEFAULT ((0)), + [time_of_last_association_change] datetime NULL DEFAULT (getdate()), + [subscription_id] uniqueidentifier NULL, + [batch_id] uniqueidentifier NULL, + [iteration_id] int NOT NULL DEFAULT ((0)) +); +GO + diff --git a/gr/ddl/tables/galaxy_data.sql b/gr/ddl/tables/galaxy_data.sql new file mode 100644 index 0000000..8eb60e2 --- /dev/null +++ b/gr/ddl/tables/galaxy_data.sql @@ -0,0 +1,7 @@ +-- Table: galaxy_data +CREATE TABLE [galaxy_data] ( + [data_type] nvarchar(256) NOT NULL, + [data] image(2147483647) NULL +); +GO + diff --git a/gr/ddl/tables/galaxy_settings.sql b/gr/ddl/tables/galaxy_settings.sql new file mode 100644 index 0000000..3c5b477 --- /dev/null +++ b/gr/ddl/tables/galaxy_settings.sql @@ -0,0 +1,8 @@ +-- Table: galaxy_settings +CREATE TABLE [galaxy_settings] ( + [galaxyid] int NULL, + [default_qs_data] ntext(1073741823) NOT NULL, + [current_qs_data] ntext(1073741823) NOT NULL +); +GO + diff --git a/gr/ddl/tables/gobject.sql b/gr/ddl/tables/gobject.sql new file mode 100644 index 0000000..1d81551 --- /dev/null +++ b/gr/ddl/tables/gobject.sql @@ -0,0 +1,35 @@ +-- Table: gobject +CREATE TABLE [gobject] ( + [gobject_id] int NOT NULL, + [template_definition_id] int NOT NULL, + [derived_from_gobject_id] int NOT NULL DEFAULT ((0)), + [contained_by_gobject_id] int NOT NULL DEFAULT ((0)), + [area_gobject_id] int NOT NULL DEFAULT ((0)), + [hosted_by_gobject_id] int NOT NULL DEFAULT ((0)), + [checked_out_by_user_guid] uniqueidentifier NULL, + [default_symbol_gobject_id] int NOT NULL DEFAULT ((0)), + [default_display_gobject_id] int NOT NULL DEFAULT ((0)), + [checked_in_package_id] int NOT NULL DEFAULT ((0)), + [checked_out_package_id] int NOT NULL DEFAULT ((0)), + [deployed_package_id] int NOT NULL DEFAULT ((0)), + [last_deployed_package_id] int NOT NULL DEFAULT ((0)), + [tag_name] nvarchar(329) NOT NULL, + [contained_name] nvarchar(32) NOT NULL DEFAULT (''), + [identity_guid] uniqueidentifier NOT NULL DEFAULT (newid()), + [configuration_guid] uniqueidentifier NOT NULL, + [configuration_version] int NOT NULL, + [deployed_version] int NOT NULL DEFAULT ((0)), + [is_template] bit NOT NULL DEFAULT ((0)), + [is_hidden] bit NOT NULL DEFAULT ((0)), + [software_upgrade_needed] bit NOT NULL DEFAULT ((0)), + [hosting_tree_level] smallint NOT NULL DEFAULT ((0)), + [hierarchical_name] nvarchar(329) NOT NULL DEFAULT (''), + [namespace_id] smallint NOT NULL DEFAULT ((1)), + [deployment_pending_status] bit NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_gobject] PRIMARY KEY ([gobject_id], [namespace_id], [template_definition_id]) +); +GO + +ALTER TABLE [gobject] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]); +GO + diff --git a/gr/ddl/tables/gobject_asset_order.sql b/gr/ddl/tables/gobject_asset_order.sql new file mode 100644 index 0000000..7750a60 --- /dev/null +++ b/gr/ddl/tables/gobject_asset_order.sql @@ -0,0 +1,11 @@ +-- Table: gobject_asset_order +CREATE TABLE [gobject_asset_order] ( + [gobject_id] int NOT NULL, + [relative_index] float(53,) NOT NULL, + CONSTRAINT [PK_gobject_asset_order] PRIMARY KEY ([gobject_id]) +); +GO + +ALTER TABLE [gobject_asset_order] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/gobject_change_log.sql b/gr/ddl/tables/gobject_change_log.sql new file mode 100644 index 0000000..070844e --- /dev/null +++ b/gr/ddl/tables/gobject_change_log.sql @@ -0,0 +1,16 @@ +-- Table: gobject_change_log +CREATE TABLE [gobject_change_log] ( + [gobject_change_log_id] int NOT NULL, + [gobject_id] int NOT NULL, + [change_date] datetime NULL, + [operation_id] smallint NOT NULL, + [user_comment] nvarchar(1024) NOT NULL DEFAULT (''), + [configuration_version] int NOT NULL DEFAULT ((0)), + [user_profile_name] nvarchar(256) NOT NULL, + CONSTRAINT [PK_gobject_change_log] PRIMARY KEY ([gobject_id], [operation_id]) +); +GO + +ALTER TABLE [gobject_change_log] ADD FOREIGN KEY ([operation_id]) REFERENCES [lookup_operation] ([operation_id]); +GO + diff --git a/gr/ddl/tables/gobject_filter_info_timestamp.sql b/gr/ddl/tables/gobject_filter_info_timestamp.sql new file mode 100644 index 0000000..57e5aff --- /dev/null +++ b/gr/ddl/tables/gobject_filter_info_timestamp.sql @@ -0,0 +1,11 @@ +-- Table: gobject_filter_info_timestamp +CREATE TABLE [gobject_filter_info_timestamp] ( + [gobject_id] int NULL, + [timestamp_of_last_change] timestamp NOT NULL, + CONSTRAINT [PK_gobject_filter_info_timestamp] PRIMARY KEY ([timestamp_of_last_change], [gobject_id]) +); +GO + +ALTER TABLE [gobject_filter_info_timestamp] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/gobject_friendly_name.sql b/gr/ddl/tables/gobject_friendly_name.sql new file mode 100644 index 0000000..f15d33b --- /dev/null +++ b/gr/ddl/tables/gobject_friendly_name.sql @@ -0,0 +1,11 @@ +-- Table: gobject_friendly_name +CREATE TABLE [gobject_friendly_name] ( + [gobject_id] int NOT NULL, + [friendly_name] nvarchar(1024) NOT NULL DEFAULT (''), + CONSTRAINT [PK_gobject_friendly_name] PRIMARY KEY ([gobject_id]) +); +GO + +ALTER TABLE [gobject_friendly_name] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/gobject_log_details.sql b/gr/ddl/tables/gobject_log_details.sql new file mode 100644 index 0000000..ff59371 --- /dev/null +++ b/gr/ddl/tables/gobject_log_details.sql @@ -0,0 +1,7 @@ +-- Table: gobject_log_details +CREATE TABLE [gobject_log_details] ( + [gobject_id] int NOT NULL, + [tag_name] nvarchar(329) NOT NULL +); +GO + diff --git a/gr/ddl/tables/gobject_protected.sql b/gr/ddl/tables/gobject_protected.sql new file mode 100644 index 0000000..5a54fbe --- /dev/null +++ b/gr/ddl/tables/gobject_protected.sql @@ -0,0 +1,6 @@ +-- Table: gobject_protected +CREATE TABLE [gobject_protected] ( + [gobject_id] int NOT NULL +); +GO + diff --git a/gr/ddl/tables/instance.sql b/gr/ddl/tables/instance.sql new file mode 100644 index 0000000..70b64f8 --- /dev/null +++ b/gr/ddl/tables/instance.sql @@ -0,0 +1,13 @@ +-- Table: instance +CREATE TABLE [instance] ( + [gobject_id] int NOT NULL, + [mx_platform_id] smallint NOT NULL DEFAULT ((0)), + [mx_engine_id] smallint NOT NULL DEFAULT ((0)), + [mx_object_id] smallint NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_instance] PRIMARY KEY ([gobject_id], [gobject_id]) +); +GO + +ALTER TABLE [instance] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/intouchviewapptemplate_allsymbols.sql b/gr/ddl/tables/intouchviewapptemplate_allsymbols.sql new file mode 100644 index 0000000..67ad651 --- /dev/null +++ b/gr/ddl/tables/intouchviewapptemplate_allsymbols.sql @@ -0,0 +1,6 @@ +-- Table: intouchviewapptemplate_allsymbols +CREATE TABLE [intouchviewapptemplate_allsymbols] ( + [gobject_id] int NOT NULL +); +GO + diff --git a/gr/ddl/tables/lookup_category.sql b/gr/ddl/tables/lookup_category.sql new file mode 100644 index 0000000..8948a03 --- /dev/null +++ b/gr/ddl/tables/lookup_category.sql @@ -0,0 +1,8 @@ +-- Table: lookup_category +CREATE TABLE [lookup_category] ( + [category_id] smallint NOT NULL, + [category_name] nvarchar(50) NOT NULL, + CONSTRAINT [PK_lookup_category] PRIMARY KEY ([category_id]) +); +GO + diff --git a/gr/ddl/tables/lookup_folder.sql b/gr/ddl/tables/lookup_folder.sql new file mode 100644 index 0000000..2c226a1 --- /dev/null +++ b/gr/ddl/tables/lookup_folder.sql @@ -0,0 +1,7 @@ +-- Table: lookup_folder +CREATE TABLE [lookup_folder] ( + [folder_type] smallint NOT NULL, + [folder_type_name] nvarchar(32) NULL +); +GO + diff --git a/gr/ddl/tables/lookup_operation.sql b/gr/ddl/tables/lookup_operation.sql new file mode 100644 index 0000000..6517ce4 --- /dev/null +++ b/gr/ddl/tables/lookup_operation.sql @@ -0,0 +1,9 @@ +-- Table: lookup_operation +CREATE TABLE [lookup_operation] ( + [operation_id] smallint NOT NULL, + [operation_code] nvarchar(50) NOT NULL, + [operation_name] nvarchar(256) NOT NULL, + CONSTRAINT [PK_lookup_operation] PRIMARY KEY ([operation_id]) +); +GO + diff --git a/gr/ddl/tables/lookup_package_op_status.sql b/gr/ddl/tables/lookup_package_op_status.sql new file mode 100644 index 0000000..e4c013a --- /dev/null +++ b/gr/ddl/tables/lookup_package_op_status.sql @@ -0,0 +1,8 @@ +-- Table: lookup_package_op_status +CREATE TABLE [lookup_package_op_status] ( + [status_id] int NOT NULL, + [status_name] nvarchar(50) NOT NULL, + CONSTRAINT [PK_lookup_package_op_status] PRIMARY KEY ([status_id]) +); +GO + diff --git a/gr/ddl/tables/lookup_status.sql b/gr/ddl/tables/lookup_status.sql new file mode 100644 index 0000000..97f6dd6 --- /dev/null +++ b/gr/ddl/tables/lookup_status.sql @@ -0,0 +1,8 @@ +-- Table: lookup_status +CREATE TABLE [lookup_status] ( + [status_id] int NOT NULL, + [status_name] nvarchar(50) NOT NULL, + CONSTRAINT [PK_lookup_status] PRIMARY KEY ([status_id]) +); +GO + diff --git a/gr/ddl/tables/lookup_table_name.sql b/gr/ddl/tables/lookup_table_name.sql new file mode 100644 index 0000000..5e16e28 --- /dev/null +++ b/gr/ddl/tables/lookup_table_name.sql @@ -0,0 +1,7 @@ +-- Table: lookup_table_name +CREATE TABLE [lookup_table_name] ( + [table_id] smallint NOT NULL, + [table_name] nvarchar(250) NULL +); +GO + diff --git a/gr/ddl/tables/namespace.sql b/gr/ddl/tables/namespace.sql new file mode 100644 index 0000000..18130f0 --- /dev/null +++ b/gr/ddl/tables/namespace.sql @@ -0,0 +1,8 @@ +-- Table: namespace +CREATE TABLE [namespace] ( + [namespace_id] smallint NOT NULL, + [namespace_name] nvarchar(32) NULL, + CONSTRAINT [PK_namespace] PRIMARY KEY ([namespace_id]) +); +GO + diff --git a/gr/ddl/tables/object_device_linkage.sql b/gr/ddl/tables/object_device_linkage.sql new file mode 100644 index 0000000..6448fcb --- /dev/null +++ b/gr/ddl/tables/object_device_linkage.sql @@ -0,0 +1,9 @@ +-- Table: object_device_linkage +CREATE TABLE [object_device_linkage] ( + [gobject_id] int NOT NULL, + [dio_id] int NOT NULL, + [sg_mx_primitive_id] smallint NOT NULL, + CONSTRAINT [PK_object_device_linkage] PRIMARY KEY ([gobject_id]) +); +GO + diff --git a/gr/ddl/tables/object_wizard_overview_symbols.sql b/gr/ddl/tables/object_wizard_overview_symbols.sql new file mode 100644 index 0000000..728e95e --- /dev/null +++ b/gr/ddl/tables/object_wizard_overview_symbols.sql @@ -0,0 +1,9 @@ +-- Table: object_wizard_overview_symbols +CREATE TABLE [object_wizard_overview_symbols] ( + [gobject_id] int NOT NULL, + [visual_element_id] int NOT NULL, + [change_type] int NOT NULL, + [mx_primitive_id] int NULL +); +GO + diff --git a/gr/ddl/tables/object_wizard_symbol_override.sql b/gr/ddl/tables/object_wizard_symbol_override.sql new file mode 100644 index 0000000..0c19b23 --- /dev/null +++ b/gr/ddl/tables/object_wizard_symbol_override.sql @@ -0,0 +1,12 @@ +-- Table: object_wizard_symbol_override +CREATE TABLE [object_wizard_symbol_override] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [symbol_overrides] image(2147483647) NULL, + CONSTRAINT [PK_object_wizard_symbol_override] PRIMARY KEY ([gobject_id], [package_id], [gobject_id], [package_id]) +); +GO + +ALTER TABLE [object_wizard_symbol_override] ADD FOREIGN KEY ([package_id]) REFERENCES [package] ([package_id]); +GO + diff --git a/gr/ddl/tables/object_wizard_symbol_override_mapping.sql b/gr/ddl/tables/object_wizard_symbol_override_mapping.sql new file mode 100644 index 0000000..1701569 --- /dev/null +++ b/gr/ddl/tables/object_wizard_symbol_override_mapping.sql @@ -0,0 +1,15 @@ +-- Table: object_wizard_symbol_override_mapping +CREATE TABLE [object_wizard_symbol_override_mapping] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [has_local_overrides] bit NOT NULL, + [thumbnail] image(2147483647) NULL, + [preview] image(2147483647) NULL, + CONSTRAINT [PK_object_wizard_symbol_override_mapping] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [object_wizard_symbol_override_mapping] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/old_checked_in_packages.sql b/gr/ddl/tables/old_checked_in_packages.sql new file mode 100644 index 0000000..c60a271 --- /dev/null +++ b/gr/ddl/tables/old_checked_in_packages.sql @@ -0,0 +1,9 @@ +-- Table: old_checked_in_packages +CREATE TABLE [old_checked_in_packages] ( + [package_id] int NULL, + [gobject_id] int NULL, + [is_template] bit NULL, + [is_being_referenced] bit NULL DEFAULT ((0)) +); +GO + diff --git a/gr/ddl/tables/operation.sql b/gr/ddl/tables/operation.sql new file mode 100644 index 0000000..12daf9d --- /dev/null +++ b/gr/ddl/tables/operation.sql @@ -0,0 +1,13 @@ +-- Table: operation +CREATE TABLE [operation] ( + [operation_id] int NOT NULL, + [user_profile_id] int NOT NULL, + [operation_name] nvarchar(300) NOT NULL, + [start_time] datetime NOT NULL DEFAULT (getdate()), + CONSTRAINT [PK_operation] PRIMARY KEY ([operation_id], [user_profile_id]) +); +GO + +ALTER TABLE [operation] ADD FOREIGN KEY ([user_profile_id]) REFERENCES [user_profile] ([user_profile_id]); +GO + diff --git a/gr/ddl/tables/operation_message.sql b/gr/ddl/tables/operation_message.sql new file mode 100644 index 0000000..8b01bb3 --- /dev/null +++ b/gr/ddl/tables/operation_message.sql @@ -0,0 +1,13 @@ +-- Table: operation_message +CREATE TABLE [operation_message] ( + [message_id] int NOT NULL, + [operation_id] int NOT NULL, + [message_text] nvarchar(300) NOT NULL, + [message_time] datetime NOT NULL DEFAULT (getdate()), + CONSTRAINT [PK_operation_message] PRIMARY KEY ([message_id], [operation_id]) +); +GO + +ALTER TABLE [operation_message] ADD FOREIGN KEY ([operation_id]) REFERENCES [operation] ([operation_id]); +GO + diff --git a/gr/ddl/tables/operation_status.sql b/gr/ddl/tables/operation_status.sql new file mode 100644 index 0000000..dd91a1c --- /dev/null +++ b/gr/ddl/tables/operation_status.sql @@ -0,0 +1,11 @@ +-- Table: operation_status +CREATE TABLE [operation_status] ( + [operation_id] int NOT NULL, + [status] int NOT NULL, + CONSTRAINT [PK_operation_status] PRIMARY KEY ([operation_id], [status], [operation_id]) +); +GO + +ALTER TABLE [operation_status] ADD FOREIGN KEY ([operation_id]) REFERENCES [operation] ([operation_id]); +GO + diff --git a/gr/ddl/tables/operation_status_look_up.sql b/gr/ddl/tables/operation_status_look_up.sql new file mode 100644 index 0000000..1c47383 --- /dev/null +++ b/gr/ddl/tables/operation_status_look_up.sql @@ -0,0 +1,8 @@ +-- Table: operation_status_look_up +CREATE TABLE [operation_status_look_up] ( + [status] int NOT NULL, + [status_name] varchar(100) NOT NULL, + CONSTRAINT [PK_operation_status_look_up] PRIMARY KEY ([status]) +); +GO + diff --git a/gr/ddl/tables/ow_group_def.sql b/gr/ddl/tables/ow_group_def.sql new file mode 100644 index 0000000..9bc8002 --- /dev/null +++ b/gr/ddl/tables/ow_group_def.sql @@ -0,0 +1,16 @@ +-- Table: ow_group_def +CREATE TABLE [ow_group_def] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [ow_group_id] int NOT NULL, + [prompt] nvarchar(260) NOT NULL, + [GUID] varchar(36) NOT NULL, + [description] nvarchar(260) NULL, + [visibility_rules] nvarchar(-1) NULL, + CONSTRAINT [PK_ow_group_def] PRIMARY KEY ([gobject_id], [package_id], [ow_group_id], [ow_group_id]) +); +GO + +ALTER TABLE [ow_group_def] ADD FOREIGN KEY ([ow_group_id]) REFERENCES [ow_group_id] ([ow_group_id]); +GO + diff --git a/gr/ddl/tables/ow_group_id.sql b/gr/ddl/tables/ow_group_id.sql new file mode 100644 index 0000000..73d74b7 --- /dev/null +++ b/gr/ddl/tables/ow_group_id.sql @@ -0,0 +1,7 @@ +-- Table: ow_group_id +CREATE TABLE [ow_group_id] ( + [ow_group_id] int NOT NULL, + CONSTRAINT [PK_ow_group_id] PRIMARY KEY ([ow_group_id]) +); +GO + diff --git a/gr/ddl/tables/ow_group_override.sql b/gr/ddl/tables/ow_group_override.sql new file mode 100644 index 0000000..5426baf --- /dev/null +++ b/gr/ddl/tables/ow_group_override.sql @@ -0,0 +1,13 @@ +-- Table: ow_group_override +CREATE TABLE [ow_group_override] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [ow_group_id] int NOT NULL, + [property_bitmask] int NOT NULL, + CONSTRAINT [PK_ow_group_override] PRIMARY KEY ([gobject_id], [package_id], [ow_group_id], [ow_group_id]) +); +GO + +ALTER TABLE [ow_group_override] ADD FOREIGN KEY ([ow_group_id]) REFERENCES [ow_group_id] ([ow_group_id]); +GO + diff --git a/gr/ddl/tables/ow_instance_setting.sql b/gr/ddl/tables/ow_instance_setting.sql new file mode 100644 index 0000000..f68caf9 --- /dev/null +++ b/gr/ddl/tables/ow_instance_setting.sql @@ -0,0 +1,17 @@ +-- Table: ow_instance_setting +CREATE TABLE [ow_instance_setting] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [name] nvarchar(329) NOT NULL, + [ow_setting_type] int NOT NULL, + [is_dmv] bit NOT NULL, + [override_value] nvarchar(-1) NULL, + [is_mx] bit NOT NULL, + [is_implicit] bit NULL, + CONSTRAINT [PK_ow_instance_setting] PRIMARY KEY ([gobject_id], [package_id], [name], [ow_setting_type], [is_dmv], [ow_setting_type]) +); +GO + +ALTER TABLE [ow_instance_setting] ADD FOREIGN KEY ([ow_setting_type]) REFERENCES [ow_lu_setting] ([ow_setting_type]); +GO + diff --git a/gr/ddl/tables/ow_link_def.sql b/gr/ddl/tables/ow_link_def.sql new file mode 100644 index 0000000..99ce9b1 --- /dev/null +++ b/gr/ddl/tables/ow_link_def.sql @@ -0,0 +1,19 @@ +-- Table: ow_link_def +CREATE TABLE [ow_link_def] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [ow_link_id] int NOT NULL, + [ow_link_type] int NOT NULL, + [name] nvarchar(329) NOT NULL, + [mx_primitive_id] smallint NULL, + [dynamic_attribute_id] bigint NULL, + [optional_id] int NULL, + [property_bitmask] int NOT NULL, + [sort] int NOT NULL, + CONSTRAINT [PK_ow_link_def] PRIMARY KEY ([gobject_id], [package_id], [ow_link_id], [ow_link_id]) +); +GO + +ALTER TABLE [ow_link_def] ADD FOREIGN KEY ([ow_link_id]) REFERENCES [ow_link_id] ([ow_link_id]); +GO + diff --git a/gr/ddl/tables/ow_link_id.sql b/gr/ddl/tables/ow_link_id.sql new file mode 100644 index 0000000..b1f6906 --- /dev/null +++ b/gr/ddl/tables/ow_link_id.sql @@ -0,0 +1,11 @@ +-- Table: ow_link_id +CREATE TABLE [ow_link_id] ( + [ow_link_id] int NOT NULL, + [ow_opt_or_choice_id] int NOT NULL, + CONSTRAINT [PK_ow_link_id] PRIMARY KEY ([ow_link_id], [ow_opt_or_choice_id]) +); +GO + +ALTER TABLE [ow_link_id] ADD FOREIGN KEY ([ow_opt_or_choice_id]) REFERENCES [ow_opt_or_choice_id] ([ow_opt_or_choice_id]); +GO + diff --git a/gr/ddl/tables/ow_lu_definition.sql b/gr/ddl/tables/ow_lu_definition.sql new file mode 100644 index 0000000..d832c27 --- /dev/null +++ b/gr/ddl/tables/ow_lu_definition.sql @@ -0,0 +1,17 @@ +-- Table: ow_lu_definition +CREATE TABLE [ow_lu_definition] ( + [ow_setting_type] int NOT NULL, + [primitive_definition_id] int NOT NULL, + [template_definition_id] int NOT NULL, + [boolean_analog] bit NOT NULL, + [ext] varchar(20) NOT NULL, + [link_setting] int NULL, + [link_feature] int NULL, + [mx_attribute_id] smallint NULL, + CONSTRAINT [PK_ow_lu_definition] PRIMARY KEY ([ow_setting_type], [primitive_definition_id], [ow_setting_type]) +); +GO + +ALTER TABLE [ow_lu_definition] ADD FOREIGN KEY ([ow_setting_type]) REFERENCES [ow_lu_setting] ([ow_setting_type]); +GO + diff --git a/gr/ddl/tables/ow_lu_setting.sql b/gr/ddl/tables/ow_lu_setting.sql new file mode 100644 index 0000000..88dec60 --- /dev/null +++ b/gr/ddl/tables/ow_lu_setting.sql @@ -0,0 +1,18 @@ +-- Table: ow_lu_setting +CREATE TABLE [ow_lu_setting] ( + [ow_setting_type] int NOT NULL, + [category] varchar(10) NOT NULL, + [mx_data_type] tinyint NULL, + [setting_name] nvarchar(30) NOT NULL, + [feature_id] int NULL, + [feature_sub_id] int NULL, + [parent_type] int NULL, + [no_ext] bit NULL, + [raw_value] nvarchar(100) NULL, + CONSTRAINT [PK_ow_lu_setting] PRIMARY KEY ([ow_setting_type], [mx_data_type]) +); +GO + +ALTER TABLE [ow_lu_setting] ADD FOREIGN KEY ([mx_data_type]) REFERENCES [data_type] ([mx_data_type]); +GO + diff --git a/gr/ddl/tables/ow_opt_or_choice_def.sql b/gr/ddl/tables/ow_opt_or_choice_def.sql new file mode 100644 index 0000000..93ea34d --- /dev/null +++ b/gr/ddl/tables/ow_opt_or_choice_def.sql @@ -0,0 +1,21 @@ +-- Table: ow_opt_or_choice_def +CREATE TABLE [ow_opt_or_choice_def] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [ow_opt_or_choice_id] int NOT NULL, + [prompt] nvarchar(260) NOT NULL, + [GUID] varchar(36) NOT NULL, + [name] varchar(10) NOT NULL, + [description] nvarchar(260) NULL, + [choice_sequence_number] int NULL, + [after_group_or_option] int NULL, + [initial_value] bit NOT NULL DEFAULT ((0)), + [visibility_rules] nvarchar(-1) NULL, + [sort] int NOT NULL, + CONSTRAINT [PK_ow_opt_or_choice_def] PRIMARY KEY ([gobject_id], [package_id], [ow_opt_or_choice_id], [ow_opt_or_choice_id]) +); +GO + +ALTER TABLE [ow_opt_or_choice_def] ADD FOREIGN KEY ([ow_opt_or_choice_id]) REFERENCES [ow_opt_or_choice_id] ([ow_opt_or_choice_id]); +GO + diff --git a/gr/ddl/tables/ow_opt_or_choice_id.sql b/gr/ddl/tables/ow_opt_or_choice_id.sql new file mode 100644 index 0000000..ee09c8a --- /dev/null +++ b/gr/ddl/tables/ow_opt_or_choice_id.sql @@ -0,0 +1,11 @@ +-- Table: ow_opt_or_choice_id +CREATE TABLE [ow_opt_or_choice_id] ( + [ow_opt_or_choice_id] int NOT NULL, + [ow_group_id] int NOT NULL, + CONSTRAINT [PK_ow_opt_or_choice_id] PRIMARY KEY ([ow_opt_or_choice_id], [ow_group_id]) +); +GO + +ALTER TABLE [ow_opt_or_choice_id] ADD FOREIGN KEY ([ow_group_id]) REFERENCES [ow_group_id] ([ow_group_id]); +GO + diff --git a/gr/ddl/tables/ow_opt_or_choice_override.sql b/gr/ddl/tables/ow_opt_or_choice_override.sql new file mode 100644 index 0000000..00486df --- /dev/null +++ b/gr/ddl/tables/ow_opt_or_choice_override.sql @@ -0,0 +1,14 @@ +-- Table: ow_opt_or_choice_override +CREATE TABLE [ow_opt_or_choice_override] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [ow_opt_or_choice_id] int NOT NULL, + [property_bitmask] int NULL, + [override_value] bit NULL, + CONSTRAINT [PK_ow_opt_or_choice_override] PRIMARY KEY ([gobject_id], [package_id], [ow_opt_or_choice_id], [ow_opt_or_choice_id]) +); +GO + +ALTER TABLE [ow_opt_or_choice_override] ADD FOREIGN KEY ([ow_opt_or_choice_id]) REFERENCES [ow_opt_or_choice_id] ([ow_opt_or_choice_id]); +GO + diff --git a/gr/ddl/tables/ow_setting_def.sql b/gr/ddl/tables/ow_setting_def.sql new file mode 100644 index 0000000..e4e4414 --- /dev/null +++ b/gr/ddl/tables/ow_setting_def.sql @@ -0,0 +1,18 @@ +-- Table: ow_setting_def +CREATE TABLE [ow_setting_def] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [ow_setting_id] int NOT NULL, + [ow_setting_type] int NOT NULL, + [reference] nvarchar(260) NULL, + [property_id] varchar(36) NULL, + [initial_value] nvarchar(-1) NULL, + [property_bitmask] int NULL, + [sort] int NOT NULL, + CONSTRAINT [PK_ow_setting_def] PRIMARY KEY ([gobject_id], [package_id], [ow_setting_id], [ow_setting_type], [ow_setting_id]) +); +GO + +ALTER TABLE [ow_setting_def] ADD FOREIGN KEY ([ow_setting_id]) REFERENCES [ow_setting_id] ([ow_setting_id]); +GO + diff --git a/gr/ddl/tables/ow_setting_id.sql b/gr/ddl/tables/ow_setting_id.sql new file mode 100644 index 0000000..db55966 --- /dev/null +++ b/gr/ddl/tables/ow_setting_id.sql @@ -0,0 +1,11 @@ +-- Table: ow_setting_id +CREATE TABLE [ow_setting_id] ( + [ow_setting_id] int NOT NULL, + [ow_link_id] int NOT NULL, + CONSTRAINT [PK_ow_setting_id] PRIMARY KEY ([ow_setting_id], [ow_link_id]) +); +GO + +ALTER TABLE [ow_setting_id] ADD FOREIGN KEY ([ow_link_id]) REFERENCES [ow_link_id] ([ow_link_id]); +GO + diff --git a/gr/ddl/tables/ow_setting_override.sql b/gr/ddl/tables/ow_setting_override.sql new file mode 100644 index 0000000..6c52e44 --- /dev/null +++ b/gr/ddl/tables/ow_setting_override.sql @@ -0,0 +1,14 @@ +-- Table: ow_setting_override +CREATE TABLE [ow_setting_override] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [ow_setting_id] int NOT NULL, + [override_value] nvarchar(-1) NULL, + [property_bitmask] int NULL, + CONSTRAINT [PK_ow_setting_override] PRIMARY KEY ([gobject_id], [package_id], [ow_setting_id], [ow_setting_id]) +); +GO + +ALTER TABLE [ow_setting_override] ADD FOREIGN KEY ([ow_setting_id]) REFERENCES [ow_setting_id] ([ow_setting_id]); +GO + diff --git a/gr/ddl/tables/ow_symbol_setting.sql b/gr/ddl/tables/ow_symbol_setting.sql new file mode 100644 index 0000000..5d82c14 --- /dev/null +++ b/gr/ddl/tables/ow_symbol_setting.sql @@ -0,0 +1,17 @@ +-- Table: ow_symbol_setting +CREATE TABLE [ow_symbol_setting] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [sym_name] nvarchar(33) NOT NULL, + [ow_setting_type] int NOT NULL, + [reference] nvarchar(260) NOT NULL, + [property_id] nvarchar(36) NULL, + [value] nvarchar(-1) NULL, + [property_bitmask] int NULL, + CONSTRAINT [PK_ow_symbol_setting] PRIMARY KEY ([gobject_id], [package_id], [sym_name], [ow_setting_type], [reference], [ow_setting_type]) +); +GO + +ALTER TABLE [ow_symbol_setting] ADD FOREIGN KEY ([ow_setting_type]) REFERENCES [ow_lu_setting] ([ow_setting_type]); +GO + diff --git a/gr/ddl/tables/owned_visual_element.sql b/gr/ddl/tables/owned_visual_element.sql new file mode 100644 index 0000000..e5d64b8 --- /dev/null +++ b/gr/ddl/tables/owned_visual_element.sql @@ -0,0 +1,21 @@ +-- Table: owned_visual_element +CREATE TABLE [owned_visual_element] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_id] int NOT NULL, + [thumbnail] image(2147483647) NULL, + [description] nvarchar(1024) NULL, + [visual_element_definition] image(2147483647) NOT NULL, + [is_thumbnail_dirty] bit NOT NULL DEFAULT ((0)), + [visual_element_definition_grm] image(2147483647) NOT NULL, + [content_type] nvarchar(1024) NULL, + [visual_element_crossRef] nvarchar(-1) NULL, + [preview] image(2147483647) NULL, + CONSTRAINT [PK_owned_visual_element] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [owned_visual_element] ADD FOREIGN KEY ([package_id]) REFERENCES [visual_element_version] ([package_id]); +GO + diff --git a/gr/ddl/tables/package.sql b/gr/ddl/tables/package.sql new file mode 100644 index 0000000..7b1329c --- /dev/null +++ b/gr/ddl/tables/package.sql @@ -0,0 +1,21 @@ +-- Table: package +CREATE TABLE [package] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [status_id] int NOT NULL DEFAULT ((0)), + [reference_status_id] int NOT NULL DEFAULT ((0)), + [instance_attributes] image(2147483647) NULL, + [operation_status] int NOT NULL DEFAULT ((0)), + [security_group] nvarchar(32) NOT NULL DEFAULT ('Default'), + [derived_from_package_id] int NOT NULL DEFAULT ((0)), + [deployable_configuration_version] int NOT NULL DEFAULT ((0)), + [package_type] nvarchar(5) NOT NULL DEFAULT ('I'), + [package_version] smallint NOT NULL DEFAULT ((0)), + [object_status] smallint NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_package] PRIMARY KEY ([gobject_id], [package_id], [gobject_id], [status_id]) +); +GO + +ALTER TABLE [package] ADD FOREIGN KEY ([status_id]) REFERENCES [lookup_status] ([status_id]); +GO + diff --git a/gr/ddl/tables/packages_to_be_deleted.sql b/gr/ddl/tables/packages_to_be_deleted.sql new file mode 100644 index 0000000..1864a86 --- /dev/null +++ b/gr/ddl/tables/packages_to_be_deleted.sql @@ -0,0 +1,8 @@ +-- Table: packages_to_be_deleted +CREATE TABLE [packages_to_be_deleted] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + CONSTRAINT [PK_packages_to_be_deleted] PRIMARY KEY ([gobject_id], [package_id]) +); +GO + diff --git a/gr/ddl/tables/platform.sql b/gr/ddl/tables/platform.sql new file mode 100644 index 0000000..b128f2b --- /dev/null +++ b/gr/ddl/tables/platform.sql @@ -0,0 +1,21 @@ +-- Table: platform +CREATE TABLE [platform] ( + [platform_id] int NOT NULL, + [platform_gobject_id] int NOT NULL, + [node_name] nvarchar(256) NOT NULL DEFAULT (''), + [last_deployed_node_name] nvarchar(256) NOT NULL DEFAULT (''), + [rmcNode_name] nvarchar(256) NOT NULL DEFAULT (''), + [last_deployed_rmcNode_name] nvarchar(256) NOT NULL DEFAULT (''), + [portNMX] int NOT NULL DEFAULT ((0)), + [last_deployed_portNMX] int NOT NULL DEFAULT ((0)), + [portRMC] int NOT NULL DEFAULT ((0)), + [last_deployed_portRMC] int NOT NULL DEFAULT ((0)), + [portRPC] int NOT NULL DEFAULT ((0)), + [last_deployed_portRPC] int NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_platform] PRIMARY KEY ([platform_gobject_id], [platform_gobject_id]) +); +GO + +ALTER TABLE [platform] ADD FOREIGN KEY ([platform_gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/platform_license.sql b/gr/ddl/tables/platform_license.sql new file mode 100644 index 0000000..fa0c7a0 --- /dev/null +++ b/gr/ddl/tables/platform_license.sql @@ -0,0 +1,11 @@ +-- Table: platform_license +CREATE TABLE [platform_license] ( + [gobject_id] int NOT NULL, + [license_type] int NOT NULL, + CONSTRAINT [PK_platform_license] PRIMARY KEY ([gobject_id], [gobject_id]) +); +GO + +ALTER TABLE [platform_license] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]); +GO + diff --git a/gr/ddl/tables/primitive_attribute_validation_results.sql b/gr/ddl/tables/primitive_attribute_validation_results.sql new file mode 100644 index 0000000..bf6e4d7 --- /dev/null +++ b/gr/ddl/tables/primitive_attribute_validation_results.sql @@ -0,0 +1,15 @@ +-- Table: primitive_attribute_validation_results +CREATE TABLE [primitive_attribute_validation_results] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [mx_attribute_id] smallint NOT NULL, + [validationCode] nvarchar(329) NOT NULL, + [validationState] smallint NOT NULL, + CONSTRAINT [PK_primitive_attribute_validation_results] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [mx_attribute_id], [validationCode], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [primitive_attribute_validation_results] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/primitive_data_type.sql b/gr/ddl/tables/primitive_data_type.sql new file mode 100644 index 0000000..0def6dc --- /dev/null +++ b/gr/ddl/tables/primitive_data_type.sql @@ -0,0 +1,11 @@ +-- Table: primitive_data_type +CREATE TABLE [primitive_data_type] ( + [mx_data_type] tinyint NOT NULL, + [primitive_definition_id] int NOT NULL, + CONSTRAINT [PK_primitive_data_type] PRIMARY KEY ([mx_data_type], [primitive_definition_id], [mx_data_type], [primitive_definition_id]) +); +GO + +ALTER TABLE [primitive_data_type] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]); +GO + diff --git a/gr/ddl/tables/primitive_definition.sql b/gr/ddl/tables/primitive_definition.sql new file mode 100644 index 0000000..5771df8 --- /dev/null +++ b/gr/ddl/tables/primitive_definition.sql @@ -0,0 +1,21 @@ +-- Table: primitive_definition +CREATE TABLE [primitive_definition] ( + [primitive_definition_id] int NOT NULL, + [template_definition_id] int NOT NULL, + [parent_mx_primitive_id] smallint NOT NULL, + [mx_primitive_id] smallint NOT NULL DEFAULT ((0)), + [primitive_name] nvarchar(329) NOT NULL DEFAULT (''), + [execution_group] smallint NOT NULL, + [is_virtual] bit NOT NULL, + [primitive_guid] uniqueidentifier NOT NULL, + [runtime_handler_clsid] uniqueidentifier NULL, + [package_handler_clsid] uniqueidentifier NULL, + [supports_dynamic_attributes] bit NOT NULL, + [major_version] int NOT NULL, + CONSTRAINT [PK_primitive_definition] PRIMARY KEY ([primitive_definition_id], [template_definition_id]) +); +GO + +ALTER TABLE [primitive_definition] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]); +GO + diff --git a/gr/ddl/tables/primitive_instance.sql b/gr/ddl/tables/primitive_instance.sql new file mode 100644 index 0000000..8f3e20c --- /dev/null +++ b/gr/ddl/tables/primitive_instance.sql @@ -0,0 +1,34 @@ +-- Table: primitive_instance +CREATE TABLE [primitive_instance] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [primitive_definition_id] int NOT NULL, + [primitive_name] nvarchar(329) NOT NULL, + [parent_mx_primitive_id] smallint NOT NULL, + [execution_group] int NOT NULL, + [execution_order] int NOT NULL DEFAULT ((-1)), + [owned_by_gobject_id] int NOT NULL DEFAULT ((0)), + [timestamp_of_last_change] bigint NULL DEFAULT ((0)), + [max_child_timestamp] bigint NULL DEFAULT ((0)), + [extension_type] nvarchar(329) NULL, + [is_object_extension] bit NULL DEFAULT ((0)), + [checked_in_primitive_version] int NOT NULL DEFAULT ((1)), + [checked_out_primitive_version] int NOT NULL DEFAULT ((1)), + [entity_change_type] int NOT NULL DEFAULT ((1)), + [operation_on_primitive_mask] int NOT NULL DEFAULT ((0)), + [created_by_parent] smallint NOT NULL DEFAULT ((0)), + [status_id] smallint NOT NULL DEFAULT ((0)), + [ref_status_id] smallint NOT NULL DEFAULT ((0)), + [primitive_attributes] image(2147483647) NULL, + [mx_value_errors] text(2147483647) NOT NULL, + [mx_value_warnings] text(2147483647) NOT NULL, + [mx_value_reference_warnings] text(2147483647) NOT NULL, + [property_bitmask] smallint NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_primitive_instance] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [gobject_id], [package_id], [gobject_id], [package_id]) +); +GO + +ALTER TABLE [primitive_instance] ADD FOREIGN KEY ([package_id]) REFERENCES [package] ([package_id]); +GO + diff --git a/gr/ddl/tables/primitive_instance_feature_link.sql b/gr/ddl/tables/primitive_instance_feature_link.sql new file mode 100644 index 0000000..064c02e --- /dev/null +++ b/gr/ddl/tables/primitive_instance_feature_link.sql @@ -0,0 +1,15 @@ +-- Table: primitive_instance_feature_link +CREATE TABLE [primitive_instance_feature_link] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [feature_id] int NOT NULL, + [feature_name] nvarchar(256) NOT NULL DEFAULT (''), + [feature_type] nvarchar(256) NOT NULL DEFAULT (''), + CONSTRAINT [PK_primitive_instance_feature_link] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [feature_id], [feature_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [primitive_instance_feature_link] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/primitive_instance_file_table_link.sql b/gr/ddl/tables/primitive_instance_file_table_link.sql new file mode 100644 index 0000000..c14c8d1 --- /dev/null +++ b/gr/ddl/tables/primitive_instance_file_table_link.sql @@ -0,0 +1,16 @@ +-- Table: primitive_instance_file_table_link +CREATE TABLE [primitive_instance_file_table_link] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [file_id] int NOT NULL, + [is_needed_for_package] bit NOT NULL DEFAULT ((0)), + [is_needed_for_runtime] bit NOT NULL DEFAULT ((0)), + [is_needed_for_editor] bit NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_primitive_instance_file_table_link] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [file_id], [file_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [primitive_instance_file_table_link] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/proxy_timestamp.sql b/gr/ddl/tables/proxy_timestamp.sql new file mode 100644 index 0000000..0676dfe --- /dev/null +++ b/gr/ddl/tables/proxy_timestamp.sql @@ -0,0 +1,9 @@ +-- Table: proxy_timestamp +CREATE TABLE [proxy_timestamp] ( + [gobject_id] int NOT NULL, + [timestamp_of_last_change] timestamp NOT NULL, + [ImportGUID] nvarchar(265) NULL, + CONSTRAINT [PK_proxy_timestamp] PRIMARY KEY ([gobject_id]) +); +GO + diff --git a/gr/ddl/tables/redundancy.sql b/gr/ddl/tables/redundancy.sql new file mode 100644 index 0000000..12f3a7a --- /dev/null +++ b/gr/ddl/tables/redundancy.sql @@ -0,0 +1,8 @@ +-- Table: redundancy +CREATE TABLE [redundancy] ( + [primary_gobject_id] int NOT NULL, + [backup_gobject_id] int NOT NULL, + CONSTRAINT [PK_redundancy] PRIMARY KEY ([primary_gobject_id], [backup_gobject_id]) +); +GO + diff --git a/gr/ddl/tables/renamed_visual_element.sql b/gr/ddl/tables/renamed_visual_element.sql new file mode 100644 index 0000000..3f433b8 --- /dev/null +++ b/gr/ddl/tables/renamed_visual_element.sql @@ -0,0 +1,14 @@ +-- Table: renamed_visual_element +CREATE TABLE [renamed_visual_element] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_id] int NOT NULL, + [old_visual_element_name] nvarchar(329) NOT NULL, + [new_visual_element_name] nvarchar(329) NOT NULL, + [visual_element_type] nvarchar(32) NOT NULL, + [timestamp_of_rename] timestamp NOT NULL, + CONSTRAINT [PK_renamed_visual_element] PRIMARY KEY ([gobject_id], [package_id], [timestamp_of_rename]) +); +GO + diff --git a/gr/ddl/tables/schema_version.sql b/gr/ddl/tables/schema_version.sql new file mode 100644 index 0000000..ef79598 --- /dev/null +++ b/gr/ddl/tables/schema_version.sql @@ -0,0 +1,9 @@ +-- Table: schema_version +CREATE TABLE [schema_version] ( + [version_number] int NOT NULL, + [version_string] nvarchar(64) NOT NULL, + [cdi_version] nvarchar(64) NOT NULL, + [old_version_number] int NOT NULL DEFAULT ((0)) +); +GO + diff --git a/gr/ddl/tables/supported_locales.sql b/gr/ddl/tables/supported_locales.sql new file mode 100644 index 0000000..81697b5 --- /dev/null +++ b/gr/ddl/tables/supported_locales.sql @@ -0,0 +1,7 @@ +-- Table: supported_locales +CREATE TABLE [supported_locales] ( + [locale_id] smallint NOT NULL, + CONSTRAINT [PK_supported_locales] PRIMARY KEY ([locale_id]) +); +GO + diff --git a/gr/ddl/tables/template.sql b/gr/ddl/tables/template.sql new file mode 100644 index 0000000..3d96c22 --- /dev/null +++ b/gr/ddl/tables/template.sql @@ -0,0 +1,11 @@ +-- Table: template +CREATE TABLE [template] ( + [gobject_id] int NOT NULL, + [toolset_id] int NOT NULL, + CONSTRAINT [PK_template] PRIMARY KEY ([gobject_id], [gobject_id], [toolset_id]) +); +GO + +ALTER TABLE [template] ADD FOREIGN KEY ([toolset_id]) REFERENCES [toolset] ([toolset_id]); +GO + diff --git a/gr/ddl/tables/template_attribute.sql b/gr/ddl/tables/template_attribute.sql new file mode 100644 index 0000000..5426adb --- /dev/null +++ b/gr/ddl/tables/template_attribute.sql @@ -0,0 +1,20 @@ +-- Table: template_attribute +CREATE TABLE [template_attribute] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [mx_attribute_id] smallint NOT NULL, + [security_classification] int NOT NULL, + [mx_data_type] smallint NOT NULL, + [mx_value] text(2147483647) NOT NULL, + [lock_type] int NOT NULL, + [original_lock_type] int NOT NULL DEFAULT ((0)), + [bit_values] smallint NOT NULL DEFAULT ((0)), + [raw_value] nvarchar(1024) NULL DEFAULT (NULL), + CONSTRAINT [PK_template_attribute] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [mx_attribute_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [template_attribute] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/template_definition.sql b/gr/ddl/tables/template_definition.sql new file mode 100644 index 0000000..51426a6 --- /dev/null +++ b/gr/ddl/tables/template_definition.sql @@ -0,0 +1,20 @@ +-- Table: template_definition +CREATE TABLE [template_definition] ( + [template_definition_id] int NOT NULL, + [base_gobject_id] int NOT NULL DEFAULT ((0)), + [original_template_tagname] nvarchar(32) NOT NULL DEFAULT (''), + [required_features] nvarchar(256) NULL, + [supported_features] nvarchar(256) NULL, + [category_id] smallint NOT NULL, + [category_clsid] uniqueidentifier NULL, + [runtime_clsid] uniqueidentifier NULL, + [base_template_location] nchar(260) NOT NULL DEFAULT (''), + [vendor_name] nvarchar(256) NULL, + [major_version] smallint NULL, + [codebase] nvarchar(322) NULL, + [codebase_minor_version] int NOT NULL DEFAULT ((0)), + [event_mask] int NOT NULL DEFAULT ((0)), + CONSTRAINT [PK_template_definition] PRIMARY KEY ([template_definition_id]) +); +GO + diff --git a/gr/ddl/tables/template_idebehavior_link.sql b/gr/ddl/tables/template_idebehavior_link.sql new file mode 100644 index 0000000..4454525 --- /dev/null +++ b/gr/ddl/tables/template_idebehavior_link.sql @@ -0,0 +1,22 @@ +-- Table: template_idebehavior_link +CREATE TABLE [template_idebehavior_link] ( + [template_definition_id] int NOT NULL, + [shown_in_template_toolbox] bit NOT NULL DEFAULT ((1)), + [shown_in_model_view] bit NOT NULL DEFAULT ((1)), + [shown_in_deployment_view] bit NOT NULL DEFAULT ((1)), + [shown_in_security_editor] bit NOT NULL DEFAULT ((1)), + [shown_tagname] bit NOT NULL DEFAULT ((1)), + [shown_containedname] bit NOT NULL DEFAULT ((1)), + [shown_in_archestra_browser] bit NOT NULL DEFAULT ((1)), + [shown_standard_archestra_editor_tab] bit NOT NULL DEFAULT ((1)), + [enable_objectviewermenu] bit NOT NULL DEFAULT ((1)), + [enable_createinstance] bit NOT NULL DEFAULT ((1)), + [enable_createtemplate] bit NOT NULL DEFAULT ((1)), + [idebehavior_flags] int NOT NULL DEFAULT ((511)), + CONSTRAINT [PK_template_idebehavior_link] PRIMARY KEY ([template_definition_id], [template_definition_id]) +); +GO + +ALTER TABLE [template_idebehavior_link] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]); +GO + diff --git a/gr/ddl/tables/template_migration_policy.sql b/gr/ddl/tables/template_migration_policy.sql new file mode 100644 index 0000000..2e5789a --- /dev/null +++ b/gr/ddl/tables/template_migration_policy.sql @@ -0,0 +1,7 @@ +-- Table: template_migration_policy +CREATE TABLE [template_migration_policy] ( + [migrate_to_codebase] nvarchar(322) NOT NULL, + [migrate_from_codebase] nvarchar(322) NOT NULL +); +GO + diff --git a/gr/ddl/tables/timestamp_record.sql b/gr/ddl/tables/timestamp_record.sql new file mode 100644 index 0000000..ab36be7 --- /dev/null +++ b/gr/ddl/tables/timestamp_record.sql @@ -0,0 +1,6 @@ +-- Table: timestamp_record +CREATE TABLE [timestamp_record] ( + [ts] timestamp NOT NULL +); +GO + diff --git a/gr/ddl/tables/toolset.sql b/gr/ddl/tables/toolset.sql new file mode 100644 index 0000000..bffb46f --- /dev/null +++ b/gr/ddl/tables/toolset.sql @@ -0,0 +1,8 @@ +-- Table: toolset +CREATE TABLE [toolset] ( + [toolset_id] int NOT NULL, + [toolset_name] nvarchar(64) NOT NULL, + CONSTRAINT [PK_toolset] PRIMARY KEY ([toolset_id]) +); +GO + diff --git a/gr/ddl/tables/user_preferences.sql b/gr/ddl/tables/user_preferences.sql new file mode 100644 index 0000000..cdba241 --- /dev/null +++ b/gr/ddl/tables/user_preferences.sql @@ -0,0 +1,13 @@ +-- Table: user_preferences +CREATE TABLE [user_preferences] ( + [user_preferences_id] int NOT NULL, + [user_profile_id] int NOT NULL, + [preference_type] nvarchar(256) NOT NULL, + [preferences] image(2147483647) NULL, + CONSTRAINT [PK_user_preferences] PRIMARY KEY ([user_preferences_id], [user_profile_id]) +); +GO + +ALTER TABLE [user_preferences] ADD FOREIGN KEY ([user_profile_id]) REFERENCES [user_profile] ([user_profile_id]); +GO + diff --git a/gr/ddl/tables/user_profile.sql b/gr/ddl/tables/user_profile.sql new file mode 100644 index 0000000..344d618 --- /dev/null +++ b/gr/ddl/tables/user_profile.sql @@ -0,0 +1,22 @@ +-- Table: user_profile +CREATE TABLE [user_profile] ( + [user_profile_id] int NOT NULL, + [user_profile_name] nvarchar(256) NOT NULL, + [user_guid] uniqueidentifier NOT NULL DEFAULT (newid()), + [password_hash] int NULL, + [default_platform_tag_name] nvarchar(32) NULL, + [default_app_engine_tag_name] nvarchar(32) NULL, + [default_view_engine_tag_name] nvarchar(32) NULL, + [default_history_engine_tag_name] nvarchar(32) NULL, + [default_area_tag_name] nvarchar(32) NULL, + [default_security_group] nvarchar(32) NOT NULL DEFAULT ('Default'), + [ide_preferences] ntext(1073741823) NULL, + [roles] ntext(1073741823) NULL, + [user_full_name] nvarchar(256) NULL, + [intouch_access_level] int NULL DEFAULT ((0)), + [user_version_id] int NOT NULL DEFAULT ((1)), + [crypto_secure_hashed_pwd] ntext(1073741823) NULL, + CONSTRAINT [PK_user_profile] PRIMARY KEY ([user_profile_id]) +); +GO + diff --git a/gr/ddl/tables/visual_element.sql b/gr/ddl/tables/visual_element.sql new file mode 100644 index 0000000..131d8b5 --- /dev/null +++ b/gr/ddl/tables/visual_element.sql @@ -0,0 +1,10 @@ +-- Table: visual_element +CREATE TABLE [visual_element] ( + [visual_element_id] int NOT NULL, + [visual_element_type] nvarchar(32) NULL, + [inheritance_status] nchar(1) NULL, + [visual_element_category] nchar(1) NULL, + CONSTRAINT [PK_visual_element] PRIMARY KEY ([visual_element_id]) +); +GO + diff --git a/gr/ddl/tables/visual_element_affected_by_undo_check_out.sql b/gr/ddl/tables/visual_element_affected_by_undo_check_out.sql new file mode 100644 index 0000000..65ed7c6 --- /dev/null +++ b/gr/ddl/tables/visual_element_affected_by_undo_check_out.sql @@ -0,0 +1,11 @@ +-- Table: visual_element_affected_by_undo_check_out +CREATE TABLE [visual_element_affected_by_undo_check_out] ( + [visual_element_name] nvarchar(329) NULL, + [visual_element_id] int NULL, + [visual_element_type] nvarchar(32) NULL, + [change_type] int NULL, + [check_out_undone_by_user_guid] uniqueidentifier NOT NULL, + [timestamp_of_last_change] timestamp NOT NULL +); +GO + diff --git a/gr/ddl/tables/visual_element_archive.sql b/gr/ddl/tables/visual_element_archive.sql new file mode 100644 index 0000000..1d1f2ae --- /dev/null +++ b/gr/ddl/tables/visual_element_archive.sql @@ -0,0 +1,12 @@ +-- Table: visual_element_archive +CREATE TABLE [visual_element_archive] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_name] nvarchar(329) NOT NULL, + [visual_element_type] nvarchar(32) NOT NULL, + [visual_element_id] int NOT NULL, + [persisted_for_gobject_id] int NOT NULL +); +GO + diff --git a/gr/ddl/tables/visual_element_id.sql b/gr/ddl/tables/visual_element_id.sql new file mode 100644 index 0000000..c864c50 --- /dev/null +++ b/gr/ddl/tables/visual_element_id.sql @@ -0,0 +1,6 @@ +-- Table: visual_element_id +CREATE TABLE [visual_element_id] ( + [visual_element_id] int NOT NULL +); +GO + diff --git a/gr/ddl/tables/visual_element_reference.sql b/gr/ddl/tables/visual_element_reference.sql new file mode 100644 index 0000000..7e57026 --- /dev/null +++ b/gr/ddl/tables/visual_element_reference.sql @@ -0,0 +1,37 @@ +-- Table: visual_element_reference +CREATE TABLE [visual_element_reference] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_reference_index] int NOT NULL, + [visual_element_bind_status] int NOT NULL, + [is_relative_reference] char(1) NOT NULL, + [checked_in_bound_visual_element_gobject_id] int NULL, + [checked_in_bound_visual_element_package_id] int NULL, + [checked_in_bound_visual_element_mx_primitive_id] smallint NULL, + [checked_out_bound_visual_element_gobject_id] int NULL, + [checked_out_bound_visual_element_package_id] int NULL, + [checked_out_bound_visual_element_mx_primitive_id] smallint NULL, + [unbound_timestamp] timestamp NOT NULL, + [checked_in_unbound_visual_element_name] nvarchar(362) NULL, + [checked_in_unbound_visual_element_type] nvarchar(32) NULL, + [checked_in_unbound_tag_name] nvarchar(329) NULL, + [checked_in_unbound_primitive_name] nvarchar(329) NULL, + [checked_in_unbound_relative_object_name] nvarchar(329) NULL, + [checked_in_unbound_visual_element_id] int NULL, + [checked_out_unbound_visual_element_name] nvarchar(362) NULL, + [checked_out_unbound_visual_element_type] nvarchar(32) NULL, + [checked_out_unbound_tag_name] nvarchar(329) NULL, + [checked_out_unbound_primitive_name] nvarchar(329) NULL, + [checked_out_unbound_relative_object_name] nvarchar(329) NULL, + [checked_out_unbound_visual_element_id] int NULL, + [checked_out_visual_element_package_id] int NULL, + [checked_out_visual_element_gobject_id] int NULL, + [checked_out_to_user_guid] uniqueidentifier NULL, + CONSTRAINT [PK_visual_element_reference] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [visual_element_reference_index], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id]) +); +GO + +ALTER TABLE [visual_element_reference] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]); +GO + diff --git a/gr/ddl/tables/visual_element_timestamp.sql b/gr/ddl/tables/visual_element_timestamp.sql new file mode 100644 index 0000000..690d051 --- /dev/null +++ b/gr/ddl/tables/visual_element_timestamp.sql @@ -0,0 +1,11 @@ +-- Table: visual_element_timestamp +CREATE TABLE [visual_element_timestamp] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_id] int NOT NULL, + [change_type] int NOT NULL, + [timestamp_of_last_change] timestamp NOT NULL +); +GO + diff --git a/gr/ddl/tables/visual_element_version.sql b/gr/ddl/tables/visual_element_version.sql new file mode 100644 index 0000000..9ff8823 --- /dev/null +++ b/gr/ddl/tables/visual_element_version.sql @@ -0,0 +1,17 @@ +-- Table: visual_element_version +CREATE TABLE [visual_element_version] ( + [gobject_id] int NOT NULL, + [package_id] int NOT NULL, + [mx_primitive_id] smallint NOT NULL, + [visual_element_id] int NOT NULL, + [inherited_from_gobject_id] int NOT NULL, + [inherited_from_package_id] int NOT NULL, + [inherited_from_mx_primitive_id] smallint NOT NULL, + [inherited_from_visual_element_id] int NULL, + CONSTRAINT [PK_visual_element_version] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [inherited_from_gobject_id], [inherited_from_mx_primitive_id], [inherited_from_package_id], [inherited_from_gobject_id], [inherited_from_mx_primitive_id], [inherited_from_package_id], [inherited_from_gobject_id], [inherited_from_mx_primitive_id], [inherited_from_package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [visual_element_id]) +); +GO + +ALTER TABLE [visual_element_version] ADD FOREIGN KEY ([visual_element_id]) REFERENCES [visual_element] ([visual_element_id]); +GO + diff --git a/gr/ddl/tables/well_known_client_controls.sql b/gr/ddl/tables/well_known_client_controls.sql new file mode 100644 index 0000000..268fbe6 --- /dev/null +++ b/gr/ddl/tables/well_known_client_controls.sql @@ -0,0 +1,8 @@ +-- Table: well_known_client_controls +CREATE TABLE [well_known_client_controls] ( + [file_id] int NULL, + [class_name] nvarchar(1024) NOT NULL, + [toolbox_info] nvarchar(-1) NULL +); +GO + diff --git a/gr/ddl/views/internal_all_alarms_view.sql b/gr/ddl/views/internal_all_alarms_view.sql new file mode 100644 index 0000000..c1443e3 --- /dev/null +++ b/gr/ddl/views/internal_all_alarms_view.sql @@ -0,0 +1,11 @@ +-- View: internal_all_alarms_view + +CREATE VIEW [dbo].[internal_all_alarms_view] +AS +select (CASE + WHEN p.extension_type = N'badvaluealarmextension' + THEN g.tag_name + N'.' + p.primitive_name + N'.Bad' + ELSE + g.tag_name + N'.' + p.pri +GO + diff --git a/gr/ddl/views/internal_all_view_app_view.sql b/gr/ddl/views/internal_all_view_app_view.sql new file mode 100644 index 0000000..6d4141a --- /dev/null +++ b/gr/ddl/views/internal_all_view_app_view.sql @@ -0,0 +1,10 @@ +-- View: internal_all_view_app_view + +-- this view becomes an indexed view and is used to increase +-- the performance of internal_mark_view_app_for_redeploy +-- +-- contains gobject_ids of all view apps +CREATE view dbo.internal_all_view_app_view with schemabinding as + select TOP 100 +GO + diff --git a/gr/ddl/views/internal_automation_object_model_hierarchy_view.sql b/gr/ddl/views/internal_automation_object_model_hierarchy_view.sql new file mode 100644 index 0000000..162933b --- /dev/null +++ b/gr/ddl/views/internal_automation_object_model_hierarchy_view.sql @@ -0,0 +1,9 @@ +-- View: internal_automation_object_model_hierarchy_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_automation_object_Model_hierarchy_view +*** description: view is use +GO + diff --git a/gr/ddl/views/internal_automation_object_model_hierarchy_view2.sql b/gr/ddl/views/internal_automation_object_model_hierarchy_view2.sql new file mode 100644 index 0000000..8b0035e --- /dev/null +++ b/gr/ddl/views/internal_automation_object_model_hierarchy_view2.sql @@ -0,0 +1,9 @@ +-- View: internal_automation_object_model_hierarchy_view2 + +/* +*********************************************************************************************************************** +*** +*** object name: internal_automation_object_Model_hierarchy_view2 +*** description: view is +GO + diff --git a/gr/ddl/views/internal_automation_object_visual_element_hierarchy_view.sql b/gr/ddl/views/internal_automation_object_visual_element_hierarchy_view.sql new file mode 100644 index 0000000..1989869 --- /dev/null +++ b/gr/ddl/views/internal_automation_object_visual_element_hierarchy_view.sql @@ -0,0 +1,9 @@ +-- View: internal_automation_object_visual_element_hierarchy_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_automation_object_visual_element_hierarchy_view +*** description: vi +GO + diff --git a/gr/ddl/views/internal_automation_object_visual_element_hierarchy_view_includetemplates.sql b/gr/ddl/views/internal_automation_object_visual_element_hierarchy_view_includetemplates.sql new file mode 100644 index 0000000..6693507 --- /dev/null +++ b/gr/ddl/views/internal_automation_object_visual_element_hierarchy_view_includetemplates.sql @@ -0,0 +1,9 @@ +-- View: internal_automation_object_visual_element_hierarchy_view_includetemplates + +/* +*********************************************************************************************************************** +*** +*** object name: internal_automation_object_visual_element_hierarchy_view +*** description: vi +GO + diff --git a/gr/ddl/views/internal_checked_in_unbound_relative_visual_element_reference_view.sql b/gr/ddl/views/internal_checked_in_unbound_relative_visual_element_reference_view.sql new file mode 100644 index 0000000..d83dc89 --- /dev/null +++ b/gr/ddl/views/internal_checked_in_unbound_relative_visual_element_reference_view.sql @@ -0,0 +1,14 @@ +-- View: internal_checked_in_unbound_relative_visual_element_reference_view + +create view + dbo.internal_checked_in_unbound_relative_visual_element_reference_view +with schemabinding +as +select + ver.gobject_id, + ver.package_id, + ver.mx_primitive_id, + ver.visual_element_reference_index, + ver.checked_in_unbou +GO + diff --git a/gr/ddl/views/internal_checked_out_unbound_relative_visual_element_reference_view.sql b/gr/ddl/views/internal_checked_out_unbound_relative_visual_element_reference_view.sql new file mode 100644 index 0000000..e878e57 --- /dev/null +++ b/gr/ddl/views/internal_checked_out_unbound_relative_visual_element_reference_view.sql @@ -0,0 +1,14 @@ +-- View: internal_checked_out_unbound_relative_visual_element_reference_view + +create view + dbo.internal_checked_out_unbound_relative_visual_element_reference_view +with schemabinding +as +select + ver.gobject_id, + ver.package_id, + ver.mx_primitive_id, + ver.visual_element_reference_index, + ver.checked_out_unb +GO + diff --git a/gr/ddl/views/internal_common_obj.sql b/gr/ddl/views/internal_common_obj.sql new file mode 100644 index 0000000..adaadc7 --- /dev/null +++ b/gr/ddl/views/internal_common_obj.sql @@ -0,0 +1,13 @@ +-- View: internal_common_obj + +create view dbo.internal_common_obj as +SELECT + gobj.gobject_id AS gobject_id, + gobj.tag_name AS tag_name, + td.category_id AS category_id, + tset.toolset_name AS toolset_name, + gobj.is_template AS is_template, + pg.status_id AS status, + gob +GO + diff --git a/gr/ddl/views/internal_control_view.sql b/gr/ddl/views/internal_control_view.sql new file mode 100644 index 0000000..b8258b6 --- /dev/null +++ b/gr/ddl/views/internal_control_view.sql @@ -0,0 +1,15 @@ +-- View: internal_control_view + +create view dbo.internal_control_view +with schemabinding +as +select + ci.entity_id, + ci.control_name as name, + g.tag_name + '.' + ci.control_name as full_name, + g.gobject_id as parent_id, + g.tag_name as parent_name, + ci.control_id, + ci.control +GO + diff --git a/gr/ddl/views/internal_get_GRMblob_preview_user_view.sql b/gr/ddl/views/internal_get_GRMblob_preview_user_view.sql new file mode 100644 index 0000000..7dff96d --- /dev/null +++ b/gr/ddl/views/internal_get_GRMblob_preview_user_view.sql @@ -0,0 +1,8 @@ +-- View: internal_get_GRMblob_preview_user_view + +/****** Object: View [dbo].[[internal_get_GRMblob_preview_user_view]] + + This purpose of this view to get GRM Definition blob for the list of all visual elements in the galaxy. + It uses the latest checked-out package for preview and consider's the +GO + diff --git a/gr/ddl/views/internal_get_TemplateToolBox_Allelements_view.sql b/gr/ddl/views/internal_get_TemplateToolBox_Allelements_view.sql new file mode 100644 index 0000000..65333ab --- /dev/null +++ b/gr/ddl/views/internal_get_TemplateToolBox_Allelements_view.sql @@ -0,0 +1,10 @@ +-- View: internal_get_TemplateToolBox_Allelements_view + +CREATE view [dbo].[internal_get_TemplateToolBox_Allelements_view] +AS +SELECT Entities.* FROM +( + SELECT + -parent_folder_id as 'parent_id', -- this '-' is required to identify the folder as parent in browisng service while generating the SQ +GO + diff --git a/gr/ddl/views/internal_get_TemplateToolBox_folders_view.sql b/gr/ddl/views/internal_get_TemplateToolBox_folders_view.sql new file mode 100644 index 0000000..bf6d0bf --- /dev/null +++ b/gr/ddl/views/internal_get_TemplateToolBox_folders_view.sql @@ -0,0 +1,15 @@ +-- View: internal_get_TemplateToolBox_folders_view + +CREATE view [dbo].[internal_get_TemplateToolBox_folders_view] +AS +SELECT folder_id, + folder_name, + parent_folder_id, + depth, + has_objects, + has_folders, + timestamp_of_last_change + FROM folder + Where folder_typ +GO + diff --git a/gr/ddl/views/internal_get_asset_graphics_preview_user_view.sql b/gr/ddl/views/internal_get_asset_graphics_preview_user_view.sql new file mode 100644 index 0000000..d0e159e --- /dev/null +++ b/gr/ddl/views/internal_get_asset_graphics_preview_user_view.sql @@ -0,0 +1,9 @@ +-- View: internal_get_asset_graphics_preview_user_view + +/****** Object: View [dbo].[internal_asset_graphics_preview_user_view] + + This purpose of this view to get Asset graphics in the galaxy. + It uses the latest checked-out package for preview and consider's the users context. +******/ +GO + diff --git a/gr/ddl/views/internal_get_attribute_definition_with_validation.sql b/gr/ddl/views/internal_get_attribute_definition_with_validation.sql new file mode 100644 index 0000000..d171d9e --- /dev/null +++ b/gr/ddl/views/internal_get_attribute_definition_with_validation.sql @@ -0,0 +1,12 @@ +-- View: internal_get_attribute_definition_with_validation + +CREATE VIEW [dbo].[internal_get_attribute_definition_with_validation] +AS +SELECT + ad.*, + pd.primitive_name, + CASE + WHEN pd.primitive_name = 'ScalingExtension' AND ad.mx_attribute_id IN (101, 102, 103, 104, 105, 106, 107, 108) THEN 1 + WHEN pd.primit +GO + diff --git a/gr/ddl/views/internal_get_device_scangroups_view.sql b/gr/ddl/views/internal_get_device_scangroups_view.sql new file mode 100644 index 0000000..a298d3d --- /dev/null +++ b/gr/ddl/views/internal_get_device_scangroups_view.sql @@ -0,0 +1,12 @@ +-- View: internal_get_device_scangroups_view + +create view [dbo].[internal_get_device_scangroups_view] +as +select +convert(int, p.mx_primitive_id) as entity_id, + p.primitive_name as name, g.gobject_id as parent_id + from gobject g + inner join [dbo].primitive_instance p + on p.gobject_id = g.gob +GO + diff --git a/gr/ddl/views/internal_get_external_content_media_types_view.sql b/gr/ddl/views/internal_get_external_content_media_types_view.sql new file mode 100644 index 0000000..18d0bd9 --- /dev/null +++ b/gr/ddl/views/internal_get_external_content_media_types_view.sql @@ -0,0 +1,15 @@ +-- View: internal_get_external_content_media_types_view + +create view dbo.internal_get_external_content_media_types_view +with schemabinding +as +select + mt.entity_id, + mt.media_type, + g.tag_name as parent_name, + ci.control_id, + mt.uri_property_name, + mt.media_type_property_name, + mt +GO + diff --git a/gr/ddl/views/internal_get_gtb_Allelements_view.sql b/gr/ddl/views/internal_get_gtb_Allelements_view.sql new file mode 100644 index 0000000..afe64ed --- /dev/null +++ b/gr/ddl/views/internal_get_gtb_Allelements_view.sql @@ -0,0 +1,14 @@ +-- View: internal_get_gtb_Allelements_view + +CREATE view [dbo].[internal_get_gtb_Allelements_view] +as + +SELECT Entities.* FROM +( + SELECT + -parent_folder_id as 'parent_id', + folder_name as name, + 'Folder' as type, + -folder_id as 'entity_id', +GO + diff --git a/gr/ddl/views/internal_get_gtb_elements_view.sql b/gr/ddl/views/internal_get_gtb_elements_view.sql new file mode 100644 index 0000000..68e75d0 --- /dev/null +++ b/gr/ddl/views/internal_get_gtb_elements_view.sql @@ -0,0 +1,14 @@ +-- View: internal_get_gtb_elements_view + +create view [dbo].[internal_get_gtb_elements_view] +as +SELECT + vev.gobject_id, + cd.last_modified, + tv.timestamp_of_last_change, + vev.user_guid, + vev.visual_element_name, + vev.thumbnail, + vev.visual_element_de +GO + diff --git a/gr/ddl/views/internal_get_gtb_folders_view.sql b/gr/ddl/views/internal_get_gtb_folders_view.sql new file mode 100644 index 0000000..8615add --- /dev/null +++ b/gr/ddl/views/internal_get_gtb_folders_view.sql @@ -0,0 +1,13 @@ +-- View: internal_get_gtb_folders_view + +create view dbo.internal_get_gtb_folders_view +as +SELECT folder_id, + folder_name, + parent_folder_id, + depth, + has_objects, + case when f.has_folders = 1 or (Select count(*) from gobject g + inner join folder_gobject_link +GO + diff --git a/gr/ddl/views/internal_get_gtb_visual_element_references_deployment_view.sql b/gr/ddl/views/internal_get_gtb_visual_element_references_deployment_view.sql new file mode 100644 index 0000000..971d382 --- /dev/null +++ b/gr/ddl/views/internal_get_gtb_visual_element_references_deployment_view.sql @@ -0,0 +1,11 @@ +-- View: internal_get_gtb_visual_element_references_deployment_view + +create view [dbo].[internal_get_gtb_visual_element_references_deployment_view] +as + +WITH linkedSymbol (visual_element_id, visual_element_bind_status, gobject_id, package_id, mx_primitive_id) + AS + ( + select vev.visual_element_id,ver +GO + diff --git a/gr/ddl/views/internal_get_gtb_visual_element_references_preview_view.sql b/gr/ddl/views/internal_get_gtb_visual_element_references_preview_view.sql new file mode 100644 index 0000000..fae53ca --- /dev/null +++ b/gr/ddl/views/internal_get_gtb_visual_element_references_preview_view.sql @@ -0,0 +1,11 @@ +-- View: internal_get_gtb_visual_element_references_preview_view + +/****** Object: View [dbo].[internal_get_gtb_visual_element_references_preview_view] + + This purpose of this view is to get Symbol VERL in the galaxy. +******/ +create view [dbo].[internal_get_gtb_visual_element_references_preview_view] +as +WITH li +GO + diff --git a/gr/ddl/views/internal_get_gtb_visual_element_references_view.sql b/gr/ddl/views/internal_get_gtb_visual_element_references_view.sql new file mode 100644 index 0000000..815e0af --- /dev/null +++ b/gr/ddl/views/internal_get_gtb_visual_element_references_view.sql @@ -0,0 +1,11 @@ +-- View: internal_get_gtb_visual_element_references_view + +create view [dbo].[internal_get_gtb_visual_element_references_view] +as + +WITH linkedSymbol (visual_element_id, visual_element_bind_status, gobject_id, package_id, mx_primitive_id) + AS + ( + select vev.visual_element_id,ver.visual_ele +GO + diff --git a/gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_preview_view.sql b/gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_preview_view.sql new file mode 100644 index 0000000..7f82822 --- /dev/null +++ b/gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_preview_view.sql @@ -0,0 +1,9 @@ +-- View: internal_get_object_wizard_symbol_override_mapping_preview_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_get_object_wizard_symbol_override_mapping_preview_view +*** description: +GO + diff --git a/gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_view.sql b/gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_view.sql new file mode 100644 index 0000000..950567f --- /dev/null +++ b/gr/ddl/views/internal_get_object_wizard_symbol_override_mapping_view.sql @@ -0,0 +1,9 @@ +-- View: internal_get_object_wizard_symbol_override_mapping_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_get_object_wizard_symbol_override_mapping_view +*** description: vie +GO + diff --git a/gr/ddl/views/internal_get_object_wizard_symbol_override_preview_view.sql b/gr/ddl/views/internal_get_object_wizard_symbol_override_preview_view.sql new file mode 100644 index 0000000..66e3a92 --- /dev/null +++ b/gr/ddl/views/internal_get_object_wizard_symbol_override_preview_view.sql @@ -0,0 +1,9 @@ +-- View: internal_get_object_wizard_symbol_override_preview_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_get_object_wizard_symbol_override_preview_view +*** description: vie +GO + diff --git a/gr/ddl/views/internal_get_object_wizard_symbol_override_view.sql b/gr/ddl/views/internal_get_object_wizard_symbol_override_view.sql new file mode 100644 index 0000000..af76a8c --- /dev/null +++ b/gr/ddl/views/internal_get_object_wizard_symbol_override_view.sql @@ -0,0 +1,9 @@ +-- View: internal_get_object_wizard_symbol_override_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_get_object_wizard_symbol_override_view +*** description: view is use +GO + diff --git a/gr/ddl/views/internal_get_visual_element_primitives_deployment_view.sql b/gr/ddl/views/internal_get_visual_element_primitives_deployment_view.sql new file mode 100644 index 0000000..5cf08f2 --- /dev/null +++ b/gr/ddl/views/internal_get_visual_element_primitives_deployment_view.sql @@ -0,0 +1,8 @@ +-- View: internal_get_visual_element_primitives_deployment_view + +/****** Object: View [dbo].[internal_get_visual_element_primitives_deployment_view] + + This purpose of this view to get GRM Definition blob for the list of all visual elements in the galaxy. + It uses the latest checked-in package for deployment +GO + diff --git a/gr/ddl/views/internal_get_visual_element_primitives_view.sql b/gr/ddl/views/internal_get_visual_element_primitives_view.sql new file mode 100644 index 0000000..581d530 --- /dev/null +++ b/gr/ddl/views/internal_get_visual_element_primitives_view.sql @@ -0,0 +1,11 @@ +-- View: internal_get_visual_element_primitives_view + +CREATE VIEW dbo.internal_get_visual_element_primitives_view +AS + WITH parentfoldersprefixed + AS (SELECT gobject_id, + contained_by_gobject_id, + Cast(tag_name AS NVARCHAR(250)) AS fullName + FROM +GO + diff --git a/gr/ddl/views/internal_gtb_symbols_hierarchy_view.sql b/gr/ddl/views/internal_gtb_symbols_hierarchy_view.sql new file mode 100644 index 0000000..b819377 --- /dev/null +++ b/gr/ddl/views/internal_gtb_symbols_hierarchy_view.sql @@ -0,0 +1,12 @@ +-- View: internal_gtb_symbols_hierarchy_view + +/****** Object: View [dbo].[internal_gtb_symbols_hierarchy_view] + + This purpose of this view to get the hierarchy for the GTB graphics in the galaxy. +******/ +CREATE view [dbo].[internal_gtb_symbols_hierarchy_view] +as + +-- Query to get GTB symbo +GO + diff --git a/gr/ddl/views/internal_linked_visual_element_definition_per_user_view.sql b/gr/ddl/views/internal_linked_visual_element_definition_per_user_view.sql new file mode 100644 index 0000000..8c3bb16 --- /dev/null +++ b/gr/ddl/views/internal_linked_visual_element_definition_per_user_view.sql @@ -0,0 +1,9 @@ +-- View: internal_linked_visual_element_definition_per_user_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_linked_visual_element_definition_per_user_view +*** description: vie +GO + diff --git a/gr/ddl/views/internal_list_automation_objects_view.sql b/gr/ddl/views/internal_list_automation_objects_view.sql new file mode 100644 index 0000000..e9252ab --- /dev/null +++ b/gr/ddl/views/internal_list_automation_objects_view.sql @@ -0,0 +1,13 @@ +-- View: internal_list_automation_objects_view + +create view [dbo].[internal_list_automation_objects_view] +AS + +select distinct g.gobject_id as 'entity_id', + g.tag_name as 'name', + 'object' as type, + '' as description, + up.user_guid, + CASE WHEN g.checked_out_by_user_guid = up.user_guid T +GO + diff --git a/gr/ddl/views/internal_list_model_objects_view.sql b/gr/ddl/views/internal_list_model_objects_view.sql new file mode 100644 index 0000000..e483805 --- /dev/null +++ b/gr/ddl/views/internal_list_model_objects_view.sql @@ -0,0 +1,18 @@ +-- View: internal_list_model_objects_view + +CREATE view dbo.internal_list_model_objects_view +AS + +with ParentFoldersPrefixed as + +( + + select gobject_id, contained_by_gobject_id,cast(tag_name as nvarchar(250)) as fullName + + from gobject + + where contained_by_gobject_id = 0 + + union a +GO + diff --git a/gr/ddl/views/internal_list_objects_view.sql b/gr/ddl/views/internal_list_objects_view.sql new file mode 100644 index 0000000..6b952e6 --- /dev/null +++ b/gr/ddl/views/internal_list_objects_view.sql @@ -0,0 +1,9 @@ +-- View: internal_list_objects_view + +/**************************************************/ +/*Object Name : internal_list_objects_view */ +/*Object Type : View. */ +/*Purpose : To provide a generic view used by the different list objects stored procedure*/ +/*Used By : CDI +GO + diff --git a/gr/ddl/views/internal_list_unassigned_objects_view.sql b/gr/ddl/views/internal_list_unassigned_objects_view.sql new file mode 100644 index 0000000..b13f5db --- /dev/null +++ b/gr/ddl/views/internal_list_unassigned_objects_view.sql @@ -0,0 +1,16 @@ +-- View: internal_list_unassigned_objects_view + +create view dbo.internal_list_unassigned_objects_view +AS + +with ParentFoldersPrefixed as +( + select gobject_id, contained_by_gobject_id + from gobject + where contained_by_gobject_id = 0 + + union all + + select c.gobject_id, c.contained_by_ +GO + diff --git a/gr/ddl/views/internal_localized_alarm_messages.sql b/gr/ddl/views/internal_localized_alarm_messages.sql new file mode 100644 index 0000000..1f7087b --- /dev/null +++ b/gr/ddl/views/internal_localized_alarm_messages.sql @@ -0,0 +1,7 @@ +-- View: internal_localized_alarm_messages + +CREATE VIEW dbo.internal_localized_alarm_messages +AS + --select g.tag_name + '.' + p.primitive_name 'Name', amd.default_message, amd.indexable_default_message, amd.phrase_id, amt.translated_message, amt.locale_id, am.gobject_id, am.package_id, am.mx_primi +GO + diff --git a/gr/ddl/views/internal_model_hierarchy_asset_view.sql b/gr/ddl/views/internal_model_hierarchy_asset_view.sql new file mode 100644 index 0000000..1e57f5e --- /dev/null +++ b/gr/ddl/views/internal_model_hierarchy_asset_view.sql @@ -0,0 +1,12 @@ +-- View: internal_model_hierarchy_asset_view + +/****** Object: View [dbo].[internal_model_hierarchy_asset_view] + + This purpose of this view to get the Model hierarchy for the objects in the galaxy. +******/ +Create view [dbo].[internal_model_hierarchy_asset_view] +as + +select g.gobject_id, +GO + diff --git a/gr/ddl/views/internal_package_status_view.sql b/gr/ddl/views/internal_package_status_view.sql new file mode 100644 index 0000000..5114f36 --- /dev/null +++ b/gr/ddl/views/internal_package_status_view.sql @@ -0,0 +1,16 @@ +-- View: internal_package_status_view + +/* + This view is used by the WWCDI:QueryObject.h + +*/ + +create view dbo.internal_package_status_view +as + select + g.gobject_id, + p.package_id, + CASE WHEN (p.status_id = 0 and p.reference_status_id = 0) THEN + CASE WHEN ver_warning_view.has_wa +GO + diff --git a/gr/ddl/views/internal_proxy_obj.sql b/gr/ddl/views/internal_proxy_obj.sql new file mode 100644 index 0000000..cbb81b8 --- /dev/null +++ b/gr/ddl/views/internal_proxy_obj.sql @@ -0,0 +1,12 @@ +-- View: internal_proxy_obj + +create view dbo.internal_proxy_obj +as +SELECT + -- These are the data used in the proxy object + gobject.gobject_id AS gobject_id, + gobject.tag_name AS tag_name, + gobject.contained_name AS contained_name, + gobject.hierarchical_name AS hierarch +GO + diff --git a/gr/ddl/views/internal_reference_primitive_attribute.sql b/gr/ddl/views/internal_reference_primitive_attribute.sql new file mode 100644 index 0000000..e089fc0 --- /dev/null +++ b/gr/ddl/views/internal_reference_primitive_attribute.sql @@ -0,0 +1,9 @@ +-- View: internal_reference_primitive_attribute + +/**********************************************************************/ +/*Object Name : internal_reference_primitive_attribute */ +/*Object Type : View. */ +/*Purpose : To provide reference and cross reference information */ +/* +GO + diff --git a/gr/ddl/views/internal_required_support_features.sql b/gr/ddl/views/internal_required_support_features.sql new file mode 100644 index 0000000..63ec7cc --- /dev/null +++ b/gr/ddl/views/internal_required_support_features.sql @@ -0,0 +1,9 @@ +-- View: internal_required_support_features + +/**************************************************/ +/*Object Name : internal_required_support_features */ +/*Object Type : View. */ +/*Purpose : To provide required_feature and supported feature of all the instances*/ +/*Used By : CDI +GO + diff --git a/gr/ddl/views/internal_runtime_attributes.sql b/gr/ddl/views/internal_runtime_attributes.sql new file mode 100644 index 0000000..442f75b --- /dev/null +++ b/gr/ddl/views/internal_runtime_attributes.sql @@ -0,0 +1,13 @@ +-- View: internal_runtime_attributes + +-- +-- This view is used to supply the GalaxyTagDictionary with tagnames +-- for the InTouch tag browser. +-- +create view dbo.internal_runtime_attributes +as +select + case when primitive_instance.primitive_name = '' then + gobject.tag_name + +GO + diff --git a/gr/ddl/views/internal_visible_packages_per_user_view.sql b/gr/ddl/views/internal_visible_packages_per_user_view.sql new file mode 100644 index 0000000..ee665bc --- /dev/null +++ b/gr/ddl/views/internal_visible_packages_per_user_view.sql @@ -0,0 +1,12 @@ +-- View: internal_visible_packages_per_user_view + +-- This view returns package versions visible to each +-- user. +-- +-- It is designed to be added to joins to select packages for +-- a given user. +-- +-- For example, to get the visual element versions to use for 'Administrator' +-- user given by @user_ +GO + diff --git a/gr/ddl/views/internal_visual_element_description_all_packages_view.sql b/gr/ddl/views/internal_visual_element_description_all_packages_view.sql new file mode 100644 index 0000000..f903117 --- /dev/null +++ b/gr/ddl/views/internal_visual_element_description_all_packages_view.sql @@ -0,0 +1,13 @@ +-- View: internal_visual_element_description_all_packages_view + +create view dbo.internal_visual_element_description_all_packages_view +as + +select + vev.gobject_id, + vev.visual_element_id, + ve.visual_element_type, + ve.visual_element_category, + case when pri.primitive_name <> '' +GO + diff --git a/gr/ddl/views/internal_visual_element_description_per_user_view.sql b/gr/ddl/views/internal_visual_element_description_per_user_view.sql new file mode 100644 index 0000000..f7e06a3 --- /dev/null +++ b/gr/ddl/views/internal_visual_element_description_per_user_view.sql @@ -0,0 +1,9 @@ +-- View: internal_visual_element_description_per_user_view + +/* +*********************************************************************************************************************** +*** +*** object name: internal_visual_element_description_per_user_view +*** description: view is u +GO + diff --git a/gr/ddl/views/internal_visual_element_description_view.sql b/gr/ddl/views/internal_visual_element_description_view.sql new file mode 100644 index 0000000..be1241e --- /dev/null +++ b/gr/ddl/views/internal_visual_element_description_view.sql @@ -0,0 +1,13 @@ +-- View: internal_visual_element_description_view + +create view dbo.internal_visual_element_description_view +with schemabinding +as +select + vev.gobject_id, + vev.visual_element_id, + ve.visual_element_type, + ve.visual_element_category, + case when pri.primitive_name +GO + diff --git a/gr/ddl/views/internal_visual_element_primitives_preview_user_view.sql b/gr/ddl/views/internal_visual_element_primitives_preview_user_view.sql new file mode 100644 index 0000000..d3f2f3b --- /dev/null +++ b/gr/ddl/views/internal_visual_element_primitives_preview_user_view.sql @@ -0,0 +1,8 @@ +-- View: internal_visual_element_primitives_preview_user_view + +/****** Object: View [dbo].[internal_visual_element_primitives_preview_user_view] + + This purpose of this view to get GRM Definition blob for the list of all visual elements in the galaxy. + It uses the latest checked-out package for preview and co +GO + diff --git a/gr/ddl/views/internal_visual_element_reference_per_user_view.sql b/gr/ddl/views/internal_visual_element_reference_per_user_view.sql new file mode 100644 index 0000000..f430ac6 --- /dev/null +++ b/gr/ddl/views/internal_visual_element_reference_per_user_view.sql @@ -0,0 +1,12 @@ +-- View: internal_visual_element_reference_per_user_view + +create view dbo.internal_visual_element_reference_per_user_view +as +select distinct + ver.gobject_id, + ver.package_id, + ver.mx_primitive_id, + ver.visual_element_bind_status, + ver.visual_element_reference_index, +GO + diff --git a/gr/ddl/views/internal_visual_element_reference_view.sql b/gr/ddl/views/internal_visual_element_reference_view.sql new file mode 100644 index 0000000..7e8dbd0 --- /dev/null +++ b/gr/ddl/views/internal_visual_element_reference_view.sql @@ -0,0 +1,13 @@ +-- View: internal_visual_element_reference_view + +create view dbo.internal_visual_element_reference_view +as +select distinct + ver.gobject_id, + ver.package_id, + ver.mx_primitive_id, + ver.visual_element_bind_status, + ver.visual_element_reference_index, + ch +GO + diff --git a/gr/ddl/views/internal_visual_element_reference_warning_status_view.sql b/gr/ddl/views/internal_visual_element_reference_warning_status_view.sql new file mode 100644 index 0000000..f0f3796 --- /dev/null +++ b/gr/ddl/views/internal_visual_element_reference_warning_status_view.sql @@ -0,0 +1,15 @@ +-- View: internal_visual_element_reference_warning_status_view + +create view dbo.internal_visual_element_reference_warning_status_view +as +select + distinct + g.gobject_id, + p.package_id, + case when (isnull(ver.gobject_id,0)> 0) then 1 + else 0 + end as has_warning +from package p with(nolock) +inner join gobject g w +GO + diff --git a/gr/ddl/views/internal_visual_element_timestamp_per_user_view.sql b/gr/ddl/views/internal_visual_element_timestamp_per_user_view.sql new file mode 100644 index 0000000..4691c3d --- /dev/null +++ b/gr/ddl/views/internal_visual_element_timestamp_per_user_view.sql @@ -0,0 +1,15 @@ +-- View: internal_visual_element_timestamp_per_user_view + +create view dbo.internal_visual_element_timestamp_per_user_view +as +select + g.gobject_id, + vev.package_id, + vev.mx_primitive_id, + vev.visual_element_id, + vet.change_type, + vet.timestamp_of_last_change, + up.user_guid, + pri.property_b +GO + diff --git a/gr/ddl/views/public_gobject_definition.sql b/gr/ddl/views/public_gobject_definition.sql new file mode 100644 index 0000000..0b7d11d --- /dev/null +++ b/gr/ddl/views/public_gobject_definition.sql @@ -0,0 +1,7 @@ +-- View: public_gobject_definition + +create view dbo.public_gobject_definition as + +SELECT gobject.tag_name, gobject.contained_name, primitive_definition.primitive_name, attribute_definition.attribute_name, attribute_definition.mx_attribute_id, attribute_definition.has_config_set_handler +GO + diff --git a/gr/hierarchy.jpeg b/gr/hierarchy.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f9924ee0f40df9eb4554bc83ee885db20847e8fd GIT binary patch literal 16788 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 C. +``` + +Examples: +- `TestMachine_001.MachineID` +- `TestMachine_001.MachineCode` +- `MESReceiver_001.MoveInBatchID` +- `MESReceiver_001.MoveInCompleteFlag` +- `MESReceiver_001.MoveInJobSequenceNumber` +- `DelmiaReceiver_001.DownloadPath` +- `DelmiaReceiver_001.JobStepNumber` +- `DelmiaReceiver_001.PartNumber` +- `DelmiaReceiver_001.RecipeDownloadFlag` + +This flat view collapses the hierarchy — all attributes across all objects appear as `tag_name.AttributeName` regardless of where the object sits in the containment tree. diff --git a/gr/parse_tables.py b/gr/parse_tables.py new file mode 100644 index 0000000..7bd72a2 --- /dev/null +++ b/gr/parse_tables.py @@ -0,0 +1,122 @@ +import re, os + +base = r"C:\Users\dohertj2\Desktop\gr" +ddl_dir = os.path.join(base, "ddl", "tables") +os.makedirs(ddl_dir, exist_ok=True) + +with open(os.path.join(base, "table_dump_raw.txt"), "r", encoding="utf-8") as f: + content = f.read() + +tables = re.findall(r'---TABLE_START:(.+?)---(.+?)---TABLE_END:\1---', content, re.DOTALL) + +schema_lines = ["# Schema Reference\n\nGenerated from the ZB (Galaxy Repository) database.\n\n## Tables\n"] + +for tbl_name, tbl_content in tables: + tbl_name = tbl_name.strip() + + # Parse columns + sections = tbl_content.strip().split("COLUMN_NAME") + cols = [] + pk_cols = [] + fks = [] + + # Parse column definitions (first section after header) + if len(sections) >= 2: + col_section = sections[1] + lines = [l.strip() for l in col_section.split("\n") if l.strip() and not l.strip().startswith("---")] + for line in lines: + parts = [p.strip() for p in line.split("|")] + if len(parts) >= 6 and parts[0] not in ("", "COLUMN_NAME") and not all(c in "-|" for c in parts[0]): + col_name = parts[0] + data_type = parts[1] + char_len = parts[2] if parts[2] != "NULL" else None + num_prec = parts[3] if parts[3] != "NULL" else None + num_scale = parts[4] if parts[4] != "NULL" else None + nullable = parts[5] + default_val = parts[6] if len(parts) > 6 and parts[6] != "NULL" else None + + # Build type string + if char_len: + type_str = f"{data_type}({char_len})" + elif num_prec and data_type not in ("int", "smallint", "bigint", "tinyint", "bit"): + type_str = f"{data_type}({num_prec},{num_scale})" + else: + type_str = data_type + + cols.append({ + "name": col_name, + "type": type_str, + "nullable": nullable, + "default": default_val + }) + + # Parse PK columns (second COLUMN_NAME section) + if len(sections) >= 3: + pk_section = sections[2] + lines = [l.strip() for l in pk_section.split("\n") if l.strip() and not l.strip().startswith("---") and not l.strip().startswith("fk_")] + for line in lines: + parts = [p.strip() for p in line.split("|")] + pk_candidate = parts[0] + if pk_candidate and pk_candidate not in ("", "COLUMN_NAME") and not all(c in "-|" for c in pk_candidate): + # Stop if we hit the FK header + if "ref_table" in line or "fk_column" in line: + break + pk_cols.append(pk_candidate) + + # Parse FKs + fk_matches = re.findall(r'fk_column\|ref_table\|ref_column\|fk_name\n-+\|.+\n(.*?)(?=---TABLE_END|$)', tbl_content, re.DOTALL) + if fk_matches: + for fk_block in fk_matches: + for line in fk_block.strip().split("\n"): + line = line.strip() + if line and "|" in line: + parts = [p.strip() for p in line.split("|")] + if len(parts) >= 3 and parts[0] and not all(c in "-" for c in parts[0]): + fks.append({"col": parts[0], "ref_table": parts[1], "ref_col": parts[2]}) + + # Write DDL file + ddl_lines = [f"-- Table: {tbl_name}\n"] + ddl_lines.append(f"CREATE TABLE [{tbl_name}] (") + col_defs = [] + for c in cols: + line = f" [{c['name']}] {c['type']}" + if c['nullable'] == 'NO': + line += " NOT NULL" + else: + line += " NULL" + if c['default']: + line += f" DEFAULT {c['default']}" + col_defs.append(line) + if pk_cols: + col_defs.append(f" CONSTRAINT [PK_{tbl_name}] PRIMARY KEY ({', '.join('[' + c + ']' for c in pk_cols)})") + ddl_lines.append(",\n".join(col_defs)) + ddl_lines.append(");\nGO\n") + + if fks: + for fk in fks: + ddl_lines.append(f"ALTER TABLE [{tbl_name}] ADD FOREIGN KEY ([{fk['col']}]) REFERENCES [{fk['ref_table']}] ([{fk['ref_col']}]);") + ddl_lines.append("GO\n") + + with open(os.path.join(ddl_dir, f"{tbl_name}.sql"), "w", encoding="utf-8") as f: + f.write("\n".join(ddl_lines)) + + # Build schema.md entry + schema_lines.append(f"### {tbl_name}\n") + schema_lines.append("| Column | Type | Nullable | Notes |") + schema_lines.append("|--------|------|----------|-------|") + for c in cols: + notes = [] + if c['name'] in pk_cols: + notes.append("PK") + for fk in fks: + if fk['col'] == c['name']: + notes.append(f"FK -> {fk['ref_table']}.{fk['ref_col']}") + if c['default']: + notes.append(f"Default: {c['default']}") + schema_lines.append(f"| {c['name']} | {c['type']} | {c['nullable']} | {' '.join(notes)} |") + schema_lines.append("") + +print(f"Processed {len(tables)} tables") + +with open(os.path.join(base, "schema_tables.md"), "w", encoding="utf-8") as f: + f.write("\n".join(schema_lines)) diff --git a/gr/queries/attributes.sql b/gr/queries/attributes.sql new file mode 100644 index 0000000..0760700 --- /dev/null +++ b/gr/queries/attributes.sql @@ -0,0 +1,63 @@ +-- Galaxy Object User-Defined Attributes/Tags for OPC UA Server +-- Returns user-defined (dynamic) attributes for automation objects. +-- These are the attributes defined on templates and inherited by instances +-- via the derived_from_gobject_id chain (e.g., MachineID, MoveInFlag). +-- +-- Use full_tag_reference for read/write operations against the runtime. +-- Join with hierarchy.sql results on gobject_id to place attributes in the OPC UA browse tree. +-- +-- For system/primitive attributes as well, see attributes_extended.sql. +-- +-- Array dimensions are extracted from the mx_value hex string on the template's +-- dynamic_attribute row (bytes 5-6, little-endian uint16 at hex positions 13-16). +-- +-- Data types (mx_data_type): +-- 1 = Boolean, 2 = Integer (Int32), 3 = Float (Single), 4 = Double, +-- 5 = String, 6 = Time (DateTime), 7 = ElapsedTime (TimeSpan), +-- 8 = (reference), 13 = (enumeration), 14 = (custom), 15 = InternationalizedString, 16 = (custom) + +;WITH template_chain AS ( + -- Start from each non-template instance + SELECT g.gobject_id, g.derived_from_gobject_id, 0 AS depth + FROM gobject g + WHERE g.is_template = 0 + UNION ALL + -- Walk up the template derivation chain + SELECT tc.gobject_id, t.derived_from_gobject_id, tc.depth + 1 + FROM template_chain tc + INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id + WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10 +) +SELECT DISTINCT + g.gobject_id, + g.tag_name, + da.attribute_name, + g.tag_name + '.' + da.attribute_name + + CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END + AS full_tag_reference, + da.mx_data_type, + dt.description AS data_type_name, + da.is_array, + CASE WHEN da.is_array = 1 + THEN CONVERT(int, CONVERT(varbinary(2), + SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2)) + ELSE NULL + END AS array_dimension, + da.mx_attribute_category, + da.security_classification +FROM template_chain tc +INNER JOIN dynamic_attribute da + ON da.gobject_id = tc.derived_from_gobject_id +INNER JOIN gobject g + ON g.gobject_id = tc.gobject_id +INNER JOIN template_definition td + ON td.template_definition_id = g.template_definition_id +LEFT JOIN data_type dt + ON dt.mx_data_type = da.mx_data_type +WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND g.is_template = 0 + AND g.deployed_package_id <> 0 + AND da.attribute_name NOT LIKE '[_]%' + AND da.attribute_name NOT LIKE '%.Description' + AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) +ORDER BY g.tag_name, da.attribute_name; diff --git a/gr/queries/attributes_extended.sql b/gr/queries/attributes_extended.sql new file mode 100644 index 0000000..aaf8fd8 --- /dev/null +++ b/gr/queries/attributes_extended.sql @@ -0,0 +1,134 @@ +-- Galaxy Object Attributes/Tags for OPC UA Server +-- Returns all runtime-readable attributes for automation objects. +-- Use full_tag_reference for read/write operations against the runtime. +-- Join with hierarchy.sql results on gobject_id to place attributes in the OPC UA browse tree. +-- +-- Two sources of attributes: +-- 1. attribute_definition (via primitive_instance) — system/primitive attributes +-- Derived from internal_runtime_attributes view logic. +-- 2. dynamic_attribute — user-defined attributes (e.g., MachineID, MoveInFlag) +-- Defined on templates, inherited by instances via derived_from_gobject_id chain. +-- Requires recursive CTE to walk the template derivation hierarchy. +-- +-- Attribute category filter (mx_attribute_category): +-- 2-11, 24 = runtime readable attributes +-- +-- Attribute names starting with '_' are internal/hidden and excluded. +-- dynamic_attribute '.Description' suffixed entries are metadata, excluded. +-- +-- Array dimensions are extracted from the mx_value hex string (bytes 5-6, little-endian +-- uint16 at hex positions 13-16). Works for both attribute_definition and dynamic_attribute. +-- +-- Data types (mx_data_type): +-- 1 = Boolean, 2 = Integer (Int32), 3 = Float (Single), 4 = Double, +-- 5 = String, 6 = Time (DateTime), 7 = ElapsedTime (TimeSpan), +-- 8 = (reference), 13 = (enumeration), 14 = (custom), 15 = InternationalizedString, 16 = (custom) + +;WITH template_chain AS ( + -- Start from each non-template instance + SELECT g.gobject_id, g.derived_from_gobject_id, 0 AS depth + FROM gobject g + WHERE g.is_template = 0 + UNION ALL + -- Walk up the template derivation chain + SELECT tc.gobject_id, t.derived_from_gobject_id, tc.depth + 1 + FROM template_chain tc + INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id + WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10 +) +SELECT DISTINCT + gobject_id, + tag_name, + primitive_name, + attribute_name, + full_tag_reference, + mx_data_type, + data_type_name, + is_array, + array_dimension, + mx_attribute_category, + security_classification, + attribute_source +FROM ( + -- Part 1: System/primitive attributes (from attribute_definition) + SELECT + g.gobject_id, + g.tag_name, + pi.primitive_name, + ad.attribute_name, + CASE WHEN pi.primitive_name = '' + THEN g.tag_name + '.' + ad.attribute_name + ELSE g.tag_name + '.' + pi.primitive_name + '.' + ad.attribute_name + END + CASE WHEN ad.is_array = 1 THEN '[]' ELSE '' END + AS full_tag_reference, + ad.mx_data_type, + dt.description AS data_type_name, + ad.is_array, + CASE WHEN ad.is_array = 1 + THEN CONVERT(int, CONVERT(varbinary(2), + SUBSTRING(ad.mx_value, 15, 2) + SUBSTRING(ad.mx_value, 13, 2), 2)) + ELSE NULL + END AS array_dimension, + ad.mx_attribute_category, + ad.security_classification, + 'primitive' AS attribute_source + FROM gobject g + INNER JOIN instance i + ON i.gobject_id = g.gobject_id + INNER JOIN template_definition td + ON td.template_definition_id = g.template_definition_id + AND td.runtime_clsid <> '{00000000-0000-0000-0000-000000000000}' + INNER JOIN package p + ON p.package_id = g.checked_in_package_id + INNER JOIN primitive_instance pi + ON pi.package_id = p.package_id + AND pi.property_bitmask & 0x10 <> 0x10 + INNER JOIN attribute_definition ad + ON ad.primitive_definition_id = pi.primitive_definition_id + AND ad.attribute_name NOT LIKE '[_]%' + AND ad.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) + LEFT JOIN data_type dt + ON dt.mx_data_type = ad.mx_data_type + WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND g.is_template = 0 + AND g.deployed_package_id <> 0 + + UNION ALL + + -- Part 2: User-defined attributes (from dynamic_attribute via template chain) + SELECT + g.gobject_id, + g.tag_name, + '' AS primitive_name, + da.attribute_name, + g.tag_name + '.' + da.attribute_name + + CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END + AS full_tag_reference, + da.mx_data_type, + dt.description AS data_type_name, + da.is_array, + CASE WHEN da.is_array = 1 + THEN CONVERT(int, CONVERT(varbinary(2), + SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2)) + ELSE NULL + END AS array_dimension, + da.mx_attribute_category, + da.security_classification, + 'dynamic' AS attribute_source + FROM template_chain tc + INNER JOIN dynamic_attribute da + ON da.gobject_id = tc.derived_from_gobject_id + INNER JOIN gobject g + ON g.gobject_id = tc.gobject_id + INNER JOIN template_definition td + ON td.template_definition_id = g.template_definition_id + LEFT JOIN data_type dt + ON dt.mx_data_type = da.mx_data_type + WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND g.is_template = 0 + AND g.deployed_package_id <> 0 + AND da.attribute_name NOT LIKE '[_]%' + AND da.attribute_name NOT LIKE '%.Description' + AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) +) all_attributes +ORDER BY tag_name, primitive_name, attribute_name; diff --git a/gr/queries/change_detection.sql b/gr/queries/change_detection.sql new file mode 100644 index 0000000..d6d3a41 --- /dev/null +++ b/gr/queries/change_detection.sql @@ -0,0 +1,8 @@ +-- Galaxy Change Detection for OPC UA Server +-- Poll this query on an interval to detect when the hierarchy needs refreshing. +-- When time_of_last_deploy changes, a deployment has occurred and the OPC UA +-- server should re-run hierarchy.sql and attributes.sql to pick up changes +-- (new/removed objects, undeployed objects, etc.). + +SELECT time_of_last_deploy +FROM galaxy; diff --git a/gr/queries/hierarchy.sql b/gr/queries/hierarchy.sql new file mode 100644 index 0000000..55ee35d --- /dev/null +++ b/gr/queries/hierarchy.sql @@ -0,0 +1,43 @@ +-- Galaxy Object Hierarchy for OPC UA Server +-- Returns the parent-child hierarchy with both browse names (contained_name) and tag names. +-- Use browse_name for OPC UA browse tree display. +-- Use tag_name for runtime read/write tag references. +-- +-- Derived from internal_automation_object_model_hierarchy_view2 logic: +-- - gobject: provides hierarchy (contained_by_gobject_id, area_gobject_id) and names +-- - template_definition: category_id filters to automation-relevant objects +-- +-- Category IDs: +-- 1 = $WinPlatform (platform objects) +-- 3 = $AppEngine (application engines) +-- 4 = $InTouchViewApp (InTouch view apps) +-- 10 = $UserDefined (user-defined automation objects) +-- 11 = $FieldReference (field reference objects) +-- 13 = $Area (areas / folders) +-- 17 = $DIObject (DI objects) +-- 24 = $DDESuiteLinkClient +-- 26 = $OPCClient + +SELECT DISTINCT + g.gobject_id, + g.tag_name, + g.contained_name, + CASE WHEN g.contained_name IS NULL OR g.contained_name = '' + THEN g.tag_name + ELSE g.contained_name + END AS browse_name, + CASE WHEN g.contained_by_gobject_id = 0 + THEN g.area_gobject_id + ELSE g.contained_by_gobject_id + END AS parent_gobject_id, + CASE WHEN td.category_id = 13 + THEN 1 + ELSE 0 + END AS is_area +FROM gobject g +INNER JOIN template_definition td + ON g.template_definition_id = td.template_definition_id +WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND g.is_template = 0 + AND g.deployed_package_id <> 0 +ORDER BY parent_gobject_id, g.tag_name; diff --git a/gr/schema.md b/gr/schema.md new file mode 100644 index 0000000..76e3efb --- /dev/null +++ b/gr/schema.md @@ -0,0 +1,2204 @@ +# Schema Reference + +Generated from the ZB (Galaxy Repository) database. + +## Tables + +### aa_sql_objects + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| object_name | nvarchar(128) | NO | PK | +| object_type | nvarchar(10) | NO | | + +### affected_overview_symbols + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | | +| package_id | int | NO | | +| mx_primitive_id | smallint | NO | | +| visual_element_id | int | NO | | + +### alarm_message_defaults + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| phrase_id | int | NO | PK | +| default_message | nvarchar(1024) | NO | | + +### alarm_message_timestamps + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| timestamp_of_populate | bigint | NO | Default: ((0)) | + +### alarm_message_translations + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| phrase_id | int | NO | PK | +| locale_id | smallint | NO | PK; FK -> supported_locales.locale_id | +| translated_message | nvarchar(1024) | NO | | + +### alarm_messages + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| phrase_id | int | NO | PK | + +### attribute_definition + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| attribute_definition_id | int | NO | | +| primitive_definition_id | int | NO | PK; FK -> primitive_definition.primitive_definition_id | +| attribute_name | nvarchar(329) | NO | | +| mx_attribute_id | smallint | NO | PK | +| has_config_set_handler | bit | NO | | +| mx_data_type | smallint | NO | | +| is_array | bit | NO | | +| security_classification | smallint | NO | | +| security_classification_needs_deployed | bit | NO | | +| mx_attribute_category | int | NO | | +| is_frequently_accessed | bit | NO | | +| is_locked | bit | NO | | +| is_locked_needs_deployed | bit | NO | | +| mx_value | text(2147483647) | NO | | +| mx_value_needs_deployed | bit | NO | | + +### attribute_reference + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| referring_mx_primitive_id | smallint | NO | PK; FK -> primitive_instance.package_id; Default: ((0)) | +| referring_mx_attribute_id | smallint | NO | PK; Default: ((0)) | +| element_index | smallint | NO | PK; Default: ((0)) | +| resolved_gobject_id | int | NO | Default: ((0)) | +| reference_string | nvarchar(700) | NO | Default: ('') | +| context_string | nvarchar(329) | NO | Default: ('') | +| object_signature | int | NO | Default: ((0)) | +| resolved_mx_primitive_id | smallint | NO | Default: ((0)) | +| resolved_mx_attribute_id | smallint | NO | Default: ((0)) | +| resolved_mx_property_id | smallint | NO | Default: ((0)) | +| attribute_signature | int | NO | Default: ((0)) | +| lock_type | int | NO | Default: ((0)) | +| is_valid | bit | NO | Default: ((0)) | +| attr_res_status | int | NO | Default: ((0)) | +| attribute_index | smallint | YES | Default: ((-1)) | + +### attributes_translation_table + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | YES | | +| attribute_name | nvarchar(329) | NO | | +| new_primitive_id | int | YES | | +| new_attribute_id | int | YES | | +| old_primitive_id | int | YES | | +| old_attribute_id | int | YES | | + +### autobind_device + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| dio_id | int | NO | PK; FK -> gobject.gobject_id | +| overridden_naming_rule_id | int | YES | PK | + +### autobind_device_category + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| category_id | smallint | NO | PK; FK -> lookup_category.category_id | +| rule_id | int | YES | PK; Default: ((0)) | + +### autobind_device_template + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| template_definition_id | int | NO | PK; FK -> template_definition.template_definition_id | +| rule_id | int | YES | PK | + +### autobind_device_topic + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| dio_id | int | NO | PK; FK -> autobind_device.dio_id | +| sg_mx_primitive_id | smallint | NO | PK; Default: ((0)) | +| overridden_naming_rule_id | int | YES | PK | +| default_xlate_rule_id | int | NO | Default: ((0)) | + +### autobind_naming_rule + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| rule_id | int | NO | PK | +| rule_name | nvarchar(329) | NO | | + +### autobind_naming_rule_spec + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| rule_id | int | NO | PK; FK -> autobind_naming_rule.rule_id | +| io_type | nchar(1) | NO | PK | +| rule_spec | nvarchar(512) | NO | | + +### autobind_translation_rule + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| xlate_rule_id | int | NO | PK | +| xlate_rule_name | nvarchar(329) | NO | | +| xlate_rule_gsub_str | nvarchar(1000) | YES | | +| xlate_rule_scope_global | bit | NO | Default: ((0)) | + +### autobound_attribute + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| dio_id | int | NO | PK | +| sg_mx_primitive_id | smallint | NO | PK; Default: ((0)) | +| gobject_id | int | NO | PK | +| mx_primitive_id | smallint | NO | PK | +| mx_attribute_id | smallint | NO | PK | +| element_index | smallint | NO | PK; Default: ((0)) | +| attr_alias | nvarchar(329) | YES | | +| xlate_rule_id | int | NO | PK; FK -> autobind_translation_rule.xlate_rule_id; Default: ((0)) | + +### client_control_class_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| file_id | int | YES | | +| class_name | nvarchar(1024) | NO | | + +### client_info + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| id | int | NO | | +| client_unique_identifier | nvarchar(4000) | NO | | +| client_name | nvarchar(64) | NO | | +| deployed_files_count | smallint | NO | | +| time_of_last_deployed_object_components | datetime | YES | Default: (getdate()) | +| timestamp_of_last_synchronized | bigint | NO | Default: ((0)) | + +### control_index + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| entity_id | int | NO | | +| gobject_id | int | NO | PK; FK -> gobject.gobject_id | +| control_id | nvarchar(329) | YES | | +| control_name | nvarchar(329) | NO | PK | +| control_description | nvarchar(2000) | YES | | +| properties | nvarchar(-1) | YES | | +| thumbnail | nvarchar(-1) | YES | | + +### ConversionQueue + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| id | int | YES | | +| Name | nvarchar(329) | YES | | +| IsCheckedOut | bit | NO | | +| Status | bit | NO | Default: ((0)) | +| MetaData | nchar(256) | YES | | +| OperationType | nchar(20) | NO | | +| timestamp_of_last_change | bigint | YES | | +| change_type | int | YES | | + +### CurrentSessionContainedName + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| Uniqeid | int | NO | PK | +| obj_id | int | YES | | +| containedname | nvarchar(32) | YES | | + +### data_type + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| mx_data_type | tinyint | NO | PK | +| description | varchar(30) | NO | | +| ow_data_type | varchar(10) | YES | | + +### deleted_gobject + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | Default: ((0)) | +| timestamp_of_delete | timestamp | NO | PK | + +### deleted_ids + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| table_id | smallint | YES | | +| deleted_id | int | NO | | +| deletion_timestamp | timestamp | NO | | +| deletion_time | datetime | YES | | + +### deleted_visual_element + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| visual_element_name | nvarchar(329) | YES | | +| visual_element_type | nvarchar(32) | YES | | +| timestamp_of_delete | timestamp | NO | | + +### deleted_visual_element_version + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| mx_primitive_id | smallint | NO | | +| visual_element_name | nvarchar(329) | NO | | +| visual_element_type | nvarchar(32) | NO | | +| timestamp_of_delete | timestamp | NO | PK | +| visual_element_id | int | NO | | + +### deployed_file + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| deployed_file_id | int | NO | PK | +| file_id | int | NO | PK; FK -> file_table.file_id | +| node_name | nvarchar(256) | NO | | +| need_to_delete | int | NO | Default: ((0)) | +| is_package_deployed | bit | NO | | +| is_editor_deployed | bit | NO | | +| is_runtime_deployed | bit | NO | | +| is_browser_deployed | bit | NO | | +| file_version | nvarchar(50) | NO | Default: ('') | +| file_modified_time | nvarchar(50) | NO | Default: ('') | + +### deployed_intouch_viewapp + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| timestamp_of_deploy | bigint | NO | Default: ((1)) | +| gobject_id | int | NO | | +| deploy_file_transfering | bit | YES | Default: ((0)) | + +### deployed_intouch_viewapp_visual_element_dependency + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | YES | | +| visual_element_name | nvarchar(2000) | YES | | + +### dynamic_attribute + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| mx_attribute_id | smallint | NO | PK | +| attribute_name | nvarchar(329) | NO | | +| mx_data_type | smallint | NO | | +| is_array | bit | NO | | +| security_classification | smallint | NO | | +| mx_attribute_category | int | NO | | +| lock_type | int | NO | | +| mx_value | text(2147483647) | NO | | +| owned_by_gobject_id | int | NO | Default: ((0)) | +| original_lock_type | int | NO | Default: ((0)) | +| dynamic_attribute_type | smallint | NO | Default: ((0)) | +| bitvalues | smallint | NO | Default: ((0)) | +| dynamic_attribute_id | bigint | NO | | + +### external_content_media_types + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| entity_id | int | NO | PK | +| media_type | nvarchar(255) | NO | | +| control_entity_id | int | NO | | +| uri_property_name | nvarchar(1023) | YES | | +| media_type_property_name | nvarchar(1023) | YES | | +| is_default | bit | YES | | + +### feature + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| feature_id | int | NO | PK | +| feature_name | nvarchar(256) | NO | | +| feature_type | nvarchar(256) | NO | | + +### feature_file_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| feature_id | int | NO | PK | +| file_id | int | NO | PK; FK -> file_table.file_id | + +### file_browserinfo_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| primitive_definition_id | int | NO | PK; FK -> primitive_definition.primitive_definition_id | +| file_id | int | NO | PK | +| assembly_strong_name | nvarchar(512) | NO | | +| assembly_type_name | nvarchar(256) | NO | | + +### file_pending_update + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| file_id | int | NO | PK; FK -> file_table.file_id | +| node_name | nvarchar(256) | NO | | + +### file_primitive_definition_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| primitive_definition_id | int | NO | PK; FK -> primitive_definition.primitive_definition_id | +| file_id | int | NO | PK | +| is_needed_for_package | bit | NO | Default: ((0)) | +| is_needed_for_runtime | bit | NO | Default: ((0)) | +| is_needed_for_editor | bit | NO | Default: ((0)) | +| is_needed_for_browser | bit | NO | Default: ((0)) | + +### file_table + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| file_id | int | NO | PK | +| file_name | nvarchar(256) | NO | | +| vendor_name | nvarchar(256) | NO | | +| registration_type | int | NO | | +| subfolder | nvarchar(256) | NO | Default: ('') | +| file_version | nvarchar(50) | NO | Default: ('') | +| file_modified_time | nvarchar(50) | NO | Default: ('') | + +### folder + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| folder_id | int | NO | PK | +| folder_type | smallint | NO | | +| folder_name | nvarchar(64) | NO | | +| parent_folder_id | int | NO | | +| depth | int | NO | | +| has_objects | bit | NO | | +| has_folders | bit | NO | | +| timestamp_of_last_change | timestamp | NO | | + +### folder_gobject_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| folder_id | int | NO | PK | +| folder_type | smallint | NO | | +| gobject_id | int | NO | PK; FK -> gobject.gobject_id | +| timestamp_of_last_change | timestamp | NO | | + +### galaxy + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| time_of_last_deploy | datetime | YES | Default: (getdate()) | +| time_of_last_config_change | datetime | YES | Default: (getdate()) | +| is_galaxy_installed | bit | NO | Default: ((1)) | +| time_of_last_reference_binding | datetime | YES | Default: (getdate()) | +| timestamp_of_last_cascade | bigint | NO | Default: ((1)) | +| timestamp_of_last_visual_element_reference_bind | bigint | NO | Default: ((0)) | +| max_proxy_timestamp | bigint | NO | Default: (CONVERT([bigint],@@dbts)) | +| max_visual_element_timestamp | bigint | NO | Default: (CONVERT([bigint],@@dbts)) | +| is_migration_in_progress | bit | NO | Default: ((0)) | +| time_of_last_association_change | datetime | YES | Default: (getdate()) | +| subscription_id | uniqueidentifier | YES | | +| batch_id | uniqueidentifier | YES | | +| iteration_id | int | NO | Default: ((0)) | + +### galaxy_data + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| data_type | nvarchar(256) | NO | | +| data | image(2147483647) | YES | | + +### galaxy_settings + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| galaxyid | int | YES | | +| default_qs_data | ntext(1073741823) | NO | | +| current_qs_data | ntext(1073741823) | NO | | + +### gobject + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| template_definition_id | int | NO | PK; FK -> template_definition.template_definition_id | +| derived_from_gobject_id | int | NO | Default: ((0)) | +| contained_by_gobject_id | int | NO | Default: ((0)) | +| area_gobject_id | int | NO | Default: ((0)) | +| hosted_by_gobject_id | int | NO | Default: ((0)) | +| checked_out_by_user_guid | uniqueidentifier | YES | | +| default_symbol_gobject_id | int | NO | Default: ((0)) | +| default_display_gobject_id | int | NO | Default: ((0)) | +| checked_in_package_id | int | NO | Default: ((0)) | +| checked_out_package_id | int | NO | Default: ((0)) | +| deployed_package_id | int | NO | Default: ((0)) | +| last_deployed_package_id | int | NO | Default: ((0)) | +| tag_name | nvarchar(329) | NO | | +| contained_name | nvarchar(32) | NO | Default: ('') | +| identity_guid | uniqueidentifier | NO | Default: (newid()) | +| configuration_guid | uniqueidentifier | NO | | +| configuration_version | int | NO | | +| deployed_version | int | NO | Default: ((0)) | +| is_template | bit | NO | Default: ((0)) | +| is_hidden | bit | NO | Default: ((0)) | +| software_upgrade_needed | bit | NO | Default: ((0)) | +| hosting_tree_level | smallint | NO | Default: ((0)) | +| hierarchical_name | nvarchar(329) | NO | Default: ('') | +| namespace_id | smallint | NO | PK; Default: ((1)) | +| deployment_pending_status | bit | NO | Default: ((0)) | + +### gobject_asset_order + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK; FK -> gobject.gobject_id | +| relative_index | float(53,) | NO | | + +### gobject_change_log + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_change_log_id | int | NO | | +| gobject_id | int | NO | PK | +| change_date | datetime | YES | | +| operation_id | smallint | NO | PK; FK -> lookup_operation.operation_id | +| user_comment | nvarchar(1024) | NO | Default: ('') | +| configuration_version | int | NO | Default: ((0)) | +| user_profile_name | nvarchar(256) | NO | | + +### gobject_filter_info_timestamp + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | YES | PK; FK -> gobject.gobject_id | +| timestamp_of_last_change | timestamp | NO | PK | + +### gobject_friendly_name + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK; FK -> gobject.gobject_id | +| friendly_name | nvarchar(1024) | NO | Default: ('') | + +### gobject_log_details + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | | +| tag_name | nvarchar(329) | NO | | + +### gobject_protected + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | | + +### ImportTransaction + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ImportOperationId | nvarchar(329) | YES | | +| Status | bit | NO | Default: ((1)) | + +### instance + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK; FK -> gobject.gobject_id | +| mx_platform_id | smallint | NO | Default: ((0)) | +| mx_engine_id | smallint | NO | Default: ((0)) | +| mx_object_id | smallint | NO | Default: ((0)) | + +### intouchviewapptemplate_allsymbols + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | | + +### lookup_category + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| category_id | smallint | NO | PK | +| category_name | nvarchar(50) | NO | | + +### lookup_folder + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| folder_type | smallint | NO | | +| folder_type_name | nvarchar(32) | YES | | + +### lookup_operation + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| operation_id | smallint | NO | PK | +| operation_code | nvarchar(50) | NO | | +| operation_name | nvarchar(256) | NO | | + +### lookup_package_op_status + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| status_id | int | NO | PK | +| status_name | nvarchar(50) | NO | | + +### lookup_status + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| status_id | int | NO | PK | +| status_name | nvarchar(50) | NO | | + +### lookup_table_name + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| table_id | smallint | NO | | +| table_name | nvarchar(250) | YES | | + +### namespace + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| namespace_id | smallint | NO | PK | +| namespace_name | nvarchar(32) | YES | | + +### object_device_linkage + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| dio_id | int | NO | | +| sg_mx_primitive_id | smallint | NO | | + +### object_wizard_overview_symbols + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | | +| visual_element_id | int | NO | | +| change_type | int | NO | | +| mx_primitive_id | int | YES | | + +### object_wizard_symbol_override + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> package.package_id | +| symbol_overrides | image(2147483647) | YES | | + +### object_wizard_symbol_override_mapping + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| has_local_overrides | bit | NO | | +| thumbnail | image(2147483647) | YES | | +| preview | image(2147483647) | YES | | + +### old_checked_in_packages + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| package_id | int | YES | | +| gobject_id | int | YES | | +| is_template | bit | YES | | +| is_being_referenced | bit | YES | Default: ((0)) | + +### operation + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| operation_id | int | NO | PK | +| user_profile_id | int | NO | PK; FK -> user_profile.user_profile_id | +| operation_name | nvarchar(300) | NO | | +| start_time | datetime | NO | Default: (getdate()) | + +### operation_message + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| message_id | int | NO | PK | +| operation_id | int | NO | PK; FK -> operation.operation_id | +| message_text | nvarchar(300) | NO | | +| message_time | datetime | NO | Default: (getdate()) | + +### operation_status + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| operation_id | int | NO | PK; FK -> operation.operation_id | +| status | int | NO | PK | + +### operation_status_look_up + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| status | int | NO | PK | +| status_name | varchar(100) | NO | | + +### ow_group_def + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| ow_group_id | int | NO | PK; FK -> ow_group_id.ow_group_id | +| prompt | nvarchar(260) | NO | | +| GUID | varchar(36) | NO | | +| description | nvarchar(260) | YES | | +| visibility_rules | nvarchar(-1) | YES | | + +### ow_group_id + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ow_group_id | int | NO | PK | + +### ow_group_override + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| ow_group_id | int | NO | PK; FK -> ow_group_id.ow_group_id | +| property_bitmask | int | NO | | + +### ow_instance_setting + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| name | nvarchar(329) | NO | PK | +| ow_setting_type | int | NO | PK; FK -> ow_lu_setting.ow_setting_type | +| is_dmv | bit | NO | PK | +| override_value | nvarchar(-1) | YES | | +| is_mx | bit | NO | | +| is_implicit | bit | YES | | + +### ow_link_def + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| ow_link_id | int | NO | PK; FK -> ow_link_id.ow_link_id | +| ow_link_type | int | NO | | +| name | nvarchar(329) | NO | | +| mx_primitive_id | smallint | YES | | +| dynamic_attribute_id | bigint | YES | | +| optional_id | int | YES | | +| property_bitmask | int | NO | | +| sort | int | NO | | + +### ow_link_id + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ow_link_id | int | NO | PK | +| ow_opt_or_choice_id | int | NO | PK; FK -> ow_opt_or_choice_id.ow_opt_or_choice_id | + +### ow_lu_definition + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ow_setting_type | int | NO | PK; FK -> ow_lu_setting.ow_setting_type | +| primitive_definition_id | int | NO | PK | +| template_definition_id | int | NO | | +| boolean_analog | bit | NO | | +| ext | varchar(20) | NO | | +| link_setting | int | YES | | +| link_feature | int | YES | | +| mx_attribute_id | smallint | YES | | + +### ow_lu_setting + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ow_setting_type | int | NO | PK | +| category | varchar(10) | NO | | +| mx_data_type | tinyint | YES | PK; FK -> data_type.mx_data_type | +| setting_name | nvarchar(30) | NO | | +| feature_id | int | YES | | +| feature_sub_id | int | YES | | +| parent_type | int | YES | | +| no_ext | bit | YES | | +| raw_value | nvarchar(100) | YES | | + +### ow_opt_or_choice_def + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| ow_opt_or_choice_id | int | NO | PK; FK -> ow_opt_or_choice_id.ow_opt_or_choice_id | +| prompt | nvarchar(260) | NO | | +| GUID | varchar(36) | NO | | +| name | varchar(10) | NO | | +| description | nvarchar(260) | YES | | +| choice_sequence_number | int | YES | | +| after_group_or_option | int | YES | | +| initial_value | bit | NO | Default: ((0)) | +| visibility_rules | nvarchar(-1) | YES | | +| sort | int | NO | | + +### ow_opt_or_choice_id + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ow_opt_or_choice_id | int | NO | PK | +| ow_group_id | int | NO | PK; FK -> ow_group_id.ow_group_id | + +### ow_opt_or_choice_override + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| ow_opt_or_choice_id | int | NO | PK; FK -> ow_opt_or_choice_id.ow_opt_or_choice_id | +| property_bitmask | int | YES | | +| override_value | bit | YES | | + +### ow_setting_def + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| ow_setting_id | int | NO | PK; FK -> ow_setting_id.ow_setting_id | +| ow_setting_type | int | NO | PK | +| reference | nvarchar(260) | YES | | +| property_id | varchar(36) | YES | | +| initial_value | nvarchar(-1) | YES | | +| property_bitmask | int | YES | | +| sort | int | NO | | + +### ow_setting_id + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ow_setting_id | int | NO | PK | +| ow_link_id | int | NO | PK; FK -> ow_link_id.ow_link_id | + +### ow_setting_override + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| ow_setting_id | int | NO | PK; FK -> ow_setting_id.ow_setting_id | +| override_value | nvarchar(-1) | YES | | +| property_bitmask | int | YES | | + +### ow_symbol_setting + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| sym_name | nvarchar(33) | NO | PK | +| ow_setting_type | int | NO | PK; FK -> ow_lu_setting.ow_setting_type | +| reference | nvarchar(260) | NO | PK | +| property_id | nvarchar(36) | YES | | +| value | nvarchar(-1) | YES | | +| property_bitmask | int | YES | | + +### owned_visual_element + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> visual_element_version.package_id | +| mx_primitive_id | smallint | NO | PK | +| visual_element_id | int | NO | | +| thumbnail | image(2147483647) | YES | | +| description | nvarchar(1024) | YES | | +| visual_element_definition | image(2147483647) | NO | | +| is_thumbnail_dirty | bit | NO | Default: ((0)) | +| visual_element_definition_grm | image(2147483647) | NO | | +| content_type | nvarchar(1024) | YES | | +| visual_element_crossRef | nvarchar(-1) | YES | | +| preview | image(2147483647) | YES | | + +### package + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| status_id | int | NO | PK; FK -> lookup_status.status_id; Default: ((0)) | +| reference_status_id | int | NO | Default: ((0)) | +| instance_attributes | image(2147483647) | YES | | +| operation_status | int | NO | Default: ((0)) | +| security_group | nvarchar(32) | NO | Default: ('Default') | +| derived_from_package_id | int | NO | Default: ((0)) | +| deployable_configuration_version | int | NO | Default: ((0)) | +| package_type | nvarchar(5) | NO | Default: ('I') | +| package_version | smallint | NO | Default: ((0)) | +| object_status | smallint | NO | Default: ((0)) | + +### packages_to_be_deleted + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | + +### platform + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| platform_id | int | NO | | +| platform_gobject_id | int | NO | PK; FK -> gobject.gobject_id | +| node_name | nvarchar(256) | NO | Default: ('') | +| last_deployed_node_name | nvarchar(256) | NO | Default: ('') | +| rmcNode_name | nvarchar(256) | NO | Default: ('') | +| last_deployed_rmcNode_name | nvarchar(256) | NO | Default: ('') | +| portNMX | int | NO | Default: ((0)) | +| last_deployed_portNMX | int | NO | Default: ((0)) | +| portRMC | int | NO | Default: ((0)) | +| last_deployed_portRMC | int | NO | Default: ((0)) | +| portRPC | int | NO | Default: ((0)) | +| last_deployed_portRPC | int | NO | Default: ((0)) | + +### platform_license + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK; FK -> gobject.gobject_id | +| license_type | int | NO | | + +### primitive_attribute_validation_results + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| mx_attribute_id | smallint | NO | PK | +| validationCode | nvarchar(329) | NO | PK | +| validationState | smallint | NO | | + +### primitive_data_type + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| mx_data_type | tinyint | NO | PK | +| primitive_definition_id | int | NO | PK; FK -> primitive_definition.primitive_definition_id | + +### primitive_definition + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| primitive_definition_id | int | NO | PK | +| template_definition_id | int | NO | PK; FK -> template_definition.template_definition_id | +| parent_mx_primitive_id | smallint | NO | | +| mx_primitive_id | smallint | NO | Default: ((0)) | +| primitive_name | nvarchar(329) | NO | Default: ('') | +| execution_group | smallint | NO | | +| is_virtual | bit | NO | | +| primitive_guid | uniqueidentifier | NO | | +| runtime_handler_clsid | uniqueidentifier | YES | | +| package_handler_clsid | uniqueidentifier | YES | | +| supports_dynamic_attributes | bit | NO | | +| major_version | int | NO | | + +### primitive_instance + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> package.package_id | +| mx_primitive_id | smallint | NO | PK | +| primitive_definition_id | int | NO | | +| primitive_name | nvarchar(329) | NO | | +| parent_mx_primitive_id | smallint | NO | | +| execution_group | int | NO | | +| execution_order | int | NO | Default: ((-1)) | +| owned_by_gobject_id | int | NO | Default: ((0)) | +| timestamp_of_last_change | bigint | YES | Default: ((0)) | +| max_child_timestamp | bigint | YES | Default: ((0)) | +| extension_type | nvarchar(329) | YES | | +| is_object_extension | bit | YES | Default: ((0)) | +| checked_in_primitive_version | int | NO | Default: ((1)) | +| checked_out_primitive_version | int | NO | Default: ((1)) | +| entity_change_type | int | NO | Default: ((1)) | +| operation_on_primitive_mask | int | NO | Default: ((0)) | +| created_by_parent | smallint | NO | Default: ((0)) | +| status_id | smallint | NO | Default: ((0)) | +| ref_status_id | smallint | NO | Default: ((0)) | +| primitive_attributes | image(2147483647) | YES | | +| mx_value_errors | text(2147483647) | NO | | +| mx_value_warnings | text(2147483647) | NO | | +| mx_value_reference_warnings | text(2147483647) | NO | | +| property_bitmask | smallint | NO | Default: ((0)) | + +### primitive_instance_feature_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| feature_id | int | NO | PK | +| feature_name | nvarchar(256) | NO | Default: ('') | +| feature_type | nvarchar(256) | NO | Default: ('') | + +### primitive_instance_file_table_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| file_id | int | NO | PK | +| is_needed_for_package | bit | NO | Default: ((0)) | +| is_needed_for_runtime | bit | NO | Default: ((0)) | +| is_needed_for_editor | bit | NO | Default: ((0)) | + +### proxy_timestamp + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| timestamp_of_last_change | timestamp | NO | | +| ImportGUID | nvarchar(265) | YES | | + +### redundancy + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| primary_gobject_id | int | NO | PK | +| backup_gobject_id | int | NO | PK | + +### renamed_visual_element + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| mx_primitive_id | smallint | NO | | +| visual_element_id | int | NO | | +| old_visual_element_name | nvarchar(329) | NO | | +| new_visual_element_name | nvarchar(329) | NO | | +| visual_element_type | nvarchar(32) | NO | | +| timestamp_of_rename | timestamp | NO | PK | + +### schema_version + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| version_number | int | NO | | +| version_string | nvarchar(64) | NO | | +| cdi_version | nvarchar(64) | NO | | +| old_version_number | int | NO | Default: ((0)) | + +### supported_locales + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| locale_id | smallint | NO | PK | + +### template + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| toolset_id | int | NO | PK; FK -> toolset.toolset_id | + +### template_attribute + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| mx_attribute_id | smallint | NO | PK | +| security_classification | int | NO | | +| mx_data_type | smallint | NO | | +| mx_value | text(2147483647) | NO | | +| lock_type | int | NO | | +| original_lock_type | int | NO | Default: ((0)) | +| bit_values | smallint | NO | Default: ((0)) | +| raw_value | nvarchar(1024) | YES | Default: (NULL) | + +### template_definition + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| template_definition_id | int | NO | PK | +| base_gobject_id | int | NO | Default: ((0)) | +| original_template_tagname | nvarchar(32) | NO | Default: ('') | +| required_features | nvarchar(256) | YES | | +| supported_features | nvarchar(256) | YES | | +| category_id | smallint | NO | | +| category_clsid | uniqueidentifier | YES | | +| runtime_clsid | uniqueidentifier | YES | | +| base_template_location | nchar(260) | NO | Default: ('') | +| vendor_name | nvarchar(256) | YES | | +| major_version | smallint | YES | | +| codebase | nvarchar(322) | YES | | +| codebase_minor_version | int | NO | Default: ((0)) | +| event_mask | int | NO | Default: ((0)) | + +### template_idebehavior_link + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| template_definition_id | int | NO | PK; FK -> template_definition.template_definition_id | +| shown_in_template_toolbox | bit | NO | Default: ((1)) | +| shown_in_model_view | bit | NO | Default: ((1)) | +| shown_in_deployment_view | bit | NO | Default: ((1)) | +| shown_in_security_editor | bit | NO | Default: ((1)) | +| shown_tagname | bit | NO | Default: ((1)) | +| shown_containedname | bit | NO | Default: ((1)) | +| shown_in_archestra_browser | bit | NO | Default: ((1)) | +| shown_standard_archestra_editor_tab | bit | NO | Default: ((1)) | +| enable_objectviewermenu | bit | NO | Default: ((1)) | +| enable_createinstance | bit | NO | Default: ((1)) | +| enable_createtemplate | bit | NO | Default: ((1)) | +| idebehavior_flags | int | NO | Default: ((511)) | + +### template_migration_policy + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| migrate_to_codebase | nvarchar(322) | NO | | +| migrate_from_codebase | nvarchar(322) | NO | | + +### timestamp_record + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| ts | timestamp | NO | | + +### toolset + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| toolset_id | int | NO | PK | +| toolset_name | nvarchar(64) | NO | | + +### user_preferences + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| user_preferences_id | int | NO | PK | +| user_profile_id | int | NO | PK; FK -> user_profile.user_profile_id | +| preference_type | nvarchar(256) | NO | | +| preferences | image(2147483647) | YES | | + +### user_profile + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| user_profile_id | int | NO | PK | +| user_profile_name | nvarchar(256) | NO | | +| user_guid | uniqueidentifier | NO | Default: (newid()) | +| password_hash | int | YES | | +| default_platform_tag_name | nvarchar(32) | YES | | +| default_app_engine_tag_name | nvarchar(32) | YES | | +| default_view_engine_tag_name | nvarchar(32) | YES | | +| default_history_engine_tag_name | nvarchar(32) | YES | | +| default_area_tag_name | nvarchar(32) | YES | | +| default_security_group | nvarchar(32) | NO | Default: ('Default') | +| ide_preferences | ntext(1073741823) | YES | | +| roles | ntext(1073741823) | YES | | +| user_full_name | nvarchar(256) | YES | | +| intouch_access_level | int | YES | Default: ((0)) | +| user_version_id | int | NO | Default: ((1)) | +| crypto_secure_hashed_pwd | ntext(1073741823) | YES | | + +### visual_element + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| visual_element_id | int | NO | PK | +| visual_element_type | nvarchar(32) | YES | | +| inheritance_status | nchar(1) | YES | | +| visual_element_category | nchar(1) | YES | | + +### visual_element_affected_by_undo_check_out + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| visual_element_name | nvarchar(329) | YES | | +| visual_element_id | int | YES | | +| visual_element_type | nvarchar(32) | YES | | +| change_type | int | YES | | +| check_out_undone_by_user_guid | uniqueidentifier | NO | | +| timestamp_of_last_change | timestamp | NO | | + +### visual_element_archive + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | | +| package_id | int | NO | | +| mx_primitive_id | smallint | NO | | +| visual_element_name | nvarchar(329) | NO | | +| visual_element_type | nvarchar(32) | NO | | +| visual_element_id | int | NO | | +| persisted_for_gobject_id | int | NO | | + +### visual_element_id + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| visual_element_id | int | NO | | + +### visual_element_reference + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK; FK -> primitive_instance.package_id | +| mx_primitive_id | smallint | NO | PK | +| visual_element_reference_index | int | NO | PK | +| visual_element_bind_status | int | NO | | +| is_relative_reference | char(1) | NO | | +| checked_in_bound_visual_element_gobject_id | int | YES | | +| checked_in_bound_visual_element_package_id | int | YES | | +| checked_in_bound_visual_element_mx_primitive_id | smallint | YES | | +| checked_out_bound_visual_element_gobject_id | int | YES | | +| checked_out_bound_visual_element_package_id | int | YES | | +| checked_out_bound_visual_element_mx_primitive_id | smallint | YES | | +| unbound_timestamp | timestamp | NO | | +| checked_in_unbound_visual_element_name | nvarchar(362) | YES | | +| checked_in_unbound_visual_element_type | nvarchar(32) | YES | | +| checked_in_unbound_tag_name | nvarchar(329) | YES | | +| checked_in_unbound_primitive_name | nvarchar(329) | YES | | +| checked_in_unbound_relative_object_name | nvarchar(329) | YES | | +| checked_in_unbound_visual_element_id | int | YES | | +| checked_out_unbound_visual_element_name | nvarchar(362) | YES | | +| checked_out_unbound_visual_element_type | nvarchar(32) | YES | | +| checked_out_unbound_tag_name | nvarchar(329) | YES | | +| checked_out_unbound_primitive_name | nvarchar(329) | YES | | +| checked_out_unbound_relative_object_name | nvarchar(329) | YES | | +| checked_out_unbound_visual_element_id | int | YES | | +| checked_out_visual_element_package_id | int | YES | | +| checked_out_visual_element_gobject_id | int | YES | | +| checked_out_to_user_guid | uniqueidentifier | YES | | + +### visual_element_timestamp + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | | +| package_id | int | NO | | +| mx_primitive_id | smallint | NO | | +| visual_element_id | int | NO | | +| change_type | int | NO | | +| timestamp_of_last_change | timestamp | NO | | + +### visual_element_version + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| gobject_id | int | NO | PK | +| package_id | int | NO | PK | +| mx_primitive_id | smallint | NO | PK | +| visual_element_id | int | NO | PK; FK -> visual_element.visual_element_id | +| inherited_from_gobject_id | int | NO | PK | +| inherited_from_package_id | int | NO | PK | +| inherited_from_mx_primitive_id | smallint | NO | PK | +| inherited_from_visual_element_id | int | YES | | + +### well_known_client_controls + +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| file_id | int | YES | | +| class_name | nvarchar(1024) | NO | | +| toolbox_info | nvarchar(-1) | YES | | + +## Views + +### internal_all_alarms_view + +| Column | Type | Nullable | +|--------|------|----------| +| name | nvarchar(663) | NO | +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | + +### internal_all_view_app_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| category_id | smallint | NO | + +### internal_automation_object_model_hierarchy_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| folderid | int | NO | +| is_area | int | NO | + +### internal_automation_object_model_hierarchy_view2 + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| folderid | int | NO | +| is_area | int | NO | + +### internal_automation_object_visual_element_hierarchy_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| folderId | int | NO | +| visual_element_name | nvarchar(362) | YES | +| visual_element_definition_id | int | NO | +| is_area | int | NO | +| defining_visual_element_name | nvarchar(362) | NO | +| category_id | smallint | NO | +| is_template | bit | NO | +| relative_asset_order | float(53,NULL) | NO | +| friendly_name | nvarchar(1024) | NO | +| property_bitmask | smallint | YES | + +### internal_automation_object_visual_element_hierarchy_view_includetemplates + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| folderId | int | NO | +| visual_element_name | nvarchar(362) | YES | +| visual_element_definition_id | int | NO | +| is_area | int | NO | +| defining_visual_element_name | nvarchar(362) | NO | +| category_id | smallint | NO | +| is_template | bit | NO | +| relative_asset_order | float(53,NULL) | NO | +| friendly_name | nvarchar(1024) | NO | +| property_bitmask | smallint | YES | + +### internal_checked_in_unbound_relative_visual_element_reference_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| visual_element_reference_index | int | NO | +| checked_in_unbound_visual_element_type | nvarchar(32) | YES | +| checked_in_unbound_hierarchical_visual_element_name | nvarchar(989) | YES | + +### internal_checked_out_unbound_relative_visual_element_reference_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| visual_element_reference_index | int | NO | +| checked_out_unbound_visual_element_type | nvarchar(32) | YES | +| checked_out_unbound_hierarchical_visual_element_name | nvarchar(989) | YES | + +### internal_common_obj + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| category_id | smallint | NO | +| toolset_name | nvarchar(64) | NO | +| is_template | bit | NO | +| status | int | NO | +| derived_from | int | NO | +| checkedout_by | uniqueidentifier | YES | +| base_type | int | NO | +| myhost | int | NO | +| required_feature_guid | nvarchar(256) | YES | +| supported_feature_guid | nvarchar(256) | YES | +| is_checkout | int | NO | +| is_deployed | int | NO | +| haspendingupdate | int | NO | +| checkedinpg | int | NO | +| checkedoutpg | int | NO | +| readpgid | int | NO | +| packagestate | int | NO | +| objectid | int | NO | +| mycontainer | int | NO | +| myarea | int | NO | +| hierarchical_name | nvarchar(329) | NO | +| is_hidden | bit | NO | +| myengine | int | NO | +| myplatform | int | NO | + +### internal_control_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| name | nvarchar(329) | NO | +| full_name | nvarchar(659) | NO | +| parent_id | int | NO | +| parent_name | nvarchar(329) | NO | +| control_id | nvarchar(329) | YES | +| description | nvarchar(2000) | YES | +| thumbnail | nvarchar(-1) | YES | +| properties | nvarchar(-1) | YES | +| relationship_id | int | NO | +| type | varchar(7) | NO | +| VisualElementType | varchar(7) | NO | +| path | int | NO | +| path_prefix | nvarchar(329) | NO | +| user_guid | int | YES | + +### internal_get_asset_graphics_preview_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| parent_id | int | NO | +| name | nvarchar(362) | YES | +| visual_element_id | int | NO | +| user_guid | uniqueidentifier | NO | + +### internal_get_attribute_definition_with_validation + +| Column | Type | Nullable | +|--------|------|----------| +| attribute_definition_id | int | NO | +| primitive_definition_id | int | NO | +| attribute_name | nvarchar(329) | NO | +| mx_attribute_id | smallint | NO | +| has_config_set_handler | bit | NO | +| mx_data_type | smallint | NO | +| is_array | bit | NO | +| security_classification | smallint | NO | +| security_classification_needs_deployed | bit | NO | +| mx_attribute_category | int | NO | +| is_frequently_accessed | bit | NO | +| is_locked | bit | NO | +| is_locked_needs_deployed | bit | NO | +| mx_value | text(2147483647) | NO | +| mx_value_needs_deployed | bit | NO | +| primitive_name | nvarchar(329) | NO | +| requires_validation | int | NO | +| requires_raw_copy | int | NO | + +### internal_get_device_scangroups_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | YES | +| name | nvarchar(329) | NO | +| parent_id | int | NO | + +### internal_get_external_content_media_types_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| media_type | nvarchar(255) | NO | +| parent_name | nvarchar(329) | NO | +| control_id | nvarchar(329) | YES | +| uri_property_name | nvarchar(1023) | YES | +| media_type_property_name | nvarchar(1023) | YES | +| is_default | bit | YES | +| user_guid | int | YES | + +### internal_get_GRMblob_preview_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| parent_id | int | NO | +| entity_id | int | NO | +| name | nvarchar(362) | YES | +| visualelementtype | nvarchar(32) | YES | +| user_guid | uniqueidentifier | NO | +| is_checkout | int | NO | +| visual_element_id | int | NO | +| definition_grm | varbinary(-1) | YES | +| definingvename | nvarchar(362) | YES | + +### internal_get_gtb_Allelements_view + +| Column | Type | Nullable | +|--------|------|----------| +| parent_id | int | YES | +| name | nvarchar(362) | YES | +| type | varchar(6) | NO | +| entity_id | int | YES | +| last_modified | datetime | YES | +| user_guid | uniqueidentifier | YES | +| is_checkout | int | YES | +| timestamp_of_last_change | timestamp | NO | +| thumbnail | varbinary(-1) | YES | +| definition | varbinary(-1) | YES | +| description | nvarchar(1024) | YES | +| status | int | YES | +| refStatus | int | YES | +| CheckedoutStatus | int | YES | +| CheckedoutbyUser | uniqueidentifier | YES | +| CheckoutByName | nvarchar(256) | YES | +| VisualElementType | nvarchar(32) | YES | +| hasObjects | bit | YES | +| hasFolders | bit | YES | +| is_hidden | int | NO | +| isprotected | int | NO | +| relationship_id | int | NO | +| path | int | YES | +| path_prefix | varchar(1) | NO | +| full_name | varchar(1) | NO | +| parent_name | varchar(1) | NO | +| control_id | varchar(1) | NO | +| content_type | nvarchar(1024) | YES | +| preview | varbinary(-1) | YES | + +### internal_get_gtb_elements_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| last_modified | datetime | YES | +| timestamp_of_last_change | timestamp | NO | +| user_guid | uniqueidentifier | NO | +| visual_element_name | nvarchar(362) | YES | +| thumbnail | image(2147483647) | YES | +| visual_element_definition | image(2147483647) | NO | +| folder_id | int | YES | +| visual_element_type | nvarchar(32) | YES | +| description | nvarchar(1024) | YES | +| is_checkout | int | NO | +| status | int | YES | +| refStatus | int | YES | +| checked_out_package_status | int | YES | +| checkedout_by | uniqueidentifier | YES | +| checkoutbyname | nvarchar(256) | YES | +| is_hidden | bit | NO | +| isprotected | int | NO | +| visual_element_id | int | NO | +| content_type | nvarchar(1024) | YES | +| preview | image(2147483647) | YES | + +### internal_get_gtb_folders_view + +| Column | Type | Nullable | +|--------|------|----------| +| folder_id | int | NO | +| folder_name | nvarchar(64) | NO | +| parent_folder_id | int | NO | +| depth | int | NO | +| has_objects | bit | NO | +| has_folders | bit | YES | +| timestamp_of_last_change | timestamp | NO | + +### internal_get_gtb_visual_element_references_deployment_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | nvarchar(694) | YES | +| gobject_id | int | NO | +| name | nvarchar(329) | NO | +| user_guid | uniqueidentifier | YES | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| referee_visual_element_id | int | YES | +| referee_visual_element_type | nvarchar(32) | YES | +| visual_element_bind_status | int | YES | +| visual_element_reference_index | int | NO | +| visual_element_id | int | YES | +| checked_in_unbound_visual_element_name | nvarchar(362) | YES | +| visual_element_type | nvarchar(32) | YES | +| primitive_name | nvarchar(329) | YES | +| tag_name | nvarchar(329) | YES | +| reference_string | nvarchar(694) | YES | +| calculated_visual_element_name | nvarchar(661) | YES | +| calculated_visual_element_type | nvarchar(32) | YES | +| package_type | nvarchar(5) | NO | +| is_relative_reference | char(1) | NO | + +### internal_get_gtb_visual_element_references_preview_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | varchar(21) | YES | +| name | nvarchar(329) | NO | +| visualelementtype | nvarchar(6) | NO | +| referee_visual_element_id | int | YES | +| referee_visual_element_type | nvarchar(32) | YES | +| visual_element_reference_index | int | NO | +| visual_element_id | int | YES | +| reference_string | nvarchar(694) | YES | + +### internal_get_gtb_visual_element_references_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | nvarchar(683) | YES | +| gobject_id | int | NO | +| name | nvarchar(329) | NO | +| user_guid | uniqueidentifier | YES | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| referee_visual_element_id | int | YES | +| referee_visual_element_type | nvarchar(32) | YES | +| visual_element_bind_status | int | YES | +| visual_element_reference_index | int | NO | +| visual_element_id | int | YES | +| checked_in_unbound_visual_element_name | nvarchar(362) | YES | +| visual_element_type | nvarchar(32) | YES | +| primitive_name | nvarchar(329) | YES | +| tag_name | nvarchar(329) | YES | +| reference_string | nvarchar(694) | YES | +| calculated_visual_element_name | nvarchar(661) | YES | +| calculated_visual_element_type | nvarchar(32) | YES | +| package_type | nvarchar(5) | NO | +| is_relative_reference | char(1) | NO | + +### internal_get_object_wizard_symbol_override_mapping_preview_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| owning_object_id | int | NO | +| package_id | int | NO | +| owning_object_name | nvarchar(329) | NO | +| visual_element_name | nvarchar(329) | NO | +| defining_object_id | int | NO | +| user_guid | uniqueidentifier | NO | + +### internal_get_object_wizard_symbol_override_mapping_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| owning_object_id | int | NO | +| owning_object_name | nvarchar(329) | NO | +| visual_element_name | nvarchar(329) | NO | +| defining_object_id | int | NO | + +### internal_get_object_wizard_symbol_override_preview_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| defining_object_name | nvarchar(329) | NO | +| package_id | int | NO | +| symbol_overrides | varbinary(-1) | YES | +| user_guid | uniqueidentifier | NO | + +### internal_get_object_wizard_symbol_override_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| defining_object_name | nvarchar(329) | NO | +| package_id | int | NO | +| symbol_overrides | varbinary(-1) | YES | + +### internal_get_TemplateToolBox_Allelements_view + +| Column | Type | Nullable | +|--------|------|----------| +| parent_id | int | YES | +| name | nvarchar(329) | NO | +| type | varchar(6) | NO | +| entity_id | int | YES | +| last_modified | datetime | YES | +| user_guid | uniqueidentifier | YES | +| is_checkout | int | YES | +| timestamp_of_last_change | bigint | YES | +| description | varchar(1) | YES | +| status | int | YES | +| refStatus | int | YES | +| CheckedoutStatus | int | YES | +| CheckedoutbyUser | uniqueidentifier | YES | +| CheckedoutbyUserName | nvarchar(256) | YES | +| hasObjects | bit | YES | +| hasFolders | bit | YES | +| is_hidden | int | NO | +| isprotected | int | NO | +| is_template | int | NO | +| namespace_id | int | NO | +| contained_name | nvarchar(32) | YES | +| hierarchical_name | nvarchar(329) | YES | +| derived_from_id | int | NO | +| base_type | int | NO | +| toolset_id | int | YES | +| checked_in_package_id | int | NO | +| template_definition_id | int | NO | +| container_id | int | NO | +| gObjectStatus | int | YES | +| category_id | int | NO | +| codebase | nvarchar(322) | YES | +| required_features | nvarchar(256) | YES | +| iswizard | int | NO | +| path | int | YES | +| path_prefix | varchar(1) | NO | + +### internal_get_TemplateToolBox_folders_view + +| Column | Type | Nullable | +|--------|------|----------| +| folder_id | int | NO | +| folder_name | nvarchar(64) | NO | +| parent_folder_id | int | NO | +| depth | int | NO | +| has_objects | bit | NO | +| has_folders | bit | NO | +| timestamp_of_last_change | timestamp | NO | + +### internal_get_visual_element_primitives_deployment_view + +| Column | Type | Nullable | +|--------|------|----------| +| parent_id | int | NO | +| entity_id | int | NO | +| package_id | int | NO | +| name | nvarchar(362) | YES | +| visualelementtype | nvarchar(32) | YES | +| owningobject | nvarchar(329) | NO | +| visual_element_id | int | NO | +| inherited_from_gobject_id | int | NO | +| inherited_from_visual_element_id | int | YES | +| definition | varbinary(-1) | YES | +| definition_grm | varbinary(-1) | YES | +| content_type | nvarchar(1024) | YES | +| definingvename | nvarchar(362) | YES | + +### internal_get_visual_element_primitives_view + +| Column | Type | Nullable | +|--------|------|----------| +| parent_id | int | NO | +| entity_id | int | NO | +| NAME | nvarchar(362) | YES | +| visualelementtype | nvarchar(32) | YES | +| type | varchar(4) | NO | +| thumbnail | image(2147483647) | YES | +| user_guid | uniqueidentifier | NO | +| is_checkout | int | NO | +| visual_element_id | int | NO | +| inherited_from_visual_element_id | int | YES | +| path | int | YES | +| path_prefix | nvarchar(329) | YES | +| definition | varbinary(-1) | YES | +| definition_grm | varbinary(-1) | YES | +| content_type | nvarchar(1024) | YES | +| preview | image(2147483647) | YES | +| owningobject | nvarchar(329) | NO | +| definingvename | nvarchar(362) | YES | + +### internal_gtb_symbols_hierarchy_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| name | nvarchar(329) | NO | +| folder_id | int | NO | + +### internal_linked_visual_element_definition_per_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| visual_element_id | int | NO | +| inherited_from_visual_element_id | int | YES | +| linked_visual_element_id | int | NO | +| name | nvarchar(362) | YES | +| linked_visual_element_package_id | int | NO | +| linked_visual_element_definition | image(2147483647) | NO | +| linked_visual_element_definition_grm | image(2147483647) | NO | +| linked_gobject_id | int | NO | +| user_guid | uniqueidentifier | NO | +| islinked | int | NO | +| thumbnail | image(2147483647) | YES | +| content_type | nvarchar(1024) | YES | +| preview | image(2147483647) | YES | +| property_bitmask | smallint | NO | + +### internal_list_automation_objects_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| name | nvarchar(329) | NO | +| type | varchar(6) | NO | +| description | varchar(1) | NO | +| user_guid | uniqueidentifier | NO | +| is_checkout | int | NO | +| last_modified | datetime | YES | +| contained_name | nvarchar(32) | NO | +| hierarchical_name | nvarchar(329) | NO | +| area_gobject_id | int | NO | +| is_template | bit | NO | +| is_hidden | bit | NO | +| status | int | NO | +| refStatus | int | NO | +| hosted_by_gobject_id | int | NO | +| derived_from_id | int | NO | +| base_type | int | NO | +| checkoutbyname | nvarchar(256) | NO | +| checkedout_by | uniqueidentifier | YES | +| toolset_id | int | YES | +| checked_in_package_id | int | NO | +| template_definition_id | int | NO | +| container_id | int | NO | +| area_id | int | NO | +| checked_out_package_status | int | NO | +| gObjectStatus | int | YES | +| category_id | smallint | NO | +| timestamp_of_last_change | bigint | YES | +| namespace_id | smallint | NO | +| folder_id | int | YES | +| codebase | nvarchar(322) | YES | +| required_features | nvarchar(256) | YES | +| isprotected | int | NO | +| iswizard | int | NO | +| isparentwizard | int | NO | + +### internal_list_model_objects_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| name | nvarchar(329) | NO | +| type | varchar(6) | NO | +| description | varchar(1) | NO | +| user_guid | int | YES | +| contained_name | nvarchar(32) | NO | +| hierarchical_name | nvarchar(329) | NO | +| area_gobject_id | int | NO | +| is_template | bit | NO | +| is_hidden | bit | NO | +| hosted_by_gobject_id | int | NO | +| derived_from_id | int | NO | +| base_type | int | NO | +| checkoutbyname | nvarchar(256) | NO | +| checkedout_by | uniqueidentifier | YES | +| checked_in_package_id | int | NO | +| template_definition_id | int | NO | +| container_id | int | NO | +| area_id | int | NO | +| gObjectStatus | int | YES | +| category_id | smallint | NO | +| namespace_id | smallint | NO | +| codebase | nvarchar(322) | YES | +| required_features | nvarchar(256) | YES | +| iswizard | int | NO | +| isparentwizard | int | NO | +| path | int | NO | +| path_prefix | nvarchar(250) | YES | +| relative_asset_order | float(53,NULL) | NO | +| friendly_name | nvarchar(1024) | NO | + +### internal_list_objects_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| contained_name | nvarchar(32) | NO | +| hierarchical_name | nvarchar(329) | NO | +| is_template | bit | NO | +| status | int | NO | +| refStatus | int | NO | +| hosted_by_gobject_id | int | NO | +| derived_from_id | int | NO | +| base_type | int | NO | +| checkoutbyname | nvarchar(256) | YES | +| checkedout_by | uniqueidentifier | YES | +| toolset_id | int | YES | +| is_checkout | int | NO | +| deployed_package_id | int | NO | +| checked_in_package_id | int | NO | +| template_definition_id | int | NO | +| hasderived_obj | int | NO | +| hasassigned_obj | int | NO | +| container_id | int | NO | +| area_id | int | NO | +| hasContained_obj | int | NO | +| hasBelongTo_obj | int | NO | +| is_hidden | bit | NO | +| software_upgrade_needed | bit | NO | +| checked_out_package_status | int | YES | +| category_id | smallint | NO | +| IsCheckedOut | int | NO | +| IsSoftwareUpgradeRequired | int | NO | +| HasPendingUpdate | int | NO | +| IsDeployed | int | NO | +| IsPrimaryEngine | int | NO | +| IsBackupEngine | int | NO | +| is_failover_enabled | int | NO | +| IsPartiallyDeployed | int | NO | +| IsPartiallyUndeployed | int | NO | +| gObjectStatus | int | YES | +| timestamp_of_last_change | bigint | YES | +| namespace_id | smallint | NO | +| folder_id | int | YES | + +### internal_list_unassigned_objects_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| area_gobject_id | int | NO | +| is_template | bit | NO | +| is_hidden | bit | NO | +| container_id | int | NO | +| category_id | smallint | NO | +| namespace_id | smallint | NO | + +### internal_localized_alarm_messages + +| Column | Type | Nullable | +|--------|------|----------| +| Name | nvarchar(663) | NO | +| default_message | nvarchar(1024) | NO | +| phrase_id | int | NO | +| translated_message | nvarchar(1024) | NO | +| locale_id | smallint | NO | +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | + +### internal_model_hierarchy_asset_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| folderId | int | NO | +| is_area | int | NO | +| relative_asset_order | float(53,NULL) | NO | +| friendly_name | nvarchar(1024) | NO | + +### internal_package_status_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| status_id | int | NO | + +### internal_proxy_obj + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| contained_name | nvarchar(32) | NO | +| hierarchical_name | nvarchar(329) | NO | +| is_template | bit | NO | +| status | int | NO | +| refStatus | int | NO | +| hosted_by_gobject_id | int | NO | +| derived_from | int | NO | +| base_type | int | NO | +| checkedout_by | uniqueidentifier | YES | +| toolset_id | int | YES | +| is_checkout | int | NO | +| deployed_package_id | int | NO | +| checkedinpg | int | NO | +| template_definition_id | int | NO | +| mycontainer | int | NO | +| myarea | int | NO | +| is_hidden | bit | NO | +| software_upgrade_needed | bit | NO | +| checked_out_package_status | int | YES | +| category_id | smallint | NO | +| IsCheckedOut | int | NO | +| IsSoftwareUpgradeRequired | int | NO | +| HasPendingUpdate | int | NO | +| IsDeployed | int | NO | +| IsPrimaryEngine | int | NO | +| IsBackupEngine | int | NO | +| is_failover_enabled | int | NO | +| IsPartiallyDeployed | int | NO | +| IsPartiallyUndeployed | int | NO | +| deploy_file_transfering | bit | NO | +| timestamp_of_last_change | bigint | YES | +| namespace_id | smallint | NO | +| folder_id | int | YES | + +### internal_reference_primitive_attribute + +| Column | Type | Nullable | +|--------|------|----------| +| referringGobjectID | int | NO | +| referring_TagName | nvarchar(329) | NO | +| referringPrimitiveName | nvarchar(329) | NO | +| referringAttributeName | nvarchar(329) | NO | +| referringAttribute_full_name | nvarchar(989) | NO | +| reference_string | nvarchar(700) | NO | +| context_string | nvarchar(329) | NO | +| referredToGobjectID | int | YES | +| Referred_TagName | nvarchar(329) | YES | +| referredToPrimitiveName | nvarchar(329) | NO | +| referredToAttributeName | nvarchar(329) | NO | +| referredAttribute_full_name | nvarchar(989) | YES | + +### internal_required_support_features + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| tag_name | nvarchar(329) | NO | +| required_features | nvarchar(256) | YES | +| supported_features | nvarchar(256) | YES | + +### internal_runtime_attributes + +| Column | Type | Nullable | +|--------|------|----------| +| full_attribute_name | nvarchar(989) | NO | +| datatype | smallint | NO | +| area | nvarchar(329) | NO | +| template | nvarchar(329) | NO | + +### internal_visible_packages_per_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| user_guid | uniqueidentifier | NO | + +### internal_visual_element_description_all_packages_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| visual_element_id | int | NO | +| visual_element_type | nvarchar(32) | YES | +| visual_element_category | nchar(1) | YES | +| visual_element_name | nvarchar(659) | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| inheritance_status | nchar(1) | YES | +| description | nvarchar(1024) | YES | +| thumbnail | image(2147483647) | YES | +| tag_name | nvarchar(329) | NO | +| primitive_name | nvarchar(329) | NO | +| gobject_tag_name | nvarchar(329) | NO | +| package_type | nvarchar(5) | NO | +| hierarchical_visual_element_name | nvarchar(659) | NO | +| gobject_hierarchical_name | nvarchar(329) | NO | +| is_library_visual_element | int | NO | +| inherited_from_visual_element_id | int | YES | +| is_thumbnail_dirty | bit | NO | +| property_bitmask | smallint | NO | + +### internal_visual_element_description_per_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| visual_element_id | int | NO | +| visual_element_type | nvarchar(32) | YES | +| visual_element_category | nchar(1) | YES | +| visual_element_name | nvarchar(362) | YES | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| inheritance_status | nchar(1) | YES | +| description | nvarchar(1024) | YES | +| primitive_name | nvarchar(329) | NO | +| tag_name | nvarchar(329) | NO | +| package_type | nvarchar(5) | NO | +| hierarchical_visual_element_name | nvarchar(362) | YES | +| gobject_hierarchical_name | nvarchar(329) | NO | +| is_library_visual_element | int | NO | +| thumbnail | image(2147483647) | YES | +| visual_element_definition | image(2147483647) | NO | +| visual_element_definition_grm | image(2147483647) | NO | +| user_guid | uniqueidentifier | NO | +| inherited_from_gobject_id | int | NO | +| is_hidden | bit | NO | +| inherited_from_visual_element_id | int | YES | +| folder_id | int | YES | +| is_thumbnail_dirty | bit | NO | +| islinked | int | NO | +| content_type | nvarchar(1024) | YES | +| preview | image(2147483647) | YES | +| property_bitmask | smallint | NO | + +### internal_visual_element_description_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| visual_element_id | int | NO | +| visual_element_type | nvarchar(32) | YES | +| visual_element_category | nchar(1) | YES | +| visual_element_name | nvarchar(362) | YES | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| inheritance_status | nchar(1) | YES | +| description | nvarchar(1024) | YES | +| primitive_name | nvarchar(329) | NO | +| tag_name | nvarchar(329) | NO | +| package_type | nvarchar(5) | NO | +| hierarchical_visual_element_name | nvarchar(362) | YES | +| gobject_hierarchical_name | nvarchar(329) | NO | +| is_library_visual_element | int | NO | +| inherited_from_visual_element_id | int | YES | +| checked_in_package_id | int | NO | +| checked_out_package_id | int | NO | +| checked_out_by_user_guid | uniqueidentifier | YES | +| inherited_from_gobject_id | int | NO | +| is_thumbnail_dirty | bit | NO | +| property_bitmask | smallint | NO | + +### internal_visual_element_primitives_preview_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| entity_id | int | NO | +| name | nvarchar(362) | YES | +| visualelementtype | nvarchar(6) | NO | +| user_guid | uniqueidentifier | YES | +| visual_element_id | int | NO | +| content_type | nvarchar(1024) | YES | +| owningobject | nvarchar(329) | YES | +| definingvename | nvarchar(362) | YES | + +### internal_visual_element_reference_per_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| visual_element_bind_status | int | NO | +| visual_element_reference_index | int | NO | +| visual_element_id | int | YES | +| checked_in_unbound_visual_element_name | nvarchar(362) | YES | +| visual_element_type | nvarchar(32) | YES | +| primitive_name | nvarchar(329) | YES | +| tag_name | nvarchar(329) | YES | +| reference_string | nvarchar(694) | YES | +| calculated_visual_element_name | nvarchar(661) | YES | +| calculated_visual_element_type | nvarchar(32) | YES | +| user_guid | uniqueidentifier | NO | +| package_type | nvarchar(5) | NO | +| is_relative_reference | char(1) | NO | + +### internal_visual_element_reference_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| visual_element_bind_status | int | NO | +| visual_element_reference_index | int | NO | +| visual_element_id | int | YES | +| checked_in_unbound_visual_element_name | nvarchar(362) | YES | +| visual_element_type | nvarchar(32) | YES | +| primitive_name | nvarchar(329) | YES | +| tag_name | nvarchar(329) | YES | +| reference_string | nvarchar(694) | YES | +| calculated_visual_element_name | nvarchar(661) | YES | +| calculated_visual_element_type | nvarchar(32) | YES | +| package_type | nvarchar(5) | NO | +| is_relative_reference | char(1) | NO | + +### internal_visual_element_reference_warning_status_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| has_warning | int | NO | + +### internal_visual_element_timestamp_per_user_view + +| Column | Type | Nullable | +|--------|------|----------| +| gobject_id | int | NO | +| package_id | int | NO | +| mx_primitive_id | smallint | NO | +| visual_element_id | int | NO | +| change_type | int | NO | +| timestamp_of_last_change | timestamp | NO | +| user_guid | uniqueidentifier | NO | +| property_bitmask | smallint | NO | + +### public_gobject_definition + +| Column | Type | Nullable | +|--------|------|----------| +| tag_name | nvarchar(329) | NO | +| contained_name | nvarchar(32) | NO | +| primitive_name | nvarchar(329) | NO | +| attribute_name | nvarchar(329) | NO | +| mx_attribute_id | smallint | NO | +| has_config_set_handler | bit | NO | +| mx_data_type | smallint | NO | +| is_array | bit | NO | +| security_classification | smallint | NO | +| mx_attribute_category | int | NO | +| is_frequently_accessed | bit | NO | +| is_locked | bit | NO | +| mx_value | text(2147483647) | NO | +| gobject_id | int | NO | +| derived_from_gobject_id | int | NO | +| contained_by_gobject_id | int | NO | +| area_gobject_id | int | NO | +| hosted_by_gobject_id | int | NO | +| default_symbol_gobject_id | int | NO | +| default_display_gobject_id | int | NO | +| checked_in_package_id | int | NO | +| checked_out_package_id | int | NO | +| deployed_package_id | int | NO | +| configuration_version | int | NO | +| deployed_version | int | NO | +| hosting_tree_level | smallint | NO | +| mx_primitive_id | smallint | NO | +| parent_mx_primitive_id | smallint | NO | +| execution_group | smallint | NO | +| is_virtual | bit | NO | +| primitive_guid | uniqueidentifier | NO | +| runtime_handler_clsid | uniqueidentifier | YES | +| package_handler_clsid | uniqueidentifier | YES | +| major_version | int | NO | +| supports_dynamic_attributes | bit | NO | +| original_template_tagname | nvarchar(32) | NO | +| required_features | nvarchar(256) | YES | +| supported_features | nvarchar(256) | YES | +| category_id | smallint | NO | +| category_clsid | uniqueidentifier | YES | +| runtime_clsid | uniqueidentifier | YES | + diff --git a/gr/tags.JPG b/gr/tags.JPG new file mode 100644 index 0000000000000000000000000000000000000000..8f534b0e1f5e90cf0ff1eb4d5d0d25ca2d03bf2f GIT binary patch literal 28675 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 XHd{7_sr!FHW&Zjt{y+Dke@y-d+>i%n literal 0 HcmV?d00001 diff --git a/lib/ArchestrA.MXAccess.dll b/lib/ArchestrA.MXAccess.dll new file mode 100644 index 0000000000000000000000000000000000000000..c2da57e4ed742204d9edb7df9be8eeef84957299 GIT binary patch literal 34968 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 literal 0 HcmV?d00001 diff --git a/mxaccess_documentation.md b/mxaccess_documentation.md new file mode 100644 index 0000000..c32a0fb --- /dev/null +++ b/mxaccess_documentation.md @@ -0,0 +1,3619 @@ +# ArchestrA MXAccess Toolkit User's Guide + +> Invensys Systems, Inc. © 2002–2010, 2014. Application Server 2014. + +# Welcome + +This guide describes how to use the MXAccess Toolkit. ArchestrA Galaxy data access is exposed to .NET and COM clients through a programmable object model called MXAccess. You can use the MXAccess object model to write programs that automate reading and writing data values in the attributes of ArchestrA objects. + +You can view this document online or you can print it, in part or whole, by using the print feature in Adobe Reader. + +This guide assumes you know how to use Microsoft Windows, including navigating menus, moving from application to application, and moving objects on the screen. If you need help with these tasks, see the Microsoft Help. + +# Documentation Conventions + +This documentation uses the following conventions: + + + + + + + + + + + + + + + + + + + + + + +
ConventionUsed for
Initial CapitalsPaths and file names.
BoldMenus, commands, dialog box names, and dialog box options.
MonospaceCode samples and display text.
+ +# Technical Support + +Wonderware Technical Support offers a variety of support options to answer any questions on Wonderware products and their implementation. + +Before you contact Technical Support, refer to the relevant section(s) in this documentation for a possible solution to the problem. If you need to contact technical support for help, have the following information ready: + +* The type and version of the operating system you are using. +* Details of how to recreate the problem. +* The exact wording of the error messages you saw. +* Any relevant output listing from the Log Viewer or any other diagnostic applications. +* Details of what you did to try to solve the problem(s) and your results. +* If known, the Wonderware Technical Support case number assigned to your problem, if this is an ongoing problem. + +# Chapter 1: Getting Started + +Use the MXAccess Toolkit to interface a program to ArchestrA Message Exchange. The Toolkit supports a simple, lightweight interface that permits: + +* Connecting to an ArchestrA Galaxy. +* Subscribing to one or more attributes and receiving data updates from these attributes. +* Writing new values to these attributes. +* Authenticating one or more users so that the program can write to attributes that require secured writes or verified writes. + +At minimum, a deployed ArchestrA Platform must exist on the computer running the client application. + +## Components of the MXAccess Toolkit + +The MXAccess Toolkit contains: + +* The MXAccess32.tlb type library . +* The MxAccess.tlb type library. +* The COM/.NET support files: + * The .NET interop DLL ArchestrA.MxAccess.dll + * The ArchestrA.MxAccess.dll .NET policy file. + * Code samples for Visual Basic, .NET, Visual C++, and C#. +* Documentation. + +The MXAccess Toolkit installs these files to the following locations. + + + + + + + + + + + + + + + + + + + + + + +
FilesLocation
Type libraries32-bit operation systems:
\Program Files\ArchestrA\Framework\bin

64-bit operating systems:
\Program Files (x86)\ArchestrA\Framework\bin
Interop file32-bit operation systems:
\Program Files\ArchestrA\Framework\bin

64-bit operating systems:
\Program Files (x86)\ArchestrA\Framework\bin
Code samples32-bit operating systems:
\Program Files\ArchestrA\Toolkits\Samples

64-bit operating systems:
\Program Files (x86)\ArchestrA\Toolkits\Samples
+ +# Registering the Interface Files + +The installation registers the type library and interop file for you. You can use the following procedure to copy these files to a computer and register them yourself. A common example requiring manual registration is running the code samples on an earlier version of Application Server. This is because earlier versions of Application Server did not contain the needed files. + +## To manually register the interface files + +1 Copy the files MxAccess.tlb and ArchestrA.MxAccess.dll as follows: +* On 32-bit operating systems, from the \Program Files\ArchestrA\Framework\Bin folder on the MXAccess Toolkit CD to the folder \Program Files\ArchestrA\Framework\bin on your hard drive. +* On 64-bit operating systems, from the \Program Files (x86)\ArchestrA\Framework\Bin folder on the MXAccess Toolkit CD to the folder \Program Files (x86)\ArchestrA\Framework\bin on your hard drive + +Registering the Interface Files 2 Register the type library. Open a command prompt window and type the regtlib command followed by the complete path to the type library. + +* For example, on a 32-bit operating system: + +``` +regtlib "C:\Program +Files\ArchestrA\Framework\bin\MxAccess32.tlb" +``` + +* For example, on a 64-bit operating system: + +``` +regtlib "C:\Program Files +(x86)\ArchestrA\Framework\bin\MxAccess32.tlb" +``` + +**Note:** You must enclose the folder path within quotation marks because there is a space in the Program Files folder name. + +3 Register the interop file. +a Open two Explorer windows. Browse to the \WINDOWS\assembly folder in one window. In the other window: +* On a 32-bit operating system, browse to the \Program Files\ArchestrA\Framework\bin folder. +* On a 64-bit operating system, browse to the \Program Files (x86)\ArchesrA\Framework\bin folder. +b If an ArchestrA.MxAccess folder exists in the \WINDOWS\assembly folder, you must uninstall it. Do this by right-clicking ArchestrA.MxAccess in the \WINDOWS\assembly folder and then clicking **Uninstall**. Drag the new ArchestrA.MxAccess.dll from the ArchestrA\Framework\bin folder to the WINDOWS\assembly folder. + +# Chapter 2: Using the Interface Files in Programs + +After you install and register the type library and interop, you can use them in your programs. + +To develop new software to use the MXAccess interface, also known as LMXProxy, you must include a reference to the interface in your project. How to do this differs according to which language you are using. This documentation describes using the MXAccess Toolkit with three languages supported by Microsoft Visual Studio: Visual Basic.NET, C#, and Visual C++. + +For the program to connect to the Galaxy and access ArchestrA data, a deployed ArchestrA Platform must exist on the computer running the client application. However, you do not need ArchestrA or a MXAccess runtime license installed on your computer to compile and link the program. + +# Adding a Toolkit Reference Using Visual Basic.NET + +You can add a toolkit reference in Visual Basic.NET. + +## To add a reference + +1. On the **Project** menu, click **Add Reference**. The **Add Reference** dialog box appears. +2. Click the **Browse** tab. +3. Navigate to: + * (32-bit operating system) \Program Files\ArchestrA\Framework\bin and select the ArchestrA.MxAccess.dll interop file. + * (64-bit operating system) \Program Files (x86)\ArchestrA\Framework\bin and select the ArchestrA.MxAccess.dll interop file. + +## To confirm that the MXAccess reference has been added to your project + +1. On the **Project** menu, click ** Properties**, where AppName is the actual name of your project. You can also use Solution Explorer and double-click the My Project icon. +2. Click the **References** tab. The **Reference Names** list includes ArchestrA.MxAccess. +3. Show the path to the DLL file. +4. On the **View** menu, click **Object Browser**. The **Object Browser** appears. +5. Expand the icon for ArchestrA.MxAccess to see all the interfaces and other members provided by the MXAccess interface. + +Adding a Toolkit Reference Using C# # Adding a Toolkit Reference Using C# + +You can add a toolkit reference in C#. + +## To add a reference + +1. On the **Project** menu, click **Add Reference**. The **Add Reference** dialog box appears. +2. Click the **Browse** tab. +3. Navigate to: + * (32-bit operating system) \Program Files\ArchestrA\Framework\bin and select the ArchestrA.MxAccess.dll interop file. + * (64-bit operating system) \Program Files(x86)\ArchestrA\Framework\bin and select the ArchestrA.MxAccess.dll interop file. + +## To confirm that the MXAccess reference has been added to your project + +1. Open Solution Explorer and expand the **References** icon. +2. On the **View** menu, click **Object Browser**. The **Object Browser** pane appears. +3. You can expand the icon for ArchestrA.MxAccess to see all the interfaces and other members provided by the MXAccess interface. + +# Adding a Toolkit Reference Using C++ + +You can add a toolkit reference in C++. + +**Note:** Visual C++ supports smart pointers. When used with COM interface pointers, smart pointers can be a powerful way to ensure that COM objects get properly instantiated at startup and cleaned up at shutdown. The code examples in this toolkit include one C++ project that uses smart pointers (the ActiveX dialog box) and another that makes it clear where the COM objects are being created and destroyed. For more information on using smart pointer templates in your C++ program, see the Visual Studio documentation. + +## To add a reference + +1. In the file stdafx.h, add a line such as the following example to import the type library: + +```c++ +#import "C:\Program Files\ArchestrA\Framework\bin\MxAccess32.tlb" no_namespace raw_interfaces_only +``` + +When you compile, Visual Studio creates a file named MxAccess.tlh. + +2. Open this file to see the interfaces and other members provided by the MXAccess interface. + +# Handling Exceptions in Your API Code + +Include exception handling when any of the API calls are invoked. + +* **Visual Basic.NET** + Add an OnError statement and handling to your functions and subroutines. +* **C#** + Enclose your code in a try block and provide error handling and display in a catch block. +* **Visual C++** + Enclose your code in a try block and provide error handling and display in a catch block. You should also inspect the HRESULT returned from the API functions and add code to handle failures. + +Declaring and Instantiating the LMXProxyServer Object The code examples show you the basic operations of using the MXAccess API function calls. Although they include rudimentary exception handling, they do not handle all possible errors and exceptions. Your program should include exception and error processing to provide robust operation and problem diagnostics appropriate for your specific application. + +# Declaring and Instantiating the LMXProxyServer Object + +To use the MXAccess Toolkit, your program must declare the LMXProxyServer object. Use the following examples, depending on which language you use. + +## Examples + +[Visual Basic.NET] +```vb +Dim WithEvents LMX_Server As ArcestrA.MxAccess.LMXProxyServer +``` + +[C#] +```csharp +ArcestrA.MxAccess.LMXProxyServer LMX_Server; +``` + +[Visual C++] +```cpp +ILMXProxyServer* pLMX_Server; +``` +or, if using a smart pointer +```cpp +CComPtr pLMX_Server; +``` +Create an instance of the LMXProxyServer. Use the following examples, depending on which language you use. + +## Examples + +[Visual Basic.NET] +```vb +LMX_Server = New ArcestrA.MxAccess.LMXProxyServer +``` + +[C#] +```csharp +LMX_Server = new ArcestrA.MxAccess.LMXProxyServer; +``` + +[Visual C++] + +```c++ +HRESULT hr = CoCreateInstance (__uuidof(LMXProxyServer), + NULL, + CLSCTX_INPROC_SERVER, + __uuidof(ILMXProxyServer), + (void **)&pLMX_Server); +``` + +or, if using a smart pointer + +```c++ +HRESULT hr = + pLMX_Server.CoCreateInstance (__uuidof( LMXProxyServer )); +``` + +In Visual C++ your program must initialize COM before it can perform any operations with COM. Most likely, you will create the LMXProxy in the main program thread. For example: + +```c++ +HRESULT hr = + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); +``` + +initializes COM for the apartment threaded model. Be sure to call CoUninitialize() when your program exits. + +Use the following examples at the end of your program to discard the LMXProxyServer. + +[Visual Basic.NET] + +```vb +LMX_Server = Nothing +``` + +(or simply let it go out of scope) + +[C#] + +```csharp +LMX_Server = null; +``` + +(or simply let it go out of scope) + +[Visual C++] + +```c++ +HRESULT hr = pLMX_Server->Release(); +pLMX_Server = NULL; +``` + +Or, if using a smart pointer, you can simply let it go out of scope. + +Connecting Handlers for Events Reported from the LMXProxyServer # Connecting Handlers for Events Reported from the LMXProxyServer + +The MXAccess Toolkit supports receiving notifications from the LMXProxyServer. In COM, these notifications are handled as connection points. In .NET, these are handled as delegates. Your program must implement a handler for each type of event you expect to receive and connect it to the events using the mechanisms appropriate for the programming language you are using. + +Visual Basic and C# make this implementation fairly easy. Visual C++ requires a little more setup to create a COM object that exposes the necessary interface, including support for the IDispatch interface. + +The events that must be handled are: + +* **OnDataChange()** + Reports the value and status of an ArcestrA Attribute to which the program has subscribed. +* **OnWriteComplete()** + Reports the status after a write operation completes, indicating such information as whether the write operation succeeded or failed. +* **OperationComplete()** + Reports the status when an Advanced Communication Management operation has been completed, indicating such information as whether it succeeded or failed. +* **OnBufferedDataChange()** + Reports the VTQ buffer and the status of an ArcestrA Attribute to which the program has subscribed. + +The specifics of what your event handlers do depend on the purpose of your program. This section focuses primarily on how to connect the handlers in your program to the event callback mechanism. For more information about what should be inside the code for the event handlers, see "Handling the OnDataChange Callback" on page 62, see "Handling the OnWriteComplete Callback" on page 72, see "Handling the OperationComplete Callback" on page 41, and see "Handling the OnBufferedDataChange Callback" on page 63. + +Handling events involves implementing required methods, exposing them as event handlers, and connecting the event handlers to the LMXProxyServer's Connection Points. + +[Visual Basic.NET] + +Declare the methods as: + +```vbnet +Private Sub LMX_OnDataChange( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByVal pvItemValue As Object, + ByVal pwItemQuality As Integer, + ByVal pftItemTimeStamp As Object, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) + Handles LMX_Server.OnDataChange +``` + +and + +```vbnet +Private Sub LMX_OnWriteComplete( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) + Handles LMX_Server.OnWriteComplete +``` + +and + +```vbnet +Private Sub LMX_OperationComplete( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) + Handles LMX_Server.OperationComplete +``` + +and + +```vbnet +Private Sub LMX_OnBufferedDataChange( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByVal dtDataType As ArchestrA.MxAccess.MxDataType, + ByVal pvItemValue As Object, + ByVal pwItemQuality As Object, + ByVal pftItemTimeStamp As Object, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) + Handles LMX_Server.OnBufferedDataChange +``` + +Connecting Handlers for Events Reported from the LMXProxyServer At run time, Visual Basic automatically connects the event handlers when you instantiate the LMX_Server object. + +**Note:** The names do not have to be LMX_OnDataChange, LMX_OnWriteComplete, and LMX_OperationComplete, but the parameter types in the function syntax must match declarations in the LMXProxy interface. + +[C#] + +Declare the methods as: + +```csharp +private void LMX_OnDataChange( + int hLMXServerHandle, + int phItemHandle, + object pvItemValue, + int pwItemQuality, + object pftItemTimeStamp, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[ ] ItemStatus +); +``` +and + +```csharp +private void LMX_OnWriteComplete( + int hLMXServerHandle, + int phItemHandle, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[ ] ItemStatus +); +``` +and + +```csharp +private void LMX_OperationComplete( + int hLMXServerHandle, + int phItemHandle, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[ ] ItemStatus +); +``` +and + +```csharp +private void LMX_OnBufferedDataChange( + int hLMXServerHandle, + int phItemHandle, + ArchestrA.MxAccess.MxDataType dtDataType, + object pvItemValue, object pvItemQuality, + object pvItemTimeStamp, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[ ] ItemStatus +); +``` + +At run time, C# does not automatically connect these event handlers for you. You must do this in your code. After you have instantiated the LMX_Server object, the following three lines will connect the methods as delegates for the object: + +```csharp +LMX_Server.OnDataChange += new + _ILMProxyServerEvents_OnDataChangeEventHandler(LMX_OnDataChange); +LMX_Server.OnWriteComplete += new + _ILMProxyServerEvents_OnWriteCompleteEventHandler(LMX_OnWriteComplete); +LMX_Server.OperationComplete += new + _ILMProxyServerEvents_OperationCompleteEventHandler(LMX_OperationComplete); +LMX_Server.OnBufferedDataChange += new + _ILMProxyServerEvents2_OnBufferedDataChangeEventHandler(LMX_OnBufferedDataChange); +``` + +**Note:** The names do not have to be LMX_OnDataChange, LMX_OnWriteComplete, and LMX_OperationComplete, but the parameter types in the function syntax must match declarations in the LMXProxy interface. + +[Visual C++] + +Declare the methods as: + +```cpp +HRESULT CLMX_Events_Shim::OnDataChange ( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + VARIANT pvItemValue, + long pwItemQuality, + VARIANT pftItemTimeStamp, + SAFEARRAY *pSAItemStatus +); +``` +and + +```cpp +HRESULT CLMX_Events_Shim::OnWriteComplete ( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + SAFEARRAY *pSAWriteItemStatus +); +``` +and + +```cpp +HRESULT CLMX_Events_Shim::OperationComplete ( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + SAFEARRAY *pSAWriteItemStatus); +``` + +Connecting Handlers for Events Reported from the LMXProxyServer and + +```c +HRESULT CLMX_Events_Shim::OnBufferDataChange( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + ArchestrA.MxAccess.MxDataType dtDataType, + VARIANT pvItemValue, + VARIANT pwItemQuality, + VARIANT pftItemTimeStamp, + SAFEARRAY *pSAItemStatus); +``` + +where CLMX_Events_Shim is a class derived from the base class LMXProxyServerEvents. + +The sample code includes a header file and implementation of CLMXProxyServerEvents that is derived from IDispatch and handles passing IDispatch methods to the OnDataChange(), OnWriteComplete(), and OperationComplete() methods. + +At run time, C++ does not automatically connect these event handlers for you. You must do this in your code. After you have instantiated the LMX_Server object, you must instantiate the CLMX_Events_Shim object. It helps to have two methods in CLMX_Events_Shim that encapsulate the Connect() and Disconnect() operations: + +```c +void CLMX_Events_Shim::Connect() +{ + // check whether we are already connected + if (!m_bConnected) + { + // attempt to call Advise on the + // IConnectionPoint + DWORD cookie = 0; + IConnectionPointContainer* pcPC = 0; + HRESULT hr = pLMX_Server->QueryInterface(&pcPC); + if (SUCCEEDED(hr)) + { + hr = pcPC->FindConnectionPoint( + __uuidof(_ILMXProxyServerEvents), &m_pCP); + if (SUCCEEDED(hr)) + { + hr = m_pCP->Advise(this, &cookie); + m_LmxInterfaceCallBacksCookie = cookie; + m_bConnected = true; + } + pcPC->Release(); + } + } +} +``` + +and + +```c++ +void CLMX_Events_Shim::Disconnect() +{ + // If we are connected then call Unadvise on + // the IConnectionPoint + if (m_bConnected) + { + m_bConnected = false; + IConnectionPoint* pConnectionPoint = m_pCP; + m_pCP->Unadvise(m_LmxInterfaceCallBacksCookie); + pConnectionPoint->Release(); + } +} +``` + +After you instantiate the LMXProxyServer object, make sure that you also instantiate an object of type CLMX_Events_Shim and then call its Connect() method. When you are preparing to delete (or even Unregister) the LMXProxyServer object, you should call the Disconnect() method to stop servicing the event callbacks. + +**Note:** The class names do not have to be CLMX_Events_Shim and CLMXProxyServerEvents but the overall organization of these classes should be similar to the code samples. Because the methods are exposed directly as a COM interface, the names DO have to be OnDataChange, OnWriteComplete, and OperationComplete and the parameter types in the function syntax must match declarations in the LMXProxy interface. + +# Initializing the LMXProxyServer Connection + +The LMXProxyServer must be initialized prior to making other method calls on the COM object's interface. Use the Register() method, which connects your component to LMX. + +## Register() Method + +Connects your component to LMX. + +### Syntax + +**[Visual Basic.NET]** +```vb +Function Register (ByVal pClientName As String) As Integer +``` +**[C#]** +```csharp +int Register(string pClientName); +``` +**[Visual C++]** +```cpp +HRESULT __stdcall Register(BSTR pClientName, long *phLMXServerHandle); +``` + +Shutting Down the LMXProxyServer Connection **Parameters** + +pClientName +Name of your component. + +**Returns** + +Handle of the connection. The handle that is returned represents the LMX connection. Your program should preserve this handle for the life time of the connection because it is used on all subsequent calls to other methods on the interface. + +**Examples** + +[Visual Basic.NET] +```vbnet +hLMX = LMX_Server.Register("TestApp") +``` +[C#] +```csharp +hLMX = LMX_Server.Register("TestApp"); +``` +[Visual C++] +```cpp +BSTR bstrTemp = ::SysAllocString(L"TestApp"); +hr = pLMX_Server->Register(bstrTemp, &hLMX); +::SysFreeString(bstrTemp); +``` + +# Shutting Down the LMXProxyServer Connection + +The LMXProxyServer connection must be released when your application shuts down or when it no longer needs to be connected to the Galaxy. Use the Unregister() method, which disconnects your component from LMX. + +Before calling the Unregister() method, you must unadvise all active items and remove them from the internal tables using the UnAdvise() and RemoveItem() methods. + +## Unregister() Method + +Disconnects your component from LMX. + +**Syntax** + +[Visual Basic.NET] +```vbnet +Sub Unregister (ByVal hLMXServerHandle As Integer) +``` +[C#] +```csharp +void Unregister(int hLMXServerHandle); +``` +[Visual C++] +```cpp +HRESULT __stdcall Unregister(long hLMXServerHandle); +``` + +**Parameters** + +* **hLMXServerHandle** + Handle of the connection. + +**Returns** + +Nothing. + +**Remarks** + +After calling the Unregister() method, you can discard the LMXProxyServer object. If your program is one that connects to LMX for a time, disconnects, and connects again, you may want to keep the LMXProxyServer object, rather than releasing it and creating a new one each time. + +**Examples** + +[Visual Basic.NET] +```vbnet +Call LMX_Server.Unregister (hLMX) +``` +[C#] +```csharp +LMX_Server.Unregister (hLMX); +``` +[Visual C++] +```cpp +hr = pLMX_Server->Unregister (hLMX); +``` + +# Adding Item References to the Internal Tables + +To access an ArchestrA Attribute, you must first add it to the internal table of attribute references that LMX maintains for your connection. Use the AddItem() or the AddItem2() method. + +## AddItem() Method + +Adds an attribute to the internal table of attributes. + +**Syntax** + +[Visual Basic.NET] +```vbnet +Function AddItem ( + ByVal hLMXServerHandle As Integer, + ByVal strItemDef As String +) +As Integer +``` + +Adding Item References to the Internal Tables [C#] + +```csharp +int AddItem( + int hLMXServerHandle, string strItemDef +); +``` + +[Visual C++] + +```cpp +HRESULT __stdcall AddItem( + long hLMXServerHandle, + BSTR strItemDef, + long *phItem +); +``` + +**Parameters** + +**hLMXServerHandle** +Handle of the connection. + +**strItemDef** +Reference string for the attribute. + +**Returns** + +Handle of the item. The handle that is returned represents the item in the LMX connection's internal table. Your program should retain this handle because it is used on all subsequent calls to other methods involved in reading and writing data for that attribute. + +**Remarks** + +You can add several attributes to the table by calling the AddItem() method for each one. + +Before you can write to an attribute or receive data update notifications for it, you must put the item on advise. For more information, see "Advise() Method" on page 33. + +When shutting a program down, you should unadvise all active items and remove all items from the internal table before calling the Unregister() method. For more information, see "UnAdvise() Method" on page 36 and "RemoveItem() Method" on page 32. + +**Examples** + +[Visual Basic.NET] + +```vbnet +hItem = LMX_Server.AddItem(hLMX, strItemDef) +``` + +[C#] + +```csharp +hItem = LMX_Server.AddItem(hLMX, strItemDef); +``` + +[Visual C++] + +```c++ +BSTR bstrTemp = ::SysAllocString(szItemDef); + hr = pLMX_Server->AddItem(hLMX, bstrTemp, &hItem); +::SysFreeString (bstrTemp); +``` + +If the attribute represents a single data value, reference the item as follows: + +[Single data value] +ud1.x + +[Single element of an array:] +ud1.ar[2] + +To reference all elements of an attribute that is an array, follow the reference with brackets: + +[Entire array:] +ud1.ar[ ] + +# AddItem2() Method + +Adds an attribute to the internal table of attributes, using a relative reference and an Object name. This makes it easier to resolve a reference for an attribute relative to an indicated Object. + +The reference string must use one of the five relative reference names: + +* MyPlatform +* MyEngine +* MyArea +* MyContainer +* Me + +The context string identifies the Object to which the reference corresponds. + +## Syntax + +[Visual Basic.NET] + +```vb +Function AddItem2 ( + ByVal hLMXServerHandle As Integer, + ByVal strItemDef As String + ByVal strItemCtxt As String +) +As Integer +``` + +Adding Item References to the Internal Tables [C#] + +```csharp +int AddItem2( + int hLMXServerHandle, + string strItemDef + string strItemCtxt +); +``` + +[Visual C++] + +```cpp +HRESULT __stdcall AddItem2 ( + long hLMXServerHandle, + BSTR strItemDef, + BSTR strItemCtxt, + long *phItem +); +``` + +**Parameters** + +**hLMXServerHandle** +Handle of the connection. + +**strItemDef** +Reference string for the attribute. + +**strItemCtxt** +Context string for the related object. + +**Returns** + +Handle of the item. The handle that is returned represents the item in the LMX connection's internal table. Your program should retain this handle because it is used on all subsequent calls to other methods involved in reading and writing data for that attribute. + +**Remarks** + +You can add several attributes to the table by calling the AddItem() method for each one. + +Before you can write to an attribute or receive data update notifications for it, you must put the item on advise. For more information, see "Advise() Method" on page 33. + +When shutting the program down, you should unadvise all active items and remove all items from the internal table before calling the Unregister() method. For more information, see "UnAdvise() Method" on page 36 and "RemoveItem() Method" on page 32. + +The AddItem2() method is on the ILMXProxyServer4 interface, which extends the ILMXProxyServer interface. + +## Examples + +[Visual Basic.NET] +```vb +hItem = LMX_Server.AddItem2(hLMX, strItemDef, strItemCtxt) +``` + +[C#] +```csharp +hItem = LMX_Server.AddItem2(hLMX, strItemDef, strItemCtxt); +``` + +[Visual C++] +```cpp +BSTR bstrTemp = ::SysAllocString(szItemDef); +BSTR bstrCtxt = ::SysAllocString(szItemCtxt); +hr = pLMX_Server->AddItem2(hLMX, bstrTemp, bstrCtxt, +&hItem); +::SysFreeString(bstrTemp); +::SysFreeString(bstrCtxt); +``` + +If the Attribute belongs to the Platform on which an Object is running, you do not need to know the actual name of the Platform. You can add an item using MyPlatform in the reference string, and specify the Object as the context for the reference, using its global name. Other relative references can be specified in the same way: + +* CPULoad on the Platform on which AnalogDevice_001 is running: + * hItem = LMX_Server.AddItem2(hLMX, "MyPlatform.CPULoad", "AnalogDevice_001"); +* Scan period of the Engine on which ud1 is hosted: + * hItem = LMX_Server.AddItem2(hLMX, "MyEngine.Scheduler.ScanPeriod", "ud1"); + +## AddBufferedItem() Method + +You can configure your client program to subscribe to buffered attributes. Buffered attributes support retrieving and processing of multiple data items which are received from field devices during AppEngine's Scan Period. The multiple data items are received by buffered attributes once per scan period in the form of buffer, in order to eliminate data folding. + +### Syntax + +[Visual Basic.NET] +```vb +Function AddBufferedItem( + hLMXServerHandle As Integer, + strItemDef As String, + strItemCtxt As String +) + As Integer + +Adding Item References to the Internal Tables [C#] + +```csharp +int AddBufferedItem( + int hLMXServerHandle, + string strItemDef, + string strItemCtxt) +``` + +[Visual C++] + +```cpp +HRESULT __stdcall AddBufferedItem ( + long hLMXServerHandle, + BSTR strItemDef, + BSTR strItemCtxt, + long * phItem ) +``` + +**Parameters** + +**hLMXServerHandle** +Handle of the connection. + +**strItemDef** +Reference string for the attribute. + +**strItemCtxt** +Context string for the related object. + +**Returns** + +Handle of the item. The handle that is returned represents the item in the LMX connection's internal table. Your program should retain this handle because it is used on all subsequent calls to other methods involved in reading and writing data for the buffered attribute. + +**Remarks** + +You can add several attributes to the table by calling the AddBufferedItem() method for each one. + +Before you can write to an attribute or receive data update notifications for it, you must put the item on advise. For more information, see "Advise() Method" on page 33. + +When shutting a program down, you should unadvise all active items and remove all items from the internal table before calling the Unregister() method. For more information, see "UnAdvise() Method" on page 36 and "RemoveItem() Method" on page 32. + +**Examples** + +[Visual Basic.NET] + +```vbnet +hItem = LMX_Server.AddBufferedItem(hLMX, strItemDef, strItemCtxt) +``` + +[C#] +```csharp +hItem = LMX_Server.AddBufferedItem(hLMX, strItemDef, + strItemCtxt); +``` + +[Visual C++] +```cpp +LMX_Server.AddBufferedItem(hLMXServerHandle, strItemDef, strItemCtxt, phItem); +``` + +# Removing Item References from Internal Tables + +When you no longer need access to a particular attribute, you can remove the reference to it from the internal table. Use the RemoveItem() method. + +## RemoveItem() Method + +Removes an attribute from the internal table of attributes. + +### Syntax + +[Visual Basic.NET] +```vbnet +Sub RemoveItem ( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer +) +``` + +[C#] +```csharp +void RemoveItem( + int hLMXServerHandle, + int hItem +); +``` + +[Visual C++] +```cpp +HRESULT __stdcall RemoveItem( + long hLMXServerHandle, + long hItem +); +``` + +### Parameters + +**hLMXServerHandle** + Handle of the connection. + +**hItem** + Handle of the item. + +Putting Items on Advise **Returns** +Nothing. + +**Remarks** +If the item is active—that is, if it has been placed on advise—unadvise the item before removing it. For more information, see "UnAdvise() Method" on page 36. +When shutting down, unadvise all active items and remove all items from the internal table before calling the Unregister() method. For more information, see "UnAdvise() Method" on page 36 and "RemoveItem() Method" on page 32. + +**Examples** +[Visual Basic.NET] +Call LMX_Server.RemoveItem(hLMX, hItem) +[C#] +LMX_Server.RemoveItem(hLMX, hItem); +[Visual C++] +hr = pLMX_Server->RemoveItem(hLMX, hItem); + +# Putting Items on Advise + +To receive data updates regarding an attribute, you can put it on advise, also known as subscribing to an item. Use the Advise() method or the AdviseSupervisory() method. + +## Advise() Method + +Adds advise to an attribute. + +### Syntax +[Visual Basic.NET] +```vbnet +Sub Advise( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer +) +``` +[C#] +```csharp +void Advise( + int hLMXServerHandle, + int hItem +); +``` + +[Visual C++] + +```c++ +HRESULT __stdcall Advise( + long hLMXServerHandle, + long hItem +); +``` + +**Parameters** + +**hLMXServerHandle** +Handle of the connection. + +**hItem** +Handle of the item. + +**Returns** + +Nothing. + +**Remarks** + +After you have put an item on advise, it is considered active. If the data of that attribute changes, your program receives a notification of the update through the OnDataChange() event. + +You must put an item on advise before you can write to it. + +You must call the AddItem() method on an item before you can put it on advise. When shutting down the program, you should unadvise all active items and remove all items from the internal table before calling Unregister(). For more information, see "UnAdvise() Method" on page 36 and "RemoveItem() Method" on page 32. + +**Examples** + +[Visual Basic.NET] + +```vb +Call LMX_Server.Advise(hLMX, hItem) +``` + +[C#] + +```csharp +LMX_Server.Advise(hLMX, hItem); +``` + +[Visual C++] + +```c++ +hr = pLMX_Server->Advise(hLMX, hItem); +``` + +Putting Items on Advise # AdviseSupervisory() Method + +Adds advise to an attribute. + +## Syntax + +**[Visual Basic.NET]** + +```vbnet +Sub AdviseSupervisory( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer +) +``` + +**[C#]** + +```csharp +void AdviseSupervisory( + int hLMXServerHandle, + int hItem +); +``` + +**[Visual C++]** + +```cpp +HRESULT __stdcall AdviseSupervisory( + long hLMXServerHandle, + long hItem +); +``` + +## Parameters + +**hLMXServerHandle** +Handle of the connection. + +**hItem** +Handle of the item. + +## Returns + +Nothing. + +## Remarks + +The Advise() method establishes a User Connection to ArchestrA data, while the AdviseSupervisory() method establishes a Supervisory Connection. Both of these connections provide updates via the OnDataChange() callback and status information via the OnWriteComplete() and OperationComplete() callbacks. When you attempt to invoke a Write operation on the item, the Supervisory Connection does a Supervisory Set on the Attribute; it does not require a login by a user. + +The AdviseSupervisory() method is on the ILMXProxyServer4 interface, which extends the ILMXProxyServer interface. + +The Supervisory Connection is appropriate if your application runs in the background, such as a Windows Service, which normally does not interact with users via a GUI. Using AdviseSupervisory() for the items enables the program to write to items that require Secured Writes or Verified Writes, without requiring a user login to permit the write. + +You must put an item on advise before you can write to it. + +You must call the AddItem() method on an item before you can put it on advise. When shutting down the program, you should unadvise all active items and remove all items from the internal table before calling Unregister(). For more information, see "UnAdvise() Method" on page 36 and "RemoveItem() Method" on page 32. + +**Examples** + +[Visual Basic.NET] +```vb +Call LMX_Server.AdviseSupervisory(hLMX, hItem) +``` +[C#] +```csharp +LMX_Server.AdviseSupervisory(hLMX, hItem); +``` +[Visual C++] +```cpp +hr = pLMX_Server->AdviseSupervisory(hLMX, hItem); +``` + +## Unadvising Items + +When your program no longer needs to receive data updates and no longer needs to write to an attribute, it can unadvise the item—that is, it can take it off advise. Use the UnAdvise() method. + +## UnAdvise() Method + +Stops the advise from the attribute. + +### Syntax + +[Visual Basic.NET] +```vb +Sub UnAdvise( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer +) +``` +[C#] +```csharp +void UnAdvise ( + int hLMXServerHandle, + int hItem +); +``` + +Unadvising Items [Visual C++] + +```c++ +HRESULT __stdcall UnAdvise ( + long hLMXServerHandle, + long hItem +); +``` + +**Parameters** + +**hLMXServerHandle** +Handle of the connection. + +**hItem** +Handle of the item. + +**Returns** + +Nothing. + +**Remarks** + +When shutting down, unadvise all active items and remove all items from the internal table before calling the Unregister() method. For more information, see "UnAdvise() Method" on page 36 and "RemoveItem() Method" on page 32. + +**Examples** + +**[Visual Basic.NET]** + +```vb +Call LMX_Server.UnAdvise(hLMX, hItem) +``` + +**[C#]** + +```csharp +LMX_Server.UnAdvise(hLMX, hItem); +``` + +**[Visual C++]** + +```cpp +hr = pLMX_Server->UnAdvise(hLMX, hItem); +``` + +# Advanced Communication Management + +After an attribute has been put on advise, the application can call Suspend() to temporarily stop receiving updates without tearing down the subscription to the attribute. Updates can be resumed by calling Activate(). The callback OperationComplete() returns the status information about processing these function calls. + +The Suspend() and Activate() methods are on the ILMXProxyServer4 interface, which extends the ILMXProxyServer interface. + +When an item is suspended or activated, that item should not be taken off advise or removed from the internal tables until the OperationComplete() event is received, indicating whether the update to the Advanced Communication Management status is successful or an error has occurred. + +If the event indicates that an OperationComplete error has been detected, check the category and error code for further information. + +To suspend updates, call the Suspend() method. + +## Suspend() Method + +Temporarily stops receiving updates without removing the subscription to the attribute. + +### Syntax + +**[Visual Basic.NET]** + +```vbnet +Sub Suspend(ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer, + ByRef pMxStatus As ArcestrA.MxAccess.MxStatus) +``` + +**[C#]** + +```csharp +void Suspend(int hLMXServerHandle, + int hItem, + out ArcestrA.MxAccess.MxStatus pMxStatus); +``` + +**[Visual C++]** + +```cpp +HRESULT Suspend(long hLMXServerHandle, + long hItem, + MxStatus *pMxStatus); +``` + +### Parameters + +**hLMXServerHandle** +Handle of the connection + +**hItem** +Handle of the item + +Advanced Communication Management **Returns** + +MxStatus indicating success, and whether operation is pending. + +**Remarks** + +If the returned MxStatus indicates an error, it may be because the handle to the LMXServer or the handle to the item is not registered. If it is successful, check the category of the returned MxStatus, as the operation may still be pending. Upon completion, an OperationComplete event will be received, indicating whether the operation has completed successfully or there is a problem. + +To resume updates, call the Activate() method. + +**Examples** + +[Visual Basic.NET] + +Call LMX_Server.Suspend(hLMX, hItem, hItemStatus) + +[C#] + +LMX_Server.Suspend(hLMX, hItem, out ItemStatus); + +[Visual C++] + +hr = pLMX_Server->Suspend(hLMX, hItem, &ItemStatus); + +## Activate() Method + +Starts receiving updates without removing the subscription to the attribute. + +**Syntax** + +[Visual Basic.NET] + +```vbnet +Sub Activate(ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer, + ByRef pMxStatus As ArcestrA.MxAccess.MxStatus) +``` + +[C#] + +```csharp +void Activate(int hLMXServerHandle, + int hItem, + ArcestrA.MxAccess.MxStatus pMxStatus); +``` + +[Visual C++] + +```cpp +HRESULT Activate(long hLMXServerHandle, + long hItem, + MxStatus *pMxStatus); +``` + +**Parameters** + +* **hLMXServerHandle** + Handle of the connection +* **hItem** + Handle of the item + +**Returns** + +MxStatus indicating success, and whether operation is pending. + +**Remarks** + +If the returned MxStatus indicates an error, it may be because the handle to the LMXServer or the handle to the item is not registered. If it is successful, check the category of the returned MxStatus, as the operation may still be pending. Upon completion, an OperationComplete event will be received, indicating whether the operation has completed successfully or there is a problem. + +When either Suspend() or Activate() is called, the completion of the status change may take some time to process. When the processing is finished, an OperationComplete event is received, indicating which item has been suspended or activated, and whether the operation was successful. + +**Examples** + +[Visual Basic.NET] +```vbnet +Call LMX_Server.Activate(hLMX, hItem, ItemStatus) +``` +[C#] +```csharp +LMX_Server.Activate(hLMX, hItem, out ItemStatus); +``` +[Visual C++] +```cpp +hr = pLMX_Server->Activate(hLMX, hItem, &ItemStatus); +``` + +Handling the OperationComplete Callback # Handling the OperationComplete Callback + +The LMXProxyInterface emits an event for OperationComplete when your program invokes a Suspend() or Activate() function. The information your program receives identifies whether the change was successful and any error code information that may be appropriate. + +## OperationComplete Event + +Reports the status of an operation on an Attribute subscription, such as Suspend or Activate. + +### Syntax + +**[Visual Basic.NET]** + +```vbnet +Private Sub LMX_OperationComplete( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByVal pvItemValue As Object, + ByVal pwItemQuality As Integer, + ByVal pftItemTimeStamp As Object, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) +Handles LMX_Server.OperationComplete +``` + +**[C#]** + +```csharp +private void LMX_OperationComplete( + int hLMXServerHandle, + int phItemHandle, + object pvItemValue, + int pwItemQuality, + object pftItemTimeStamp, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[] ItemStatus +); +``` + +**[Visual C++]** + +```cpp +HRESULT CLMX_Events_Shim::OperationComplete( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + VARIANT pvItemValue, + long pwItemQuality, + VARIANT pftItemTimeStamp, + SAFEARRAY *pSAItemStatus +); +``` + +**Parameters** + +* **hLMXServerHandle** + Handle of the connection. +* **phItemHandle** + Handle of the item. +* **ItemStatus** + Array of Message Exchange statuses. + +**Returns** + +Nothing. + +**Remarks** + +For more information, including the function prototypes for the different language, see "Connecting Handlers for Events Reported from the LMXProxyServer" on page 19. + +Interpreting the OperationComplete status is very much like interpreting the data change status and involves examining the contents of the MxStatus structure. If the success member of the status is true, the Suspend() or Activate() operation has been accepted and completed. If the success member is false, check the other structure members for additional information about why the operation failed. + +**Examples** + +**[Visual Basic.NET]** + +```vbnet +If ItemStatus(0).success Then + CommunicationManagement_Status.Text = "Operation Complete - status OK" +Else + CommunicationManagement_Status.Text = "Operation Complete w/error - cat: " & ItemStatus(0).category & " Src: " & ItemStatus(0).detectedBy & " detail: " & ItemStatus(0).detail +End If +``` + +**[C#]** + +```csharp +if (ItemStatus[0].success != 0) { + CommunicationManagement_Status.Text = "Operation Complete - status OK"; +} else { + CommunicationManagement_Status.Text = "Operation Complete w/error - cat: " + ItemStatus[0].category + " Src: " + ItemStatus[0].detectedBy + " detail: " + ItemStatus[0].detail; +} +``` + +Writing Data Values [Visual C++] + +```c +if (ItemStatus[0].success != 0) { + SetDlgItemText(hMainDlg, IDC_CommunicationManagement_Status, + L"Operation Complete - status OK"); +} else { + TCHAR error_text[256]; + swprintf_s(error_text, + sizeof(error_text)/sizeof(TCHAR), L"Operation Complete + w/error - cat: %d Src: %d detail: %d", + ItemStatus[0].category, ItemStatus[0].detectedBy, + ItemStatus[0].detail); + SetDlgItemText(hMainDlg, + IDC_CommunicationManagement_Status, error_text); +} +``` + +# Writing Data Values + +To write data to an attribute, call the Write() method or the WriteSecured() method. + +To write data to an attribute, call the Write() method or the WriteSecured() method. + +To write data and a specific timestamp to an attribute, call the Write2() method or the WriteSecured2() method. + +To write to an item, the data must be in an object (or in C++ a VARIANT) of the proper internal type—integer, double, string, or other supported data types. + +String data can be used to write to an attribute of any data type, so long as the string can be converted to the appropriate data type. Otherwise, the MxStatus returned by the OnWriteComplete() event indicates an error. For example, writing "1/27/2014 03:43:02 PM" to an integer value generates an error. + +# Write() Method + +Writes data to an attribute. + +## Syntax + +[Visual Basic.NET] + +```vb +Sub Write( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer, + ByVal pItemValue As Object, + ByVal UserID As Integer +) +``` + +[C#] + +```csharp +void Write( + int hLMXServerHandle, + int hItem, + object pItemValue, + int UserID +); +``` + +[Visual C++] + +```cpp +HRESULT __stdcall Write( + long hLMXServerHandle, + long hItem, + VARIANT pItemValue, + long UserID +); +``` + +**Parameters** + +**hLMXServerHandle** +The handle of the connection. + +**hItem** +The handle of the item. + +**pItemValue** +The new data value. + +**UserID** +The ID "cookie" for the user. If ArchestrA security is not enabled, this value is -1 or 0. + +**Returns** + +None. + +**Remarks** + +Before you can write to an attribute, you must add the item to the internal table and put it on advise. For more information, see "AddItem() Method" on page 26 and "Advise() Method" on page 33. + +Upon completion of the write, your program receives notification of the success/failure status through the OnWriteComplete() event. + +When an item is written, that item should not be taken off advise or removed from the internal tables until the OnWriteComplete() event is received, indicating whether the write is successful or an error has occurred. + +Writing Data Values If the event indicates that an OnWriteComplete error has been detected, check the error code. + +* A code of 1008 indicates that the user does not have the proper security to write to this item. +* A code of 1012 indicates that a secured write is required. +* A code of 1013 indicates that a verified write is required. + +For more information, see "WriteSecured() Method" on page 47 and "AuthenticateUser() Method" on page 54. Also check the documentation on LMX details for other possible error codes. + +If a secured write or verified write is required, call the WriteSecured() method with the same item handle as was used for the Write() method. + +**Examples** + +[Visual Basic.NET] +```vbnet +Call LMX_Server.Write(hLMX, hItem, pItemValue, uindex1) +``` +[C#] +```csharp +LMX_Server.Write(hLMX, hItem, pItemValue, uindex1); +``` +[Visual C++] +```cpp +hr = pLMX_Server->Write(hLMX, hItem, pItemValue, uindex1); +``` + +## Write2() Method + +Writes data and a timestamp to an attribute. + +### Syntax + +[Visual Basic.NET] +```vbnet +Sub Write2( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer, + ByVal pItemValue As Object, + ByVal pItemTime As Object, + ByVal UserID As Integer +) +``` +[C#] +```csharp +void Write2( + int hLMXServerHandle, + int hItem, + object pItemValue, + object pItemTime, + int UserID +); +``` + +[Visual C++] + +```c +HRESULT __stdcall Write2( + long hLMXServerHandle, + long hItem, + VARIANT pItemValue, + VARIANT pItemTime, + long UserID +); +``` + +**Parameters** + +**hLMXServerHandle** +The handle of the connection. + +**hItem** +The handle of the item. + +**pItemValue** +The new data value. + +**pItemTime** +The new timestamp. + +**UserID** +The ID "cookie" for the user. If ArcestrA security is not enabled, this value is -1 or 0. + +**Returns** + +None. + +**Remarks** + +Before you can write to an attribute, you must add the item to the internal table and put it on advise. For more information, see "AddItem() Method" on page 26 and "Advise() Method" on page 33. + +Upon completion of the write, your program receives notification of the success/failure status through the OnWriteComplete() event. + +The Write2() method is on the ILMXProxyServer4 interface, which extends the ILMXProxyServer interface. + +If a secured write or verified write with a timestamp is required, call the WriteSecured2() method with the same item handle as was used for the Write2() method. + +**Examples** + +[Visual Basic.NET] + +```vb +Call LMX_Server.Write2(hLMX, hItem, pItemValue, pItemTime, uindex1) +``` + +[C#] + +```csharp +LMX_Server.Write2(hLMX, hItem, pItemValue, pItemTime, uindex1); +``` + +Writing Data Values [Visual C++] +```c++ +hr = pLMX_Server->Write2(hLMX, hItem, pItemValue, pItemTime, + uindex1); +``` + +# WriteSecured() Method + +Writes data to an attribute that requires authentication, either as a secured write or as a verified write. + +## Syntax + +[Visual Basic.NET] +```vb +Sub WriteSecured( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer, + ByVal CurrentUserID As Integer, + ByVal VerifiedUserID As Integer, + ByVal pItemValue As Object +) +``` +[C#] +```csharp +void WriteSecured( + int hLMXServerHandle, + int hItem, + int CurrentUserID, + int VerifiedUserID, + object pItemValue +); +``` +[Visual C++] +```c++ +HRESULT __stdcall WriteSecured( + long hLMXServerHandle, + long hItem, + long CurrentUserID, + long VerifiedUserID, + VARIANT pItemValue +); +``` + +## Parameters + +**hLMXServerHandle** +* Handle of the connection. + +**hItem** +* Handle of the item. + +**CurrentUserID** +* ID "cookie" for the current user. + +**VerifiedUserID** +ID "cookie" for the verified user, or -1 or 0 if only a secured write is being done. + +**pItemValue** +The new data. + +**Returns** +Nothing. + +**Remarks** +If the attribute requires a secured write or verified write, you write to it with the WriteSecured() method. A secured write is analogous to submitting a change request that requires one signature, while a verified write corresponds to a change request that requires two signatures. + +Each user must be authenticated by name and password, thereby obtaining the ID "cookie." For more information, see "AuthenticateUser() Method" on page 54. + +Before you can write to an attribute, you must add the item to the internal table and put it on advise. For more information, see "AddItem() Method" on page 26 and "Advise() Method" on page 33. + +Upon completion of the write, your program receives notification of the success/failure status through the OnWriteComplete() event. + +**Examples** + +[Visual Basic.NET] +```vb +Call LMX_Server.WriteSecured(hLMX, hItem, uindex1, uindex2, + pItemValue) +``` + +[C#] +```csharp +LMX_Server.WriteSecured(hLMX, hItem, uindex1, uindex2, + pItemValue); +``` + +[Visual C++] +```cpp +hr = pLMX_Server->WriteSecured(hLMX, hItem, uindex1, uindex2, + pItemValue); + +Writing Data Values # WriteSecured2() Method + +Writes data and timestamp to an attribute that requires authentication, either as a secured write or as a verified write. + +## Syntax + +**[Visual Basic.NET]** + +```vbnet +Sub WriteSecured2 ( + ByVal hLMXServerHandle As Integer, + ByVal hItem As Integer, + ByVal CurrentUserID As Integer, + ByVal VerifiedUserID As Integer, + ByVal pItemValue As Object, + ByVal pItemTime As Object +) +``` + +**[C#]** + +```csharp +void WriteSecured2 ( + int hLMXServerHandle, + int hItem, + int CurrentUserID, + int VerifiedUserID, + object pItemValue, + object pItemTime +); +``` + +**[Visual C++]** + +```cpp +HRESULT __stdcall WriteSecured2 ( + long hLMXServerHandle, + long hItem, + long CurrentUserID, + long VerifiedUserID, + VARIANT pItemValue, + VARIANT pItemTime +); +``` + +## Parameters + +**hLMXServerHandle** +Handle of the connection. + +**hItem** +Handle of the item. + +**CurrentUserID** +ID "cookie" for the current user. + +**VerifiedUserID** +ID "cookie" for the verified user, or -1 or 0 if only a secured write is being done. + +pItemValue +The new data. + +pItemTime +The new timestamp. + +**Returns** +Nothing. + +**Remarks** +If the attribute requires a secured write or verified write, you write to it with the WriteSecured2() method. A secured write is analogous to submitting a change request that requires one signature, while a verified write corresponds to a change request that requires two signatures. + +Each user must be authenticated by name and password, thereby obtaining the ID "cookie." For more information, see "AuthenticateUser() Method" on page 54. + +Before you can write to an attribute, you must add the item to the internal table and put it on advise. For more information, see "AddItem() Method" on page 26 and "Advise() Method" on page 33. + +Upon completion of the write, your program receives notification of the success/failure status through the OnWriteComplete() event. + +The WriteSecured2() method is on the ILMXProxyServer4 interface, which extends the ILMXProxyServer interface. + +**Examples** + +[Visual Basic.NET] +```vbnet +Call LMX_Server.WriteSecured2 (hLMX, hItem, uindex1, +uindex2, pItemValue, pItemTime) +``` + +[C#] +```csharp +LMX_Server.WriteSecured2 (hLMX, hItem, uindex1, +uindex2, pItemValue, pItemTime); +``` + +[Visual C++] +```cpp +hr = pLMX_Server->WriteSecured2 (hLMX, hItem, +uindex1, uindex2, pItemValue, pItemTime); +``` + +Writing Data Values # Writing to a Single Element + +When writing to a single ArchestrA element—that is, when you are not writing to several members of an array—it is possible to pass a string object (or VARIANT) to the Write() or WriteSecured() method: + +[Visual Basic.NET] +```vb +Dim vValue as String +Call LMX_Server.Write(hLMX, hItem, vValue, uindex1) +``` + +[C#] +```csharp +string vValue; +LMX_Server.Write(hLMX, hItem, vValue, uindex1); +``` + +[Visual C++] +```cpp +TCHAR sValue; + ... // set value of sValue to string + // representing the new value +VARIANT varItem; + if (ItemAsVariant (&varItem, sValue) { + hr = pLMX_Server->Write(hLMX, hItem, varItem, uindex1); + } +``` + +# Writing to an Array + +When writing to an array of ArchestrA elements, you must build an object (or in C++ a VARIANT) that contains the data as an array. It can be an array of strings or it can be an array of the actual data type. + +For more information on implementing the handling in the ItemArrayAsVariant() and ReleaseItemArrayAsVariant() methods, see "Encapsulating Data Values in Visual C++" on page 52. + +[Visual Basic.NET] +```vb +Dim vArray(2) as String +vArray(0) = vValue1.Text +vArray(1) = vValue2.Text +vArray(2) = vValue3.Text +Call LMX_Server.Write(hLMX, hItem, vArray, uindex1) +``` + +[C#] +```csharp +string[] vArray = { vValue1.Text, vValue2.Text, vValue3.Text }; +LMX_Server.Write(hLMX, hItem, vArray, uindex1); +``` + +[Visual C++] + +```c++ +#define INTARRAY_LEN 256 +typedef TCHAR INTARRAY_ELEM [INTARRAY_LEN] +INTARRAY_ELEM intarray[3]; + +... // set values of intarray[0], intarray[1], //intarray[2] to strings representing new values + +VARIANT varArray; + +// build SAFEARRAY and put it into varArray +// (see example of implementation below) + +if (ItemArrayAsVariant (&varArray, VT_BSTR, intarray, 3)) +{ + hr = pLMX_Server->Write(hLMX, hItem, varItem, uindex1); + ReleaseItemArrayAsVariant (&varItem, 3); +} +``` + +# Encapsulating Data Values in Visual C++ + +To encapsulate the new data values as a SAFEARRAY, the simplest approach is to let the VARIANT mechanism handle the data conversion to the appropriate data type. Note that there is a slight difference on how the data should be handled if strings (BSTRs) are being stored in the SAFEARRAY. You must copy the string pointer into the entry, instead of the actual value. + +The following example shows how to convert the new entries and put them into a SAFEARRAY. This same function is also included in the sample code for C++. + +```c++ +bool ItemArrayAsVariant (VARIANT* pvarItems, + VARTYPE datatype, + INTARRAY_ELEM* intarray, long NumEntries) + +{ + bool bRetVal = false; + long i; + HRESULT hr = S_OK; + VariantInit (pvarItems); + // set up a safe array to hold the data + SAFEARRAYBOUND rgsabound[1]; + rgsabound[0].lBound = 0; + rgsabound[0].cElements = NumEntries; + SAFEARRAY *psa = SafeArrayCreate (datatype, 1, rgsabound); + if (psa) + { + // create entries + VARIANT varEntry; +``` + +Encapsulating Data Values in Visual C++ ```c++ +for (i = 0; (i < NumEntries) && (SUCCEEDED(hr)); i++) { + VariantInit(&varEntry); + varEntry.vt = VT_BSTR; + varEntry.bstrVal = ::SysAllocString(intarray[i]); + switch (datatype) { + case VT_BSTR : { + SafeArrayPutElement(psa, &i, (void *)varEntry.bstrVal); + } + break; + default : { + hr = VariantChangeType(&varEntry, + &varEntry, 0, datatype); + // since VARIANT is a union, just + // point to where value is stored + SafeArrayPutElement(psa, &i, (void *)&varEntry.lVal); + } + break; + } // switch // + // free the temporary string + if (varEntry.vt == VT_BSTR) { + ::SysFreeString(varEntry.bstrVal); + } +} +// modify the flags in fFeatures +if (datatype == VT_BSTR) { + psa->fFeatures |= (unsigned short) FADF_BSTR; +} else { + psa->fFeatures = 0; +} +if (SUCCEEDED(hr)) { + // attach the array to the variant + pvarItems->parray = psa; + pvarItems->vt = (unsigned short) (datatype | VT_ARRAY); + // indicate successfully created + bRetVal = true; +} +return (bRetVal); +} +``` + +```c +void ReleaseItemArrayAsVariant (VARIANT* pvarItems, long NumEntries) +{ + UNREFERENCED_PARAMETER(NumEntries); + SAFEARRAY *psa = pvarItems->parray; + if (psa) { + SafeArrayDestroy(psa); + } +} +``` + +# Authenticating Users + +Some ArchestraA data is secured. The entire Galaxy can have security enabled and individual attributes can be secured in such a way that they can be updated only if the user has certain login credentials. Some attributes require a verified write, which means that two different users must confirm that the update should take place. This involves calling the AuthenticateUser() method, which asks the Galaxy to check a user's credentials and return an ID "cookie" that can be used in the Write() and WriteSecured() method calls. + +# AuthenticateUser() Method + +## Syntax + +**[Visual Basic.NET]** + +```vb +Function AuthenticateUser( + ByVal hLMXServerHandle As Integer, + ByVal VerifyUser As String, + ByVal VerifyUserPsw As String +) + As Integer +``` + +**[C#]** + +```csharp +int AuthenticateUser( + intLMXServerHandle, + string VerifyUser, + string VerifyUserPsw +); +``` + +**[Visual C++]** + +```cpp +HRESULT __stdcall AuthenticateUser( + long hLMXServerHandle, + BSTR VerifyUser, + BSTR VerifyUserPsw, + long *UserId +); +``` + +Authenticating Users **Parameters** + +* **hLMXServerHandle** + Handle of the connection. +* **VerifyUser** + User account name. +* **VerifyUserPsw** + User password. + +**Returns** + +ID "cookie" for the user, or zero if the user's credentials are rejected. + +**Remarks** + +The AuthenticateUser() method gives a name and password to the Galaxy for authorization. If the user name and password are valid, a GUID is generated for the log-in. However, instead of returning this GUID to the caller, the LMXProxyServer stores the GUID in its internal tables and assigns it an integer index—an ID "cookie"—that can be used in subsequent calls to other methods. If the user's credentials are invalid (wrong name, wrong password or both), the underlying COM function returns HRESULT 0x80070057 indicating that the parameters are invalid. In C#, this will throw a System.ArgumentException; and in VB it will throw an error which is identified as Err.Number 5. Your program should be prepared to check the HRESULT or handle the exception, and indicate that the user's credentials are invalid. + +**Examples** + +[Visual Basic.NET] +```vbnet +uindex1 = LMX_Server.AuthenticateUser(hLMX, uidText, pwdText) +``` +[C#] +```csharp +uindex1 = LMX_Server.AuthenticateUser(hLMX, uidText, pwdText); +``` +[Visual C++] +```cpp +BSTR bstrUser = ::SysAllocString(szUserId); +BSTR bstrPwd = ::SysAllocString(szUserPwd); +hr = pLMX_Server->AuthenticateUser(hLMX, bstrUser, bstrPwd, + &uindex1); +::SysFreeString(bstrPwd); +::SysFreeString(bstrUser); +``` + +# Using an Established User Authentication + +When your program calls the AuthenticateUser() method, LMX authenticates the user name and password and gets a GUID from the ArchestrA Galaxy that is valid for the current session. To simplify use in MXAccess, the LMXProxyServer then places this GUID in an internal table and associates it with an integer ID "cookie" so that the interfaces do not need to pass a GUID as a parameter every time an operation such as the Write() method or WriteSecured() is invoked. In short, the GUID is obtained, but your program never sees it. + +If your program is a component hosted in InTouch—for example, an ActiveX object hosted in an InTouch application window—the policy is for the InTouch HMI to handle the authentication process, using its own log-in dialog boxes. In particular, WindowViewer presents a log-in dialog box when: + +* An operator logs into the InTouch application. +* A secured write is to be performed. +* A verified write is to be performed. + +WindowViewer keeps track of the GUIDs for these users. For your program to use these GUIDs to perform secured writes and verified writes, you must add them to the internal tables for the LMXProxyServer. Do this by calling the ArchestrAUserId() method, which passes the GUID to the LMXProxyServer, checks it for validity, and returns the corresponding ID cookie. You can then use this cookie with the other interface methods, such as the Write() and WriteSecured() method. + +Note that this method is on the ILMXProxyServer2 interface, which extends the ILMXProxyServer interface. + +## ArchestrAUserId() Method + +Adds a GUID to the internal table and returns a user ID. + +### Syntax + +[Visual Basic.NET] + +```vbnet +Function ArchestrAUserId( + ByVal hLMXServerHandle As Integer, + ByVal UserIdGuid As String +) +As Integer +``` + +Using an Established User Authentication [C#] + +```csharp +int ArcestrAUserToId( + int hLMXServerHandle, + string UserIdGuid); +``` + +[Visual C++] + +```cpp +HRESULT __stdcall ArcestrAUserToId( + long hLMXServerHandle, + BSTR UserIdGuid, long *UserId +); +``` + +**Parameters** + +**hLMXServerHandle** +Handle of the connection. + +**UserIdGuid** +String containing the GUID for the user ID. + +**Returns** + +User ID cookie, or zero if the user credentials are rejected. + +**Remarks** + +This method is used primarily in the context of a component hosted by the InTouch HMI. The InTouch HMI is written mostly in unmanaged code and therefore does not generally support hosting .NET visual components in an InTouch application window. However, an ActiveX component can be hosted in an InTouch window. Code samples in this Toolkit include an example in C++, implementing a simple ActiveX component that uses this interface. + +Every time you call the ArcestrAUserToId() method with a valid GUID, the GUID is added to the internal table and a new ID is returned. If you add the same GUIDs over and over again, the table continues to grow and previous IDs returned for those same GUIDs can still be used to perform Write() and WriteSecured() operations. + +To minimize duplicate entries, your program can maintain a map of GUIDs already submitted to the ArcestrAUserToId() method and their corresponding ID cookies and submit a GUID only if it has not already been used. + +**Examples** + +[Visual Basic.NET] + +```vbnet +uindex1 = LMX_Server.ArcestrAUserToId(hLMX, uidGuid) +``` + +[C#] + +```csharp +uindex1 = LMX_Server.ArcestrAUserToId(hLMX, uidGuid); +``` + +[Visual C++] + +```c++ +BSTR bstrGuid = ::SysAllocString(szUserIdGuid); +hr = pLMX_Server->ArchestrAUserToId(hLMX, bstrGuid, &uindex1); +::SysFreeString(bstrGuid); +``` + +# Obtaining User Authentication from InTouch + +When a component hosted by the InTouch HMI requires a user log-in, as for a secured write or verified write, let the InTouch HMI handle the authentication using its own user log-in dialog boxes. There are two ways to get user GUIDs from the InTouch HMI: + +* Call the get_LoggedInUserGuid() method to retrieve the GUID for the user currently logged in, if any. +* Call the LogInInTouchUser() method to show a confirmation log-in from the currently logged-in user (and, if appropriate, a verification user). + +These are not methods of the MXAccess Toolkit, but are instead methods provided by the InTouch HMI itself, through the AppServerSecurity.dll. + +To access the AppServerSecurity methods, you must import a reference to the AppServerSecurity.dll into your program. + +[Visual C++] + +* In the file stdafx.h, add a line such as the following to import the dll: + +```c++ +#import "C:\Program Files\Wonderware\InTouch\AppServerSecurity.dll" +no_namespace, raw_interfaces_only +``` +* When you compile, Visual Studio creates a file AppServerSecurity.tlh. You can open this file to see the interface methods and other members provided by the AppServerSecurity interface. + +In addition to the methods described above, the DLL includes the following enumerations for the LogInInTouchUser() method: + +```c++ +enum WriteType +{ + WriteType_Undefined = 0, + WriteType_VerifiedWrite = 1, + WriteType_SecuredWrite = 2 +}; +``` + +```c++ +enum LoginDetailError { + LoginDetailError_NoError = 0, + LoginDetailError_SecuredLoginFailed = 1, + LoginDetailError_VerifierLoginFailed = 2 +}; + +To retrieve the GUID of the currently logged-in user, call the get_LoggedInUserGuid() method. + +[Visual C++] + +## Syntax + +```c++ +HRESULT get_LoggedInUserGuid (BSTR * loggedInUserId); +``` + +## Example + +```c++ +// get GUID from InTouch for user +// currently logged in, if any +CComPtr pInTouchLogin; +HRESULT hr = pInTouchLogin.CoCreateInstance( + _uuidof( CurrentInTouchSecurity )); +if (SUCCEEDED(hr)) +{ + CComBSTR bstrGUID; + hr = pInTouchLogin->get_LoggedInUserGuid(&bstrGUID); + if (SUCCEEDED(hr)) + { + // add GUID to internal table + if ((m_pPLMX_Server != NULL) && (m_hLMX != 0)) + { + long tempIndex = 0; + hr = pLMX_Server->ArchestrAUserToId(m_hLMX, + bstrGUID, &tempIndex); + if (SUCCEEDED(hr) && (tempIndex != 0)) + { + // use tempIndex as new user index + ... + } + } + } +} +``` + +To invoke an InTouch log-in dialog and get GUIDs for the currently logged-in user (if any) and a verification user (if appropriate), call the LogInInTouchUser() method. + +[Visual C++] + +## Syntax + +```c++ +HRESULT LogInInTouchUser ( enum WriteType typeOfWrite, + BSTR * userId, + BSTR * verifierId, + VARIANT_BOOL * success, + enum LoginDetailError * detailCode); +``` + +## Example + +```c++ +// get GUID(s) from InTouch by invoking +// log-in dialog +CComPtr pInTouchLogin; +HRESULT hr = pInTouchLogin.CoCreateInstance( + _uuidof( CurrentInTouchSecurity )); +if (SUCCEEDED(hr)) +{ + WriteType typeOfWrite = + WriteType_VerifiedWrite; + CComBSTR bstrUserID; + CComBSTR bstrVerifierID; + VARIANT_BOOL varbSuccess = VARIANT_FALSE; + LoginDetailError detailCode = + LoginDetailError_NoError; + // determine type of login required + if (bNeedVerifiedWrite) + typeOfWrite = WriteType_VerifiedWrite; + else + typeOfWrite = WriteType_SecuredWrite; + // Ask InTouch to get user login(s) and + // return GUIDs + hr = pInTouchLogin->LogInInTouchUser(typeOfWrite, + &bstrUserID, + &bstrVerifierID, &varbSuccess, &detailCode); + if (SUCCEEDED(hr)) + { + // get status information + bool bSuccess = + (varbSuccess != VARIANT_FALSE); + if (bSuccess) + { + // GUID(s) obtained + // Add them to internal tables, + // get corresponding ID(s) + if ((pLMX_Server != NULL) && (hLMX != 0)) +``` + +```c++ +{ + long tempIndex1 = 0; + long tempIndex2 = 0; + hr = + m_pLMX_Server->ArchestrAUserToId(m_hLMX, + bstrUserID, &tempIndex1); + if (SUCCEEDED(hr) && + (typeOfWrite == WriteType_VerifiedWrite)) + { + hr = + m_pLMX_Server->ArchestrAUserToId(m_hLMX, + bstrVerifierID, &tempIndex2); + } + // now use tempIndex1, + // tempIndex2 to update the IDs + if (tempIndex1 != 0) + // secured user + uindex1 = tempIndex1; + if (tempIndex2 != 0) + // verifier user + uindex2 = tempIndex2; + } +} +else +{ + // handle error or indicate problem, + // using returned detailCode for + // further information + ... +} +} +} + +The new user ID "cookies" may now be used in the WriteSecured() +method. +``` + +# Handling the OnDataChange Callback + +The LMXProxyInterface triggers an OnDataChange event when an update occurs to an ArcestrA Attribute that your program has on advise. The information your program receives contains the updated data values and status information. If appropriate, it may also provide error code information. + +## OnDataChange Event + +Reports the value and the status of an ArcestrA Attribute to which the program has subscribed. + +### Syntax + +**[Visual Basic.NET]** + +```vbnet +Private Sub LMX_OnDataChange( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByVal pvItemValue As Object, + ByVal pwItemQuality As Integer, + ByVal pftItemTimeStamp As Object, + ByRef ItemStatus() As ArcestrA.MxAccess.MXSTATUS_PROXY +) +``` + +```vbnet +Handles LMX_Server.OnDataChange +``` + +**[C#]** + +```csharp +private void LMX_OnDataChange ( + int hLMXServerHandle, + int phItemHandle, + object pvItemValue, + int pwItemQuality, + object pftItemTimeStamp, + ref ArcestrA.MxAccess.MXSTATUS_PROXY[] ItemStatus +); +``` + +**[Visual C++]** + +```cpp +HRESULT CLMX_Events_Shim::OnDataChange ( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + VARIANT pvItemValue, + long pwItemQuality, + VARIANT pftItemTimeStamp, + SAFEARRAY *pSAItemStatus +); +``` + +Handling the OnBufferedDataChange Callback **Parameters** + +* **hLMXServerHandle** + * Handle of the connection +* **phItemHandle** + * Handle of the item +* **pvItemValue** + * New data value +* **pwItemQuality** + * New quality +* **pftItemTimeStamp** + * New timestamp +* **ItemStatus** + * Array of Message Exchange statuses + +**Returns** + +Nothing. + +**Remarks** + +For more information, including the function syntax for the different languages, see "Connecting Handlers for Events Reported from the LMXProxyServer" on page 19. + +# Handling the OnBufferedDataChange Callback + +You can configure your client program to subscribe to buffered attributes. Your client program can also be configured to receive data change events for those attributes. + +When LMXProxy passes buffered data back to the client application, it builds three parallel SafeArray Variants, one each for: + +* Value (V) +* Timestamp (T) +* Quality (Q) + +# OnBufferDataChange Event + +Reports the VTQ buffer and the status of an ArchestrA Attribute to which the program has subscribed. + +## Syntax + +**[Visual Basic.NET]** + +```vb +Sub LMX_OnBufferDataChange( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByVal dtDataType As ArchestrA.MxAccess.MxDataType, + ByVal pvItemValue As Object, + ByVal pwItemQuality As Object, + ByVal pftItemTimeStamp As Object, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) + Handles LMX_Server.OnBufferDataChange +``` + +**[C#]** + +```csharp +LMX_OnBufferDataChange( + int hLMXServerHandle, + int phItemHandle, + ArchestrA.MxAccess.MxDataType dtDataType, + object pvItemValue, object pvItemQuality, + object pvItemTimeStamp, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[] ItemStatus) +``` + +**[Visual C++]** + +```cpp +HRESULT OnBufferDataChange ( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + MxDataType dtDataType, + VARIANT pvItemValue, + VARIANT pwItemQuality, + VARIANT pftItemTimeStamp, + SAFEARRAY *pSAItemStatus) +``` + +## Parameters + +**hLMXServerHandle** +Handle of the connection. + +**phItemHandle** +Handle of the item. + +**dtDataType** +Data type of the buffered data. + +**pvItemValue** +New data value. + +Determining Data Change Status * *pwItemQuality* + New quality. +* *pftItemTimeStamp* + New timestamp. +* *ItemStatus* + Array of Message Exchange statuses. + +**Returns** + +Nothing. + +**Remarks** + +You must note that when your client program attempts a buffered subscription to an attribute that does not have buffering enabled, a data change event occurs. This data change event message will have an MXStatus category of **MXCategoryConfigurationError** and will have a detail code of **MX_E_InvalidPropertyId**. + +For more information, including the function syntax for the different languages, see "Connecting Handlers for Events Reported from the LMXProxyServer" on page 19. + +# Determining Data Change Status + +If the data change notification indicates success, then you should process the received data. Otherwise, you should notify the user or log the error, as is appropriate for your application. + +[Visual Basic.NET] + +```vbnet +Dim sErrorStatus As String + +If ItemStatus(0).success Then + ' process data, update displays, etc. +Else + sErrorStatus = "Data Change Error - C:" & _ + ItemStatus(0).category & " S:" & ItemStatus(0).detectedBy & _ + " D:" & ItemStatus(0).detail +End If +``` + +[C#] + +```csharp +if (ItemStatus[0].success != 0) { + // process data, update displays, etc. +} else { + string sErrorStatus; + sErrorStatus = "Data Change Error - C:" + + ItemStatus[0].category + " S:" + ItemStatus[0].detectedBy + + " D:" + ItemStatus[0].detail; +} +``` + +[Visual C++] + +MXSTATUS_PROXY *ItemStatus; + +hr = SafeArrayAccessData(pSAItemStatus, (void HUGEP* FAR*)&ItemStatus); + +if (ItemStatus[0].success != 0) { + // process data, update displays, etc. +} else { + TCHAR sErrorStatus [256]; + swprintf_s (sErrorStatus, + sizeof(sErrorStatus)/sizeof(TCHAR), L"OnDataChange w/error - cat: %d Src: %d detail: %d", ItemStatus[0].category, ItemStatus[0].detectedBy, ItemStatus[0].detail); +} + +hr = SafeArrayUnaccessData(pSAItemStatus); + +# Displaying Data Quality and Time + +The new data value has an associated Quality, which is an integer, and represents whether the data is good, bad, pending, and initializing. The new data value also has an associated timestamp, which represents when the data was updated. These properties can be easily displayed in .NET. For displaying the Time in Visual C++, refer to the section below on Retrieving Data Types in Visual C++. + +## Syntax + +[Visual Basic.NET] + +```vbnet +Private Sub LMX_OnDataChange( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByVal pvItemValue As Object, + ByVal pwItemQuality As Integer, + ByVal pftItemTimeStamp As Object, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) +Handles LMX_Server.OnDataChange +``` + +## Example + +```vbnet +Quality.Text = pwItemQuality +Time.Text = pftItemTimeStamp +``` + +Displaying Data Quality and Time [C#] + +```csharp +private void LMX_OnDataChange( + int hLMXServerHandle, + int phItemHandle, + object pvItemValue, + int pwItemQuality, + object pftItemTimeStamp, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[ ] ItemStatus +); +``` + +**Example** + +```csharp +Quality.Text = pwItemQuality.ToString(); +Time.Text = pftItemTimeStamp.ToString(); +``` + +[Visual C++] + +```cpp +HRESULT CLMX_Events_Shim::OnDataChange ( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + VARIANT pvItemValue, + long pwItemQuality, + VARIANT pftItemTimeStamp, + SAFEARRAY *pSAIItemStatus +); +``` + +**Example** + +```cpp +swprintf_s (strDisplayedText, TotalLen, L"%d", pwItemQuality); +SetDlgItemText (hMainDlg, IDC_Quality, strDisplayedText); +Variant_to_String (&pftItemTimeStamp, strDisplayedText, +sizeof(strDisplayedText)/sizeof(TCHAR)); +SetDlgItemText (hMainDlg, IDC_Time, strDisplayedText); +``` + +[ActiveX] + +**Example** + +```cpp +swprintf_s (strDisplayedText, TotalLen, L"%d", pwItemQuality); +m_pMxAccessAxDlg->SetDlgItemText(IDC_Quality, +strDisplayedText); +Variant_to_String (&pftItemTimeStamp, strDisplayedText, +sizeof(strDisplayedText)/sizeof(TCHAR)); +m_pMxAccessAxDlg->SetDlgItemText(IDC_Time, strDisplayedText); +``` + +# Determining Data Type + +Because the new data value is encapsulated as an object (or in Visual C++ as a VARIANT), it is necessary to cast the new data value to the appropriate data type to process it. If the data is an array of values, you must un-package the object and treat it as an array of the appropriate type. This is easily handled in .NET, but requires more effort in Visual C++. + +[Visual Basic.NET] + +```vbnet +Dim objectType As Type +Dim count As Integer + +' check whether data is array or single item +objectType = pvItemValue.GetType() + +If Not objectType.IsArray Then + ' received data is a single entry, handle it + ' do something with pvItemValue + ... +Else + ' received data is an array, + ' handle all the values + For count = 0 To UBound(pvItemValue, 1) Step 1 + ' do something with pvItemValue(count) + ... + Next count +End If +``` + +[C#] + +```csharp +Type objectType; +int count; + +objectType = pvItemValue.GetType(); + +if (!objectType.IsArray) { + // received data is a single entry, handle it + // do something with pvItemValue + ... +} +``` + +Determining Data Type ```csharp +} else { + // received data is an array, re-cast data type + // and handle all the values + Array pvArray = (Array) pvItemValue; + for (count = pvArray.GetLowerBound(0); + count < pvArray.GetUpperBound(0); + count++) { + // do something with pvArray[count] + ... + } +} +``` + +[Visual C++] + +```c +VARTYPE Variant_Type; + +long count; + +Variant_Type = pvItemValue.vt; + +if ((Variant_Type & VT_ARRAY) == 0) { + // received data is a single entry, handle it + // do something with pvItemValue + ... +} else { + // received data is an array, + // re-cast data type and handle all the values + Variant_Type = (VARTYPE) (Variant_Type ^ VT_ARRAY); + SAFEARRAY *pvArray = pvItemValue.parray; + SAFEARRAYBOUND* pSABound = pvArray->rgsabound; + long LowerBound = pSABound->lbound; + long UpperBound = pSABound->cElements + LowerBound; + for (count = LowerBound; count < UpperBound; count++) { + // do something with pvArray at index count + // see example implementation below + Process_SafeArrayElement (pvArray, Variant_Type, count, + ...); + } +} +``` + +For more information about implementing handling in the `Process_SafeArrayElement()` function, see "Retrieving Data Types in Visual C++" on page 70. + +When you have the new data value in the appropriate form, you can use it and the array of Message Exchange statuses to update the values that are tracked by your program. What your program does with status information up to you—whether to update status displays, perform error handling, and so on. + +# Retrieving Data Types in Visual C++ + +The most reliable way to retrieve data from a SAFEARRAY of values is to use the SafeArrayGetElement() method. However, this method call must be made with a data variable of the appropriate type, so it can be processed correctly. For an array of BSTR, it is slightly more efficient to access the entire array and reference the individual strings by index. + +There are several ways to handle the DATE data type. The following are two simple approaches to coerce the value to a string: + +* Use the VariantChangeType() function to convert the DATE to a BSTR. +* Use the VariantTimeToDosDateTime() function to convert the DATE to a DOS date and a DOS time, which can then be formatted into a string. + +The following code example shows how to extract entries from the SAFEARRAY. This function is included in the sample code for C++. + +```c++ +void SafeArrayElement_to_String(SAFEARRAY *pvArray, VARTYPE datatype, long count, LPTSTR szValue, size_t BufLen) +{ + switch (datatype) + { + case VT_BOOL : + { + VARIANT varBoolItem; + VariantInit(&varBoolItem); + SafeArrayGetElement(pvArray, &count, &varBoolItem); + swprintf_s(szValue, BufLen, L"%s", + (varBoolItem.boolVal ? L"true" : L"false")); + } + break; + case VT_I4 : + { + long iLongElement = 0; + SafeArrayGetElement(pvArray, &count, &iLongElement); + swprintf_s(szValue, BufLen, L"%ld", iLongElement); + } + break; + case VT_R8 : + { + double iDoubleElement = 0.0; + SafeArrayGetElement(pvArray, &count, + &iDoubleElement); + swprintf_s(szValue, BufLen, L"%f", iDoubleElement); + } + break; + } +} +``` + +Retrieving Data Types in Visual C++ ```c++ +case VT_R4 : +{ + float iFloatElement = 0.0; + SafeArrayGetElement (pvArray, &count, &iFloatElement); + swprintf_s (szValue, BufLen, L"%f", iFloatElement); +} +break; +case VT_DATE : +{ + DATE dtElement; + SafeArrayGetElement (pvArray, &count, &dtElement); + if (bDisplayDateUsingVariant) + { + // One way to display is to let + // the VARIANT mechanism handle it + VARIANT varDate; + VariantInit (&varDate); + varDate.vt = VT_DATE; + varDate.date = dtElement; + HRESULT hr = VariantChangeType (&varDate, &varDate, + 0, VT_BSTR); + if (SUCCEEDED(hr)) + { + wscpy_s (szValue, BufLen, varDate.bstrVal); + ::SysFreeString(varDate.bstrVal); + } else { + wcscpy_s (szValue, BufLen, L"????"); + } + } + else + { + // Another way is to extract the + // date/time fields explicitly + // note that a DOS time is only + // accurate to a resolution of + // 2 seconds + unsigned short wDosDate; + unsigned short wDosTime; + if (VariantTimeToDosDateTime (dtElement, &wDosDate, + &wDosTime)) + { + int da = wDosDate & 0x001F; + int mo = (wDosDate & 0x01E0) >> 5; + int yr = ((wDosDate & 0xFE00) >> 9) + 1980; + int sc = (wDosTime & 0x001F) << 1; + int mn = (wDosTime & 0x07E0) >> 5; + int hr = (wDosTime & 0xF800) >> 11; + swprintf_s (szValue, BufLen, + L"%02d/%02d/%04d %02d:%02d", + mo, da, yr, hr, mn, sc); + } + } +``` + +```c +else +{ + wcscpy_s (szValue, BufLen, L"????"); +} +} +break; +case VT_BSTR : +{ + BSTR HUGE *pbstr; + HRESULT hr = SafeArrayAccessData(pvArray, (void +HUGE**) &pbstr); + swprintf_s (szValue, BufLen, L"%s", pbstr[count]); + hr = SafeArrayUnaccessData(pvArray); +} +break; +default : +{ + wcscpy_s (szValue, BufLen, L"????"); +} +break; +} // switch // +} +``` + +# Handling the OnWriteComplete Callback + +The LMXProxyInterface triggers an event for OnWriteComplete when your program calls the Write() or WriteSecured() function. The information your program receives identifies whether the write was successful and any error code information that may be appropriate. + +## OnWriteComplete Event + +Reports the status when a write operation has been completed, indicating such information as whether it succeeded or failed. + +### Syntax + +[Visual Basic.NET] + +```vb +Private Sub LMX_OnWriteComplete( + ByVal hLMXServerHandle As Integer, + ByVal phItemHandle As Integer, + ByRef ItemStatus() As ArchestrA.MxAccess.MXSTATUS_PROXY +) + Handles LMX_Server.OnWriteComplete +``` + +Handling the OnWriteComplete Callback [C#] + +```csharp +private void LMX_OnWriteComplete( + int hLMXServerHandle, + int phItemHandle, + ref ArchestrA.MxAccess.MXSTATUS_PROXY[ ] ItemStatus +); +``` + +[Visual C++] + +```cpp +HRESULT CLMX_Events_Shim::OnWriteComplete( + long hLMXServerHandle, + ITEMHANDLE phItemHandle, + SAFEARRAY *pSAWriteItemStatus +); +``` + +where CLMX_Events_Shim is a class derived from the base class LMXProxyServerEvents. + +## Parameters + +**hLMXServerHandle** +Handle of the connection. + +**phItemHandle** +Handle of the item. + +**ItemStatus** +Array of Message Exchange statuses. + +## Returns + +Nothing + +## Remarks + +For more information, including the function syntax for the different languages, see "Connecting Handlers for Events Reported from the LMXProxyServer" on page 19. + +Interpreting the write status is very much like interpreting the data change status and involves examining the contents of the MxStatus structure. + +* If the success member of the status is true, the write operation was accepted, but it may not be complete. Check the category member of the status to determine whether the write is still pending. +* If the success member is false, check the other structure members for additional information about why the write operation failed. It could be that the operation requires a secured or verified write, instead of an "ordinary" write. + +You can check the Message Exchange statuses to determine whether the write was successful and to examine any error codes that have been returned. What your program does with the status information up to you—whether to update status displays, perform error handling, and so on. + +Any attempt to write to a buffered item will result in a OnWriteComplete status event returning an MxStatus with an MxCategoryOperationalError category and an detail code of MX_E_NotWriteable. + +## Examples + +[Visual Basic.NET] + +```vbnet +If ItemStatus(0).success Then + If ItemStatus(0).category = ArchestrA.MxAccess.MxStatusCategory.MxCategoryPending Then + WriteStatus.Text = "Write Pending..." + Else + WriteStatus.Text = "Write Complete - status OK" + End If +Else + If ItemStatus(0).detail = MX_E_SecuredWrite Then + ' 1012, secured write + ' either re-try as a secured write + ' or generate an error message + ... + Else + If ItemStatus(0).detail = MX_E_VerifiedWrite Then + ' 1013, verified write + ' either re-try as a verified write + ' or show an error message + ... + Else + ' some other kind of error, + ' display information + WriteStatus.Text = "Write Complete w/error - cat: " & + ItemStatus(0).category & " Src: " & + ItemStatus(0).detectedBy & " detail: " & + ItemStatus(0).detail + End If + End If +End If +``` + +Handling the OnWriteComplete Callback [C#] + +```csharp +if (ItemStatus[0].success != 0) { + if (ItemStatus[0].category == ArchestrA.MxAccess.MxStatusCategory.MxCatgeoryPending) { + WriteStatus.Text = "Write Pending..."; + } else { + WriteStatus.Text = "Write Complete - status OK"; + } +} else { + if (ItemStatus[0].detail == MX_E_SecuredWrite) { + // 1012, secured write + // either re-try as a secured write + // or generate an error message + ... + } else { + if (ItemStatus[0].detail == MX_E_VerifiedWrite_val) { + // 1013, verified write + // either re-try as a verified write + // or show an error message + } else { + // some other kind of error, + // display information + PokeValue.Text = + "Write Complete w/error - cat: " + + ItemStatus[0].category + " Src: " + + ItemStatus[0].detectedBy + + " detail: " + ItemStatus[0].detail; + } + } +} +``` + +[Visual C++] + +```cpp +if (ItemStatus[0].success != 0) { + if (ItemStatus[0].category == MxCatgeoryPending) { + SetDlgItemText(hMainDlg, IDC_PokeValue, + L"Write Pending..."); + } else { + SetDlgItemText(hMainDlg, IDC_PokeValue, + L"Write Complete - status OK"); + } +} + +```c +} else { + if (ItemStatus[0].detail == MX_E_SecuredWrite) { + // 1012, secured write + // either re-try as a secured write + // or generate an error message + ... + } else { + if (ItemStatus[0].detail == MX_E_VerifiedWrite) { + // 1013, verified write + // either re-try as a verified write + // or show an error message + ... + } else { + // some other kind of error, + // display information + TCHAR error_text[256]; + swprintf_s (error_text, + sizeof(error_text)/sizeof(TCHAR), + L"Write Complete w/error - cat: " + L"%d Src: %d" "detail: %d", + ItemStatus[0].category, ItemStatus[0].detectedBy, + ItemStatus[0].detail); + SetDlgItemText (hMainDlg, IDC_WriteStatus, + error_text); + } + } +} +``` + +# Setting Update Intervals for Buffered Attributes + +LMXProxy will also allow your client program to configure the rate at which buffered data change updates are received. If you do not set this rate, the buffered data update events are sent to the client program once per second. + +## Syntax + +**[Visual Basic.NET]** + +```vb +Sub SetBufferedUpdateInterval ( + ByVal hLMXServerHandle As Integer, + ByVal lUpdateInterval As Integer) +``` + +**[C#]** + +```csharp +void SetBufferedUpdateInterval ( + int hLMXServerHandle, + int lUpdateInterval) +``` + +Setting Update Intervals for Buffered Attributes [Visual C++] + +```c++ +HRESULT __stdcall SetBufferedUpdateInterval( + long hLMXServerHandle, + long lUpdateInterval) +``` + +**Parameters** + +* **hLMXServerHandle** + * Handle of the connection + +* **lUpdateInterval** + * Specifies the update interval in milliseconds + +**Examples** + +[Visual Basic.NET] + +```vb +LMX_Server.SetBufferedUpdateInterval(hLMX, Interval) +``` + +[C#] + +```csharp +LMX_Server.SetBufferedUpdateInterval(hLMX, Interval); +``` + +[Visual C++] + +```c++ +m_pServerCMInterface->SetBufferedUpdateInterval(hLMXServerHandle, 3000); +``` + +**Remarks** + +If you set a **Negative** or **Zero (0)** value, it will return an error with value E-INVALIDARG HResult. + +If you set a **Positive** value, the value will be rounded to the next modulo 100 value. For example, a value of 1-100 will be 100, and 101 will be set to 200 and so on. + +# Chapter 3: Using Code Samples + +Code samples support Microsoft Visual Studio 2012 or later. Code samples run on Application Server 2014 or later version nodes that have a Platform deployed. For information about running the samples on a node with an earlier version of Application Server (for example, 2.1 Patch 02), see "Registering the Interface Files" on page 10. + +To build the sample projects VcppMxAccessActiveX and VcppMxSample on a 64-bit operating system, you must modify the import statement to include (x86) in the file path. + +**To build the sample projects on a 64-bit operating system** + +1. Modify the import statement in stdafx.h.as shown in the following example: + +```c +#import "C:\Program Files (x86)\ArchestrA\Framework\Bin\MxAccess32.tlb" no_namespace, raw_interfaces_only +``` + +2. Modify the import path in stdafx.h to match the InTouch installation directory as shown in the following example: + +```c +#import "C:\Program Files (x86)\Wonderware\InTouch\AppServerSecurity.dll" no_namespace, raw_interfaces_only +``` + +# To implement the basic program + +The examples for a stand-alone program are in Visual Basic, C#, and Visual C++. All three examples implement the basic program. + +MxAccess C# Example window showing various buttons and text fields for application registration, item functions, item value, poke item, security, operation status, communication management functions, and buffered data interval management. + +The code examples show how to connect to Message Exchange, access ArchestrA data, and shut down. The text fields and buttons allow you to select an attribute, add a reference for it, put it on advise, receive data updates, and write to the attribute. It also supports logging on under one or two names, to demonstrate secured writes and verified writes. + +For information about setting up a simple ArchestrA Galaxy configuration for running the sample application, see "Setting Up a Simple Galaxy Configuration for the Sample Applications" on page 86. + +This is a very simple demonstration program and does not keep track of adding and advising multiple items, even though the LMXProxy is capable of doing so. If you enter a new item name and add or advise that item, the program unadvises and removes the present item, if any. + +The sample program has a few safeguards in place to demonstrate the possibility of including such safeguards in your own code: + +* If you click **Add Item** before clicking **Register**, the program registers for you. +* If you click **Advise** before clicking **Add Item**, the program adds the item and registers it, if necessary. +* If you click **Remove Item**, the program checks whether there are any items on advise. If so, it unadvises them for you. +* If you click **Unregister**, the program checks whether there are any items on advise. If so, it unadvises them for you. It then checks whether there are any items in the reference table. If so, it removes those items for you and then unregisters. + +**To access data in an ArchestrA Galaxy** + +1. Start the program. +2. In the **Application Level Functions** area, click **Register** to connect to LMX Message Exchange. +3. In the **Item Functions** area, do the following: + * a Type the item name. + * b Click **AddItem** to add the item to the reference tables. + * c If you wish use AdviseSupervisory, check the box marked **Supervisory Connection**. If you wish to use Advise, clear the box. + * d Click **Advise** to put the item on advise. Updates for the item appear. If item is not on advise, you do not receive data updates. + +4 In the **Poke into item** area, do the following: +a Type the value in the first text box. +b If you are poking to only a single-valued attribute, leave the remaining text boxes blank. If you are poking to an array, type values for the array in the two remaining text boxes. +c If you wish to write the timestamp as well as the value, type the desired timestamp in the **Time** text box. Otherwise, leave the **Time** text box blank. +d Click **Poke** to write the value. + +Note: Success or failure is shown in the first text box. If the attribute requires a secured or verified write, the program attempts it automatically. It succeeds only if you have logged on under one or more account names. + +5 In the **Security** area, do the following: +a Type the name and password for **User1** and **User2**. If you are logging on to one account, use the **User1** and **Pwd1** text boxes. If you are logging on to two accounts, use **User2** and **Pwd2** text boxes. +b Click **Register UserIds** to log on. After logging on, the program succeeds in secured writes and verified writes, if the accounts have the appropriate credentials for the selected item. + +6 In the **Communication Management Functions** area, do the following: +a Click **Suspend** to pause data updates. +b Click **Activate** to resume updates. + +7 In the **Items Function** area, do the following: +a Click **Unadvise** to take the item off advise. You no longer receive data updates. +b Click **RemoveItem** to remove the item from the reference tables. + +8 In the **Applications Level Functions** area, click **Unregister** to disconnect from LMX Message Exchange. + +9 Shut down the program. + +Using an ActiveX Code Sample # Using an ActiveX Code Sample + +This example of ActiveX code demonstrates the ArchestrAUserToId() method. Use this method in applications, such as an InTouch application, where the user log on is done outside your component. The program is similar to the other code samples, although there are some minor differences. + +MxAccess ActiveX Sample window with various functions and fields including Application Level Functions (Register, Unregister), Item Functions (Advise, UserDefined_001.Attr1, Unadvise), Item (Value 1000, Quality 192, Time 8/6/2008 4:28:59.696 PM, Status), Poke into item (Value 1000, Time 8/6/2008 4:28:59.696 PM, Status Write Complete - status OK), Communication Management Functions (Suspend, Activate, Status Operation Complete - status OK), GUID of Logged-in User (Get GUID, ID {84F0B924-B158-4777-BEFD-A4BE10CC774A}), Security (Secured Write, Verified Write, Log In), IDs (IDs 1, 2, 3), Status SUCCESS, Detail OK. + +* The edit fields for PokeValue1 and PokeValue2 are omitted to leave more room for other buttons and display fields. Although the control shows array attributes, it does not support writing an array of new values. See the other code examples for an implementation of writing an array. +* Buttons and edit fields have been added for retrieving and displaying the GUIDs and corresponding ID "cookies". + +The ActiveX control must be registered on your computer and then registered with WindowMaker. Create a simple InTouch application with a single window and place an instance of the ActiveX control on the window. Configure the application for ArchestrA security. Configure WindowViewer so the application window is the start-up window for the application. + +**To access data in an ArchestrA Galaxy using ActiveX** + +1. Start WindowViewer. +2. In the **Application Level Functions** area, click **Register** to connect to LMX Message Exchange. +3. In the **Item Functions** area, do the following: + a. Type the item name. + b. Click **Advise** to put the item on advise. Updates for the item appear. +4. In the **Poke into item** area, do the following: + a. Type the value in the **Value** text box. + b. If you wish to write the timestamp as well as the value, type the desired timestamp in the **Time** text box. Otherwise, leave the **Time** text box blank. + c. Click **Poke** to write the value. Results appear in the **Status** box. + If the Galaxy is secured, and you have not obtained the ID for the logged on user, the write should fail. If the attribute requires a secured write or a verified write, and the program has not yet obtained the required IDs (of the logged on user and of a verified user, if needed), then the write fails. +5. In the **GUID of Logged in User** area, click **Get GUID**. + If the user has not logged on, zeros appear for the GUID. The ID "cookie" should also be zero. In the InTouch application, click **Log-in** on the **Security** menu to log on, and then click **Get GUID**. The display shows the GUID of the logged on user and a non-zero ID "cookie." +6. In the **Poke into item** area, click **Poke**. Attributes that require only an ordinary write, or a secured write succeed. +7. In the **Item Functions** area, click **Unadvise** and edit the item name to select an attribute that requires a secured write or a verified write. Then, click **Advise** to put the new item on advise. + +Using an ActiveX Code Sample **8** In the **Security** area, click **Log In** to show an InTouch log on dialog box. + +* If **Secured Write** is selected, a confirmation dialog box just for the logged on user is shown. If **Verified Write** is selected, a dialog box is shown that prompts the logged on user to confirm the password and asks the verifying user to enter a name and password. +* The display should show the GUID of the user(s) and corresponding ID "cookies," along with the status and detail information about the log on. +* If the user password is incorrect, the function indicates it failed to obtain a GUID. +* If **Verified Write** is selected and the logged on user password is incorrect, the function indicates it failed to obtain the GUIDs, even if the verifying user logon is correct. +* If **Verified Write** is selected and the verifying user logon is incorrect, the function indicates it failed to obtain the GUIDs, even if the logged-in user's password is correct. +* If the proper GUIDs have been obtained, secured and verified writes should now work. + +**9** In the **Communication Management Functions** area, do the following: + a Click **Suspend** to pause data updates. + b Click **Activate** to resume updates. + +**10** In the **Items Function** area, click **Unadvise** to take the item off advise. You no longer receive data updates. + +**11** In the **Applications Level Functions** area, click **Unregister** to disconnect from LMX Message Exchange. + +This sample program has the same safeguards in place as the other examples to ensure that the Register operation is invoked before attempting any other operations and to ensure that a proper shut down takes place, even if the user does not click **Unregister** before terminating WindowViewer. + +# Setting Up a Simple Galaxy Configuration for the Sample Applications + +You must have an ArchestrA Galaxy and a MXAccess runtime license to run the sample applications. For most operations you can use a Galaxy with no security. You can register, add items, put them on advise, see the updates, and write to them. + +If you already have a Galaxy set up, you may want to simply run the program and enter the name of an attribute of some object that has been created and deployed. + +## Setting up Attributes + +You can establish a simple set of attributes that can be referenced in the sample application dialog box and will update automatically. If no security is enabled, you can simply start up the sample application and even write to the selected attributes. + +**Note:** To advise an entire array, the reference should be of the form "ud1.ar[ ]". + +### To set up a simple set of attributes + +1. Start the ArchestrA IDE. +2. In the **Deployment** window, do the following: + a. Create an instance of a WinPlatform. + b. Create an AppEngine and assign it to the WinPlatform. + c. Create an Area and assign it to the AppEngine. + d. Create an instance of $UserDefined, give it the name "ud1", and assign it to the Area. + e. Double click ud1 to start the editor. +3. On the **UDAs** tab, add the following attributes and make them all user writable: + * x, of data type Integer + * ar, of data type Integer, an array of 3 elements + * dt, of data type Time + * ardt, of data type Time, an array of 3 elements + +Setting Up a Simple Galaxy Configuration for the Sample Applications 4 On the **Scripts** tab, create the following script s1: + Execution type: execute + Expression: 1 + Trigger type: While true + Trigger period and Deadband 0 + Statements + * me.x = me.x + 1; + * me.ar[1] = me.ar[1] + 1; + * me.ar[2] = me.ar[2] + 2; + * me.ar[3] = me.ar[3] + 3; + +5 Save and close the editor. + +6 Deploy the WinPlatform and all objects underneath. + +# Setting up a Simple Security Configuration + +To exercise secured writes and verified writes, you must configure the Galaxy for security and have at least two users that can be configured to log in. + +## To set up a simple security configuration + +1 Start ArchestrA IDE, and select **Galaxy**, and then **Configure** and **Security**. The **Configure Security** dialog box appears. + +2 Click the **Authentication Mode** tab and review the settings. +If the current security mode is "none" you must enable security. For the purposes of this exercise, select Galaxy security. If the security mode changes, you must shut down the Galaxy, open the IDE again and log on. A recommended log on in this case is as Administrator. See the ArchestrA documentation for more information. + +3 Click the **Roles** tab, and then do the following: + a Select the **Default** role, and then clear all the permissions. + b Create the **Writers** role. Check all the **General** permissions and all the **Operational** permissions. + c Create the **NonWriters** role. Check all the **General** permissions and all the **Operational** permissions, except for the **Can modify Operate attributes**. Leave that box unchecked. + +4 Click the **Users** tab, then do the following: +a Add user **Writer1** with Default and Writers roles. +b Add user **Writer2** with Default and Writers roles. +c Add user **NonWriter** with Default and NonWriters roles. + +5 Create a unique password for each user. + +With security in place, you can put an attribute on advise and see updates, without logging on. But to write to an attribute, even one with simple Operate security, you must log on as Writer1 or Writer2. If you are logged on as NonWriter, an attempt to write produces an error message with error code 1008. + +To exercise the secured write and verified write operations, you can edit ud1 and add two more attributes. + +**To add more attributes** + +1 Click the **UDAs** tab. + a Add y, of data type Integer, with security mode of Secured Write. + b Add z, of data type Integer, with security mode of Verified Write. + +2 Click the **Scripts** tab, and then add the following lines to the existing script s1: + ``` + me.y = me.y + 1; + me.z = me.z + 1; + ``` + +3 Save and close the editor. + +4 Undeploy and re-deploy ud1. + +This adds ud1.y and ud1.z as attributes that you can advise and write, using the appropriate secured or verified write options. + +The sample application does a Write() first to obtain a status using the OnWriteComplete() event and from that status determines whether the attribute requires a secured write or a verified write, then attempts the appropriate call to WriteSecured() - or reports an error status. If you are running the application in debug mode, you can set breakpoints inside the OnDataChange() event handler to trace through the code and see this process in action. + +Setting Up a Simple Galaxy Configuration for the Sample Applications # Example Security for the Role of Writers + +The following dialog box shows how you can configure security for the role of writers. + +Screenshot of the "Configure Security" dialog box showing roles and permissions configuration. + +# Example Security for the Role of NonWriters + +The following dialog box shows how you can configure security for the role of non-writers. + +Screenshot of the "Configure Security" dialog box showing roles and permissions configuration. + +# Appendix A: Status and Error Codes + +MXAccess status and error codes report on the health of the application. + +The status and error codes are presented in one of four types: + +* MxStatusDetail Values +* MxStatusCategory Values +* MxStatusSource Values +* ResolutionStatus Values + +An MxStatus is actually a structure, which contains four data fields: + +```c +typedef struct MxStatus { + VARIANT_BOOL success; + MxStatusCategory category; + MxStatusSource detectedBy; + short detail; +} MxStatus; +``` + +## MxStatusDetail Values + +The MxStatusDetail values are shown in the following table: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueStatus
0MX_S_Success
1MX_E_RequestTimedOut
2MX_E_PlatformCommunicationError
3MX_E_InvalidPlatformId
4MX_E_InvalidEngineId
5MX_E_EngineCommunicationError
6MX_E_InvalidReference
7MX_E_NoGalaxyRepository
8MX_E_InvalidObjectId
9MX_E_ObjectSignatureMismatch
10MX_E_AttributeSignatureMismatch
11MX_E_ResolvingAttribute
12MX_E_ResolvingObject
13MX_E_WrongDataType
14MX_E_WrongNumberOfDimensions
15MX_E_InvalidIndex
16MX_E_IndexOutOfOrder
17MX_E_DimensionDoesNotExist
18MX_E_ConversionNotSupported
19MX_E_UnableToConvertString
20MX_E_Overflow
21MX_E_NmxVersionMismatch
22MX_E_NmxInvalidCommand
23MX_E_LmxVersionMismatch
24MX_E_LmxInvalidCommand
25MX_E_GalaxyRepositoryBusy
26MX_E_EngineOverloaded
1000MX_E_InvalidPrimitiveId
1001MX_E_InvalidAttributeId
1002MX_E_InvalidPropertyId
1003MX_E_IndexOutOfRange
1004MX_E_DataOutOfRange
1005MX_E_IncorrectDataType
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueStatus
1006MX_E_NotReadable
1007MX_E_NotWriteable
1008MX_E_WriteAccessDenied
1009MX_E_UnknownError
1010MX_E_ObjectInitializing
1011MX_E_EngineInitializing
1012MX_E_SecuredWrite
1013MX_E_VerifiedWrite
1014MX_E_NoAlarmAckPrivilege
8000MX_E_AutomationObjectSpecificError
+ +## MxStatusCategory Values + +The MxStatusCategory values are shown in the following table: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueStatus
-1MxStatusCategoryUnknown
0MxCategoryOk
1MxCategoryPending
2MxCategoryWarning
3MxCategoryCommunicationError
4MxCategoryConfigurationError
5MxCategoryOperationalError
6MxCategorySecurityError
7MxCategorySoftwareError
8MxCategoryOtherError
+ +## MxStatusSource Values + +The MxStatusSource values are shown in the following table: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueStatus
-1MxSourceUnknown
0MxSourceRequestingLmx
1MxSourceRespondingLmx
2MxSourceRequestingNmx
3MxSourceRespondingNmx
4MxSourceRequestingAutomationObject
5MxSourceRespondingAutomationObject
+ +## ResolutionStatus Values + +The ResolutionStatus values are shown in the following table: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueStatus
0unresolved
1resolvingObject
2resolvingAttribute
3resolved
4invalidReference
5noGalaxyRepository
6waitingToResolveAgainstDb
7waitingToResolveAttribute
8retrievingRedundancyStatus
diff --git a/session.dat b/session.dat new file mode 100644 index 0000000..1651a03 --- /dev/null +++ b/session.dat @@ -0,0 +1 @@ +opc.tcp://opcuademo.sterfive.com:26543 \ No newline at end of file diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/AppConfiguration.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/AppConfiguration.cs new file mode 100644 index 0000000..974df8b --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/AppConfiguration.cs @@ -0,0 +1,13 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration +{ + /// + /// Top-level configuration holder binding all sections from appsettings.json. (SVC-003) + /// + public class AppConfiguration + { + public OpcUaConfiguration OpcUa { get; set; } = new OpcUaConfiguration(); + public MxAccessConfiguration MxAccess { get; set; } = new MxAccessConfiguration(); + public GalaxyRepositoryConfiguration GalaxyRepository { get; set; } = new GalaxyRepositoryConfiguration(); + public DashboardConfiguration Dashboard { get; set; } = new DashboardConfiguration(); + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/ConfigurationValidator.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/ConfigurationValidator.cs new file mode 100644 index 0000000..a172c56 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/ConfigurationValidator.cs @@ -0,0 +1,69 @@ +using Serilog; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration +{ + /// + /// Validates and logs effective configuration at startup. (SVC-003, SVC-005) + /// + public static class ConfigurationValidator + { + private static readonly ILogger Log = Serilog.Log.ForContext(typeof(ConfigurationValidator)); + + public static bool ValidateAndLog(AppConfiguration config) + { + bool valid = true; + + Log.Information("=== Effective Configuration ==="); + + // OPC UA + Log.Information("OpcUa.Port={Port}, EndpointPath={EndpointPath}, ServerName={ServerName}, GalaxyName={GalaxyName}", + config.OpcUa.Port, config.OpcUa.EndpointPath, config.OpcUa.ServerName, config.OpcUa.GalaxyName); + Log.Information("OpcUa.MaxSessions={MaxSessions}, SessionTimeoutMinutes={SessionTimeout}", + config.OpcUa.MaxSessions, config.OpcUa.SessionTimeoutMinutes); + + if (config.OpcUa.Port < 1 || config.OpcUa.Port > 65535) + { + Log.Error("OpcUa.Port must be between 1 and 65535"); + valid = false; + } + + if (string.IsNullOrWhiteSpace(config.OpcUa.GalaxyName)) + { + Log.Error("OpcUa.GalaxyName must not be empty"); + valid = false; + } + + // MxAccess + Log.Information("MxAccess.ClientName={ClientName}, ReadTimeout={ReadTimeout}s, WriteTimeout={WriteTimeout}s, MaxConcurrent={MaxConcurrent}", + config.MxAccess.ClientName, config.MxAccess.ReadTimeoutSeconds, config.MxAccess.WriteTimeoutSeconds, + config.MxAccess.MaxConcurrentOperations); + Log.Information("MxAccess.MonitorInterval={MonitorInterval}s, AutoReconnect={AutoReconnect}, ProbeTag={ProbeTag}, ProbeStaleThreshold={ProbeStale}s", + config.MxAccess.MonitorIntervalSeconds, config.MxAccess.AutoReconnect, + config.MxAccess.ProbeTag ?? "(none)", config.MxAccess.ProbeStaleThresholdSeconds); + + if (string.IsNullOrWhiteSpace(config.MxAccess.ClientName)) + { + Log.Error("MxAccess.ClientName must not be empty"); + valid = false; + } + + // Galaxy Repository + Log.Information("GalaxyRepository.ConnectionString={ConnectionString}, ChangeDetectionInterval={ChangeInterval}s, CommandTimeout={CmdTimeout}s", + config.GalaxyRepository.ConnectionString, config.GalaxyRepository.ChangeDetectionIntervalSeconds, + config.GalaxyRepository.CommandTimeoutSeconds); + + if (string.IsNullOrWhiteSpace(config.GalaxyRepository.ConnectionString)) + { + Log.Error("GalaxyRepository.ConnectionString must not be empty"); + valid = false; + } + + // Dashboard + Log.Information("Dashboard.Enabled={Enabled}, Port={Port}, RefreshInterval={Refresh}s", + config.Dashboard.Enabled, config.Dashboard.Port, config.Dashboard.RefreshIntervalSeconds); + + Log.Information("=== Configuration {Status} ===", valid ? "Valid" : "INVALID"); + return valid; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/DashboardConfiguration.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/DashboardConfiguration.cs new file mode 100644 index 0000000..da8daa0 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/DashboardConfiguration.cs @@ -0,0 +1,12 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration +{ + /// + /// Status dashboard configuration. (SVC-003, DASH-001) + /// + public class DashboardConfiguration + { + public bool Enabled { get; set; } = true; + public int Port { get; set; } = 8081; + public int RefreshIntervalSeconds { get; set; } = 10; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/GalaxyRepositoryConfiguration.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/GalaxyRepositoryConfiguration.cs new file mode 100644 index 0000000..468fc4d --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/GalaxyRepositoryConfiguration.cs @@ -0,0 +1,12 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration +{ + /// + /// Galaxy repository database configuration. (SVC-003, GR-005) + /// + public class GalaxyRepositoryConfiguration + { + public string ConnectionString { get; set; } = "Server=localhost;Database=ZB;Integrated Security=true;"; + public int ChangeDetectionIntervalSeconds { get; set; } = 30; + public int CommandTimeoutSeconds { get; set; } = 30; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/MxAccessConfiguration.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/MxAccessConfiguration.cs new file mode 100644 index 0000000..207f32e --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/MxAccessConfiguration.cs @@ -0,0 +1,19 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration +{ + /// + /// MXAccess client configuration. (SVC-003, MXA-008, MXA-009) + /// + public class MxAccessConfiguration + { + public string ClientName { get; set; } = "LmxOpcUa"; + public string? NodeName { get; set; } + public string? GalaxyName { get; set; } + public int ReadTimeoutSeconds { get; set; } = 5; + public int WriteTimeoutSeconds { get; set; } = 5; + public int MaxConcurrentOperations { get; set; } = 10; + public int MonitorIntervalSeconds { get; set; } = 5; + public bool AutoReconnect { get; set; } = true; + public string? ProbeTag { get; set; } + public int ProbeStaleThresholdSeconds { get; set; } = 60; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs new file mode 100644 index 0000000..833b59d --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs @@ -0,0 +1,15 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration +{ + /// + /// OPC UA server configuration. (SVC-003, OPC-001, OPC-012, OPC-013) + /// + public class OpcUaConfiguration + { + public int Port { get; set; } = 4840; + public string EndpointPath { get; set; } = "/LmxOpcUa"; + public string ServerName { get; set; } = "LmxOpcUa"; + public string GalaxyName { get; set; } = "ZB"; + public int MaxSessions { get; set; } = 100; + public int SessionTimeoutMinutes { get; set; } = 30; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionState.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionState.cs new file mode 100644 index 0000000..0198274 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionState.cs @@ -0,0 +1,15 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// MXAccess connection lifecycle states. (MXA-002) + /// + public enum ConnectionState + { + Disconnected, + Connecting, + Connected, + Disconnecting, + Error, + Reconnecting + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionStateChangedEventArgs.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionStateChangedEventArgs.cs new file mode 100644 index 0000000..86e9d0a --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/ConnectionStateChangedEventArgs.cs @@ -0,0 +1,21 @@ +using System; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Event args for connection state transitions. (MXA-002) + /// + public class ConnectionStateChangedEventArgs : EventArgs + { + public ConnectionState PreviousState { get; } + public ConnectionState CurrentState { get; } + public string Message { get; } + + public ConnectionStateChangedEventArgs(ConnectionState previous, ConnectionState current, string message = "") + { + PreviousState = previous; + CurrentState = current; + Message = message ?? ""; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyAttributeInfo.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyAttributeInfo.cs new file mode 100644 index 0000000..632701b --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyAttributeInfo.cs @@ -0,0 +1,17 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// DTO matching attributes.sql result columns. (GR-002) + /// + public class GalaxyAttributeInfo + { + public int GobjectId { get; set; } + public string TagName { get; set; } = ""; + public string AttributeName { get; set; } = ""; + public string FullTagReference { get; set; } = ""; + public int MxDataType { get; set; } + public string DataTypeName { get; set; } = ""; + public bool IsArray { get; set; } + public int? ArrayDimension { get; set; } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyObjectInfo.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyObjectInfo.cs new file mode 100644 index 0000000..706a252 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/GalaxyObjectInfo.cs @@ -0,0 +1,15 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// DTO matching hierarchy.sql result columns. (GR-001) + /// + public class GalaxyObjectInfo + { + public int GobjectId { get; set; } + public string TagName { get; set; } = ""; + public string ContainedName { get; set; } = ""; + public string BrowseName { get; set; } = ""; + public int ParentGobjectId { get; set; } + public bool IsArea { get; set; } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IGalaxyRepository.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IGalaxyRepository.cs new file mode 100644 index 0000000..e99b03e --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IGalaxyRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Interface for Galaxy repository database queries. (GR-001 through GR-004) + /// + public interface IGalaxyRepository + { + Task> GetHierarchyAsync(CancellationToken ct = default); + Task> GetAttributesAsync(CancellationToken ct = default); + Task GetLastDeployTimeAsync(CancellationToken ct = default); + Task TestConnectionAsync(CancellationToken ct = default); + + event Action? OnGalaxyChanged; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxAccessClient.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxAccessClient.cs new file mode 100644 index 0000000..a1f24c1 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxAccessClient.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Abstraction over MXAccess COM client for tag read/write/subscribe operations. + /// (MXA-001 through MXA-009, OPC-007, OPC-008, OPC-009) + /// + public interface IMxAccessClient : IDisposable + { + ConnectionState State { get; } + event EventHandler? ConnectionStateChanged; + event Action? OnTagValueChanged; + + Task ConnectAsync(CancellationToken ct = default); + Task DisconnectAsync(); + + Task SubscribeAsync(string fullTagReference, Action callback); + Task UnsubscribeAsync(string fullTagReference); + + Task ReadAsync(string fullTagReference, CancellationToken ct = default); + Task WriteAsync(string fullTagReference, object value, CancellationToken ct = default); + + int ActiveSubscriptionCount { get; } + int ReconnectCount { get; } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxProxy.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxProxy.cs new file mode 100644 index 0000000..c821754 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxProxy.cs @@ -0,0 +1,41 @@ +using System; +using ArchestrA.MxAccess; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Delegate matching LMXProxyServer.OnDataChange COM event signature. + /// + public delegate void MxDataChangeHandler( + int hLMXServerHandle, + int phItemHandle, + object pvItemValue, + int pwItemQuality, + object pftItemTimeStamp, + ref MXSTATUS_PROXY[] ItemStatus); + + /// + /// Delegate matching LMXProxyServer.OnWriteComplete COM event signature. + /// + public delegate void MxWriteCompleteHandler( + int hLMXServerHandle, + int phItemHandle, + ref MXSTATUS_PROXY[] ItemStatus); + + /// + /// Abstraction over LMXProxyServer COM object to enable testing without the COM runtime. (MXA-001) + /// + public interface IMxProxy + { + int Register(string clientName); + void Unregister(int handle); + int AddItem(int handle, string address); + void RemoveItem(int handle, int itemHandle); + void AdviseSupervisory(int handle, int itemHandle); + void UnAdviseSupervisory(int handle, int itemHandle); + void Write(int handle, int itemHandle, object value, int securityClassification); + + event MxDataChangeHandler? OnDataChange; + event MxWriteCompleteHandler? OnWriteComplete; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxDataTypeMapper.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxDataTypeMapper.cs new file mode 100644 index 0000000..e9c2fb0 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxDataTypeMapper.cs @@ -0,0 +1,81 @@ +using System; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Maps Galaxy mx_data_type integers to OPC UA data types and CLR types. (OPC-005) + /// See gr/data_type_mapping.md for full mapping table. + /// + public static class MxDataTypeMapper + { + /// + /// Maps mx_data_type to OPC UA DataType NodeId numeric identifier. + /// Unknown types default to String (i=12). + /// + public static uint MapToOpcUaDataType(int mxDataType) + { + return mxDataType switch + { + 1 => 1, // Boolean → i=1 + 2 => 6, // Integer → Int32 i=6 + 3 => 10, // Float → Float i=10 + 4 => 11, // Double → Double i=11 + 5 => 12, // String → String i=12 + 6 => 13, // Time → DateTime i=13 + 7 => 11, // ElapsedTime → Double i=11 (seconds) + 8 => 12, // Reference → String i=12 + 13 => 6, // Enumeration → Int32 i=6 + 14 => 12, // Custom → String i=12 + 15 => 21, // InternationalizedString → LocalizedText i=21 + 16 => 12, // Custom → String i=12 + _ => 12 // Unknown → String i=12 + }; + } + + /// + /// Maps mx_data_type to the corresponding CLR type. + /// + public static Type MapToClrType(int mxDataType) + { + return mxDataType switch + { + 1 => typeof(bool), + 2 => typeof(int), + 3 => typeof(float), + 4 => typeof(double), + 5 => typeof(string), + 6 => typeof(DateTime), + 7 => typeof(double), // ElapsedTime as seconds + 8 => typeof(string), // Reference as string + 13 => typeof(int), // Enum backing integer + 14 => typeof(string), + 15 => typeof(string), // LocalizedText stored as string + 16 => typeof(string), + _ => typeof(string) + }; + } + + /// + /// Returns the OPC UA type name for a given mx_data_type. + /// + public static string GetOpcUaTypeName(int mxDataType) + { + return mxDataType switch + { + 1 => "Boolean", + 2 => "Int32", + 3 => "Float", + 4 => "Double", + 5 => "String", + 6 => "DateTime", + 7 => "Double", + 8 => "String", + 13 => "Int32", + 14 => "String", + 15 => "LocalizedText", + 16 => "String", + _ => "String" + }; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxErrorCodes.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxErrorCodes.cs new file mode 100644 index 0000000..97289d7 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/MxErrorCodes.cs @@ -0,0 +1,43 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Translates MXAccess error codes (1008, 1012, 1013, etc.) to human-readable messages. (MXA-009) + /// + public static class MxErrorCodes + { + public const int MX_E_InvalidReference = 1008; + public const int MX_E_WrongDataType = 1012; + public const int MX_E_NotWritable = 1013; + public const int MX_E_RequestTimedOut = 1014; + public const int MX_E_CommFailure = 1015; + public const int MX_E_NotConnected = 1016; + + public static string GetMessage(int errorCode) + { + return errorCode switch + { + 1008 => "Invalid reference: the tag address does not exist or is malformed", + 1012 => "Wrong data type: the value type does not match the attribute's expected type", + 1013 => "Not writable: the attribute is read-only or locked", + 1014 => "Request timed out: the operation did not complete within the allowed time", + 1015 => "Communication failure: lost connection to the runtime", + 1016 => "Not connected: no active connection to the Galaxy runtime", + _ => $"Unknown MXAccess error code: {errorCode}" + }; + } + + public static Quality MapToQuality(int errorCode) + { + return errorCode switch + { + 1008 => Quality.BadConfigError, + 1012 => Quality.BadConfigError, + 1013 => Quality.BadOutOfService, + 1014 => Quality.BadCommFailure, + 1015 => Quality.BadCommFailure, + 1016 => Quality.BadNotConnected, + _ => Quality.Bad + }; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Quality.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Quality.cs new file mode 100644 index 0000000..aa9c223 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Quality.cs @@ -0,0 +1,36 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// OPC DA quality codes mapped from MXAccess quality values. (MXA-009, OPC-005) + /// + public enum Quality : byte + { + // Bad family (0-63) + Bad = 0, + BadConfigError = 4, + BadNotConnected = 8, + BadDeviceFailure = 12, + BadSensorFailure = 16, + BadCommFailure = 20, + BadOutOfService = 24, + BadWaitingForInitialData = 32, + + // Uncertain family (64-191) + Uncertain = 64, + UncertainLastUsable = 68, + UncertainSensorNotAccurate = 80, + UncertainEuExceeded = 84, + UncertainSubNormal = 88, + + // Good family (192+) + Good = 192, + GoodLocalOverride = 216 + } + + public static class QualityExtensions + { + public static bool IsGood(this Quality q) => (byte)q >= 192; + public static bool IsUncertain(this Quality q) => (byte)q >= 64 && (byte)q < 192; + public static bool IsBad(this Quality q) => (byte)q < 64; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/QualityMapper.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/QualityMapper.cs new file mode 100644 index 0000000..75e958e --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/QualityMapper.cs @@ -0,0 +1,54 @@ +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Maps MXAccess integer quality to domain Quality enum and OPC UA StatusCodes. (MXA-009, OPC-005) + /// + public static class QualityMapper + { + /// + /// Maps an MXAccess quality integer (OPC DA quality byte) to domain Quality. + /// Uses category bits: 192+ = Good, 64-191 = Uncertain, 0-63 = Bad. + /// + public static Quality MapFromMxAccessQuality(int mxQuality) + { + var b = (byte)(mxQuality & 0xFF); + + // Try exact match first + if (System.Enum.IsDefined(typeof(Quality), b)) + return (Quality)b; + + // Fall back to category + if (b >= 192) return Quality.Good; + if (b >= 64) return Quality.Uncertain; + return Quality.Bad; + } + + /// + /// Maps domain Quality to OPC UA StatusCode uint32. + /// + public static uint MapToOpcUaStatusCode(Quality quality) + { + return quality switch + { + Quality.Good => 0x00000000u, // Good + Quality.GoodLocalOverride => 0x00D80000u, // Good_LocalOverride + Quality.Uncertain => 0x40000000u, // Uncertain + Quality.UncertainLastUsable => 0x40900000u, + Quality.UncertainSensorNotAccurate => 0x40930000u, + Quality.UncertainEuExceeded => 0x40940000u, + Quality.UncertainSubNormal => 0x40950000u, + Quality.Bad => 0x80000000u, // Bad + Quality.BadConfigError => 0x80890000u, + Quality.BadNotConnected => 0x808A0000u, + Quality.BadDeviceFailure => 0x808B0000u, + Quality.BadSensorFailure => 0x808C0000u, + Quality.BadCommFailure => 0x80050000u, + Quality.BadOutOfService => 0x808D0000u, + Quality.BadWaitingForInitialData => 0x80320000u, + _ => quality.IsGood() ? 0x00000000u : + quality.IsUncertain() ? 0x40000000u : + 0x80000000u + }; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Vtq.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Vtq.cs new file mode 100644 index 0000000..bfd41a7 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/Vtq.cs @@ -0,0 +1,32 @@ +using System; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Domain +{ + /// + /// Value-Timestamp-Quality triplet for tag data. (MXA-003, OPC-007) + /// + public readonly struct Vtq : IEquatable + { + public object? Value { get; } + public DateTime Timestamp { get; } + public Quality Quality { get; } + + public Vtq(object? value, DateTime timestamp, Quality quality) + { + Value = value; + Timestamp = timestamp; + Quality = quality; + } + + public static Vtq Good(object? value) => new Vtq(value, DateTime.UtcNow, Quality.Good); + public static Vtq Bad(Quality quality = Quality.Bad) => new Vtq(null, DateTime.UtcNow, quality); + public static Vtq Uncertain(object? value) => new Vtq(value, DateTime.UtcNow, Quality.Uncertain); + + public bool Equals(Vtq other) => + Equals(Value, other.Value) && Timestamp == other.Timestamp && Quality == other.Quality; + + public override bool Equals(object? obj) => obj is Vtq other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Value, Timestamp, Quality); + public override string ToString() => $"Vtq({Value}, {Timestamp:O}, {Quality})"; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/ChangeDetectionService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/ChangeDetectionService.cs new file mode 100644 index 0000000..aeffa6f --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/ChangeDetectionService.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository +{ + /// + /// Polls the Galaxy database for deployment changes and fires OnGalaxyChanged. (GR-003, GR-004) + /// + public class ChangeDetectionService : IDisposable + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + private readonly IGalaxyRepository _repository; + private readonly int _intervalSeconds; + private CancellationTokenSource? _cts; + private DateTime? _lastKnownDeployTime; + + public event Action? OnGalaxyChanged; + public DateTime? LastKnownDeployTime => _lastKnownDeployTime; + + public ChangeDetectionService(IGalaxyRepository repository, int intervalSeconds) + { + _repository = repository; + _intervalSeconds = intervalSeconds; + } + + public void Start() + { + _cts = new CancellationTokenSource(); + Task.Run(() => PollLoopAsync(_cts.Token)); + Log.Information("Change detection started (interval={Interval}s)", _intervalSeconds); + } + + public void Stop() + { + _cts?.Cancel(); + Log.Information("Change detection stopped"); + } + + private async Task PollLoopAsync(CancellationToken ct) + { + // First poll always triggers + bool firstPoll = true; + + while (!ct.IsCancellationRequested) + { + try + { + var deployTime = await _repository.GetLastDeployTimeAsync(ct); + + if (firstPoll) + { + firstPoll = false; + _lastKnownDeployTime = deployTime; + Log.Information("Initial deploy time: {DeployTime}", deployTime); + OnGalaxyChanged?.Invoke(); + } + else if (deployTime != _lastKnownDeployTime) + { + Log.Information("Galaxy deployment change detected: {Previous} → {Current}", + _lastKnownDeployTime, deployTime); + _lastKnownDeployTime = deployTime; + OnGalaxyChanged?.Invoke(); + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Log.Warning(ex, "Change detection poll failed, will retry next interval"); + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(_intervalSeconds), ct); + } + catch (OperationCanceledException) + { + break; + } + } + } + + public void Dispose() + { + Stop(); + _cts?.Dispose(); + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryService.cs new file mode 100644 index 0000000..93e434c --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryService.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository +{ + /// + /// Implements IGalaxyRepository using SQL queries against the Galaxy ZB database. (GR-001 through GR-007) + /// + public class GalaxyRepositoryService : IGalaxyRepository + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + private readonly GalaxyRepositoryConfiguration _config; + + public event Action? OnGalaxyChanged; + + #region SQL Queries (GR-006: const string, no dynamic SQL) + + private const string HierarchySql = @" +SELECT DISTINCT + g.gobject_id, + g.tag_name, + g.contained_name, + CASE WHEN g.contained_name IS NULL OR g.contained_name = '' + THEN g.tag_name + ELSE g.contained_name + END AS browse_name, + CASE WHEN g.contained_by_gobject_id = 0 + THEN g.area_gobject_id + ELSE g.contained_by_gobject_id + END AS parent_gobject_id, + CASE WHEN td.category_id = 13 + THEN 1 + ELSE 0 + END AS is_area +FROM gobject g +INNER JOIN template_definition td + ON g.template_definition_id = td.template_definition_id +WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND g.is_template = 0 + AND g.deployed_package_id <> 0 +ORDER BY parent_gobject_id, g.tag_name"; + + private const string AttributesSql = @" +;WITH template_chain AS ( + SELECT g.gobject_id, g.derived_from_gobject_id, 0 AS depth + FROM gobject g + WHERE g.is_template = 0 + UNION ALL + SELECT tc.gobject_id, t.derived_from_gobject_id, tc.depth + 1 + FROM template_chain tc + INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id + WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10 +) +SELECT DISTINCT + g.gobject_id, + g.tag_name, + da.attribute_name, + g.tag_name + '.' + da.attribute_name + + CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END + AS full_tag_reference, + da.mx_data_type, + dt.description AS data_type_name, + da.is_array, + CASE WHEN da.is_array = 1 + THEN CONVERT(int, CONVERT(varbinary(2), + SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2)) + ELSE NULL + END AS array_dimension, + da.mx_attribute_category, + da.security_classification +FROM template_chain tc +INNER JOIN dynamic_attribute da + ON da.gobject_id = tc.derived_from_gobject_id +INNER JOIN gobject g + ON g.gobject_id = tc.gobject_id +INNER JOIN template_definition td + ON td.template_definition_id = g.template_definition_id +LEFT JOIN data_type dt + ON dt.mx_data_type = da.mx_data_type +WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND g.is_template = 0 + AND g.deployed_package_id <> 0 + AND da.attribute_name NOT LIKE '[_]%' + AND da.attribute_name NOT LIKE '%.Description' + AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) +ORDER BY g.tag_name, da.attribute_name"; + + private const string ChangeDetectionSql = "SELECT time_of_last_deploy FROM galaxy"; + + private const string TestConnectionSql = "SELECT 1"; + + #endregion + + public GalaxyRepositoryService(GalaxyRepositoryConfiguration config) + { + _config = config; + } + + public async Task> GetHierarchyAsync(CancellationToken ct = default) + { + var results = new List(); + + using var conn = new SqlConnection(_config.ConnectionString); + await conn.OpenAsync(ct); + + using var cmd = new SqlCommand(HierarchySql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; + using var reader = await cmd.ExecuteReaderAsync(ct); + + while (await reader.ReadAsync(ct)) + { + results.Add(new GalaxyObjectInfo + { + GobjectId = Convert.ToInt32(reader.GetValue(0)), + TagName = reader.GetString(1), + ContainedName = reader.IsDBNull(2) ? "" : reader.GetString(2), + BrowseName = reader.GetString(3), + ParentGobjectId = Convert.ToInt32(reader.GetValue(4)), + IsArea = Convert.ToInt32(reader.GetValue(5)) == 1 + }); + } + + if (results.Count == 0) + Log.Warning("GetHierarchyAsync returned zero rows"); + else + Log.Information("GetHierarchyAsync returned {Count} objects", results.Count); + + return results; + } + + public async Task> GetAttributesAsync(CancellationToken ct = default) + { + var results = new List(); + + using var conn = new SqlConnection(_config.ConnectionString); + await conn.OpenAsync(ct); + + using var cmd = new SqlCommand(AttributesSql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; + using var reader = await cmd.ExecuteReaderAsync(ct); + + while (await reader.ReadAsync(ct)) + { + results.Add(new GalaxyAttributeInfo + { + GobjectId = reader.GetInt32(0), + TagName = reader.GetString(1), + AttributeName = reader.GetString(2), + FullTagReference = reader.GetString(3), + MxDataType = Convert.ToInt32(reader.GetValue(4)), + DataTypeName = reader.IsDBNull(5) ? "" : reader.GetString(5), + IsArray = Convert.ToBoolean(reader.GetValue(6)), + ArrayDimension = reader.IsDBNull(7) ? null : (int?)Convert.ToInt32(reader.GetValue(7)) + }); + } + + Log.Information("GetAttributesAsync returned {Count} attributes", results.Count); + return results; + } + + public async Task GetLastDeployTimeAsync(CancellationToken ct = default) + { + using var conn = new SqlConnection(_config.ConnectionString); + await conn.OpenAsync(ct); + + using var cmd = new SqlCommand(ChangeDetectionSql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; + var result = await cmd.ExecuteScalarAsync(ct); + + return result is DateTime dt ? dt : null; + } + + public async Task TestConnectionAsync(CancellationToken ct = default) + { + try + { + using var conn = new SqlConnection(_config.ConnectionString); + await conn.OpenAsync(ct); + + using var cmd = new SqlCommand(TestConnectionSql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; + await cmd.ExecuteScalarAsync(ct); + + Log.Information("Galaxy repository database connection successful"); + return true; + } + catch (Exception ex) + { + Log.Warning(ex, "Galaxy repository database connection failed"); + return false; + } + } + + public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke(); + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryStats.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryStats.cs new file mode 100644 index 0000000..c09aa15 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/GalaxyRepository/GalaxyRepositoryStats.cs @@ -0,0 +1,17 @@ +using System; + +namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository +{ + /// + /// POCO for dashboard: Galaxy repository status info. (DASH-009) + /// + public class GalaxyRepositoryStats + { + public string GalaxyName { get; set; } = ""; + public bool DbConnected { get; set; } + public DateTime? LastDeployTime { get; set; } + public int ObjectCount { get; set; } + public int AttributeCount { get; set; } + public DateTime? LastRebuildTime { get; set; } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Metrics/PerformanceMetrics.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Metrics/PerformanceMetrics.cs new file mode 100644 index 0000000..619a17d --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Metrics/PerformanceMetrics.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Serilog; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics +{ + /// + /// Disposable scope returned by . (MXA-008) + /// + public interface ITimingScope : IDisposable + { + void SetSuccess(bool success); + } + + /// + /// Statistics snapshot for a single operation type. + /// + public class MetricsStatistics + { + public long TotalCount { get; set; } + public long SuccessCount { get; set; } + public double SuccessRate { get; set; } + public double AverageMilliseconds { get; set; } + public double MinMilliseconds { get; set; } + public double MaxMilliseconds { get; set; } + public double Percentile95Milliseconds { get; set; } + } + + /// + /// Per-operation timing and success tracking with a 1000-entry rolling buffer. (MXA-008) + /// + public class OperationMetrics + { + private readonly List _durations = new List(); + private readonly object _lock = new object(); + private long _totalCount; + private long _successCount; + private double _totalMilliseconds; + private double _minMilliseconds = double.MaxValue; + private double _maxMilliseconds; + + public void Record(TimeSpan duration, bool success) + { + lock (_lock) + { + _totalCount++; + if (success) _successCount++; + + var ms = duration.TotalMilliseconds; + _durations.Add(ms); + _totalMilliseconds += ms; + + if (ms < _minMilliseconds) _minMilliseconds = ms; + if (ms > _maxMilliseconds) _maxMilliseconds = ms; + + if (_durations.Count > 1000) _durations.RemoveAt(0); + } + } + + public MetricsStatistics GetStatistics() + { + lock (_lock) + { + if (_totalCount == 0) + return new MetricsStatistics(); + + var sorted = _durations.OrderBy(d => d).ToList(); + var p95Index = Math.Max(0, (int)Math.Ceiling(sorted.Count * 0.95) - 1); + + return new MetricsStatistics + { + TotalCount = _totalCount, + SuccessCount = _successCount, + SuccessRate = (double)_successCount / _totalCount, + AverageMilliseconds = _totalMilliseconds / _totalCount, + MinMilliseconds = _minMilliseconds, + MaxMilliseconds = _maxMilliseconds, + Percentile95Milliseconds = sorted[p95Index] + }; + } + } + } + + /// + /// Tracks per-operation performance metrics with periodic logging. (MXA-008) + /// + public class PerformanceMetrics : IDisposable + { + private static readonly ILogger Logger = Log.ForContext(); + + private readonly ConcurrentDictionary _metrics + = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly Timer _reportingTimer; + private bool _disposed; + + public PerformanceMetrics() + { + _reportingTimer = new Timer(ReportMetrics, null, + TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)); + } + + public void RecordOperation(string operationName, TimeSpan duration, bool success = true) + { + var metrics = _metrics.GetOrAdd(operationName, _ => new OperationMetrics()); + metrics.Record(duration, success); + } + + public ITimingScope BeginOperation(string operationName) + { + return new TimingScope(this, operationName); + } + + public OperationMetrics? GetMetrics(string operationName) + { + return _metrics.TryGetValue(operationName, out var metrics) ? metrics : null; + } + + public Dictionary GetStatistics() + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _metrics) + result[kvp.Key] = kvp.Value.GetStatistics(); + return result; + } + + private void ReportMetrics(object? state) + { + foreach (var kvp in _metrics) + { + var stats = kvp.Value.GetStatistics(); + if (stats.TotalCount == 0) continue; + + Logger.Information( + "Metrics: {Operation} — Count={Count}, SuccessRate={SuccessRate:P1}, " + + "AvgMs={AverageMs:F1}, MinMs={MinMs:F1}, MaxMs={MaxMs:F1}, P95Ms={P95Ms:F1}", + kvp.Key, stats.TotalCount, stats.SuccessRate, + stats.AverageMilliseconds, stats.MinMilliseconds, + stats.MaxMilliseconds, stats.Percentile95Milliseconds); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _reportingTimer.Dispose(); + ReportMetrics(null); + } + + private class TimingScope : ITimingScope + { + private readonly PerformanceMetrics _metrics; + private readonly string _operationName; + private readonly Stopwatch _stopwatch; + private bool _success = true; + private bool _disposed; + + public TimingScope(PerformanceMetrics metrics, string operationName) + { + _metrics = metrics; + _operationName = operationName; + _stopwatch = Stopwatch.StartNew(); + } + + public void SetSuccess(bool success) => _success = success; + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _stopwatch.Stop(); + _metrics.RecordOperation(_operationName, _stopwatch.Elapsed, _success); + } + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Connection.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Connection.cs new file mode 100644 index 0000000..85e9f06 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Connection.cs @@ -0,0 +1,113 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + public sealed partial class MxAccessClient + { + public async Task ConnectAsync(CancellationToken ct = default) + { + if (_state == ConnectionState.Connected) return; + + SetState(ConnectionState.Connecting); + try + { + _connectionHandle = await _staThread.RunAsync(() => + { + _proxy.OnDataChange += HandleOnDataChange; + _proxy.OnWriteComplete += HandleOnWriteComplete; + return _proxy.Register(_config.ClientName); + }); + + Log.Information("MxAccess registered with handle {Handle}", _connectionHandle); + SetState(ConnectionState.Connected); + + // Replay stored subscriptions + await ReplayStoredSubscriptionsAsync(); + + // Start probe if configured + if (!string.IsNullOrWhiteSpace(_config.ProbeTag)) + { + _probeTag = _config.ProbeTag; + _lastProbeValueTime = DateTime.UtcNow; + await SubscribeInternalAsync(_probeTag); + Log.Information("Probe tag subscribed: {ProbeTag}", _probeTag); + } + } + catch (Exception ex) + { + Log.Error(ex, "MxAccess connection failed"); + SetState(ConnectionState.Error, ex.Message); + throw; + } + } + + public async Task DisconnectAsync() + { + if (_state == ConnectionState.Disconnected) return; + + SetState(ConnectionState.Disconnecting); + try + { + await _staThread.RunAsync(() => + { + // UnAdvise + RemoveItem for all active subscriptions + foreach (var kvp in _addressToHandle) + { + try + { + _proxy.UnAdviseSupervisory(_connectionHandle, kvp.Value); + _proxy.RemoveItem(_connectionHandle, kvp.Value); + } + catch (Exception ex) + { + Log.Warning(ex, "Error cleaning up subscription for {Address}", kvp.Key); + } + } + + // Unwire events before unregister + _proxy.OnDataChange -= HandleOnDataChange; + _proxy.OnWriteComplete -= HandleOnWriteComplete; + + // Unregister + try { _proxy.Unregister(_connectionHandle); } + catch (Exception ex) { Log.Warning(ex, "Error during Unregister"); } + }); + + _handleToAddress.Clear(); + _addressToHandle.Clear(); + _pendingWrites.Clear(); + } + catch (Exception ex) + { + Log.Warning(ex, "Error during disconnect"); + } + finally + { + SetState(ConnectionState.Disconnected); + } + } + + public async Task ReconnectAsync() + { + SetState(ConnectionState.Reconnecting); + Interlocked.Increment(ref _reconnectCount); + Log.Information("MxAccess reconnect attempt #{Count}", _reconnectCount); + + try + { + await DisconnectAsync(); + await ConnectAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Reconnect failed"); + SetState(ConnectionState.Error, ex.Message); + } + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.EventHandlers.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.EventHandlers.cs new file mode 100644 index 0000000..e994b31 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.EventHandlers.cs @@ -0,0 +1,101 @@ +using System; +using ArchestrA.MxAccess; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + public sealed partial class MxAccessClient + { + /// + /// COM event handler for MxAccess OnDataChange events. + /// Signature matches the ArchestrA.MxAccess ILMXProxyServerEvents interface. + /// + private void HandleOnDataChange( + int hLMXServerHandle, + int phItemHandle, + object pvItemValue, + int pwItemQuality, + object pftItemTimeStamp, + ref MXSTATUS_PROXY[] ItemStatus) + { + try + { + if (!_handleToAddress.TryGetValue(phItemHandle, out var address)) + { + Log.Debug("OnDataChange for unknown handle {Handle}", phItemHandle); + return; + } + + var quality = QualityMapper.MapFromMxAccessQuality(pwItemQuality); + + // Check MXSTATUS_PROXY — if success is false, use more specific quality + if (ItemStatus != null && ItemStatus.Length > 0 && ItemStatus[0].success == 0) + { + quality = MxErrorCodes.MapToQuality(ItemStatus[0].detail); + } + + var timestamp = ConvertTimestamp(pftItemTimeStamp); + var vtq = new Vtq(pvItemValue, timestamp, quality); + + // Update probe timestamp + if (string.Equals(address, _probeTag, StringComparison.OrdinalIgnoreCase)) + { + _lastProbeValueTime = DateTime.UtcNow; + } + + // Invoke stored subscription callback + if (_storedSubscriptions.TryGetValue(address, out var callback)) + { + callback(address, vtq); + } + + // Global handler + OnTagValueChanged?.Invoke(address, vtq); + } + catch (Exception ex) + { + Log.Error(ex, "Error processing OnDataChange for handle {Handle}", phItemHandle); + } + } + + /// + /// COM event handler for MxAccess OnWriteComplete events. + /// + private void HandleOnWriteComplete( + int hLMXServerHandle, + int phItemHandle, + ref MXSTATUS_PROXY[] ItemStatus) + { + try + { + if (_pendingWrites.TryRemove(phItemHandle, out var tcs)) + { + bool success = ItemStatus == null || ItemStatus.Length == 0 || ItemStatus[0].success != 0; + if (success) + { + tcs.TrySetResult(true); + } + else + { + var detail = ItemStatus![0].detail; + var message = MxErrorCodes.GetMessage(detail); + Log.Warning("Write failed for handle {Handle}: {Message}", phItemHandle, message); + tcs.TrySetResult(false); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Error processing OnWriteComplete for handle {Handle}", phItemHandle); + } + } + + private static DateTime ConvertTimestamp(object pftItemTimeStamp) + { + if (pftItemTimeStamp is DateTime dt) + return dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime(); + return DateTime.UtcNow; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Monitor.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Monitor.cs new file mode 100644 index 0000000..4c45f58 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Monitor.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + public sealed partial class MxAccessClient + { + public void StartMonitor() + { + _monitorCts = new CancellationTokenSource(); + Task.Run(() => MonitorLoopAsync(_monitorCts.Token)); + Log.Information("MxAccess monitor started (interval={Interval}s)", _config.MonitorIntervalSeconds); + } + + public void StopMonitor() + { + _monitorCts?.Cancel(); + } + + private async Task MonitorLoopAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(_config.MonitorIntervalSeconds), ct); + } + catch (OperationCanceledException) + { + break; + } + + try + { + if (_state == ConnectionState.Disconnected && _config.AutoReconnect) + { + Log.Information("Monitor: connection lost, attempting reconnect"); + await ReconnectAsync(); + continue; + } + + if (_state == ConnectionState.Connected && _probeTag != null) + { + var elapsed = DateTime.UtcNow - _lastProbeValueTime; + if (elapsed.TotalSeconds > _config.ProbeStaleThresholdSeconds) + { + Log.Warning("Monitor: probe stale ({Elapsed:F0}s > {Threshold}s), forcing reconnect", + elapsed.TotalSeconds, _config.ProbeStaleThresholdSeconds); + await ReconnectAsync(); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Monitor loop error"); + } + } + + Log.Information("MxAccess monitor stopped"); + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.ReadWrite.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.ReadWrite.cs new file mode 100644 index 0000000..beedcab --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.ReadWrite.cs @@ -0,0 +1,139 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + public sealed partial class MxAccessClient + { + public async Task ReadAsync(string fullTagReference, CancellationToken ct = default) + { + if (_state != ConnectionState.Connected) + return Vtq.Bad(Quality.BadNotConnected); + + await _operationSemaphore.WaitAsync(ct); + try + { + using var scope = _metrics.BeginOperation("Read"); + var tcs = new TaskCompletionSource(); + + // Subscribe, get first value, unsubscribe + void OnValue(string addr, Vtq vtq) => tcs.TrySetResult(vtq); + + var itemHandle = await _staThread.RunAsync(() => + { + var h = _proxy.AddItem(_connectionHandle, fullTagReference); + _proxy.AdviseSupervisory(_connectionHandle, h); + return h; + }); + + _handleToAddress[itemHandle] = fullTagReference; + _addressToHandle[fullTagReference] = itemHandle; + _storedSubscriptions[fullTagReference] = OnValue; + + try + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(TimeSpan.FromSeconds(_config.ReadTimeoutSeconds)); + cts.Token.Register(() => tcs.TrySetResult(Vtq.Bad(Quality.BadCommFailure))); + + return await tcs.Task; + } + catch + { + scope.SetSuccess(false); + return Vtq.Bad(Quality.BadCommFailure); + } + finally + { + _storedSubscriptions.TryRemove(fullTagReference, out _); + _handleToAddress.TryRemove(itemHandle, out _); + _addressToHandle.TryRemove(fullTagReference, out _); + + try + { + await _staThread.RunAsync(() => + { + _proxy.UnAdviseSupervisory(_connectionHandle, itemHandle); + _proxy.RemoveItem(_connectionHandle, itemHandle); + }); + } + catch (Exception ex) + { + Log.Warning(ex, "Error cleaning up read subscription for {Address}", fullTagReference); + } + } + } + finally + { + _operationSemaphore.Release(); + } + } + + public async Task WriteAsync(string fullTagReference, object value, CancellationToken ct = default) + { + if (_state != ConnectionState.Connected) return false; + + await _operationSemaphore.WaitAsync(ct); + try + { + using var scope = _metrics.BeginOperation("Write"); + + var itemHandle = await _staThread.RunAsync(() => + { + var h = _proxy.AddItem(_connectionHandle, fullTagReference); + _proxy.AdviseSupervisory(_connectionHandle, h); + return h; + }); + + _handleToAddress[itemHandle] = fullTagReference; + _addressToHandle[fullTagReference] = itemHandle; + + var tcs = new TaskCompletionSource(); + _pendingWrites[itemHandle] = tcs; + + try + { + await _staThread.RunAsync(() => _proxy.Write(_connectionHandle, itemHandle, value, -1)); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(TimeSpan.FromSeconds(_config.WriteTimeoutSeconds)); + cts.Token.Register(() => tcs.TrySetResult(true)); // timeout assumes success + + return await tcs.Task; + } + catch (Exception ex) + { + scope.SetSuccess(false); + Log.Error(ex, "Write failed for {Address}", fullTagReference); + return false; + } + finally + { + _pendingWrites.TryRemove(itemHandle, out _); + _handleToAddress.TryRemove(itemHandle, out _); + _addressToHandle.TryRemove(fullTagReference, out _); + + try + { + await _staThread.RunAsync(() => + { + _proxy.UnAdviseSupervisory(_connectionHandle, itemHandle); + _proxy.RemoveItem(_connectionHandle, itemHandle); + }); + } + catch (Exception ex) + { + Log.Warning(ex, "Error cleaning up write subscription for {Address}", fullTagReference); + } + } + } + finally + { + _operationSemaphore.Release(); + } + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Subscription.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Subscription.cs new file mode 100644 index 0000000..58f8589 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Subscription.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + public sealed partial class MxAccessClient + { + public async Task SubscribeAsync(string fullTagReference, Action callback) + { + _storedSubscriptions[fullTagReference] = callback; + if (_state != ConnectionState.Connected) return; + + await SubscribeInternalAsync(fullTagReference); + } + + public async Task UnsubscribeAsync(string fullTagReference) + { + _storedSubscriptions.TryRemove(fullTagReference, out _); + + // Don't unsubscribe the probe tag + if (string.Equals(fullTagReference, _probeTag, StringComparison.OrdinalIgnoreCase)) + return; + + if (_addressToHandle.TryRemove(fullTagReference, out var itemHandle)) + { + _handleToAddress.TryRemove(itemHandle, out _); + + if (_state == ConnectionState.Connected) + { + await _staThread.RunAsync(() => + { + try + { + _proxy.UnAdviseSupervisory(_connectionHandle, itemHandle); + _proxy.RemoveItem(_connectionHandle, itemHandle); + } + catch (Exception ex) + { + Log.Warning(ex, "Error unsubscribing {Address}", fullTagReference); + } + }); + } + } + } + + private async Task SubscribeInternalAsync(string address) + { + using var scope = _metrics.BeginOperation("Subscribe"); + try + { + var itemHandle = await _staThread.RunAsync(() => + { + var h = _proxy.AddItem(_connectionHandle, address); + _proxy.AdviseSupervisory(_connectionHandle, h); + return h; + }); + + _handleToAddress[itemHandle] = address; + _addressToHandle[address] = itemHandle; + Log.Debug("Subscribed to {Address} (handle={Handle})", address, itemHandle); + } + catch (Exception ex) + { + scope.SetSuccess(false); + Log.Error(ex, "Failed to subscribe to {Address}", address); + throw; + } + } + + private async Task ReplayStoredSubscriptionsAsync() + { + foreach (var kvp in _storedSubscriptions) + { + try + { + await SubscribeInternalAsync(kvp.Key); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to replay subscription for {Address}", kvp.Key); + } + } + + Log.Information("Replayed {Count} stored subscriptions", _storedSubscriptions.Count); + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.cs new file mode 100644 index 0000000..4bcc5fe --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + /// + /// Core MXAccess client implementing IMxAccessClient via IMxProxy abstraction. + /// Split across partial classes: Connection, Subscription, ReadWrite, EventHandlers, Monitor. + /// (MXA-001 through MXA-009) + /// + public sealed partial class MxAccessClient : IMxAccessClient + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + private readonly StaComThread _staThread; + private readonly IMxProxy _proxy; + private readonly MxAccessConfiguration _config; + private readonly PerformanceMetrics _metrics; + private readonly SemaphoreSlim _operationSemaphore; + + private int _connectionHandle; + private volatile ConnectionState _state = ConnectionState.Disconnected; + private CancellationTokenSource? _monitorCts; + + // Handle mappings + private readonly ConcurrentDictionary _handleToAddress = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _addressToHandle = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + // Subscription storage + private readonly ConcurrentDictionary> _storedSubscriptions + = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + + // Pending writes + private readonly ConcurrentDictionary> _pendingWrites + = new ConcurrentDictionary>(); + + // Probe + private string? _probeTag; + private DateTime _lastProbeValueTime = DateTime.UtcNow; + private int _reconnectCount; + + public ConnectionState State => _state; + public int ActiveSubscriptionCount => _storedSubscriptions.Count; + public int ReconnectCount => _reconnectCount; + + public event EventHandler? ConnectionStateChanged; + public event Action? OnTagValueChanged; + + public MxAccessClient(StaComThread staThread, IMxProxy proxy, MxAccessConfiguration config, PerformanceMetrics metrics) + { + _staThread = staThread; + _proxy = proxy; + _config = config; + _metrics = metrics; + _operationSemaphore = new SemaphoreSlim(config.MaxConcurrentOperations, config.MaxConcurrentOperations); + } + + private void SetState(ConnectionState newState, string message = "") + { + var previous = _state; + if (previous == newState) return; + _state = newState; + Log.Information("MxAccess state: {Previous} → {Current} {Message}", previous, newState, message); + ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(previous, newState, message)); + } + + public void Dispose() + { + try + { + _monitorCts?.Cancel(); + DisconnectAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Log.Warning(ex, "Error during MxAccessClient dispose"); + } + finally + { + _operationSemaphore.Dispose(); + _monitorCts?.Dispose(); + } + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxProxyAdapter.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxProxyAdapter.cs new file mode 100644 index 0000000..7e60957 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxProxyAdapter.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.InteropServices; +using ArchestrA.MxAccess; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + /// + /// Wraps the real ArchestrA.MxAccess.LMXProxyServer COM object, forwarding calls to IMxProxy. + /// Uses strongly-typed interop — same pattern as the reference LmxProxy implementation. (MXA-001) + /// + public sealed class MxProxyAdapter : IMxProxy + { + private LMXProxyServer? _lmxProxy; + + public event MxDataChangeHandler? OnDataChange; + public event MxWriteCompleteHandler? OnWriteComplete; + + public int Register(string clientName) + { + _lmxProxy = new LMXProxyServer(); + + _lmxProxy.OnDataChange += ProxyOnDataChange; + _lmxProxy.OnWriteComplete += ProxyOnWriteComplete; + + var handle = _lmxProxy.Register(clientName); + if (handle <= 0) + throw new InvalidOperationException($"LMXProxyServer.Register returned invalid handle: {handle}"); + + return handle; + } + + public void Unregister(int handle) + { + if (_lmxProxy != null) + { + try + { + _lmxProxy.OnDataChange -= ProxyOnDataChange; + _lmxProxy.OnWriteComplete -= ProxyOnWriteComplete; + _lmxProxy.Unregister(handle); + } + finally + { + Marshal.ReleaseComObject(_lmxProxy); + _lmxProxy = null; + } + } + } + + public int AddItem(int handle, string address) => _lmxProxy!.AddItem(handle, address); + public void RemoveItem(int handle, int itemHandle) => _lmxProxy!.RemoveItem(handle, itemHandle); + public void AdviseSupervisory(int handle, int itemHandle) => _lmxProxy!.AdviseSupervisory(handle, itemHandle); + public void UnAdviseSupervisory(int handle, int itemHandle) => _lmxProxy!.UnAdvise(handle, itemHandle); + public void Write(int handle, int itemHandle, object value, int securityClassification) + => _lmxProxy!.Write(handle, itemHandle, value, securityClassification); + + private void ProxyOnDataChange(int hLMXServerHandle, int phItemHandle, object pvItemValue, + int pwItemQuality, object pftItemTimeStamp, ref MXSTATUS_PROXY[] ItemStatus) + { + OnDataChange?.Invoke(hLMXServerHandle, phItemHandle, pvItemValue, pwItemQuality, pftItemTimeStamp, ref ItemStatus); + } + + private void ProxyOnWriteComplete(int hLMXServerHandle, int phItemHandle, ref MXSTATUS_PROXY[] ItemStatus) + { + OnWriteComplete?.Invoke(hLMXServerHandle, phItemHandle, ref ItemStatus); + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/StaComThread.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/StaComThread.cs new file mode 100644 index 0000000..20489cc --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/StaComThread.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Serilog; + +namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess +{ + /// + /// Dedicated STA thread with a raw Win32 message pump for COM interop. + /// All MxAccess COM objects must be created and called on this thread. (MXA-001) + /// + public sealed class StaComThread : IDisposable + { + private const uint WM_APP = 0x8000; + private const uint PM_NOREMOVE = 0x0000; + + private static readonly ILogger Log = Serilog.Log.ForContext(); + private static readonly TimeSpan PumpLogInterval = TimeSpan.FromMinutes(5); + + private readonly Thread _thread; + private readonly TaskCompletionSource _ready = new TaskCompletionSource(); + private readonly ConcurrentQueue _workItems = new ConcurrentQueue(); + private volatile uint _nativeThreadId; + private bool _disposed; + + private long _totalMessages; + private long _appMessages; + private long _dispatchedMessages; + private long _workItemsExecuted; + private DateTime _lastLogTime; + + public StaComThread() + { + _thread = new Thread(ThreadEntry) + { + Name = "MxAccess-STA", + IsBackground = true + }; + _thread.SetApartmentState(ApartmentState.STA); + } + + public bool IsRunning => _nativeThreadId != 0 && !_disposed; + + public void Start() + { + _thread.Start(); + _ready.Task.GetAwaiter().GetResult(); + Log.Information("STA COM thread started (ThreadId={ThreadId})", _thread.ManagedThreadId); + } + + public Task RunAsync(Action action) + { + if (_disposed) throw new ObjectDisposedException(nameof(StaComThread)); + + var tcs = new TaskCompletionSource(); + _workItems.Enqueue(() => + { + try + { + action(); + tcs.TrySetResult(true); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero); + return tcs.Task; + } + + public Task RunAsync(Func func) + { + if (_disposed) throw new ObjectDisposedException(nameof(StaComThread)); + + var tcs = new TaskCompletionSource(); + _workItems.Enqueue(() => + { + try + { + tcs.TrySetResult(func()); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero); + return tcs.Task; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + try + { + if (_nativeThreadId != 0) + PostThreadMessage(_nativeThreadId, WM_APP + 1, IntPtr.Zero, IntPtr.Zero); + _thread.Join(TimeSpan.FromSeconds(5)); + } + catch (Exception ex) + { + Log.Warning(ex, "Error shutting down STA COM thread"); + } + + Log.Information("STA COM thread stopped"); + } + + private void ThreadEntry() + { + try + { + _nativeThreadId = GetCurrentThreadId(); + + MSG msg; + PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE); + + _ready.TrySetResult(true); + _lastLogTime = DateTime.UtcNow; + + Log.Debug("STA message pump entering loop"); + + while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0) + { + _totalMessages++; + + if (msg.message == WM_APP) + { + _appMessages++; + DrainQueue(); + } + else if (msg.message == WM_APP + 1) + { + DrainQueue(); + PostQuitMessage(0); + } + else + { + _dispatchedMessages++; + TranslateMessage(ref msg); + DispatchMessage(ref msg); + } + + LogPumpStatsIfDue(); + } + + Log.Information("STA message pump exited (Total={Total}, App={App}, Dispatched={Dispatched}, WorkItems={WorkItems})", + _totalMessages, _appMessages, _dispatchedMessages, _workItemsExecuted); + } + catch (Exception ex) + { + Log.Error(ex, "STA COM thread crashed"); + _ready.TrySetException(ex); + } + } + + private void DrainQueue() + { + while (_workItems.TryDequeue(out var workItem)) + { + _workItemsExecuted++; + try { workItem(); } + catch (Exception ex) { Log.Error(ex, "Unhandled exception in STA work item"); } + } + } + + private void LogPumpStatsIfDue() + { + var now = DateTime.UtcNow; + if (now - _lastLogTime < PumpLogInterval) return; + Log.Debug("STA pump alive: Total={Total}, App={App}, Dispatched={Dispatched}, WorkItems={WorkItems}, Pending={Pending}", + _totalMessages, _appMessages, _dispatchedMessages, _workItemsExecuted, _workItems.Count); + _lastLogTime = now; + } + + #region Win32 PInvoke + + [StructLayout(LayoutKind.Sequential)] + private struct MSG + { + public IntPtr hwnd; + public uint message; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public POINT pt; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int x; + public int y; + } + + [DllImport("user32.dll")] + private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TranslateMessage(ref MSG lpMsg); + + [DllImport("user32.dll")] + private static extern IntPtr DispatchMessage(ref MSG lpMsg); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern void PostQuitMessage(int nExitCode); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); + + [DllImport("kernel32.dll")] + private static extern uint GetCurrentThreadId(); + + #endregion + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/AddressSpaceBuilder.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/AddressSpaceBuilder.cs new file mode 100644 index 0000000..3d996c0 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/AddressSpaceBuilder.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa +{ + /// + /// Builds the tag reference mappings from Galaxy hierarchy and attributes. + /// Testable without an OPC UA server. (OPC-002, OPC-003, OPC-004) + /// + public class AddressSpaceBuilder + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + /// + /// Node info for the address space tree. + /// + public class NodeInfo + { + public int GobjectId { get; set; } + public string TagName { get; set; } = ""; + public string BrowseName { get; set; } = ""; + public int ParentGobjectId { get; set; } + public bool IsArea { get; set; } + public List Attributes { get; set; } = new(); + public List Children { get; set; } = new(); + } + + public class AttributeNodeInfo + { + public string AttributeName { get; set; } = ""; + public string FullTagReference { get; set; } = ""; + public int MxDataType { get; set; } + public bool IsArray { get; set; } + public int? ArrayDimension { get; set; } + } + + /// + /// Result of building the address space model. + /// + public class AddressSpaceModel + { + public List RootNodes { get; set; } = new(); + public Dictionary NodeIdToTagReference { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public int ObjectCount { get; set; } + public int VariableCount { get; set; } + } + + public static AddressSpaceModel Build(List hierarchy, List attributes) + { + var model = new AddressSpaceModel(); + var objectMap = hierarchy.ToDictionary(h => h.GobjectId); + + var attrsByObject = attributes + .GroupBy(a => a.GobjectId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Build parent→children map + var childrenByParent = hierarchy.GroupBy(h => h.ParentGobjectId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Find root objects (parent not in hierarchy) + var knownIds = new HashSet(hierarchy.Select(h => h.GobjectId)); + + foreach (var obj in hierarchy) + { + var nodeInfo = BuildNodeInfo(obj, attrsByObject, childrenByParent, model); + + if (!knownIds.Contains(obj.ParentGobjectId)) + model.RootNodes.Add(nodeInfo); + } + + Log.Information("Address space model: {Objects} objects, {Variables} variables, {Mappings} tag refs", + model.ObjectCount, model.VariableCount, model.NodeIdToTagReference.Count); + + return model; + } + + private static NodeInfo BuildNodeInfo(GalaxyObjectInfo obj, + Dictionary> attrsByObject, + Dictionary> childrenByParent, + AddressSpaceModel model) + { + var node = new NodeInfo + { + GobjectId = obj.GobjectId, + TagName = obj.TagName, + BrowseName = obj.BrowseName, + ParentGobjectId = obj.ParentGobjectId, + IsArea = obj.IsArea + }; + + if (!obj.IsArea) + model.ObjectCount++; + + if (attrsByObject.TryGetValue(obj.GobjectId, out var attrs)) + { + foreach (var attr in attrs) + { + node.Attributes.Add(new AttributeNodeInfo + { + AttributeName = attr.AttributeName, + FullTagReference = attr.FullTagReference, + MxDataType = attr.MxDataType, + IsArray = attr.IsArray, + ArrayDimension = attr.ArrayDimension + }); + + model.NodeIdToTagReference[attr.FullTagReference] = attr.FullTagReference; + model.VariableCount++; + } + } + + return node; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/DataValueConverter.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/DataValueConverter.cs new file mode 100644 index 0000000..21ccc2f --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/DataValueConverter.cs @@ -0,0 +1,80 @@ +using System; +using Opc.Ua; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa +{ + /// + /// Converts between domain Vtq and OPC UA DataValue. Handles all data_type_mapping.md types. (OPC-005, OPC-007) + /// + public static class DataValueConverter + { + public static DataValue FromVtq(Vtq vtq) + { + var statusCode = new StatusCode(QualityMapper.MapToOpcUaStatusCode(vtq.Quality)); + + var dataValue = new DataValue + { + Value = ConvertToOpcUaValue(vtq.Value), + StatusCode = statusCode, + SourceTimestamp = vtq.Timestamp.Kind == DateTimeKind.Utc ? vtq.Timestamp : vtq.Timestamp.ToUniversalTime(), + ServerTimestamp = DateTime.UtcNow + }; + + return dataValue; + } + + public static Vtq ToVtq(DataValue dataValue) + { + var quality = MapStatusCodeToQuality(dataValue.StatusCode); + var timestamp = dataValue.SourceTimestamp != DateTime.MinValue + ? dataValue.SourceTimestamp + : DateTime.UtcNow; + + return new Vtq(dataValue.Value, timestamp, quality); + } + + private static object? ConvertToOpcUaValue(object? value) + { + if (value == null) return null; + + return value switch + { + bool _ => value, + int _ => value, + float _ => value, + double _ => value, + string _ => value, + DateTime dt => dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime(), + TimeSpan ts => ts.TotalSeconds, // ElapsedTime → Double seconds + short s => (int)s, + long l => l, + byte b => (int)b, + bool[] _ => value, + int[] _ => value, + float[] _ => value, + double[] _ => value, + string[] _ => value, + DateTime[] _ => value, + _ => value.ToString() + }; + } + + private static Quality MapStatusCodeToQuality(StatusCode statusCode) + { + var code = statusCode.Code; + if (StatusCode.IsGood(statusCode)) return Quality.Good; + if (StatusCode.IsUncertain(statusCode)) return Quality.Uncertain; + + return code switch + { + StatusCodes.BadNotConnected => Quality.BadNotConnected, + StatusCodes.BadCommunicationError => Quality.BadCommFailure, + StatusCodes.BadConfigurationError => Quality.BadConfigError, + StatusCodes.BadOutOfService => Quality.BadOutOfService, + StatusCodes.BadWaitingForInitialData => Quality.BadWaitingForInitialData, + _ => Quality.Bad + }; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs new file mode 100644 index 0000000..8da9d8b --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Opc.Ua; +using Opc.Ua.Server; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; + +namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa +{ + /// + /// Custom node manager that builds the OPC UA address space from Galaxy hierarchy data. + /// (OPC-002 through OPC-013) + /// + public class LmxNodeManager : CustomNodeManager2 + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + private readonly IMxAccessClient _mxAccessClient; + private readonly PerformanceMetrics _metrics; + private readonly string _namespaceUri; + + // NodeId → full_tag_reference for read/write resolution + private readonly Dictionary _nodeIdToTagReference = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Ref-counted MXAccess subscriptions + private readonly Dictionary _subscriptionRefCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _tagToVariableNode = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private readonly object _lock = new object(); + private IDictionary>? _externalReferences; + + public IReadOnlyDictionary NodeIdToTagReference => _nodeIdToTagReference; + public int VariableNodeCount { get; private set; } + public int ObjectNodeCount { get; private set; } + + public LmxNodeManager( + IServerInternal server, + ApplicationConfiguration configuration, + string namespaceUri, + IMxAccessClient mxAccessClient, + PerformanceMetrics metrics) + : base(server, configuration, namespaceUri) + { + _namespaceUri = namespaceUri; + _mxAccessClient = mxAccessClient; + _metrics = metrics; + + // Wire up data change delivery + _mxAccessClient.OnTagValueChanged += OnMxAccessDataChange; + } + + public override void CreateAddressSpace(IDictionary> externalReferences) + { + lock (Lock) + { + _externalReferences = externalReferences; + base.CreateAddressSpace(externalReferences); + } + } + + /// + /// Builds the address space from Galaxy hierarchy and attributes data. (OPC-002, OPC-003) + /// + public void BuildAddressSpace(List hierarchy, List attributes) + { + lock (Lock) + { + _nodeIdToTagReference.Clear(); + _tagToVariableNode.Clear(); + VariableNodeCount = 0; + ObjectNodeCount = 0; + + // Build lookup: gobject_id → object info + var objectMap = hierarchy.ToDictionary(h => h.GobjectId); + + // Build lookup: gobject_id → list of attributes + var attrsByObject = attributes + .GroupBy(a => a.GobjectId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Find root objects (those whose parent is not in the hierarchy) + var rootFolder = CreateFolder(null, "ZB", "ZB"); + rootFolder.NodeId = new NodeId("ZB", NamespaceIndex); + rootFolder.AddReference(ReferenceTypeIds.Organizes, true, ObjectIds.ObjectsFolder); + + // Add reverse reference from Objects folder to our root + var extRefs = _externalReferences ?? new Dictionary>(); + AddExternalReference(ObjectIds.ObjectsFolder, ReferenceTypeIds.Organizes, false, rootFolder.NodeId, extRefs); + + AddPredefinedNode(SystemContext, rootFolder); + + // Create nodes for each object in hierarchy + var nodeMap = new Dictionary(); + var parentIds = new HashSet(hierarchy.Select(h => h.ParentGobjectId)); + + foreach (var obj in hierarchy) + { + NodeState parentNode; + if (nodeMap.TryGetValue(obj.ParentGobjectId, out var p)) + parentNode = p; + else + parentNode = rootFolder; + + NodeState node; + if (obj.IsArea) + { + // Areas → FolderType + Organizes reference + var folder = CreateFolder(parentNode, obj.BrowseName, obj.BrowseName); + folder.NodeId = new NodeId(obj.TagName, NamespaceIndex); + node = folder; + } + else + { + // Non-areas → BaseObjectType + HasComponent reference + var objNode = CreateObject(parentNode, obj.BrowseName, obj.BrowseName); + objNode.NodeId = new NodeId(obj.TagName, NamespaceIndex); + node = objNode; + ObjectNodeCount++; + } + + AddPredefinedNode(SystemContext, node); + nodeMap[obj.GobjectId] = node; + + // Create variable nodes for this object's attributes + if (attrsByObject.TryGetValue(obj.GobjectId, out var objAttrs)) + { + foreach (var attr in objAttrs) + { + CreateAttributeVariable(node, attr); + } + } + } + + Log.Information("Address space built: {Objects} objects, {Variables} variables, {Mappings} tag references", + ObjectNodeCount, VariableNodeCount, _nodeIdToTagReference.Count); + } + } + + /// + /// Rebuilds the address space, removing old nodes and creating new ones. (OPC-010) + /// + public void RebuildAddressSpace(List hierarchy, List attributes) + { + lock (Lock) + { + Log.Information("Rebuilding address space..."); + + // Remove all predefined nodes + var nodesToRemove = new List(); + foreach (var kvp in _nodeIdToTagReference) + { + var nodeId = new NodeId(kvp.Key, NamespaceIndex); + nodesToRemove.Add(nodeId); + } + + foreach (var nodeId in PredefinedNodes.Keys.ToList()) + { + try { DeleteNode(SystemContext, nodeId); } + catch { /* ignore cleanup errors */ } + } + + PredefinedNodes.Clear(); + _nodeIdToTagReference.Clear(); + _tagToVariableNode.Clear(); + _subscriptionRefCounts.Clear(); + + // Rebuild + BuildAddressSpace(hierarchy, attributes); + Log.Information("Address space rebuild complete"); + } + } + + private void CreateAttributeVariable(NodeState parent, GalaxyAttributeInfo attr) + { + var opcUaDataTypeId = MxDataTypeMapper.MapToOpcUaDataType(attr.MxDataType); + var variable = CreateVariable(parent, attr.AttributeName, attr.AttributeName, new NodeId(opcUaDataTypeId), + attr.IsArray ? ValueRanks.OneDimension : ValueRanks.Scalar); + + var nodeIdString = attr.FullTagReference; + variable.NodeId = new NodeId(nodeIdString, NamespaceIndex); + + if (attr.IsArray && attr.ArrayDimension.HasValue) + { + variable.ArrayDimensions = new ReadOnlyList(new List { (uint)attr.ArrayDimension.Value }); + } + + variable.AccessLevel = AccessLevels.CurrentReadOrWrite; + variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite; + variable.StatusCode = StatusCodes.BadWaitingForInitialData; + variable.Timestamp = DateTime.UtcNow; + + AddPredefinedNode(SystemContext, variable); + _nodeIdToTagReference[nodeIdString] = attr.FullTagReference; + _tagToVariableNode[attr.FullTagReference] = variable; + VariableNodeCount++; + } + + private FolderState CreateFolder(NodeState? parent, string path, string name) + { + var folder = new FolderState(parent) + { + SymbolicName = name, + ReferenceTypeId = ReferenceTypes.Organizes, + TypeDefinitionId = ObjectTypeIds.FolderType, + NodeId = new NodeId(path, NamespaceIndex), + BrowseName = new QualifiedName(name, NamespaceIndex), + DisplayName = new LocalizedText("en", name), + WriteMask = AttributeWriteMask.None, + UserWriteMask = AttributeWriteMask.None, + EventNotifier = EventNotifiers.None + }; + + parent?.AddChild(folder); + return folder; + } + + private BaseObjectState CreateObject(NodeState parent, string path, string name) + { + var obj = new BaseObjectState(parent) + { + SymbolicName = name, + ReferenceTypeId = ReferenceTypes.HasComponent, + TypeDefinitionId = ObjectTypeIds.BaseObjectType, + NodeId = new NodeId(path, NamespaceIndex), + BrowseName = new QualifiedName(name, NamespaceIndex), + DisplayName = new LocalizedText("en", name), + WriteMask = AttributeWriteMask.None, + UserWriteMask = AttributeWriteMask.None, + EventNotifier = EventNotifiers.None + }; + + parent.AddChild(obj); + return obj; + } + + private BaseDataVariableState CreateVariable(NodeState parent, string path, string name, NodeId dataType, int valueRank) + { + var variable = new BaseDataVariableState(parent) + { + SymbolicName = name, + ReferenceTypeId = ReferenceTypes.HasComponent, + TypeDefinitionId = VariableTypeIds.BaseDataVariableType, + NodeId = new NodeId(path, NamespaceIndex), + BrowseName = new QualifiedName(name, NamespaceIndex), + DisplayName = new LocalizedText("en", name), + WriteMask = AttributeWriteMask.None, + UserWriteMask = AttributeWriteMask.None, + DataType = dataType, + ValueRank = valueRank, + AccessLevel = AccessLevels.CurrentReadOrWrite, + UserAccessLevel = AccessLevels.CurrentReadOrWrite, + Historizing = false, + StatusCode = StatusCodes.Good, + Timestamp = DateTime.UtcNow + }; + + parent.AddChild(variable); + return variable; + } + + #region Read/Write Handlers + + public override void Read(OperationContext context, double maxAge, IList nodesToRead, + IList results, IList errors) + { + base.Read(context, maxAge, nodesToRead, results, errors); + + for (int i = 0; i < nodesToRead.Count; i++) + { + var nodeId = nodesToRead[i].NodeId; + if (nodeId.NamespaceIndex != NamespaceIndex) continue; + + var nodeIdStr = nodeId.Identifier as string; + if (nodeIdStr == null) continue; + + if (_nodeIdToTagReference.TryGetValue(nodeIdStr, out var tagRef)) + { + try + { + var vtq = _mxAccessClient.ReadAsync(tagRef).GetAwaiter().GetResult(); + results[i] = DataValueConverter.FromVtq(vtq); + errors[i] = ServiceResult.Good; + } + catch (Exception ex) + { + Log.Warning(ex, "Read failed for {TagRef}", tagRef); + errors[i] = new ServiceResult(StatusCodes.BadInternalError); + } + } + } + } + + public override void Write(OperationContext context, IList nodesToWrite, + IList errors) + { + base.Write(context, nodesToWrite, errors); + + for (int i = 0; i < nodesToWrite.Count; i++) + { + var nodeId = nodesToWrite[i].NodeId; + if (nodeId.NamespaceIndex != NamespaceIndex) continue; + + var nodeIdStr = nodeId.Identifier as string; + if (nodeIdStr == null) continue; + + if (_nodeIdToTagReference.TryGetValue(nodeIdStr, out var tagRef)) + { + try + { + var value = nodesToWrite[i].Value.WrappedValue.Value; + var success = _mxAccessClient.WriteAsync(tagRef, value).GetAwaiter().GetResult(); + errors[i] = success ? ServiceResult.Good : new ServiceResult(StatusCodes.BadInternalError); + } + catch (Exception ex) + { + Log.Warning(ex, "Write failed for {TagRef}", tagRef); + errors[i] = new ServiceResult(StatusCodes.BadInternalError); + } + } + } + } + + #endregion + + #region Subscription Delivery + + /// + /// Subscribes to MXAccess for the given tag reference. Called by the service wiring layer. + /// + public void SubscribeTag(string fullTagReference) + { + lock (_lock) + { + if (_subscriptionRefCounts.TryGetValue(fullTagReference, out var count)) + { + _subscriptionRefCounts[fullTagReference] = count + 1; + } + else + { + _subscriptionRefCounts[fullTagReference] = 1; + _ = _mxAccessClient.SubscribeAsync(fullTagReference, (_, _) => { }); + } + } + } + + private void OnMxAccessDataChange(string address, Vtq vtq) + { + if (_tagToVariableNode.TryGetValue(address, out var variable)) + { + try + { + var dataValue = DataValueConverter.FromVtq(vtq); + variable.Value = dataValue.Value; + variable.StatusCode = dataValue.StatusCode; + variable.Timestamp = dataValue.SourceTimestamp; + variable.ClearChangeMasks(SystemContext, false); + } + catch (Exception ex) + { + Log.Warning(ex, "Error updating variable node for {Address}", address); + } + } + } + + #endregion + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs new file mode 100644 index 0000000..1af07b9 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using Opc.Ua; +using Opc.Ua.Server; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; + +namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa +{ + /// + /// Custom OPC UA server that creates the LmxNodeManager. (OPC-001, OPC-012) + /// + public class LmxOpcUaServer : StandardServer + { + private readonly string _galaxyName; + private readonly IMxAccessClient _mxAccessClient; + private readonly PerformanceMetrics _metrics; + private LmxNodeManager? _nodeManager; + + public LmxNodeManager? NodeManager => _nodeManager; + public int ActiveSessionCount + { + get + { + try { return ServerInternal?.SessionManager?.GetSessions()?.Count ?? 0; } + catch { return 0; } + } + } + + public LmxOpcUaServer(string galaxyName, IMxAccessClient mxAccessClient, PerformanceMetrics metrics) + { + _galaxyName = galaxyName; + _mxAccessClient = mxAccessClient; + _metrics = metrics; + } + + protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration) + { + var namespaceUri = $"urn:{_galaxyName}:LmxOpcUa"; + _nodeManager = new LmxNodeManager(server, configuration, namespaceUri, _mxAccessClient, _metrics); + + var nodeManagers = new List { _nodeManager }; + return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray()); + } + + protected override ServerProperties LoadServerProperties() + { + var properties = new ServerProperties + { + ManufacturerName = "ZB MOM", + ProductName = "LmxOpcUa Server", + ProductUri = $"urn:{_galaxyName}:LmxOpcUa", + SoftwareVersion = GetType().Assembly.GetName().Version?.ToString() ?? "1.0.0", + BuildNumber = "1", + BuildDate = System.DateTime.UtcNow + }; + return properties; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaQualityMapper.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaQualityMapper.cs new file mode 100644 index 0000000..bef991c --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaQualityMapper.cs @@ -0,0 +1,23 @@ +using Opc.Ua; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa +{ + /// + /// Maps domain Quality to OPC UA StatusCodes for the OPC UA server layer. (OPC-005) + /// + public static class OpcUaQualityMapper + { + public static StatusCode ToStatusCode(Quality quality) + { + return new StatusCode(QualityMapper.MapToOpcUaStatusCode(quality)); + } + + public static Quality FromStatusCode(StatusCode statusCode) + { + if (StatusCode.IsGood(statusCode)) return Quality.Good; + if (StatusCode.IsUncertain(statusCode)) return Quality.Uncertain; + return Quality.Bad; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs new file mode 100644 index 0000000..20c521c --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs @@ -0,0 +1,165 @@ +using System; +using System.Threading.Tasks; +using Opc.Ua; +using Opc.Ua.Configuration; +using Opc.Ua.Server; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; + +namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa +{ + /// + /// Manages the OPC UA ApplicationInstance lifecycle. Programmatic config, no XML. (OPC-001, OPC-012, OPC-013) + /// + public class OpcUaServerHost : IDisposable + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + private readonly OpcUaConfiguration _config; + private readonly IMxAccessClient _mxAccessClient; + private readonly PerformanceMetrics _metrics; + private ApplicationInstance? _application; + private LmxOpcUaServer? _server; + + public LmxNodeManager? NodeManager => _server?.NodeManager; + public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0; + public bool IsRunning => _server != null; + + public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics) + { + _config = config; + _mxAccessClient = mxAccessClient; + _metrics = metrics; + } + + public async Task StartAsync() + { + var namespaceUri = $"urn:{_config.GalaxyName}:LmxOpcUa"; + + var appConfig = new ApplicationConfiguration + { + ApplicationName = _config.ServerName, + ApplicationUri = namespaceUri, + ApplicationType = ApplicationType.Server, + ProductUri = namespaceUri, + + ServerConfiguration = new ServerConfiguration + { + BaseAddresses = { $"opc.tcp://0.0.0.0:{_config.Port}{_config.EndpointPath}" }, + MaxSessionCount = _config.MaxSessions, + MaxSessionTimeout = _config.SessionTimeoutMinutes * 60 * 1000, // ms + MinSessionTimeout = 10000, + SecurityPolicies = + { + new ServerSecurityPolicy + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + } + }, + UserTokenPolicies = + { + new UserTokenPolicy(UserTokenType.Anonymous) + } + }, + + SecurityConfiguration = new SecurityConfiguration + { + ApplicationCertificate = new CertificateIdentifier + { + StoreType = CertificateStoreType.Directory, + StorePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "OPC Foundation", "pki", "own"), + SubjectName = $"CN={_config.ServerName}, O=ZB MOM, DC=localhost" + }, + TrustedIssuerCertificates = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "OPC Foundation", "pki", "issuer") + }, + TrustedPeerCertificates = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "OPC Foundation", "pki", "trusted") + }, + RejectedCertificateStore = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "OPC Foundation", "pki", "rejected") + }, + AutoAcceptUntrustedCertificates = true + }, + + TransportQuotas = new TransportQuotas + { + OperationTimeout = 120000, + MaxStringLength = 4 * 1024 * 1024, + MaxByteStringLength = 4 * 1024 * 1024, + MaxArrayLength = 65535, + MaxMessageSize = 4 * 1024 * 1024, + MaxBufferSize = 65535, + ChannelLifetime = 600000, + SecurityTokenLifetime = 3600000 + }, + + TraceConfiguration = new TraceConfiguration + { + OutputFilePath = null, + TraceMasks = 0 + } + }; + + await appConfig.Validate(ApplicationType.Server); + + _application = new ApplicationInstance + { + ApplicationName = _config.ServerName, + ApplicationType = ApplicationType.Server, + ApplicationConfiguration = appConfig + }; + + // Check/create application certificate + bool certOk = await _application.CheckApplicationInstanceCertificate(false, 2048); + if (!certOk) + { + Log.Warning("Application certificate check failed, attempting to create..."); + certOk = await _application.CheckApplicationInstanceCertificate(false, 2048); + } + + _server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics); + await _application.Start(_server); + + Log.Information("OPC UA server started on opc.tcp://localhost:{Port}{EndpointPath} (namespace={Namespace})", + _config.Port, _config.EndpointPath, namespaceUri); + } + + public void Stop() + { + try + { + _server?.Stop(); + Log.Information("OPC UA server stopped"); + } + catch (Exception ex) + { + Log.Warning(ex, "Error stopping OPC UA server"); + } + finally + { + _server = null; + _application = null; + } + } + + public void Dispose() => Stop(); + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs new file mode 100644 index 0000000..b775e14 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs @@ -0,0 +1,285 @@ +using System; +using System.Threading; +using Microsoft.Extensions.Configuration; +using Serilog; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; +using ZB.MOM.WW.LmxOpcUa.Host.Status; + +namespace ZB.MOM.WW.LmxOpcUa.Host +{ + /// + /// Full service implementation wiring all components together. (SVC-004, SVC-005, SVC-006) + /// + internal sealed class OpcUaService + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + private readonly AppConfiguration _config; + private readonly IMxProxy? _mxProxy; + private readonly IGalaxyRepository? _galaxyRepository; + + private CancellationTokenSource? _cts; + private PerformanceMetrics? _metrics; + private StaComThread? _staThread; + private MxAccessClient? _mxAccessClient; + private ChangeDetectionService? _changeDetection; + private OpcUaServerHost? _serverHost; + private LmxNodeManager? _nodeManager; + private HealthCheckService? _healthCheck; + private StatusReportService? _statusReport; + private StatusWebServer? _statusWebServer; + private GalaxyRepositoryStats? _galaxyStats; + + /// + /// Production constructor. Loads configuration from appsettings.json. + /// + public OpcUaService() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _config = new AppConfiguration(); + configuration.GetSection("OpcUa").Bind(_config.OpcUa); + configuration.GetSection("MxAccess").Bind(_config.MxAccess); + configuration.GetSection("GalaxyRepository").Bind(_config.GalaxyRepository); + configuration.GetSection("Dashboard").Bind(_config.Dashboard); + + _mxProxy = new MxProxyAdapter(); + _galaxyRepository = new GalaxyRepositoryService(_config.GalaxyRepository); + } + + /// + /// Test constructor. Accepts injected dependencies. + /// + internal OpcUaService(AppConfiguration config, IMxProxy? mxProxy, IGalaxyRepository? galaxyRepository) + { + _config = config; + _mxProxy = mxProxy; + _galaxyRepository = galaxyRepository; + } + + public void Start() + { + Log.Information("LmxOpcUa service starting"); + + try + { + // Step 2: Validate config + if (!ConfigurationValidator.ValidateAndLog(_config)) + { + Log.Error("Configuration validation failed"); + throw new InvalidOperationException("Configuration validation failed"); + } + + // Step 3: Register exception handler (SVC-006) + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + // Step 4: Create PerformanceMetrics + _cts = new CancellationTokenSource(); + _metrics = new PerformanceMetrics(); + + // Step 5: Create MxAccessClient → Connect + if (_mxProxy != null) + { + try + { + _staThread = new StaComThread(); + _staThread.Start(); + _mxAccessClient = new MxAccessClient(_staThread, _mxProxy, _config.MxAccess, _metrics); + _mxAccessClient.ConnectAsync(_cts.Token).GetAwaiter().GetResult(); + + // Step 6: Start monitor loop + _mxAccessClient.StartMonitor(); + } + catch (Exception ex) + { + Log.Warning(ex, "MxAccess connection failed — continuing without runtime data access"); + _mxAccessClient?.Dispose(); + _mxAccessClient = null; + _staThread?.Dispose(); + _staThread = null; + } + } + + // Step 7: Create GalaxyRepositoryService → TestConnection + _galaxyStats = new GalaxyRepositoryStats { GalaxyName = _config.OpcUa.GalaxyName }; + + if (_galaxyRepository != null) + { + var dbOk = _galaxyRepository.TestConnectionAsync(_cts.Token).GetAwaiter().GetResult(); + _galaxyStats.DbConnected = dbOk; + if (!dbOk) + Log.Warning("Galaxy repository database connection failed — continuing without initial data"); + } + + // Step 8: Create OPC UA server host + node manager + IMxAccessClient mxClient = _mxAccessClient ?? (IMxAccessClient)new NullMxAccessClient(); + _serverHost = new OpcUaServerHost(_config.OpcUa, mxClient, _metrics); + + // Step 9-10: Query hierarchy, start server, build address space + if (_galaxyRepository != null && _galaxyStats.DbConnected) + { + try + { + var hierarchy = _galaxyRepository.GetHierarchyAsync(_cts.Token).GetAwaiter().GetResult(); + var attributes = _galaxyRepository.GetAttributesAsync(_cts.Token).GetAwaiter().GetResult(); + _galaxyStats.ObjectCount = hierarchy.Count; + _galaxyStats.AttributeCount = attributes.Count; + + _serverHost.StartAsync().GetAwaiter().GetResult(); + _nodeManager = _serverHost.NodeManager; + + if (_nodeManager != null) + { + _nodeManager.BuildAddressSpace(hierarchy, attributes); + _galaxyStats.LastRebuildTime = DateTime.UtcNow; + } + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to build initial address space"); + if (!_serverHost.IsRunning) + { + _serverHost.StartAsync().GetAwaiter().GetResult(); + _nodeManager = _serverHost.NodeManager; + } + } + } + else + { + _serverHost.StartAsync().GetAwaiter().GetResult(); + _nodeManager = _serverHost.NodeManager; + } + + // Step 11-12: Change detection wired to rebuild + if (_galaxyRepository != null) + { + _changeDetection = new ChangeDetectionService(_galaxyRepository, _config.GalaxyRepository.ChangeDetectionIntervalSeconds); + _changeDetection.OnGalaxyChanged += OnGalaxyChanged; + _changeDetection.Start(); + } + + // Step 13: Dashboard + _healthCheck = new HealthCheckService(); + _statusReport = new StatusReportService(_healthCheck, _config.Dashboard.RefreshIntervalSeconds); + _statusReport.SetComponents(_mxAccessClient, _metrics, _galaxyStats, _serverHost); + + if (_config.Dashboard.Enabled) + { + _statusWebServer = new StatusWebServer(_statusReport, _config.Dashboard.Port); + _statusWebServer.Start(); + } + + // Step 14 + Log.Information("LmxOpcUa service started successfully"); + } + catch (Exception ex) + { + Log.Fatal(ex, "LmxOpcUa service failed to start"); + throw; + } + } + + public void Stop() + { + Log.Information("LmxOpcUa service stopping"); + + try + { + _cts?.Cancel(); + _changeDetection?.Stop(); + _serverHost?.Stop(); + + if (_mxAccessClient != null) + { + _mxAccessClient.StopMonitor(); + _mxAccessClient.DisconnectAsync().GetAwaiter().GetResult(); + _mxAccessClient.Dispose(); + } + _staThread?.Dispose(); + + _statusWebServer?.Dispose(); + _metrics?.Dispose(); + _changeDetection?.Dispose(); + _cts?.Dispose(); + + AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; + } + catch (Exception ex) + { + Log.Warning(ex, "Error during service shutdown"); + } + + Log.Information("Service shutdown complete"); + } + + private void OnGalaxyChanged() + { + Log.Information("Galaxy change detected — rebuilding address space"); + try + { + if (_galaxyRepository == null || _nodeManager == null) return; + + var hierarchy = _galaxyRepository.GetHierarchyAsync().GetAwaiter().GetResult(); + var attributes = _galaxyRepository.GetAttributesAsync().GetAwaiter().GetResult(); + + _nodeManager.RebuildAddressSpace(hierarchy, attributes); + + if (_galaxyStats != null) + { + _galaxyStats.ObjectCount = hierarchy.Count; + _galaxyStats.AttributeCount = attributes.Count; + _galaxyStats.LastRebuildTime = DateTime.UtcNow; + _galaxyStats.LastDeployTime = _changeDetection?.LastKnownDeployTime; + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to rebuild address space"); + } + } + + private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception (IsTerminating={IsTerminating})", e.IsTerminating); + } + + // Accessors for testing + internal IMxAccessClient? MxClient => _mxAccessClient; + internal PerformanceMetrics? Metrics => _metrics; + internal OpcUaServerHost? ServerHost => _serverHost; + internal LmxNodeManager? NodeManagerInstance => _nodeManager; + internal ChangeDetectionService? ChangeDetectionInstance => _changeDetection; + internal StatusWebServer? StatusWeb => _statusWebServer; + internal StatusReportService? StatusReportInstance => _statusReport; + internal GalaxyRepositoryStats? GalaxyStatsInstance => _galaxyStats; + } + + /// + /// Null implementation of IMxAccessClient for when MXAccess is not available. + /// + internal sealed class NullMxAccessClient : IMxAccessClient + { + public ConnectionState State => ConnectionState.Disconnected; + public int ActiveSubscriptionCount => 0; + public int ReconnectCount => 0; + public event EventHandler? ConnectionStateChanged; + public event Action? OnTagValueChanged; + + public System.Threading.Tasks.Task ConnectAsync(CancellationToken ct = default) => System.Threading.Tasks.Task.CompletedTask; + public System.Threading.Tasks.Task DisconnectAsync() => System.Threading.Tasks.Task.CompletedTask; + public System.Threading.Tasks.Task SubscribeAsync(string fullTagReference, Action callback) => System.Threading.Tasks.Task.CompletedTask; + public System.Threading.Tasks.Task UnsubscribeAsync(string fullTagReference) => System.Threading.Tasks.Task.CompletedTask; + public System.Threading.Tasks.Task ReadAsync(string fullTagReference, CancellationToken ct = default) => System.Threading.Tasks.Task.FromResult(Vtq.Bad()); + public System.Threading.Tasks.Task WriteAsync(string fullTagReference, object value, CancellationToken ct = default) => System.Threading.Tasks.Task.FromResult(false); + public void Dispose() { } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Program.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Program.cs new file mode 100644 index 0000000..61473d4 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Program.cs @@ -0,0 +1,53 @@ +using System; +using Serilog; +using Topshelf; + +namespace ZB.MOM.WW.LmxOpcUa.Host +{ + internal static class Program + { + static int Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console() + .WriteTo.File( + path: "logs/lmxopcua-.log", + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 31) + .CreateLogger(); + + try + { + var exitCode = HostFactory.Run(host => + { + host.UseSerilog(); + + host.Service(svc => + { + svc.ConstructUsing(() => new OpcUaService()); + svc.WhenStarted(s => s.Start()); + svc.WhenStopped(s => s.Stop()); + }); + + host.SetServiceName("LmxOpcUa"); + host.SetDisplayName("LMX OPC UA Server"); + host.SetDescription("OPC UA server exposing System Platform Galaxy tags via MXAccess."); + host.RunAsLocalSystem(); + host.StartAutomatically(); + }); + + return (int)exitCode; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/HealthCheckService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/HealthCheckService.cs new file mode 100644 index 0000000..64352a1 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/HealthCheckService.cs @@ -0,0 +1,57 @@ +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Status +{ + /// + /// Determines health status based on connection state and operation success rates. (DASH-003) + /// + public class HealthCheckService + { + public HealthInfo CheckHealth(ConnectionState connectionState, PerformanceMetrics? metrics) + { + // Rule 1: Not connected → Unhealthy + if (connectionState != ConnectionState.Connected) + { + return new HealthInfo + { + Status = "Unhealthy", + Message = $"MXAccess not connected (state: {connectionState})", + Color = "red" + }; + } + + // Rule 2: Success rate < 50% with > 100 ops → Degraded + if (metrics != null) + { + var stats = metrics.GetStatistics(); + foreach (var kvp in stats) + { + if (kvp.Value.TotalCount > 100 && kvp.Value.SuccessRate < 0.5) + { + return new HealthInfo + { + Status = "Degraded", + Message = $"{kvp.Key} success rate is {kvp.Value.SuccessRate:P0} ({kvp.Value.TotalCount} ops)", + Color = "yellow" + }; + } + } + } + + // Rule 3: All good + return new HealthInfo + { + Status = "Healthy", + Message = "All systems operational", + Color = "green" + }; + } + + public bool IsHealthy(ConnectionState connectionState, PerformanceMetrics? metrics) + { + var health = CheckHealth(connectionState, metrics); + return health.Status != "Unhealthy"; + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs new file mode 100644 index 0000000..498a5a6 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Status +{ + /// + /// DTO containing all dashboard data. (DASH-001 through DASH-009) + /// + public class StatusData + { + public ConnectionInfo Connection { get; set; } = new(); + public HealthInfo Health { get; set; } = new(); + public SubscriptionInfo Subscriptions { get; set; } = new(); + public GalaxyInfo Galaxy { get; set; } = new(); + public Dictionary Operations { get; set; } = new(); + public FooterInfo Footer { get; set; } = new(); + } + + public class ConnectionInfo + { + public string State { get; set; } = "Disconnected"; + public int ReconnectCount { get; set; } + public int ActiveSessions { get; set; } + } + + public class HealthInfo + { + public string Status { get; set; } = "Unknown"; + public string Message { get; set; } = ""; + public string Color { get; set; } = "gray"; + } + + public class SubscriptionInfo + { + public int ActiveCount { get; set; } + } + + public class GalaxyInfo + { + public string GalaxyName { get; set; } = ""; + public bool DbConnected { get; set; } + public DateTime? LastDeployTime { get; set; } + public int ObjectCount { get; set; } + public int AttributeCount { get; set; } + public DateTime? LastRebuildTime { get; set; } + } + + public class FooterInfo + { + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public string Version { get; set; } = ""; + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs new file mode 100644 index 0000000..8ce5359 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs @@ -0,0 +1,146 @@ +using System; +using System.Text; +using System.Text.Json; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Status +{ + /// + /// Aggregates status from all components and generates HTML/JSON reports. (DASH-001 through DASH-009) + /// + public class StatusReportService + { + private readonly HealthCheckService _healthCheck; + private readonly int _refreshIntervalSeconds; + + private IMxAccessClient? _mxAccessClient; + private PerformanceMetrics? _metrics; + private GalaxyRepositoryStats? _galaxyStats; + private OpcUaServerHost? _serverHost; + + public StatusReportService(HealthCheckService healthCheck, int refreshIntervalSeconds) + { + _healthCheck = healthCheck; + _refreshIntervalSeconds = refreshIntervalSeconds; + } + + public void SetComponents(IMxAccessClient? mxAccessClient, PerformanceMetrics? metrics, + GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost) + { + _mxAccessClient = mxAccessClient; + _metrics = metrics; + _galaxyStats = galaxyStats; + _serverHost = serverHost; + } + + public StatusData GetStatusData() + { + var connectionState = _mxAccessClient?.State ?? ConnectionState.Disconnected; + + return new StatusData + { + Connection = new ConnectionInfo + { + State = connectionState.ToString(), + ReconnectCount = _mxAccessClient?.ReconnectCount ?? 0, + ActiveSessions = _serverHost?.ActiveSessionCount ?? 0 + }, + Health = _healthCheck.CheckHealth(connectionState, _metrics), + Subscriptions = new SubscriptionInfo + { + ActiveCount = _mxAccessClient?.ActiveSubscriptionCount ?? 0 + }, + Galaxy = new GalaxyInfo + { + GalaxyName = _galaxyStats?.GalaxyName ?? "", + DbConnected = _galaxyStats?.DbConnected ?? false, + LastDeployTime = _galaxyStats?.LastDeployTime, + ObjectCount = _galaxyStats?.ObjectCount ?? 0, + AttributeCount = _galaxyStats?.AttributeCount ?? 0, + LastRebuildTime = _galaxyStats?.LastRebuildTime + }, + Operations = _metrics?.GetStatistics() ?? new(), + Footer = new FooterInfo + { + Timestamp = DateTime.UtcNow, + Version = typeof(StatusReportService).Assembly.GetName().Version?.ToString() ?? "1.0.0" + } + }; + } + + public string GenerateHtml() + { + var data = GetStatusData(); + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine($""); + sb.AppendLine("LmxOpcUa Status"); + sb.AppendLine(""); + sb.AppendLine("

LmxOpcUa Status Dashboard

"); + + // Connection panel + var connColor = data.Connection.State == "Connected" ? "green" : data.Connection.State == "Connecting" ? "yellow" : "red"; + sb.AppendLine($"

Connection

"); + sb.AppendLine($"

State: {data.Connection.State} | Reconnects: {data.Connection.ReconnectCount} | Sessions: {data.Connection.ActiveSessions}

"); + sb.AppendLine("
"); + + // Health panel + sb.AppendLine($"

Health

"); + sb.AppendLine($"

Status: {data.Health.Status} — {data.Health.Message}

"); + sb.AppendLine("
"); + + // Subscriptions panel + sb.AppendLine("

Subscriptions

"); + sb.AppendLine($"

Active: {data.Subscriptions.ActiveCount}

"); + sb.AppendLine("
"); + + // Galaxy Info panel + sb.AppendLine("

Galaxy Info

"); + sb.AppendLine($"

Galaxy: {data.Galaxy.GalaxyName} | DB: {(data.Galaxy.DbConnected ? "Connected" : "Disconnected")}

"); + sb.AppendLine($"

Last Deploy: {data.Galaxy.LastDeployTime:O} | Objects: {data.Galaxy.ObjectCount} | Attributes: {data.Galaxy.AttributeCount}

"); + sb.AppendLine($"

Last Rebuild: {data.Galaxy.LastRebuildTime:O}

"); + sb.AppendLine("
"); + + // Operations table + sb.AppendLine("

Operations

"); + sb.AppendLine(""); + foreach (var kvp in data.Operations) + { + var s = kvp.Value; + sb.AppendLine($"" + + $""); + } + sb.AppendLine("
OperationCountSuccess RateAvg (ms)Min (ms)Max (ms)P95 (ms)
{kvp.Key}{s.TotalCount}{s.SuccessRate:P1}{s.AverageMilliseconds:F1}{s.MinMilliseconds:F1}{s.MaxMilliseconds:F1}{s.Percentile95Milliseconds:F1}
"); + + // Footer + sb.AppendLine("

Footer

"); + sb.AppendLine($"

Generated: {data.Footer.Timestamp:O} | Version: {data.Footer.Version}

"); + sb.AppendLine("
"); + + sb.AppendLine(""); + return sb.ToString(); + } + + public string GenerateJson() + { + var data = GetStatusData(); + return JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); + } + + public bool IsHealthy() + { + var state = _mxAccessClient?.State ?? ConnectionState.Disconnected; + return _healthCheck.IsHealthy(state, _metrics); + } + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusWebServer.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusWebServer.cs new file mode 100644 index 0000000..b2b5ca7 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusWebServer.cs @@ -0,0 +1,144 @@ +using System; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Serilog; + +namespace ZB.MOM.WW.LmxOpcUa.Host.Status +{ + /// + /// HTTP server for status dashboard. Routes: / → HTML, /api/status → JSON, /api/health → 200/503. (DASH-001) + /// + public class StatusWebServer : IDisposable + { + private static readonly ILogger Log = Serilog.Log.ForContext(); + + private readonly StatusReportService _reportService; + private readonly int _port; + private HttpListener? _listener; + private CancellationTokenSource? _cts; + + public bool IsRunning => _listener?.IsListening ?? false; + + public StatusWebServer(StatusReportService reportService, int port) + { + _reportService = reportService; + _port = port; + } + + public void Start() + { + try + { + _listener = new HttpListener(); + _listener.Prefixes.Add($"http://+:{_port}/"); + _listener.Start(); + + _cts = new CancellationTokenSource(); + Task.Run(() => ListenLoopAsync(_cts.Token)); + + Log.Information("Status dashboard started on http://localhost:{Port}/", _port); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to start status dashboard on port {Port}", _port); + _listener = null; + } + } + + public void Stop() + { + _cts?.Cancel(); + try + { + _listener?.Stop(); + _listener?.Close(); + } + catch { /* ignore */ } + _listener = null; + Log.Information("Status dashboard stopped"); + } + + private async Task ListenLoopAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested && _listener != null && _listener.IsListening) + { + try + { + var context = await _listener.GetContextAsync(); + _ = HandleRequestAsync(context); + } + catch (ObjectDisposedException) { break; } + catch (HttpListenerException) { break; } + catch (Exception ex) + { + Log.Warning(ex, "Dashboard listener error"); + } + } + } + + private async Task HandleRequestAsync(HttpListenerContext context) + { + try + { + var request = context.Request; + var response = context.Response; + + // Only allow GET + if (request.HttpMethod != "GET") + { + response.StatusCode = 405; + response.Close(); + return; + } + + // No-cache headers + response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); + response.Headers.Add("Pragma", "no-cache"); + response.Headers.Add("Expires", "0"); + + var path = request.Url?.AbsolutePath ?? "/"; + + switch (path) + { + case "/": + await WriteResponse(response, _reportService.GenerateHtml(), "text/html", 200); + break; + + case "/api/status": + await WriteResponse(response, _reportService.GenerateJson(), "application/json", 200); + break; + + case "/api/health": + var isHealthy = _reportService.IsHealthy(); + var healthJson = isHealthy ? "{\"status\":\"healthy\"}" : "{\"status\":\"unhealthy\"}"; + await WriteResponse(response, healthJson, "application/json", isHealthy ? 200 : 503); + break; + + default: + response.StatusCode = 404; + response.Close(); + break; + } + } + catch (Exception ex) + { + Log.Warning(ex, "Error handling dashboard request"); + try { context.Response.Close(); } catch { } + } + } + + private static async Task WriteResponse(HttpListenerResponse response, string body, string contentType, int statusCode) + { + var buffer = Encoding.UTF8.GetBytes(body); + response.StatusCode = statusCode; + response.ContentType = contentType; + response.ContentLength64 = buffer.Length; + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.Close(); + } + + public void Dispose() => Stop(); + } +} diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/ZB.MOM.WW.LmxOpcUa.Host.csproj b/src/ZB.MOM.WW.LmxOpcUa.Host/ZB.MOM.WW.LmxOpcUa.Host.csproj new file mode 100644 index 0000000..bf94ba0 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/ZB.MOM.WW.LmxOpcUa.Host.csproj @@ -0,0 +1,54 @@ + + + + Exe + net48 + x86 + 9.0 + enable + ZB.MOM.WW.LmxOpcUa.Host + ZB.MOM.WW.LmxOpcUa.Host + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\lib\ArchestrA.MxAccess.dll + false + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json b/src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json new file mode 100644 index 0000000..29c9623 --- /dev/null +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json @@ -0,0 +1,32 @@ +{ + "OpcUa": { + "Port": 4840, + "EndpointPath": "/LmxOpcUa", + "ServerName": "LmxOpcUa", + "GalaxyName": "ZB", + "MaxSessions": 100, + "SessionTimeoutMinutes": 30 + }, + "MxAccess": { + "ClientName": "LmxOpcUa", + "NodeName": null, + "GalaxyName": null, + "ReadTimeoutSeconds": 5, + "WriteTimeoutSeconds": 5, + "MaxConcurrentOperations": 10, + "MonitorIntervalSeconds": 5, + "AutoReconnect": true, + "ProbeTag": null, + "ProbeStaleThresholdSeconds": 60 + }, + "GalaxyRepository": { + "ConnectionString": "Server=localhost;Database=ZB;Integrated Security=true;", + "ChangeDetectionIntervalSeconds": 30, + "CommandTimeoutSeconds": 30 + }, + "Dashboard": { + "Enabled": true, + "Port": 8081, + "RefreshIntervalSeconds": 10 + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/SampleIntegrationTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/SampleIntegrationTest.cs new file mode 100644 index 0000000..bad6eba --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/SampleIntegrationTest.cs @@ -0,0 +1,14 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests +{ + public class SampleIntegrationTest + { + [Fact] + public void Placeholder_ShouldPass() + { + true.ShouldBeTrue(); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/ZB.MOM.WW.LmxOpcUa.IntegrationTests.csproj b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/ZB.MOM.WW.LmxOpcUa.IntegrationTests.csproj new file mode 100644 index 0000000..0a7bcd4 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/ZB.MOM.WW.LmxOpcUa.IntegrationTests.csproj @@ -0,0 +1,38 @@ + + + + net48 + x86 + 9.0 + enable + false + true + ZB.MOM.WW.LmxOpcUa.IntegrationTests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/appsettings.test.json b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/appsettings.test.json new file mode 100644 index 0000000..81a34e7 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/appsettings.test.json @@ -0,0 +1,5 @@ +{ + "GalaxyRepository": { + "ConnectionString": "Server=localhost;Database=ZB;Integrated Security=true;" + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/xunit.runner.json b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/xunit.runner.json new file mode 100644 index 0000000..08c512b --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Configuration/ConfigurationLoadingTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Configuration/ConfigurationLoadingTests.cs new file mode 100644 index 0000000..6361b22 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Configuration/ConfigurationLoadingTests.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Configuration; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration +{ + public class ConfigurationLoadingTests + { + private static AppConfiguration LoadFromJson() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false) + .Build(); + + var config = new AppConfiguration(); + configuration.GetSection("OpcUa").Bind(config.OpcUa); + configuration.GetSection("MxAccess").Bind(config.MxAccess); + configuration.GetSection("GalaxyRepository").Bind(config.GalaxyRepository); + configuration.GetSection("Dashboard").Bind(config.Dashboard); + return config; + } + + [Fact] + public void OpcUa_Section_BindsCorrectly() + { + var config = LoadFromJson(); + config.OpcUa.Port.ShouldBe(4840); + config.OpcUa.EndpointPath.ShouldBe("/LmxOpcUa"); + config.OpcUa.ServerName.ShouldBe("LmxOpcUa"); + config.OpcUa.GalaxyName.ShouldBe("ZB"); + config.OpcUa.MaxSessions.ShouldBe(100); + config.OpcUa.SessionTimeoutMinutes.ShouldBe(30); + } + + [Fact] + public void MxAccess_Section_BindsCorrectly() + { + var config = LoadFromJson(); + config.MxAccess.ClientName.ShouldBe("LmxOpcUa"); + config.MxAccess.ReadTimeoutSeconds.ShouldBe(5); + config.MxAccess.WriteTimeoutSeconds.ShouldBe(5); + config.MxAccess.MaxConcurrentOperations.ShouldBe(10); + config.MxAccess.MonitorIntervalSeconds.ShouldBe(5); + config.MxAccess.AutoReconnect.ShouldBe(true); + config.MxAccess.ProbeStaleThresholdSeconds.ShouldBe(60); + } + + [Fact] + public void GalaxyRepository_Section_BindsCorrectly() + { + var config = LoadFromJson(); + config.GalaxyRepository.ConnectionString.ShouldContain("ZB"); + config.GalaxyRepository.ChangeDetectionIntervalSeconds.ShouldBe(30); + config.GalaxyRepository.CommandTimeoutSeconds.ShouldBe(30); + } + + [Fact] + public void Dashboard_Section_BindsCorrectly() + { + var config = LoadFromJson(); + config.Dashboard.Enabled.ShouldBe(true); + config.Dashboard.Port.ShouldBe(8081); + config.Dashboard.RefreshIntervalSeconds.ShouldBe(10); + } + + [Fact] + public void DefaultValues_AreCorrect() + { + var config = new AppConfiguration(); + config.OpcUa.Port.ShouldBe(4840); + config.MxAccess.ClientName.ShouldBe("LmxOpcUa"); + config.GalaxyRepository.ChangeDetectionIntervalSeconds.ShouldBe(30); + config.Dashboard.Enabled.ShouldBe(true); + } + + [Fact] + public void Validator_ValidConfig_ReturnsTrue() + { + var config = LoadFromJson(); + ConfigurationValidator.ValidateAndLog(config).ShouldBe(true); + } + + [Fact] + public void Validator_InvalidPort_ReturnsFalse() + { + var config = new AppConfiguration(); + config.OpcUa.Port = 0; + ConfigurationValidator.ValidateAndLog(config).ShouldBe(false); + } + + [Fact] + public void Validator_EmptyGalaxyName_ReturnsFalse() + { + var config = new AppConfiguration(); + config.OpcUa.GalaxyName = ""; + ConfigurationValidator.ValidateAndLog(config).ShouldBe(false); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxDataTypeMapperTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxDataTypeMapperTests.cs new file mode 100644 index 0000000..e8b4ea5 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxDataTypeMapperTests.cs @@ -0,0 +1,71 @@ +using System; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain +{ + public class MxDataTypeMapperTests + { + [Theory] + [InlineData(1, 1u)] // Boolean + [InlineData(2, 6u)] // Integer → Int32 + [InlineData(3, 10u)] // Float + [InlineData(4, 11u)] // Double + [InlineData(5, 12u)] // String + [InlineData(6, 13u)] // DateTime + [InlineData(7, 11u)] // ElapsedTime → Double + [InlineData(8, 12u)] // Reference → String + [InlineData(13, 6u)] // Enumeration → Int32 + [InlineData(14, 12u)] // Custom → String + [InlineData(15, 21u)] // InternationalizedString → LocalizedText + [InlineData(16, 12u)] // Custom → String + public void MapToOpcUaDataType_AllKnownTypes(int mxDataType, uint expectedNodeId) + { + MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(expectedNodeId); + } + + [Theory] + [InlineData(0)] + [InlineData(99)] + [InlineData(-1)] + public void MapToOpcUaDataType_UnknownDefaultsToString(int mxDataType) + { + MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(12u); // String + } + + [Theory] + [InlineData(1, typeof(bool))] + [InlineData(2, typeof(int))] + [InlineData(3, typeof(float))] + [InlineData(4, typeof(double))] + [InlineData(5, typeof(string))] + [InlineData(6, typeof(DateTime))] + [InlineData(7, typeof(double))] + [InlineData(8, typeof(string))] + [InlineData(13, typeof(int))] + [InlineData(15, typeof(string))] + public void MapToClrType_AllKnownTypes(int mxDataType, Type expectedType) + { + MxDataTypeMapper.MapToClrType(mxDataType).ShouldBe(expectedType); + } + + [Fact] + public void MapToClrType_UnknownDefaultsToString() + { + MxDataTypeMapper.MapToClrType(999).ShouldBe(typeof(string)); + } + + [Fact] + public void GetOpcUaTypeName_Boolean() + { + MxDataTypeMapper.GetOpcUaTypeName(1).ShouldBe("Boolean"); + } + + [Fact] + public void GetOpcUaTypeName_Unknown_ReturnsString() + { + MxDataTypeMapper.GetOpcUaTypeName(999).ShouldBe("String"); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxErrorCodesTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxErrorCodesTests.cs new file mode 100644 index 0000000..49454ac --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/MxErrorCodesTests.cs @@ -0,0 +1,46 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain +{ + public class MxErrorCodesTests + { + [Theory] + [InlineData(1008, "Invalid reference")] + [InlineData(1012, "Wrong data type")] + [InlineData(1013, "Not writable")] + [InlineData(1014, "Request timed out")] + [InlineData(1015, "Communication failure")] + [InlineData(1016, "Not connected")] + public void GetMessage_KnownCodes_ContainsDescription(int code, string expectedSubstring) + { + MxErrorCodes.GetMessage(code).ShouldContain(expectedSubstring); + } + + [Fact] + public void GetMessage_UnknownCode_ReturnsUnknown() + { + MxErrorCodes.GetMessage(9999).ShouldContain("Unknown"); + MxErrorCodes.GetMessage(9999).ShouldContain("9999"); + } + + [Theory] + [InlineData(1008, Quality.BadConfigError)] + [InlineData(1012, Quality.BadConfigError)] + [InlineData(1013, Quality.BadOutOfService)] + [InlineData(1014, Quality.BadCommFailure)] + [InlineData(1015, Quality.BadCommFailure)] + [InlineData(1016, Quality.BadNotConnected)] + public void MapToQuality_KnownCodes(int code, Quality expected) + { + MxErrorCodes.MapToQuality(code).ShouldBe(expected); + } + + [Fact] + public void MapToQuality_UnknownCode_ReturnsBad() + { + MxErrorCodes.MapToQuality(9999).ShouldBe(Quality.Bad); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/QualityMapperTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/QualityMapperTests.cs new file mode 100644 index 0000000..a79a117 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Domain/QualityMapperTests.cs @@ -0,0 +1,101 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain +{ + public class QualityMapperTests + { + [Theory] + [InlineData(0, Quality.Bad)] + [InlineData(4, Quality.BadConfigError)] + [InlineData(20, Quality.BadCommFailure)] + [InlineData(32, Quality.BadWaitingForInitialData)] + public void MapFromMxAccess_BadFamily(int input, Quality expected) + { + QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected); + } + + [Theory] + [InlineData(64, Quality.Uncertain)] + [InlineData(68, Quality.UncertainLastUsable)] + [InlineData(88, Quality.UncertainSubNormal)] + public void MapFromMxAccess_UncertainFamily(int input, Quality expected) + { + QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected); + } + + [Theory] + [InlineData(192, Quality.Good)] + [InlineData(216, Quality.GoodLocalOverride)] + public void MapFromMxAccess_GoodFamily(int input, Quality expected) + { + QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected); + } + + [Fact] + public void MapFromMxAccess_UnknownBadValue_ReturnsBad() + { + QualityMapper.MapFromMxAccessQuality(63).ShouldBe(Quality.Bad); + } + + [Fact] + public void MapFromMxAccess_UnknownUncertainValue_ReturnsUncertain() + { + QualityMapper.MapFromMxAccessQuality(100).ShouldBe(Quality.Uncertain); + } + + [Fact] + public void MapFromMxAccess_UnknownGoodValue_ReturnsGood() + { + QualityMapper.MapFromMxAccessQuality(200).ShouldBe(Quality.Good); + } + + [Fact] + public void MapToOpcUa_Good_Returns0() + { + QualityMapper.MapToOpcUaStatusCode(Quality.Good).ShouldBe(0x00000000u); + } + + [Fact] + public void MapToOpcUa_Bad_Returns80000000() + { + QualityMapper.MapToOpcUaStatusCode(Quality.Bad).ShouldBe(0x80000000u); + } + + [Fact] + public void MapToOpcUa_BadCommFailure() + { + QualityMapper.MapToOpcUaStatusCode(Quality.BadCommFailure).ShouldBe(0x80050000u); + } + + [Fact] + public void MapToOpcUa_Uncertain() + { + QualityMapper.MapToOpcUaStatusCode(Quality.Uncertain).ShouldBe(0x40000000u); + } + + [Fact] + public void QualityExtensions_IsGood() + { + Quality.Good.IsGood().ShouldBe(true); + Quality.Good.IsBad().ShouldBe(false); + Quality.Good.IsUncertain().ShouldBe(false); + } + + [Fact] + public void QualityExtensions_IsBad() + { + Quality.Bad.IsBad().ShouldBe(true); + Quality.Bad.IsGood().ShouldBe(false); + } + + [Fact] + public void QualityExtensions_IsUncertain() + { + Quality.Uncertain.IsUncertain().ShouldBe(true); + Quality.Uncertain.IsGood().ShouldBe(false); + Quality.Uncertain.IsBad().ShouldBe(false); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/EndToEnd/FullDataFlowTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/EndToEnd/FullDataFlowTest.cs new file mode 100644 index 0000000..9a9be92 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/EndToEnd/FullDataFlowTest.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd +{ + /// + /// THE ULTIMATE SMOKE TEST: Full service with fakes, verifying the complete data flow. + /// (1) Address space built, (2) MXAccess data change → callback, (3) read → correct tag ref, + /// (4) write → correct tag+value, (5) dashboard has real data. + /// + public class FullDataFlowTest + { + [Fact] + public void FullDataFlow_EndToEnd() + { + var config = new AppConfiguration + { + OpcUa = new OpcUaConfiguration { Port = 14842, GalaxyName = "TestGalaxy", EndpointPath = "/LmxOpcUa" }, + MxAccess = new MxAccessConfiguration { ClientName = "Test", ReadTimeoutSeconds = 2, WriteTimeoutSeconds = 2 }, + GalaxyRepository = new GalaxyRepositoryConfiguration { ChangeDetectionIntervalSeconds = 60 }, + Dashboard = new DashboardConfiguration { Enabled = false } + }; + + var proxy = new FakeMxProxy(); + var repo = new FakeGalaxyRepository + { + Hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "DEV", BrowseName = "DEV", ParentGobjectId = 0, IsArea = true }, + new GalaxyObjectInfo { GobjectId = 2, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 1, IsArea = false }, + new GalaxyObjectInfo { GobjectId = 3, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 2, IsArea = false } + }, + Attributes = new List + { + new GalaxyAttributeInfo { GobjectId = 2, TagName = "TestMachine_001", AttributeName = "MachineID", FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 3, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 3, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber", FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false } + } + }; + + var service = new OpcUaService(config, proxy, repo); + service.Start(); + + try + { + // (1) OPC UA server host created + service.ServerHost.ShouldNotBeNull(); + + // (2) MXAccess connected and proxy registered + proxy.IsRegistered.ShouldBe(true); + service.MxClient.ShouldNotBeNull(); + service.MxClient!.State.ShouldBe(ConnectionState.Connected); + + // (3) Address space model can be built from the same data + var model = Host.OpcUa.AddressSpaceBuilder.Build(repo.Hierarchy, repo.Attributes); + model.NodeIdToTagReference.ContainsKey("TestMachine_001.MachineID").ShouldBe(true); + model.NodeIdToTagReference.ContainsKey("DelmiaReceiver_001.DownloadPath").ShouldBe(true); + model.NodeIdToTagReference.ContainsKey("DelmiaReceiver_001.JobStepNumber").ShouldBe(true); + model.VariableCount.ShouldBe(3); + model.ObjectCount.ShouldBe(2); // TestMachine + DelmiaReceiver (DEV is area) + + // (4) Tag reference resolves correctly for read/write + var tagRef = model.NodeIdToTagReference["DelmiaReceiver_001.DownloadPath"]; + tagRef.ShouldBe("DelmiaReceiver_001.DownloadPath"); + + // (5) Galaxy stats have real data + service.GalaxyStatsInstance.ShouldNotBeNull(); + service.GalaxyStatsInstance!.GalaxyName.ShouldBe("TestGalaxy"); + service.GalaxyStatsInstance.DbConnected.ShouldBe(true); + service.GalaxyStatsInstance.ObjectCount.ShouldBe(3); + service.GalaxyStatsInstance.AttributeCount.ShouldBe(3); + + // (5b) Status report has real data + service.StatusReportInstance.ShouldNotBeNull(); + var html = service.StatusReportInstance!.GenerateHtml(); + html.ShouldContain("TestGalaxy"); + html.ShouldContain("Connected"); + + var json = service.StatusReportInstance.GenerateJson(); + json.ShouldContain("TestGalaxy"); + + service.StatusReportInstance.IsHealthy().ShouldBe(true); + + // Verify change detection is wired + service.ChangeDetectionInstance.ShouldNotBeNull(); + + // Verify metrics created + service.Metrics.ShouldNotBeNull(); + } + finally + { + service.Stop(); + } + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/GalaxyRepository/ChangeDetectionServiceTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/GalaxyRepository/ChangeDetectionServiceTests.cs new file mode 100644 index 0000000..56f265a --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/GalaxyRepository/ChangeDetectionServiceTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository +{ + public class ChangeDetectionServiceTests + { + [Fact] + public async Task FirstPoll_AlwaysTriggers() + { + var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; + var service = new ChangeDetectionService(repo, intervalSeconds: 1); + var triggered = false; + service.OnGalaxyChanged += () => triggered = true; + + service.Start(); + await Task.Delay(500); + service.Stop(); + + triggered.ShouldBe(true); + service.Dispose(); + } + + [Fact] + public async Task SameTimestamp_DoesNotTriggerAgain() + { + var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; + var service = new ChangeDetectionService(repo, intervalSeconds: 1); + var triggerCount = 0; + service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount); + + service.Start(); + await Task.Delay(2500); // Should have polled at least twice + service.Stop(); + + triggerCount.ShouldBe(1); // Only the first poll + service.Dispose(); + } + + [Fact] + public async Task ChangedTimestamp_TriggersAgain() + { + var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; + var service = new ChangeDetectionService(repo, intervalSeconds: 1); + var triggerCount = 0; + service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount); + + service.Start(); + await Task.Delay(500); + + // Change the deploy time + repo.LastDeployTime = new DateTime(2024, 2, 1); + await Task.Delay(1500); + service.Stop(); + + triggerCount.ShouldBeGreaterThanOrEqualTo(2); + service.Dispose(); + } + + [Fact] + public async Task FailedPoll_DoesNotCrash_RetriesNext() + { + var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; + var service = new ChangeDetectionService(repo, intervalSeconds: 1); + var triggerCount = 0; + service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount); + + service.Start(); + await Task.Delay(500); + + // Make it fail + repo.ShouldThrow = true; + await Task.Delay(1500); + + // Restore and it should recover + repo.ShouldThrow = false; + repo.LastDeployTime = new DateTime(2024, 3, 1); + await Task.Delay(1500); + service.Stop(); + + // Should have triggered at least on first poll and on the changed timestamp + triggerCount.ShouldBeGreaterThanOrEqualTo(1); + service.Dispose(); + } + + [Fact] + public void Stop_BeforeStart_DoesNotThrow() + { + var repo = new FakeGalaxyRepository(); + var service = new ChangeDetectionService(repo, intervalSeconds: 30); + service.Stop(); // Should not throw + service.Dispose(); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeGalaxyRepository.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeGalaxyRepository.cs new file mode 100644 index 0000000..bf0a3a2 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeGalaxyRepository.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers +{ + public class FakeGalaxyRepository : IGalaxyRepository + { + public event Action? OnGalaxyChanged; + + public List Hierarchy { get; set; } = new List(); + public List Attributes { get; set; } = new List(); + public DateTime? LastDeployTime { get; set; } = DateTime.UtcNow; + public bool ConnectionSucceeds { get; set; } = true; + public bool ShouldThrow { get; set; } + + public Task> GetHierarchyAsync(CancellationToken ct = default) + { + if (ShouldThrow) throw new Exception("Simulated DB failure"); + return Task.FromResult(Hierarchy); + } + + public Task> GetAttributesAsync(CancellationToken ct = default) + { + if (ShouldThrow) throw new Exception("Simulated DB failure"); + return Task.FromResult(Attributes); + } + + public Task GetLastDeployTimeAsync(CancellationToken ct = default) + { + if (ShouldThrow) throw new Exception("Simulated DB failure"); + return Task.FromResult(LastDeployTime); + } + + public Task TestConnectionAsync(CancellationToken ct = default) + { + if (ShouldThrow) throw new Exception("Simulated DB failure"); + return Task.FromResult(ConnectionSucceeds); + } + + public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke(); + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxAccessClient.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxAccessClient.cs new file mode 100644 index 0000000..46bd3a0 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxAccessClient.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers +{ + public class FakeMxAccessClient : IMxAccessClient + { + public ConnectionState State { get; set; } = ConnectionState.Connected; + public int ActiveSubscriptionCount => _subscriptions.Count; + public int ReconnectCount { get; set; } + + public event EventHandler? ConnectionStateChanged; + public event Action? OnTagValueChanged; + + private readonly ConcurrentDictionary> _subscriptions = new(StringComparer.OrdinalIgnoreCase); + public ConcurrentDictionary TagValues { get; } = new(StringComparer.OrdinalIgnoreCase); + public List<(string Tag, object Value)> WrittenValues { get; } = new(); + public bool WriteResult { get; set; } = true; + + public Task ConnectAsync(CancellationToken ct = default) + { + State = ConnectionState.Connected; + return Task.CompletedTask; + } + + public Task DisconnectAsync() + { + State = ConnectionState.Disconnected; + return Task.CompletedTask; + } + + public Task SubscribeAsync(string fullTagReference, Action callback) + { + _subscriptions[fullTagReference] = callback; + return Task.CompletedTask; + } + + public Task UnsubscribeAsync(string fullTagReference) + { + _subscriptions.TryRemove(fullTagReference, out _); + return Task.CompletedTask; + } + + public Task ReadAsync(string fullTagReference, CancellationToken ct = default) + { + if (TagValues.TryGetValue(fullTagReference, out var vtq)) + return Task.FromResult(vtq); + return Task.FromResult(Vtq.Bad(Quality.BadNotConnected)); + } + + public Task WriteAsync(string fullTagReference, object value, CancellationToken ct = default) + { + WrittenValues.Add((fullTagReference, value)); + return Task.FromResult(WriteResult); + } + + public void SimulateDataChange(string address, Vtq vtq) + { + OnTagValueChanged?.Invoke(address, vtq); + if (_subscriptions.TryGetValue(address, out var callback)) + callback(address, vtq); + } + + public void RaiseConnectionStateChanged(ConnectionState prev, ConnectionState curr) + { + State = curr; + ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(prev, curr)); + } + + public void Dispose() { } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxProxy.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxProxy.cs new file mode 100644 index 0000000..aa08758 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxProxy.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using ArchestrA.MxAccess; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers +{ + /// + /// Fake IMxProxy for testing without the MxAccess COM runtime. + /// Simulates connections, subscriptions, data changes, and writes. + /// + public class FakeMxProxy : IMxProxy + { + private int _nextHandle = 1; + private int _connectionHandle; + private bool _registered; + + public event MxDataChangeHandler? OnDataChange; + public event MxWriteCompleteHandler? OnWriteComplete; + + public ConcurrentDictionary Items { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary AdvisedItems { get; } = new ConcurrentDictionary(); + public List<(string Address, object Value)> WrittenValues { get; } = new List<(string, object)>(); + + public bool IsRegistered => _registered; + public int RegisterCallCount { get; private set; } + public int UnregisterCallCount { get; private set; } + public bool ShouldFailRegister { get; set; } + public bool ShouldFailWrite { get; set; } + public int WriteCompleteStatus { get; set; } = 0; // 0 = success + + public int Register(string clientName) + { + RegisterCallCount++; + if (ShouldFailRegister) throw new InvalidOperationException("Register failed (simulated)"); + _registered = true; + _connectionHandle = Interlocked.Increment(ref _nextHandle); + return _connectionHandle; + } + + public void Unregister(int handle) + { + UnregisterCallCount++; + _registered = false; + _connectionHandle = 0; + } + + public int AddItem(int handle, string address) + { + var itemHandle = Interlocked.Increment(ref _nextHandle); + Items[itemHandle] = address; + return itemHandle; + } + + public void RemoveItem(int handle, int itemHandle) + { + Items.TryRemove(itemHandle, out _); + } + + public void AdviseSupervisory(int handle, int itemHandle) + { + AdvisedItems[itemHandle] = true; + } + + public void UnAdviseSupervisory(int handle, int itemHandle) + { + AdvisedItems.TryRemove(itemHandle, out _); + } + + public void Write(int handle, int itemHandle, object value, int securityClassification) + { + if (ShouldFailWrite) throw new InvalidOperationException("Write failed (simulated)"); + + if (Items.TryGetValue(itemHandle, out var address)) + WrittenValues.Add((address, value)); + + // Simulate async write complete callback + var status = new MXSTATUS_PROXY[1]; + if (WriteCompleteStatus == 0) + { + status[0].success = 1; + } + else + { + status[0].success = 0; + status[0].detail = (short)WriteCompleteStatus; + } + OnWriteComplete?.Invoke(_connectionHandle, itemHandle, ref status); + } + + /// + /// Simulates an MXAccess data change event for a specific item handle. + /// + public void SimulateDataChange(int itemHandle, object value, int quality = 192, DateTime? timestamp = null) + { + var status = new MXSTATUS_PROXY[1]; + status[0].success = 1; + OnDataChange?.Invoke(_connectionHandle, itemHandle, value, quality, + (object)(timestamp ?? DateTime.UtcNow), ref status); + } + + /// + /// Simulates data change for a specific address (finds handle by address). + /// + public void SimulateDataChangeByAddress(string address, object value, int quality = 192, DateTime? timestamp = null) + { + foreach (var kvp in Items) + { + if (string.Equals(kvp.Value, address, StringComparison.OrdinalIgnoreCase)) + { + SimulateDataChange(kvp.Key, value, quality, timestamp); + return; + } + } + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Metrics/PerformanceMetricsTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Metrics/PerformanceMetricsTests.cs new file mode 100644 index 0000000..45fa61d --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Metrics/PerformanceMetricsTests.cs @@ -0,0 +1,122 @@ +using System; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics +{ + public class PerformanceMetricsTests + { + [Fact] + public void EmptyState_ReturnsZeroStatistics() + { + using var metrics = new PerformanceMetrics(); + var stats = metrics.GetStatistics(); + stats.ShouldBeEmpty(); + } + + [Fact] + public void RecordOperation_TracksCounts() + { + using var metrics = new PerformanceMetrics(); + metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true); + metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(20), false); + + var stats = metrics.GetStatistics(); + stats.ShouldContainKey("Read"); + stats["Read"].TotalCount.ShouldBe(2); + stats["Read"].SuccessCount.ShouldBe(1); + stats["Read"].SuccessRate.ShouldBe(0.5); + } + + [Fact] + public void RecordOperation_TracksMinMaxAverage() + { + using var metrics = new PerformanceMetrics(); + metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(10)); + metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(30)); + metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(20)); + + var stats = metrics.GetStatistics()["Write"]; + stats.MinMilliseconds.ShouldBe(10); + stats.MaxMilliseconds.ShouldBe(30); + stats.AverageMilliseconds.ShouldBe(20); + } + + [Fact] + public void P95_CalculatedCorrectly() + { + using var metrics = new PerformanceMetrics(); + for (int i = 1; i <= 100; i++) + metrics.RecordOperation("Op", TimeSpan.FromMilliseconds(i)); + + var stats = metrics.GetStatistics()["Op"]; + stats.Percentile95Milliseconds.ShouldBe(95); + } + + [Fact] + public void RollingBuffer_EvictsOldEntries() + { + var opMetrics = new OperationMetrics(); + for (int i = 0; i < 1100; i++) + opMetrics.Record(TimeSpan.FromMilliseconds(i), true); + + var stats = opMetrics.GetStatistics(); + stats.TotalCount.ShouldBe(1100); + // P95 should be from the last 1000 entries (100-1099) + stats.Percentile95Milliseconds.ShouldBeGreaterThan(1000); + } + + [Fact] + public void BeginOperation_TimingScopeRecordsOnDispose() + { + using var metrics = new PerformanceMetrics(); + + using (var scope = metrics.BeginOperation("Test")) + { + // Simulate some work + System.Threading.Thread.Sleep(5); + } + + var stats = metrics.GetStatistics(); + stats.ShouldContainKey("Test"); + stats["Test"].TotalCount.ShouldBe(1); + stats["Test"].SuccessCount.ShouldBe(1); + stats["Test"].AverageMilliseconds.ShouldBeGreaterThan(0); + } + + [Fact] + public void BeginOperation_SetSuccessFalse() + { + using var metrics = new PerformanceMetrics(); + + using (var scope = metrics.BeginOperation("Test")) + { + scope.SetSuccess(false); + } + + var stats = metrics.GetStatistics()["Test"]; + stats.TotalCount.ShouldBe(1); + stats.SuccessCount.ShouldBe(0); + } + + [Fact] + public void GetMetrics_UnknownOperation_ReturnsNull() + { + using var metrics = new PerformanceMetrics(); + metrics.GetMetrics("NonExistent").ShouldBeNull(); + } + + [Fact] + public void OperationNames_AreCaseInsensitive() + { + using var metrics = new PerformanceMetrics(); + metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10)); + metrics.RecordOperation("read", TimeSpan.FromMilliseconds(20)); + + var stats = metrics.GetStatistics(); + stats.Count.ShouldBe(1); + stats["READ"].TotalCount.ShouldBe(2); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientConnectionTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientConnectionTests.cs new file mode 100644 index 0000000..ee160dc --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientConnectionTests.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess +{ + public class MxAccessClientConnectionTests : IDisposable + { + private readonly StaComThread _staThread; + private readonly FakeMxProxy _proxy; + private readonly PerformanceMetrics _metrics; + private readonly MxAccessClient _client; + private readonly List<(ConnectionState Previous, ConnectionState Current)> _stateChanges = new(); + + public MxAccessClientConnectionTests() + { + _staThread = new StaComThread(); + _staThread.Start(); + _proxy = new FakeMxProxy(); + _metrics = new PerformanceMetrics(); + var config = new MxAccessConfiguration(); + _client = new MxAccessClient(_staThread, _proxy, config, _metrics); + _client.ConnectionStateChanged += (_, e) => _stateChanges.Add((e.PreviousState, e.CurrentState)); + } + + public void Dispose() + { + _client.Dispose(); + _staThread.Dispose(); + _metrics.Dispose(); + } + + [Fact] + public void InitialState_IsDisconnected() + { + _client.State.ShouldBe(ConnectionState.Disconnected); + } + + [Fact] + public async Task Connect_TransitionsToConnected() + { + await _client.ConnectAsync(); + + _client.State.ShouldBe(ConnectionState.Connected); + _stateChanges.ShouldContain(s => s.Previous == ConnectionState.Disconnected && s.Current == ConnectionState.Connecting); + _stateChanges.ShouldContain(s => s.Previous == ConnectionState.Connecting && s.Current == ConnectionState.Connected); + } + + [Fact] + public async Task Connect_RegistersCalled() + { + await _client.ConnectAsync(); + _proxy.RegisterCallCount.ShouldBe(1); + } + + [Fact] + public async Task Disconnect_TransitionsToDisconnected() + { + await _client.ConnectAsync(); + await _client.DisconnectAsync(); + + _client.State.ShouldBe(ConnectionState.Disconnected); + _stateChanges.ShouldContain(s => s.Current == ConnectionState.Disconnecting); + _stateChanges.ShouldContain(s => s.Current == ConnectionState.Disconnected); + } + + [Fact] + public async Task Disconnect_UnregistersCalled() + { + await _client.ConnectAsync(); + await _client.DisconnectAsync(); + _proxy.UnregisterCallCount.ShouldBe(1); + } + + [Fact] + public async Task ConnectFails_TransitionsToError() + { + _proxy.ShouldFailRegister = true; + + await Should.ThrowAsync(_client.ConnectAsync()); + _client.State.ShouldBe(ConnectionState.Error); + } + + [Fact] + public async Task DoubleConnect_NoOp() + { + await _client.ConnectAsync(); + await _client.ConnectAsync(); // Should be no-op + _proxy.RegisterCallCount.ShouldBe(1); + } + + [Fact] + public async Task Reconnect_IncrementsCount() + { + await _client.ConnectAsync(); + _client.ReconnectCount.ShouldBe(0); + + await _client.ReconnectAsync(); + _client.ReconnectCount.ShouldBe(1); + _client.State.ShouldBe(ConnectionState.Connected); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientMonitorTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientMonitorTests.cs new file mode 100644 index 0000000..071b6c3 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientMonitorTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess +{ + public class MxAccessClientMonitorTests : IDisposable + { + private readonly StaComThread _staThread; + private readonly FakeMxProxy _proxy; + private readonly PerformanceMetrics _metrics; + + public MxAccessClientMonitorTests() + { + _staThread = new StaComThread(); + _staThread.Start(); + _proxy = new FakeMxProxy(); + _metrics = new PerformanceMetrics(); + } + + public void Dispose() + { + _staThread.Dispose(); + _metrics.Dispose(); + } + + [Fact] + public async Task Monitor_ReconnectsOnDisconnect() + { + var config = new MxAccessConfiguration + { + MonitorIntervalSeconds = 1, + AutoReconnect = true + }; + var client = new MxAccessClient(_staThread, _proxy, config, _metrics); + + await client.ConnectAsync(); + await client.DisconnectAsync(); + + client.StartMonitor(); + + // Wait for monitor to detect disconnect and reconnect + await Task.Delay(2500); + + client.StopMonitor(); + client.State.ShouldBe(ConnectionState.Connected); + client.ReconnectCount.ShouldBeGreaterThan(0); + client.Dispose(); + } + + [Fact] + public async Task Monitor_StopsOnCancel() + { + var config = new MxAccessConfiguration { MonitorIntervalSeconds = 1 }; + var client = new MxAccessClient(_staThread, _proxy, config, _metrics); + + await client.ConnectAsync(); + client.StartMonitor(); + client.StopMonitor(); + + // Should not throw + await Task.Delay(200); + client.Dispose(); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientReadWriteTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientReadWriteTests.cs new file mode 100644 index 0000000..5e894ad --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientReadWriteTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess +{ + public class MxAccessClientReadWriteTests : IDisposable + { + private readonly StaComThread _staThread; + private readonly FakeMxProxy _proxy; + private readonly PerformanceMetrics _metrics; + private readonly MxAccessClient _client; + + public MxAccessClientReadWriteTests() + { + _staThread = new StaComThread(); + _staThread.Start(); + _proxy = new FakeMxProxy(); + _metrics = new PerformanceMetrics(); + var config = new MxAccessConfiguration { ReadTimeoutSeconds = 2, WriteTimeoutSeconds = 2 }; + _client = new MxAccessClient(_staThread, _proxy, config, _metrics); + } + + public void Dispose() + { + _client.Dispose(); + _staThread.Dispose(); + _metrics.Dispose(); + } + + [Fact] + public async Task Read_NotConnected_ReturnsBad() + { + var result = await _client.ReadAsync("Tag.Attr"); + result.Quality.ShouldBe(Quality.BadNotConnected); + } + + [Fact] + public async Task Read_ReturnsValueOnDataChange() + { + await _client.ConnectAsync(); + + // Start read in background + var readTask = _client.ReadAsync("TestTag.Attr"); + + // Give it a moment to set up subscription, then simulate data change + await Task.Delay(50); + _proxy.SimulateDataChangeByAddress("TestTag.Attr", 42, 192); + + var result = await readTask; + result.Value.ShouldBe(42); + result.Quality.ShouldBe(Quality.Good); + } + + [Fact] + public async Task Read_Timeout_ReturnsBadCommFailure() + { + await _client.ConnectAsync(); + + // No data change simulated, so it will timeout + var result = await _client.ReadAsync("TestTag.Attr"); + result.Quality.ShouldBe(Quality.BadCommFailure); + } + + [Fact] + public async Task Write_NotConnected_ReturnsFalse() + { + var result = await _client.WriteAsync("Tag.Attr", 42); + result.ShouldBe(false); + } + + [Fact] + public async Task Write_Success_ReturnsTrue() + { + await _client.ConnectAsync(); + _proxy.WriteCompleteStatus = 0; + + var result = await _client.WriteAsync("TestTag.Attr", 42); + result.ShouldBe(true); + _proxy.WrittenValues.ShouldContain(w => w.Address == "TestTag.Attr" && (int)w.Value == 42); + } + + [Fact] + public async Task Write_ErrorCode_ReturnsFalse() + { + await _client.ConnectAsync(); + _proxy.WriteCompleteStatus = 1012; // Wrong data type + + var result = await _client.WriteAsync("TestTag.Attr", "bad_value"); + result.ShouldBe(false); + } + + [Fact] + public async Task Read_RecordsMetrics() + { + await _client.ConnectAsync(); + + var readTask = _client.ReadAsync("TestTag.Attr"); + await Task.Delay(50); + _proxy.SimulateDataChangeByAddress("TestTag.Attr", 1, 192); + await readTask; + + var stats = _metrics.GetStatistics(); + stats.ShouldContainKey("Read"); + stats["Read"].TotalCount.ShouldBe(1); + } + + [Fact] + public async Task Write_RecordsMetrics() + { + await _client.ConnectAsync(); + await _client.WriteAsync("TestTag.Attr", 42); + + var stats = _metrics.GetStatistics(); + stats.ShouldContainKey("Write"); + stats["Write"].TotalCount.ShouldBe(1); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientSubscriptionTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientSubscriptionTests.cs new file mode 100644 index 0000000..c4e46ec --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientSubscriptionTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess +{ + public class MxAccessClientSubscriptionTests : IDisposable + { + private readonly StaComThread _staThread; + private readonly FakeMxProxy _proxy; + private readonly PerformanceMetrics _metrics; + private readonly MxAccessClient _client; + + public MxAccessClientSubscriptionTests() + { + _staThread = new StaComThread(); + _staThread.Start(); + _proxy = new FakeMxProxy(); + _metrics = new PerformanceMetrics(); + _client = new MxAccessClient(_staThread, _proxy, new MxAccessConfiguration(), _metrics); + } + + public void Dispose() + { + _client.Dispose(); + _staThread.Dispose(); + _metrics.Dispose(); + } + + [Fact] + public async Task Subscribe_CreatesItemAndAdvises() + { + await _client.ConnectAsync(); + await _client.SubscribeAsync("TestTag.Attr", (_, _) => { }); + + _proxy.Items.Count.ShouldBeGreaterThan(0); + _proxy.AdvisedItems.Count.ShouldBeGreaterThan(0); + _client.ActiveSubscriptionCount.ShouldBe(1); + } + + [Fact] + public async Task Unsubscribe_RemovesItemAndUnadvises() + { + await _client.ConnectAsync(); + await _client.SubscribeAsync("TestTag.Attr", (_, _) => { }); + await _client.UnsubscribeAsync("TestTag.Attr"); + + _client.ActiveSubscriptionCount.ShouldBe(0); + } + + [Fact] + public async Task OnDataChange_InvokesCallback() + { + await _client.ConnectAsync(); + + Vtq? received = null; + await _client.SubscribeAsync("TestTag.Attr", (addr, vtq) => received = vtq); + + _proxy.SimulateDataChangeByAddress("TestTag.Attr", 42, 192); + + received.ShouldNotBeNull(); + received.Value.Value.ShouldBe(42); + received.Value.Quality.ShouldBe(Quality.Good); + } + + [Fact] + public async Task OnDataChange_InvokesGlobalHandler() + { + await _client.ConnectAsync(); + + string? globalAddr = null; + _client.OnTagValueChanged += (addr, vtq) => globalAddr = addr; + + await _client.SubscribeAsync("TestTag.Attr", (_, _) => { }); + _proxy.SimulateDataChangeByAddress("TestTag.Attr", "hello", 192); + + globalAddr.ShouldBe("TestTag.Attr"); + } + + [Fact] + public async Task StoredSubscriptions_ReplayedAfterReconnect() + { + await _client.ConnectAsync(); + var callbackInvoked = false; + await _client.SubscribeAsync("TestTag.Attr", (_, _) => callbackInvoked = true); + + // Reconnect + await _client.ReconnectAsync(); + + // After reconnect, subscription should be replayed + _client.ActiveSubscriptionCount.ShouldBe(1); + + // Simulate data change on the re-subscribed item + _proxy.SimulateDataChangeByAddress("TestTag.Attr", "value", 192); + callbackInvoked.ShouldBe(true); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/StaComThreadTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/StaComThreadTests.cs new file mode 100644 index 0000000..20bb01c --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/StaComThreadTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess +{ + public class StaComThreadTests : IDisposable + { + private readonly StaComThread _thread; + + public StaComThreadTests() + { + _thread = new StaComThread(); + _thread.Start(); + } + + public void Dispose() => _thread.Dispose(); + + [Fact] + public async Task RunAsync_ExecutesOnStaThread() + { + var apartmentState = await _thread.RunAsync(() => Thread.CurrentThread.GetApartmentState()); + apartmentState.ShouldBe(ApartmentState.STA); + } + + [Fact] + public async Task RunAsync_Action_Completes() + { + var executed = false; + await _thread.RunAsync(() => executed = true); + executed.ShouldBe(true); + } + + [Fact] + public async Task RunAsync_Func_ReturnsResult() + { + var result = await _thread.RunAsync(() => 42); + result.ShouldBe(42); + } + + [Fact] + public async Task RunAsync_PropagatesException() + { + await Should.ThrowAsync( + _thread.RunAsync(() => throw new InvalidOperationException("test error"))); + } + + [Fact] + public void Dispose_Stops_Thread() + { + var thread = new StaComThread(); + thread.Start(); + thread.IsRunning.ShouldBe(true); + thread.Dispose(); + // After dispose, should not accept new work + Should.Throw(() => thread.RunAsync(() => { }).GetAwaiter().GetResult()); + } + + [Fact] + public async Task MultipleWorkItems_ExecuteInOrder() + { + var results = new System.Collections.Concurrent.ConcurrentBag(); + await Task.WhenAll( + _thread.RunAsync(() => results.Add(1)), + _thread.RunAsync(() => results.Add(2)), + _thread.RunAsync(() => results.Add(3))); + + results.Count.ShouldBe(3); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/DataValueConverterTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/DataValueConverterTests.cs new file mode 100644 index 0000000..0f2a21d --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/DataValueConverterTests.cs @@ -0,0 +1,122 @@ +using System; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa +{ + public class DataValueConverterTests + { + [Fact] + public void FromVtq_Boolean() + { + var vtq = Vtq.Good(true); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe(true); + Opc.Ua.StatusCode.IsGood(dv.StatusCode).ShouldBe(true); + } + + [Fact] + public void FromVtq_Int32() + { + var vtq = Vtq.Good(42); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe(42); + } + + [Fact] + public void FromVtq_Float() + { + var vtq = Vtq.Good(3.14f); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe(3.14f); + } + + [Fact] + public void FromVtq_Double() + { + var vtq = Vtq.Good(3.14159); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe(3.14159); + } + + [Fact] + public void FromVtq_String() + { + var vtq = Vtq.Good("hello"); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe("hello"); + } + + [Fact] + public void FromVtq_DateTime_IsUtc() + { + var utcTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + var vtq = new Vtq(utcTime, utcTime, Quality.Good); + var dv = DataValueConverter.FromVtq(vtq); + ((DateTime)dv.Value).Kind.ShouldBe(DateTimeKind.Utc); + } + + [Fact] + public void FromVtq_TimeSpan_ConvertedToSeconds() + { + var vtq = Vtq.Good(TimeSpan.FromMinutes(2.5)); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe(150.0); + } + + [Fact] + public void FromVtq_StringArray() + { + var arr = new[] { "a", "b", "c" }; + var vtq = Vtq.Good(arr); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe(arr); + } + + [Fact] + public void FromVtq_IntArray() + { + var arr = new[] { 1, 2, 3 }; + var vtq = Vtq.Good(arr); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBe(arr); + } + + [Fact] + public void FromVtq_BadQuality_MapsToStatusCode() + { + var vtq = Vtq.Bad(Quality.BadCommFailure); + var dv = DataValueConverter.FromVtq(vtq); + Opc.Ua.StatusCode.IsBad(dv.StatusCode).ShouldBe(true); + } + + [Fact] + public void FromVtq_UncertainQuality() + { + var vtq = Vtq.Uncertain(42); + var dv = DataValueConverter.FromVtq(vtq); + Opc.Ua.StatusCode.IsUncertain(dv.StatusCode).ShouldBe(true); + } + + [Fact] + public void FromVtq_NullValue() + { + var vtq = Vtq.Good(null); + var dv = DataValueConverter.FromVtq(vtq); + dv.Value.ShouldBeNull(); + } + + [Fact] + public void ToVtq_RoundTrip() + { + var original = new Vtq(42, DateTime.UtcNow, Quality.Good); + var dv = DataValueConverter.FromVtq(original); + var roundTrip = DataValueConverter.ToVtq(dv); + + roundTrip.Value.ShouldBe(42); + roundTrip.Quality.ShouldBe(Quality.Good); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerBuildTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerBuildTests.cs new file mode 100644 index 0000000..234a7b3 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerBuildTests.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa +{ + public class LmxNodeManagerBuildTests + { + private static (List hierarchy, List attributes) CreateTestData() + { + var hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "DEV", ContainedName = "DEV", BrowseName = "DEV", ParentGobjectId = 0, IsArea = true }, + new GalaxyObjectInfo { GobjectId = 2, TagName = "TestArea", ContainedName = "TestArea", BrowseName = "TestArea", ParentGobjectId = 1, IsArea = true }, + new GalaxyObjectInfo { GobjectId = 3, TagName = "TestMachine_001", ContainedName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 2, IsArea = false }, + new GalaxyObjectInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 3, IsArea = false }, + }; + + var attributes = new List + { + new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineID", FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber", FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "BatchItems", FullTagReference = "TestMachine_001.BatchItems[]", MxDataType = 5, IsArray = true, ArrayDimension = 50 }, + }; + + return (hierarchy, attributes); + } + + [Fact] + public void BuildAddressSpace_CreatesCorrectNodeCounts() + { + var (hierarchy, attributes) = CreateTestData(); + var model = AddressSpaceBuilder.Build(hierarchy, attributes); + + model.ObjectCount.ShouldBe(2); // TestMachine_001, DelmiaReceiver + model.VariableCount.ShouldBe(4); // MachineID, DownloadPath, JobStepNumber, BatchItems + } + + [Fact] + public void BuildAddressSpace_TagReferencesPopulated() + { + var (hierarchy, attributes) = CreateTestData(); + var model = AddressSpaceBuilder.Build(hierarchy, attributes); + + model.NodeIdToTagReference.ContainsKey("TestMachine_001.MachineID").ShouldBe(true); + model.NodeIdToTagReference.ContainsKey("DelmiaReceiver_001.DownloadPath").ShouldBe(true); + model.NodeIdToTagReference.ContainsKey("DelmiaReceiver_001.JobStepNumber").ShouldBe(true); + model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true); + } + + [Fact] + public void BuildAddressSpace_ArrayVariable_HasCorrectInfo() + { + var (hierarchy, attributes) = CreateTestData(); + var model = AddressSpaceBuilder.Build(hierarchy, attributes); + + model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true); + } + + [Fact] + public void BuildAddressSpace_Areas_AreNotCountedAsObjects() + { + var hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "Area1", BrowseName = "Area1", ParentGobjectId = 0, IsArea = true }, + new GalaxyObjectInfo { GobjectId = 2, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 1, IsArea = false } + }; + + var model = AddressSpaceBuilder.Build(hierarchy, new List()); + model.ObjectCount.ShouldBe(1); // Only Obj1, not Area1 + } + + [Fact] + public void BuildAddressSpace_RootNodes_AreTopLevel() + { + var hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "Root1", BrowseName = "Root1", ParentGobjectId = 0, IsArea = true }, + new GalaxyObjectInfo { GobjectId = 2, TagName = "Child1", BrowseName = "Child1", ParentGobjectId = 1, IsArea = false } + }; + + var model = AddressSpaceBuilder.Build(hierarchy, new List()); + model.RootNodes.Count.ShouldBe(1); // Only Root1 is a root + } + + [Fact] + public void BuildAddressSpace_DataTypeMappings() + { + var hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "Obj", BrowseName = "Obj", ParentGobjectId = 0, IsArea = false } + }; + var attributes = new List + { + new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "BoolAttr", FullTagReference = "Obj.BoolAttr", MxDataType = 1, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "IntAttr", FullTagReference = "Obj.IntAttr", MxDataType = 2, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "FloatAttr", FullTagReference = "Obj.FloatAttr", MxDataType = 3, IsArray = false }, + new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "StrAttr", FullTagReference = "Obj.StrAttr", MxDataType = 5, IsArray = false }, + }; + + var model = AddressSpaceBuilder.Build(hierarchy, attributes); + model.VariableCount.ShouldBe(4); + model.NodeIdToTagReference.Count.ShouldBe(4); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerRebuildTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerRebuildTests.cs new file mode 100644 index 0000000..2db661b --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/LmxNodeManagerRebuildTests.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa +{ + public class LmxNodeManagerRebuildTests + { + [Fact] + public void Rebuild_NewBuild_ReplacesOldData() + { + var hierarchy1 = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "OldObj", BrowseName = "OldObj", ParentGobjectId = 0, IsArea = false } + }; + var attrs1 = new List + { + new GalaxyAttributeInfo { GobjectId = 1, TagName = "OldObj", AttributeName = "OldAttr", FullTagReference = "OldObj.OldAttr", MxDataType = 5, IsArray = false } + }; + + var model1 = AddressSpaceBuilder.Build(hierarchy1, attrs1); + model1.NodeIdToTagReference.ContainsKey("OldObj.OldAttr").ShouldBe(true); + + // Rebuild with new data + var hierarchy2 = new List + { + new GalaxyObjectInfo { GobjectId = 2, TagName = "NewObj", BrowseName = "NewObj", ParentGobjectId = 0, IsArea = false } + }; + var attrs2 = new List + { + new GalaxyAttributeInfo { GobjectId = 2, TagName = "NewObj", AttributeName = "NewAttr", FullTagReference = "NewObj.NewAttr", MxDataType = 2, IsArray = false } + }; + + var model2 = AddressSpaceBuilder.Build(hierarchy2, attrs2); + + // Old nodes not in new model, new nodes present + model2.NodeIdToTagReference.ContainsKey("OldObj.OldAttr").ShouldBe(false); + model2.NodeIdToTagReference.ContainsKey("NewObj.NewAttr").ShouldBe(true); + } + + [Fact] + public void Rebuild_UpdatesNodeCounts() + { + var hierarchy1 = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 0, IsArea = false }, + new GalaxyObjectInfo { GobjectId = 2, TagName = "Obj2", BrowseName = "Obj2", ParentGobjectId = 0, IsArea = false } + }; + var model1 = AddressSpaceBuilder.Build(hierarchy1, new List()); + model1.ObjectCount.ShouldBe(2); + + var hierarchy2 = new List + { + new GalaxyObjectInfo { GobjectId = 3, TagName = "Obj3", BrowseName = "Obj3", ParentGobjectId = 0, IsArea = false } + }; + var model2 = AddressSpaceBuilder.Build(hierarchy2, new List()); + model2.ObjectCount.ShouldBe(1); + } + + [Fact] + public void EmptyHierarchy_ProducesEmptyModel() + { + var model = AddressSpaceBuilder.Build(new List(), new List()); + model.RootNodes.ShouldBeEmpty(); + model.NodeIdToTagReference.ShouldBeEmpty(); + model.ObjectCount.ShouldBe(0); + model.VariableCount.ShouldBe(0); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/OpcUaQualityMapperTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/OpcUaQualityMapperTests.cs new file mode 100644 index 0000000..0d813e3 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/OpcUa/OpcUaQualityMapperTests.cs @@ -0,0 +1,60 @@ +using Opc.Ua; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa +{ + public class OpcUaQualityMapperTests + { + [Fact] + public void Good_MapsToGoodStatusCode() + { + var sc = OpcUaQualityMapper.ToStatusCode(Quality.Good); + StatusCode.IsGood(sc).ShouldBe(true); + } + + [Fact] + public void Bad_MapsToBadStatusCode() + { + var sc = OpcUaQualityMapper.ToStatusCode(Quality.Bad); + StatusCode.IsBad(sc).ShouldBe(true); + } + + [Fact] + public void Uncertain_MapsToUncertainStatusCode() + { + var sc = OpcUaQualityMapper.ToStatusCode(Quality.Uncertain); + StatusCode.IsUncertain(sc).ShouldBe(true); + } + + [Fact] + public void BadCommFailure_MapsCorrectly() + { + var sc = OpcUaQualityMapper.ToStatusCode(Quality.BadCommFailure); + StatusCode.IsBad(sc).ShouldBe(true); + } + + [Fact] + public void FromStatusCode_Good() + { + var q = OpcUaQualityMapper.FromStatusCode(StatusCodes.Good); + q.ShouldBe(Quality.Good); + } + + [Fact] + public void FromStatusCode_Bad() + { + var q = OpcUaQualityMapper.FromStatusCode(StatusCodes.Bad); + q.ShouldBe(Quality.Bad); + } + + [Fact] + public void FromStatusCode_Uncertain() + { + var q = OpcUaQualityMapper.FromStatusCode(StatusCodes.Uncertain); + q.ShouldBe(Quality.Uncertain); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/SampleTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/SampleTest.cs new file mode 100644 index 0000000..ec49499 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/SampleTest.cs @@ -0,0 +1,14 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.LmxOpcUa.Tests +{ + public class SampleTest + { + [Fact] + public void Placeholder_ShouldPass() + { + true.ShouldBeTrue(); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/HealthCheckServiceTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/HealthCheckServiceTests.cs new file mode 100644 index 0000000..add2505 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/HealthCheckServiceTests.cs @@ -0,0 +1,82 @@ +using System; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.Status; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Status +{ + public class HealthCheckServiceTests + { + private readonly HealthCheckService _sut = new(); + + [Fact] + public void NotConnected_ReturnsUnhealthy() + { + var result = _sut.CheckHealth(ConnectionState.Disconnected, null); + result.Status.ShouldBe("Unhealthy"); + result.Color.ShouldBe("red"); + result.Message.ShouldContain("not connected"); + } + + [Fact] + public void Connected_NoMetrics_ReturnsHealthy() + { + var result = _sut.CheckHealth(ConnectionState.Connected, null); + result.Status.ShouldBe("Healthy"); + result.Color.ShouldBe("green"); + } + + [Fact] + public void Connected_GoodMetrics_ReturnsHealthy() + { + using var metrics = new PerformanceMetrics(); + for (int i = 0; i < 200; i++) + metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true); + + var result = _sut.CheckHealth(ConnectionState.Connected, metrics); + result.Status.ShouldBe("Healthy"); + } + + [Fact] + public void Connected_LowSuccessRate_ReturnsDegraded() + { + using var metrics = new PerformanceMetrics(); + for (int i = 0; i < 40; i++) + metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true); + for (int i = 0; i < 80; i++) + metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), false); + + var result = _sut.CheckHealth(ConnectionState.Connected, metrics); + result.Status.ShouldBe("Degraded"); + result.Color.ShouldBe("yellow"); + } + + [Fact] + public void IsHealthy_Connected_ReturnsTrue() + { + _sut.IsHealthy(ConnectionState.Connected, null).ShouldBe(true); + } + + [Fact] + public void IsHealthy_Disconnected_ReturnsFalse() + { + _sut.IsHealthy(ConnectionState.Disconnected, null).ShouldBe(false); + } + + [Fact] + public void Error_ReturnsUnhealthy() + { + var result = _sut.CheckHealth(ConnectionState.Error, null); + result.Status.ShouldBe("Unhealthy"); + } + + [Fact] + public void Reconnecting_ReturnsUnhealthy() + { + var result = _sut.CheckHealth(ConnectionState.Reconnecting, null); + result.Status.ShouldBe("Unhealthy"); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs new file mode 100644 index 0000000..46c0a30 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs @@ -0,0 +1,127 @@ +using System; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository; +using ZB.MOM.WW.LmxOpcUa.Host.Metrics; +using ZB.MOM.WW.LmxOpcUa.Host.Status; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Status +{ + public class StatusReportServiceTests + { + [Fact] + public void GenerateHtml_ContainsAllPanels() + { + var sut = CreateService(); + var html = sut.GenerateHtml(); + + html.ShouldContain("Connection"); + html.ShouldContain("Health"); + html.ShouldContain("Subscriptions"); + html.ShouldContain("Galaxy Info"); + html.ShouldContain("Operations"); + html.ShouldContain("Footer"); + } + + [Fact] + public void GenerateHtml_ContainsMetaRefresh() + { + var sut = CreateService(); + var html = sut.GenerateHtml(); + html.ShouldContain("meta http-equiv='refresh' content='10'"); + } + + [Fact] + public void GenerateHtml_ConnectionPanel_ShowsState() + { + var sut = CreateService(); + var html = sut.GenerateHtml(); + html.ShouldContain("Connected"); + } + + [Fact] + public void GenerateHtml_GalaxyPanel_ShowsName() + { + var sut = CreateService(); + var html = sut.GenerateHtml(); + html.ShouldContain("TestGalaxy"); + } + + [Fact] + public void GenerateHtml_OperationsTable_ShowsHeaders() + { + var sut = CreateService(); + var html = sut.GenerateHtml(); + html.ShouldContain("Count"); + html.ShouldContain("Success Rate"); + html.ShouldContain("Avg (ms)"); + html.ShouldContain("Min (ms)"); + html.ShouldContain("Max (ms)"); + html.ShouldContain("P95 (ms)"); + } + + [Fact] + public void GenerateHtml_Footer_ContainsTimestampAndVersion() + { + var sut = CreateService(); + var html = sut.GenerateHtml(); + html.ShouldContain("Generated:"); + html.ShouldContain("Version:"); + } + + [Fact] + public void GenerateJson_Deserializes() + { + var sut = CreateService(); + var json = sut.GenerateJson(); + + json.ShouldNotBeNullOrWhiteSpace(); + json.ShouldContain("Connection"); + json.ShouldContain("Health"); + json.ShouldContain("Subscriptions"); + json.ShouldContain("Galaxy"); + json.ShouldContain("Operations"); + json.ShouldContain("Footer"); + } + + [Fact] + public void IsHealthy_WhenConnected_ReturnsTrue() + { + var sut = CreateService(); + sut.IsHealthy().ShouldBe(true); + } + + [Fact] + public void IsHealthy_WhenDisconnected_ReturnsFalse() + { + var mxClient = new FakeMxAccessClient { State = ConnectionState.Disconnected }; + var sut = new StatusReportService(new HealthCheckService(), 10); + sut.SetComponents(mxClient, null, null, null); + sut.IsHealthy().ShouldBe(false); + } + + private static StatusReportService CreateService() + { + var mxClient = new FakeMxAccessClient(); + using var metrics = new PerformanceMetrics(); + metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10)); + metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(20)); + + var galaxyStats = new GalaxyRepositoryStats + { + GalaxyName = "TestGalaxy", + DbConnected = true, + LastDeployTime = new DateTime(2024, 6, 1), + ObjectCount = 42, + AttributeCount = 200, + LastRebuildTime = DateTime.UtcNow + }; + + var sut = new StatusReportService(new HealthCheckService(), 10); + sut.SetComponents(mxClient, metrics, galaxyStats, null); + return sut; + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusWebServerTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusWebServerTests.cs new file mode 100644 index 0000000..6693d79 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusWebServerTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Status; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Status +{ + public class StatusWebServerTests : IDisposable + { + private readonly StatusWebServer _server; + private readonly HttpClient _client; + private readonly int _port; + + public StatusWebServerTests() + { + _port = new Random().Next(18000, 19000); + var reportService = new StatusReportService(new HealthCheckService(), 10); + var mxClient = new FakeMxAccessClient(); + reportService.SetComponents(mxClient, null, null, null); + _server = new StatusWebServer(reportService, _port); + _server.Start(); + _client = new HttpClient { BaseAddress = new Uri($"http://localhost:{_port}") }; + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + + [Fact] + public async Task Root_ReturnsHtml200() + { + var response = await _client.GetAsync("/"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Content.Headers.ContentType?.MediaType.ShouldBe("text/html"); + } + + [Fact] + public async Task ApiStatus_ReturnsJson200() + { + var response = await _client.GetAsync("/api/status"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json"); + } + + [Fact] + public async Task ApiHealth_Returns200WhenHealthy() + { + var response = await _client.GetAsync("/api/health"); + // FakeMxAccessClient starts as Connected → healthy + response.StatusCode.ShouldBe(HttpStatusCode.OK); + var body = await response.Content.ReadAsStringAsync(); + body.ShouldContain("healthy"); + } + + [Fact] + public async Task UnknownPath_Returns404() + { + var response = await _client.GetAsync("/unknown"); + response.StatusCode.ShouldBe(HttpStatusCode.NotFound); + } + + [Fact] + public async Task PostMethod_Returns405() + { + var response = await _client.PostAsync("/", new StringContent("")); + response.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); + } + + [Fact] + public async Task CacheHeaders_Present() + { + var response = await _client.GetAsync("/"); + response.Headers.CacheControl?.NoCache.ShouldBe(true); + response.Headers.CacheControl?.NoStore.ShouldBe(true); + } + + [Fact] + public void StartStop_DoesNotThrow() + { + var server2 = new StatusWebServer( + new StatusReportService(new HealthCheckService(), 10), + new Random().Next(19000, 20000)); + server2.Start(); + server2.IsRunning.ShouldBe(true); + server2.Stop(); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ChangeDetectionToRebuildWiringTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ChangeDetectionToRebuildWiringTest.cs new file mode 100644 index 0000000..0bdce48 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ChangeDetectionToRebuildWiringTest.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring +{ + /// + /// Verifies: Galaxy change detection → OnGalaxyChanged → address space rebuild + /// + public class ChangeDetectionToRebuildWiringTest + { + [Fact] + public async Task ChangedTimestamp_TriggersRebuild() + { + var repo = new FakeGalaxyRepository + { + LastDeployTime = new DateTime(2024, 1, 1), + Hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 0, IsArea = false } + }, + Attributes = new List + { + new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj1", AttributeName = "Attr1", FullTagReference = "Obj1.Attr1", MxDataType = 5, IsArray = false } + } + }; + + var rebuildCount = 0; + var service = new ChangeDetectionService(repo, intervalSeconds: 1); + service.OnGalaxyChanged += () => Interlocked.Increment(ref rebuildCount); + + service.Start(); + await Task.Delay(500); // First poll triggers + rebuildCount.ShouldBeGreaterThanOrEqualTo(1); + + // Change deploy time → should trigger rebuild + repo.LastDeployTime = new DateTime(2024, 2, 1); + await Task.Delay(1500); + service.Stop(); + + rebuildCount.ShouldBeGreaterThanOrEqualTo(2); + service.Dispose(); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/MxAccessToNodeManagerWiringTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/MxAccessToNodeManagerWiringTest.cs new file mode 100644 index 0000000..2653401 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/MxAccessToNodeManagerWiringTest.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring +{ + /// + /// Verifies: FakeMxProxy OnDataChange → MxAccessClient → OnTagValueChanged → node manager delivery + /// + public class MxAccessToNodeManagerWiringTest + { + [Fact] + public async Task DataChange_ReachesGlobalHandler() + { + var mxClient = new FakeMxAccessClient(); + string? receivedAddress = null; + Vtq? receivedVtq = null; + + mxClient.OnTagValueChanged += (addr, vtq) => + { + receivedAddress = addr; + receivedVtq = vtq; + }; + + mxClient.SimulateDataChange("TestTag.Attr", Vtq.Good(42)); + + receivedAddress.ShouldBe("TestTag.Attr"); + receivedVtq.ShouldNotBeNull(); + receivedVtq.Value.Value.ShouldBe(42); + receivedVtq.Value.Quality.ShouldBe(Quality.Good); + } + + [Fact] + public async Task DataChange_ReachesSubscriptionCallback() + { + var mxClient = new FakeMxAccessClient(); + Vtq? received = null; + + await mxClient.SubscribeAsync("TestTag.Attr", (addr, vtq) => received = vtq); + mxClient.SimulateDataChange("TestTag.Attr", Vtq.Good(99)); + + received.ShouldNotBeNull(); + received.Value.Value.ShouldBe(99); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaReadToMxAccessWiringTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaReadToMxAccessWiringTest.cs new file mode 100644 index 0000000..0014043 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaReadToMxAccessWiringTest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring +{ + /// + /// Verifies: OPC UA Read → NodeManager → IMxAccessClient.ReadAsync with correct full_tag_reference + /// + public class OpcUaReadToMxAccessWiringTest + { + [Fact] + public async Task Read_ResolvesCorrectTagReference() + { + var mxClient = new FakeMxAccessClient(); + mxClient.TagValues["DelmiaReceiver_001.DownloadPath"] = Vtq.Good("/some/path"); + + var hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 0, IsArea = false }, + new GalaxyObjectInfo { GobjectId = 2, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 1, IsArea = false } + }; + var attributes = new List + { + new GalaxyAttributeInfo { GobjectId = 2, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false } + }; + + var model = AddressSpaceBuilder.Build(hierarchy, attributes); + + // The model should contain the correct tag reference + model.NodeIdToTagReference.ContainsKey("DelmiaReceiver_001.DownloadPath").ShouldBe(true); + model.NodeIdToTagReference["DelmiaReceiver_001.DownloadPath"].ShouldBe("DelmiaReceiver_001.DownloadPath"); + + // The MxAccessClient should be able to read using the tag reference + var vtq = await mxClient.ReadAsync("DelmiaReceiver_001.DownloadPath"); + vtq.Value.ShouldBe("/some/path"); + vtq.Quality.ShouldBe(Quality.Good); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaWriteToMxAccessWiringTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaWriteToMxAccessWiringTest.cs new file mode 100644 index 0000000..ba498d4 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/OpcUaWriteToMxAccessWiringTest.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring +{ + /// + /// Verifies: OPC UA Write → NodeManager → IMxAccessClient.WriteAsync with correct tag+value + /// + public class OpcUaWriteToMxAccessWiringTest + { + [Fact] + public async Task Write_SendsCorrectTagAndValue() + { + var mxClient = new FakeMxAccessClient(); + + var hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 0, IsArea = false } + }; + var attributes = new List + { + new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestMachine_001", AttributeName = "MachineCode", FullTagReference = "TestMachine_001.MachineCode", MxDataType = 5, IsArray = false } + }; + + var model = AddressSpaceBuilder.Build(hierarchy, attributes); + var tagRef = model.NodeIdToTagReference["TestMachine_001.MachineCode"]; + + // Write through MxAccessClient + var result = await mxClient.WriteAsync(tagRef, "NEW_CODE"); + + result.ShouldBe(true); + mxClient.WrittenValues.ShouldContain(w => w.Tag == "TestMachine_001.MachineCode" && (string)w.Value == "NEW_CODE"); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ServiceStartupSequenceTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ServiceStartupSequenceTest.cs new file mode 100644 index 0000000..12ac6a9 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ServiceStartupSequenceTest.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring +{ + /// + /// Verifies: OpcUaService Start() creates and wires all components with fakes. + /// + public class ServiceStartupSequenceTest + { + [Fact] + public void Start_WithFakes_AllComponentsCreated() + { + var config = new AppConfiguration + { + OpcUa = new OpcUaConfiguration + { + Port = 14840, + GalaxyName = "TestGalaxy", + EndpointPath = "/LmxOpcUa" + }, + MxAccess = new MxAccessConfiguration { ClientName = "Test" }, + GalaxyRepository = new GalaxyRepositoryConfiguration(), + Dashboard = new DashboardConfiguration { Enabled = false } // Don't start HTTP listener in tests + }; + + var proxy = new FakeMxProxy(); + var repo = new FakeGalaxyRepository + { + Hierarchy = new List + { + new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false } + }, + Attributes = new List + { + new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr", FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false } + } + }; + + var service = new OpcUaService(config, proxy, repo); + service.Start(); + + try + { + // Verify all components were created + service.MxClient.ShouldNotBeNull(); + service.MxClient!.State.ShouldBe(ConnectionState.Connected); + service.Metrics.ShouldNotBeNull(); + service.ServerHost.ShouldNotBeNull(); + service.ChangeDetectionInstance.ShouldNotBeNull(); + service.GalaxyStatsInstance.ShouldNotBeNull(); + service.GalaxyStatsInstance!.GalaxyName.ShouldBe("TestGalaxy"); + service.GalaxyStatsInstance.DbConnected.ShouldBe(true); + service.StatusReportInstance.ShouldNotBeNull(); + + // Dashboard disabled → no web server + service.StatusWeb.ShouldBeNull(); + + // MxProxy should have been registered + proxy.IsRegistered.ShouldBe(true); + } + finally + { + service.Stop(); + } + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ShutdownCompletesTest.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ShutdownCompletesTest.cs new file mode 100644 index 0000000..2e2bb5b --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Wiring/ShutdownCompletesTest.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Shouldly; +using Xunit; +using ZB.MOM.WW.LmxOpcUa.Host; +using ZB.MOM.WW.LmxOpcUa.Host.Configuration; +using ZB.MOM.WW.LmxOpcUa.Host.Domain; +using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; + +namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring +{ + /// + /// Verifies: Start then Stop completes within 30 seconds. (SVC-004) + /// + public class ShutdownCompletesTest + { + [Fact] + public void Shutdown_CompletesWithin30Seconds() + { + var config = new AppConfiguration + { + OpcUa = new OpcUaConfiguration { Port = 14841, GalaxyName = "TestGalaxy" }, + MxAccess = new MxAccessConfiguration { ClientName = "Test" }, + Dashboard = new DashboardConfiguration { Enabled = false } + }; + + var proxy = new FakeMxProxy(); + var repo = new FakeGalaxyRepository(); + var service = new OpcUaService(config, proxy, repo); + + service.Start(); + + var sw = Stopwatch.StartNew(); + service.Stop(); + sw.Stop(); + + sw.Elapsed.TotalSeconds.ShouldBeLessThan(30); + } + } +} diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/ZB.MOM.WW.LmxOpcUa.Tests.csproj b/tests/ZB.MOM.WW.LmxOpcUa.Tests/ZB.MOM.WW.LmxOpcUa.Tests.csproj new file mode 100644 index 0000000..3602e39 --- /dev/null +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/ZB.MOM.WW.LmxOpcUa.Tests.csproj @@ -0,0 +1,44 @@ + + + + net48 + x86 + 9.0 + enable + false + true + ZB.MOM.WW.LmxOpcUa.Tests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + ..\..\lib\ArchestrA.MxAccess.dll + false + + + + + + PreserveNewest + + + + diff --git a/tools/opcuacli-dotnet/Commands/BrowseCommand.cs b/tools/opcuacli-dotnet/Commands/BrowseCommand.cs new file mode 100644 index 0000000..ff9fc56 --- /dev/null +++ b/tools/opcuacli-dotnet/Commands/BrowseCommand.cs @@ -0,0 +1,92 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Opc.Ua; +using Opc.Ua.Client; + +namespace OpcUaCli.Commands; + +[Command("browse", Description = "Browse the OPC UA address space")] +public class BrowseCommand : ICommand +{ + [CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)] + public string Url { get; init; } = default!; + + [CommandOption("node", 'n', Description = "Node ID to browse (default: Objects folder)")] + public string? NodeId { get; init; } + + [CommandOption("depth", 'd', Description = "Maximum browse depth")] + public int Depth { get; init; } = 1; + + [CommandOption("recursive", 'r', Description = "Browse recursively (uses --depth as max depth)")] + public bool Recursive { get; init; } + + public async ValueTask ExecuteAsync(IConsole console) + { + using var session = await OpcUaHelper.ConnectAsync(Url); + + var startNode = string.IsNullOrEmpty(NodeId) + ? ObjectIds.ObjectsFolder + : new NodeId(NodeId); + + var maxDepth = Recursive ? Depth : 1; + await BrowseNodeAsync(session, console, startNode, maxDepth, 0); + } + + private static async Task BrowseNodeAsync( + ISession session, IConsole console, NodeId nodeId, int maxDepth, int currentDepth) + { + var indent = new string(' ', currentDepth * 2); + + var (_, continuationPoint, references) = await session.BrowseAsync( + null, + null, + nodeId, + 0u, + BrowseDirection.Forward, + ReferenceTypeIds.HierarchicalReferences, + true, + (uint)NodeClass.Object | (uint)NodeClass.Variable | (uint)NodeClass.Method); + + if (references == null) return; + + // Handle continuation points for large result sets + while (references.Count > 0) + { + foreach (var reference in references) + { + var nodeClass = reference.NodeClass; + var marker = nodeClass switch + { + NodeClass.Object => "[Object]", + NodeClass.Variable => "[Variable]", + NodeClass.Method => "[Method]", + _ => $"[{nodeClass}]" + }; + + await console.Output.WriteLineAsync( + $"{indent}{marker} {reference.DisplayName} (NodeId: {reference.NodeId})"); + + if (currentDepth + 1 < maxDepth && nodeClass == NodeClass.Object) + { + var childNodeId = ExpandedNodeId.ToNodeId( + reference.NodeId, session.NamespaceUris); + await BrowseNodeAsync(session, console, childNodeId, maxDepth, currentDepth + 1); + } + } + + // Follow continuation point if present + if (continuationPoint != null && continuationPoint.Length > 0) + { + var (_, nextCp, nextRefs) = await session.BrowseNextAsync( + null, false, continuationPoint); + continuationPoint = nextCp; + references = nextRefs; + } + else + { + break; + } + } + } +} diff --git a/tools/opcuacli-dotnet/Commands/ConnectCommand.cs b/tools/opcuacli-dotnet/Commands/ConnectCommand.cs new file mode 100644 index 0000000..f1d17bd --- /dev/null +++ b/tools/opcuacli-dotnet/Commands/ConnectCommand.cs @@ -0,0 +1,22 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; + +namespace OpcUaCli.Commands; + +[Command("connect", Description = "Test connection to an OPC UA server")] +public class ConnectCommand : ICommand +{ + [CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)] + public string Url { get; init; } = default!; + + public async ValueTask ExecuteAsync(IConsole console) + { + using var session = await OpcUaHelper.ConnectAsync(Url); + await console.Output.WriteLineAsync($"Connected to: {session.Endpoint.EndpointUrl}"); + await console.Output.WriteLineAsync($"Server: {session.Endpoint.Server!.ApplicationName}"); + await console.Output.WriteLineAsync($"Security Mode: {session.Endpoint.SecurityMode}"); + await console.Output.WriteLineAsync($"Security Policy: {session.Endpoint.SecurityPolicyUri}"); + await console.Output.WriteLineAsync("Connection successful."); + } +} diff --git a/tools/opcuacli-dotnet/Commands/ReadCommand.cs b/tools/opcuacli-dotnet/Commands/ReadCommand.cs new file mode 100644 index 0000000..5f6b1d8 --- /dev/null +++ b/tools/opcuacli-dotnet/Commands/ReadCommand.cs @@ -0,0 +1,31 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Opc.Ua; +using Opc.Ua.Client; + +namespace OpcUaCli.Commands; + +[Command("read", Description = "Read a value from a node")] +public class ReadCommand : ICommand +{ + [CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)] + public string Url { get; init; } = default!; + + [CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)] + public string NodeId { get; init; } = default!; + + public async ValueTask ExecuteAsync(IConsole console) + { + using var session = await OpcUaHelper.ConnectAsync(Url); + + var node = new NodeId(NodeId); + var value = await session.ReadValueAsync(node); + + await console.Output.WriteLineAsync($"Node: {NodeId}"); + await console.Output.WriteLineAsync($"Value: {value.Value}"); + await console.Output.WriteLineAsync($"Status: {value.StatusCode}"); + await console.Output.WriteLineAsync($"Source Time: {value.SourceTimestamp:O}"); + await console.Output.WriteLineAsync($"Server Time: {value.ServerTimestamp:O}"); + } +} diff --git a/tools/opcuacli-dotnet/Commands/SubscribeCommand.cs b/tools/opcuacli-dotnet/Commands/SubscribeCommand.cs new file mode 100644 index 0000000..d70f4cb --- /dev/null +++ b/tools/opcuacli-dotnet/Commands/SubscribeCommand.cs @@ -0,0 +1,61 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Opc.Ua; +using Opc.Ua.Client; + +namespace OpcUaCli.Commands; + +[Command("subscribe", Description = "Monitor a node for value changes")] +public class SubscribeCommand : ICommand +{ + [CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)] + public string Url { get; init; } = default!; + + [CommandOption("node", 'n', Description = "Node ID to monitor", IsRequired = true)] + public string NodeId { get; init; } = default!; + + [CommandOption("interval", 'i', Description = "Polling interval in milliseconds")] + public int Interval { get; init; } = 1000; + + public async ValueTask ExecuteAsync(IConsole console) + { + using var session = await OpcUaHelper.ConnectAsync(Url); + + var subscription = new Subscription(session.DefaultSubscription) + { + PublishingInterval = Interval, + DisplayName = "CLI Subscription" + }; + + var item = new MonitoredItem(subscription.DefaultItem) + { + StartNodeId = new NodeId(NodeId), + DisplayName = NodeId, + SamplingInterval = Interval + }; + + item.Notification += (_, e) => + { + if (e.NotificationValue is MonitoredItemNotification notification) + { + console.Output.WriteLine( + $"[{notification.Value.SourceTimestamp:O}] {NodeId} = {notification.Value.Value} ({notification.Value.StatusCode})"); + } + }; + + subscription.AddItem(item); + session.AddSubscription(subscription); + await subscription.CreateAsync(); + + await console.Output.WriteLineAsync( + $"Subscribed to {NodeId} (interval: {Interval}ms). Press Ctrl+C to stop."); + + var ct = console.RegisterCancellationHandler(); + + try { await Task.Delay(Timeout.Infinite, ct); } + catch (OperationCanceledException) { } + + await console.Output.WriteLineAsync("Unsubscribed."); + } +} diff --git a/tools/opcuacli-dotnet/Commands/WriteCommand.cs b/tools/opcuacli-dotnet/Commands/WriteCommand.cs new file mode 100644 index 0000000..7ea13ce --- /dev/null +++ b/tools/opcuacli-dotnet/Commands/WriteCommand.cs @@ -0,0 +1,44 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Opc.Ua; +using Opc.Ua.Client; + +namespace OpcUaCli.Commands; + +[Command("write", Description = "Write a value to a node")] +public class WriteCommand : ICommand +{ + [CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)] + public string Url { get; init; } = default!; + + [CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)] + public string NodeId { get; init; } = default!; + + [CommandOption("value", 'v', Description = "Value to write", IsRequired = true)] + public string Value { get; init; } = default!; + + public async ValueTask ExecuteAsync(IConsole console) + { + using var session = await OpcUaHelper.ConnectAsync(Url); + + var node = new NodeId(NodeId); + var current = await session.ReadValueAsync(node); + var typedValue = OpcUaHelper.ConvertValue(Value, current.Value); + + var writeValue = new WriteValue + { + NodeId = node, + AttributeId = Attributes.Value, + Value = new DataValue(new Variant(typedValue)) + }; + + var request = new WriteValueCollection { writeValue }; + var response = await session.WriteAsync(null, request, CancellationToken.None); + + if (StatusCode.IsGood(response.Results[0])) + await console.Output.WriteLineAsync($"Write successful: {NodeId} = {typedValue}"); + else + await console.Output.WriteLineAsync($"Write failed: {response.Results[0]}"); + } +} diff --git a/tools/opcuacli-dotnet/OpcUaHelper.cs b/tools/opcuacli-dotnet/OpcUaHelper.cs new file mode 100644 index 0000000..9edf5d5 --- /dev/null +++ b/tools/opcuacli-dotnet/OpcUaHelper.cs @@ -0,0 +1,81 @@ +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; + +namespace OpcUaCli; + +public static class OpcUaHelper +{ + public static async Task ConnectAsync(string endpointUrl) + { + var config = new ApplicationConfiguration + { + ApplicationName = "OpcUaCli", + ApplicationUri = "urn:localhost:OpcUaCli", + ApplicationType = ApplicationType.Client, + SecurityConfiguration = new SecurityConfiguration + { + ApplicationCertificate = new CertificateIdentifier + { + StoreType = CertificateStoreType.Directory, + StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "own") + }, + TrustedIssuerCertificates = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "issuer") + }, + TrustedPeerCertificates = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "trusted") + }, + RejectedCertificateStore = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "rejected") + }, + AutoAcceptUntrustedCertificates = true + }, + ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 } + }; + +#pragma warning disable CS0618 // Sync/obsolete API is fine for a CLI tool + await config.Validate(ApplicationType.Client); + config.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true; + + var endpoint = CoreClientUtils.SelectEndpoint(config, endpointUrl, false); + var endpointConfig = EndpointConfiguration.Create(config); + var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig); + + var session = await Session.Create( + config, + configuredEndpoint, + false, + "OpcUaCli", + 60000, + null, + null); + + return session; +#pragma warning restore CS0618 + } + + public static object ConvertValue(string rawValue, object? currentValue) + { + return currentValue switch + { + bool => bool.Parse(rawValue), + byte => byte.Parse(rawValue), + short => short.Parse(rawValue), + ushort => ushort.Parse(rawValue), + int => int.Parse(rawValue), + uint => uint.Parse(rawValue), + long => long.Parse(rawValue), + ulong => ulong.Parse(rawValue), + float => float.Parse(rawValue), + double => double.Parse(rawValue), + _ => rawValue + }; + } +} diff --git a/tools/opcuacli-dotnet/Program.cs b/tools/opcuacli-dotnet/Program.cs new file mode 100644 index 0000000..ea3c478 --- /dev/null +++ b/tools/opcuacli-dotnet/Program.cs @@ -0,0 +1,8 @@ +using CliFx; + +return await new CliApplicationBuilder() + .AddCommandsFromThisAssembly() + .SetExecutableName("opcuacli-dotnet") + .SetDescription("OPC UA CLI - command-line tool for testing OPC UA servers") + .Build() + .RunAsync(args); diff --git a/tools/opcuacli-dotnet/README.md b/tools/opcuacli-dotnet/README.md new file mode 100644 index 0000000..f9f6e7f --- /dev/null +++ b/tools/opcuacli-dotnet/README.md @@ -0,0 +1,123 @@ +# OPC UA CLI Tool (.NET) + +Command-line utility for testing OPC UA server functions. Built with the [OPC Foundation UA .NET Standard](https://github.com/OPCFoundation/UA-.NETStandard) client library and [CliFx](https://github.com/Tyrrrz/CliFx). + +- **Runtime**: .NET 10 +- **OPC UA Client**: OPCFoundation.NetStandard.Opc.Ua.Client + +## Build & Run + +```bash +cd tools/opcuacli-dotnet +dotnet build +dotnet run -- [options] +``` + +## Commands + +### connect + +Test connection to an OPC UA server: + +``` +dotnet run -- connect -u opc.tcp://localhost:4840 +``` + +| Flag | Description | +|------|-------------| +| `-u` | OPC UA server endpoint URL (required) | + +### read + +Read a value from a node: + +``` +dotnet run -- read -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" +``` + +| Flag | Description | +|------|-------------| +| `-u` | OPC UA server endpoint URL (required) | +| `-n` | Node ID to read (required) | + +### write + +Write a value to a node (auto-detects the data type from the current value): + +``` +dotnet run -- write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 +``` + +| Flag | Description | +|------|-------------| +| `-u` | OPC UA server endpoint URL (required) | +| `-n` | Node ID to write to (required) | +| `-v` | Value to write (required) | + +### subscribe + +Monitor a node for value changes: + +``` +dotnet run -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500 +``` + +| Flag | Description | +|------|-------------| +| `-u` | OPC UA server endpoint URL (required) | +| `-n` | Node ID to monitor (required) | +| `-i` | Polling interval in milliseconds (default: 1000) | + +### browse + +Browse the OPC UA address space: + +```bash +# Browse top-level Objects folder +dotnet run -- browse -u opc.tcp://localhost:4840 + +# Browse a specific node +dotnet run -- browse -u opc.tcp://localhost:4840 -n "ns=2;s=MyFolder" + +# Browse recursively (depth 3) +dotnet run -- browse -u opc.tcp://localhost:4840 -r -d 3 +``` + +| Flag | Description | +|------|-------------| +| `-u` | OPC UA server endpoint URL (required) | +| `-n` | Node ID to browse (default: Objects folder) | +| `-d` | Maximum browse depth (default: 1) | +| `-r` | Browse recursively using `--depth` as max depth | + +## Example: Testing the LmxOpcUa Server + +```bash +cd tools/opcuacli-dotnet + +# Connect to the local OPC UA server +dotnet run -- connect -u opc.tcp://localhost:4840 + +# Browse the address space +dotnet run -- browse -u opc.tcp://localhost:4840 -r -d 3 + +# Read a tag value +dotnet run -- read -u opc.tcp://localhost:4840 -n "ns=2;s=TestMachine_001.SomeAttribute" + +# Subscribe to live updates +dotnet run -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=TestMachine_001.SomeAttribute" -i 500 +``` + +## Example: Testing with the OPC PLC Sample Server + +```bash +# Start the sample server (from another terminal) +cd tools/opcsampleserver/publish +dotnet opcplc.dll --pn=50000 --autoaccept --unsecuretransport + +# Browse the sample server +dotnet run -- browse -u opc.tcp://localhost:50000 -r -d 2 + +# Read a sample node +dotnet run -- read -u opc.tcp://localhost:50000 -n "ns=2;s=SlowUInt1" +``` diff --git a/tools/opcuacli-dotnet/opcuacli-dotnet.csproj b/tools/opcuacli-dotnet/opcuacli-dotnet.csproj new file mode 100644 index 0000000..3bff643 --- /dev/null +++ b/tools/opcuacli-dotnet/opcuacli-dotnet.csproj @@ -0,0 +1,16 @@ + + + + Exe + net10.0 + opcuacli_dotnet + enable + enable + + + + + + + +