Reference · for Developers
Functions SDK
The C# base class and helpers for functions that run on edge gateways, with a complete example that adds a calculated tag and sends a notification.
A function is a C# class that Proxus compiles and runs on the edge gateways you choose. It sees the values the gateway reads, can change or add to them, and can save values, call other systems and send notifications. This page describes the programming interface. How to create, build and deploy a function in the hub is in Functions.
The shape of a function
A function is one class that derives from FunctionBase and passes the three constructor arguments on unchanged. The gateway creates it once when the function is deployed and stops it when it is removed or redeployed.
using System;
using Proxus.Common;
using Proxus.Common.Messages;
using Proxus.SDK.BaseFunctions;
namespace Proxus.Functions {
// Adds a calculated tag to every reading of the case packers and warns when the motor current is high.
public sealed class PackerPowerFunction(object sys, object log, object config) : FunctionBase(sys, log, config) {
private const double CurrentLimit = 45.0; // A
private const string ChannelName = "Shift supervisors";
private DateTime _lastWarning = DateTime.MinValue;
protected override void OnStarted() {
// Receive the readings of every device on this gateway. Use device IDs instead of "*" to narrow it.
Subscriptions?.Add(new SubscriptionContext {
Type = typeof(TransportData),
Topics = ["*"]
});
// Write a heartbeat to the function log every minute.
ExecuteScheduledTask(TimeSpan.FromMinutes(1), () => LogInformation("PackerPowerFunction is running"));
base.OnStarted();
}
protected override void OnMessageReceive(FunctionContext ctx) {
if (ctx.Message is not TransportData data) return;
var deviceName = data.GetMetaDataValueByKey("DeviceName") ?? ctx.Topic;
if (!deviceName.StartsWith("LYS_PACKER_", StringComparison.Ordinal)) return;
var current = data.GetPayloadValueByName<double>("MotorCurrent");
if (current <= 0) return;
// A new tag on the same reading: it is stored and published like a tag read from the machine.
var powerKw = Math.Round(400 * current * Math.Sqrt(3) * 0.85 / 1000, 2);
data.SetPayloadValue("MotorPower", powerKw, "kW");
if (current > CurrentLimit && DateTime.UtcNow - _lastWarning > TimeSpan.FromMinutes(10)) {
_lastWarning = DateTime.UtcNow;
LogWarning($"{deviceName}: motor current {current} A is above {CurrentLimit} A");
SendNotification(ChannelName, $"{deviceName}: motor current {current} A is above {CurrentLimit} A");
}
}
}
}Rules that follow from how the gateway runs functions:
- Add your subscriptions in
OnStarted, then callbase.OnStarted(). The base method connects the subscriptions, so a subscription added after it receives nothing. OnMessageReceiveis called for each reading, before the reading is stored, sent to outbound targets or run through pipelines. A payload you add or change on theTransportDataobject goes on with the reading.- Readings that a function itself produced (
BackToPipeline,Save) are not delivered to functions again, so a function cannot loop on its own output. - Keep
OnMessageReceiveshort. Start long work (HTTP calls, database queries) as a task and do not wait for it.
Lifecycle
| Member | When it runs | What to do there |
|---|---|---|
protected virtual void OnStarted() | Once, after deployment. | Add subscriptions, start timers, then call base.OnStarted(). |
protected virtual void OnMessageReceive(FunctionContext ctx) | For each message that matches a subscription. | Your logic. |
protected virtual void OnStopping() | When the function is undeployed or replaced. | Stop timers, dispose clients, then call base.OnStopping(). |
public string? GatewayId, GatewayName | Always. | The gateway the function runs on. |
FunctionContext has Topic (for device data, the device ID) and Message (the object received, usually a TransportData).
Subscriptions
Subscriptions?.Add(new SubscriptionContext {
Type = typeof(TransportData),
Topics = ["69", "70"], // device IDs, or "*" for every device on the gateway
EdgeGateways = ["2"] // optional: also receive what gateway 2 publishes with PublishToEdgeGateways
});| Field | Meaning |
|---|---|
Type | typeof(TransportData) for device readings. typeof(GenericMessage) for messages published with PublishLocal or Publish. |
Topics | For readings: device IDs (the device's Oid), or "*". For generic messages: the message topics. |
EdgeGateways | IDs of other gateways whose PublishToEdgeGateways output this function receives. |
Reading and changing a reading
A reading is a TransportData: a list of Payload (one per tag) and a list of MetaData (facts about the reading, such as DeviceId, DeviceName, Enterprise, Site, Area, ProcessCell, EquipmentModule).
Method (namespace Proxus.Common) | What it does |
|---|---|
T? GetPayloadValueByName<T>(string payloadName) | The tag's value converted to T, or default when the tag is absent. |
object? GetPayloadValueByName(string payloadName) | The tag's value. |
string? GetMetaDataValueByKey(string key) | A metadata value, for example "DeviceName". |
void SetPayloadValue<T>(string payloadName, T? value, string unit = "") | Sets a tag, or adds it when it is not there. |
void AddPayload(string key, object? value, string? type, string unit = "") | Adds a tag. type is a data type name such as "Double". |
void AddMetaData(string key, string value) | Adds a metadata entry. |
Payload has Key, Value (text), NumericValue, Type, Unit and Time.
Sending data on
| Method | What it does |
|---|---|
Task Save(TransportData data, bool usePipeline = true) | Stores a reading of an existing device. The reading must have a numeric DeviceId metadata entry, or it is skipped with the warning Save skipped: missing or invalid DeviceId in MetaData. |
Task Save(List<Payload> payloads, string deviceName, bool usePipeline = true) | Stores values under a device name. If no device of that name exists, Proxus creates one on this gateway. |
void BackToPipeline(TransportData data) | Sends a reading back into the gateway's normal path (storage, targets, pipelines). It needs a DeviceId metadata entry, or it logs Error in BackToPipeline! DeviceId is null! Check your code. |
void PublishLocal(object message) | Publishes a message to other functions on the same gateway. |
Task PublishToEdgeGateways(TransportData data) | Publishes a reading to functions on other gateways that list this gateway in EdgeGateways. |
void PublishMqttMessage(string topic, string payload) | Publishes to an MQTT broker. |
void SubscribeToMqttTopic(string topic) | Subscribes to an MQTT topic; override HandleMqttMessage(MqttApplicationMessage message) to receive. |
The MQTT helpers connect to the broker given by the overridable properties MqttBrokerHost (default "localhost"), MqttBrokerPort (1883), MqttBrokerUser and MqttBrokerPassword (empty). Override them to reach another broker, or set MqttBrokerUser to the gateway's MasterToken to use the built-in one.
Devices, notifications and databases
| Method | What it does |
|---|---|
void SendNotification(string channelName, string message) | Sends a message through the notification channel with this name. |
Task UpdateDeviceStatusAsync(int deviceId, string status) | Sets the status text shown for a device. |
Task SwitchDeviceEnabledAsync(int deviceId, bool isEnabled) | Turns a device on or off. |
SelectStatementResult[]? ExecuteQuery(TargetType dbType, Dictionary<string, string> parameters, string query) | Runs a query on a database, given its type and the same parameters an outbound target of that type uses. TargetType.SystemDB with null parameters reads the Proxus database. |
int? ExecuteNonQuery(TargetType dbType, Dictionary<string, string> parameters, string nonQuery) | Runs a statement and returns the affected row count. |
TransportData ExecuteQueryAndTransformResult(TargetType dbType, Dictionary<string, string> parameters, string query) | Runs a query and returns the rows as payloads. |
Timers
| Method | What it does |
|---|---|
CancellationTokenSource? ExecuteScheduledTask(TimeSpan period, Action action) | Runs action every period. Cancel the returned source to stop. |
CancellationTokenSource? ExecuteScheduledTask(TimeSpan period, Action action, bool align, bool strict = false) | With align: true, runs on whole multiples of period (every full minute, every full hour). |
CancellationTokenSource? SendRepeatedly(TimeSpan period, object message) | Sends message to the function itself every period. |
TimeSpan? EnableCacheWithExpirationTime | Set before base.OnStarted() to keep received readings in Cache for this long. |
Timers started with these methods are stopped for you when the function stops.
Logging and failures
LogTrace, LogDebug, LogInformation, LogWarning, LogError and LogCritical each take (object message, bool asJson = false). The lines appear under Logs on the function and in System logs, prefixed with the class name.
Two settings in the [UserFunctions] section of the gateway's configuration limit a misbehaving function:
| Key | Default | Meaning |
|---|---|---|
ExceptionThresholdPerFunction | 10 | After this many unhandled exceptions, the gateway undeploys the function and marks it as not deployed. 0 means no limit. |
LogLevelPolicy | ["Error:2 1m", "Critical:2 1m"] | The most log lines per level and time window, for example "Warning:100 1h". When the limit is reached, one warning says so and further lines of that level are dropped until the window ends. |
UseAutoTryCatchInjector | true | The compiler wraps your methods so an exception is logged and counted instead of stopping the gateway. |
There is no time or memory limit per call. A function that blocks or loops holds up the gateway's processing of the devices it subscribes to, so keep the work in OnMessageReceive small.
What the compiler offers
Your code can use the .NET base library (collections, LINQ, tasks, HttpClient, sockets, JSON, XML), the Proxus types above, MQTT client types and ML.NET. There is no package manager: a library outside this set cannot be referenced.
For safety, code that uses these namespaces is refused when it is built, with the message Blacklisted namespace used: <name>. Reason: ...:
| Namespace | Reason given |
|---|---|
System.IO | No direct file access. |
System.Diagnostics | No access to processes and system diagnostics. |
System.Reflection | No reflection. |
System.Data, System.Data.Sql | No direct database access. Use ExecuteQuery and ExecuteNonQuery. |
System.Text.RegularExpressions | No regular expressions. |
System.Web, System.Drawing, System.Management, Microsoft.Win32, Microsoft.AspNetCore, Microsoft.VisualStudio | Not available to functions. |
Build errors are listed as <code> (line <n>, column <n>): <message>.
Templates
Templates on the function screen starts a function from one of these:
| Template | What it does |
|---|---|
| Derived metrics | Adds calculated values (power from voltage and current, counter delta and rate) to incoming readings. |
| Device health monitor | Marks devices healthy or stale from the time of their last reading. |
| Historical JSON import | Imports timestamped JSON values into the historian. |
| HTTP JSON polling | Polls a REST endpoint on a timer and saves chosen JSON fields as tags. |
| MQTT JSON ingestion | Subscribes to an MQTT topic and saves the JSON properties as tags of the named device. |
| OEE tracker | Calculates availability, performance, quality and OEE from production counters. |
| Payload normalizer | Renames tags, converts units and scales values before they are stored. |
| SQL polling ingestion | Reads a database table or view on a timer and saves the result as tags. |
| Threshold alarm | Raises alarms with a cooldown and hysteresis and sends a notification. |
What's next
- Functions: create, build, deploy and read the logs of a function in the hub.
- Pipelines and rules: when a function is the right tool and when a pipeline or rule is.
- Troubleshoot automation: compile and deploy errors.
Was this page helpful?
Evaluating Proxus? See it on your own machines in a 30-minute technical session.