Yukon design megathread

Besides node ID, a general filter operating on the contents of the received messages/responses could be useful (that is implemented in the old GUI tool).

Can we not just require the user (for now) to build their plotting pipeline in a way that produces a flat array of scalars at the output? We could add more sophisticated representations for matrices or images (like below) later.

uint16 PIXELS_PER_ROW = 1280
uint16 ROWS_PER_IMAGE = 1024

uavcan.time.SynchronizedTimestamp timestamp     # Image capture time
RGB888[PIXELS_PER_ROW*ROWS_PER_IMAGE] pixels    # Row major, top-left pixel first

So that, for example, the user could select an image viewer as the target plotter and pass pixels to the final stage of the pipeline.

Also, it should be possible to save/restore the plotter config to/from a human-readable text file format (like YAML). This is important because setting up a plotter can be very tedious — so many forms and fields to click through. Ideally, the file format should be user friendly enough to allow power users to just write their desired configuration straight as text rather than pointing and clicking.

These all sound very logical.

  • The general filter should happen on the frontend or backend side?

  • I think that a flat array should be produced, but each element’s content depend on the value extractor (not only scalar). This is how maximum extendibility is achieved. The only missing piece is DSDL data and format transport over JSON, which should be our priority for now. I’m going to research that for now.

It’s probably best to do as much work as we can on the back end, but you understand the architecture better.

Ok.

For the configuration file’s syntax: How should each data subscription and value extraction look like? I think it makes sense to have it in such (or similar) formatting manner: nodeId:port.dataType[optionalRequestObject]->extractor , where extractor is in a.b.c.d.. form. What kinds of filters do you think would be possible? is a min-max-equal sufficient? Complexity goes up a lot if you want to include more math-heavy stuff that depend on previous state.

So, the backend sends the frontend data and the frontend already knows the ‘label’ of each vector element. Content-wise, we need to agree on some representation. It does not make much sense to just send bytes over REST.

There can be 2 types (that can be extracted): primitives and composites. Primitives could be sent in perhaps this manner:

If the type is composite, do this tree-like representation.

{
  "PIXELS_PER_ROW": 1280,
  "ROWS_PER_IMAGE": 1024,
  "timestamp": { ... },
  "pixels": [[255, 255, 255], [33, 25, 105], ...]
}

If the type is just the extracted primitive, the corresponding vector element would just contain the value

