ami-iit / yarp-openmct

Repo for YARP and OpenMCT integration.
BSD 3-Clause "New" or "Revised" License
6 stars 1 forks source link

Add iFeel suit ports to the telemetry server and respective entries in the visualizer GUI #43

Closed nunoguedelha closed 1 year ago

nunoguedelha commented 2 years ago

We discussed with @RiccardoGrieco on the requirements for visualizing the iFeel suit sensor information within the Open-CT based visualizer tool. We recap below a quick guide on how to add a new telemetry data source Yarp port to the modules iCubTelemVizServer and openmctStaticServer:

(throughout the following description, we shall use the IMU measurements entry as an example) image.png

(*) A JSON string is a string containing nested JSON object literals. A JSON object literal is a list of key/value pairs. Refer to https://www.w3schools.com/js/js_json_objects.asp. (**) In this example the IMU measurements are included in the group "iCub Telemetry" but you can create a new group of measurements where to add your new telemetry entries.

nunoguedelha commented 2 years ago

Hi @RiccardoGrieco , let me know if I left some point obscure of it's clear enough, or feel free to amend the guide yourself.

nunoguedelha commented 2 years ago

CC @evalli-iit , @lrapetti , @S-Dafarra , @traversaro

S-Dafarra commented 2 years ago

I think this is way too complicated to be done every time we need to add a new port :thinking:

RiccardoGrieco commented 2 years ago

@nunoguedelha thank you for the recap, I think you have not missed anything.

Anyway, as discussed with @evalli-iit and @lrapetti, the idea is to monitor and control all the nodes of the iFeel system with a more "product-oriented" GUI, that also does not need YARP to run. Therefore the visualizer will not be used for this purpose.

The tool will instead be used to visualize data related to the human state, but further discussion is still needed.

For the moment, I'm keeping this issue in the icebox.

nunoguedelha commented 1 year ago

Ciao @RiccardoGrieco , could you please give me the following info:

RiccardoGrieco commented 1 year ago

At the following link the definition of the messages: https://github.com/robotology/wearables/blob/b9a6fe404b837e1b594c89ac1a1ac37dd46a1567/msgs/thrift/WearableData.thrift#L173-L191

Attached instead there is the output of yarp read. The port is/iFeelSuit/WearableData/data:o.

iFeelSuitMessages.txt

RiccardoGrieco commented 1 year ago

A small dataset that can be replayed with yarpdataplayer --withExtraTimeCol 2

ifeel_data.zip

nunoguedelha commented 1 year ago

@RiccardoGrieco , I would have a few questions regarding the format file. Below I mention the higher structure and as an example the Accelerometer structure: https://github.com/robotology/wearables/blob/b9a6fe404b837e1b594c89ac1a1ac37dd46a1567/msgs/thrift/WearableData.thrift#L173-L191

struct WearableData {
1: required string producerName;
2: optional map<string,Accelerometer> accelerometers;
3: optional map<string,EmgSensor> emgSensors;
4: optional map<string,Force3DSensor> force3DSensors;
5: optional map<string,ForceTorque6DSensor> forceTorque6DSensors;
6: optional map<string,FreeBodyAccelerationSensor> freeBodyAccelerationSensors;
7: optional map<string,Gyroscope> gyroscopes;
8: optional map<string,Magnetometer> magnetometers;
9: optional map<string,OrientationSensor> orientationSensors;
10: optional map<string,PoseSensor> poseSensors;
11: optional map<string,PositionSensor> positionSensors;
12: optional map<string,SkinSensor> skinSensors;
13: optional map<string,TemperatureSensor> temperatureSensors;
14: optional map<string,Torque3DSensor> torque3DSensors;
15: optional map<string,VirtualLinkKinSensor> virtualLinkKinSensors;
16: optional map<string,VirtualJointKinSensor> virtualJointKinSensors;
17: optional map<string,VirtualSphericalJointKinSensor> virtualSphericalJointKinSensors;
}

https://github.com/robotology/wearables/blob/b9a6fe404b837e1b594c89ac1a1ac37dd46a1567/msgs/thrift/WearableData.thrift#L89-L92

struct Accelerometer {
  1: SensorInfo info;
  2: VectorXYZ data;
}

https://github.com/robotology/wearables/blob/b9a6fe404b837e1b594c89ac1a1ac37dd46a1567/msgs/thrift/WearableData.thrift#L17-L20

struct SensorInfo{
  1: string name;
  2: SensorStatus status = SensorStatus.UNKNOWN;
}
lrapetti commented 1 year ago

I will try to answer

  • Are the higher level map key and the SensorInfo.name always the same on the currently streamed data?
  • If that's the case, out of curiosity, why the redundancy?
  • Why not just use n array of Accelerometer structures, or the Accelerometer structure have only the status and data?

