Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This file is the persistent repo-level handoff context for future Copilot sessio
- Machine tab station cards enhanced with task-level visuals + tiny animated processing glyphs
- Gateway mode indicator in HMI (Local vs Remote TwinCAT mock)
- Auto-scrolling execution logger panel and speed control slider
- Embedded WebView2 tab for prime mover and machine web HMIs

## Core Runtime Files
- Engine: `Services/XTSSimulationEngine.cs`
Expand Down
7 changes: 7 additions & 0 deletions App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@

<!-- Applied only when remote mock is enabled -->
<sys:Int32 x:Key="RemoteTwinCatMachineGatewayMockLatencyMs">40</sys:Int32>

<!-- Embedded web HMI launch URLs (override with XTS_PRIME_MOVER_HMI_URL / XTS_MACHINE_{0-3}_HMI_URL env vars) -->
<sys:String x:Key="PrimeMoverHmiUrl">http://localhost:3000</sys:String>
<sys:String x:Key="Machine0HmiUrl">http://localhost:3001/?machineId=0</sys:String>
<sys:String x:Key="Machine1HmiUrl">http://localhost:3002/?machineId=1</sys:String>
<sys:String x:Key="Machine2HmiUrl">http://localhost:3003/?machineId=2</sys:String>
<sys:String x:Key="Machine3HmiUrl">http://localhost:3004/?machineId=3</sys:String>
</Application.Resources>
</Application>
48 changes: 48 additions & 0 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XTSPrimeMoverProject"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
Title="Beckhoff XTS Prime Mover System" Height="900" Width="1400"
Background="#1E1E1E">
Expand Down Expand Up @@ -1065,6 +1066,53 @@
ContentTemplate="{StaticResource MachineDetailTemplate}"/>
</TabItem>

<TabItem Header="Embedded Web HMIs">
<Grid Background="#1A1A1C" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<Border Background="#252526" BorderBrush="#3F3F46" BorderThickness="1" CornerRadius="6" Padding="12" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="WEB HMI MIRROR" Foreground="#9CDCFE" FontSize="13" FontWeight="Bold"/>
<TextBlock Text="Loads the React HMIs inside the WPF desktop shell so the same web screens can be viewed in-browser and on the desktop HMI." Foreground="#CCCCCC" FontSize="11" Margin="0,6,0,0" TextWrapping="Wrap"/>
</StackPanel>
</Border>

<TabControl Grid.Row="1"
Style="{StaticResource DarkTabControlStyle}"
ItemContainerStyle="{StaticResource DarkTabItemStyle}"
ItemsSource="{Binding EmbeddedWebHmiScreens, Mode=OneWay}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName, Mode=OneWay}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<Border Background="#252526" BorderBrush="#3F3F46" BorderThickness="1" CornerRadius="6" Padding="10" Margin="0,0,0,8">
<StackPanel>
<TextBlock Text="{Binding Description, Mode=OneWay}" Foreground="#DCDCAA" FontSize="11" TextWrapping="Wrap"/>
<TextBlock Text="{Binding UrlDisplay, Mode=OneWay}" Foreground="#9CDCFE" FontSize="10" Margin="0,4,0,0" TextWrapping="Wrap"/>
</StackPanel>
</Border>