[105, "abcd", 10.3, [[1,2], [3,4]]

Note on arrays:
An array of primitives or composites:
[1,2,3] or ["a", "b", "c"] or [{"a": "A", "b": "B"}, {"a": "otherA", "b": "otherB"}]

It makes sense to take this a step further if the array is composed out of a composite object which only contains primitives and encode it as follows:
[[255, 255, 255], [33, 25, 105], ...] (for the pixel example stated above at least).


The same way is going to be used for the register’s types, but we have 2 extra problems:

  • Displaying it
    It is possible to detect an “array of arrays” which could be translated as a 2d array and shown as a row-major matrix. (The recognization process is slower but register batch viewing is a lot less-realtime than the plotter). Same thing with collections of primitives.
  • Providing metadata
    This would be used for generating the editor UI: Perhaps another tree-like view of the register’s writeable contents (constants have "metadata": {"constant": true}:
    along with the primitive (or composite) "value": ..snip.. . Each leaf of the editor would be presented as part of a <form>, by means of an <input>. Text inputs have a means of client-side data checking, with min max and max-length fields. Min Max and Default (Reset) is already detectable from the ><= suffix of the parameter list, but other stuff like the data_type are not. This leads me to the conclusion that for primitives/leafs of the tree, this data type should exist and contain relevant information: (I need some help with this, perhaps the types supported by pydsdl?)

I am unable to come up with an automatic legend ‘tag’ inference for the plots. We could possibly have some easy to look up dictionary mapping from primitive type names to user-understandable text. Does depending on the dsdl naming sound like a good idea? For the generic plotters, for example, map any (t, temp, temperature) to "Kelvin" seems bad, because t could be used for time. Time could be milliseconds many times, although SI equivalent would be seconds. (Unless we add more meta-data)

I think the syntax you described for value extraction specification is generally sensible, although some minor details could be, perhaps, improved slightly. In general, I would recommend keeping the syntax somewhat reminiscent of DSDL (relevant: https://github.com/UAVCAN/specification/pull/46). I came up with a few examples:

123:uavcan.node.Heartbeat.0.1.health – node ID 123, data type uavcan.node.Heartbeat version 0.1, field health. The port ID is not specified, which means that we need to use the fixed port ID. If there was no fixed port ID defined with this data type, it would be a semantic error.

456:uavcan.register.Access.1.0(name="temperature").value.real32[0]@10Hz – node ID 456, data type uavcan.register.Access.1.0. This is a service type, so we must specify the request object, which is described as (name="temperature"). If the request schema contains composite type fields, things get complex: (baz=vendor.MyCompositeType.1.0(foo=123.456, bar=[1, 2, 3])), but it’s still sensible and so far I don’t see a simpler solution. If there are no fields to specify for the request object, the form is reduced to a pair of empty parens (); essentially this is to look like RPC (remote procedure call). Since this is a service type, we’ll never get any values until we explicitly ask for them, so we must specify the polling rate via @10Hz; the syntax is questionable though. The polling rate could default to 1 Hz if not specified.

The rate specifier like @10Hz could also be generalized to messages for rate limiting purposes, but this is probably not very useful in general.

We could incorporate filter expressions into this format later; for now it’s important to agree on the general syntax. The expressions like above essentially denote the concept of a “remote value”, which can be used flexibly to define visualization configurations and just about anything network-value-related in general. I imagine we could use a simple YAML schema for describing visualization configurations in particular:

visualizers:
    American temperature plot:      # Keys contain human-readable titles to be displayed in the GUI
        type: time-y
        y:  # The expressions convert values to Fahrenheit
            - (456:uavcan.register.Access.1.0(name="temperature.device").value.real32[0]@10Hz - 273.15) * 9/5 + 32
            - (456:uavcan.register.Access.1.0(name="temperature.outside").value.real32[0]@10Hz - 273.15) * 9/5 + 32
        y-right:  # "right" means the right-side Y axis, which is optional
            # Electrical power in kW:
            - 123:5678.uavcan.si.electric_current.Scalar.1.0.ampere * 456:5678.uavcan.si.voltage.Scalar.1.0.volt * 1e-3

    Camera image viewer:
        type: image
        image: 123:5678.vendor.camera.Frame1280x1024.1.0.pixels
        width: 123:5678.vendor.camera.Frame1280x1024.PIXELS_PER_ROW
        pixel_format: RGB888

    Simple position:
        type: x-y
        style: line
        palette: jet
        thickness: 3px
        x: 1234:5678.uavcan.si.length.WideVector3.meter[0]
        y: 1234:5678.uavcan.si.length.WideVector3.meter[1]
        color: 1234:5678.uavcan.si.length.WideVector3.meter[2]

    Satellites:
        type: histogram
        bin_size: 1
        value: 123:5678.uavcan.primitive.scalar.Integer8.1.0.value

(would you agree that “visualizer” might be a better word than “plotter”?)

In terms of the three-stage pipeline model I described here earlier, a “remote value specifier” string (like 1234:5678.uavcan.si.length.WideVector3.meter[2]) contains specification for the first two stages of the pipeline, and the YAML above specifies the last one.

The backend should be able to identify value specifiers that refer to the same object and reuse subscribers/service clients as much as possible to avoid resource overutilization. For example, 1234:5678.uavcan.si.length.WideVector3.meter[0] is repeated thrice where only the index differs; maintaining a dedicated subscription per value makes no sense here.

1 Like

The proposed JSON schema is not good: it fails to distinguish between static type information and runtime defined values. I propose an alternative schema which closely follows the DSDL data model.

  • Primitive values are represented in JSON as-is. For example, bool is just true or false, a floating point number is just a JSON float literal.
  • Void-typed values are not represented at all.
  • Array values are represented as JSON arrays.
  • Composite values are represented as JSON dicts, where each field has a dedicated entry indexed by name. Additionally, a special entry under the key _type_ is introduced, which contains the name and version of the data type of the current composite. See example below.

This is for Heartbeat:

{
    "_type_": ["uavcan.node.Heartbeat", 1, 0],
    "uptime": 123456,
    "health": 2,
    "mode": 0,
    "vendor_specific_status_code": 12345
}

This complex example is for register access request (name reads hello world; DSDL does not differentiate between arrays of uint8 and UTF8 text):

{
    "_type_": ["uavcan.register.Access.Request", 1, 0],
    "name": {
        "_type_": ["uavcan.register.Name", 1, 0],
        "name": [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
    },
    "value": {
        "_type_": ["uavcan.register.Value", 1, 0],
        "real32": {
            "_type_": ["uavcan.primitive.array.Real32", 1, 0],
            "value": [123.456]
        }
    }
}

The data type uavcan.register.Access.Request actually does not exist, there is only uavcan.register.Access which has a request schema definition.

This representation does not require us to transfer constant values over the REST interface, which is good because constant values never change. The front-end can request data type definition as necessary and cache the response forever since data type definitions never change (unless the major version number is zero — those may change and so they shouldn’t be cached or the cache should expire quickly). Data types can be modeled like in PyDSDL, by naive translation of the Python data structures into JSON. For example:

{
    "full_name": "uavcan.register.Access",
    "version": [1, 0],
    "is_service": true,
    "fixed_port_id": 384,
    "constants": {},
    "fields": {
        "request": {
            "type": ["uavcan.register.Access.Request", 1, 0],
        },
        "response": {
            "type": ["uavcan.register.Access.Response", 1, 0],
        }
    }
}
{
    "full_name": "uavcan.register.Access.Request",
    "version": [1, 0],
    "constants": {
        "EXAMPLE_CONSTANT": {
            "type": "saturated float64",
            "value": 123.456
        }
    },
    "fields": {
        "name": {
            "type": ["uavcan.register.Name", 1, 0]
        },
        "foo": {
            "type": ["uavcan.register.Value", 1, 0]
        },
        "bar": {
            "type": "saturated int32[<=123]"
        }
    }
}

Such representations can be constructed trivially with the help of PyDSDL.

1 Like

I’m going to prototype a dynamic UI generation based on a register’s data type.
Are all strings going to be utf8 text, encoded as an array like

?
If it is, how are we going to distinguish received int arrays from text?

How are we going to show the register’s value on the GRV, in a compact form? I could just show some constants as an array, till it does not fit, then hover over for complete overview (?)

Yes. For now, anyway. At some point we might add a dedicated string type.

Heuristically! Look:

Generally, we can say that an array is a string of text if it is of type uint8[<=] (meaning, a dynamic array of uint8, both saturated and truncated), is non-empty, and does not contain values other than [32, 126].

Yep, I think standard text overflow handling rules should work.

1 Like

Ok.

Would it make sense to represent a type with the primary version number?

uavcan.register.Access.Request, 1, 0 -> uavcan.register.Access.Request.1, 0 ? Much easier lookup and conflict resolution on the JS side. If not, I can always re-map it upon each request.

Maybe I am missing something here, but it seems inconsistent. We should either include both version numbers into the type name or none.

1 Like

Here’s a first prototype for a ‘generic value display’ component. Vue supports recursion inside components so, it’s super elegant to implement. I need some UX/UI input here.


Will do the dynamic form editor/generator later and the visualizer UI after that.

PS> I’ll be away at Brussels for a week, this is why I’ll miss the next 2 dev-calls. I’m planning to do much work this weekend and during easter holidays. I’ll be in touch though.

The value display looks cool. :fire:

Can it detect uint8[<=x] containing strings of text rather than bytes? Would be awesome to build this heuristic in, possibly also allowing the user to switch between text and bytes if necessary. Although this can be safely postponed until much later (just like radix selection for integer values and displaying of matrices).

Should we expect any scalability issues with bool[<=2032] or RGB888[1280*1024] pixels?

The last two questions lead me to suggest that maybe the idea of representation selection could be generalized for all value types. For example, for uint8[<=50] name, the set of available representations would include “bytes” and “text”; for RGB888[1280*1024] pixels that would be “array” and “image” (we can show an image, can we not?). Again, this is something we should have on the roadmap, right now it is not so critical.

I am not quite fascinated by the treatment of the version numbers. Is it possible to show either neither or both?

If the register value is encoded as uavcan.register.Access.Response, the flags mutable and persistent will be contained there, so it may not be necessary to repeat them in the outer structure. Alternatively, the fields can be kept in the outer structure and the value item can be of type uavcan.register.Value.

The reason why I suggested to keep the type information under _type_ is because the pattern _.*_ is a reserved word pattern in DSDL, meaning that this name is guaranteed to never conflict with a user-defined type, whereas _type might.

It’s just a prototype. Currently, it only recursively applies left margins and does recurse when a non-primitive value is found. I thought you decided to use type and not _type because you were used to that in python. I will switch it to type.

How should version numbers be displayed? name.primary.secondary , name(primary.secondary) I don’t know, you decide. I used that notation so that I can have fast lookup by object attribute (assuming that changes in only the secondary version number do not cause breaking changes, not sure what to do here, though).

Mutability and persistency can be whatever, as soon as it’s not a burden to extract their values.

Type (for display purposes) could be inferred and other possible representations could be shown as a drop-down, with our best-guess as the default. Javascript has now got typed arrays so we can easily do such things. I’m not sure how we can infer sizes without type information. The current prototype only depends on detecting leafs based on whether they have a defined _type attribute.

Also, the second row is in the format Node$id]. Do you want to show the node name as well somehow?

It is also needed to decide how the more compact/fitting in a table’s cell representation going to be.


After the GRV’s UX is decided, and the prototypes have settled down, I’ll migrate from per-component hierarchical state to app-global state with vuex, ensuring caching to disk or local storage as well.

I was also thinking that it would make sense to work on a tiny parser of the type text, that maps (where it is possible – mostly enums, text and primitive number types – not arrays) data type characteristics to form data validation fields.

.
(That is, strictly on the Yukon side, not intended for public usage)

Checking array inputs for user-input mistakes would be a bit more tricky, as it would require parsing textfields by hand and working out each element, but it’s not that hard to do.

Another quick question: Is the only place where we need to have different display methods on a type’s value, the array case?

A simple array check can be done, so that the user can select viewtype, from bitstring, array format, image, or text.

On the image side of things, would it be appropriate to assume that it is an image by the large size of the array? I don’t know what heuristic to use for the other ones. Maybe by leveraging pydsdl we can have a _displayhint_ attribute on that – one of [text, array, bitstring, image].

Yes, name.major.minor is the way to go.

The name could be useful but it’s not strictly necessary. Remember, the name can’t be changed by the user, it is set once by the vendor, so one system may have dozens of nodes under the same name.

I would make this generic. Say, an integer value could be viewed as decimal, binary, or hexadecimal. More display methods are likely to come up in the future.

Would it not be wise to postpone this until much later, when we have an actual MVP that can connect to other nodes and be generally useful?

1 Like

Let’s postpone the advanced display features for later. I updated the version display.

name[id] seems like a better choice for displaying the node name.

How are we going to represent non-primitive and array values on the GRV table?
Two things come to mind: either ... (dots) with a hover-over popup that shows the full details, or a simple traversal of the type tree, showing a truncated text of the first few primitives and arrays found, with that hover-over feature too.

How’s that for a register UI edit?

(client side data validations are wip)

A register value may be one of the few predefined types, all of which are arrays of primitives (except Empty, which contains nothing):

Therefore, my proposal is to display them as follows:

  • uavcan.primitive.Empty - special placeholder like “(none)”.
  • uavcan.primitive.String - as text.
  • uavcan.primitive.Bit - or .
  • uavcan.primitive.Unstructured, uavcan.primitive.array.Integer*, uavcan.primitive.array.Natural*, and uavcan.primitive.array.Real* - raw values (more clever representations to come, as discussed above).

I can see (from the dsdl definition) that these are the only types that the registers support. That implicitly signifies that there is only one indentation level on 384.Access.0.1.uavcan . Shall I refactor the current generic TypeValue component not to use recursion, assuming that we are only going to ever preview register values? (Im my mind: No, that needs to be generic, in a uniform way, showing some kind of preview without the need of knowing type info, just by recursively traversing the type tree, until you meet a constant).

Input is different though. Would there be a need on any other place inside Yukon, where a user expects to edit (on a web-form), a data type that is different than uavcan.register.Value ? If there is no need to support a more generic form generation mechanism, the UX could be improved and the SLoC drastically reduced. (Provided that the scope of the types of the form generation is only 1-nested level deep and said types are only the ones you mention on the table above).

Is the user expected to ever want to change a register’s Value to none ? Maybe we can treat an empty input field as none. Can none be used if the type is an array or any primitive? (I guess it signifies null or undefined ?)

Base64 encoded string representation too.