diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dad8197d..7a07babaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,54 +1,27 @@ -### [FIX] `axdev` password guard contradicted the secrets complexity policy +### [COMPONENTS.COGNEX.VISION] AxoVisionProNet — TCP/.NET alternative to the PROFINET AxoVisionPro -**Note:** Bug fix in `src/axopen.dev`. Branch: `feat/axdev-user-secrets-loader`. +**Note:** Additive change. New component `AxoVisionProNet` in `src/components.cognex.vision/ctrl/src/AxoVisionProNet/`, its .NET twin + TCP protocol stack in `src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/`, a Blazor proxy view, and full showcase/doc wiring. No public-API removal; existing `AxoVisionPro` (PROFINET) is unchanged. Branch: `1104-new-featureaxovisionpro-alternative`. -- fix: `AXOpen.Dev.Validation.PasswordValidator` no longer rejects `$ & ( ) *`. These are endorsed by the set-time complexity policy (`configure-secrets.sh` requires a special char from `!@#$%^&*()_+-=`), so a password that satisfied the complexity rule was then rejected at use time by `axdev alf` / `axdev all` with "The PASSWORD contains problematic characters." The blocklist now keeps only genuinely-dangerous shell metacharacters (`` ` \ " ' | ; < > ? [ ] { } `` and whitespace) — safe because arguments reach apax/openssl via CliWrap (no shell). Error message and `PasswordValidatorTests` updated. - -**Impact:** `apax alf` / `apax all` accept the same passwords the secrets-setup flow accepts; no more spurious rejection of compliant passwords. - -**Testing:** `dotnet test src/axopen.dev/AXOpen.Dev.Tests` — 180 passed. - -### [BUILD] `axdev` loads dotnet user-secrets at startup - -**Note:** Developer-CLI enhancement in `src/axopen.dev`. No PLC source change, no public-API removal. Branch: `feat/axdev-user-secrets-loader`. - -- feat: `AXOpen.Dev.Secrets.UserSecretsLoader` reads dotnet user-secrets into the process environment so PLC verbs resolve `AX_TARGET_PWD` / `AX_USERNAME` without a prior `source load-secrets.sh`. It locates the twin project's `` (probes `../axpansion/twin` then `.`, overridable via the `AX_SECRETS_PROJECT` environment variable), reads its `secrets.json` from the OS user-secrets root, and flattens nested keys with the standard `key:subkey` convention. -- feat: New shared entry point `AxdevApp.Run(args)` calls `UserSecretsLoader.Load()` then builds and runs the command app. Both the packed `dotnet axdev` tool (`AXOpen.Dev.Tool/Program.cs`) and the in-repo dispatcher (`src/scripts/dev.cs`) now call `AxdevApp.Run` instead of `AxdevApp.Build().Run`. -- test: `AXOpen.Dev.Tests/Secrets/UserSecretsLoaderTests.cs` covers apply-from-store, existing-env-wins precedence, missing project / missing store / malformed JSON / no-`UserSecretsId` no-ops, the `AX_SECRETS_PROJECT` override, and nested-key flattening. - -**Impact:** -- The template's credential UX (per-project `dotnet user-secrets`) is preserved while removing the bash `source load-secrets.sh` step, so apax verbs can call `axdev` directly on any platform. -- Precedence is non-surprising: an already-set environment variable (or apax variable) always wins over the secrets store; an explicit `-p/--password` still overrides everything in `PlcCommandSettings.ResolvePassword`. - -**Risks/Review:** -- Secret loading is best-effort: a missing twin project, missing store, or unreadable JSON is a silent no-op, and the per-command argument guards still report any genuinely missing credential. - -**Testing:** -- `dotnet test src/axopen.dev/AXOpen.Dev.Tests` — full suite green (176 passed), including the 8 new `UserSecretsLoaderTests`. - -### [BUILD] Dependency-maintenance tooling + AXSharp `0.47.0-alpha.484` bump - -**Note:** Build/CI tooling and dependency maintenance. No public-API change, no PLC source change. Branch: `deps-update`. - -- feat: `scripts/update-latest-deps.ps1` — bumps all non-AXSharp dependencies (NuGet + npm) to their latest stable versions, sharing common helpers via `scripts/_deps-common.ps1`. -- feat: `scripts/update-vulnerable-deps.ps1` — scans npm and NuGet dependencies for known vulnerabilities and emits a report. -- chore: AXSharp packages bumped to `0.47.0-alpha.484` in `Directory.Packages.props`, with transitive dependencies reconciled. `.config/dotnet-tools.json` updated to match. -- chore: Added `.claude/skills/update-axsharp-version/SKILL.md` — skill for updating AXSharp and Inxton.Operon package versions. -- chore: Removed obsolete `package.json` / `package-lock.json` files across `src/components.abb.robotics`, `src/components.abstractions`, `src/data`, `src/data/src/AXOpen.Data.Blazor`, `src/inspectors`, and a stray `apax.yml`, to clean up the project structure. -- chore: `develop` branch GitVersion mode changed to `ContinuousDeployment`. -- chore: Styling dependencies refreshed (`src/styling/src/package.json` / lock; `momentum.css` regenerated). +- feat: `AxoVisionProNet` (`AXOpen.Components.Cognex.Vision`) — drives a Cognex VisionPro PC over a TCP/.NET channel instead of a PROFINET IO frame. The PLC `Invoke()`s `AxoRemoteTask`s (`Trigger`, `InspectionResult`, `SetRecipe`, `SendSpecificData`, `ReceiveSpecificData`, `TriggerWithSpecificData`, `SendSpecificDataAndTypes`) whose handlers run on the .NET twin; `Restore` is a local `AxoTask`. Use this variant when the camera PC is reachable over the network but is not wired as a PROFINET device. +- feat: `Control` (writable: `TriggerId`, `PartId`, `VariantId`) carries the per-request parameters; read-only `Config` holds the task supervision timers (`InfoTime` 5 s, `ErrorTime` 10 s, `TaskTimeout` 50 s); `Status` surfaces `Accepted`, `TriggerId`, `ErrorCode`, `ActionDescription`, `ErrorDescription`, `RejectReason`. Each remote task is checked for `HasRemoteException` per cycle and its `ErrorDetails` copied into `Status.ErrorDescription`; `Restore()` clears `ErrorCode` and re-arms the tasks. `SendSpecificDataAndTypes` is gated to manual control (commissioning only); `TriggerWithSpecificData` is disabled while `Trigger`/`SendSpecificData`/`SetRecipe` are busy. +- feat: .NET twin TCP protocol stack under `AxoVisonProNet/VisionProtocol/` — `VisionTcpClient` (async connect/send/receive loop with connect-timeout and cancellation), `VisionEnvelope` + `EnvelopeMessages` (framing), and `VisionTypedPayloadSerializer` (typed payload encode/decode). The socket is opened once at host start-up via `InitializeVisionClientAsync(host, port)` on the twin; `AxoVisionProNetSpecificDataContainer` exchanges the typed specific-data payloads. +- feat: `AxoVisionProNetView` (`AXOpen.Components.Cognex.Vision.blazor`) — dedicated proxy view on `AxoComponentContainerView` with `AxoVisionProNetStatusView` / `AxoVisionProNetCommandView` / `AxoVisionProNetSpotView` derivatives, so `RenderableContentControl` auto-selects the dedicated rendering by `Presentation`. +- feat: Showcase — `AxoVisionProNet_Example` (`src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st`) demonstrates the sequencer workflow, `TriggerWithSpecificData`, manual/commissioning send, and error recovery; wired into `CognexVision.st`, the Blazor `CognexVision.razor` page, `Program.cs` (`InitializeVisionClientAsync`), and the search registry. +- docs: Added `src/components.cognex.vision/docs/AxoVisionProNet.md` (CONTROLLER / .NET TWIN / BLAZOR tabs), linked it from `toc.yml`, referenced both VisionPro variants in `README.md`, and bumped `src/components.cognex.vision/docs/CHANGELOG.md` + `GitVersion.yml` to `0.57.0`. **Impact:** -- Routine dependency bumps and vulnerability scanning are now scriptable and reproducible. -- AXSharp consumers build against `0.47.0-alpha.484`. -- Dead npm lockfiles no longer pollute the tree or trigger spurious tooling. +- Applications can integrate a Cognex VisionPro inspection over plain TCP without provisioning a PROFINET device, while keeping the same component-level operate/monitor/spot UX as the PROFINET `AxoVisionPro`. +- Remote-task exceptions are surfaced on `Status.ErrorDescription`/`ErrorCode` and cleared deterministically through `Restore()`. **Risks/Review:** -- Dependency version bumps can introduce behavioural drift; verify a full `dotnet build` and the styling render after pulling. +- The .NET twin source folder is spelled `AxoVisonProNet` (missing the `i`) while the PLC `ctrl/` and Blazor folders use the correct `AxoVisionProNet`. Class/type names are correct so it compiles; the folder name is a cosmetic inconsistency. +- No automated test coverage was added: the TCP protocol classes (`VisionTcpClient`, `VisionEnvelope`, `VisionTypedPayloadSerializer`) have no xUnit tests in `tests/AXOpen.Components.Cognex.Vision.Tests`, and the ST trigger→result→restore flow has no AxUnit case in `ctrl/test/tests.st`. +- `InitializeVisionClientAsync` must be called by the host before the component runs, otherwise the remote tasks report "REMOTE TASK IS NOT INITIALIZED". The showcase `Program.cs` shows the required wiring (hardcoded demo endpoint `192.168.100.142:8500`). **Testing:** -- Run `scripts/update-latest-deps.ps1` and `scripts/update-vulnerable-deps.ps1` end-to-end (exit code 0). -- `dotnet build` from solution root succeeds against the bumped package set. +- `apax ib` in `src/components.cognex.vision/ctrl` — PLC library + twin compile (`dotnet ixc`). +- `dotnet build src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor` — proxy view compiles (verified: 0 errors). +- Showcase `Pages/components-cognex-vision/Documentation/CognexVision.razor`, AxoVisionProNet tab — exercise the sequencer, commissioning send, and error-recovery scenarios against a reachable VisionPro endpoint. ### [CORE] AxoSequencer step-timeout alarm — does not fall after timeout clears diff --git a/src/components.cognex.vision/ctrl/src/AxoVisionProNet/AxoVisionProNet.st b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/AxoVisionProNet.st new file mode 100644 index 000000000..acbb71a02 --- /dev/null +++ b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/AxoVisionProNet.st @@ -0,0 +1,223 @@ +USING AXOpen.Core; +USING AXOpen.Messaging; +USING AXOpen.Messaging.Static; +USING AXOpen.Components.Abstractions; +USING Siemens.Simatic.Hardware.Utilities; +USING Siemens.Simatic.MemoryAccess; +USING AXOpen.Timers; + +NAMESPACE AXOpen.Components.Cognex.Vision + {S7.extern=ReadWrite} + {#ix-prop: public string DeviceIpAddress} + {#ix-prop: public string Proxy} + CLASS AxoVisionProNet EXTENDS AXOpen.Core.AxoComponent + + VAR PUBLIC // TASKS + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#Restore#>"} + RestoreTask : AXOpen.Core.AxoTask; + + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#Trigger#>"} + TriggerTask : AXOpen.Core.AxoRemoteTask; + + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#InspectionResult#>"} + InspectionResultTask : AXOpen.Core.AxoRemoteTask; + + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#SetRecipe#>"} + SetRecipeTask : AXOpen.Core.AxoRemoteTask; + + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#SendSpecificData#>"} + SendSpecificDataTask : AXOpen.Core.AxoRemoteTask; + + + + + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#ReceiveSpecificData#>"} + ReceiveSpecificDataTask : AXOpen.Core.AxoRemoteTask; + + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#TriggerWithSpecificData#>"} + TriggerWithSpecificDataTask : AXOpen.Core.AxoRemoteTask; + + + {#ix-attr:[Container(Layout.Wrap)]} + {#ix-attr:[ComponentDetails("Tasks")]} + {#ix-set:AttributeName = "<#SendSpecificDataAndTypes#>"} + SendSpecificDataAndTypesTask : AXOpen.Core.AxoRemoteTask; + + END_VAR + + VAR PUBLIC // CONFIG + {#ix-attr:[Container(Layout.Stack)]} + {#ix-attr:[ComponentDetails("Config")]} + {#ix-attr:[ReadOnly()]} + Config : AxoVisionProNet_Config; + END_VAR + + VAR PUBLIC // CONTROL + {#ix-attr:[Container(Layout.Stack)]} + {#ix-attr:[ComponentDetails("Control")]} + Control : AxoVisionProNet_Control; + END_VAR + + VAR PUBLIC // STATUS + {#ix-attr:[Container(Layout.Stack)]} + {#ix-attr:[ComponentDetails("Status")]} + Status : AxoVisionProNet_Component_Status; + Messenger : AXOpen.Messaging.Static.AxoMessenger; + TaskMessenger : AXOpen.Messaging.Static.AxoMessenger; + END_VAR + + + + VAR PRIVATE + _progress : INT; + _infoTimer : AXOpen.Timers.OnDelayTimer; + _errorTimer : AXOpen.Timers.OnDelayTimer; + + END_VAR + + /// + /// Runs tasks and logic of this component. + /// >[!IMPORTANT] This method must or one of its overloads be called cyclically. + /// + METHOD PUBLIC OVERRIDE Run + VAR_INPUT + inParent : IAxoObject; + + END_VAR + VAR_OUTPUT + + END_VAR + + + SUPER.Run(inParent); + + Messenger.Serve(THIS); + + RestoreTask.Run(THIS); + + + TriggerTask.Execute(THIS); + if TriggerTask.HasRemoteException and not TriggerTask.IsBusy() then + Status.ErrorDescription := TriggerTask.ErrorDetails; + END_IF; + InspectionResultTask.Execute(THIS); + if InspectionResultTask.HasRemoteException and not InspectionResultTask.IsBusy() then + Status.ErrorDescription := InspectionResultTask.ErrorDetails; + END_IF; + SetRecipeTask.Execute(THIS); + if SetRecipeTask.HasRemoteException and not SetRecipeTask.IsBusy() then + Status.ErrorDescription := SetRecipeTask.ErrorDetails; + END_IF; + SendSpecificDataTask.Execute(THIS); if SendSpecificDataTask.HasRemoteException and not SendSpecificDataTask.IsBusy() then + Status.ErrorDescription := SendSpecificDataTask.ErrorDetails; + END_IF; + ReceiveSpecificDataTask.Execute(THIS); + if ReceiveSpecificDataTask.HasRemoteException and not ReceiveSpecificDataTask.IsBusy() then + Status.ErrorDescription := ReceiveSpecificDataTask.ErrorDetails; + END_IF; + TriggerWithSpecificDataTask.SetIsDisabled(TriggerTask.IsBusy() OR SendSpecificDataTask.IsBusy() OR SetRecipeTask.IsBusy()); + TriggerWithSpecificDataTask.Execute(THIS); + IF TriggerWithSpecificDataTask.HasRemoteException and not TriggerWithSpecificDataTask.IsBusy() then + Status.ErrorDescription := TriggerWithSpecificDataTask.ErrorDetails; + END_IF; + SendSpecificDataAndTypesTask.SetIsDisabled(not THIS.IsManuallyControllable()); // This task is only for commissioning purposes, so it can only be executed in manual mode. + SendSpecificDataAndTypesTask.Execute(THIS); + IF SendSpecificDataAndTypesTask.HasRemoteException and not SendSpecificDataAndTypesTask.IsBusy() then + Status.ErrorDescription := SendSpecificDataAndTypesTask.ErrorDetails; + END_IF; + + + //*************RESTORE******************** + RestoreTask.SetIsDisabled(FALSE); + IF RestoreTask.Execute(THIS) THEN + THIS.Restore(); + Status.ActionDescription := '<#Component restored#>'; + END_IF; + //**************************************** + + + + + END_METHOD + + METHOD PUBLIC Trigger : IAxoTaskState + Trigger := TriggerTask.Invoke(THIS); + END_METHOD + + METHOD PUBLIC InspectionResult : IAxoTaskState + InspectionResult := InspectionResultTask.Invoke(THIS); + END_METHOD + + METHOD PUBLIC SetRecipe : IAxoTaskState + SetRecipe := SetRecipeTask.Invoke(THIS); + END_METHOD + + METHOD PUBLIC SendSpecificData : IAxoTaskState + SendSpecificData := SendSpecificDataTask.Invoke(THIS); + END_METHOD + + METHOD PUBLIC ReceiveSpecificData : IAxoTaskState + ReceiveSpecificData := ReceiveSpecificDataTask.Invoke(THIS); + END_METHOD + METHOD PUBLIC TriggerWithSpecificData : IAxoTaskState + TriggerWithSpecificData := TriggerWithSpecificDataTask.Invoke(THIS); + END_METHOD + METHOD PUBLIC SendSpecificDataAndTypes : IAxoTaskState + SendSpecificDataAndTypes := SendSpecificDataAndTypesTask.Invoke(THIS); + END_METHOD + + METHOD PROTECTED OVERRIDE ManualControl + THIS._isManuallyControllable := TRUE; + + + END_METHOD + METHOD PRIVATE CallTimers + VAR_INPUT + signal : BOOL; + END_VAR + + _infoTimer.OnDelay(THIS, signal AND Config.InfoTime > LT#0S ,Config.InfoTime); + _errorTimer.OnDelay(THIS, signal AND Config.ErrorTime > LT#0S , Config.ErrorTime); + END_METHOD + + /// + /// Restores this component into intial state. + /// + METHOD PUBLIC OVERRIDE Restore + VAR + _index : INT; + END_VAR + + + + TriggerTask.Restore(); + SetRecipeTask.Restore(); + SendSpecificDataTask.Restore(); + SendSpecificDataAndTypesTask.Restore(); + InspectionResultTask.Restore(); + ReceiveSpecificDataTask.Restore(); + TriggerWithSpecificDataTask.Restore(); + Status.ActionDescription := ''; + Status.ErrorDescription := ''; + Status.Accepted := FALSE; + Status.ErrorCode := 0; + Status.RejectReason := ''; + RestoreTask.DoneWhen(TRUE); + + END_METHOD + END_CLASS +END_NAMESPACE \ No newline at end of file diff --git a/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNetSpecificData.st b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNetSpecificData.st new file mode 100644 index 000000000..4c83a4631 --- /dev/null +++ b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNetSpecificData.st @@ -0,0 +1,6 @@ +NAMESPACE AXOpen.Components.Cognex.Vision + {S7.extern=ReadWrite} + CLASS PUBLIC AxoVisionProNetSpecificData + + END_CLASS +END_NAMESPACE \ No newline at end of file diff --git a/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNetSpecificDataContainer.st b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNetSpecificDataContainer.st new file mode 100644 index 000000000..5a45b3366 --- /dev/null +++ b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNetSpecificDataContainer.st @@ -0,0 +1,7 @@ +NAMESPACE AXOpen.Components.Cognex.Vision + {S7.extern=ReadWrite} + {#ix-generic:} + CLASS PUBLIC AxoVisionProNetSpecificDataContainer + + END_CLASS +END_NAMESPACE \ No newline at end of file diff --git a/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Component_Status.st b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Component_Status.st new file mode 100644 index 000000000..1888fe498 --- /dev/null +++ b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Component_Status.st @@ -0,0 +1,16 @@ +NAMESPACE AXOpen.Components.Cognex.Vision + {S7.extern=ReadWrite} + {#ix-attr:[Container(Layout.Stack)]} + CLASS PUBLIC AxoVisionProNet_Component_Status + + VAR PUBLIC + + ActionDescription : String; + ErrorDescription : string; + Accepted : BOOL; + TriggerId : INT; + ErrorCode : INT; + RejectReason : STRING; + END_VAR + END_CLASS +END_NAMESPACE \ No newline at end of file diff --git a/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Config.st b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Config.st new file mode 100644 index 000000000..2b238c6d9 --- /dev/null +++ b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Config.st @@ -0,0 +1,15 @@ +NAMESPACE AXOpen.Components.Cognex.Vision + {S7.extern=ReadWrite} + {#ix-attr:[Container(Layout.Stack)]} + CLASS PUBLIC AxoVisionProNet_Config + VAR PUBLIC + {#ix-set:AttributeName = "<#Info time#>"} + InfoTime : LTIME := LT#5S; + {#ix-set:AttributeName = "<#Error time#>"} + ErrorTime : LTIME := LT#10S; + {#ix-set:AttributeName = "<#Task timeout#>"} + TaskTimeout : LTIME := LT#50S; + + END_VAR + END_CLASS +END_NAMESPACE \ No newline at end of file diff --git a/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Control.st b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Control.st new file mode 100644 index 000000000..d5107031a --- /dev/null +++ b/src/components.cognex.vision/ctrl/src/AxoVisionProNet/Structures/AxoVisionProNet_Control.st @@ -0,0 +1,11 @@ +NAMESPACE AXOpen.Components.Cognex.Vision + {S7.extern=ReadWrite} + {#ix-attr:[Container(Layout.Stack)]} + CLASS PUBLIC AxoVisionProNet_Control + VAR PUBLIC + TriggerId : INT; + PartId : STRING; + VariantId : STRING; + END_VAR + END_CLASS +END_NAMESPACE \ No newline at end of file diff --git a/src/components.cognex.vision/ctrl/test/AxoDatamanTests.st b/src/components.cognex.vision/ctrl/test/AxoDatamanTests.st new file mode 100644 index 000000000..75f930845 --- /dev/null +++ b/src/components.cognex.vision/ctrl/test/AxoDatamanTests.st @@ -0,0 +1,85 @@ +USING AXOpen.Core; +USING AXOpen.Messaging; +USING AXOpen.Messaging.Static; + +// Tests for AxoDataman (Cognex Dataman v6.0.0 code reader). +// +// AxoDataman.Run(inParent, hwID) performs PROFINET hardware-slot introspection +// (ReadSlotFromHardwareID / ReadHardwareIDFromSlot / ReadHardwareIOAddress) before any +// inspection logic. On the LLVM unit-test simulator there is no real PROFINET device, so +// the "settled inspection" happy path is not reachable here (it requires real hardware). +// What IS deterministic on the simulator is the input-guard / hardware-resolution error +// path: those branches set a fixed Status.Error.Id and are exercised below. Expected IDs +// are the component's own documented guard constants (TROUBLES.md is a stub). +NAMESPACE Cognex.Vision.Tests.Dataman + {S7.extern=ReadWrite} + CLASS TestContext EXTENDS AXOpen.Core.Dummies.MockAxoContext + METHOD PROTECTED OVERRIDE Main + ; + END_METHOD + END_CLASS + + {TestFixture} + {S7.extern=ReadWrite} + CLASS AxoDatamanTests + VAR + context : TestContext; + sut : AXOpen.Components.Cognex.Vision.v_6_0_0_0.AxoDataman; + parent : AXOpen.Core.AxoObject; + _rtc : AXOpen.Core.Dummies.MockAxoRtc; + _rtm : AXOpen.Core.Dummies.MockAxoRtm; + _nullParent : IAxoObject; // unassigned interface reference == NULL + END_VAR + + VAR + contextBlank : TestContext; + sutBlank : AXOpen.Components.Cognex.Vision.v_6_0_0_0.AxoDataman; + parentBlank : AXOpen.Core.AxoObject; + END_VAR + + {TestSetup} + METHOD PUBLIC TestSetup + context := contextBlank; + sut := sutBlank; + parent := parentBlank; + _rtm.SetElapsed(LTIME#2s); + context.InjectRtm(_rtm); + _rtc.SetNowUTC(LDATE_AND_TIME#2012-01-12-15:58:12.123); + context.InjectRtc(_rtc); + context.InitializeRootObject(parent); + END_METHOD + + // inParent == NULL is a programming error -> Status.Error.Id := 700. + {Test} + METHOD PUBLIC Run_WithNullParent_RaisesError700 + context.Open(); + sut.Run(_nullParent, UINT#16); + context.Close(); + + AxUnit.Assert.Equal(UINT#700, sut.Status.Error.Id); + END_METHOD + + // hwID == 0 (no device hardware identifier) is a programming error -> 701. + {Test} + METHOD PUBLIC Run_WithZeroHardwareId_RaisesError701 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#0); + context.Close(); + + AxUnit.Assert.Equal(UINT#701, sut.Status.Error.Id); + END_METHOD + + // A non-zero hwID resolves the device, but slot 1 (AcquisitionControl) cannot be + // resolved on the simulator (the unmocked hardware reads return default 0) -> 710. + {Test} + METHOD PUBLIC Run_WithoutHardwarePresent_RaisesSlotError710 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#16); + context.Close(); + + AxUnit.Assert.Equal(UINT#710, sut.Status.Error.Id); + END_METHOD + END_CLASS +END_NAMESPACE diff --git a/src/components.cognex.vision/ctrl/test/AxoInsightV24Tests.st b/src/components.cognex.vision/ctrl/test/AxoInsightV24Tests.st new file mode 100644 index 000000000..b5c545bad --- /dev/null +++ b/src/components.cognex.vision/ctrl/test/AxoInsightV24Tests.st @@ -0,0 +1,77 @@ +USING AXOpen.Core; +USING AXOpen.Messaging; +USING AXOpen.Messaging.Static; + +// Tests for AxoInsight v24.0.0 (Cognex In-Sight 2800 vision sensor). +// +// As with the v6 variant, Run(inParent, hwID) performs PROFINET hardware-slot introspection +// before any vision logic, so on the LLVM simulator only the input-guard / hardware-resolution +// error path is deterministically reachable. Expected IDs are the component's guard constants. +NAMESPACE Cognex.Vision.Tests.InsightV24 + {S7.extern=ReadWrite} + CLASS TestContext EXTENDS AXOpen.Core.Dummies.MockAxoContext + METHOD PROTECTED OVERRIDE Main + ; + END_METHOD + END_CLASS + + {TestFixture} + {S7.extern=ReadWrite} + CLASS AxoInsightV24Tests + VAR + context : TestContext; + sut : AXOpen.Components.Cognex.Vision.v_24_0_0.AxoInsight; + parent : AXOpen.Core.AxoObject; + _rtc : AXOpen.Core.Dummies.MockAxoRtc; + _rtm : AXOpen.Core.Dummies.MockAxoRtm; + _nullParent : IAxoObject; + END_VAR + + VAR + contextBlank : TestContext; + sutBlank : AXOpen.Components.Cognex.Vision.v_24_0_0.AxoInsight; + parentBlank : AXOpen.Core.AxoObject; + END_VAR + + {TestSetup} + METHOD PUBLIC TestSetup + context := contextBlank; + sut := sutBlank; + parent := parentBlank; + _rtm.SetElapsed(LTIME#2s); + context.InjectRtm(_rtm); + _rtc.SetNowUTC(LDATE_AND_TIME#2012-01-12-15:58:12.123); + context.InjectRtc(_rtc); + context.InitializeRootObject(parent); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithNullParent_RaisesError700 + context.Open(); + sut.Run(_nullParent, UINT#16); + context.Close(); + + AxUnit.Assert.Equal(UINT#700, sut.Status.Error.Id); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithZeroHardwareId_RaisesError701 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#0); + context.Close(); + + AxUnit.Assert.Equal(UINT#701, sut.Status.Error.Id); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithoutHardwarePresent_RaisesSlotError710 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#16); + context.Close(); + + AxUnit.Assert.Equal(UINT#710, sut.Status.Error.Id); + END_METHOD + END_CLASS +END_NAMESPACE diff --git a/src/components.cognex.vision/ctrl/test/AxoInsightV6Tests.st b/src/components.cognex.vision/ctrl/test/AxoInsightV6Tests.st new file mode 100644 index 000000000..201dedecb --- /dev/null +++ b/src/components.cognex.vision/ctrl/test/AxoInsightV6Tests.st @@ -0,0 +1,79 @@ +USING AXOpen.Core; +USING AXOpen.Messaging; +USING AXOpen.Messaging.Static; + +// Tests for AxoInsight v6.0.0 (Cognex In-Sight vision sensor). +// +// AxoInsight.Run(inParent, hwID) performs PROFINET hardware-slot introspection before any +// vision logic. On the LLVM unit-test simulator there is no real PROFINET device, so the +// settled inspection happy path is not reachable here. The deterministic, simulator-runnable +// behaviour is the input-guard / hardware-resolution error path; expected IDs are the +// component's own guard constants. +NAMESPACE Cognex.Vision.Tests.InsightV6 + {S7.extern=ReadWrite} + CLASS TestContext EXTENDS AXOpen.Core.Dummies.MockAxoContext + METHOD PROTECTED OVERRIDE Main + ; + END_METHOD + END_CLASS + + {TestFixture} + {S7.extern=ReadWrite} + CLASS AxoInsightV6Tests + VAR + context : TestContext; + sut : AXOpen.Components.Cognex.Vision.v_6_0_0_0.AxoInsight; + parent : AXOpen.Core.AxoObject; + _rtc : AXOpen.Core.Dummies.MockAxoRtc; + _rtm : AXOpen.Core.Dummies.MockAxoRtm; + _nullParent : IAxoObject; + END_VAR + + VAR + contextBlank : TestContext; + sutBlank : AXOpen.Components.Cognex.Vision.v_6_0_0_0.AxoInsight; + parentBlank : AXOpen.Core.AxoObject; + END_VAR + + {TestSetup} + METHOD PUBLIC TestSetup + context := contextBlank; + sut := sutBlank; + parent := parentBlank; + _rtm.SetElapsed(LTIME#2s); + context.InjectRtm(_rtm); + _rtc.SetNowUTC(LDATE_AND_TIME#2012-01-12-15:58:12.123); + context.InjectRtc(_rtc); + context.InitializeRootObject(parent); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithNullParent_RaisesError700 + context.Open(); + sut.Run(_nullParent, UINT#16); + context.Close(); + + AxUnit.Assert.Equal(UINT#700, sut.Status.Error.Id); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithZeroHardwareId_RaisesError701 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#0); + context.Close(); + + AxUnit.Assert.Equal(UINT#701, sut.Status.Error.Id); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithoutHardwarePresent_RaisesSlotError710 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#16); + context.Close(); + + AxUnit.Assert.Equal(UINT#710, sut.Status.Error.Id); + END_METHOD + END_CLASS +END_NAMESPACE diff --git a/src/components.cognex.vision/ctrl/test/AxoVisionProNetTests.st b/src/components.cognex.vision/ctrl/test/AxoVisionProNetTests.st new file mode 100644 index 000000000..bf1917c53 --- /dev/null +++ b/src/components.cognex.vision/ctrl/test/AxoVisionProNetTests.st @@ -0,0 +1,102 @@ +USING AXOpen.Core; +USING AXOpen.Messaging; +USING AXOpen.Messaging.Static; + +// Tests for AxoVisionProNet (Cognex VisionPro over a TCP/.NET remote-task channel). +// +// Unlike the PROFINET variants, Run(inParent) performs NO hardware-slot introspection, so the +// component runs end-to-end on the LLVM simulator. Its data-exchange tasks are AxoRemoteTasks +// whose handlers live on the .NET twin, which is not present under `apax test`; therefore these +// tests cover only twin-independent, deterministic behaviour: +// * a clean cyclic Run leaves the component error-free (Status.ErrorCode = 0), +// * Restore() clears Status (ErrorCode/Accepted/descriptions) per the docs, +// * manual control gates the commissioning task (IsManuallyControllable after activation). +// Remote-task busy/done/exception states (which depend on the missing twin handler) are not +// asserted. +NAMESPACE Cognex.Vision.Tests.VisionProNet + {S7.extern=ReadWrite} + CLASS TestContext EXTENDS AXOpen.Core.Dummies.MockAxoContext + METHOD PROTECTED OVERRIDE Main + ; + END_METHOD + END_CLASS + + {TestFixture} + {S7.extern=ReadWrite} + CLASS AxoVisionProNetTests + VAR + context : TestContext; + sut : AXOpen.Components.Cognex.Vision.AxoVisionProNet; + parent : AXOpen.Core.AxoObject; + _rtc : AXOpen.Core.Dummies.MockAxoRtc; + _rtm : AXOpen.Core.Dummies.MockAxoRtm; + END_VAR + + VAR + contextBlank : TestContext; + sutBlank : AXOpen.Components.Cognex.Vision.AxoVisionProNet; + parentBlank : AXOpen.Core.AxoObject; + END_VAR + + {TestSetup} + METHOD PUBLIC TestSetup + context := contextBlank; + sut := sutBlank; + parent := parentBlank; + _rtm.SetElapsed(LTIME#2s); + context.InjectRtm(_rtm); + _rtc.SetNowUTC(LDATE_AND_TIME#2012-01-12-15:58:12.123); + context.InjectRtc(_rtc); + context.InitializeRootObject(parent); + END_METHOD + + METHOD PRIVATE Cycle + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent); + context.Close(); + END_METHOD + + // A clean cyclic run with nothing invoked leaves the component free of error. + {Test} + METHOD PUBLIC Run_WhenIdle_KeepsErrorCodeZero + VAR i : INT; END_VAR + FOR i := 0 TO 3 DO + THIS.Cycle(); + END_FOR; + + AxUnit.Assert.Equal(INT#0, sut.Status.ErrorCode); + END_METHOD + + // Restore() clears the status fields back to their initial state. + {Test} + METHOD PUBLIC Restore_ClearsStatus + THIS.Cycle(); + + // dirty the status, then restore + sut.Status.ErrorCode := INT#42; + sut.Status.Accepted := TRUE; + + context.Open(); + context.InitializeRootObject(parent); + sut.Restore(); + sut.Run(parent); + context.Close(); + + AxUnit.Assert.Equal(INT#0, sut.Status.ErrorCode); + AxUnit.Assert.Equal(FALSE, sut.Status.Accepted); + END_METHOD + + // After ActivateManualControl(), the component reports itself manually controllable + // for the activating context cycle (and the next), per AxoComponent semantics. + {Test} + METHOD PUBLIC ManualControl_WhenActivated_IsManuallyControllable + THIS.Cycle(); // bind the component to the context + + sut.ActivateManualControl(); + THIS.Cycle(); // evaluate manual-control state within the window + + AxUnit.Assert.Equal(TRUE, sut.IsManuallyControllable()); + END_METHOD + END_CLASS +END_NAMESPACE diff --git a/src/components.cognex.vision/ctrl/test/AxoVisionProTests.st b/src/components.cognex.vision/ctrl/test/AxoVisionProTests.st new file mode 100644 index 000000000..809eb1400 --- /dev/null +++ b/src/components.cognex.vision/ctrl/test/AxoVisionProTests.st @@ -0,0 +1,81 @@ +USING AXOpen.Core; +USING AXOpen.Messaging; +USING AXOpen.Messaging.Static; + +// Tests for AxoVisionPro (Cognex VisionPro over PROFINET, multi-camera). +// +// Run(inParent, hwID, CameraNo) performs extensive PROFINET hardware-slot introspection +// (17 slots) before any vision logic. On the LLVM simulator there is no real PROFINET device, +// so the settled inspection happy path is not reachable here. The deterministic, +// simulator-runnable behaviour is the input-guard / hardware-resolution error path; expected +// IDs are the component's guard constants. +NAMESPACE Cognex.Vision.Tests.VisionPro + {S7.extern=ReadWrite} + CLASS TestContext EXTENDS AXOpen.Core.Dummies.MockAxoContext + METHOD PROTECTED OVERRIDE Main + ; + END_METHOD + END_CLASS + + {TestFixture} + {S7.extern=ReadWrite} + CLASS AxoVisionProTests + VAR + context : TestContext; + sut : AXOpen.Components.Cognex.Vision.AxoVisionPro; + parent : AXOpen.Core.AxoObject; + _rtc : AXOpen.Core.Dummies.MockAxoRtc; + _rtm : AXOpen.Core.Dummies.MockAxoRtm; + _nullParent : IAxoObject; + END_VAR + + VAR + contextBlank : TestContext; + sutBlank : AXOpen.Components.Cognex.Vision.AxoVisionPro; + parentBlank : AXOpen.Core.AxoObject; + END_VAR + + {TestSetup} + METHOD PUBLIC TestSetup + context := contextBlank; + sut := sutBlank; + parent := parentBlank; + _rtm.SetElapsed(LTIME#2s); + context.InjectRtm(_rtm); + _rtc.SetNowUTC(LDATE_AND_TIME#2012-01-12-15:58:12.123); + context.InjectRtc(_rtc); + context.InitializeRootObject(parent); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithNullParent_RaisesError700 + context.Open(); + sut.Run(_nullParent, UINT#16, BYTE#1); + context.Close(); + + AxUnit.Assert.Equal(UINT#700, sut.Status.Error.Id); + END_METHOD + + {Test} + METHOD PUBLIC Run_WithZeroHardwareId_RaisesError701 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#0, BYTE#1); + context.Close(); + + AxUnit.Assert.Equal(UINT#701, sut.Status.Error.Id); + END_METHOD + + // Non-zero hwID resolves the device, but slot 1 (SystemControl) cannot be resolved on + // the simulator (unmocked hardware reads return default 0) -> 710. + {Test} + METHOD PUBLIC Run_WithoutHardwarePresent_RaisesSlotError710 + context.Open(); + context.InitializeRootObject(parent); + sut.Run(parent, UINT#16, BYTE#1); + context.Close(); + + AxUnit.Assert.Equal(UINT#710, sut.Status.Error.Id); + END_METHOD + END_CLASS +END_NAMESPACE diff --git a/src/components.cognex.vision/docs/AxoVisionProNet.md b/src/components.cognex.vision/docs/AxoVisionProNet.md new file mode 100644 index 000000000..ec9602d95 --- /dev/null +++ b/src/components.cognex.vision/docs/AxoVisionProNet.md @@ -0,0 +1,127 @@ +# AxoVisionProNet + +_Cognex VisionPro vision system over a TCP/.NET remote-task channel_ + +`AxoVisionProNet` is a TCP/.NET alternative to the PROFINET-based [`AxoVisionPro`](AxoVisionPro.md). +Instead of exchanging data through a PROFINET IO frame, it drives the VisionPro PC +through `AxoRemoteTask`s whose handlers run on the .NET twin and talk to the camera +over a TCP socket. Use this variant when the vision PC is reachable over the network +but is not wired as a PROFINET device. + +## Configuration + +The `Config` struct (read-only at runtime) holds the task supervision timers: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `InfoTime` | `LTIME` | `LT#5S` | Delay before an informational state is raised. | +| `ErrorTime` | `LTIME` | `LT#10S` | Delay before a stalled exchange is flagged as an error. | +| `TaskTimeout` | `LTIME` | `LT#50S` | Maximum time a remote task may run before timing out. | + +The writable `Control` struct carries the request parameters sent to the vision PC +on every call: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `TriggerId` | `INT` | Identifier correlating a trigger with its inspection result. | +| `PartId` | `STRING` | Part being inspected. | +| `VariantId` | `STRING` | Recipe / product variant selected on the camera. | + +## Status + +| Field | Type | Description | +|-------|------|-------------| +| `Accepted` | `BOOL` | Last exchange was acknowledged by the vision PC. | +| `TriggerId` | `INT` | `TriggerId` echoed back with the result. | +| `ErrorCode` | `INT` | Non-zero when an exchange failed; `0` after `Restore()`. | +| `ActionDescription` | `STRING` | Human-readable description of the current action. | +| `ErrorDescription` | `STRING` | Detail copied from the failing task's `ErrorDetails`. | +| `RejectReason` | `STRING` | Reason a part was rejected by the inspection. | + +# [CONTROLLER](#tab/controller) + +## Declare component + +[!code-pascal[](../../showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st?name=ComponentDeclaration)] + +## Declare initialization variables + +[!code-pascal[](../../showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st?name=InitializationArgumentsDeclaration)] + +## Initialize & Run + +[!code-pascal[](../../showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st?name=Initialization)] + +[!INCLUDE [IntializeAndRun](../../../docfx/articles/notes/CYCLIC_UPDATE_NOTICE.md)] + +## Use + +[!code-pascal[](../../showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st?name=Usage)] + +## Manual control + +*The commissioning task `SendSpecificDataAndTypes` is enabled only while manual control is active.* + +[!code-pascal[](../../showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st?name=VisionProNetManualControl)] + +## Commissioning + +[!code-pascal[](../../showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st?name=VisionProNetCommissioning)] + +## Error recovery + +*Each remote task is checked for `HasRemoteException`; recovery calls `Restore()` to clear `ErrorCode` and re-arm the tasks.* + +[!code-pascal[](../../showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st?name=VisionProNetErrorRecovery)] + +## Source + +View the library source at [`AxoVisionProNet`](https://github.com/Inxton/AXOpen/tree/1104-new-featureaxovisionpro-alternative/src/components.cognex.vision/ctrl/src/AxoVisionProNet/). + +# [.NET TWIN](#tab/twin) + +`AxoVisionProNet` relies on its .NET twin for connectivity. The component's tasks are +`AxoRemoteTask`s: the PLC `Invoke()`s them and the .NET side executes the handler that +exchanges data with the vision PC over TCP. The TCP socket must be opened once during +host start-up by calling `InitializeVisionClientAsync(host, port)` on the twin. + +## TCP client initialization + +[!code-csharp[](../../showcase/app/ix-blazor/showcase.blazor/Program.cs?name=AxoVisionProNetInitialize)] + +## Source + +View the .NET twin source at [`AXOpen.Components.Cognex.Vision`](https://github.com/Inxton/AXOpen/tree/1104-new-featureaxovisionpro-alternative/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/). + +# [BLAZOR](#tab/blazor) + +`AxoVisionProNet` ships a dedicated Blazor view, `AxoVisionProNetView`, in the +`AXOpen.Components.Cognex.Vision.blazor` package. It builds on `AxoComponentContainerView` +and exposes the `AxoVisionProNetStatusView`, `AxoVisionProNetCommandView`, and +`AxoVisionProNetSpotView` derivatives. `RenderableContentControl` inspects the component +type at runtime and selects the matching derivative based on the `Presentation` attribute, +so the generic rendering below resolves to the dedicated view automatically. + +## Status display + +[!code-html[](../../showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor?name=GenericComponentStatusView)] + +## Command control + +[!code-html[](../../showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor?name=GenericComponentCommandView)] + +## Type-agnostic status view + +[!code-html[](../../showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor?name=RccComponentStatusView)] + +## Type-agnostic command view + +[!code-html[](../../showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor?name=RccComponentCommandView)] + +Available `Presentation` values: `Status-Display`, `Command-Control`, `Service-Control`, `Spot`, `Compact`. + +## Source + +View the Blazor package at [`AXOpen.Components.Cognex.Vision.blazor`](https://github.com/Inxton/AXOpen/tree/1104-new-featureaxovisionpro-alternative/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor/). + +--- diff --git a/src/components.cognex.vision/docs/CHANGELOG.md b/src/components.cognex.vision/docs/CHANGELOG.md index 68b64cd95..45f65fcb8 100644 --- a/src/components.cognex.vision/docs/CHANGELOG.md +++ b/src/components.cognex.vision/docs/CHANGELOG.md @@ -23,6 +23,14 @@ on every run. --> +### 0.57.0 + +**New features:** +- Added `AxoVisionProNet.md` documenting the TCP/.NET VisionPro variant (`AxoVisionProNet`): CONTROLLER tab (declaration, init, usage, manual control, commissioning, error recovery), .NET TWIN tab (`InitializeVisionClientAsync` TCP wiring), BLAZOR tab (dedicated `AxoVisionProNetView` + RCC presentations). + +**Other:** +- Linked `AxoVisionProNet.md` from `toc.yml` and referenced both VisionPro variants from `README.md`. + ### 0.43.0 **Other:** diff --git a/src/components.cognex.vision/docs/README.md b/src/components.cognex.vision/docs/README.md index b9ecaa9fb..e493a21b8 100644 --- a/src/components.cognex.vision/docs/README.md +++ b/src/components.cognex.vision/docs/README.md @@ -2,7 +2,7 @@ The **components.cognex.vision** is a set of libraries covering the product portfolio of the vision systems from the vendor [Cognex](https://www.cognex.com/) for the target PLC platform [Siemens AX](https://www.siemens.com/global/en/products/automation/industry-software/automation-software/simatic-ax.html) and [AxOpen](https://github.com/inxton/AXOpen?tab=readme-ov-file) framework. -The package consists of a PLC library providing control logic and its .NET twin counterpart aimed at the visualization part. This package currently covers the Dataman production family with the firmware 6.0.0.0 and the Insight production family with the firmware 6.0.0.0. +The package consists of a PLC library providing control logic and its .NET twin counterpart aimed at the visualization part. This package currently covers the Dataman production family with the firmware 6.0.0.0 and the Insight production family with the firmware 6.0.0.0. It also covers the VisionPro vision system in two variants: [`AxoVisionPro`](AxoVisionPro.md) over PROFINET and [`AxoVisionProNet`](AxoVisionProNet.md) over a TCP/.NET remote-task channel. ### Links to documentation [Siemens AX-documentation](https://developer.siemens.com/simatic-ax/developer.html) diff --git a/src/components.cognex.vision/docs/toc.yml b/src/components.cognex.vision/docs/toc.yml index 4a62cf707..5cd52c20f 100644 --- a/src/components.cognex.vision/docs/toc.yml +++ b/src/components.cognex.vision/docs/toc.yml @@ -20,6 +20,8 @@ href: AxoInsight_v_24_0_0.md - name: AxoVisionPro href: AxoVisionPro.md + - name: AxoVisionProNet + href: AxoVisionProNet.md - name: Changes href: CHANGELOG.md - name: Troubleshooting diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor/AxoVisionProNet/AxoVisionProNetView.razor b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor/AxoVisionProNet/AxoVisionProNetView.razor new file mode 100644 index 000000000..0bbd1d386 --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor/AxoVisionProNet/AxoVisionProNetView.razor @@ -0,0 +1,124 @@ +@namespace AXOpen.Components.Cognex.Vision +@using AXOpen.Core +@using AXOpen.Core.Blazor +@using AXSharp.Presentation.Blazor.Controls +@using AXSharp.Presentation.Blazor.Controls.RenderableContent +@inherits AxoComponentViewBase + + + +
+ @if (Component.Status.Accepted.Cyclic) + { + Accepted + } + else + { + Idle + } + @if (Component.Status.ErrorCode.Cyclic != 0) + { + Error @Component.Status.ErrorCode.Cyclic + } + @Component.DeviceIpAddress +
+
+ + +
+
+
Inspection
+
+ + + + +
+
+
+
Specific data
+
+ + + + +
+
+
+
+ + +
+
+
Inspection
+
+ + + + +
+
+
+
Specific data
+
+ + + + +
+
+
+
+ + + @* Control: writable request parameters fed to every Vision PC call (TriggerId / PartId / VariantId). *@ +
+ +
+
+ + +
+ + +
+
Device: @Component.DeviceIpAddress
+
Proxy: @Component.Proxy
+
+
+
+ + +
+
@Component.AttributeName
+
+
+ @if (Component.Status.ErrorCode.Cyclic != 0) + { + Error @Component.Status.ErrorCode.Cyclic + } + @Component.Status.ActionDescription.Cyclic +
+
+
+
+ +@code { + public override async void ConfigurePolling() + { + // Status primitives read directly in markup (header / spot) + this.StartPolling(Component.Status.Accepted, 500); + this.StartPolling(Component.Status.ErrorCode, 500); + this.StartPolling(Component.Status.ActionDescription, 500); + + // Task statuses (mirrored Operate/Monitor) + this.StartPolling(Component.RestoreTask.Status); + this.StartPolling(Component.TriggerTask.Status); + this.StartPolling(Component.InspectionResultTask.Status); + this.StartPolling(Component.SetRecipeTask.Status); + this.StartPolling(Component.SendSpecificDataTask.Status); + this.StartPolling(Component.ReceiveSpecificDataTask.Status); + this.StartPolling(Component.TriggerWithSpecificDataTask.Status); + this.StartPolling(Component.SendSpecificDataAndTypesTask.Status); + } +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor/AxoVisionProNet/AxoVisionProNetView.razor.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor/AxoVisionProNet/AxoVisionProNetView.razor.cs new file mode 100644 index 000000000..fe9646f2b --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision.blazor/AxoVisionProNet/AxoVisionProNetView.razor.cs @@ -0,0 +1,23 @@ +using AXOpen.Core.Blazor; + +namespace AXOpen.Components.Cognex.Vision +{ + public partial class AxoVisionProNetView : AxoComponentViewBase + { + } + + public class AxoVisionProNetStatusView : AxoVisionProNetView + { + public AxoVisionProNetStatusView() { this.ViewType = eViewType.Status; } + } + + public class AxoVisionProNetCommandView : AxoVisionProNetView + { + public AxoVisionProNetCommandView() { this.ViewType = eViewType.Command; } + } + + public class AxoVisionProNetSpotView : AxoVisionProNetView + { + public AxoVisionProNetSpotView() { this.ViewType = eViewType.Spot; } + } +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.TcpClient.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.TcpClient.cs new file mode 100644 index 000000000..c44974dbe --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.TcpClient.cs @@ -0,0 +1,315 @@ +using AXOpen.Components.Cognex.Vision.VisionProtocol; +using AXSharp.Connector; +using System.Text.Json; + +namespace AXOpen.Components.Cognex.Vision; + +/// +/// Partial class — TCP client integration for AxoVisionProNet. +/// This file owns the instance and the +/// async implementations that back each . +/// +public partial class AxoVisionProNet +{ + private VisionTcpClient? _visionClient; + + /// + /// Configures and connects the Vision PC TCP client. + /// The component's own is used + /// automatically as ComponentSymbol in every message envelope. + /// + /// Hostname or IP of the Vision PC TCP server. + /// TCP port (default 8500). + /// Persistent keeps socket open, PerRequest opens/closes per call. + /// Optional cancellation token. + public async Task InitializeVisionClientAsync( + string host, + int port = 8500, + VisionConnectionMode connectionMode = VisionConnectionMode.Persistent, + CancellationToken ct = default) + { + if (_visionClient is not null) + await _visionClient.DisposeAsync(); + + var options = new VisionTcpClientOptions + { + Host = host, + Port = port, + ConnectionMode = connectionMode, + ComponentSymbol = this.Symbol // full twin symbol, e.g. "Ctx.VisionStation1" + }; + + _visionClient = new VisionTcpClient(options); + + if (connectionMode == VisionConnectionMode.Persistent) + await _visionClient.ConnectAsync(ct); + } + + // ────────────────────────────────────────────────────────────── + // RemoteTask handlers + // ────────────────────────────────────────────────────────────── + + /// + /// Executed by when PLC invokes the remote call. + /// + /// If true, includes specific data in the trigger request. + private async Task Trigger() + { + if (_visionClient is null) + throw new InvalidOperationException( + "VisionTcpClient is not initialized. Call InitializeVisionClientAsync first."); + + var control = await Control.OnlineToPlainAsync(eAccessPriority.High); + + JsonElement? dataElement = null; + + + var payload = new TriggerRequestPayload + { + TriggerId = control.TriggerId, + PartId = control.PartId, + Variant = control.VariantId, + Data = dataElement + }; + + var result = await _visionClient.TriggerAsync(payload); + + + + var status = Status.CreateEmptyPoco(); + status.Accepted = result.Accepted; + status.TriggerId = result.TriggerId; + status.ErrorCode = result.ErrorCode; + status.RejectReason = result.RejectReason; + await Status.PlainToOnline(status, priority: eAccessPriority.High); + + if (!result.Accepted) + throw new InvalidOperationException( + $"TriggerRequest rejected by Vision PC: [{result.ErrorCode}] {result.RejectReason}"); + } + + /// + /// Executed by when PLC invokes the remote call. + /// + private async Task SetRecipe() + { + if (_visionClient is null) + throw new InvalidOperationException( + "VisionTcpClient is not initialized. Call InitializeVisionClientAsync first."); + + + + JsonElement? dataElement = null; + var control = await Control.OnlineToPlainAsync(eAccessPriority.High); + + var payload = new SetRecipeRequestPayload + { + Variant = control.VariantId, + Data = dataElement + }; + + var result = await _visionClient.SetRecipeAsync(payload); + + + + var status = Status.CreateEmptyPoco(); + status.Accepted = result.Success; + status.TriggerId = 0; + status.ErrorCode = result.ErrorCode; + status.RejectReason = result.Reason; + await Status.PlainToOnline(status, priority: eAccessPriority.High); + + if (!result.Success) + throw new InvalidOperationException( + $"SetRecipeRequest failed: [{result.ErrorCode}] {result.Reason}"); + } + + + /// + /// Executed by when PLC invokes the remote call. + /// + private async Task SendSpecificData() + { + await SendSpecificDataCore(includeTypes: false); + } + + /// + /// Executed by when PLC invokes the remote call. + /// + private async Task SendSpecificDataTypes() + { + await SendSpecificDataCore(includeTypes: true); + } + + private async Task SendSpecificDataCore(bool includeTypes) + { + if (_visionClient is null) + throw new InvalidOperationException( + "VisionTcpClient is not initialized. Call InitializeVisionClientAsync first."); + + var container = SpecificDataContainer; + if (container == null) return; + + var plainData = (await GetDataAsync(eAccessPriority.Normal))?.Plain; + if (plainData == null) return; + + var payload = new SendSpecificDataRequestPayload + { + Data = includeTypes + ? VisionTypedPayloadSerializer.SerializeToElement(plainData) + : JsonSerializer.SerializeToElement(plainData, plainData.GetType(), VisionJsonOptions.Default) + }; + + var result = includeTypes + ? await _visionClient.SendSpecificDataTypesAsync(payload) + : await _visionClient.SendSpecificDataAsync(payload); + + + var status = Status.CreateEmptyPoco(); + status.Accepted = result.Success; + status.TriggerId = 0; + status.ErrorCode = result.ErrorCode; + status.RejectReason = result.Reason; + await Status.PlainToOnline(status, priority: eAccessPriority.High); + + + if (!result.Success) + throw new InvalidOperationException( + $"{(includeTypes ? VisionEnvelope.MessageTypes.SendSpecificDataTypesRequest : VisionEnvelope.MessageTypes.SendSpecificDataRequest)} failed: [{result.ErrorCode}] {result.Reason}"); + } + + + /// + /// Executed by when PLC invokes the remote call. + /// + private async Task ReceiveSpecificData() + { + if (_visionClient is null) + throw new InvalidOperationException( + "VisionTcpClient is not initialized. Call InitializeVisionClientAsync first."); + + + + var plainData = (await GetDataAsync(eAccessPriority.Normal))?.Plain; + + + if (plainData == null) return; + + var payload = new ReceiveSpecificDataRequestPayload + { + Data = JsonSerializer.SerializeToElement(plainData, plainData.GetType(), VisionJsonOptions.Default) + }; + + var result = await _visionClient.ReceiveSpecificDataAsync(payload); + + + + var status = Status.CreateEmptyPoco(); + status.Accepted = result.Success; + status.TriggerId = 0; + status.ErrorCode = result.ErrorCode; + status.RejectReason = result.Reason; + + await Status.PlainToOnline(status, priority: eAccessPriority.High); + if (!result.Data.HasValue) + return; + + var data = result.Data.Value.Deserialize(plainData.GetType(), VisionJsonOptions.Default); + if (data == null) + return; + + await PlainToOnlineAsync(data, eAccessPriority.Normal); + + if (!result.Success) + throw new InvalidOperationException( + $"ReceiveSpecificDataRequest failed: [{result.ErrorCode}] {result.Reason}"); + + } + + + private async Task TriggerWithSpecificData() + { + if (_visionClient is null) + throw new InvalidOperationException( + "VisionTcpClient is not initialized. Call InitializeVisionClientAsync first."); + + var control = await Control.OnlineToPlainAsync(eAccessPriority.High); + var container = SpecificDataContainer; + if (container == null) return; + + var plainData = (await GetDataAsync(eAccessPriority.Normal))?.Plain; + if (plainData == null) return; + + + var payload = new TriggerRequestPayload + { + TriggerId = control.TriggerId, + PartId = control.PartId, + Variant = control.VariantId, + Data = JsonSerializer.SerializeToElement(plainData, plainData.GetType(), VisionJsonOptions.Default) + }; + + + + var result = await _visionClient.TriggerWithSpecificDataAsync(payload); + + + + var status = Status.CreateEmptyPoco(); + status.Accepted = result.Accepted; + status.TriggerId = result.TriggerId; + status.ErrorCode = result.ErrorCode; + status.RejectReason = result.RejectReason; + await Status.PlainToOnline(status, priority: eAccessPriority.High); + + if (!result.Data.HasValue) + return; + + var data = result.Data.Value.Deserialize(plainData.GetType(), VisionJsonOptions.Default); + if (data == null) + return; + + await PlainToOnlineAsync(data, eAccessPriority.Normal); + + if (!result.Accepted) + throw new InvalidOperationException( + $"TriggerWithSpecificDataRequest rejected by Vision PC: [{result.ErrorCode}] {result.RejectReason}"); + } + + /// + /// Executed by when PLC invokes the remote call. + /// + private async Task InspectionResult() + { + if (_visionClient is null) + throw new InvalidOperationException( + "VisionTcpClient is not initialized. Call InitializeVisionClientAsync first."); + + var container = SpecificDataContainer; + if (container == null) return; + + var plainData = (await GetDataAsync(eAccessPriority.Normal))?.Plain; + if (plainData == null) return; + + var payload = new InspectionResultRequestPayload + { + Data = VisionTypedPayloadSerializer.SerializeToElement(plainData) + }; + + var result = await _visionClient.InspectionResultAsync(payload); + + + + await PlainToOnlineAsync(plainData, eAccessPriority.Normal); + + if (!result.Success) + throw new InvalidOperationException( + $"InspectionResultRequest failed: [{result.ErrorCode}] {result.Reason}"); + } + + + + + + +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.cs new file mode 100644 index 000000000..778f42235 --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.cs @@ -0,0 +1,358 @@ +using AXOpen.Messaging.Static; +using AXSharp.Connector; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace AXOpen.Components.Cognex.Vision +{ + public partial class AxoVisionProNet + { + private ITwinObject? _specificDataContainer; + private bool _specificDataContainerResolved; + + public ITwinObject? SpecificDataContainer + { + get + { + if (!_specificDataContainerResolved) + { + _specificDataContainer = FindChildDerivedFrom(typeof(AxoVisionProNetSpecificDataContainer<,>)); + _specificDataContainerResolved = true; + } + + return _specificDataContainer; + } + } + + partial void PostConstruct(ITwinObject parent, string readableTail, string symbolTail) + { + try + { + + this.TriggerTask.InitializeExclusively(() => RunWithLifecycleAsync(TaskLifecycleCodes.TriggerInvoked, TaskLifecycleCodes.TriggerFinished, TaskLifecycleCodes.TriggerFailed, Trigger)); + this.SetRecipeTask.InitializeExclusively(() => RunWithLifecycleAsync(TaskLifecycleCodes.SetRecipeInvoked, TaskLifecycleCodes.SetRecipeFinished, TaskLifecycleCodes.SetRecipeFailed, SetRecipe)); + this.InspectionResultTask.InitializeExclusively(() => RunWithLifecycleAsync(TaskLifecycleCodes.InspectionResultInvoked, TaskLifecycleCodes.InspectionResultFinished, TaskLifecycleCodes.InspectionResultFailed, InspectionResult)); + this.SendSpecificDataTask.InitializeExclusively(() => RunWithLifecycleAsync(TaskLifecycleCodes.SendSpecificDataInvoked, TaskLifecycleCodes.SendSpecificDataFinished, TaskLifecycleCodes.SendSpecificDataFailed, SendSpecificData)); + this.ReceiveSpecificDataTask.InitializeExclusively(() => RunWithLifecycleAsync(TaskLifecycleCodes.ReceiveSpecificDataInvoked, TaskLifecycleCodes.ReceiveSpecificDataFinished, TaskLifecycleCodes.ReceiveSpecificDataFailed, ReceiveSpecificData)); + this.SendSpecificDataAndTypesTask.InitializeExclusively(() => RunWithLifecycleAsync(TaskLifecycleCodes.SendSpecificDataAndTypesInvoked, TaskLifecycleCodes.SendSpecificDataAndTypesFinished, TaskLifecycleCodes.SendSpecificDataAndTypesFailed, SendSpecificDataTypes)); + this.TriggerWithSpecificDataTask.InitializeExclusively(() => RunWithLifecycleAsync(TaskLifecycleCodes.TriggerWithSpecificDataInvoked, TaskLifecycleCodes.TriggerWithSpecificDataFinished, TaskLifecycleCodes.TriggerWithSpecificDataFailed, TriggerWithSpecificData)); + + + } + catch (Exception) + { + throw; + } + } + + /// + /// Lifecycle message codes written to Status.Action.Id (invoked / finished) + /// and Status.Error.Id (failed) by . + /// + internal static class TaskLifecycleCodes + { + public const ulong TriggerInvoked = 1000; + public const ulong TriggerFinished = 1001; + public const ulong TriggerFailed = 10000; + + public const ulong InspectionResultInvoked = 1010; + public const ulong InspectionResultFinished = 1011; + public const ulong InspectionResultFailed = 10010; + + public const ulong SetRecipeInvoked = 1020; + public const ulong SetRecipeFinished = 1021; + public const ulong SetRecipeFailed = 10020; + + public const ulong SendSpecificDataInvoked = 1030; + public const ulong SendSpecificDataFinished = 1031; + public const ulong SendSpecificDataFailed = 10030; + + public const ulong ReceiveSpecificDataInvoked = 1040; + public const ulong ReceiveSpecificDataFinished = 1041; + public const ulong ReceiveSpecificDataFailed = 10040; + + public const ulong TriggerWithSpecificDataInvoked = 1050; + public const ulong TriggerWithSpecificDataFinished = 1051; + public const ulong TriggerWithSpecificDataFailed = 10050; + + public const ulong SendSpecificDataAndTypesInvoked = 1060; + public const ulong SendSpecificDataAndTypesFinished = 1061; + public const ulong SendSpecificDataAndTypesFailed = 10060; + } + + /// + /// Wraps a remote-task body so the lifecycle (invoked / finished / failed) + /// is written to the component's Status.Action.Id and Status.Error.Id. + /// On exception the failed code is published and the original exception is rethrown. + /// + private async Task RunWithLifecycleAsync(ulong invokedCode, ulong finishedCode, ulong failedCode, Func body) + { + await Status.ActionDescription.SetAsync(GetTaskActionMessage(invokedCode)); + try + { + await body(); + await Status.ActionDescription.SetAsync(GetTaskActionMessage(invokedCode)); + } + catch + { + await Status.ActionDescription.SetAsync(GetDefaultErrorMessage(failedCode)); + throw; + } + } + + + + + + private static void CollectAllPrimitives(ITwinObject current, List result) + { + // Collect primitive value tags at this level + foreach (var tag in current.GetValueTags()) + { + result.Add(tag); + } + + // Recurse into child ITwinObjects + foreach (var child in current.GetChildren()) + { + CollectAllPrimitives(child, result); + } + } + + /// + /// Gets the data entity with both online and plain representations from the container. + /// + public async Task<(AxoVisionProNetSpecificData Online,Pocos.AXOpen.Components.Cognex.Vision.AxoVisionProNetSpecificData Plain)?> GetDataAsync(eAccessPriority priority = eAccessPriority.Normal) + { + var container = SpecificDataContainer; + if (container == null) return null; + + var method = container.GetType().GetMethod("GetDataAsync"); + if (method == null) return null; + + var taskObj = method.Invoke(container, new object[] { priority }); + if (taskObj is not Task task) return null; + + await task.ConfigureAwait(false); + + var resultProp = task.GetType().GetProperty("Result"); + var result = resultProp?.GetValue(task); + if (result == null) return null; + + var tupleType = result.GetType(); + var online = tupleType.GetField("Item1")?.GetValue(result) as AxoVisionProNetSpecificData; + var plain = tupleType.GetField("Item2")?.GetValue(result) as Pocos.AXOpen.Components.Cognex.Vision.AxoVisionProNetSpecificData; + + if (online == null) return null; + return (online, plain!); + } + + /// + /// Gets the plain data from the specific data container by reading from the PLC. + /// + public async Task GetPlainDataAsync(eAccessPriority priority = eAccessPriority.Normal) + { + var container = SpecificDataContainer; + if (container == null) return null; + + var method = container.GetType().GetMethod("GetPlainDataAsync"); + if (method == null) return null; + + var taskObj = method.Invoke(container, new object[] { priority }); + if (taskObj is not Task task) return null; + + await task.ConfigureAwait(false); + + var resultProp = task.GetType().GetProperty("Result"); + return resultProp?.GetValue(task); + } + + /// + /// Writes plain data back to the PLC via the specific data container. + /// + public async Task PlainToOnlineAsync(object plain, eAccessPriority priority = eAccessPriority.Normal) + { + var container = SpecificDataContainer; + if (container == null) return; + + var method = container + .GetType() + .GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(m => m.Name == "PlainToOnlineAsync") + .Select(m => new { Method = m, Params = m.GetParameters() }) + .Where(x => x.Params.Length == 2 + && x.Params[1].ParameterType == typeof(eAccessPriority) + && x.Params[0].ParameterType.IsAssignableFrom(plain.GetType())) + .Select(x => x.Method) + .FirstOrDefault(); + + if (method == null) return; + + var taskObj = method.Invoke(container, new object[] { plain, priority }); + if (taskObj is Task task) + await task.ConfigureAwait(false); + } + + private ITwinObject? FindChildDerivedFrom(Type openGenericBase) + { + for (var type = GetType(); type != null; type = type.BaseType) + { + var properties = type.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.DeclaredOnly); + + foreach (var prop in properties) + { + if (!prop.CanRead || prop.GetIndexParameters().Length > 0) + continue; + + if (DerivesFromOpenGeneric(prop.PropertyType, openGenericBase)) + return prop.GetValue(this) as ITwinObject; + } + } + + return null; + } + + private static bool DerivesFromOpenGeneric(Type type, Type openGenericBase) + { + // Walk base types + for (var t = type; t != null; t = t.BaseType) + { + if (t.IsGenericType && t.GetGenericTypeDefinition() == openGenericBase) + return true; + } + + // Walk interfaces + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == openGenericBase) + return true; + } + + return false; + } + + + + + /// + /// Resolves the text shown for an Error.Id failure code. Prefers + /// the runtime details published by the failing remote task; falls back to + /// the static error message. + /// + internal string GetTaskErrorMessage(ulong messageCode) + { + var task = GetTaskByMessageCode(messageCode); + var runtimeMessage = task?.RemoteExceptionDetails; + + if (string.IsNullOrWhiteSpace(runtimeMessage)) + { + runtimeMessage = task?.ErrorDetails?.LastValue; + } + + return string.IsNullOrWhiteSpace(runtimeMessage) + ? GetDefaultErrorMessage(messageCode) + : runtimeMessage; + } + + /// + /// Resolves the text shown for an Action.Id lifecycle code. Pure + /// static lookup — never reads runtime error details, so action and + /// error descriptions stay independent. + /// + internal string GetTaskActionMessage(ulong messageCode) + => GetDefaultActionMessage(messageCode); + + private AXOpen.Core.AxoRemoteTask? GetTaskByMessageCode(ulong messageCode) + { + return messageCode switch + { + 10000 or 10001 => TriggerTask, + 10010 or 10011 => InspectionResultTask, + 10020 or 10021 => SetRecipeTask, + 10030 or 10031 => SendSpecificDataTask, + 10040 or 10041 => ReceiveSpecificDataTask, + 10050 or 10051 => TriggerWithSpecificDataTask, + 10060 or 10061 => SendSpecificDataAndTypesTask, + _ => null, + }; + } + + /// + /// Returns the static text describing an Action.Id lifecycle code + /// (task invoked / finished, restore, etc.). Does NOT consult error details. + /// + internal static string GetDefaultActionMessage(ulong messageCode) + { + return messageCode switch + { + + TaskLifecycleCodes.TriggerInvoked => "TriggerTask invoked.", + TaskLifecycleCodes.TriggerFinished => "TriggerTask finished.", + TaskLifecycleCodes.InspectionResultInvoked => "InspectionResultTask invoked.", + TaskLifecycleCodes.InspectionResultFinished => "InspectionResultTask finished.", + TaskLifecycleCodes.SetRecipeInvoked => "SetRecipeTask invoked.", + TaskLifecycleCodes.SetRecipeFinished => "SetRecipeTask finished.", + TaskLifecycleCodes.SendSpecificDataInvoked => "SendSpecificDataTask invoked.", + TaskLifecycleCodes.SendSpecificDataFinished => "SendSpecificDataTask finished.", + TaskLifecycleCodes.ReceiveSpecificDataInvoked => "ReceiveSpecificDataTask invoked.", + TaskLifecycleCodes.ReceiveSpecificDataFinished => "ReceiveSpecificDataTask finished.", + TaskLifecycleCodes.TriggerWithSpecificDataInvoked => "TriggerWithSpecificDataTask invoked.", + TaskLifecycleCodes.TriggerWithSpecificDataFinished => "TriggerWithSpecificDataTask finished.", + TaskLifecycleCodes.SendSpecificDataAndTypesInvoked => "SendSpecificDataAndTypesTask invoked.", + TaskLifecycleCodes.SendSpecificDataAndTypesFinished => "SendSpecificDataAndTypesTask finished.", + _ => " ", + }; + } + + /// + /// Returns the static text describing an Error.Id failure code. + /// + internal static string GetDefaultErrorMessage(ulong messageCode) + { + return messageCode switch + { + 10000 => "TriggerTask finished with error!", + 10001 => "TriggerTask was aborted, while not yet completed!", + 10010 => "InspectionResultTask finished with error!", + 10011 => "InspectionResultTask was aborted, while not yet completed!", + 10020 => "SetRecipeTask finished with error!", + 10021 => "SetRecipeTask was aborted, while not yet completed!", + 10030 => "SendSpecificDataTask finished with error!", + 10031 => "SendSpecificDataTask was aborted, while not yet completed!", + 10040 => "ReceiveSpecificDataTask finished with error!", + 10041 => "ReceiveSpecificDataTask was aborted, while not yet completed!", + 10050 => "TriggerWithSpecificDataTask finished with error!", + 10051 => "TriggerWithSpecificDataTask was aborted, while not yet completed!", + 10060 => "SendSpecificDataAndTypesTask finished with error!", + 10061 => "SendSpecificDataAndTypesTask was aborted, while not yet completed!", + _ => " ", + }; + } + + /// + /// Backwards-compatible alias kept for callers that still ask for a generic + /// task-message lookup. Prefer or + /// . + /// + internal static string GetDefaultTaskMessage(ulong messageCode) + { + var action = GetDefaultActionMessage(messageCode); + if (!string.IsNullOrWhiteSpace(action) && action != " ") + return action; + return GetDefaultErrorMessage(messageCode); + } + + + } + + + +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNetAttribute.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNetAttribute.cs new file mode 100644 index 000000000..5bc0aafd1 --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNetAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AXOpen.Components.Cognex.Vision +{ + + [System.AttributeUsage(System.AttributeTargets.Property)] + public class AxoVisionProNetAttribute : Attribute + { + } +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNetSpecificDataContainer.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNetSpecificDataContainer.cs new file mode 100644 index 000000000..18ef9565b --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNetSpecificDataContainer.cs @@ -0,0 +1,93 @@ +using AXOpen.Messaging.Static; +using AXSharp.Connector; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace AXOpen.Components.Cognex.Vision +{ + public partial class AxoVisionProNetSpecificDataContainer where TOnline : AxoVisionProNetSpecificData + where TPlain : Pocos.AXOpen.Components.Cognex.Vision.AxoVisionProNetSpecificData//, new() + { + partial void PostConstruct(ITwinObject parent, string readableTail, string symbolTail) + { + + } + + + private TOnline _onlineData; + public TOnline OnlineData + { + get + { + if (_onlineData == null) _onlineData = (TOnline)GetDataSetProperty(); + + return _onlineData; + } + } + + public async Task GetPlainDataAsync(eAccessPriority priority = eAccessPriority.Normal) + { + var onlineData = OnlineData; + return await onlineData.OnlineToPlain(priority); + } + + public async Task<(TOnline Online, TPlain Plain)> GetDataAsync(eAccessPriority priority = eAccessPriority.Normal) + { + var onlineData = OnlineData; + var plainData = await onlineData.OnlineToPlain(priority); + return (onlineData, plainData); + } + + public async Task PlainToOnlineAsync(TPlain plain, eAccessPriority priority = eAccessPriority.Normal) + { + var onlineData = OnlineData; + await onlineData.PlainToOnline(plain, priority); + } + + private AxoVisionProNetSpecificData? GetDataSetProperty() where TA : Attribute + { + var dataObjectPropertyInfo = GetDataSetPropertyInfo(); + var dataObject = dataObjectPropertyInfo?.GetValue(this) as AxoVisionProNetSpecificData; + if (dataObject == null) + throw new Exception( + $"Data member annotated with '{nameof(TA)}' in '{Symbol}' does not inherit from '{nameof(AxoVisionProNetSpecificData)}'"); + + return dataObject; + } + + private PropertyInfo? GetDataSetPropertyInfo() where TA : Attribute + { + var properties = GetType().GetProperties(); + PropertyInfo? DataPropertyInfo = null; + + // iterate properties and look for AxoDataEntityAttribute + foreach (var prop in properties) + { + var attr = prop.GetCustomAttribute(); + if (attr != null) + { + //if already set, that means multiple data attributes are present, we want to throw error + if (DataPropertyInfo != null) + throw new Exception( + $"{GetType()} contains multiple {nameof(TA)}s! Make sure it contains only one."); + DataPropertyInfo = prop; + break; + } + } + + if (DataPropertyInfo == null) + throw new Exception($"There is no member annotated with '{nameof(AxoVisionProNetAttribute)}' in '{Symbol}'."); + + return DataPropertyInfo; + } + + + + } + + +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/EnvelopeMessages.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/EnvelopeMessages.cs new file mode 100644 index 000000000..d7fcbf5a4 --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/EnvelopeMessages.cs @@ -0,0 +1,196 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace AXOpen.Components.Cognex.Vision.VisionProtocol; + +// ────────────────────────────────────────────────────────────── +// TriggerRequest (Gateway → Vision) +// ────────────────────────────────────────────────────────────── + +/// +/// Payload attached to a TriggerRequest message. +/// Extend with fields that match the PlcToPc data contract. +/// +public sealed class TriggerRequestPayload +{ + [JsonPropertyName("triggerid")] + /// Trigger identification forwarded from PLC. + public short TriggerId { get; set; } + + /// Part identification forwarded from PLC. + [JsonPropertyName("partId")] + public string? PartId { get; set; } + + /// Variant / recipe selector. + [JsonPropertyName("variant")] + public string? Variant { get; set; } + + /// Serialized specific data from the PLC twin. + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } +} + +// ────────────────────────────────────────────────────────────── +// TriggerAccepted / TriggerRejected (Vision → Gateway) +// ────────────────────────────────────────────────────────────── + +/// +/// Payload attached to a TriggerAccepted message. +/// +public sealed class TriggerAcceptedPayload +{ + [JsonPropertyName("accepted")] + public bool Accepted { get; set; } + + + [JsonPropertyName("triggerId")] + public short TriggerId { get; set; } + + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } + +} + +/// +/// Payload attached to a TriggerRejected message. +/// +public sealed class TriggerRejectedPayload +{ + [JsonPropertyName("triggerId")] + public short TriggerId { get; set; } + + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("errorCode")] + public short ErrorCode { get; set; } +} + +// ────────────────────────────────────────────────────────────── +// Result returned to the RemoteTask handler +// ────────────────────────────────────────────────────────────── + +/// +/// Result of a call. +/// +public sealed class TriggerResult +{ + public bool Accepted { get; init; } + public short TriggerId { get; init; } + public short ErrorCode { get; init; } + public string? RejectReason { get; init; } + public JsonElement? Data { get; init; } + + public static TriggerResult Ok(short triggerId = 0, JsonElement? data = null) => + new() { Accepted = true, TriggerId = triggerId, Data = data }; + + public static TriggerResult Fail(string reason, short code = -1, short triggerId = 0) => + new() { Accepted = false, RejectReason = reason, ErrorCode = code, TriggerId = triggerId }; +} + +// ────────────────────────────────────────────────────────────── +// InspectionResult (Gateway → Vision) +// ────────────────────────────────────────────────────────────── + +public sealed class InspectionResultRequestPayload +{ + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } +} + +public sealed class InspectionResultCompletedPayload +{ + [JsonPropertyName("triggerId")] + public short TriggerId { get; set; } + + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } +} + +public sealed class InspectionFaultPayload +{ + [JsonPropertyName("triggerId")] + public short TriggerId { get; set; } + + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("errorCode")] + public short ErrorCode { get; set; } +} + +public sealed class VisionRequestResult +{ + public bool Success { get; init; } + public short ErrorCode { get; init; } + public string? Reason { get; init; } + public JsonElement? Data { get; init; } + + public static VisionRequestResult Ok(JsonElement? data = null) => + new() { Success = true, Data = data }; + + public static VisionRequestResult Fail(string reason, short code = -1) => + new() { Success = false, Reason = reason, ErrorCode = code }; +} + +// ────────────────────────────────────────────────────────────── +// SendSpecificData (Gateway → Vision) +// ────────────────────────────────────────────────────────────── + +public sealed class SendSpecificDataRequestPayload +{ + + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } +} + +public sealed class SendSpecificDataCompletedPayload +{ + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +// ────────────────────────────────────────────────────────────── +// ReceiveSpecificData (Gateway → Vision) +// ────────────────────────────────────────────────────────────── + +public sealed class ReceiveSpecificDataRequestPayload +{ + + + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } +} + +public sealed class ReceiveSpecificDataCompletedPayload +{ + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } +} + +// ────────────────────────────────────────────────────────────── +// SetRecipe (Gateway → Vision) +// ────────────────────────────────────────────────────────────── + +public sealed class SetRecipeRequestPayload +{ + /// Variant / recipe selector. + [JsonPropertyName("variant")] + public string? Variant { get; set; } + + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } +} + +public sealed class SetRecipeCompletedPayload +{ + [JsonPropertyName("success")] + public bool Success { get; set; } +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionEnvelope.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionEnvelope.cs new file mode 100644 index 000000000..85dcb37d0 --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionEnvelope.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AXOpen.Components.Cognex.Vision.VisionProtocol; + +/// +/// Common JSON envelope for every runtime message exchanged between +/// the PLC PC Gateway (client) and the Vision PC (server). +/// +public sealed class VisionEnvelope +{ + [JsonPropertyName("protocolVersion")] + public int ProtocolVersion { get; set; } = 1; + + [JsonPropertyName("schemaVersion")] + public string SchemaVersion { get; set; } = "1.0.0"; + + [JsonPropertyName("messageType")] + public string MessageType { get; set; } = string.Empty; + + [JsonPropertyName("messageId")] + public string MessageId { get; set; } = Guid.NewGuid().ToString(); + + [JsonPropertyName("correlationId")] + public string? CorrelationId { get; set; } + + /// + /// Full twin symbol of the AxoVisionProNet component instance (e.g. "Ctx.VisionStation1.Inspection1"). + /// Used by the Vision PC server to route the message to the correct handler. + /// + [JsonPropertyName("componentSymbol")] + public string ComponentSymbol { get; set; } = string.Empty; + + [JsonPropertyName("sequenceNumber")] + public long SequenceNumber { get; set; } + + [JsonPropertyName("timestampUtc")] + public DateTime TimestampUtc { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("ackRequired")] + public bool AckRequired { get; set; } + + /// + /// Raw JSON payload — deserialized on demand into a typed payload class. + /// + [JsonPropertyName("payload")] + public JsonElement? Payload { get; set; } + + /// + /// Deserializes the payload into a strongly-typed object. + /// + public T? GetPayload() => + Payload.HasValue + ? Payload.Value.Deserialize(VisionJsonOptions.Default) + : default; + + /// + /// Well-known message type name constants. + /// + public static class MessageTypes + { + public const string TriggerRequest = "TriggerRequest"; + public const string TriggerAccepted = "TriggerAccepted"; + public const string TriggerRejected = "TriggerRejected"; + public const string InspectionResultRequest= "InspectionResultRequest"; + public const string InspectionCompleted = "InspectionCompleted"; + public const string InspectionFault = "InspectionFault"; + public const string SendSpecificDataRequest = "SendSpecificDataRequest"; + public const string SendSpecificDataCompleted = "SendSpecificDataCompleted"; + public const string SendSpecificDataTypesRequest = "SendSpecificDataTypesRequest"; + public const string SendSpecificDataTypesCompleted = "SendSpecificDataTypesCompleted"; + public const string ReceiveSpecificDataRequest = "ReceiveSpecificDataRequest"; + public const string ReceiveSpecificDataCompleted= "ReceiveSpecificDataCompleted"; + public const string SetRecipeRequest = "SetRecipeRequest"; + public const string SetRecipeCompleted = "SetRecipeCompleted"; + public const string TriggerWithSpecificDataRequest = "TriggerWithSpecificDataRequest"; + public const string TriggerWithSpecificDataCompleted = "TriggerWithSpecificDataCompleted"; + + } +} + +/// +/// Shared JSON serializer options used across all protocol serialization. +/// +public static class VisionJsonOptions +{ + public static readonly JsonSerializerOptions Default = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionTcpClient.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionTcpClient.cs new file mode 100644 index 000000000..684215c72 --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionTcpClient.cs @@ -0,0 +1,831 @@ +using System.Collections.Concurrent; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Threading.Channels; + +namespace AXOpen.Components.Cognex.Vision.VisionProtocol; + +/// +/// Determines when the TCP socket is opened and closed. +/// +public enum VisionConnectionMode +{ + /// + /// Keep one socket open and reuse it for all requests. + /// + Persistent, + + /// + /// Open a socket for each request and close it after response. + /// + PerRequest +} + +// ────────────────────────────────────────────────────────────── +// Connection options +// ────────────────────────────────────────────────────────────── + +/// +/// Configuration for a instance. +/// +public sealed class VisionTcpClientOptions +{ + /// Hostname or IP address of the Vision PC TCP server. + public required string Host { get; init; } + + /// TCP port the Vision PC server listens on. + public int Port { get; init; } = 8500; + + /// + /// Full twin symbol of the AxoVisionProNet component instance. + /// Set automatically from AxoVisionProNet.Symbol when calling + /// . + /// + public required string ComponentSymbol { get; init; } + + /// + /// Selects if the client stays connected all the time or reconnects per request. + /// + public VisionConnectionMode ConnectionMode { get; init; } = VisionConnectionMode.Persistent; + + /// Maximum time allowed for establishing the TCP connection. + public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(5); + + /// How long to wait for a TriggerAccepted / TriggerRejected after sending TriggerRequest. + public TimeSpan TriggerAcceptTimeout { get; init; } + + /// How long to wait for InspectionCompleted after sending InspectionResultRequest. + public TimeSpan InspectionResultTimeout { get; init; } + + /// How long to wait for SendSpecificDataCompleted after sending SendSpecificDataRequest. + public TimeSpan SendSpecificDataTimeout { get; init; } + + /// How long to wait for ReceiveSpecificDataCompleted after sending ReceiveSpecificDataRequest. + public TimeSpan ReceiveSpecificDataTimeout { get; init; } + + /// How long to wait for SetRecipeCompleted after sending SetRecipeRequest. + public TimeSpan SetRecipeTimeout { get; init; } + + /// How long to wait for a TriggerWithSpecificData response after sending TriggerWithSpecificDataRequest. + public TimeSpan TriggerWithSpecificDataAcceptTimeout { get; init; } + + /// How long to attempt reconnecting before giving up one cycle. + public TimeSpan ReconnectDelay { get; init; } = TimeSpan.FromSeconds(2); + + /// + /// Creates a new instance. The provided + /// value is applied to every per-task timeout. + /// Defaults to 5000 ms. + /// + /// Timeout (ms) applied to all per-task response waits. + public VisionTcpClientOptions(int taskTimeoutMs = 5000) + { + var timeout = TimeSpan.FromMilliseconds(taskTimeoutMs); + + TriggerAcceptTimeout = timeout; + InspectionResultTimeout = timeout; + SendSpecificDataTimeout = timeout; + ReceiveSpecificDataTimeout = timeout; + SetRecipeTimeout = timeout; + TriggerWithSpecificDataAcceptTimeout = timeout; + } +} + +// ────────────────────────────────────────────────────────────── +// TCP client +// ────────────────────────────────────────────────────────────── + +/// +/// Persistent TCP client that handles the PLC ↔ Vision PC runtime channel. +/// +/// Transport: newline-delimited JSON (one JSON object per line, UTF-8). +/// Framing is simple, human-readable, and easy to capture with Wireshark or netcat. +/// +/// +/// Concurrency: a single background receive loop dispatches responses using +/// per-request channels keyed by the outgoing MessageId. +/// The Vision PC echoes that ID back in CorrelationId. +/// +/// +public sealed class VisionTcpClient : IAsyncDisposable +{ + private readonly VisionTcpClientOptions _options; + + private TcpClient? _tcp; + private StreamWriter? _writer; + private StreamReader? _reader; + private CancellationTokenSource? _cts; + private Task? _receiveLoop; + + private long _sequenceNumber; + + // Pending awaits: key = MessageId we sent, value = channel with one or more correlated responses. + private readonly ConcurrentDictionary> _pending = new(); + private string? _lastInboundSummary; + + public bool IsConnected => _tcp?.Connected ?? false; + + public VisionTcpClient(VisionTcpClientOptions options) + { + _options = options; + } + + // ────────────────────────────────────────────────────────── + // Connect / disconnect + // ────────────────────────────────────────────────────────── + + /// + /// Opens the TCP connection to Vision PC and starts the receive loop. + /// Safe to call repeatedly; it will only connect when disconnected. + /// + public async Task ConnectAsync(CancellationToken ct = default) + { + if (IsConnected) + return; + + await DisconnectInternalAsync(); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + + _tcp = new TcpClient(); + using var connectTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + if (_options.ConnectTimeout > TimeSpan.Zero) + connectTimeoutCts.CancelAfter(_options.ConnectTimeout); + + try + { + await _tcp.ConnectAsync(_options.Host, _options.Port, connectTimeoutCts.Token); + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + throw new TimeoutException( + $"TCP connect timed out after {_options.ConnectTimeout.TotalMilliseconds:0} ms to {_options.Host}:{_options.Port}."); + } + + var stream = _tcp.GetStream(); + _writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true, NewLine = "\n" }; + _reader = new StreamReader(stream, Encoding.UTF8); + + _receiveLoop = Task.Run(() => ReceiveLoopAsync(_cts.Token), CancellationToken.None); + } + + // ────────────────────────────────────────────────────────── + // Trigger flow (step 1) + // ────────────────────────────────────────────────────────── + + /// + /// Sends a TriggerRequest and awaits one of the supported trigger responses + /// from Vision PC. + /// + /// Data to forward from PLC. + /// Cancellation token from the RemoteTask handler. + /// + /// is true when Vision accepted the trigger. + /// + public async Task TriggerAsync( + TriggerRequestPayload payload, + CancellationToken ct = default) + { + bool disconnectAfterRequest = _options.ConnectionMode == VisionConnectionMode.PerRequest; + + await ConnectAsync(ct); + + VisionEnvelope envelope = BuildEnvelope( + VisionEnvelope.MessageTypes.TriggerRequest, + payload, + ackRequired: true); + + // Register a response queue keyed by our own MessageId. + // Vision echoes MessageId back as CorrelationId in its responses. + Channel responseChannel = RegisterPending(envelope.MessageId); + + try + { + await SendAsync(envelope, ct); + + VisionEnvelope response = await ReadPendingAsync( + responseChannel.Reader, + _options.TriggerAcceptTimeout, + ct); + + return response.MessageType switch + { + VisionEnvelope.MessageTypes.TriggerAccepted => + await AwaitTriggerCompletionAfterAcceptedAsync( + acceptedResponse: response, + responseReader: responseChannel.Reader, + ct), + + VisionEnvelope.MessageTypes.TriggerRejected => + ToTriggerRejectedResult(response), + + // Some Vision implementations send the inspection result directly + // as a completion of the trigger request. + VisionEnvelope.MessageTypes.InspectionCompleted => + ToTriggerResultFromInspectionCompleted(response), + + VisionEnvelope.MessageTypes.InspectionFault => + ToTriggerFaultResult(response), + + _ => TriggerResult.Fail( + $"Unexpected message type: {response.MessageType}") + }; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + // Timeout path (inner token fired, outer token is still valid) + return TriggerResult.Fail( + BuildTimeoutReason("TriggerAccepted/TriggerRejected/InspectionCompleted/InspectionFault", envelope.MessageId), + -2); + } + finally + { + _pending.TryRemove(envelope.MessageId, out _); + + if (disconnectAfterRequest) + await DisconnectInternalAsync(); + } + } + + // ────────────────────────────────────────────────────────── + // TriggerWithSpecificData flow + // ────────────────────────────────────────────────────────── + + /// + /// Sends a TriggerWithSpecificDataRequest and awaits one of the supported + /// trigger responses from Vision PC. Mirrors but uses + /// the dedicated TriggerWithSpecificData message type so the Vision PC can + /// distinguish a plain trigger from a trigger that carries specific payload data. + /// + /// Data to forward from PLC. + /// Cancellation token from the RemoteTask handler. + /// + /// is true when Vision accepted the trigger. + /// + public async Task TriggerWithSpecificDataAsync( + TriggerRequestPayload payload, + CancellationToken ct = default) + { + bool disconnectAfterRequest = _options.ConnectionMode == VisionConnectionMode.PerRequest; + + await ConnectAsync(ct); + + VisionEnvelope envelope = BuildEnvelope( + VisionEnvelope.MessageTypes.TriggerWithSpecificDataRequest, + payload, + ackRequired: true); + + // Register a response queue keyed by our own MessageId. + // Vision echoes MessageId back as CorrelationId in its responses. + Channel responseChannel = RegisterPending(envelope.MessageId); + + try + { + await SendAsync(envelope, ct); + + VisionEnvelope response = await ReadPendingAsync( + responseChannel.Reader, + _options.TriggerWithSpecificDataAcceptTimeout, + ct); + + return response.MessageType switch + { + VisionEnvelope.MessageTypes.TriggerAccepted => + await AwaitTriggerCompletionAfterAcceptedAsync( + acceptedResponse: response, + responseReader: responseChannel.Reader, + ct), + + VisionEnvelope.MessageTypes.TriggerRejected => + ToTriggerRejectedResult(response), + + // Vision may complete the trigger directly with the dedicated + // TriggerWithSpecificDataCompleted message instead of going through + // the InspectionCompleted path. + VisionEnvelope.MessageTypes.TriggerWithSpecificDataCompleted => + ToTriggerResultFromInspectionCompleted(response), + + // Some Vision implementations send the inspection result directly + // as a completion of the trigger request. + VisionEnvelope.MessageTypes.InspectionCompleted => + ToTriggerResultFromInspectionCompleted(response), + + VisionEnvelope.MessageTypes.InspectionFault => + ToTriggerFaultResult(response), + + _ => TriggerResult.Fail( + $"Unexpected message type: {response.MessageType}") + }; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + // Timeout path (inner token fired, outer token is still valid) + return TriggerResult.Fail( + BuildTimeoutReason("TriggerAccepted/TriggerRejected/TriggerWithSpecificDataCompleted/InspectionCompleted/InspectionFault", envelope.MessageId), + -2); + } + finally + { + _pending.TryRemove(envelope.MessageId, out _); + + if (disconnectAfterRequest) + await DisconnectInternalAsync(); + } + } + + // ────────────────────────────────────────────────────────── + // InspectionResult flow + // ────────────────────────────────────────────────────────── + + public async Task InspectionResultAsync( + InspectionResultRequestPayload payload, + CancellationToken ct = default) + { + bool disconnectAfterRequest = _options.ConnectionMode == VisionConnectionMode.PerRequest; + + await ConnectAsync(ct); + + VisionEnvelope envelope = BuildEnvelope( + VisionEnvelope.MessageTypes.InspectionResultRequest, + payload, + ackRequired: true); + + Channel responseChannel = RegisterPending(envelope.MessageId); + + try + { + await SendAsync(envelope, ct); + + VisionEnvelope response = await ReadPendingAsync( + responseChannel.Reader, + _options.InspectionResultTimeout, + ct); + + return response.MessageType switch + { + VisionEnvelope.MessageTypes.InspectionCompleted => + ToVisionRequestResult(response), + + _ => VisionRequestResult.Fail( + $"Unexpected message type: {response.MessageType}") + }; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + return VisionRequestResult.Fail( + BuildTimeoutReason("InspectionCompleted", envelope.MessageId), + -2); + } + finally + { + _pending.TryRemove(envelope.MessageId, out _); + + if (disconnectAfterRequest) + await DisconnectInternalAsync(); + } + } + + // ────────────────────────────────────────────────────────── + // SendSpecificData flow + // ────────────────────────────────────────────────────────── + + public async Task SendSpecificDataAsync( + SendSpecificDataRequestPayload payload, + CancellationToken ct = default) + { + return await SendSpecificDataCoreAsync( + payload, + VisionEnvelope.MessageTypes.SendSpecificDataRequest, + VisionEnvelope.MessageTypes.SendSpecificDataCompleted, + ct); + } + + public async Task SendSpecificDataTypesAsync( + SendSpecificDataRequestPayload payload, + CancellationToken ct = default) + { + return await SendSpecificDataCoreAsync( + payload, + VisionEnvelope.MessageTypes.SendSpecificDataTypesRequest, + VisionEnvelope.MessageTypes.SendSpecificDataTypesCompleted, + ct); + } + + private async Task SendSpecificDataCoreAsync( + SendSpecificDataRequestPayload payload, + string requestMessageType, + string completedMessageType, + CancellationToken ct = default) + { + bool disconnectAfterRequest = _options.ConnectionMode == VisionConnectionMode.PerRequest; + + await ConnectAsync(ct); + + VisionEnvelope envelope = BuildEnvelope( + requestMessageType, + payload, + ackRequired: true); + + Channel responseChannel = RegisterPending(envelope.MessageId); + + try + { + await SendAsync(envelope, ct); + + VisionEnvelope response = await ReadPendingAsync( + responseChannel.Reader, + _options.SendSpecificDataTimeout, + ct); + + return response.MessageType switch + { + _ when response.MessageType == completedMessageType => + ToVisionRequestResult(response), + + _ => VisionRequestResult.Fail( + $"Unexpected message type: {response.MessageType}") + }; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + return VisionRequestResult.Fail( + BuildTimeoutReason(completedMessageType, envelope.MessageId), + -2); + } + finally + { + _pending.TryRemove(envelope.MessageId, out _); + + if (disconnectAfterRequest) + await DisconnectInternalAsync(); + } + } + + // ────────────────────────────────────────────────────────── + // ReceiveSpecificData flow + // ────────────────────────────────────────────────────────── + + public async Task ReceiveSpecificDataAsync( + ReceiveSpecificDataRequestPayload payload, + CancellationToken ct = default) + { + bool disconnectAfterRequest = _options.ConnectionMode == VisionConnectionMode.PerRequest; + + await ConnectAsync(ct); + + VisionEnvelope envelope = BuildEnvelope( + VisionEnvelope.MessageTypes.ReceiveSpecificDataRequest, + payload, + ackRequired: true); + + Channel responseChannel = RegisterPending(envelope.MessageId); + + try + { + await SendAsync(envelope, ct); + + VisionEnvelope response = await ReadPendingAsync( + responseChannel.Reader, + _options.ReceiveSpecificDataTimeout, + ct); + + return response.MessageType switch + { + VisionEnvelope.MessageTypes.ReceiveSpecificDataCompleted => + ToVisionRequestResult(response), + + _ => VisionRequestResult.Fail( + $"Unexpected message type: {response.MessageType}") + }; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + return VisionRequestResult.Fail( + BuildTimeoutReason("ReceiveSpecificDataCompleted", envelope.MessageId), + -2); + } + finally + { + _pending.TryRemove(envelope.MessageId, out _); + + if (disconnectAfterRequest) + await DisconnectInternalAsync(); + } + } + + // ────────────────────────────────────────────────────────── + // SetRecipe flow + // ────────────────────────────────────────────────────────── + + public async Task SetRecipeAsync( + SetRecipeRequestPayload payload, + CancellationToken ct = default) + { + bool disconnectAfterRequest = _options.ConnectionMode == VisionConnectionMode.PerRequest; + + await ConnectAsync(ct); + + VisionEnvelope envelope = BuildEnvelope( + VisionEnvelope.MessageTypes.SetRecipeRequest, + payload, + ackRequired: true); + + Channel responseChannel = RegisterPending(envelope.MessageId); + + try + { + await SendAsync(envelope, ct); + + VisionEnvelope response = await ReadPendingAsync( + responseChannel.Reader, + _options.SetRecipeTimeout, + ct); + + return response.MessageType switch + { + VisionEnvelope.MessageTypes.SetRecipeCompleted => + ToVisionRequestResult(response), + + _ => VisionRequestResult.Fail( + $"Unexpected message type: {response.MessageType}") + }; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + return VisionRequestResult.Fail( + BuildTimeoutReason("SetRecipeCompleted", envelope.MessageId), + -2); + } + finally + { + _pending.TryRemove(envelope.MessageId, out _); + + if (disconnectAfterRequest) + await DisconnectInternalAsync(); + } + } + + // ────────────────────────────────────────────────────────── + // Internal helpers + // ────────────────────────────────────────────────────────── + + private Channel RegisterPending(string messageId) + { + var channel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false + }); + + _pending[messageId] = channel; + return channel; + } + + private static async Task ReadPendingAsync( + ChannelReader responseReader, + TimeSpan timeout, + CancellationToken ct) + { + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeoutCts.CancelAfter(timeout); + return await responseReader.ReadAsync(timeoutCts.Token); + } + + private async Task AwaitTriggerCompletionAfterAcceptedAsync( + VisionEnvelope acceptedResponse, + ChannelReader responseReader, + CancellationToken ct) + { + TriggerResult accepted = ToTriggerResult(acceptedResponse); + if (!accepted.Accepted) + return accepted; + + // If inspection completion follows TriggerAccepted on the same correlation, + // consume it here so one TriggerAsync call handles both messages. + try + { + VisionEnvelope followUp = await ReadPendingAsync( + responseReader, + _options.InspectionResultTimeout, + ct); + + return followUp.MessageType switch + { + VisionEnvelope.MessageTypes.InspectionCompleted => + ToTriggerResultFromInspectionCompleted(followUp), + + VisionEnvelope.MessageTypes.InspectionFault => + ToTriggerFaultResult(followUp), + + // Duplicate or out-of-order ack. Keep the accepted result. + VisionEnvelope.MessageTypes.TriggerAccepted => accepted, + + VisionEnvelope.MessageTypes.TriggerRejected => + ToTriggerRejectedResult(followUp), + + _ => TriggerResult.Fail( + $"Unexpected follow-up message type: {followUp.MessageType}") + }; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + // Keep backward compatibility: accepted trigger is still considered success + // when no follow-up frame arrives within completion timeout. + return accepted; + } + } + + private VisionEnvelope BuildEnvelope(string messageType, T payload, bool ackRequired = false) + { + return new VisionEnvelope + { + MessageType = messageType, + MessageId = Guid.NewGuid().ToString(), + ComponentSymbol = _options.ComponentSymbol, + SequenceNumber = Interlocked.Increment(ref _sequenceNumber), + TimestampUtc = DateTime.UtcNow, + AckRequired = ackRequired, + Payload = JsonSerializer.SerializeToElement(payload, VisionJsonOptions.Default) + }; + } + + private async Task SendAsync(VisionEnvelope envelope, CancellationToken ct) + { + if (_writer is null) + throw new InvalidOperationException("Not connected — call ConnectAsync first."); + + string json = JsonSerializer.Serialize(envelope, VisionJsonOptions.Default); + await _writer.WriteLineAsync(json.AsMemory(), ct); + } + + /// + /// Background loop — reads newline-delimited frames and dispatches them. + /// + private async Task ReceiveLoopAsync(CancellationToken ct) + { + if (_reader is null) return; + + try + { + while (!ct.IsCancellationRequested) + { + string? line = await _reader.ReadLineAsync(ct); + + if (line is null) + break; // Server closed the connection. + + if (string.IsNullOrWhiteSpace(line)) + continue; + + VisionEnvelope? envelope; + try + { + envelope = JsonSerializer.Deserialize( + line, VisionJsonOptions.Default); + } + catch (JsonException) + { + // Malformed frame — skip, do not crash the loop. + continue; + } + + if (envelope is not null) + { + _lastInboundSummary = + $"type={envelope.MessageType}, corr={envelope.CorrelationId ?? ""}, msg={envelope.MessageId}"; + Dispatch(envelope); + } + } + } + catch (OperationCanceledException) { /* intentional shutdown */ } + catch (IOException) { /* connection dropped */ } + } + + /// + /// Routes an inbound envelope to the matching pending awaiter using + /// CorrelationIdMessageId lookup. + /// + private void Dispatch(VisionEnvelope envelope) + { + // Primary path: Vision echoes request MessageId as CorrelationId. + if (!string.IsNullOrWhiteSpace(envelope.CorrelationId) && + _pending.TryGetValue(envelope.CorrelationId, out var responseChannel)) + { + responseChannel.Writer.TryWrite(envelope); + return; + } + + // Compatibility fallback: some servers mirror the request id in MessageId + // and omit CorrelationId entirely. + if (!string.IsNullOrWhiteSpace(envelope.MessageId) && + _pending.TryGetValue(envelope.MessageId, out responseChannel)) + { + responseChannel.Writer.TryWrite(envelope); + return; + } + + // Last-resort compatibility: if there is exactly one pending request, + // complete it with the inbound response even if IDs are not correlated. + // This helps interop with simple servers that omit correlation metadata. + if (_pending.Count == 1) + { + foreach (var pending in _pending) + { + pending.Value.Writer.TryWrite(envelope); + return; + } + } + + // Unmatched responses are ignored by design (unsolicited messages/events). + // Future: route unsolicited messages (InspectionFault, etc.) to an event. + } + + private string BuildTimeoutReason(string expectedResponseType, string requestMessageId) + { + return $"{expectedResponseType} not received within timeout. requestMessageId={requestMessageId}, pending={_pending.Count}, lastInbound={_lastInboundSummary ?? ""}"; + } + + // ────────────────────────────────────────────────────────── + // Response converters + // ────────────────────────────────────────────────────────── + + private static TriggerResult ToTriggerResult(VisionEnvelope envelope) + { + var payload = envelope.GetPayload(); + return payload?.Accepted == true + ? TriggerResult.Ok(payload?.TriggerId ?? 0) + : TriggerResult.Fail("Vision responded Accepted=false"); + } + + private static TriggerResult ToTriggerRejectedResult(VisionEnvelope envelope) + { + var payload = envelope.GetPayload(); + return TriggerResult.Fail( + payload?.Reason ?? "TriggerRejected", + payload?.ErrorCode ?? -1, + payload?.TriggerId ?? 0); + } + + private static TriggerResult ToTriggerResultFromInspectionCompleted(VisionEnvelope envelope) + { + var payload = envelope.GetPayload(); + + // Treat completion without explicit success flag as successful completion. + if (payload.Success) + return TriggerResult.Ok(payload.TriggerId,payload.Data); + + return TriggerResult.Fail("InspectionCompleted with Success=false"); + } + + private static TriggerResult ToTriggerFaultResult(VisionEnvelope envelope) + { + var payload = envelope.GetPayload(); + return TriggerResult.Fail( + payload?.Reason ?? "InspectionFault", + payload?.ErrorCode ?? -1, + payload?.TriggerId ?? 0); + } + + private static VisionRequestResult ToVisionRequestResult(VisionEnvelope envelope) + { + var payload = envelope.GetPayload(); + return payload?.Success == true + ? VisionRequestResult.Ok(payload.Data) + : VisionRequestResult.Fail("Vision responded Success=false"); + } + + // ────────────────────────────────────────────────────────── + // Dispose + // ────────────────────────────────────────────────────────── + + public async ValueTask DisposeAsync() + { + await DisconnectInternalAsync(); + } + + private async Task DisconnectInternalAsync() + { + if (_cts is not null) + { + await _cts.CancelAsync(); + _cts.Dispose(); + } + + if (_receiveLoop is not null) + { + try { await _receiveLoop; } + catch { /* already cancelled */ } + } + + _writer?.Dispose(); + _reader?.Dispose(); + _tcp?.Dispose(); + + _writer = null; + _reader = null; + _tcp = null; + _cts = null; + _receiveLoop = null; + } +} diff --git a/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionTypedPayloadSerializer.cs b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionTypedPayloadSerializer.cs new file mode 100644 index 000000000..00137774b --- /dev/null +++ b/src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/VisionProtocol/VisionTypedPayloadSerializer.cs @@ -0,0 +1,138 @@ +using System.Collections; +using System.Reflection; +using System.Text.Json; + +namespace AXOpen.Components.Cognex.Vision.VisionProtocol; + +internal static class VisionTypedPayloadSerializer +{ + public static JsonElement SerializeToElement(object value) => + JsonSerializer.SerializeToElement(ToTypedNode(value, value.GetType()), VisionJsonOptions.Default); + + private static object? ToTypedNode(object? value, Type declaredType) + { + Type effectiveType = Nullable.GetUnderlyingType(declaredType) ?? declaredType; + + if (value is null) + { + return new Dictionary + { + ["type"] = GetTypeName(effectiveType), + ["value"] = null + }; + } + + Type runtimeType = value.GetType(); + + if (IsScalar(runtimeType)) + { + return new Dictionary + { + ["type"] = GetTypeName(runtimeType), + ["value"] = value + }; + } + + if (value is IEnumerable enumerable && value is not string) + { + var items = new List(); + foreach (var item in enumerable) + { + items.Add(ToTypedNode(item, item?.GetType() ?? typeof(object))); + } + + return new Dictionary + { + ["type"] = GetTypeName(runtimeType), + ["items"] = items + }; + } + + var properties = runtimeType + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(property => property.CanRead && property.GetIndexParameters().Length == 0); + + var result = new Dictionary + { + ["type"] = GetTypeName(runtimeType) + }; + + foreach (var property in properties) + { + object? propertyValue = property.GetValue(value); + result[ToCamelCase(property.Name)] = ToTypedNode(propertyValue, property.PropertyType); + } + + return result; + } + + private static bool IsScalar(Type type) + { + Type effectiveType = Nullable.GetUnderlyingType(type) ?? type; + + return effectiveType.IsPrimitive + || effectiveType.IsEnum + || effectiveType == typeof(string) + || effectiveType == typeof(decimal) + || effectiveType == typeof(DateTime) + || effectiveType == typeof(DateTimeOffset) + || effectiveType == typeof(Guid) + || effectiveType == typeof(TimeSpan); + } + + private static string GetTypeName(Type type) + { + Type effectiveType = Nullable.GetUnderlyingType(type) ?? type; + + if (effectiveType.IsArray) + return $"array<{GetTypeName(effectiveType.GetElementType()!)}>"; + + if (effectiveType == typeof(bool)) + return "bool"; + if (effectiveType == typeof(byte)) + return "byte"; + if (effectiveType == typeof(short)) + return "int"; + if (effectiveType == typeof(ushort)) + return "uint"; + if (effectiveType == typeof(int)) + return "dint"; + if (effectiveType == typeof(uint)) + return "udint"; + if (effectiveType == typeof(long)) + return "lint"; + if (effectiveType == typeof(ulong)) + return "ulint"; + if (effectiveType == typeof(float)) + return "real"; + if (effectiveType == typeof(double)) + return "lreal"; + if (effectiveType == typeof(decimal)) + return "decimal"; + if (effectiveType == typeof(string)) + return "string"; + if (effectiveType == typeof(DateTime)) + return "datetime"; + if (effectiveType == typeof(DateTimeOffset)) + return "datetimeoffset"; + if (effectiveType == typeof(Guid)) + return "guid"; + if (effectiveType == typeof(TimeSpan)) + return "timespan"; + if (typeof(IEnumerable).IsAssignableFrom(effectiveType) && effectiveType != typeof(string)) + return "array"; + + return effectiveType.Name; + } + + private static string ToCamelCase(string value) + { + if (string.IsNullOrEmpty(value) || char.IsLower(value[0])) + return value; + + if (value.Length == 1) + return char.ToLowerInvariant(value[0]).ToString(); + + return char.ToLowerInvariant(value[0]) + value[1..]; + } +} \ No newline at end of file diff --git a/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json b/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json index 716d0466d..813ae6213 100644 --- a/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json +++ b/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json @@ -1,9 +1,9 @@ { - "PKIData": "AQAAAAAAAAAAAAAAAAAAAAEBAb4gAAACAAAAAAAAAAAAAwAAAN8ALS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUhvd0ZBWUhLb1pJemowQ0FRWUpLeVFEQXdJSUFRRUxBMklBQkdsYW1ZSHdRUzRpMjVlU2kvdHE1TzlIb0k1YgpQb1l3MFJiaDF2bTlVRFY4SHVVVXkrSiswM1c4RklrVmhhRG1IbjVMQ0Q5T01TZEs4VTNMQUp6OE1PcDhZMTAwCk91T3lCYnhmam5zT1RpNVZlMERCUlp6MWM1b0xsRjJkK3hhR2NBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCnsBAQACAAAnEAAgnsAjq+7cjygk1ZekVCSLVoaYwBE5U7VZFHO517+umjEBACAADAAQV3bwXMC9ZEyD26iryWx8WwEAAAE2JMWPXDSGhOCk43nnssaQua6rtSc1GAyveGPNqx1x6RkGSJBTDkSBa9yVkHesruve/lR6hGgnfN+fpT39LIPg7eiKZeZv+GsTwuRNSK/YZOSRigZhc1Ngtp8EcI6j/sjWzPmnErcmnypwXuZIAR4p4iERHkRtUdgGPeGNs19DxBKQcD2BTrcOE596UuiLGjDu7qvnv6I1SAnm7bqu4EQ1ABk7JglqbU7YjLE1Y/b47ebihx+J89thRQh5xInQUJuxd8kcB+pjVEsvgC99J4GaLqLpXJPlBwVnrVPRpNFkkfmAgVSxmZEt+I87aGnHTQg5hclrXdnpipuGS6lOOnq9aMF9OXf1QfplxrSGRaubcJEBSTrBbPOzEFWeO9X0fz1c/gBfGs8897uT8g4UzNPfMdlll/AdQQIACWxvY2FsaG9zdAEAAAAAAAAAAQAAAAIAAAAAAAAAFADfZfmK8KmL0EF8M7zqbCWJPcaRM7QGAQAGsC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQ0KTUlJRXN6Q0NBNXVnQXdJQkFnSVVDUGdQeTR6RFdUb2VBZVl1T0tWRzYrMUdtaTh3RFFZSktvWklodmNOQVFFTEJRQXdlekVMTUFrRw0KQTFVRUJoTUNXRmd4RWpBUUJnTlZCQWdNQ1ZOMFlYUmxUbUZ0WlRFUk1BOEdBMVVFQnd3SVEybDBlVTVoYldVeEZEQVNCZ05WQkFvTQ0KQzBOdmJYQmhibmxPWVcxbE1Sc3dHUVlEVlFRTERCSkRiMjF3WVc1NVUyVmpkR2x2Yms1aGJXVXhFakFRQmdOVkJBTU1DV3h2WTJGcw0KYUc5emREQWVGdzB5TmpBMU16QXhPVEF6TXpSYUZ3MHpOakExTWpjeE9UQXpNelJhTUhzeEN6QUpCZ05WQkFZVEFsaFlNUkl3RUFZRA0KVlFRSURBbFRkR0YwWlU1aGJXVXhFVEFQQmdOVkJBY01DRU5wZEhsT1lXMWxNUlF3RWdZRFZRUUtEQXREYjIxd1lXNTVUbUZ0WlRFYg0KTUJrR0ExVUVDd3dTUTI5dGNHRnVlVk5sWTNScGIyNU9ZVzFsTVJJd0VBWURWUVFEREFsc2IyTmhiR2h2YzNRd2dnRWlNQTBHQ1NxRw0KU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRG9IY2tOZzhtcUszcXdsei9velhUS2RUTkVvbC93YkMxTVJLNFc5N1Y4a1JENQ0KeW04S1ljOUs0dHBTQXh6ck1QcUkvaG9SMXJpTjhmRlhvNU1HeHNVeDdpRTZrRXNjZ1RhL1p4NWZDMWhzRFEzL3NMVnhtblJLb29SSA0KdktWbVZtS1g1UDE4NVBLTUUyaUtyWDNNY0xxUkJ1WS9EbE5weVgxQmlndFFISFZhaFJSSlhDWElZYjlOZ0ZSVnlOTzNnL29qTFdabg0KWVcyWGhGMS9xaGNzOFRZS0MzS2dXNlRyc2FLcS9YZ3dtRDRBN3JtMksvNllUYXdLTy9tVEd2YjJGcCtWcUVhY0UzZ3YxMFdGaTVycQ0KalNkcXBOZWFySUJtSVZCalRwdjVKVEs3cHlzTzRDNmJTTlFHb1lnWmZDblJJSEpmK2cxVUlZYkZ6R29iZ0JHMVpXS3pBZ01CQUFHag0KZ2dFdE1JSUJLVEFKQmdOVkhSTUVBakFBTUE0R0ExVWREd0VCL3dRRUF3SUM5REFkQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQVFZSQ0KS3dZQkJRVUhBd0l3RXdZRFZSMFJCQXd3Q29JQWh3VEFxR1FCaGdBd0hRWURWUjBPQkJZRUZBOTh4WGFXVEJNUDloL0FmZWxwT2R2Kw0KVXZYME1JRzRCZ05WSFNNRWdiQXdnYTJBRkE5OHhYYVdUQk1QOWgvQWZlbHBPZHYrVXZYMG9YK2tmVEI3TVFzd0NRWURWUVFHRXdKWQ0KV0RFU01CQUdBMVVFQ0F3SlUzUmhkR1ZPWVcxbE1SRXdEd1lEVlFRSERBaERhWFI1VG1GdFpURVVNQklHQTFVRUNnd0xRMjl0Y0dGdQ0KZVU1aGJXVXhHekFaQmdOVkJBc01Fa052YlhCaGJubFRaV04wYVc5dVRtRnRaVEVTTUJBR0ExVUVBd3dKYkc5allXeG9iM04wZ2hRSQ0KK0EvTGpNTlpPaDRCNWk0NHBVYnI3VWFhTHpBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQUZGd05BWGd2OGt1OW1jZmFOcE1ydWRPdg0KTHpleHB0QnpzUUlvQUd5K09raEQzbTVOekpvc2tUWFIyb3FUL1NJeGp1OGVyczg4NjNKQ2lRMVdwSnZnMUZoVVNXaUI4dDQvbFAxbQ0KcGdaOVBoQVJHY0c5czlNSUVqaUNjbWxiOFpoMDNHNGVZN09ydmNZZ1ZPWjR5MGx2dThrNzFyN1pzVTB2eXBzaHd5NEg3MisyQ1A1dg0KcVZlUEdiQ3pVcEowT2JYVHd0bXpxMHVIVE9GVFl1N3A0bnhMVzhPcklaaDBGMFk1R0hTcEJ3VDVFb3BVY3BmbnhadnFuS0l1em5WWg0KOVlEdzNxMUt6VFh6UTdtL3c3QnlWMkk4MldReGVCSmhZTXBmOEFsYXZ3UHFMdFNyQWI5VHl5UkJCSWNYaGFNa1N1RTM4SHVpYjR0VQ0KSGZkQWxMZzVOWHlGdEE9PQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0KFwgBAACzAQACAHwwejAUBgcqhkjOPQIBBgkrJAMDAggBAQsDYgAEesms4Jr2FTqrIcOap2DlOx8AHNBX/zq4jx+k9SCw95moNI7LUxmf89RsDA3EOh3dRuUftkZZ2E1WsLqh0TLflgDK3x66P9OoakIplJ5saZLamHsrp1J7RsBz/vc4FHOrGc8R/d8aP8yqCh2C5jJP4QAgORTMYx+3he7iDh5VU3YKo7e6HyFD9FkD06n4vFjRCaEHXi0tLS0tQkVHSU4gRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KTUlJRk5UQmZCZ2txaGtpRzl3MEJCUTB3VWpBeEJna3Foa2lHOXcwQkJRd3dKQVFRYkNCbUZTZGx3dHFCZ2RpUQpyL2Jub3dJQ0NBQXdEQVlJS29aSWh2Y05BZ2tGQURBZEJnbGdoa2dCWlFNRUFTb0VFSFRvMTNrcHRuVGFQZFVkCitSakR4N0VFZ2dUUURyT1pvM0VadGY2RVd3YnQwY1JSSk8yWHdPUUd4c2xvd1JXZ3dIc1c4bU9xL2ZhNnV4dGsKbmw2T1p6a2VGU1ltTTlSWFdoME5Fd3c4ZE01RkhiZG1qU0pkREVKOFdnWFVnZDJJMjJHWDFROFVqNFlHZ0FqWgpHM2VUTlVadmFjRDNUamZwVHEySE9SS0g4MjJkV3R3dkJlRDQ3WHpzOXVUN21sQzVSQ0lzQ0MzUW03V25PRmNNCm1YMjBMRWFPSk9tVFRDYmtlcFBWQVhXV2hGWlRnK1BxSElmOHNOczBjOE5YcEg2b2NGaTE3elY1dDR6Z2dNL0cKRkgyb2lJaFEyMkxxMjhLa2I5YTlBaThhMWZvd3pFUFhKTGhoQUtMV25zUVJ1bTcveHhlZDJzSmF1dDJpbUpyNApXMHBZeW5zOW85Q3hjaXlySEd5MWs4dVNxTFh3ajc3ZE5ISVNZcVdvRUFKMkJXMkF5c0tlMXlTdTdNZ3dxd0ZICnM0OGV5ZGNnTFJmY003TGo1a1ZkSUZhUDlHM054RW1DeWZLaEJ6WGhUS2tKQk1leXArZXRJTkE3SDVJNnRiem8KTWdSbWRVdUFJZ3l3VitIamFMbWp3dmFyL3gzYk52YkhRRjNBQjRCbHZmQVpaYjNmbitRSWVmdk9WekJKeUhTMgpVMjN4eGh3Q0RnY2xmU0VGemxFNGVWL1ZSMndrcDRBWWVUN0QzOVhFeUV3SFVRREExV3FJSVI1UG1QblBlVU9tCm41VCtEL24xQmVrTE9WVFpnb0NhSExuZ21mcmNHUnBydFd6bGRqc1V4ZWpCZ1dBNTNIQjZnMUlEWVdKY1UwaEwKTzBTNlBuTGVZYjZ3K1ovcUI1Z0RpZG55TFRTVDVUbzVGcGRtblFUOGNuSFA2bHhNRW10TzFsY0dzVjRoWlRWdwpDNTVuQytzeHZwaXJVY2EvTHFzdzBUSFhjSTJmd2RVN1lQamlKcTV2WTlDRlFUKzAybVVBMlJJZjIvcmh1VnBrCnFCZlpPYVZTa0FiNTBvRnU5blFtRkY3UC80Z2RudWxFT0xldm43NmJyaVE2R1BhK0J0S2tINDlRcnZFNWZnY0oKUlRVZ0FoZ0tqSjJ1NFo1MWw0blVxWHR0VVQxa2J6OCtSVXRLSGFsc1lscERubTVpc2pBWS91ZVFwcGhlKzR5RAp0ejNJNkRJaTd2dTRjTktLTmlUeC9hbytkWTRTbGt3cjVQNkRiNzNHRk5EczhSNHliaDJYYlBJZCtRckRWR0FnCnkwSDVpRVZEUGhZcERCS05GWGdUdTNOVS9FOS8zS3lQZkcvM3pTZ3dJNlZmV2dCd1Era2JvajVXU0FBcWVaSDMKOU5XZitPcVgwb1FFVWhENExONWxiYmhtVEJqcjlrL0dMUmtKdm9kZ0RiV0pqUFE3VUhSQUhjOVI5MjhBZHJndgpaT0g1WFl1Z2xYaktzeldyNUZQc2szVjF2QXBBOTh5K3ptVm5kU0MrZmZMdmoySXphY0pXSkRsQVByZHhzZUcyClJqYlJUaG5BT05zc2JYSnoyTVFxRkpYcXFPQzQ3c3BCemlHM0Y2T3dHQW9Qc3VaUXQ4OGl3YlR1SWpLSE9jYXkKUGlQaG96cm9VY0JsTHZhVllhdU5xNFdkNDFnRUROWnVtSkptVXR4NkExQVFQSk1kTWluN2NDSDhWYVZBMUNYSgo3SU05S09zYXRqYUdJNkFUUWpEb0UyaHNHL0YvZjBNNnU2dTZkY1JQYWFxKzRpQWlDV24yNTljMGVEQzdRdWlhCjVQZjRiZ2Y0T2UrTDBQOGNiWmxXR0JpYjZLSVFoKzJteXRKYnZ1ZkpGWTE3OWh3Ukx2bEJMUzRRSHoxNmJoNVYKY09MOW5WTmpVWjQ1bG9WMjdtdGVNdmZITkVadGYvQzdSUFViaG5wcGJlVHgrVHNsOUdoTWs5bm0rd2hvOTVrRQpEVENiamFvdlgycmtQSEMyRTA4UGZPanc2NHBBRXk1U0JQNXdtOGR0UTJYeFdKN2llOHFUR1phTkZsbTVrRlNQCkIzTVFPZzZEaFlqaWw0ZW5DL2R3eVJaK3lJQUxIVUVEdkE1LzM5MzRMc2hOejYzaVorTnhwd1FkSytEZjgxWlAKeGJwdWtINUVLdmdvYmt2R3pwLzk3TmhBNzA2SHhaVlQ5NWczTFlheU5XWkJuVm1XM0xMdWJOUT0KLS0tLS1FTkQgRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KJEROUzosIElQIEFkZHJlc3M6MTkyLjE2OC4xMDAuMSwgVVJJOglsb2NhbGhvc3QCAAAAAAAAAAEAAAACAAAAAAAAABQA32X5ivCpi9BBfDO86mwliT3GkTO0BgEABrAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0NCk1JSUVzekNDQTV1Z0F3SUJBZ0lVQ1BnUHk0ekRXVG9lQWVZdU9LVkc2KzFHbWk4d0RRWUpLb1pJaHZjTkFRRUxCUUF3ZXpFTE1Ba0cNCkExVUVCaE1DV0ZneEVqQVFCZ05WQkFnTUNWTjBZWFJsVG1GdFpURVJNQThHQTFVRUJ3d0lRMmwwZVU1aGJXVXhGREFTQmdOVkJBb00NCkMwTnZiWEJoYm5sT1lXMWxNUnN3R1FZRFZRUUxEQkpEYjIxd1lXNTVVMlZqZEdsdmJrNWhiV1V4RWpBUUJnTlZCQU1NQ1d4dlkyRnMNCmFHOXpkREFlRncweU5qQTFNekF4T1RBek16UmFGdzB6TmpBMU1qY3hPVEF6TXpSYU1Ic3hDekFKQmdOVkJBWVRBbGhZTVJJd0VBWUQNClZRUUlEQWxUZEdGMFpVNWhiV1V4RVRBUEJnTlZCQWNNQ0VOcGRIbE9ZVzFsTVJRd0VnWURWUVFLREF0RGIyMXdZVzU1VG1GdFpURWINCk1Ca0dBMVVFQ3d3U1EyOXRjR0Z1ZVZObFkzUnBiMjVPWVcxbE1SSXdFQVlEVlFRRERBbHNiMk5oYkdodmMzUXdnZ0VpTUEwR0NTcUcNClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURvSGNrTmc4bXFLM3F3bHovb3pYVEtkVE5Fb2wvd2JDMU1SSzRXOTdWOGtSRDUNCnltOEtZYzlLNHRwU0F4enJNUHFJL2hvUjFyaU44ZkZYbzVNR3hzVXg3aUU2a0VzY2dUYS9aeDVmQzFoc0RRMy9zTFZ4bW5SS29vUkgNCnZLVm1WbUtYNVAxODVQS01FMmlLclgzTWNMcVJCdVkvRGxOcHlYMUJpZ3RRSEhWYWhSUkpYQ1hJWWI5TmdGUlZ5Tk8zZy9vakxXWm4NCllXMlhoRjEvcWhjczhUWUtDM0tnVzZUcnNhS3EvWGd3bUQ0QTdybTJLLzZZVGF3S08vbVRHdmIyRnArVnFFYWNFM2d2MTBXRmk1cnENCmpTZHFwTmVhcklCbUlWQmpUcHY1SlRLN3B5c080QzZiU05RR29ZZ1pmQ25SSUhKZitnMVVJWWJGekdvYmdCRzFaV0t6QWdNQkFBR2oNCmdnRXRNSUlCS1RBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lDOURBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUkNCkt3WUJCUVVIQXdJd0V3WURWUjBSQkF3d0NvSUFod1RBcUdRQmhnQXdIUVlEVlIwT0JCWUVGQTk4eFhhV1RCTVA5aC9BZmVscE9kdisNClV2WDBNSUc0QmdOVkhTTUVnYkF3Z2EyQUZBOTh4WGFXVEJNUDloL0FmZWxwT2R2K1V2WDBvWCtrZlRCN01Rc3dDUVlEVlFRR0V3SlkNCldERVNNQkFHQTFVRUNBd0pVM1JoZEdWT1lXMWxNUkV3RHdZRFZRUUhEQWhEYVhSNVRtRnRaVEVVTUJJR0ExVUVDZ3dMUTI5dGNHRnUNCmVVNWhiV1V4R3pBWkJnTlZCQXNNRWtOdmJYQmhibmxUWldOMGFXOXVUbUZ0WlRFU01CQUdBMVVFQXd3SmJHOWpZV3hvYjNOMGdoUUkNCitBL0xqTU5aT2g0QjVpNDRwVWJyN1VhYUx6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFGRndOQVhndjhrdTltY2ZhTnBNcnVkT3YNCkx6ZXhwdEJ6c1FJb0FHeStPa2hEM201TnpKb3NrVFhSMm9xVC9TSXhqdThlcnM4ODYzSkNpUTFXcEp2ZzFGaFVTV2lCOHQ0L2xQMW0NCnBnWjlQaEFSR2NHOXM5TUlFamlDY21sYjhaaDAzRzRlWTdPcnZjWWdWT1o0eTBsdnU4azcxcjdac1Uwdnlwc2h3eTRINzIrMkNQNXYNCnFWZVBHYkN6VXBKME9iWFR3dG16cTB1SFRPRlRZdTdwNG54TFc4T3JJWmgwRjBZNUdIU3BCd1Q1RW9wVWNwZm54WnZxbktJdXpuVloNCjlZRHczcTFLelRYelE3bS93N0J5VjJJODJXUXhlQkpoWU1wZjhBbGF2d1BxTHRTckFiOVR5eVJCQkljWGhhTWtTdUUzOEh1aWI0dFUNCkhmZEFsTGc1Tlh5RnRBPT0NCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0NChcIAQAAswEAAgB8MHowFAYHKoZIzj0CAQYJKyQDAwIIAQELA2IABFxKLs7Le4khH9IYkEsz9kcDDGDF8nvEVwgn+eeo0AtGH8Grj+KWKY5rS+d89rmWV2QhEsLD6MCq1wF3RvCRE3/4SboaY6x3+Sg5/roZ+lZ4KHRPJ1k59MfQ+CAUeYZIKIUNNk0LoDNbtoVKaMPRJ0QAINkAgqWvmvNdaLoz5A4cw06qvAvDvPM0TQcAvh10fZSQB14tLS0tLUJFR0lOIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCk1JSUZOVEJmQmdrcWhraUc5dzBCQlEwd1VqQXhCZ2txaGtpRzl3MEJCUXd3SkFRUXQvOHkzTXNtbUo1L25YdmYKbllUdWh3SUNDQUF3REFZSUtvWklodmNOQWdrRkFEQWRCZ2xnaGtnQlpRTUVBU29FRUh3ZVZURDBKUVltYmxmSQpITmNuRkgwRWdnVFFtRy9NanZabTFobUpHWHJZeUJxMU9iNjU5Mmx0V1pndlE5VmR0RzBYTHpocW1VS1JZbFVqCktXaTE3K1NiNi94VldHUHZMRU1HNDR6NVZPYWlXR1FSODA4cWY3cVZJY3NkNVdBNlk2UDJmR2JOeFBxYzNCWVUKK0xhUlJVdTRiSkZMSUNpM2htY1FFRFpzQkh4Ti8vekM4Wm9Balp2ajZoajlPcnoyMXhSK0hoTkRUUEtlelIzaApTTnExU2hSL0Yrb0haUXN2ZklwckxXQ1ppMEVEU0NRY2E0N2dVc1FHK2JyRkFlTHh1NlZ0azl0OGthVG5JTUk0CkJtc1gyRWYrem4yUmRmamhPVEh1SlZjcjRwRG1mQTVqT1E4NzNtOWdGM0NtWXlsTjgwK1huaFlGRm9WM3hqdjMKL0wxM01xU2x2Yldpa2VMUjhEcW1qZWhJaUlnVnZVazlzU3BEbnBQZjVVbEpwa1owb3VuMlVKTGpRN2ZVVDNWMApQZFBBN0o4TzVpdnJlMEFINkFKamVDdy9rZzlubWhmZDluYVZVazdOMjdPWHBOSEhLRDNjSFpkbDI3NDhPY2ZVCnl6K3hpRDlIY0FBdVRiSTNXdmQ2c0toVXhzbWZaQzk1dWhqWlNyR3FqTkdUVHRsbm1Od1JseURtc0tTZDZVaVkKclMxalR4ZFkzSWFwckxManRHZFlSeFFPaEcrcjZWakRoay9WT1V4UTQ1TklvWno0QXFNZm5XcnRYQktsd25nWgp6VS9BUEN5dGhEUWxGeHZOMGNadnM2bnp2RTVRMVpaN0wwaStEc01vbTR3MEZTWFhua1hWZzZzdVhNSmVBR0k4CmlwNC9NanFFM2x2Z2NqRE11cXRwZytzQzlyU0RqMzR5ZUtabTU1R0o3dmhSd2c0dlJ4TUVsbkNnR2ZjS2NQSFoKS0FqNlZaU3Y1cnE2Y0dEeEhYRTJYbTBEajlTd01qeFJqcnJ0QThEY3pycWdOZ2pYNzV4S3d3TkY5SlY5U3VEdAp2enlMYktZcFpHMUpIRThPT2ZWK3VVdFdCeXZwemJmLzEwWjZ2RHhTUkdJbFZmSzA4L0NJRmFzeEd3RG10VEsrCnJDNHdWS29aWk5vWWg4OHJiMGhVdGlCbnRDdlpqN1VoWFlHL1lOdjNmQ2NPSUVUb2hrcDIvQ3JFR1RJVjVTZFMKU1ltRDhVYlJJbzFNTExQSFhPMld4SC8rYU9aVmhubkY3N0tiTzlYdU1Md0VNVWh2V0FrM05jN1Vhdy9SQXY1UQo2b0NvOUMvZXdtTlFaU2NwRzN0eFN2QUh4bENKTG5tb05ISDNMOUtYZHhiNHU2MUpzN2JlamI4WnBVUFg0bGlRCnNwWGQ5ZXpWQ2Z4bkJ4LzNoSzJtNzZXMkRLNTVYOGQ5Vis0WjZzVFZwNnpWQ3g5cW9ldnNGTUk4Z0wrL1htVjkKUEtXeGREQW5SSXlXbGFkVm5BZ25ZUWJFSU1vZnF5Q0d3OE9Hb1BZeHl6eEtFeU1XM2RqaWlObG1HajllOFovVgp3NE5xNnV5SERyNlBER1ZNaFRWSXgrL3dieStCcDVhTXlpNEh2dElPcTQxZEhFVmhMb2pkMVRVRFFaYW51RFpJCmxia1ZtaFZBTjFld0trclY1QzNPTE9leUt0ZklITUliZVM1Q2lmRjJjRDh1YUIybG1jSDljMGtLblBQa2R3bTcKQUpSRktYZTV1REtTMG05WE9lMWVhdVVBMGpUWGxhRXV5dnZCNHh1Nmpzay96ZDM5V1lKQ0x3RUhXR2NuaWR1QgppSGcwOFliVTNJT2l0NVM5K2FZaU8zLzJpTGw2VEFtZnFEVldmdlpKSDdsY2Y4aTczbHpNaVNLM0haam5EeXgyCm0yUkJ0NVVlREZmUGcxelFmdFpCQTJQOGt6UnZYQ1Z2ZlZ0b25JY0ppdEgxSlRTb2U3UHpRQnYxS3RNV0ZzblUKbnZBb3VEVkkrcHVGTE1DelN0QUZBVXk2SDFrWGZsL1FYUFRtWE9ZK0NJakN1ZDZUVElMdUNyVC8wQ1ZncmN5cgpMSVFRWkRqUTVDNUdkWWg0RHRha3MyOEU4emV5S0M1MmZEZ0RCRkowcE11UEdGanJlRTUvLzhQZGtWZDl0VkpiCkdEdVhuODlQTWdhV0czYUxtY1ZtM1F4UkZKcHJCcEF5T3VnZzFKbEtQcDRtY1lLM3J6SGdFWFU9Ci0tLS0tRU5EIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCiRETlM6LCBJUCBBZGRyZXNzOjE5Mi4xNjguMTAwLjEsIFVSSTo=", - "UserData": "AgAAAAAAAAAAAAAAAAAAAAEAAAAFAGFkbWludAAAAAEAAAEBAAEFAAAnEAAAACAKxabqh8/HqSSmiUjmPTle47rkpbXX5RLQz3bEWqEJBQAAAECno5ExkpVRGcsAI9wjyUgLDqKgCbjXLR+ywfJt1ccUqxR9KuiOEwmq+UTuKxk1pLjkm2qOmARp03EtMTgAE1tBAA==", + "PKIData": "AQAAAAAAAAAAAAAAAAAAAAEBAb4gAAACAAAAAAAAAAAAAwAAAN8ALS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUhvd0ZBWUhLb1pJemowQ0FRWUpLeVFEQXdJSUFRRUxBMklBQkdGWTArTHlpZU9DS1kyYUc0S2F2QVRDUDIxTgp6bmJkaEhhU1lCWVNFaEM2QVlDRkxGUlB2a2I3OGR4Zjk4b20xQmpMUEp5ZkUvQ0pjaWtEMGxVa2Q2aHBkZ3Q4Ci9jN0xta3NPZk9xR01EdmxRTmtvUHJSdVVLMWhYYjRIYitKN29nPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCnsBAQACAAAnEAAg+l96Mdkf906jYRf6suG5MhGs3097++b3xD4nKcNDPMcBACAADAAQ1qWg6KaKPizxkqagk365WgEAAAE2pWwqs+AN6by45wXNhExVqm99Tudswy/F2eyZvSPiPYGXs3OiZdDlOqbdc2JMOHOm8P0usliLuZMUftK0aP7NnkrB0eRwsCaqokrkglliq97eQqZ4N0uJhfpbyyloRWDJWGcFtGLYeqkbQLp0fT0pgDdqhAh/VjrqJaI93olFW0ugyb6dZOj65lBjEUP/zXzsQhZ5je82LNco91oHN5+sSWW307kZlZcZmGB9fwrjtfub8UsSTg40Os/vh/ulWazkkg4hCiGoTUxjRJWEb8syNhYUUZiDTabalnhYoPE4fXaLwkl8jcrTHYpnUGrHEEL45xsVCy111IsrWuiKACEheJCQ80iHVH10O/FuUB0dSU66+8WUB8ftAdz68LCpgUlgWOIW+OJkl2vWqmNLYJKG7ktO8hj1bAIACWxvY2FsaG9zdAEAAAAAAAAAAQAAAAIAAAAAAAAAFAAZD2lGNabFzvAOAgsxOLkDEYDomrQGAQAGsC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQ0KTUlJRXN6Q0NBNXVnQXdJQkFnSVVNL2Rzb09QMEp6UlZuOGhZR08rc1N6VWgveFV3RFFZSktvWklodmNOQVFFTEJRQXdlekVMTUFrRw0KQTFVRUJoTUNXRmd4RWpBUUJnTlZCQWdNQ1ZOMFlYUmxUbUZ0WlRFUk1BOEdBMVVFQnd3SVEybDBlVTVoYldVeEZEQVNCZ05WQkFvTQ0KQzBOdmJYQmhibmxPWVcxbE1Sc3dHUVlEVlFRTERCSkRiMjF3WVc1NVUyVmpkR2x2Yms1aGJXVXhFakFRQmdOVkJBTU1DV3h2WTJGcw0KYUc5emREQWVGdzB5TmpBMU1qa3dOVFEwTlRsYUZ3MHpOakExTWpZd05UUTBOVGxhTUhzeEN6QUpCZ05WQkFZVEFsaFlNUkl3RUFZRA0KVlFRSURBbFRkR0YwWlU1aGJXVXhFVEFQQmdOVkJBY01DRU5wZEhsT1lXMWxNUlF3RWdZRFZRUUtEQXREYjIxd1lXNTVUbUZ0WlRFYg0KTUJrR0ExVUVDd3dTUTI5dGNHRnVlVk5sWTNScGIyNU9ZVzFsTVJJd0VBWURWUVFEREFsc2IyTmhiR2h2YzNRd2dnRWlNQTBHQ1NxRw0KU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ2hIUVUrZ0djUWxIci85TnVZT2M4ZVljRFR3THl2TG1FU1p2dFVlNGpIOEtnZw0KUkxhOERFaXR1T3hubExmVE43cFQyd2U0K01NQ1ZxYXg1UXpyL1Z5czU5MUN6YXVscTg0UktSYlowRGFvNlA1OWhud25paEhPbGNYNg0KdXp2VHgrRDNHbDhZR0ZUeWY2Q05EK3hvd3k4bUNyc0MvTTFJOEpWVXpsK0lwaG5oREticEd5RWtmZnRQOWFiK0E2TlRicElJMTZRdw0KUGxjUzNOTDB5bG4wZTV2bVNka3V5M0RQVEN3MDNSR3Z1V04yOHNBZ1VteStTR1huRG1VcUV1d2tnRk5FdDc0QzlOZU14djVoV2Qreg0KZFFnVVM1dEp4Rm5yR2haLzRqcHF5QVR5NC95UXNZTWx4SzN1eWZiTHhSdlBEcE84SmhQYkg1b3d5QkYrL2dPeFF2aUJBZ01CQUFHag0KZ2dFdE1JSUJLVEFKQmdOVkhSTUVBakFBTUE0R0ExVWREd0VCL3dRRUF3SUM5REFkQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQVFZSQ0KS3dZQkJRVUhBd0l3RXdZRFZSMFJCQXd3Q29JQWh3VEFxR1FCaGdBd0hRWURWUjBPQkJZRUZPWE5KZzB6SjVxUkxCendWbG1PR3BmUQ0KcExGWE1JRzRCZ05WSFNNRWdiQXdnYTJBRk9YTkpnMHpKNXFSTEJ6d1ZsbU9HcGZRcExGWG9YK2tmVEI3TVFzd0NRWURWUVFHRXdKWQ0KV0RFU01CQUdBMVVFQ0F3SlUzUmhkR1ZPWVcxbE1SRXdEd1lEVlFRSERBaERhWFI1VG1GdFpURVVNQklHQTFVRUNnd0xRMjl0Y0dGdQ0KZVU1aGJXVXhHekFaQmdOVkJBc01Fa052YlhCaGJubFRaV04wYVc5dVRtRnRaVEVTTUJBR0ExVUVBd3dKYkc5allXeG9iM04wZ2hReg0KOTJ5ZzQvUW5ORldmeUZnWTc2eExOU0gvRlRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWJ1bFFPQjJiU3p0WlRnd0lYYkZtR2M4Vw0KN3NQRlYyUHRPUVVqTmQ4QmxGbUUreURHU1EyNkMwUE9XaFZRbGR5a2EzYUJZWnI4RkZBZG1NU3Q5bENnZHN2TVEzdklQUzFqRnNOTA0KVzFLd3BDRjhQY3dkWmFYV1BvYllzSlpaaWh2WTR0ZDU3anFqcGh5ZlFHdEk2Zk1CekZ2cythLytMUE05Y09LSVluTGQ0azUvQk9uRw0KeXRhMkZ1U3VjQnY4RXJ4SVJVWk5peFkxanMvTzFEbWFZYXA4WGNoYUwzT0pJL2lrZDJpRGF4Y1R6V25lR1dJUHBvVHF4bXdWaS9FNw0KcDI4V2cxcjhMTnpDMTg2MHNNeDJSVVgwUVRvblAwaXJSQUp6WnRmMXdBa2hYYlk2a2tyTDR2ZGZVVUVkeGtRSU9UcXpxN21SdGNKcw0Kb3hGeDZhTW1ad09qT0E9PQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0KFwgBAACzAQACAHwwejAUBgcqhkjOPQIBBgkrJAMDAggBAQsDYgAEIAhdXQCKSZYm7LEM4EwbfDjqoW5mvDvNP1bSpyUzI3FCtlYFVvPEDIYgWBIfE3bQHWb1AEXd7U4VD2IFT32xVdfaN8W+gR04KTRGA6VXhzgYO8GWasNPD2L4farlOgVzElWl6dyM+iwL0iAWPvnIQQAgrV10O32jjAUTl5v3/piuv7uXHCTI45et3CnyMHqRHSoHXi0tLS0tQkVHSU4gRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KTUlJRk5UQmZCZ2txaGtpRzl3MEJCUTB3VWpBeEJna3Foa2lHOXcwQkJRd3dKQVFRcWhudXNBcDUwSGJxRTlMSApMbW5NU2dJQ0NBQXdEQVlJS29aSWh2Y05BZ2tGQURBZEJnbGdoa2dCWlFNRUFTb0VFT2daRmlab0NRSkFLTUJICmIySUdXUVVFZ2dUUTdReGZNcGorQVJ5aWFKT2xxN3ZQOWFpVlpYaHBUUHhXQ2FWUVFKbE45Qi9hZVFzc0tlZGEKRzE2VHplcTVNdWtCZWh0bHFNT3lGY3B1bWxHTTJibnQycXpDZXJNRmpkNCsyNlExa25OY09CNTg5QnJOUWo1UAo0ZmlLVzc3NHE4Rk1xUjF1WkNPTTVTU0VKNGhEeEU1SFFrckJpdUFETlJxK1JvOHNJMkxMVEFjcG54QkQyVWIvCkwxTFZLc3oyUGZFeVFCVDllTVdldmVwM1BIWnoza2RwbXZrM2owSjlrZXdKVjJuK09sOXBTQ2luaEg3MDY2Tm8KWU9COFl4NmNCQkdzaU9MdE1TbzRsbDlBcFhJYnAybDNjR0g5YWtnZjZnemFaeVJXUDJISHRRNThETHdvajZOdwpVelJ6YVBjRUw3VWhNRk1OWXFJeE9SdTRQc3A4dU5xemZ2ci9EeG9kT1Z2RklBYk90Wno3WTVwNXNYcko1UHBDCmZTNHFZK1BXUVJ6NncvMWdVTVNxNE5MYnhPUnlqRGMvc0JEUlo4T2JvK0ttUURpMklTdEMzUlJESWQ5R2tHOEQKYmFza2NxUTFZWjhMZ2QzOHVxbHphd0ZKb0NGNjIvaHVRVTdjV1RsMzdBNG1KRi9CTjJFYnNxakdVL2FsRXNBUwprTlRLdTc5VU4ycENxNzBjZm04aUlkNHZVdGVmR0UxWnNXQkh1S0JnL0FoN2Z3NlQ4ZWVxQ2RtK1A1VXVGYSs2CkpzZXlFdkcwT241RDV2Nk5tbERqUHdLY0FUTWVPTlZoZkh1UU91VDEzakZkQXc2d2c5RVd1TEhLcjdUSlVVQSsKL09zRjU5azJjemlpTTZJMmdUSS9XME8yWGltMFJuc1hENVo2MWozNzEzc1YzaStUU0xNQlBWMG5WdHNhTEY2egpydUJpOUNiOW0yZVhpV0cyS1V5dW1IbVFYcForb25nbnE2UDY1TUw0VUZNampidENMR3lrYW1xQmtLNDZUK0dJCjJoRzlSRkxPVWZnd3htSEJHbzdjYzN6VmZOSy9nMlNmZjhlR2RKUXZZM05ia0JWWndmV3J5YXNLamlScXpadkgKRi9BSW4wYm1ycGViTWZ0UjhCeDBwczVNdXpnSVp2MjA2b2s5NHRuQ214ZmFpYmFOUnJydk8yd2xDOWN4a0ZCRwpOYTJRaE5sOHlOT0tMcTlXL3IrSkx1R01rZ3FqTm0wRjFQVi9QRW1tNXNqQVd6YW1DUUlvWGJUZXcyWVVFMlBpCnpOcTBucHl2bnNLOVNreXdEWSt3aklIMm10dS9aZVZheDJkNUFxbFlINzU2MWNSQS9JTjgyeTBHZ3dHUWFVbE0KS3BSenJRK2ZHc0I3YUhwTEsyL1lWQTlqRjBCQ1RuTExxT0VJUUNSVy8wOFUrQ0ZEeFUvcU16VXJLd2UyNFlaUwprVXRxeXR4elZlaDlGOXVNaXlTY1FCdUx5L1pMM0ZDaGI2Vm1FemU2SW5GQmlOVjNFWUhnTXlHSXhNWmZVM3dFCmFLRVBLRHQvY1dhNUhkdE9oaHMvOVVRN2x2blNvT3o3MHNvcm94SDl3aWxmVTZFYWI3Q1UwTTF1dVFENFJnb3gKcTFmNjR2V3JmL3JaTkFMdjBsMGV2aXgwVnZhYlF0c1BZa1pEWnptTWlTMTkzUkNzdEphaVpuU0JSTmhCV3FNYwpBODBnaWVEdldOSkpoWnNUWE12Rlp6RGdJWmRMdXh4bHZqV0E3Zk9jdFB4WjF1Y3VPSzNpNkpkR1o0YTg5SVlWCmNhb250VDMra2k5S3Y3TDV5TnJsSWlqc1NFdElwWDdHazZuUE1IMVFrR0h5WjNxSHcwdkorVHJOa25zcG83T2gKaVpEWFZ4WUx5ZjNCMzZWMlhLRUgrQW01amNzVlFOZW1WWVVSTmk3L20rdks4a1dqVkNVemtpRk45ckNOYW5ZZgpZTGlteWlSVEdGNkc4TENJQ2dPZm1RNEQ4NHBOZ0ZDNVhXQmRnWW1HSXVMZFFqYUZ1blc1U0FhNVZFT3RkaXRTCnE5UkxvK0JFV24wZzJEUmhjditFUXhRd1dYZm5SSjVmV2sxK1kreU01d2kyNExtdFhVQk9tTkJ0QmNCZXhLcmMKbFBxQ3JIWnNFSkhNV2NpZVFYM2dNdXZYRy84ci8xMWZaRWQ1RnFCTFo1cUZORVZvQUZZcy9YYz0KLS0tLS1FTkQgRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KJEROUzosIElQIEFkZHJlc3M6MTkyLjE2OC4xMDAuMSwgVVJJOglsb2NhbGhvc3QCAAAAAAAAAAEAAAACAAAAAAAAABQAGQ9pRjWmxc7wDgILMTi5AxGA6Jq0BgEABrAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0NCk1JSUVzekNDQTV1Z0F3SUJBZ0lVTS9kc29PUDBKelJWbjhoWUdPK3NTelVoL3hVd0RRWUpLb1pJaHZjTkFRRUxCUUF3ZXpFTE1Ba0cNCkExVUVCaE1DV0ZneEVqQVFCZ05WQkFnTUNWTjBZWFJsVG1GdFpURVJNQThHQTFVRUJ3d0lRMmwwZVU1aGJXVXhGREFTQmdOVkJBb00NCkMwTnZiWEJoYm5sT1lXMWxNUnN3R1FZRFZRUUxEQkpEYjIxd1lXNTVVMlZqZEdsdmJrNWhiV1V4RWpBUUJnTlZCQU1NQ1d4dlkyRnMNCmFHOXpkREFlRncweU5qQTFNamt3TlRRME5UbGFGdzB6TmpBMU1qWXdOVFEwTlRsYU1Ic3hDekFKQmdOVkJBWVRBbGhZTVJJd0VBWUQNClZRUUlEQWxUZEdGMFpVNWhiV1V4RVRBUEJnTlZCQWNNQ0VOcGRIbE9ZVzFsTVJRd0VnWURWUVFLREF0RGIyMXdZVzU1VG1GdFpURWINCk1Ca0dBMVVFQ3d3U1EyOXRjR0Z1ZVZObFkzUnBiMjVPWVcxbE1SSXdFQVlEVlFRRERBbHNiMk5oYkdodmMzUXdnZ0VpTUEwR0NTcUcNClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNoSFFVK2dHY1FsSHIvOU51WU9jOGVZY0RUd0x5dkxtRVNadnRVZTRqSDhLZ2cNClJMYThERWl0dU94bmxMZlRON3BUMndlNCtNTUNWcWF4NVF6ci9WeXM1OTFDemF1bHE4NFJLUmJaMERhbzZQNTlobnduaWhIT2xjWDYNCnV6dlR4K0QzR2w4WUdGVHlmNkNORCt4b3d5OG1DcnNDL00xSThKVlV6bCtJcGhuaERLYnBHeUVrZmZ0UDlhYitBNk5UYnBJSTE2UXcNClBsY1MzTkwweWxuMGU1dm1TZGt1eTNEUFRDdzAzUkd2dVdOMjhzQWdVbXkrU0dYbkRtVXFFdXdrZ0ZORXQ3NEM5TmVNeHY1aFdkK3oNCmRRZ1VTNXRKeEZuckdoWi80anBxeUFUeTQveVFzWU1seEszdXlmYkx4UnZQRHBPOEpoUGJINW93eUJGKy9nT3hRdmlCQWdNQkFBR2oNCmdnRXRNSUlCS1RBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lDOURBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUkNCkt3WUJCUVVIQXdJd0V3WURWUjBSQkF3d0NvSUFod1RBcUdRQmhnQXdIUVlEVlIwT0JCWUVGT1hOSmcweko1cVJMQnp3VmxtT0dwZlENCnBMRlhNSUc0QmdOVkhTTUVnYkF3Z2EyQUZPWE5KZzB6SjVxUkxCendWbG1PR3BmUXBMRlhvWCtrZlRCN01Rc3dDUVlEVlFRR0V3SlkNCldERVNNQkFHQTFVRUNBd0pVM1JoZEdWT1lXMWxNUkV3RHdZRFZRUUhEQWhEYVhSNVRtRnRaVEVVTUJJR0ExVUVDZ3dMUTI5dGNHRnUNCmVVNWhiV1V4R3pBWkJnTlZCQXNNRWtOdmJYQmhibmxUWldOMGFXOXVUbUZ0WlRFU01CQUdBMVVFQXd3SmJHOWpZV3hvYjNOMGdoUXoNCjkyeWc0L1FuTkZXZnlGZ1k3NnhMTlNIL0ZUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFidWxRT0IyYlN6dFpUZ3dJWGJGbUdjOFcNCjdzUEZWMlB0T1FVak5kOEJsRm1FK3lER1NRMjZDMFBPV2hWUWxkeWthM2FCWVpyOEZGQWRtTVN0OWxDZ2Rzdk1RM3ZJUFMxakZzTkwNClcxS3dwQ0Y4UGN3ZFphWFdQb2JZc0paWmlodlk0dGQ1N2pxanBoeWZRR3RJNmZNQnpGdnMrYS8rTFBNOWNPS0lZbkxkNGs1L0JPbkcNCnl0YTJGdVN1Y0J2OEVyeElSVVpOaXhZMWpzL08xRG1hWWFwOFhjaGFMM09KSS9pa2QyaURheGNUelduZUdXSVBwb1RxeG13VmkvRTcNCnAyOFdnMXI4TE56QzE4NjBzTXgyUlVYMFFUb25QMGlyUkFKelp0ZjF3QWtoWGJZNmtrckw0dmRmVVVFZHhrUUlPVHF6cTdtUnRjSnMNCm94Rng2YU1tWndPak9BPT0NCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0NChcIAQAAswEAAgB8MHowFAYHKoZIzj0CAQYJKyQDAwIIAQELA2IABEjFWuCvXGdXFgLcIUNcWH2wkh0A7xs0sGabDArqxS4ueT9TwWu8CCRXTI7jGJ/gEV9GUbI8uCmO2TqfC2V+ZBREJDqYpTF1ligILh504UTmH7yOMq54Ul7rthINmU0nbWa2AHmsXNpeRQLqAW2pQIQAIG9b5YjfA5Cm3vv3tbKNeDxwnUSFpmWY7GZXgJbswseuB14tLS0tLUJFR0lOIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCk1JSUZOVEJmQmdrcWhraUc5dzBCQlEwd1VqQXhCZ2txaGtpRzl3MEJCUXd3SkFRUWtZbUxrdWNzLzJpOEMyMUwKaE1ma2xRSUNDQUF3REFZSUtvWklodmNOQWdrRkFEQWRCZ2xnaGtnQlpRTUVBU29FRUphOTh4SVNKWGJaTWFyZgo4aGp0RWhNRWdnVFFGdHVRMDkxdVNZbjRqamRvUy9Na1JkWWhocG1EVVVKejhoclRqeXVxb0hNUUl1S2tEcXc3CkRGVmM1c3NHVlZIMGw0VGhndmg5UnNaYWpxQm10U2lqZnBGU0xsQnFRc3Y5SjROYmNoMDhJZmZzTjhHampMVkgKTjJaV1QrblkvQWROUm5XNThYVWhMNHRobXlmZTl4WEZIVmxsc3BGU1E0Tk52OUwxTGE0RURieVJJTDk5Mzh3Qwp2U2dnUXJVcjlKMkFYTTEraWJlQWJ5OCtlbVl5ZHRmZDhTTS8xMEpNd1k2eWF0L25lZVVpT09EMXFJWG8ydGQwCndGc2Z1cXptTnhTbUtWVW9HeWtXU2hkR2xJV0V2UHdaM2h4SUVQTFlYaHhsYlUvbEN4NEUvcUNYR0dhS0Z0WS8KbGc4NDhSWHhtOTNGUXNFaGtFbFN5NjNVTW0vV3dGNWpZWnhmZm1YZU9tdVdMdEozREo3ajMwSU1DZkNyRTI0SworQ1RsVnVpK1FyMW04Q0FjWDBUK3NKRlZ5WkxHU1JjcGs0T3JOR1FxbHFzVWxnV2NDbVExaFVkeWxQUU1jNGZECm9TNFdQR013QmZDTjNJK1B5dnRBZ0VQeFBnMGVnV0dJeDJWZHdqWlltbVhWRG5uV0ZEdGhaNU1Hd2tUczM0bjAKaCtTNG5JMFh0bTh6MmtPS3FCcXBBcWNrREszejdGQVFrSUlzN1B2dmd6M1k5Mlhwa3JyYS9kTytydmQyejZENQptaVhmdnNOUkpoY1l4R1BXVDdSekI3RmdwbUdoQzNob3ZrZnlCc2w4YTVPNkY0RTJiMEdtZ0dUNm5EMHE4bUpQCndQTGd3WUE0T3VKTFArVW04T0J5TmhWL1B1MjhmdXU3UjBVRlVieDFxbTRJOWgzVjdHZzJyRHFhdENmZC9qZjMKekhzT0g0YytMTzVQSjhiTFdzK2NDTUM2TjhIMUZRZGNiNlVmaVJGRnpjcnViR2JnSVpkUWtxNHZnNk12c1BNeApLcVBrSlUvOU9IMjYzZy9lQUFhV3FlNzhsTlVTVElRaFpTQkphMkV3YWJoREFGSTI2UDU0Y3RmT0VVS1V6M2hDCmlDQzR1OXprK1drNlpWZnIxVUl1bk4zcEVrWHlmTStQNEF5L3FyTS9vVDQyQTFJMEdwN0FWU3NPYktXQk9EWEYKS1RFL2xEekxFdU9JckFubzh2Uklwak5hbEZybjU4Y3VyY1Z6Nm12R1BoZmlGcnF4OW9Yb252UStudWRNZ3o4UQozT0VVNU1XRDlDcEFpOHJKMlVoVXBYdmxEQlVDMWlLR0t1RHd6L2taMzJWbmYwVjVGQzY2K2dLRXdhRGMxdDRLCmJvdENMdkdXV1c1Nkhqa3NhMVYrNDluOVg5dXBFeno4WFBjMUhxZzI2RVJqN0I1M1dWMWxBeXphekRQVlV1K28KY1REa0hhYlExTklXZnlvNG81dFFtYWhwZ1BrUUkrVlJWNUN1ZjhEdkdPQlFXbk84b0M2SnM1WGUrRXhjQXkvUwpiNVJkOUlHOThtR2NINFlRMzU4UnZKMzJHMjFZa3Vod2N0R1BjOGlIRk5Id2g2bzJlQmg5aTN1eGtUL1V6MmNxCm5SblA5d240ZkZUSHVQaEVyOExYQzVFNUhTeFp0aDlKZUdWdytTNXd5VEk2OURtT0UwWHBKZWpjYUk3a3hoZC8KSGZEcW5nVGh6Z1hwOWxka1FTb0xLQklVR3pCQ2VuOWIxRm1uQ0pqSGJNV2NjNEEwZFZONU5zSVVwQUJJTzdtYgpOYnRJQ2lBcG9oaWRpZFJ1bTRNMDhsSE1INFhRbHZlUU1yYVdaSGZpMWJGcnpoekN5WjFUYWp2aC9hejNZeEVnClBxaUsxTFdIMGZOY01maGpmamRNcEIzdjgycDd2dHJoT1ZzVWdsbGdDVm0rWkdQVjQ1R2RLS20vV3VaTHlSVlYKTWVWd3R2ZWhDNlJlbEJnOWg4a004UVFDblJKTkhzZEtHL3FCS3REdDg2TlE0bzYySGJ2MW01UXdHVjZFdUcwYQpCT1BLRUw5NmNJdFhNa1hIaWJTb1NRam9abVQxVlRBYmZaQlMrRDBwU2dXSGRGeUFRVEJSQWlwWklsZFlNWFlxCjlqcVFmMlg0VE1nMkpsWVMxTFJUZGNEcmlsRHZMNnd2MmdjdG95Tnc1a2EvL1NUZHcvYmEwcVk9Ci0tLS0tRU5EIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCiRETlM6LCBJUCBBZGRyZXNzOjE5Mi4xNjguMTAwLjEsIFVSSTo=", + "UserData": "AgAAAAAAAAAAAAAAAAAAAAEAAAAFAGFkbWludAAAAAEAAAEBAAEFAAAnEAAAACBrwDawmVm+NhfrGO0XK+ixYtLL+Z0BnlRIcNxv44XOYQAAAEB9JaMNLcNZhiI2DhViOrG4YOg8SAN0F42CWLQkRQznjfpxta9pbJXGJ9/Wt+0d9FiGGOktixYwDq/LVaP8CV/FAA==", "CertificateAssignments": { "TLS": 1, "WebServer": 2 }, - "AccessProtectionData": "AQAAAAAAAAAAAAAAAAAAAAABAABQAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAaEJNxqgEdFxXPR2gDNGn9FjPUzRFBg6WoJ3btPYSa/dQAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAGkVXUjiOBK8kPCZRHAArnvGe9nDngtos9GOsXUHRt41QAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAaEJNxqgEdFxXPR2gDNGn9FjPUzRFBg6WoJ3btPYSa/dQAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAaEJNxqgEdFxXPR2gDNGn9FjPUzRFBg6WoJ3btPYSa/cgAAAADaRd5aKDHmWAkFvNhM/5u8oDWeBjkIf0jmVbPrUloiE=" + "AccessProtectionData": "AQAAAAAAAAAAAAAAAAAAAAABAABQAAAAAAEBAAAAAgDQBwAAIACwOt5ApS9g8FqdirP6d9PuSlQI+jS4eM/baMJBEvTjnCAAH5XI3t7MPZhvLGdwLzb5I/YilB3WeKJS9tUqSZ23FDdQAAAAAAEBAAAAAgDQBwAAIACwOt5ApS9g8FqdirP6d9PuSlQI+jS4eM/baMJBEvTjnCAACg6QBgHbuRwaOq838geKJZtzFNP3G0lBL05j20nZ+2BQAAAAAAEBAAAAAgDQBwAAIACwOt5ApS9g8FqdirP6d9PuSlQI+jS4eM/baMJBEvTjnCAAH5XI3t7MPZhvLGdwLzb5I/YilB3WeKJS9tUqSZ23FDdQAAAAAAEBAAAAAgDQBwAAIACwOt5ApS9g8FqdirP6d9PuSlQI+jS4eM/baMJBEvTjnCAAH5XI3t7MPZhvLGdwLzb5I/YilB3WeKJS9tUqSZ23FDcgAAAAsDreQKUvYPBanYqz+nfT7kpUCPo0uHjP22jCQRL045w=" } \ No newline at end of file diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor b/src/showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor index 41a73fdf8..4be2c880f 100644 --- a/src/showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor +++ b/src/showcase/app/ix-blazor/showcase.blazor/Pages/components-cognex-vision/Documentation/CognexVision.razor @@ -27,7 +27,7 @@
  • Category: Vision
  • Vendor: Cognex
  • -
  • Components: AxoInsight v6, AxoInsight v24, AxoDataman (2), AxoVisionPro
  • +
  • Components: AxoInsight v6, AxoInsight v24, AxoDataman (2), AxoVisionPro, AxoVisionProNet
  • Context: cognex_vision_documentation
@@ -40,6 +40,7 @@
  • +
  • @@ -63,6 +64,8 @@
  • +
  • +
  • @@ -788,6 +791,141 @@ + +
    +
    + Maturity: + +
    +
    + TCP / .NET alternative. + AxoVisionProNet talks to the Vision PC over a TCP socket from its .NET twin — + no PROFINET hardware. Each operation is an AxoRemoteTask + (Trigger, SetRecipe, InspectionResult, TriggerWithSpecificData, ReceiveSpecificData, + SendSpecificDataAndTypes). The socket is opened once from Program.cs. +
    + + +
    + +
    +
    + +
    +
    +

    Component declaration (ST)

    +

    + + — <ComponentDeclaration> region +

    + @if (_comp6DeclarationSnippet != null && !_comp6DeclarationSnippet.IsError) + { + + } + else if (_loadingCode) + { +
    Loading...
    + } + else + { +

    Unable to load snippet

    + } +
    +
    +

    Component Run call (ST)

    +

    + + — <Initialization> region +

    + @if (_comp6InitSnippet != null && !_comp6InitSnippet.IsError) + { + + } + else if (_loadingCode) + { +
    Loading...
    + } + else + { +

    Unable to load snippet

    + } +
    +
    +

    Commissioning task (ST)

    +

    + + — <VisionProNetCommissioning> region (manual-mode only) +

    + @if (_comp6CommissioningSnippet != null && !_comp6CommissioningSnippet.IsError) + { + + } + else if (_loadingCode) + { +
    Loading...
    + } + else + { +

    Unable to load snippet

    + } +
    +
    +
    + +
    +

    + + — <AxoVisionProNetInitialize> region +

    + @if (_netInitSnippet != null && !_netInitSnippet.IsError) + { + + } + else if (_loadingCode) + { +
    Loading...
    + } + else + { +

    Unable to load snippet

    + } +
    + Remote-task handlers self-initialize in the twin's PostConstruct, + so no per-task Initialize() wiring is needed. Until the TCP client is + connected, the PLC tasks report HasRemoteException — exercise the + Error-recovery step to clear them. +
    +
    +
    + +
    + + + @if (_c6StepLogicBlocks.Count > 0) + { +
    + @foreach (var step in _c6StepLogicBlocks) + { + _c6StepsBySymbol.TryGetValue(step.StepSymbol, out var liveStep); + + } +
    + } + else if (_loadingCode) + { +
    Loading step logic...
    + } + else + { +
    + No sequencer step blocks were detected. +
    + } +
    +
    +
    +
    +
    @@ -818,6 +956,12 @@ private CodeSnippet? _comp5DeclarationSnippet; private CodeSnippet? _comp5InitSnippet; private List _c5StepLogicBlocks = new(); + // Component 6 snippets (AxoVisionProNet — TCP/.NET alternative) + private CodeSnippet? _comp6DeclarationSnippet; + private CodeSnippet? _comp6InitSnippet; + private CodeSnippet? _comp6CommissioningSnippet; + private CodeSnippet? _netInitSnippet; + private List _c6StepLogicBlocks = new(); // Hardware snippets — per component private CodeSnippet? _hwc1DeviceInstanceSnippet; private CodeSnippet? _hwc1DeviceTemplateSnippet; @@ -842,6 +986,7 @@ private Dictionary _c3StepsBySymbol = new(); private Dictionary _c4StepsBySymbol = new(); private Dictionary _c5StepsBySymbol = new(); + private Dictionary _c6StepsBySymbol = new(); private bool _loadingCode = true; private string _selectedPresentation = "Command-Control"; // PLC source paths @@ -851,6 +996,8 @@ private readonly string _plcComponent3Path = "src/showcase/app/src/components.cognex.vision/Documentation/AxoDataman_Secondary.st"; private readonly string _plcComponent4Path = "src/showcase/app/src/components.cognex.vision/Documentation/AxoInsight_v_24_0_0.st"; private readonly string _plcComponent5Path = "src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionPro.st"; + private readonly string _plcComponent6Path = "src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st"; + private readonly string _programCsPath = "src/showcase/app/ix-blazor/showcase.blazor/Program.cs"; // Hardware configuration paths private readonly string _hwcPlcLineYmlPath = "src/showcase/app/hwc/plc_line.hwl.yml"; private readonly string _hwcDevice1TemplatePath = "src/showcase/app/hwc/library_templates/Cognex_Vision_Insight_V_6_0_0/Cognex_Vision_Insight_V_6_0_0.hwl.yml"; @@ -870,6 +1017,8 @@ private readonly string _libAxoInsightV24StPath = "src/components.cognex.vision/ctrl/src/AxoInsight/v_24_0_0/AxoInsight.st"; private readonly string _libAxoDatamanStPath = "src/components.cognex.vision/ctrl/src/AxoDataman/v_6_0_0/AxoDataman.st"; private readonly string _libAxoVisionProStPath = "src/components.cognex.vision/ctrl/src/AxoVisionPro/AxoVisionPro.st"; + private readonly string _libAxoVisionProNetStPath = "src/components.cognex.vision/ctrl/src/AxoVisionProNet/AxoVisionProNet.st"; + private readonly string _libAxoVisionProNetNetPath = "src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.TcpClient.cs"; private readonly string _libApaxYmlPath = "src/components.cognex.vision/ctrl/apax.yml"; protected override async Task OnInitializedAsync() { @@ -885,11 +1034,16 @@ var comp4InitTask = CodeProvider.GetTaggedRegionAsync(_plcComponent4Path, "Initialization"); var comp5DeclTask = CodeProvider.GetTaggedRegionAsync(_plcComponent5Path, "ComponentDeclaration"); var comp5InitTask = CodeProvider.GetTaggedRegionAsync(_plcComponent5Path, "Initialization"); + var comp6DeclTask = CodeProvider.GetTaggedRegionAsync(_plcComponent6Path, "ComponentDeclaration"); + var comp6InitTask = CodeProvider.GetTaggedRegionAsync(_plcComponent6Path, "Initialization"); + var comp6CommissioningTask = CodeProvider.GetTaggedRegionAsync(_plcComponent6Path, "VisionProNetCommissioning"); + var netInitTask = CodeProvider.GetTaggedRegionAsync(_programCsPath, "AxoVisionProNetInitialize"); var c1StepsTask = CodeProvider.GetStepLogicBlocksAsync(_plcComponent1Path); var c2StepsTask = CodeProvider.GetStepLogicBlocksAsync(_plcComponent2Path); var c3StepsTask = CodeProvider.GetStepLogicBlocksAsync(_plcComponent3Path); var c4StepsTask = CodeProvider.GetStepLogicBlocksAsync(_plcComponent4Path); var c5StepsTask = CodeProvider.GetStepLogicBlocksAsync(_plcComponent5Path); + var c6StepsTask = CodeProvider.GetStepLogicBlocksAsync(_plcComponent6Path); // var stepLogicTask = CodeProvider.GetStepLogicBlocksAsync(_plcComponent1Path); // Hardware snippets — per component var hwc1InstanceTask = CodeProvider.GetTaggedRegionAsync(_hwcPlcLineYmlPath, "CognexInsight7600Device"); @@ -909,7 +1063,8 @@ var hwc5IoSystemTask = CodeProvider.GetTaggedRegionAsync(_hwcPlcLineYmlPath, "CognexVisionProIoSystem"); await Task.WhenAll(comp1DeclTask, comp1InitTask, comp2DeclTask, comp2InitTask, comp3DeclTask, comp3InitTask, comp4DeclTask, comp4InitTask, - comp5DeclTask, comp5InitTask, c1StepsTask, c2StepsTask, c3StepsTask, c4StepsTask, c5StepsTask, //stepLogicTask, + comp5DeclTask, comp5InitTask, comp6DeclTask, comp6InitTask, comp6CommissioningTask, netInitTask, + c1StepsTask, c2StepsTask, c3StepsTask, c4StepsTask, c5StepsTask, c6StepsTask, //stepLogicTask, hwc1InstanceTask, hwc1TemplateTask, hwc1IoSystemTask, hwc2InstanceTask, hwc2TemplateTask, hwc2IoSystemTask, hwc3InstanceTask, hwc3TemplateTask, hwc3IoSystemTask, @@ -925,12 +1080,17 @@ _comp4InitSnippet = await comp4InitTask; _comp5DeclarationSnippet = await comp5DeclTask; _comp5InitSnippet = await comp5InitTask; + _comp6DeclarationSnippet = await comp6DeclTask; + _comp6InitSnippet = await comp6InitTask; + _comp6CommissioningSnippet = await comp6CommissioningTask; + _netInitSnippet = await netInitTask; // _stepLogicBlocks = await stepLogicTask; _c1StepLogicBlocks = await c1StepsTask; _c2StepLogicBlocks = await c2StepsTask; _c3StepLogicBlocks = await c3StepsTask; _c4StepLogicBlocks = await c4StepsTask; _c5StepLogicBlocks = await c5StepsTask; + _c6StepLogicBlocks = await c6StepsTask; _hwc1DeviceInstanceSnippet = await hwc1InstanceTask; _hwc1DeviceTemplateSnippet = await hwc1TemplateTask; _hwc1IoSystemSnippet = await hwc1IoSystemTask; @@ -976,6 +1136,11 @@ s => s.Symbol.Substring(s.Symbol.LastIndexOf('.') + 1), s => s ); + _c6StepsBySymbol = Entry.Plc.Ctx.cognex_vision_documentation.axoVisionProNet.Steps + .ToDictionary( + s => s.Symbol.Substring(s.Symbol.LastIndexOf('.') + 1), + s => s + ); } catch (Exception ex) { @@ -1044,6 +1209,16 @@ this.StartPolling(step.Order, 250); this.StartPolling(step.Descr, 500); } + var seq6 = Entry.Plc.Ctx.cognex_vision_documentation.axoVisionProNet.Sequencer; + var steps6 = Entry.Plc.Ctx.cognex_vision_documentation.axoVisionProNet.Steps; + this.StartPolling(seq6, 250); + foreach (var step in steps6) + { + this.StartPolling(step.IsActive, 250); + this.StartPolling(step.IsEnabled, 250); + this.StartPolling(step.Order, 250); + this.StartPolling(step.Descr, 500); + } } } diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Program.cs b/src/showcase/app/ix-blazor/showcase.blazor/Program.cs index 5eba4845c..08d7fab55 100644 --- a/src/showcase/app/ix-blazor/showcase.blazor/Program.cs +++ b/src/showcase/app/ix-blazor/showcase.blazor/Program.cs @@ -176,6 +176,16 @@ }); // +// +// AxoVisionProNet drives the Vision PC over a TCP socket. Its remote-task +// handlers self-initialize in the twin's PostConstruct, but they throw until the +// TCP client is connected. Open the socket once at startup (fire-and-forget); +// point Host at the Vision PC. Until connected, the PLC tasks surface +// HasRemoteException — which the showcase's Error-recovery step then clears. +_ = Entry.Plc.Ctx.cognex_vision_documentation.axoVisionProNet.VisionProNet + .InitializeVisionClientAsync(host: "192.168.100.142", port: 8500); +// + // Initialize content search index (fire-and-forget, non-blocking) _ = app.Services.GetRequiredService().InitializeAsync(); diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Services/Search/ShowcasePageRegistry.cs b/src/showcase/app/ix-blazor/showcase.blazor/Services/Search/ShowcasePageRegistry.cs index a8e4dbe93..955cd520a 100644 --- a/src/showcase/app/ix-blazor/showcase.blazor/Services/Search/ShowcasePageRegistry.cs +++ b/src/showcase/app/ix-blazor/showcase.blazor/Services/Search/ShowcasePageRegistry.cs @@ -395,9 +395,9 @@ public static List GetAllPages() => LibraryNamespace = "AXOpen.Components.Cognex.Vision", Category = "Vendor Components", Vendor = "Cognex", - Description = "Practical reference for integrating Cognex vision systems in SIMATIC AX applications with runnable command widgets and live component status.", + Description = "Practical reference for integrating Cognex vision systems in SIMATIC AX applications with runnable command widgets and live component status. Covers PROFINET (AxoVisionPro) and the TCP/.NET remote-task alternative (AxoVisionProNet).", Icon = "document-text", - Tags = ["Cognex", "vision", "camera", "inspection", "image"], + Tags = ["Cognex", "vision", "camera", "inspection", "image", "VisionPro", "VisionProNet", "TCP", "remote task"], SourceFilePaths = [ "src/showcase/app/src/components.cognex.vision/Documentation/CognexVision.st", "src/showcase/app/src/components.cognex.vision/Documentation/AxoInsight_v_6_0_0.st", @@ -405,6 +405,7 @@ public static List GetAllPages() => "src/showcase/app/src/components.cognex.vision/Documentation/AxoDataman.st", "src/showcase/app/src/components.cognex.vision/Documentation/AxoDataman_Secondary.st", "src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionPro.st", + "src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st", "src/components.cognex.vision/docs/README.md", "src/components.cognex.vision/docs/AxoInsight_v_6_0_0_0.md", "src/components.cognex.vision/docs/AxoInsight_v_24_0_0.md", @@ -415,6 +416,8 @@ public static List GetAllPages() => "src/components.cognex.vision/ctrl/src/AxoInsight/v_24_0_0/AxoInsight.st", "src/components.cognex.vision/ctrl/src/AxoDataman/v_6_0_0/AxoDataman.st", "src/components.cognex.vision/ctrl/src/AxoVisionPro/AxoVisionPro.st", + "src/components.cognex.vision/ctrl/src/AxoVisionProNet/AxoVisionProNet.st", + "src/components.cognex.vision/src/AXOpen.Components.Cognex.Vision/AxoVisonProNet/AxoVisionProNet.TcpClient.cs", "src/components.cognex.vision/ctrl/apax.yml", "src/showcase/app/hwc/library_templates/Cognex_Vision_Insight_V_6_0_0/Cognex_Vision_Insight_V_6_0_0.hwl.yml", "src/showcase/app/hwc/library_templates/cognex_vision_dataman280/Cognex_Dataman280.hwl.yml", diff --git a/src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st b/src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st new file mode 100644 index 000000000..954debdc1 --- /dev/null +++ b/src/showcase/app/src/components.cognex.vision/Documentation/AxoVisionProNet.st @@ -0,0 +1,134 @@ +USING AXOpen.Core; + +NAMESPACE AXOpen.Components.Cognex.Vision + + {S7.extern=ReadWrite} + CLASS AxoVisionProNet_Example EXTENDS AXOpen.Core.AxoObject + // + VAR PUBLIC + VisionProNet : AXOpen.Components.Cognex.Vision.AxoVisionProNet; + END_VAR + // + + // + VAR PUBLIC + {#ix-set:AttributeName = "<#Activate manual control#>"} + ActivateManualControl : BOOL; + END_VAR + // + + METHOD PUBLIC OVERRIDE Run + VAR_INPUT + inParent : IAxoObject; + END_VAR + + SUPER.Run(inParent); + + IF ActivateManualControl THEN + // + VisionProNet.ActivateManualControl(); + // + END_IF; + + // + // AxoVisionProNet talks to the Vision PC over TCP via its .NET twin. + // The socket is opened from Program.cs (InitializeVisionClientAsync); + // here we only configure timeouts and run the component cyclically. + VisionProNet.Config.InfoTime := LTIME#5S; + VisionProNet.Config.ErrorTime := LTIME#10S; + VisionProNet.Config.TaskTimeout := LTIME#50S; + VisionProNet.Run(inParent := THIS); + // + + THIS.UseInSequencer(); + END_METHOD + + // + VAR PUBLIC + Sequencer : AxoSequencer; + Steps : ARRAY[0..10] OF AXOpen.Core.AxoStep; + END_VAR + + METHOD PRIVATE UseInSequencer + IF Sequencer.GetContext().OpenCycleCount() <= ULINT#10 THEN + Sequencer.SequenceMode := eAxoSequenceMode#RunOnce; + END_IF; + + Sequencer.Run(THIS); + Sequencer.Open(); + + // Restore clears all remote tasks and seeds the recipe / part + // identification carried in every Vision PC request. + IF(Steps[0].Execute(Sequencer, 'Restore and set part identification')) THEN + VisionProNet.Control.VariantId := 'VARIANT-A'; + VisionProNet.Control.PartId := 'PART-0001'; + VisionProNet.Control.TriggerId := INT#1; + VisionProNet.Restore(); + Sequencer.MoveNext(); + END_IF; + + // Select the inspection recipe / job on the Vision PC. + IF(Steps[1].Execute(Sequencer, 'Select recipe on Vision PC')) THEN + IF(VisionProNet.SetRecipe().IsDone()) THEN + Sequencer.MoveNext(); + END_IF; + END_IF; + + // Fire an inspection for the configured TriggerId / PartId. + IF(Steps[2].Execute(Sequencer, 'Trigger inspection')) THEN + IF(VisionProNet.Trigger().IsDone()) THEN + Sequencer.MoveNext(); + END_IF; + END_IF; + + // Pull the inspection verdict back from the Vision PC. + IF(Steps[3].Execute(Sequencer, 'Read inspection result')) THEN + IF(VisionProNet.InspectionResult().IsDone()) THEN + Sequencer.MoveNext(); + END_IF; + END_IF; + + // Combined path: send application-specific payload AND trigger in one + // gated task (disabled by the component while Trigger/SetRecipe run). + IF(Steps[4].Execute(Sequencer, 'Trigger with specific data')) THEN + IF(VisionProNet.TriggerWithSpecificData().IsDone()) THEN + Sequencer.MoveNext(); + END_IF; + END_IF; + + // Receive the typed result payload into the specific-data container. + IF(Steps[5].Execute(Sequencer, 'Receive specific data')) THEN + IF(VisionProNet.ReceiveSpecificData().IsDone()) THEN + Sequencer.MoveNext(); + END_IF; + END_IF; + + // Error recovery: a remote task that faulted (e.g. Vision PC offline, + // request rejected) raises HasRemoteException. Restore() clears every + // task back to idle so the sequence can be re-run. + IF(Steps[6].Execute(Sequencer, 'Error recovery')) THEN + // + IF VisionProNet.TriggerTask.HasRemoteException + OR VisionProNet.SetRecipeTask.HasRemoteException + OR VisionProNet.InspectionResultTask.HasRemoteException + OR VisionProNet.TriggerWithSpecificDataTask.HasRemoteException + OR VisionProNet.ReceiveSpecificDataTask.HasRemoteException THEN + VisionProNet.Restore(); + END_IF; + // + Sequencer.CompleteSequence(); + END_IF; + END_METHOD + // + + // Commissioning-only task. The component gates SendSpecificDataAndTypes on + // manual mode, so it is invoked from a manual-control helper rather than the + // automatic sequence. Activate manual control (ActivateManualControl) first. + METHOD PUBLIC CommissioningSendData : IAxoTaskState + // + CommissioningSendData := VisionProNet.SendSpecificDataAndTypes(); + // + END_METHOD + + END_CLASS +END_NAMESPACE diff --git a/src/showcase/app/src/components.cognex.vision/Documentation/CognexVision.st b/src/showcase/app/src/components.cognex.vision/Documentation/CognexVision.st index a9d1bbb83..c1be177b9 100644 --- a/src/showcase/app/src/components.cognex.vision/Documentation/CognexVision.st +++ b/src/showcase/app/src/components.cognex.vision/Documentation/CognexVision.st @@ -11,6 +11,7 @@ NAMESPACE AXOpen.Components.Cognex.Vision axoDataman_v_6_0_0_0_2 : AxoDataman_Secondary_Example; axoInsight_v_24_0_0 : AxoInsight_v_24_0_0_Example; axoVisionPro : AxoVisionPro_Example; + axoVisionProNet : AxoVisionProNet_Example; END_VAR METHOD PUBLIC Execute @@ -20,6 +21,7 @@ NAMESPACE AXOpen.Components.Cognex.Vision axoDataman_v_6_0_0_0_2.Run(_rootObject); axoInsight_v_24_0_0.Run(_rootObject); axoVisionPro.Run(_rootObject); + axoVisionProNet.Run(_rootObject); END_METHOD END_CLASS END_NAMESPACE \ No newline at end of file