Skip to content

Edge Messaging

The gateway uses Azure Service Bus as the transport for bidirectional communication between the cloud platform and each edge gateway instance. All messaging infrastructure is defined in Voltimax.Edge.Messaging and wired up in Voltimax.Edge.Gateway.Service.

Queue Topology

Five queues are declared in MessagingExtensions.CreateTopology:

QueueMessage typeDirectionSessions
telemetryTelemetryFrameOutboundNo
gateway-heartbeatGatewayHeartbeatOutboundNo
pricing-commandsFetchPricingRequestInboundNo
gateway-commandsPolymorphic (see below)InboundYes
gateway-configurationGetGatewayConfigurationRequestInboundNo

The topology is exposed via ServiceBusTopology as a record of QueueBuilder instances. This allows the Voltimax.Iot.AppHost orchestrator to introspect all required entities when provisioning the Service Bus namespace for local development.

Session-Based Command Routing

gateway-commands is a polymorphic, session-enabled queue. Every message carries the target GatewayId as its session ID. During startup, ConfigureMessaging reads GatewayId from configuration and registers the session filter:

csharp
serviceBus
    .GatewayCommands
    .WithAcceptedSessionId(gatewayId)
    .MapMessage<PingGatewayRequest, PingGatewayHandler>()
    // ...

This ensures each gateway instance only dequeues messages addressed to it. The platform sends all command types to the single gateway-commands queue and the Service Bus session broker routes each session to the correct receiver.

Authentication

MessagingExtensions.AddMessaging supports two authentication modes, selected by the format of the ServiceBus:ConnectionString configuration value:

  • SAS connection string (contains Endpoint=): used as-is.
  • Fully-qualified namespace (URL or bare hostname emitted by Aspire / managed identity): the hostname is extracted and DefaultAzureCredential is used.

This means no code changes are needed when moving from a local Service Bus emulator (SAS) to Azure Container Apps with a managed identity.

Configuration

json
// appsettings.json
{
  "ServiceBus": {
    "Enabled": true,
    "ConnectionString": "<SAS connection string or FQNS URL>"
  }
}

When Enabled is false, no connection is established and outbound publishers silently skip sending. Inbound handlers are not registered in that case - calling AddMessaging will throw if Enabled is false.

Inbound Command Handlers

All command handlers live in Voltimax.Edge.Gateway.Service.Messaging and implement IMessageHandler<TRequest, TResponse>. Each returns a typed response that is sent back to the platform via the reply queue (cloud-replies).

Core handlers

HandlerRequestBehaviour
PingGatewayHandlerPingGatewayRequestEchoes a pong response with receive/respond timestamps.
GetGatewayModeRequestHandlerGetGatewayModeRequestReturns IGatewayModeAccessor.CurrentMode. Read-only, no side effects.
SetGatewayModeRequestHandlerSetGatewayModeRequestCalls IGatewayModeAccessor.SetMode and returns the previous mode.
GetStrategyRequestHandlerGetStrategyRequestReturns IStrategyAccessor.CurrentStrategy. Read-only.
SetStrategyRequestHandlerSetStrategyRequestCalls IStrategyAccessor.SetStrategy; the new strategy takes effect on the next ApplyStrategyJob tick.
GetBatteryStatusRequestHandlerGetBatteryStatusRequestDispatches GetBatteryStatusQuery via Mediator and maps the result to the response contract. Returns Success = false with an error message instead of throwing.
SetBatterySetpointRequestHandlerSetBatterySetpointRequestDispatches SetBatterySetpointCommand via Mediator. Rejects (returns Success = false) when the gateway is in Auto mode.
GetEnergyFlowRequestHandlerGetEnergyFlowRequestDispatches AggregateEnergyFlowQuery and maps the aggregate summary to the response.
RefreshConfigurationRequestHandlerRefreshConfigurationRequestDispatches RefreshConfigurationCommand via Mediator. Returns Success = false on failure without re-throwing.

Mode guard

Any handler that applies a physical or logical change to the system rejects incoming messages when the gateway is in Auto mode (GatewayMode.Auto):

json
{
  "Success": false,
  "ErrorMessage": "Gateway is in Auto mode; manual setpoint commands are not accepted. Switch to Manual mode first."
}

This mutual-exclusion prevents race conditions between the strategy executor and manual overrides from the platform.

Simulation handlers

Simulation-specific command handlers live in Voltimax.Edge.Gateway.Service.Simulation.Messaging and override physical asset values for testing and commissioning scenarios. They all extend SimulationHandlerBase<TRequest, TCommand, TResponse>, which provides the shared mode guard, logging, and command dispatch pattern.

HandlerRequestEffect
SetChargeStationConnectionRequestHandlerSetChargeStationConnectionRequestSimulates a charger connection state change.
SetChargeStationSetpointRequestHandlerSetChargeStationSetpointRequestOverrides the charger power setpoint.
SetGridMeterPowerRequestHandlerSetGridMeterPowerRequestOverrides the simulated grid import/export power.
SetLoadScaleRequestHandlerSetLoadScaleRequestScales simulated building load by a factor.
SetSolarIrradianceRequestHandlerSetSolarIrradianceRequestOverrides solar irradiance (simulates cloud cover). Clear by sending null.
SetSolarInverterStateRequestHandlerSetSolarInverterStateRequestSimulates the solar inverter going online or offline.

Simulation handlers also reject commands in Auto mode, consistent with the core handlers.

Outbound Publishing

Telemetry frames

TelemetryFrameServiceBusPublisherHandler listens for SnapshotReceivedNotification via the internal Mediator notification bus. On each snapshot it:

  1. Converts the metric snapshot to a TelemetryFrame (via TelemetryFrameExtensions.FromMetricValues).
  2. Calls IMessageSender<TelemetryFrame>.SendAsync to publish to the telemetry queue.

Publishing is skipped when ServiceBus:Enabled is false, so telemetry data is never silently lost during local runs without a Service Bus connection.

Configuration-change restart

ConfigurationUpdatedNotificationHandler handles ConfigurationUpdatedNotification. When the updated configuration carries RequiresRestart = true it calls IHostApplicationLifetime.StopApplication(), allowing the process manager (systemd or Kubernetes) to restart the service with the new configuration applied.

Adding a New Command

  1. Define MyRequest and MyResponse records in Voltimax.Iot.Contracts.Messaging.

  2. Register the message type on the polymorphic builder in MessagingExtensions.CreateTopology:

    csharp
    .MapMessage<MyRequest>()
  3. Create MyRequestHandler : IMessageHandler<MyRequest, MyResponse> in Voltimax.Edge.Gateway.Service.Messaging.

  4. Wire the handler in Program.ConfigureMessaging:

    csharp
    .MapMessage<MyRequest, MyRequestHandler>()

If the command is simulation-specific, extend SimulationHandlerBase instead and place the handler in Simulation/Messaging/.