<Border Grid.Row="1" Background="#111827" BorderBrush="#3F3F46" BorderThickness="1" CornerRadius="6" ClipToBounds="True">
<wv2:WebView2 Source="{Binding SourceUri, Mode=OneWay}"/>
</Border>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</TabItem>
<TabItem Header="DB Tables Viewer">
<Grid Margin="10">
<Grid.RowDefinitions>
Expand Down
117 changes: 103 additions & 14 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Text;
Expand All @@ -22,30 +23,67 @@ namespace XTSPrimeMoverProject
public partial class MainWindow : Window
{
private Services.XTSSimulationEngine? _engine;

private Services.RemoteRestGateway? _remoteGateway;

/// <summary>
/// Gateway mode is selected by the environment variable XTS_GATEWAY_MODE:
/// "local" — in-process engine (original behaviour, default)
/// "remote-mock" — local engine wrapped in RemoteTwinCatMachineGatewayMock (adds latency)
/// "remote" — connects to the Temporal/K8s Prime Mover API via REST + SignalR
/// The URL for remote mode is read from XTS_PRIME_MOVER_API_URL or defaults to http://localhost:8082.
/// </summary>
public MainWindow()
{
InitializeComponent();

try
{
var engine = new Services.XTSSimulationEngine();
_engine = engine;
var localGateway = new Services.LocalSimulationServiceGateway(engine);
string gatewayMode = ReadAppStringSetting("GatewayMode",
Environment.GetEnvironmentVariable("XTS_GATEWAY_MODE") ?? "local");

Services.IMachineGatewayService machineGateway;
Services.IDataGatewayService dataGateway;
string gatewayModeStatus;

if (gatewayMode.Equals("remote", StringComparison.OrdinalIgnoreCase))
{
// ── Remote mode: WPF becomes a thick REST/SignalR client ──────────
string apiUrl = ReadAppStringSetting("PrimeMoverApiUrl",
Environment.GetEnvironmentVariable("XTS_PRIME_MOVER_API_URL") ?? "http://localhost:8082");

var remoteGateway = new Services.RemoteRestGateway(apiUrl);
_remoteGateway = remoteGateway;
machineGateway = remoteGateway;
dataGateway = remoteGateway;
gatewayModeStatus = $"Machine Gateway: Remote Temporal/K8s — {apiUrl}";
}
else
{
// ── Local mode: in-process engine (original behaviour) ────────────
var engine = new Services.XTSSimulationEngine();
_engine = engine;
var localGateway = new Services.LocalSimulationServiceGateway(engine);

bool useRemoteMock = ReadAppBoolSetting("UseRemoteTwinCatMachineGatewayMock", defaultValue: true);
int latencyMs = ReadAppIntSetting("RemoteTwinCatMachineGatewayMockLatencyMs", defaultValue: 40);
bool useRemoteMock = gatewayMode.Equals("remote-mock", StringComparison.OrdinalIgnoreCase)
|| ReadAppBoolSetting("UseRemoteTwinCatMachineGatewayMock", defaultValue: true);
int latencyMs = ReadAppIntSetting("RemoteTwinCatMachineGatewayMockLatencyMs", defaultValue: 40);

Services.IMachineGatewayService machineGateway = useRemoteMock
? new RemoteTwinCatMachineGatewayMock(localGateway, commandLatencyMs: latencyMs)
: localGateway;
machineGateway = useRemoteMock
? new RemoteTwinCatMachineGatewayMock(localGateway, commandLatencyMs: latencyMs)
: localGateway;

string gatewayModeStatus = useRemoteMock
? $"Machine Gateway: Remote TwinCAT Mock ({latencyMs} ms)"
: "Machine Gateway: Local In-Process";
gatewayModeStatus = useRemoteMock
? $"Machine Gateway: Remote TwinCAT Mock ({latencyMs} ms)"
: "Machine Gateway: Local In-Process";

var dataGateway = (Services.IDataGatewayService)localGateway;
var viewModel = new MainViewModel(machineGateway, dataGateway, gatewayModeStatus);
dataGateway = localGateway;
}

var viewModel = new MainViewModel(
machineGateway,
dataGateway,
gatewayModeStatus,
CreateEmbeddedWebHmiScreens());
DataContext = viewModel;

viewModel.ExecutionLogs.CollectionChanged += OnExecutionLogsCollectionChanged;
Expand All @@ -70,6 +108,8 @@ private void OnWindowClosed(object? sender, EventArgs e)
{
_engine?.Dispose();
_engine = null;
_remoteGateway?.Dispose();
_remoteGateway = null;
}

private void OnExecutionLogsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
Expand Down Expand Up @@ -105,6 +145,55 @@ private static int ReadAppIntSetting(string key, int defaultValue)

return defaultValue;
}

private static string ReadAppStringSetting(string key, string defaultValue)
{
if (Application.Current?.Resources[key] is string value && !string.IsNullOrEmpty(value))
{
return value;
}

return defaultValue;
}

private static IReadOnlyList<EmbeddedWebHmiScreenViewModel> CreateEmbeddedWebHmiScreens()
{
var screens = new List<EmbeddedWebHmiScreenViewModel>();

AddWebHmiScreen(
screens,
"Prime Mover Web HMI",
"Embedded browser mirror of the prime mover web control room.",
ReadAppStringSetting(
"PrimeMoverHmiUrl",
Environment.GetEnvironmentVariable("XTS_PRIME_MOVER_HMI_URL") ?? "http://localhost:3000"));

for (int machineId = 0; machineId < 4; machineId++)
{
AddWebHmiScreen(
screens,
$"Machine M{machineId} Web HMI",
$"Embedded browser mirror of the machine M{machineId} web HMI.",
ReadAppStringSetting(
$"Machine{machineId}HmiUrl",
Environment.GetEnvironmentVariable($"XTS_MACHINE_{machineId}_HMI_URL")
?? $"http://localhost:{3001 + machineId}/?machineId={machineId}"));
}

return screens;
}

private static void AddWebHmiScreen(
ICollection<EmbeddedWebHmiScreenViewModel> screens,
string displayName,
string description,
string url)
{
if (Uri.TryCreate(url, UriKind.Absolute, out var sourceUri))
{
screens.Add(new EmbeddedWebHmiScreenViewModel(displayName, description, sourceUri));
}
}
}

public class MoverPositionConverter : IMultiValueConverter
Expand Down
115 changes: 114 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Tables currently used:
- `ErrorLogs`
- `Alarms`

## Build / Run
## Build / Run (Desktop WPF Simulation)

1. Open solution in Visual Studio 2026+.
2. Restore/build (`Debug | Any CPU`).
Expand All @@ -132,6 +132,119 @@ Tables currently used:
- `RESET`
- Speed slider (`0.1x` .. `5.0x`)

## Full-Stack Test Guide (Temporal + APIs + HMIs)

This verifies the distributed stack end-to-end: infrastructure, orchestration, machine services, APIs, HMIs, and observability.

### 1) Prerequisites

- Docker Desktop or Docker Engine with Compose v2
- At least 8 CPU cores / 16 GB RAM recommended
- Ports available: `3000-3004`, `5432`, `6379`, `7233`, `8080`, `8082`, `8088`, `8090-8097`, `9200`, `9999`, `16686`

### 2) Start the full stack

From repository root:

```bash
cd docker
docker compose -f docker-compose.dev.yml up -d --build
docker compose -f docker-compose.dev.yml ps
```

Expected: containers for Temporal, PostgreSQL, Redis, prime mover service/API/HMI, 4 machine service/API/HMI sets, workers, and observability are `Up`.

### 3) Smoke-check core endpoints

Open and confirm these load without errors:

- Prime mover HMI: `http://localhost:3000`
- Machine HMIs: `http://localhost:3001`, `3002`, `3003`, `3004`
- Prime mover API Swagger: `http://localhost:8082/swagger`
- Machine API Swagger examples: `http://localhost:8091/swagger`, `http://localhost:8093/swagger`
- Temporal UI: `http://localhost:8088`
- Prometheus: `http://localhost:9999`
- Grafana: `http://localhost:3100` (`admin / xts_grafana`)
- Jaeger: `http://localhost:16686`

### 4) Functional end-to-end validation

1. In Prime mover HMI, start production flow.
2. Verify parts enter, route machine-by-machine (`M0 -> M1 -> M2 -> M3 -> Exit`), and exit as Good/Bad.
3. Open each machine HMI and confirm live station progression + ET/PT style runtime changes.
4. In Temporal UI:
- Confirm active workflow execution for prime mover orchestration.
- Confirm per-part lifecycle workflow creation and completion.
- Confirm machine workflow activity transitions while parts are processed.
5. In APIs (Swagger), call read/status endpoints and verify responses update as runtime state changes.
6. Validate alarms/watchdog behavior by observing fault and recovery events in HMIs/log streams (if a stall/fault is triggered).

### 5) Data and observability validation

- Verify metrics appear in Prometheus targets and queries.
- Verify Grafana connects to Prometheus and dashboards update over time.
- Verify traces/events are visible in Jaeger for workflow/service operations.
- Verify database-backed runtime records continue updating while production runs.

### 6) Pass/Fail checklist

Pass when all are true:

- All required containers stay healthy and do not crash-loop.
- Prime mover + all machine HMIs load and show live-changing runtime state.
- Temporal workflows are created, progress, and complete without repeated failure.
- APIs remain responsive during active production.
- Observability tools (Prometheus/Grafana/Jaeger) show current runtime signals.

Fail if any service is unavailable, state is not progressing, or workflow retries/failures persist without recovery.

## Embedded Web HMIs Inside WPF

The desktop WPF HMI now includes an **Embedded Web HMIs** tab that hosts the existing React HMIs inside the desktop shell through WebView2.

- Prime mover web HMI default: `http://localhost:3000`
- Machine web HMI defaults: `http://localhost:3001` through `http://localhost:3004`
- Override launch targets with environment variables:
- `XTS_PRIME_MOVER_HMI_URL`
- `XTS_MACHINE_0_HMI_URL`
- `XTS_MACHINE_1_HMI_URL`
- `XTS_MACHINE_2_HMI_URL`
- `XTS_MACHINE_3_HMI_URL`

This keeps the backend shared while allowing the same web screens to run both in a browser and inside the .NET WPF operator client.

### 7) Stop and clean up

```bash
cd docker
docker compose -f docker-compose.dev.yml down
```

For full reset (including local volumes/data):

```bash
cd docker
docker compose -f docker-compose.dev.yml down -v
```

## Demo Runbook for Larger Audience

Use this sequence for team demos, leadership reviews, or stakeholder walkthroughs:

1. **Context (2-3 min):** Explain architecture boundaries (machine runtime, HMI runtime, data/observability).
2. **Live startup (2 min):** Show stack is already running (`docker compose ... ps`) and all major endpoints are reachable.
3. **Production flow (5-7 min):** Start line in Prime mover HMI and narrate part journey from entry to exit.
4. **Machine deep-dive (4-5 min):** Open one machine HMI and explain station-level ET/PT progression.
5. **Orchestration proof (3-4 min):** Show Temporal workflows for prime mover + parts + machines.
6. **Reliability proof (3-4 min):** Show alarms/watchdog/fault visibility and recovery behavior.
7. **Observability proof (3-4 min):** Show Prometheus metrics, Grafana dashboard updates, and Jaeger traces.
8. **Q&A ready artifacts:** Keep links/ports list, screenshots, and one short recording ready for follow-up sharing.

Recommended presenter roles for larger crowd:
- **Narrator:** explains business flow and success criteria.
- **Operator:** drives HMI interactions.
- **Observer:** watches Temporal/monitoring tabs and calls out evidence in real time.

## Continue Development on Another Laptop (Copilot-friendly)

1. Clone repo:
Expand Down
Loading