MDA - API pro podpůrné služby
Aplikace Mervis Data Terminal (MDT) a Mervis Data Concentrator (MDC) sdílejí jednotné API (MDA) pro poskytování přístupu k datům třetím stranám.
MDA je postavené na rámci gRPC a nabízí následující vlastnosti:
- Používá HTTP/2 – přináší výhody jako multiplexing (více souběžných požadavků), server push, vyšší výkon a nižší latenci.
- Serializace pomocí Protocol Buffers (Protobuf) – kompaktní binární formát, rychlejší a efektivnější než JSON nebo XML.
- Podpora více jazyků – včetně C++, Java, Python, Go, C#, Node.js a dalších.
- Generování klientského a serverového kódu ze souboru .proto – umožňuje snadné vytvoření a implementaci rozhraní bez opakovaného kódu.
MDA .proto soubory
Ukázkový C# klient
Shrnutí toho, co tento kód provádí:
- Připojí se k Mervis MDA serveru.
- Ověří se pomocí tokenu.
- Načte metadata (popisy) všech dostupných proměnných.
- Volitelně:
- Jednorázově načte všechny hodnoty.
- Přihlásí se k odběru změn jedné hodnoty.
- Zapíše dvě hodnoty zpět na server.
- Program.cs
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml; using ESG.MervisDataApi.Shared; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; namespace SimpleDemo { internal class Program { /// <summary> /// Authentication token used for secure access to the MervisDataApi. /// </summary> private static readonly Credentials m_Credentials = new Credentials { Token = new TokenCredentials { Token = "XXXXXXXXX" } }; static async Task Main(string[] args) { // Create a gRPC channel to communicate with the MervisDataApi service using (GrpcChannel channel = GrpcChannel.ForAddress("https://mda_server:5555")) { var client = new MervisDataApiService.MervisDataApiServiceClient(channel); // Retrieve metadata (descriptions) for all available values GetValueDescriptionsResponse descriptionResponse = await RetrieveAllValueDescriptions(client); // Read current values for all available items once await ReadAllValuesOnce(client, descriptionResponse); const bool subscriptionDemo = true; // Toggle subscription demo const bool writeDemo = false; // Toggle write demo if (subscriptionDemo) { // Subscribe to live updates for the first available value if (descriptionResponse.Descriptions.Count > 0) { await SubscribeAndRead(client, descriptionResponse.Descriptions[0]); } } if (writeDemo) { // Send (write) example values to the server await WriteValues(client); } } } /// <summary> /// Sends two hardcoded values to the API. /// </summary> private static async Task WriteValues(MervisDataApiService.MervisDataApiServiceClient client) { Console.WriteLine("-------------------------------"); Console.WriteLine(nameof(WriteValues)); var req = new SetValuesRequest { Credentials = m_Credentials }; // Add first value to be written req.Values.Add(new MdaValue { Id = new ValueId { Native = "xxxx" }, Quality = MdaValueQuality.Good, Float64 = 1, Timestamp = DateTime.UtcNow.ToTimestamp() }); // Add second value to be written req.Values.Add(new MdaValue { Id = new ValueId { Native = "yyyy" }, Quality = MdaValueQuality.Good, Float64 = 500, Timestamp = DateTime.UtcNow.ToTimestamp() }); // Send request to the server var resp = await client.SetValuesAsync(req); } /// <summary> /// Subscribes to value updates from the API and prints them in real-time. /// </summary> private static async Task SubscribeAndRead(MervisDataApiService.MervisDataApiServiceClient client, MdaValueDescription whatToSubscribe) { Console.WriteLine("-------------------------------"); Console.WriteLine(nameof(SubscribeAndRead)); // Prepare filter to subscribe only to one specific value ValueIdFilter idFilter = new ValueIdFilter(); idFilter.Ids.Add(new ValueId { Native = whatToSubscribe.Id.Native }); // Start the subscription call to receive updates using (var readingCall = client.SubscribeValues(new SubscribeValuesRequest { Credentials = m_Credentials, Filter = new SelectionFilter { ById = idFilter } })) { // Read updates from the stream await foreach (var data in readingCall.ResponseStream.ReadAllAsync(CancellationToken.None)) { foreach (var value in data.Values) { Console.WriteLine($"{value.Id.Native}: {GetValueText(value)} [{value.Timestamp.ToDateTime()}]"); } await Task.Delay(100); // Wait briefly before continuing (helps readability) } } } /// <summary> /// Reads all values once and prints their latest data. /// </summary> private static async Task ReadAllValuesOnce(MervisDataApiService.MervisDataApiServiceClient client, GetValueDescriptionsResponse descriptions) { Console.WriteLine("-------------------------------"); Console.WriteLine(nameof(ReadAllValuesOnce)); ValueIdFilter idFilter = new ValueIdFilter(); // Prepare a list of all IDs to request their values foreach (var desc in descriptions.Descriptions) { idFilter.Ids.Add(new ValueId { Native = desc.Id.Native }); } // Send the request to get current values GetValuesResponse values = await client.GetValuesAsync(new GetValuesRequest { Credentials = m_Credentials, Filter = new SelectionFilter { ById = idFilter }, ReturnIdKind = MdaValueIdKind.Native }); Console.WriteLine("Values:"); foreach (var value in values.Values) { Console.WriteLine($"{value.Id.Native}: {GetValueText(value)} [{value.Timestamp.ToDateTime()}]"); } Console.WriteLine("Invalid IDs:"); foreach (var invalidId in values.InvalidIds) { Console.WriteLine($"{invalidId.Native}"); } } /// <summary> /// Converts the MdaValue to a string representation based on its type. /// </summary> private static string GetValueText(MdaValue value) { switch (value.ValueCase) { case MdaValue.ValueOneofCase.Float64: return XmlConvert.ToString(value.Float64); case MdaValue.ValueOneofCase.Boolean: return XmlConvert.ToString(value.Boolean); case MdaValue.ValueOneofCase.Text: return value.Text; default: return "N/A"; // Unknown or unsupported value type } } /// <summary> /// Retrieves metadata (descriptions) for all available variables. /// </summary> private static async Task<GetValueDescriptionsResponse> RetrieveAllValueDescriptions(MervisDataApiService.MervisDataApiServiceClient client) { Console.WriteLine("-------------------------------"); Console.WriteLine(nameof(RetrieveAllValueDescriptions)); // Create a path filter with an empty path (meaning: retrieve everything) PathFilter pf = new PathFilter(); pf.Paths.Add(new Path()); // empty path = wildcard = all paths var request = new GetValueDescriptionsRequest { Credentials = m_Credentials, Filter = new SelectionFilter { ByPath = pf } }; var descs = await client.GetValueDescriptionsAsync(request); // Print out the descriptions for inspection foreach (var desc in descs.Descriptions) { Console.WriteLine($"{desc.Id.Native} [{desc.Type}] {WritePath(desc.Paths.ElementAtOrDefault(0))}"); } return descs; } /// <summary> /// Formats a `Path` object as a readable string. /// </summary> private static string WritePath(Path path) { if (path != null) { const char delimiter = '/'; return delimiter + string.Join(delimiter, path.Segments); } else { return "N/A"; } } } }