Yes, that seems to be the case, it can be seen from the code here and here. Actually, it seems to be redundant, but changing it would break the compatibility with all the previous dataset, so for the time being I would avoid to consider it.

nunoguedelha commented 1 year ago

Sure, it was just for my understanding. It's pretty clear, thanks @lrapetti .

nunoguedelha commented 1 year ago

Implementation steps

In order to properly handle the optional telemetry entries and group them conveniently, we shall use here the capability of adding sub-folders to a telemetry dictionary (#133 , implemented by #149 ).

nunoguedelha commented 1 year ago

Add the iFeel suit telemetry dictionary main entry and sub-folders (empty for now)

image

Commit 80036a9f2ddf0e3d876ced686dc88c301a9960ab.

Dictionary structure

  "name": "iFeel Suit Telemetry",
  "key": "iFeelSuitTelemetry",
  ...
  "telemetryEntries": [
    {
      "name": "Accelerometers",
      "key": "accSens",
      "type": "folder",
      "telemetryEntries": []
    },
    {
      "name": "EMG Sensors",
      "key": "emgSens",
      "type": "folder",
      "telemetryEntries": []
    },
    ...
  ]
nunoguedelha commented 1 year ago

Edited https://github.com/ami-iit/yarp-openmct/issues/43#issuecomment-1275113568.

nunoguedelha commented 1 year ago

Updated the estimate to 5.

nunoguedelha commented 1 year ago

Add the code skeleton for the iFeel suit telemetry port configuration, the data parsing, the dictionary generation...

Parsing of the incoming Yarp data and notification to the web client:

https://github.com/ami-iit/yarp-openmct/blob/fd6931481ce0e098f82ae96fdb09c52e14361062/iCubTelemVizServer/wearableDataParser.js#L9-L19

Open-MCT populates the iFeel Telemetry tree through the objectAPI by calling the objectProvider and compositionProvider. During this process:

Results

The overall code data flow is working except that the telemetry entries, iFeelSuitTelemetry.accSens.iFeelSuit::acc::Node#10 and iFeelSuitTelemetry.accSens.iFeelSuit::acc::Node#11, are not clickable, so no subscription is ever sent to the telemetry server. image

Debugging now the Vue.js...

nunoguedelha commented 1 year ago

Telemetry Entries not Clickable...

Vue.js Debugging

We observe the click event handling when e click on an telemetry entry.

Successful case: if we click on the the entry iCub Telemetry->IMU sensors->Legacy IMU sensor measurements, we get the following function call sequence:

handleClick
boundFn
navigateOrPreview
navigate
handleLocationChange
doPathChange
Details on sequence ending... https://github.com/nasa/openmct/blob/ab7e2c57470d3c6076459ad7e3eaa1883cc3b8b0/src/ui/router/ApplicationRouter.js#L300-L318 ``` /** * @private * Compare new and old path and on change emit event 'change:path' * * @param {string} newPath new path of url * @param {string} oldPath old path of url */ doPathChange(newPath, oldPath) { if (newPath === oldPath) { return; } let route = this.routes.filter(r => r.matcher.test(newPath))[0]; if (route) { route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); } this.emit('change:path', newPath, oldPath); } ```

The full function doPathChange is executed and the change:path event is emitted.

Failing case: Instead, if we click on the the entry iFeel Suit Telemetry->Accelerometers->iFeelSuit::acc::Node#10, we get the same function call sequence as before, but with a different execution in the last function call doPathChange.

https://github.com/nasa/openmct/blob/ab7e2c57470d3c6076459ad7e3eaa1883cc3b8b0/src/ui/router/ApplicationRouter.js#L300-L318

    /**
     * @private
     * Compare new and old path and on change emit event 'change:path'
     *
     * @param {string} newPath new path of url
     * @param {string} oldPath old path of url
     */
    doPathChange(newPath, oldPath) {
        if (newPath === oldPath) {
            return;
        }

        let route = this.routes.filter(r => r.matcher.test(newPath))[0];
        if (route) {
            route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); <-------- (*)
        }

        this.emit('change:path', newPath, oldPath);
    }

(*) The route to the new path never returns and times out and the event change:path is never emitted.

This is due to the fact that the routed path was /browse/yarpopenmct:iFeelSuitTelemetry/yarpopenmct:iFeelSuitTelemetry.accSens/yarpopenmct:iFeelSuitTelemetry.accSens.iFeelSuit::acc::Node where #10 is missing.

That last segment was truncated in handleLocationChange by the URL constructor: https://github.com/nasa/openmct/blob/ab7e2c57470d3c6076459ad7e3eaa1883cc3b8b0/src/ui/router/ApplicationRouter.js#L355

   let newLocation = this.createLocation(pathString);

https://github.com/nasa/openmct/blob/ab7e2c57470d3c6076459ad7e3eaa1883cc3b8b0/src/ui/router/ApplicationRouter.js#L287-L290

   let url = new URL(
       pathString,
       `${location.protocol}//${location.host}${location.pathname}`
   );

