# 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.

Source: https://proxus.io/docs/develop/functions-sdk

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](https://proxus.io/docs/automate/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.

```csharp
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 call `base.OnStarted()`. The base method connects the subscriptions,
  so a subscription added after it receives nothing.
- `OnMessageReceive` is called for each reading, before the reading is stored, sent to outbound targets or run
  through pipelines. A payload you add or change on the `TransportData` object 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 `OnMessageReceive` short. 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

```csharp
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](https://proxus.io/docs/connect/notification-channels) 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](https://proxus.io/docs/reference/outbound-targets) 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](https://proxus.io/docs/administer/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](https://proxus.io/docs/automate/functions): create, build, deploy and read the logs of a function in the hub.
- [Pipelines and rules](https://proxus.io/docs/concepts/pipelines-and-rules): when a function is the right tool and when a pipeline or rule is.
- [Troubleshoot automation](https://proxus.io/docs/troubleshoot/automation): compile and deploy errors.
