Broadcast System — Inter-Application Communication
PSI’s Broadcast system enables real-time inter-application communication across the .NET desktop suite. When a user navigates to a Part, Work Order, or Account in one app, all other apps with broadcast receiving enabled navigate to the same record. Each app independently controls whether it sends and receives broadcasts.
Overview
| Metric | Value |
|---|---|
| Source | C:\git\PSI.All\PSI.Shared\trunk\PSI.Common\Broadcast\ |
| Framework | .NET Framework 4.8 / Raw Sockets (UDP) + Native WebSocket |
| Local Service | PSIBroadcast (Windows Service, UDP port 40000 loopback, WebSocket port 5050) |
| Global Service | PSIBroadcastGlobal (Windows Service, TCP port 30000) |
| Client Libraries | BroadcastClient (local), BroadcastGlobalClient (enterprise) |
| Web Clients | Native WebSocket on ws://localhost:5050/broadcast (JSON protocol) |
| Participating Apps | 15+ .NET desktop apps + PSI Explorer Web |
Architecture
The system is a two-tier hub-and-spoke messaging architecture:
┌──────────────────┐
│ Global Broadcast │ TCP :30000
│ Server │ (Enterprise-wide)
│ (PSIBroadcastGlobal) │
└────┬───┬───┬──────┘
│ │ │
┌────────┘ │ └────────┐
│ │ │
Machine A Machine B Machine C
┌─────────────────┐
│ Local Broadcast │
│ Server │
│ (PSIBroadcast) │
│ │
│ UDP :40000 │ ← .NET desktop apps (loopback)
│ WS :5050 │ ← Web apps (native WebSocket)
│ TCP :2001/2002 │ ← VB6 legacy apps
└──┬──┬──┬──┬─────┘
│ │ │ │
App1 App2 PSI Explorer Web VB6 App
| Tier | Transport | Scope | Server | Purpose |
|---|---|---|---|---|
| Local | UDP (loopback) | Single machine | PSIBroadcast service | Navigate .NET desktop apps on same workstation |
| Local | WebSocket | Single machine | PSIBroadcast service (port 5050) | Navigate web apps (PSI Explorer) on same workstation |
| Global | TCP | Enterprise (cross-machine) | PSIBroadcastGlobal service | Shop floor events (dispatch, operator progress) |
Packet Protocol
All messages use a custom binary packet format defined in Packet.cs:
┌──────────────┬────────────┬────────────────┬──────────┬──────────┐
│DataIdentifier│NameLength │ MessageLength │ Name │ Message │
│ 4 bytes │ 4 bytes │ 4 bytes │ variable │ variable │
└──────────────┴────────────┴────────────────┴──────────┴──────────┘
DataIdentifier Types
| Value | Name | Purpose |
|---|---|---|
| 0 | Message | Application data (part number, WO, account, etc.) |
| 1 | LogIn | Client registration with server |
| 2 | LogOut | Client disconnection |
| 3 | Ping | Keep-alive (every 3s local, every 60s global) |
| 4 | Null | Default/unused |
Message Payload Encoding
The Message field carries the business data as a delimited string:
- Local broadcast:
"{messageTypeInt}:{data}"(colon separator) - Global broadcast:
"{messageTypeInt}\t{data}"(tab separator)
For multi-line broadcasts (lists), individual items are separated by |.
Message Types
Local Broadcast (BroadcastMessageType)
Standard navigation messages between desktop apps on the same machine:
| Enum | Value | Description |
|---|---|---|
eAccountNo | 1 | Navigate to Account |
eContactNo | 2 | Navigate to Contact |
eCustomerNo | 3 | Navigate to Customer |
eEmployeeNo | 4 | Navigate to Employee |
ePartNo | 5 | Navigate to Part |
ePO | 6 | Navigate to Purchase Order |
ePOLine | 7 | Navigate to PO Line |
eProjectNo | 8 | Navigate to Project |
eQuoteParaNo | 9 | Navigate to Quote |
eSONo | 10 | Navigate to Sales Order |
eSPN | 11 | Navigate to SPN |
eVendorNo | 12 | Navigate to Vendor |
eWONo | 13 | Navigate to Work Order |
eIMLoaded | 14 | Program launch notification |
eInvoice | 15 | Navigate to Invoice |
ePDMIDList | 16 | PDM ID list |
ePDMOpenPath | 17 | Open PDM path |
eRFQ | 114 | Navigate to RFQ |
ePurchaseRequest | 115 | Navigate to Purchase Request |
eMailFromVirtual | 1997 | Virtual mail notification |
List variants (101-113) exist for multi-select broadcasts (e.g., ePartNoList = 105).
Global Broadcast (BroadcastGlobalMessageType)
Enterprise-wide events across machines (shop floor/MES):
| Enum | Value | Description |
|---|---|---|
OperatorProgress | 1 | Operator status update |
WorkOrderNote | 2 | WO note changed |
LineNote | 3 | Line note changed |
IssueLocation | 4 | Issue location changed |
FinishLocation | 5 | Finish location changed |
FocusPriority | 6 | Priority changed |
DispatchListPublished | 7 | Dispatch list published |
Send/Receive Toggle — The Key Feature
Every application independently controls two directions of broadcast participation using static boolean flags:
// BroadcastGlobalClient.cs, lines 524-525
public static bool IsBroadCastSend = false; // Controls outbound messages
public static bool IsBroadCastReceive = false; // Controls inbound messagesBoth default to false — users must explicitly opt-in via toolbar toggles.
UI Controls
The PSI toolbar provides two toggle buttons bound to ViewModel properties:
| Button | Icon On | Icon Off | Controls |
|---|---|---|---|
cmdSend | broadcast_send_on.png | broadcast_send_off.png | IsBroadCastSend |
cmdReceive | broadcast_receive_on.png | broadcast_receive_off.png | IsBroadCastReceive |
Icons are in PSI.Shared\trunk\PSI.Controls\Resources\.
ViewModel Binding
Each app’s ViewModel exposes the toggles as bindable properties:
// BaseViewModel.cs
public bool IsBroadcastReceive
{
get { return isBroadcastReceive; }
set
{
BroadcastGlobalClient.IsBroadCastReceive = isBroadcastReceive = value;
RaisePropertyChanged("IsBroadcastReceive");
}
}
public bool IsBroadcastSend
{
get { return isBroadcastSend; }
set
{
BroadcastGlobalClient.IsBroadCastSend = isBroadcastSend = value;
RaisePropertyChanged("IsBroadcastSend");
}
}Message Flow
Sending
User navigates to Part "ABC-123" in BOM Manager
│
▼
BroadcastGlobalClient.ExecuteBroadcastPartCommand("ABC-123")
│
├── Check: IsBroadCastSend == true? ── No ──> (nothing sent)
│
▼ Yes
SendBroadcast(ePartNo, "ABC-123")
│
├── Temporarily disable IsBroadCastReceive (prevent self-echo)
│
▼
BroadcastClient.SendMessage(ePartNo, "ABC-123", false, ViewModelGUID)
│
├── Create Packet: DataIdentifier=Message, Message="5:ABC-123"
├── Serialize to byte[]
├── UDP SendTo server (localhost:40000)
│
▼
Internal MVVM Messenger.Default.Send(BroadcastReceivedMessage)
│── (so other ViewModels in the SAME app can also react)
│
▼
Restore IsBroadCastReceive to previous value
Server Fan-Out
When a message arrives from any source (UDP, WebSocket, or VB6), the server fans it out to all other client types:
Message arrives (from any source)
│
├── Deserialize / parse
│
▼
Fan out to all client types:
│
├── SendToLoopBackClients() → UDP to all loopback clients (port 40000)
├── SendToClients() → UDP to all remote clients (port 30000, legacy)
├── ForwardToWebSocketClients() → JSON to all WebSocket clients (port 5050)
└── SendToVB6() → TCP to VB6 legacy ports (2001/2002)
This means a click in PSI Explorer (WebSocket) navigates ProViewer (.NET/UDP), and vice versa.
Receiving
App2's socket receives UDP data
│
▼
ReceiveData callback fires
│
├── Deserialize Packet
├── Parse message: "5:ABC-123" → type=ePartNo, data="ABC-123"
│
▼
Messenger.Default.Send(new BroadcastReceivedMessage {
MessageType = ePartNo,
Message = "ABC-123",
IsMultiLineBroadcast = false
})
│
▼
ViewModel.ExecuteBroadcastReceivedMessage()
│
├── Check: IsBroadcastReceive == true? ── No ──> (ignored)
├── Check: message.GUID == ViewModelGUID? ── Yes ──> (ignore own message)
│
▼ Pass both checks
Navigate to Part "ABC-123"
Connection Lifecycle
Local Broadcast Client
- Connect:
BroadcastClient.Connect(clientName)creates a UDP socket tolocalhost:40000 - LogIn: Sends
DataIdentifier.LogInpacket with client name - Listen:
BeginReceiveFromstarts async receive loop - Keep-alive: Background worker sends
Pingevery 3 seconds - Reconnect: If no ping response for 4+ seconds, disconnects and reconnects
- Close: Sends
DataIdentifier.LogOutpacket, closes socket
Global Broadcast Client
- Connect:
BroadcastGlobalClient.Connect(clientName)creates a TCP socket to configured host on port 30000 - Production only: Global broadcast only runs when
EnvironmentConstants.Environment == Production - Listen:
BeginReceivestarts async receive loop - Keep-alive: Background worker sends
Pingevery 60 seconds - Timeout: If no ping response for 90+ seconds, disconnects and attempts reconnection
- Retry: Up to 2 connection attempts on failure
Server Client Tracking
The server maintains separate client lists per transport:
// UDP clients (both remote and loopback)
private struct Client
{
public Guid guid; // Unique client ID
public EndPoint endPoint; // Socket endpoint for sending
public string name; // Client name (app identifier)
public DateTime lastPing; // Last activity timestamp
}
private List<Client> clientList; // Remote clients (port 30000, legacy)
private List<Client> loopClientList; // Loopback clients (port 40000)WebSocket clients are tracked separately in BroadcastWebSocketListener using a ConcurrentDictionary<string, WsClient> with independent send/receive toggle state per connection.
Server Keep-Alive and Stale Cleanup
The server’s background worker runs every 3 seconds:
| Client Type | Ping | Stale Timeout | Cleanup |
|---|---|---|---|
| Remote (port 30000) | Server sends Ping every 3s | 120 seconds | Removed from list |
| Loopback (port 40000) | Server sends Ping every 3s | 600 seconds (10 min) | Removed from list |
| WebSocket (port 5050) | WebSocket protocol keep-alive | On disconnect | Removed from dictionary |
Important: The server must ping loopback clients. Without server pings, the
BroadcastClientreconnect worker detects a staleLastPingafter 4 seconds and enters a reconnect cycle — destroying and recreating its socket every ~6 seconds. Messages sent to the old (dead) port during the gap are lost.
Configuration
Local Broadcast
| Setting | Source | Default |
|---|---|---|
| Server host | {LiveDataPath}\APPS\MES\SupportFiles\Broadcast\{MachineName}client.txt | localhost (loopback) |
| Client port | Hardcoded | 40000 (UDP) |
| WebSocket port | Hardcoded | 5050 (HTTP/WS) |
| EnableWebSocket | App.config appSettings | false |
| Ping interval | Hardcoded | 3 seconds |
| Ping timeout | Hardcoded | 4 seconds |
| Loopback stale timeout | Hardcoded | 600 seconds (10 min) |
To enable native WebSocket support, set in the service’s App.config:
<appSettings>
<add key="EnableWebSocket" value="true" />
</appSettings>Global Broadcast
| Setting | Source | Default |
|---|---|---|
| Server host | EnvironmentConstants.GlobalBroadcastHost (from connection map) | Configured per environment |
| Server port | Hardcoded | 30000 (TCP) |
| Ping interval | Hardcoded | 60 seconds |
| Ping timeout | Hardcoded | 90 seconds |
| Environment gate | EnvironmentConstants.Environment | Production only |
Remote Host Resolution
The local broadcast server resolves a remote counterpart for cross-machine forwarding:
- Checks
{LiveDataPath}\APPS\MES\SupportFiles\Broadcast\{MachineName}.txtfor explicit host - Falls back to
{MachineName}v(virtual machine naming convention)
Key Source Files
| File | Path | Purpose |
|---|---|---|
Packet.cs | PSI.Common\Broadcast\Packet.cs | Binary packet format and serialization |
BroadcastClient.cs | PSI.Common\Broadcast\BroadcastClient.cs | Local UDP client (with auto-reconnect worker) |
BroadcastGlobalClient.cs | PSI.Common\Broadcast\BroadcastGlobalClient.cs | Global TCP client + send/receive toggles + command methods |
BraodcastUDPServer.cs | PSI.Common\Broadcast\BraodcastUDPServer.cs | Local server: UDP + WebSocket + VB6 fan-out |
BroadcastWebSocketListener.cs | PSI.Common\Broadcast\BroadcastWebSocketListener.cs | Native WebSocket listener (HttpListener on port 5050) |
BraodcastUDPGlobalServer.cs | PSI.Common\Broadcast\BraodcastUDPGlobalServer.cs | Global TCP server (Windows Service) |
BroadcastEnum.cs | PSI.Common\Enum\BroadcastEnum.cs | Local message type enum |
BroadcastGlobalEnum.cs | PSI.Common\Enum\BroadcastGlobalEnum.cs | Global message type enum |
BroadcastReceivedMessage.cs | PSI.Common\Broadcast\ | MVVM message class (local) |
BroadcastGlobalReceivedMessage.cs | PSI.Common\Broadcast\ | MVVM message class (global) |
PSIToolbar.Designer.cs | PSI.Controls\ | Toolbar toggle buttons |
BaseViewModel.cs | Various apps | Send/receive property binding |
deploy-local.ps1 | PSI.BroadCast.Service\ | Local deployment script (stops Health + Broadcast, copies binaries, restarts) |
Note: The server filenames contain a typo (
Braodcastinstead ofBroadcast). This is historical and should not be “fixed” without renaming all references.
VB6 Legacy Support
The local broadcast server maintains backward compatibility with legacy VB6 applications:
- Listens on a separate VB6 socket
- Forwards messages to ports 2001/2002 for VB6 clients
- Message format is adapted for VB6 string parsing
Web App Integration
The Broadcast system was designed for .NET desktop apps using raw sockets. Browsers cannot open raw UDP/TCP connections, so web app support was added in phases. Phase 3 (native WebSocket) is the current architecture.
Phase 3: Native WebSocket in Broadcast Server — CURRENT
Status: Implemented and deployed. Replaces the Phase 1 WebBridge.
Feature flag: EnableWebSocket=true in service App.config
Desktop Apps (.NET) Web Apps (Browser)
│ │
│ UDP :40000 │ ws://localhost:5050/broadcast
│ Binary Packet │ JSON messages
│ │
▼ ▼
┌──────────────────────────────────┐
│ PSIBroadcast Service │
│ │
│ BraodcastUDPServer │
│ ├── loopSocket (UDP :40000) │ ← .NET desktop apps
│ ├── BroadcastWebSocketListener│ ← Web apps (port 5050)
│ └── vb6Socket (TCP :2001) │ ← VB6 legacy
│ │
│ Fan-out: every message from any │
│ source goes to ALL other clients│
└──────────────────────────────────┘
How it works:
BroadcastWebSocketListener(in PSI.Common) runs anHttpListeneron port 5050- Web apps connect via
ws://localhost:5050/broadcast - Each WebSocket connection has independent
isSending/isReceivingstate - Messages fan out to all client types: UDP loopback, WebSocket, VB6
- Same JSON protocol as the original WebBridge — zero changes needed in web apps
- Max 50 concurrent WebSocket clients, 64KB max message size
JSON Protocol (browser ↔ server):
| Direction | Action | Example |
|---|---|---|
| Browser → Server | Send broadcast | { "action": "broadcast", "type": "ePartNo", "data": "359920" } |
| Browser → Server | Toggle send | { "action": "setSend", "enabled": true } |
| Browser → Server | Toggle receive | { "action": "setReceive", "enabled": true } |
| Server → Browser | Received broadcast | { "action": "received", "type": "ePartNo", "data": "359920" } |
| Server → Browser | Status update | { "action": "status", "connected": true, "sending": false, "receiving": false } |
PSI Explorer Integration — LIVE
PSI Explorer Web (psi-explorer-web repo) connects directly to the native WebSocket endpoint. The app header includes Send/Receive toggle buttons and a connection status indicator.
| Action | Broadcast Behavior |
|---|---|
| Click a part in BOM tree/table | Sends ePartNo to .NET apps (when Send is ON) |
Receive ePartNo from .NET app | Switches to BOM page, searches by part number |
Receive eWONo from .NET app | Switches to BOM page, searches by job number |
Receive eProjectNo from .NET app | Switches to Projects page |
Receive eAccountNo/eCustomerNo/eVendorNo/eContactNo | Switches to Accounts page |
Header controls:
[PSI Tools] [Projects Home] [PSI Explorer] [Accounts] [↑ Send] [↓ Recv] [●] Connected to UniData
React integration:
import { useBroadcast } from './useBroadcast';
function MyComponent() {
const { isConnected, messages, setSending, setReceiving, send } = useBroadcast();
useEffect(() => { setReceiving(true); }, [setReceiving]);
const latest = messages[messages.length - 1];
// React to incoming broadcasts...
}Phase 1: WebSocket Bridge (MVP) — SUPERSEDED
Note: Phase 1 (PSI.Broadcast.WebBridge) has been superseded by Phase 3. The standalone .NET 8 bridge at
PSI.Broadcast.WebBridge\is no longer needed — the broadcast server speaks WebSocket natively. The bridge code remains in the repo for reference but should not be deployed.
The bridge was a separate .NET 8 console app that registered as a UDP broadcast client and translated messages to/from WebSocket JSON. Phase 3 eliminates this middleware by building WebSocket support directly into the server.
Future: SignalR Hub
For web apps with an ASP.NET Core backend that need server-side broadcast integration:
Browser ←──SignalR──→ ASP.NET Hub ←──UDP/TCP──→ Broadcast Server
This remains a future option if needed for centralized web deployments, but the native WebSocket approach covers the current use cases.
Design Principle
The core feature is preserved: send and receive are independent toggles per client. Web app UI presents the same two toggle buttons (Send On/Off, Receive On/Off), and the server respects them before forwarding messages in either direction.
Reliability Notes
Several reliability issues were identified and fixed during the Phase 3 implementation:
ICMP Port-Unreachable Poisoning
Problem: On Windows, sending a UDP packet to a closed port triggers an ICMP port-unreachable response. This causes the next EndReceiveFrom on the same socket to throw SocketException: An existing connection was forcibly closed by the remote host, even though UDP is connectionless.
Fix: Apply SIO_UDP_CONNRESET to both UDP sockets at startup:
const int SIO_UDP_CONNRESET = -1744830452;
socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0 }, null);Reconnect Storm from Missing Loopback Pings
Problem: The BroadcastClient reconnect worker checks LastPing every few seconds. If no Ping packet is received from the server within 4 seconds, it destroys the socket and reconnects with a new ephemeral port. The server was pinging remote clients (port 30000) but not loopback clients (port 40000), so loopback clients entered a perpetual reconnect cycle (~6 second period). This caused:
- ~60% message loss (messages sent to dead ports during reconnect gap)
- Unbounded growth of
loopClientList(zombie entries never cleaned up)
Fix: Server now calls SendLoopPing() for every loopback client in the same 3-second worker loop that pings remote clients. Loopback stale cleanup uses a generous 10-minute timeout to avoid evicting idle-but-alive clients.
Per-Callback Receive Buffers
Problem: The original server used a single shared byte[] dataStream across all async receive callbacks. Under load, two callbacks could process the same buffer concurrently, causing corrupted packets.
Fix: Each BeginReceiveFrom allocates its own buffer, passed via AsyncState. The callback reads from its own buffer, never a shared field.
Deployment
Local Development Deployment
Use the deploy-local.ps1 script in the Broadcast Service project:
# Run as Administrator
.\deploy-local.ps1The script:
- Stops
PSIHealthservice (prevents auto-restart of broadcast during deploy) - Stops
PSIBroadcastservice and waits for process exit (up to 30s, then force kills) - Copies updated
PSI.Common.dll,PSI.Broadcast.Service.exe, and config to install directory - Restarts both services
Service Paths
| Service | Install Path | Process Name |
|---|---|---|
PSIBroadcast | C:\Program Files (x86)\Progressive Surface\PSI.Broadcast\ | PSI.Broadcast.Service |
PSIHealth | C:\Program Files (x86)\Progressive Surface\PSI.Health\ | PSI.HealthMonitor.Service |
Important: The PSIHealth service monitors PSIBroadcast and auto-restarts it if it goes down. Always stop PSIHealth first when deploying broadcast updates.
Debug Logging
The service uses log4net. To enable debug file logging, add a RollingFileAppender to the service config:
<log4net>
<root>
<level value="DEBUG" />
<appender-ref ref="RollingFileAppender" />
</root>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="C:\temp\broadcast-service.log" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="3" />
<maximumFileSize value="10MB" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %message%newline%exception" />
</layout>
</appender>
</log4net>Key debug log prefixes:
[Loopback] LogIn:— Client registered on port 40000[Loopback] SendToLoopBackClients:— Message fan-out with client count[WebSocket] Client ... broadcasting:— WebSocket message received[WebSocket] Client ... connected/disconnected— WebSocket lifecycle
Related Pages
- PSI.All Architecture — Overall .NET codebase structure
- Applications — PSI application inventory
- Terminology — PSI-specific terms
Last updated: February 25, 2026 Source: Analysis of PSI.Common.Broadcast namespace in PSI.All codebase