base-driver is a framework for creating drivers for complex interfaces or data transformation tasks. This guide will walk you through the framework concepts, components, and their purpose.
Platform is not part of the base-driver package, but is rather implemented as part of individual drivers to support communication with the native data source.
- Methods inside
Platformimplementations faciliate communication with the data source via a localMachineinstance reference. Platformmethods are invoked by aCollectorinstance.- No data source transformation logic should occur inside the
Platformimplementation. Veneerclasses are responsible for transformation logic.
Retrieve all tags from FactoryIO instance.
public async Task<dynamic> GetTagsAsync()
{
var request = new RestRequest("tags", DataFormat.Json);
var response = await _machine.Client.ExecuteGetAsync(request);
return JArray.Parse(response.Content);
}Write tags to FactoryIO instance.
public async Task<dynamic> WriteTagsByNameAsync(string tag_array)
{
var request = new RestRequest("tag/values/by-name", Method.PUT, DataFormat.Json);
request.AddParameter("application/json", tag_array, ParameterType.RequestBody);
var response = await _machine.Client.ExecuteAsync(request);
return JArray.Parse(response.Content);
}Retrieve multiple items from an OPC XML-DA server.
public dynamic ReadMultipleTags(List<dynamic> descriptors)
{
DAVtqResult[] results = null;
NativeDispatchReturn ndr = nativeDispatch(() =>
{
results = _machine.Client.ReadMultipleItems(
new ServerDescriptor { UrlString = _machine.OpcxmldaEndpoint.URI },
Array.ConvertAll(descriptors.ToArray(),
new Converter<dynamic, DAItemDescriptor>(item =>
new DAItemDescriptor( (string)((Dictionary<object, object>.KeyCollection)item.Keys) .ElementAt(0))
)));
return true;
});
var nr = new
{
invocationMs = ndr.ElapsedMilliseconds,
request = new {read_multiple_tags = new {descriptors}},
response = new {read_multiple_tags = new {results}}
};
_logger.Trace($"[{_machine.Id}] Platform invocation result:\n{JObject.FromObject(nr).ToString()}");
return nr;
}Retrieve machine identifier from Fanuc controller.
public dynamic CNCId()
{
uint[] cncid = new uint[4];
NativeDispatchReturn ndr = nativeDispatch(() =>
{
return (Focas.focas_ret) Focas.cnc_rdcncid(_handle, cncid);
});
var nr = new
{
method = "cnc_rdcncid",
invocationMs = ndr.ElapsedMilliseconds,
doc = "https://ladder99.github.io/fanuc-driver/focas/SpecE/Misc/cnc_rdcncid",
success = ndr.RC == Focas.EW_OK,
rc = ndr.RC,
request = new {cnc_rdcncid = new { }},
response = new {cnc_rdcncid = new {cncid}}
};
_logger.Trace($"[{_machine.Id}] Platform invocation result:\n{JObject.FromObject(nr).ToString()}");
return nr;
}A Machine instance includes:
- native connectivity information and setup (from
config.yaml) - data source access (
Platform) - data output post-processor (
Handler) - data collection strategy (
Collector) - data transformations (
Veneer)
Drivers extend the Machine class to provide functionality unique to the data source.
A concrete Machine type, including assembly name, is referenced in the configuration at path machines[id].type. Example: l99.driver.fanuc.FanucMachine, fanuc.
Communication protocol configuration and HTTP client are managed by the Machine instance.
public FactoryioRemoteMachine(Machines machines, bool enabled, string id, object config) : base(machines, enabled, id, config)
{
dynamic cfg = (dynamic) config;
this["cfg"] = cfg;
this["platform"] = new Platform(this);
_factoryioRemoteEndpoint = new FactoryioRemoteEndpoint(cfg.type["net_uri"], (short)cfg.type["net_timeout_s"]);
_client = new RestClient($"{cfg.type["net_uri"]}/api");
_client.Timeout = cfg.type["net_timeout_s"] * 1000;
}Communication protocol configuration and QuickOPC DA client are managed by the Machine instance.
public OpcxmldaMachine(Machines machines, bool enabled, string id, object config) : base(machines, enabled, id, config)
{
dynamic cfg = (dynamic) config;
this["cfg"] = cfg;
this["data"] = cfg.type["data"];
this["platform"] = new Platform(this);
_opcxmldaEndpoint = new OpcxmldaEndpoint(cfg.type["net_uri"], (short)cfg.type["net_timeout_s"]);
_client = new EasyDAClient();
}Communciation protocol configuration is managed by the Machine instance.
public FanucMachine(Machines machines, bool enabled, string id, object config) : base(machines, enabled, id, config)
{
dynamic cfg = (dynamic) config;
_focasEndpoint = new FocasEndpoint(cfg.type["net_ip"], (ushort)cfg.type["net_port"], (short)cfg.type["net_timeout_s"]);
this["platform"] = new Platform(this);
}veneer : to overlay or plate (a surface, as of a common sort of wood) with a thin layer of finer wood for outer finish or decoration
peel : to break away from a group or formation —often used with off
Collected data requires processing before it can be relayed to another system. Veneer instances are responsible for:
- simple or complex source data transformations
- making source data human readable
- creating richer (more valuable to the consumer) data from multiple data points
- tracking data changes
- creating data hierarchies
Veneer instances are "applied" over native data points and "peeled" during the data collection cycle to reveal modified data structures.
Veneers can be applied/peeled as a whole. Veneers can be sliced and applied/peeled across logical boundaries (e.g. path, axis, spindle). Atomic values should be used for slicing veneers. Sliced veneers must be marked before peeling in order to convey the logical hierarchy of the observation to downstream systems.
Transformation into an intermediate format.
protected override async Task<dynamic> AnyAsync(dynamic input, params dynamic?[] additionalInputs)
{
var current_value = new
{
name = additionalInputs[0],
type = input.Vtq?.ValueType.Name,
value = input.Vtq?.Value,
good = input.Vtq?.Quality.IsGood
};
await onDataArrivedAsync(input, current_value);
if (current_value.IsDifferentString((object)lastChangedValue))
{
await onDataChangedAsync(input, current_value);
}
return new { veneer = this };
}Transformation into an human readable format.
protected override async Task<dynamic> AnyAsync(dynamic input, params dynamic?[] additionalInputs)
{
if (input.success)
{
var current_value = new
{
cncid = string.Join("-", ((uint[])input.response.cnc_rdcncid.cncid).Select(x => x.ToString("X")).ToArray())
};
await onDataArrivedAsync(input, current_value);
if (!current_value.Equals(lastChangedValue))
{
await onDataChangedAsync(input, current_value);
}
}
else
{
await onErrorAsync(input);
}
return new { veneer = this };
}Tracking executed G-code blocks.
protected override async Task<dynamic> AnyAsync(dynamic input, params dynamic?[] additionalInputs)
{
if (input.success && additionalInputs[0].success && additionalInputs[1].success)
{
_blocks.Add(input.response.cnc_rdblkcount.prog_bc,
additionalInputs[0].response.cnc_rdactpt.blk_no,
additionalInputs[1].response.cnc_rdexecprog.data);
var current_value = new
{
blocks = _blocks.ExecutedBlocks
};
await onDataArrivedAsync(input, current_value);
var last_keys = ((List<gcode.Block>)lastChangedValue.blocks).Select(x => x.BlockNumber);
var current_keys = ((List<gcode.Block>)current_value.blocks).Select(x => x.BlockNumber);
if (last_keys.Except(current_keys).Count() + current_keys.Except(last_keys).Count() > 0)
{
await onDataChangedAsync(input, current_value);
}
}
else
{
await onErrorAsync(input);
}
return new { veneer = this };
}Tracking driver performance.
protected override async Task<dynamic> AnyAsync(dynamic input, params dynamic?[] additionalInputs)
{
var max = ((List<dynamic>)input.focas_invocations).MaxBy(o => o.invocationMs).First();
var min = ((List<dynamic>)input.focas_invocations).MinBy(o => o.invocationMs).First();
var avg = (int)((List<dynamic>)input.focas_invocations).Average(o => (int)o.invocationMs);
var sum = ((List<dynamic>) input.focas_invocations).Sum(o => (int)o.invocationMs);
var failedMethods = ((List<dynamic>) input.focas_invocations)
.Where(o => o.rc != 0)
.Select(o => new { o.method, o.rc });
var current_value = new
{
input.sweepMs,
invocation = new
{
count = input.focas_invocations.Count,
maxMethod = max.method,
maxMs = max.invocationMs,
minMs = min.invocationMs,
avgMs = avg,
sumMs = sum,
failedMethods
}
};;
await onDataArrivedAsync(input, current_value);
return new { veneer = this };
}Example of a generated observation marker for spindle 'S' on execution path '1'.
"marker": [
{
"path_no": 1
},
{
"name": "S",
"suff1": "",
"suff2": ""
}
]Collector is a data collection strategy and an interface to the data source. The Collector is responsible for:
- establishing and tearing down connection with the data source via a
Platformimplementation - applying
Veneerover collected data - invoking
Veneerover collected data - tracking data source connectivity failures
A concrete Collector type, including assembly name, is referenced in the configuration at path machines[id].strategy. Example: l99.driver.fanuc.BlockTracker, fanuc.
Initialization, "applying veneers".
public override async Task<dynamic?> InitializeAsync()
{
try
{
foreach (dynamic descriptor in machine["data"])
{
machine.ApplyVeneer(typeof(opcxmlda.veneers.Tag), getDataKey(descriptor));
}
machine.VeneersApplied = true;
}
catch (Exception ex)
{
logger.Error(ex, $"[{machine.Id}] Collector initialization failed.");
}
return null;
}Collection cycle, "peeling veneers".
public override async Task<dynamic?> CollectAsync()
{
try
{
dynamic tags = await machine["platform"].ReadMultipleTagsAsync(machine["data"]);
for (int i = 0; i < tags.response.read_multiple_tags.results.Length; i++)
{
var tag = tags.response.read_multiple_tags.results[i];
string descriptor = getDataKey(i);
await machine.PeelVeneerAsync(descriptor, tag, descriptor);
}
LastSuccess = true;
}
catch (Exception ex)
{
logger.Error(ex, $"[{machine.Id}] Collector sweep failed.");
}
return null;
}Initialization, "applying veneers".
public override async Task<dynamic?> InitializeAsync()
{
try
{
while (!machine.VeneersApplied)
{
dynamic connect = await machine["platform"].ConnectAsync();
if (connect.success)
{
machine.ApplyVeneer(typeof(fanuc.veneers.Connect), "connect");
machine.ApplyVeneer(typeof(fanuc.veneers.CNCId), "cnc_id");
machine.ApplyVeneer(typeof(fanuc.veneers.RdParamLData), "power_on_time");
machine.ApplyVeneer(typeof(fanuc.veneers.SysInfo), "sys_info");
machine.ApplyVeneer(typeof(fanuc.veneers.GetPath), "get_path");
dynamic disconnect = await machine["platform"].DisconnectAsync();
machine.VeneersApplied = true;
}
else
{
await Task.Delay(sweepMs);
}
}
}
catch (Exception ex)
{
logger.Error(ex, $"[{machine.Id}] Collector initialization failed.");
}
return null;
}Collection cycle, "peeling veneers".
public override async Task<dynamic?> CollectAsync()
{
try
{
dynamic connect = await machine["platform"].ConnectAsync();
await machine.PeelVeneerAsync("connect", connect);
if (connect.success)
{
dynamic cncid = await machine["platform"].CNCIdAsync();
await machine.PeelVeneerAsync("cnc_id", cncid);
dynamic poweron = await machine["platform"].RdParamDoubleWordNoAxisAsync(6750);
await machine.PeelVeneerAsync("power_on_time", poweron);
dynamic info = await machine["platform"].SysInfoAsync();
await machine.PeelVeneerAsync("sys_info", info);
dynamic paths = await machine["platform"].GetPathAsync();
await machine.PeelVeneerAsync("get_path", paths);
dynamic disconnect = await machine["platform"].DisconnectAsync();
}
LastSuccess = connect.success;
}
catch (Exception ex)
{
logger.Error(ex, $"[{machine.Id}] Collector sweep failed.");
}
return null;
}Handler is an observation post-processor and interface to target systems. Data gathered via a Collector is transformed through a Veneer and acted upon the processing stages of:
- data arrival
- data change
- data source errors
- data collection cycle completion
A concrete Handler type, including assembly name, is referenced in the configuration at path machines[id].handler. Example: l99.driver.fanuc.handlers.SparkplugB, fanuc.
The handler prepares changed data into Splunk metric format.
public override async Task<dynamic?> OnDataChangeAsync(Veneers veneers, Veneer veneer, dynamic? beforeChange)
{
var payload = new
{
time = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds(),
@event = "metric",
host = veneers.Machine.Id,
fields = new
{
metric_name = veneer.LastArrivedValue.name,
_value = veneer.LastArrivedValue.value,
type = veneer.LastArrivedValue.type,
good = veneer.LastArrivedValue.good
}
};
return payload;
}The Splunk metric payload is then published to an MQTT broker. Alternatively, an HTTP request to the Splunk HEC endpoint could be executed.
protected override async Task afterDataChangeAsync(Veneers veneers, Veneer veneer, dynamic? onChange)
{
if (onChange == null)
return;
var topic = $"opcxmlda/{veneers.Machine.Id}/splunk/{veneer.Name}";
string payload = JObject.FromObject(onChange).ToString();
await veneers.Machine.Broker.PublishChangeAsync(topic, payload);
}The handler prepares changed data into InfluxDb line format.
public override async Task<dynamic?> OnDataChangeAsync(Veneers veneers, Veneer veneer, dynamic? beforeChange)
{
if (veneer.Name == "axis_data")
{
var payload = new LineProtocolWriter(Precision.Milliseconds)
.Measurement(veneer.Name)
.Tag("machine_id", veneers.Machine.Id)
.Tag("path_no", veneer.Marker[0].path_no.ToString())
.Tag("axis_name", (veneer.Marker[1].name + veneer.Marker[1].suff).ToString())
.Field("position", (float) veneer.LastArrivedValue.pos.absolute)
.Field("feed", (float) veneer.LastArrivedValue.actf);
return payload;
}
return null;
}The InluxDb line payload is then published to an MQTT broker.
protected override async Task afterDataChangeAsync(Veneers veneers, Veneer veneer, dynamic? onChange)
{
if (onChange == null)
{
return;
}
var topic = $"fanuc/{veneers.Machine.Id}/influx";
string payload = JObject.FromObject(onChange).ToString();
await veneers.Machine.Broker.PublishChangeAsync(topic, payload);
}graph LR
start:::in -->
id1(parse args) -->
id2(parse config) -->
id3(create machines) -->
id4(execute) -->
id5(shutdown) -->
stop:::out
classDef in fill:lightgreen;
classDef out fill:red;
static async Task Main(string[] args)
{
dynamic config = await Bootstrap.Start(args);
Machines machines = await Machines.CreateMachines(config);
await machines.RunAsync();
await Bootstrap.Stop();
}Relative or absolute path to logging configuration file.
Argument: --nlog
Default: nlog.config
Example: nlog.config, /etc/fanuc/nlog.config
WARNING: Target log file is defined inside
nlog.config
Relative or absolute path to driver configuration file.
Argument: --config
Default: config.yml
Example: config.yml, /etc/fanuc/config.yml
Data collection will continue to run until the application is stopped or Shutdown() is invoked on all Machine instances.
Driver configuration is maintained in the config.yml file. You can read more about YAML structure here. Multiple machines can be added and are differentiated by their id key.
Parameters relevant to driver initialization.
id : Unique machine identifier.
enabled : Toggles the active collection state. Disabled machines are not initialized upon startup.
type: Machine class type initialized on startup.
strategy : Collector class type initialized on startup.
handler : Handler class type initialized on startup.
Examples:
machines:
- id: bender01
enabled: !!bool true
type: l99.driver.opcxmlda.OpcxmldaMachine, opcxmlda
strategy: l99.driver.opcxmlda.collectors.Basic01, opcxmlda
handler: l99.driver.opcxmlda.handlers.SHDR, opcxmldamachines:
- id: cnc01
enabled: !!bool true
type: l99.driver.fanuc.FanucMachine, fanuc
strategy: l99.driver.fanuc.collectors.NLuaRunner, fanuc
handler: l99.driver.fanuc.handlers.Native, fanucParameters relevant to built-in MQTT client.
enabled : Toggles the active client state. Disabled clients are not initialized upon startup.
net_ip : Broker IP address.
net_port : Broker TCP port.
auto_connect : Automatically connect to broker on startup. In the example of SparkplugB, this parameter should be false.
publish_status : Publish status, typically at the end of sweep.
publish_arrivals : Publish all data every sweep.
publish_changes : Publish data only when it changes.
publish_disco : Publish machine information to discovery topic.
disco_base_topic : Topic used for discovery.
anonymous : Connect anonymously or use credentials.
user : Broker user.
password : Broker password.
Examples:
machines:
- id: bender01
...
broker:
enabled: !!bool true
net_ip: 10.20.30.40
net_port: !!int 1883
auto_connect: !!bool true
publish_status: !!bool true
publish_arrivals: !!bool true
publish_changes: !!bool true
publish_disco: !!bool true
disco_base_topic: opcxmlda
anonymous: !!bool false
user: client1
password: secret123machine[id].type specific configuration.
Examples:
machines:
- id: demo
type: l99.driver.opcxmlda.OpcxmldaMachine, opcxmlda
...
l99.driver.opcxmlda.OpcxmldaMachine, opcxmlda:
sweep_ms: !!int 1000
net_uri: http://opcxml.demo-this.com/XmlDaSampleServer/Service.asmx
net_timeout_s: !!int 3
data:
- Dynamic/Analog Types/Double:
- Dynamic/Analog Types/Int:
- Dynamic/Analog Types/Double[]:
- Static/Simple Types/String:
- Static/Simple Types/DateTime:
- Static/ArrayTypes/Object[]:
- Dynamic/Analog Types/Fools/Guildenstern:
- Dynamic/Enumerated Types/Gems:
- SomeUnknownItem:machines:
- id: cnc01
type: l99.driver.fanuc.FanucMachine, fanuc
...
l99.driver.fanuc.FanucMachine, fanuc:
sweep_ms: !!int 1000
net_ip: 10.20.30.50
net_port: !!int 8193
net_timeout_s: !!int 3machines:
- id: fio01
type: l99.driver.factoryio.FactoryioLocalMachine, factoryio
...
l99.driver.factoryio.FactoryioLocalMachine, factoryio:
sweep_ms: !!int 100machines:
- id: fio02
type: l99.driver.factoryio.FactoryioRemoteMachine, factoryio
...
l99.driver.factoryio.FactoryioRemoteMachine, factoryio:
sweep_ms: !!int 1000
net_uri: http://10.20.32.6:7410
net_timeout_s: !!int 3machine[id].strategy specific configuration.
machines:
- id: cnc02
strategy: l99.driver.fanuc.collectors.NLuaRunner, fanuc
...
l99.driver.fanuc.collectors.NLuaRunner, fanuc:
script: lua/test1.luamachines:
- id: fio01
strategy: l99.driver.factoryio.collectors.BasicLocal01, factoryio
...
l99.driver.factoryio.collectors.BasicLocal01, factoryio:
sub_topic: factoryio/fio01/iomachine[id].handler specific configuration.
machines:
- id: bender01
handler: l99.driver.opcxmlda.handlers.SHDR, opcxmlda
...
l99.driver.opcxmlda.handlers.SHDR, opcxmlda:
port: !!int 7878
verbose: !!bool true
lua_head: |
luanet.load_assembly 'System';
luanet.load_assembly 'Newtonsoft.Json';
JObject = luanet.import_type 'Newtonsoft.Json.Linq.JObject';
data:
- avail:
shdr:
name: avail
category: event
eval: |
return "AVAILABLE";