For allowing a free naming of the iFeel suit sensors, while avoiding such issues, we'll use just numbers as the last segment of the domain object keys.

Fixed in commit f5ff44d8b18a99ab10beb4cd6a07ca8c8b5df58e.

nunoguedelha commented 1 year ago

Complete the folders child entry IDs retrieval implementation

Result

image

Here we replayed with the yarpdataplayer the dataset ifeel_data.zip twice to trigger tje pipeline but we were only sending dummy data to the visualizer client.

Implemented in 927ae72d10742028e03cda16db1d3c318fa24162.

nunoguedelha commented 1 year ago

Complete the telemetry sample parsing

Implemented in 4ebb02676bd230e2ccec2f3bba6ed40aeeb48ec0.

[^1]: Each of the Map telemKeyTree entries has a respective index which matches the modality index i. E.g. "iFeelSuitTelemetry.accSens" matches modality index 0.

nunoguedelha commented 1 year ago

Test and Issues

Tested https://github.com/ami-iit/yarp-openmct/files/9714389/ifeel_data.zip and found the following issues:

nunoguedelha commented 1 year ago

Analysis Issue 1

Reference Object API call sequence

E.g. for the entry iFeelSuit::gyro::Node#1, we get the sequence:

-> navigateToPath(identifier)
    -> pathToObjects(identifier)
        -> migrate(identifier)
            -> openmct.objects.get(identifier)
                -> provider.get(identifier)

For the identifier.key iteratively equal to iFeelSuitTelemetry, iFeelSuitTelemetry.gyroSens, iFeelSuitTelemetry.gyroSens.0, which follows the decomposition of the last object path. Subsequently, we get the sequence:

-> getOriginalPath(iFeelSuitTelemetry.gyroSens.0)
    -> migrate(iFeelSuitTelemetry.gyroSens.0)
        -> ...
    -> getOriginalPath(domainObject.location = iFeelSuitTelemetry)
        -> migrate(iFeelSuitTelemetry)
            -> ...
        -> getOriginalPath(domainObject.location = ROOT)
            -> migrate(ROOT)
                -> ...

Faulty Object API call sequence

For the entry iFeelSuit::fbAcc::Node#1, only the first sequence is executed for each of the domain objects in the path:

-> navigateToPath(identifier)
    -> pathToObjects(identifier)
        -> migrate(identifier)
            -> openmct.objects.get(identifier)
                -> provider.get(identifier)

The call sequence initiating in getOriginalPath() does not occur. The problem occurs in between.

We then look into the domain object returned by the ObjectProvider for the object iFeelSuitTelemetry.FBaccSens.0. We can see in the below image that the returned result has wrong content in telemetry.values.

image

That is probably the cause preventing the execution sequence that should have followed.

nunoguedelha commented 1 year ago

Analysis Issue 1 ...continued

dictionary.presetValuesBase has wrong content for keys FBaccSens, orientSens and positionSens.

{
  "presetValuesBase": {
    "FBaccSens": "[object Object],[object Object],[object Object]"
    "orientSens": "[object Object],[object Object],[object Object],[object Object]"
    "positionSens": "[object Object],[object Object],[object Object]"
  }
}

There was a JSON.stringify() missing for those entries.

Fixed in 7e980cb7cced49b5902c71f97b9b09506ed59792.

nunoguedelha commented 1 year ago

Analysis Issue 2

For instance, value.oriQuat.w, value.pos.x, value.linearVel.x are always identical.

Fixed in 62ba537a6a3da320fef3718f391c3588bdfb7df6.

nunoguedelha commented 1 year ago

Optimised wearable data parsing in 75a4b570170f3ba6d47878465911a63cc935be03 and 390b246cd628a1aff415d0375d935706f48dd28e. Refer to commit comments for further details.

The goal was to:

This allows to execute, in the anonymous functions, lines like:

[parsedData.oriQuat.w,parsedData.oriQuat.x,parsedData.oriQuat.y,parsedData.oriQuat.z,parsedData.pos.x,parsedData.pos.y,parsedData.pos.z,parsedData.linearVel.x,parsedData.linearVel.y,parsedData.linearVel.z,parsedData.angVel.x,parsedData.angVel.y,parsedData.angVel.z,parsedData.linearAcc.x,parsedData.linearAcc.y,parsedData.linearAcc.z,parsedData.angAcc.x,parsedData.angAcc.y,parsedData.angAcc.z] = sensorData;

directly mapping the sensorData table elements to the fields of parsedData, instead of iteratively parsing the quaternion data, then the position, linear velocity, etc. Such line is not hardcode, but instead generated in the constructor from the structure WearableDataParser.telemKeyTree.

nunoguedelha commented 1 year ago

Work complete.

CC @S-Dafarra @RiccardoGrieco