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

MetricValue
SourceC:\git\PSI.All\PSI.Shared\trunk\PSI.Common\Broadcast\
Framework.NET Framework 4.8 / Raw Sockets (UDP) + Native WebSocket
Local ServicePSIBroadcast (Windows Service, UDP port 40000 loopback, WebSocket port 5050)
Global ServicePSIBroadcastGlobal (Windows Service, TCP port 30000)
Client LibrariesBroadcastClient (local), BroadcastGlobalClient (enterprise)
Web ClientsNative WebSocket on ws://localhost:5050/broadcast (JSON protocol)
Participating Apps15+ .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
TierTransportScopeServerPurpose
LocalUDP (loopback)Single machinePSIBroadcast serviceNavigate .NET desktop apps on same workstation
LocalWebSocketSingle machinePSIBroadcast service (port 5050)Navigate web apps (PSI Explorer) on same workstation
GlobalTCPEnterprise (cross-machine)PSIBroadcastGlobal serviceShop 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

ValueNamePurpose
0MessageApplication data (part number, WO, account, etc.)
1LogInClient registration with server
2LogOutClient disconnection
3PingKeep-alive (every 3s local, every 60s global)
4NullDefault/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:

EnumValueDescription
eAccountNo1Navigate to Account
eContactNo2Navigate to Contact
eCustomerNo3Navigate to Customer
eEmployeeNo4Navigate to Employee
ePartNo5Navigate to Part
ePO6Navigate to Purchase Order
ePOLine7Navigate to PO Line
eProjectNo8Navigate to Project
eQuoteParaNo9Navigate to Quote
eSONo10Navigate to Sales Order
eSPN11Navigate to SPN
eVendorNo12Navigate to Vendor
eWONo13Navigate to Work Order
eIMLoaded14Program launch notification
eInvoice15Navigate to Invoice
ePDMIDList16PDM ID list
ePDMOpenPath17Open PDM path
eRFQ114Navigate to RFQ
ePurchaseRequest115Navigate to Purchase Request
eMailFromVirtual1997Virtual 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):

EnumValueDescription
OperatorProgress1Operator status update
WorkOrderNote2WO note changed
LineNote3Line note changed
IssueLocation4Issue location changed
FinishLocation5Finish location changed
FocusPriority6Priority changed
DispatchListPublished7Dispatch 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 messages

Both default to false — users must explicitly opt-in via toolbar toggles.

UI Controls

The PSI toolbar provides two toggle buttons bound to ViewModel properties:

ButtonIcon OnIcon OffControls
cmdSendbroadcast_send_on.pngbroadcast_send_off.pngIsBroadCastSend
cmdReceivebroadcast_receive_on.pngbroadcast_receive_off.pngIsBroadCastReceive

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

  1. Connect: BroadcastClient.Connect(clientName) creates a UDP socket to localhost:40000
  2. LogIn: Sends DataIdentifier.LogIn packet with client name
  3. Listen: BeginReceiveFrom starts async receive loop
  4. Keep-alive: Background worker sends Ping every 3 seconds
  5. Reconnect: If no ping response for 4+ seconds, disconnects and reconnects
  6. Close: Sends DataIdentifier.LogOut packet, closes socket

Global Broadcast Client

  1. Connect: BroadcastGlobalClient.Connect(clientName) creates a TCP socket to configured host on port 30000
  2. Production only: Global broadcast only runs when EnvironmentConstants.Environment == Production
  3. Listen: BeginReceive starts async receive loop
  4. Keep-alive: Background worker sends Ping every 60 seconds
  5. Timeout: If no ping response for 90+ seconds, disconnects and attempts reconnection
  6. 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 TypePingStale TimeoutCleanup
Remote (port 30000)Server sends Ping every 3s120 secondsRemoved from list
Loopback (port 40000)Server sends Ping every 3s600 seconds (10 min)Removed from list
WebSocket (port 5050)WebSocket protocol keep-aliveOn disconnectRemoved from dictionary

Important: The server must ping loopback clients. Without server pings, the BroadcastClient reconnect worker detects a stale LastPing after 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

