Calculators

Calculators have no direct connection to the hardware, but communicate exclusively with the signal hub.

The calculators are called up in special calculator tasks. A calculator task first calls the calculator assigned to it in read mode. This gives the calculator the opportunity to read signals from the signal hub. Once the signals have been read, the calculator is called up in calculate mode. In the last step, the calculator is called up in write mode and can now write the calculated values back to the signal hub.

Temperature monitoring

In this part of the tutorial, we will add the temperature monitoring to our application in the form of a calculator.

Calculator

The temperature monitoring requires the current CPU temperature as an input signal. Depending on the temperature, we set the status to OK, Warning or Alarm. If the system is in Warning or Alarm status, the fan should also be switched on.

Create a new class TemperatureMonitoring.

using SignalF.Controller;
using SignalF.Controller.Signals;
using SignalF.Controller.Signals.Calculators;
using SignalF.Datamodel.Calculation;

namespace Tutorial.Monitoring;

public class TemperatureMonitoring : Calculator<ICalculatorConfiguration>
{
    public TemperatureMonitoring(ISignalHub signalHub, ILogger<TemperatureMonitoring> logger)
        : base(signalHub, logger)
    {
    }
}

All input and output signals relevant for the calculator are available via the properties ‘SignalSinks’ (inputs) and ‘SignalSources’ (outputs). The individual signals are accessed via an index, which must first be determined in the configuration phase.

To save the indices, we first add the following fields:

    private int _indexTemperature = -1;
    private int _indexOk = -1;
    private int _indexWarning = -1;
    private int _indexAlarm = -1;
    private int _indexFan = -1;

In the next step, we overwrite the OnConfigure() method as follows:

    protected override void OnConfigure(ICalculatorConfiguration configuration)
    {
        base.OnConfigure(configuration);

        foreach (var signalSink in configuration.SignalSinks)
        {
            var signalName = signalSink.Definition.Name;
            switch (signalName)
            {
                case "CpuTemperature":
                {
                    _indexTemperature = GetSignalIndex(signalSink);
                    break;
                }
                default:
                {
                    throw new ControllerException($"Unknown signal {signalName}");
                }
            }
        }

        foreach (var signalSource in configuration.SignalSources)
        {
            var signalName = signalSource.Definition.Name;
            switch (signalName)
            {
                case "OK":
                {
                    _indexOk = GetSignalIndex(signalSource);
                    break;
                }
                case "Warning":
                {
                    _indexWarning = GetSignalIndex(signalSource);
                    break;
                }
                case "Alarm":
                {
                    _indexAlarm = GetSignalIndex(signalSource);
                    break;
                }
                case "Fan":
                {
                    _indexFan = GetSignalIndex(signalSource);
                    break;
                }
                default:
                {
                    throw new ControllerException($"Unknown signal {signalName}");
                }
            }
        }
    }

The indices of the individual signals currently correspond to their position in the configuration. However, this is not guaranteed and may change in future versions of SignalF. To determine the correct index, we therefore call the GetSignalIndex() method for each signal and then assign the returned index to the corresponding fields.

To finalise the temperature monitoring, we define the desired limit values and overwrite the OnCalculate() method.

    private const double WarnLevel = 70.0;
    private const double CriticalLevel = 100.0;

    protected override void OnCalculate()
    {
        var timestamp = SignalHub.GetTimestamp();
        var temperature = SignalSinks[_indexTemperature].Value;

        var signalSources = SignalSources;
        signalSources[_indexOk].AssignWith(temperature < WarnLevel ? 1.0 : 0.0, timestamp);
        signalSources[_indexWarning].AssignWith(temperature is >= WarnLevel and < CriticalLevel ? 1.0 : 0.0, timestamp);
        signalSources[_indexAlarm].AssignWith(temperature >= CriticalLevel ? 1.0 : 0.0, timestamp);
        signalSources[_indexFan].AssignWith(temperature >= WarnLevel ? 1.0 : 0.0, timestamp);
    }

Configuration

Once we have created the temperature monitoring component, we still need to register it in our application and configure the inputs and outputs. To do this, first create the extension method for registering the component.

using Scotec.Math.Units;
using SignalF.Configuration;
using SignalF.Datamodel.Signals;
using SignalF.Extensions.Configuration;

namespace Tutorial.Monitoring;

public static class MonitoringExtensions
{
    public static IServiceCollection AddTemperatureMonitoring(this IServiceCollection services)
    {
        return services.AddTransient<TemperatureMonitoring>();
    }
}

Now open the file Program.cs and add a call to the extension method.

                              .ConfigureServices(services =>
                              {
                                  services.AddSignalFControllerService();
                                  services.AddTransient<ISystemConfiguration, SystemConfiguration>();

                                  // Register device implementations
                                  services.AddCpuTemperature();
                                  
                                  // Register calculator implementations
                                  services.AddTemperatureMonitoring();
                              });

Signal provider definition

In order to configure the monitoring component, you must first create a signal provider definition for the monitoring component. This definition is a description of the implementation that can be read by the SignalF controller and corresponds roughly to a class in object-orientated programming. Among other things, the definition contains a list of the signal sources and sinks.

    private static ISignalFConfiguration AddTemperatureMonitoringDefinition(this ISignalFConfiguration configuration)
    {
        return configuration.AddCalculatorDefinition<TemperatureMonitoring>(builder =>
        {
            builder.UseTemplate("DefaultTemplate")
                   .SetName("TemperatureMonitoring")
                   .AddSignalSinkDefinition("CpuTemperature", EUnitType.Temperature)
                   .AddSignalSourceDefinition("OK")
                   .AddSignalSourceDefinition("Warning")
                   .AddSignalSourceDefinition("Alarm")
                   .AddSignalSourceDefinition("Fan");
        });
    }

Signal provider configuration