OpenProtocolInterpreter

repository·master·Indexed 41 days ago

https://github.com/rickedb/openprotocolinterpreter

A C# library for simplifying communication with Atlas Copco Tightening Controllers and compatible devices. It converts raw Open Protocol MID strings or byte arrays into strongly-typed C# objects and vice versa, eliminating manual string manipulation. Features include a MidInterpreter for parsing and packing, support for custom MIDs, and marker interfaces for generating replies and acknowledgments.

Tokens
4.9K
Snippets
12
Records
15
Agent score
2%

What's inside OpenProtocolInterpreter

  1. How OpenProtocolInterpreter works

    master

    OpenProtocolInterpreter is a library designed to convert Open Protocol communication packages (MIDs), which are typically raw strings or byte arrays, into strongly-typed C# objects. This eliminates the need for manual string manipulation and substringing.

    For example, if you receive a string like "00240005001 0018", the interpreter identifies it as MID 5 and returns an instance of the Mid0005 class with all data fields populated.

  2. Implement a delegate-based MID dispatch system

    master

    For complex applications, you can create a dispatch system that maps MID types to specific actions using delegates.

    1. Define a delegate (e.g., ReceivedCommandActionDelegate) that accepts ReceivedMidEventArgs.
    2. Create a dictionary mapping Type to your delegate.
    3. When a package is received, parse it using the interpreter, look up the type in your dictionary, and invoke the corresponding delegate.
    // 1. Define delegate and args
    protected delegate void ReceivedCommandActionDelegate(ReceivedMidEventArgs e);
    
    public class ReceivedMidEventArgs : EventArgs
    {
        public Mid ReceivedMid { get; set; }
    }
    
    // 2. Register MIDs to delegates
    protected Dictionary<Type, ReceivedCommandActionDelegate> RegisterOnAsyncReceivedMids()
    {
        var receivedMids = new Dictionary<Type, ReceivedCommandActionDelegate>();
        receivedMids.Add(typeof(Mid0005), new ReceivedCommandActionDelegate(OnCommandAcceptedReceived));
        return receivedMids;
    }
    
    // 3. Dispatch on receipt
    protected void OnPackageReceived(string message)
    {
        var mid = Interpreter.Parse(message);
        if (OnReceivedMid.TryGetValue(mid.GetType(), out var action))
        {
            action(new ReceivedMidEventArgs { ReceivedMid = mid });
        }
    }
  3. Run the OpenProtocolInterpreter benchmarks

    master

    The benchmarks are implemented using BenchmarkDotNet. You must run them in Release mode, as BenchmarkDotNet refuses to run debug builds. All arguments following the -- separator are passed directly to BenchmarkDotNet.

    Use the following commands to run different benchmark scenarios:

    # Run everything (full precision, takes several minutes)
    dotnet run -c Release --project src/OpenProtocolInterpreter.Benchmarks
    
    # Run a specific class (e.g., InterpreterModeBenchmarks)
    dotnet run -c Release --project src/OpenProtocolInterpreter.Benchmarks -- --filter '*InterpreterModeBenchmarks*'
    
    # Run a quick job to verify the suite still runs
    dotnet run -c Release --project src/OpenProtocolInterpreter.Benchmarks -- --quick
    
    # List all available benchmarks
    dotnet run -c Release --project src/OpenProtocolInterpreter.Benchmarks -- --list flat
  4. Handle Extra Data in MIDs 0006, 0008, and 0009

    master

    MIDs 0006 (request), 0008 (subscription), and 0009 (unsubscription) contain free-form payloads that depend on the specific MID being requested. These are modeled using ExtraData classes.

    Packing: Use SetExtraData on the container MID. This automatically populates SubscriptionMid, WantedRevision, ExtraDataLength, and ExtraData.

    Parsing: First, parse the container MID. Then, instantiate the appropriate ExtraData class using the WantedRevision from the container and pass the ExtraData bytes to its .Parse() method.

    // Packing extra data
    var subscription = new Mid0008();
    subscription.SetExtraData(new Mid1201ExtraDataSubscription
    {
        SendAlternatives = 1,
        DataIdentifierTimestamp = new DateTime(2026, 8, 7, 13, 45, 0),
        SendObjectData = true
    });
    var package = subscription.Pack();
    
    // Parsing extra data
    var mid08 = interpreter.Parse<Mid0008>(package);
    var extraData = (Mid1201ExtraDataSubscription)new Mid1201ExtraDataSubscription(mid08.WantedRevision)
                                                        .Parse(mid08.ExtraData);
  5. Add a new benchmark

    master

    When adding new benchmarks to the suite, follow these guidelines:

    1. Use Sample Packages: Do not inline raw strings. Add new sample packages to Packages.cs (copied from the test suite) to ensure they are valid and known to the parser.
    2. Group by Direction: Keep packages grouped by direction (IController mids are sent by the controller, IIntegrator mids by the integrator) to ensure mode benchmarks remain accurate.
    3. Avoid Assertions: Benchmark bodies must not contain assertions. Assertions measure the cost of the assertion itself rather than the code being benchmarked. Correctness should be verified in MIDTesters.Core.
    4. Use [GlobalSetup] for Validation: If a benchmark depends on successful package parsing, perform a single check inside a [GlobalSetup] method. This ensures that a broken sample fails the run immediately rather than reporting the cost of an exception during the benchmark loop.
  6. Parse Open Protocol packages with MidInterpreter

    master

    To parse packages, you must first instantiate a MidInterpreter and configure it with the MIDs it should recognize. Use UseAllMessages() to enable all known MIDs.

    Parsing Strategies

    • Known MID: Use the generic Parse<T>(package) method when you know the expected MID type. This returns the specific class (e.g., Mid0004). Note that this throws an InvalidCastException if the package does not match the requested type.
    • Unknown MID: Use the non-generic Parse(package) method when you do not know which MID is arriving. This returns a base Mid object.

    Both string and byte[] packages are supported.

    // Setup the interpreter
    var interpreter = new MidInterpreter().UseAllMessages();
    
    // Parsing a specific known MID
    var midPackage = "00260004001         001802";
    var myMid04 = interpreter.Parse<Mid0004>(midPackage);
    
    // Accessing properties
    int myFailedMid = myMid04.FailedMid;    // 18
    Error errorCode = myMid04.ErrorCode;    // Error.ParameterSetIdNotPresent
    
    // Parsing when the MID type is unknown
    byte[] rawPackage = socket.Receive();
    var mid = interpreter.Parse(rawPackage); // returns base Mid
  7. Add undocumented MIDs to the interpreter

    master

    You can inject MIDs that are not part of the official documentation using the UseCustomMessage method. This method accepts a Dictionary<int, Type> mapping the MID integer to your custom class.

    Your custom class must inherit from Mid and define its own data fields using attributes like [TimestampDataFieldDefinition] or [StringDataFieldDefinition] to specify how the raw bytes should be mapped to properties.

    Note: Custom messages may be slower than built-in MIDs because they lack specific lookup optimizations.

    var interpreter = new MidInterpreter()
                            .UseAllMessages()
                            .UseCustomMessage(new Dictionary<int, Type> { { NewMid0083.MID, typeof(NewMid0083) } });
    
    public class NewMid0083 : Mid
    {
        public const int MID = 83;
    
        [TimestampDataFieldDefinition(revision: 1, field: 1, Index = 20, HasPrefix = false)]
        public DateTime Time { get; set; }
    
        [StringDataFieldDefinition(revision: 1, field: 2, Index = 39, Size = 2, HasPrefix = false)]
        public string TimeZone { get; set; }
    
        public NewMid0083() : base(MID, DEFAULT_REVISION) { }
        public NewMid0083(Header header) : base(header) { }
    }
  8. Customize registered MIDs in MidInterpreter

    master

    To improve performance and reduce memory footprint, you can restrict the MidInterpreter to only recognize a specific subset of MIDs. You can register specific types using UseAllMessages or register entire categories using specialized extension methods like UseCommunicationMessages, UseTighteningMessages, or UseAlarmMessages.

    When registering categories, you can specify an InterpreterMode to filter messages based on whether the device is acting as a Controller or an Integrator:

    • InterpreterMode.Integrator: Keeps only MIDs a controller can send to you.
    • InterpreterMode.Controller: Keeps only MIDs an integrator can send to you.
    • InterpreterMode.Both: Keeps everything (default).

    Extension methods accept a mode, an IEnumerable<Type>, or an IDictionary<int, Type>.

    // Register specific MIDs
    var myCustomInterpreter = new MidInterpreter()
                                    .UseAllMessages(new Type[]
                                    {
                                        typeof(Mid0001),
                                        typeof(Mid0002),
                                        typeof(Mid0003),
                                        typeof(Mid0004),
                                        typeof(Mid0106)
                                    });
    
    // Register categories with a specific mode
    var interpreter = new MidInterpreter()
                            .UseCommunicationMessages(InterpreterMode.Integrator)
                            .UseTighteningMessages(InterpreterMode.Integrator)
                            .UseAlarmMessages(InterpreterMode.Integrator);
  9. Pack MIDs into strings or bytes

    master

    You can generate Open Protocol packages from object instances using the following methods:

    • Pack(): Returns the package as a string.
    • PackBytes(): Returns the package as a byte[] encoded with Mid.DefaultEncoding (ASCII).
    • PackWithNul(): Returns the package as a string with a NUL character appended (often required by certain controllers).

    The header length is automatically recalculated during packing based on the Revision specified in the MID's header.

    var jobUploadRequest = new Mid0032(revision: 2) { JobId = 1 };
    
    string package = jobUploadRequest.Pack();
    // Generated package => 00240032002         0001
    
    byte[] bytes = jobUploadRequest.PackBytes();       // ASCII encoded
    string terminated = jobUploadRequest.PackWithNul(); // Appends NUL
  10. Override existing MIDs with custom implementations

    master

    If a controller implements a MID non-standardly, or if you want to add custom properties/logic to an existing MID, you can override it in the MidInterpreter.

    To do this, use a Use...Messages extension method and provide a Dictionary<int, Type> mapping the original MID integer to your custom class type. Your custom class must inherit from the original MID class and must expose a parameterless constructor.

    // Override Mid 81 with a custom implementation
    var interpreter = new MidInterpreter()
                            .UseAllMessages()
                            .UseTimeMessages(new Dictionary<int, Type> { { Mid0081.MID, typeof(OverridedMid0081) } });
    
    public class OverridedMid0081 : Mid0081
    {
        public string FormattedDate
        {
            get => Time.ToString("dd/MM/yyyy HH:mm:ss");
            set => Time = DateTime.Parse(value);
        }
    
        public OverridedMid0081() { }
    }