felixwrt / sml-rs

Smart Message Language parser written in Rust
Other
14 stars 3 forks source link

Introduce High-Level API #15

Open felixwrt opened 1 year ago

felixwrt commented 1 year ago

Here's a first version of a more high-level API.

Motivation

SML is a pretty flexible (some might say over-engineered) protocol. The parsers implemented in the parser module parse SML data in a way that all valid SML transmissions can be parsed* and all possible attributes are accessible.

Real world data collected from power meters shows that they use SML in a certain way. For example, an SML file can consist of any number of SML messages, but all power meters send SML files containing exactly three messages: OpenResponse, GetListResponse, CloseResponse. There are also optional attributes in SML that aren't used by any power meter (e.g. signature fields).

This library's primary use case is to read data from power meters. The current API and data structures provided by the parser are very general, which makes them difficult to use in the context of power meters.

Approach

The idea with what I call the "application layer" is to provide an API that is designed for the power meter use case. This API will not expose attributes that aren't used by power meters and will assume a certain structure of the data (number and order of messages; obis codes for value descriptions etc.).

The "application layer" API should cover 95% of use cases. For everything else, it's still possible to use the parser layer directly. This exposes all data and allows handling more diverse data.

To me this approach (optimizing for the common use case while still allowing more flexibility when needed) looks promising.

Examples

The following block shows a debug output of the data structures created from a typical sml transmission when using the parser layer:

[
    Message {
        transaction_id: [1, 56, 128, 49],
        group_no: 0,
        abort_on_error: 0,
        message_body: OpenResponse(
            OpenResponse {
                codepage: None,
                client_id: None,
                req_file_id: [0, 104, 42, 187],
                server_id: [10, 1, 73, 83, 75, 0, 4, 50, 94, 197],
                ref_time: Some(SecIndex(6825875)),
                sml_version: Some(1),
            },
        ),
    },
    Message {
        transaction_id: [1, 56, 128, 50],
        group_no: 0,
        abort_on_error: 0,
        message_body: GetListResponse(
            GetListResponse {
                client_id: None,
                server_id: [10, 1, 73, 83, 75, 0, 4, 50, 94, 197],
                list_name: Some([1, 0, 98, 10, 255, 255]),
                act_sensor_time: Some(SecIndex(6825875)),
                val_list: [
                    ListEntry {
                        obj_name: [1, 0, 96, 50, 1, 1],
                        status: None,
                        val_time: None,
                        unit: None,
                        scaler: None,
                        value: Bytes([73, 83, 75]),
                        value_signature: None,
                    },
                    ListEntry {
                        obj_name: [1, 0, 96, 1, 0, 255],
                        status: None,
                        val_time: None,
                        unit: None,
                        scaler: None,
                        value: Bytes([10, 1, 73, 83, 75, 0, 4, 50, 94, 197]),
                        value_signature: None,
                    },
                    ListEntry {
                        obj_name: [1, 0, 1, 8, 0, 255],
                        status: Some(Status32(1868036)),
                        val_time: None,
                        unit: Some(30),
                        scaler: Some(-1),
                        value: U32(1989273),
                        value_signature: None,
                    },
                    ListEntry {
                        obj_name: [1, 0, 16, 7, 0, 255],
                        status: None,
                        val_time: None,
                        unit: Some(27),
                        scaler: Some(0),
                        value: I8(26),
                        value_signature: None,
                    },
                ],
                list_signature: None,
                act_gateway_time: None,
            },
        ),
    },
    Message {
        transaction_id: [1, 56, 128, 51],
        group_no: 0,
        abort_on_error: 0,
        message_body: CloseResponse(
            CloseResponse {
                global_signature: None,
            },
        ),
    },
]

Many fields aren't set, information is duplicated and typical users won't be interested in many of the provided values.

In comparison, this is what the debug output of the "application layer" data structure looks like:

PowerMeterTransmission {
    server_id: [10, 1, 73, 83, 75, 0, 4, 50, 94, 197],
    time: Some( SecIndex { secs: 6825875 } ),
    values: [
        (
            ObisCode { inner: [1, 0, 1, 8, 0] },
            Value {
                value: 1989273,
                unit: WattHour,
                scaler: -1,
            },
        ),
        (
            ObisCode { inner: [1, 0, 16, 7, 0] },
            Value {
                value: 26,
                unit: Watt,
                scaler: 0,
            },
        ),
    ],
}

The data structure is much more focused and still contains the most relevant information.

Extracting relevant data from the data structure is also much easier in the "application layer".

* Well, the subset of SML I found real world examples for.

felixwrt commented 8 months ago

Status: this is still WIP. I don't like that PowerMeterTransmission is generic over the value type, which could either be Vec<(ObisCode, Value)> (allocating use case) or [Value; N] (for the extractor use-case). Support for statically allocated vectors is also missing.

Using PowerMeterTransmission's from_bytes_extract is not possible from the SmlReader. This needs work as well.