SettingSourceDefault
Server host{LiveDataPath}\APPS\MES\SupportFiles\Broadcast\{MachineName}client.txtlocalhost (loopback)
Client portHardcoded40000 (UDP)
WebSocket portHardcoded5050 (HTTP/WS)
EnableWebSocketApp.config appSettingsfalse
Ping intervalHardcoded3 seconds
Ping timeoutHardcoded4 seconds
Loopback stale timeoutHardcoded600 seconds (10 min)

To enable native WebSocket support, set in the service’s App.config:

<appSettings>
  <add key="EnableWebSocket" value="true" />
</appSettings>

Global Broadcast

SettingSourceDefault
Server hostEnvironmentConstants.GlobalBroadcastHost (from connection map)Configured per environment
Server portHardcoded30000 (TCP)
Ping intervalHardcoded60 seconds
Ping timeoutHardcoded90 seconds
Environment gateEnvironmentConstants.EnvironmentProduction only

Remote Host Resolution

The local broadcast server resolves a remote counterpart for cross-machine forwarding:

  • Checks {LiveDataPath}\APPS\MES\SupportFiles\Broadcast\{MachineName}.txt for explicit host
  • Falls back to {MachineName}v (virtual machine naming convention)

Key Source Files

FilePathPurpose
Packet.csPSI.Common\Broadcast\Packet.csBinary packet format and serialization
BroadcastClient.csPSI.Common\Broadcast\BroadcastClient.csLocal UDP client (with auto-reconnect worker)
BroadcastGlobalClient.csPSI.Common\Broadcast\BroadcastGlobalClient.csGlobal TCP client + send/receive toggles + command methods
BraodcastUDPServer.csPSI.Common\Broadcast\BraodcastUDPServer.csLocal server: UDP + WebSocket + VB6 fan-out
BroadcastWebSocketListener.csPSI.Common\Broadcast\BroadcastWebSocketListener.csNative WebSocket listener (HttpListener on port 5050)
BraodcastUDPGlobalServer.csPSI.Common\Broadcast\BraodcastUDPGlobalServer.csGlobal TCP server (Windows Service)
BroadcastEnum.csPSI.Common\Enum\BroadcastEnum.csLocal message type enum
BroadcastGlobalEnum.csPSI.Common\Enum\BroadcastGlobalEnum.csGlobal message type enum
BroadcastReceivedMessage.csPSI.Common\Broadcast\MVVM message class (local)
BroadcastGlobalReceivedMessage.csPSI.Common\Broadcast\MVVM message class (global)
PSIToolbar.Designer.csPSI.Controls\Toolbar toggle buttons
BaseViewModel.csVarious appsSend/receive property binding
deploy-local.ps1PSI.BroadCast.Service\Local deployment script (stops Health + Broadcast, copies binaries, restarts)

Note: The server filenames contain a typo (Braodcast instead of Broadcast). 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 an HttpListener on port 5050
  • Web apps connect via ws://localhost:5050/broadcast
  • Each WebSocket connection has independent isSending / isReceiving state
  • 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):

DirectionActionExample
Browser → ServerSend broadcast{ "action": "broadcast", "type": "ePartNo", "data": "359920" }
Browser → ServerToggle send{ "action": "setSend", "enabled": true }
Browser → ServerToggle receive{ "action": "setReceive", "enabled": true }
Server → BrowserReceived broadcast{ "action": "received", "type": "ePartNo", "data": "359920" }
Server → BrowserStatus 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.

ActionBroadcast Behavior
Click a part in BOM tree/tableSends ePartNo to .NET apps (when Send is ON)
Receive ePartNo from .NET appSwitches to BOM page, searches by part number
Receive eWONo from .NET appSwitches to BOM page, searches by job number
Receive eProjectNo from .NET appSwitches to Projects page
Receive eAccountNo/eCustomerNo/eVendorNo/eContactNoSwitches 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.ps1

The script:

  1. Stops PSIHealth service (prevents auto-restart of broadcast during deploy)
  2. Stops PSIBroadcast service and waits for process exit (up to 30s, then force kills)
  3. Copies updated PSI.Common.dll, PSI.Broadcast.Service.exe, and config to install directory
  4. Restarts both services

Service Paths

ServiceInstall PathProcess Name
PSIBroadcastC:\Program Files (x86)\Progressive Surface\PSI.Broadcast\PSI.Broadcast.Service
PSIHealthC:\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


Last updated: February 25, 2026 Source: Analysis of PSI.Common.Broadcast namespace in PSI.All codebase