0

So I have couple of virtual goggle .. each one of them has different calibration parameters. I decided to save these parameters into a yaml file (as a configuration file) .. each goggle has its own serial/identification number ... and based on this number, I select which one to use. If there is no pre-saved information for the goggle. I calibrate it and I add these parameters into the file

So right now I am trying to write to a yaml file which looks like this :

Headset:
  IdentificationNumber: b630cc42-9a03-42da-a039-0e023cf5b090
  GyroOffset:
    GyroX:
      Value: -0.013776619
    GyroY:
      Value: -0.016475508
    GyroZ:
      Value: -0.0114268782

and this is what I get actually:

Headset2:
  IdentificationNumber: b630cc42-9a03-42da-a039-0e023cf5b090
? GyroOffset:
    GyroX:
      Value: -0.013776619
  ? GyroY:
      Value: -0.016475508
  : GyroZ:
      Value: -0.0114268782

I do not figure out what I am doing wrong ! .. here is my function which writes to the yaml file:

void ParseInputDeviceYaml::addCalibrationToConfigFile(const char* identificationNumber, const float* in)
{
    try {
        std::ofstream updatedFile;
        updatedFile.open(m_filename.toStdString(), std::ios::app);

        std::map<std::string, std::string>                  IDNumber;
        std::map<std::string, std::map<std::string, float>> gyroXOffset;
        std::map<std::string, std::map<std::string, float>> gyroYOffset;
        std::map<std::string, std::map<std::string, float>> gyroZOffset;

        IDNumber["IdentificationNumber"] = identificationNumber;

        gyroXOffset["GyroX"]["Value"] = *in;
        gyroYOffset["GyroY"]["Value"] = *(in + 1);
        gyroZOffset["GyroZ"]["Value"] = *(in + 2);

        YAML::Emitter newNode;


        newNode << YAML::BeginMap;
        newNode << YAML::Key << "Headset2";
        newNode << YAML::Value << YAML::BeginMap << YAML::Key << "IdentificationNumber" << YAML::Value << identificationNumber << YAML::EndMap;
        newNode << YAML::BeginMap << YAML::Key << "GyroOffset" << YAML::Value << gyroXOffset << gyroYOffset << gyroZOffset << YAML::EndMap;
        newNode << YAML::EndMap;

        updatedFile << newNode.c_str() << "\n";

        updatedFile.close();
    } catch (std::exception& e) {
        LOG4CPLUS_FATAL(m_logger, e.what());
        throw std::runtime_error(QObject::tr("Writing gyroscope offsets ").toStdString());
    }
}
Hesham.K
  • 29
  • 7
  • Why is everything in your YAML inside sequences? Do you expect one Headset to have multiple `IdentificationNumber`s or multiple `GyroOffset` lists? Reading it would be simpler if you dropped all the sequences, I don't see a reason for any of them. – flyx Jan 23 '20 at 21:36
  • Also, it's not good to have `Version` at the same level as `Headset1` (I assume there will be more headsets here) since `Version` is not a headset. This is an unnecessary burden to the implementation, it would be simpler if after `Version` there would be `Heasets:` which is a map containing the headsets. – flyx Jan 23 '20 at 21:39
  • I believe I changed the question now to be more concrete. For sure it is simpler to use the maps to read the data. But I have another problem in that. it is stated now in the question. – Hesham.K Jan 24 '20 at 09:36
  • Okay, it is more clear that the YAML you show with explicit `?` keys is the actual output; I wrote my answer assuming it is your crafted input. The problem is that you write a mapping into a key, which is still something that can be avoided if you use the approach with helper classes I suggested. – flyx Jan 24 '20 at 09:49

1 Answers1

0

The main problem seems to be that you're building up on a lot of misinformation. I'll try to clear some things:

  • Using sequences or not has absolutely nothing to do with being able to modify existing values or adding new values to the file. The problem is that you append to the file using std::ios::app, which will always create a new entry. Instead, you should load the file into a YAML node, modify the contents of that node, and then write back the whole node.
  • The YAML file without the sequence you give certainly does not do what you think it does since you place ? GyroOffset at the same depth as Headset2:, making it a sibling of Headset2. Also note that mixing implicit (foo:) with explicit (? foo) keys in the same mapping is kind-of a corner case that can confuses some implementations. The YAML file could simply look like this:
Headset2:
  IdentificationNumber: b630cc42-9a03-42da-a039-0e023cf5b090
  GyroOffset:
    GyroX:
      Value: -0.012388126
    GyroY:
      Value: -0.0155748781
    GyroZ:
      Value: -0.0115196211

To make your code more readable, I suggest to use helper classes to access your values. Assuming above code is the whole YAML file, it could look like this:

struct Value {
  YAML::Node data;
  // access existing node
  explicit Value(YAML::Node data): data(data) {
    assert(data.IsMapping());
  }
  // create new node
  explicit Value(float value) {
    data["Value"] = value;
  }

  float get() { return data["Value"].as<float>(); }
  void set(float value) { data["Value"] = value; }
};

struct GyroOffset {
  YAML::Node data;
  explicit GyroOffset(YAML::Node data): data(data) {
    assert(data.IsMapping());
  }
  GyroOffset(float x, float y, float z) {
    data["GyroX"] = Value(x).data;
    data["GyroY"] = Value(y).data;
    data["GyroZ"] = Value(z).data;
  }
  Value gyroX() { return Value(data["GyroX"]); }
  Value gyroX() { return Value(data["GyroY"]); }
  Value gyroZ() { return Value(data["GyroZ"]); }
};

struct Headset {
  YAML::Node data;
  Headset(YAML::Node data): data(data) {
    assert(data.IsMapping());
  }
  Headset(const char *id) {
    data["IdentificationNumber"] = id;
    // initialize with zero values
    data["GyroOffset"] = GyroOffset(0, 0, 0).data;
  }

  std::string id() { return data["IdentificationNumber"].as<std::string>(); }
  void setId(const char *value) { data["IdentificationNumber"] = value; }

  GyroOffset gyroOffset() { return GyroOffset(data["GyroOffset"]); }
}

Now, finding the GyroOffset of a given identification number looks like this (I show a simple function because I don't know your class' fields as you don't show them):

// write found values to output of found
bool findHedasetGyroOffset(Yaml::Node &input /* the file as shown above */, const char *id, GyroOffset &output) {
  for (auto it = input.begin(); it != input.end(); ++it) {
    Headset hs(it->second);
    if (hs.id() == id) {
      output = hs.gyroOffset();
      return true;
    }
  }
  return false;
}

Since YAML::Node is basically a reference, when you change values inside the returned GyroOffset, the original data changes. You can then write the root node back to the file (not append it) and have an updated file.

Appending a new headset would look like this:

void addCalibrationToConfigFile(Yaml::Node &file, const char* identificationNumber, const float* in) {
  Headset newHs(identificationNumber);
  auto go = newHs.gyroOffset();
  go.gyroX().set(*in);
  go.gyroY().set(*(in + 1));
  go.gyroZ().set(*(in + 2));
  // note that this will overwrite an existing Headset2
  file["Headset2"] = newHs.data;
}

While I tried to adhere to the structure you show, I have the feeling that the actual key in the mapping should not be Headset2, but the IdentificationNumber:

b630cc42-9a03-42da-a039-0e023cf5b090:
  Name: Headset2
  GyroOffset:
    GyroX:
      Value: -0.012388126
    GyroY:
      Value: -0.0155748781
    GyroZ:
      Value: -0.0115196211

Since you do lookup based on ID, this would make more sense. Also, creating a new config will actually work (currently, due to the hardcoded "Headset2" value, it will always overwrite that headset if it exists).

Beware, I wrote the code as demonstration and did not test it; there may be errors.

flyx
  • 35,506
  • 7
  • 89
  • 126
  • Thanks for the answer. I will look to it now and get to work on it :) For sure "Headset2" is hardcoded only for now. As soon as I start reading and writing with no problerms. I will think to introduce some sort of a counter which counts how many goggles are identified. That is why I thought of appending actually. So in the end the file will be like that Headset1: .. Headset2:.. Headst3: till Headsetn I will come back in case of further questions ;) ... have a great day ! – Hesham.K Jan 24 '20 at 10:04
  • If you don't have meaningful names for your headsets, *that* could certainly be a sequence instead of a map! :) – flyx Jan 24 '20 at 10:22
  • Okay now I read your code and I have couple of questions i like to understand as much as possible )) .. 1. as you said , you are making structures to help accessing the values.. what does explicit do actually ? I googled but I could not really quite get it with the converting and copy constructors ! ( is it necessary ? or its more of your coding style ? ) .. 2. I have no meaningful names for the headset , so it will be a sequence. I think I have to assert the headset structure to isSequence instead of mapping ? .. – Hesham.K Jan 24 '20 at 11:16
  • 3. Here only overwriting the node is possible, that is not my goal. After using the counter thing, Do I have to use emitter later .. or how will I append the new headset to the node ? Thanks a lot ! and sorry if they are too many questions :) – Hesham.K Jan 24 '20 at 11:16
  • 1. `explicit` avoids implicit conversions from `YAML::Node` to the wrapper class. You'll get warnings if you miss it on single-argument constructors with tools like clang-tidy. 2. Regarding the assertions, those are basically placeholders for real error handling but yes, `IsSequence` would be the thing to check. For 3. I'm not sure what you're missing, I showed code for both accessing an existing object that can be altered, and for appending a new object. After any modification, you always write the root node via the emitter. – flyx Jan 24 '20 at 11:44
  • Thanks ! I already started to implement your code. However, I get the error in both structures Headset and GyroOffset for the fucntions to retrieve the Value and GyroOffset "No matching conversion for functional-style cast from 'YAML::Node' to float .. or to HeadsetCalibrateParameters::Value" ... even though you initialized the constructors ?! for the 3rd point, I meant in addCalibrationToConfigFile , the Input argument is a YAML::Node .. but It has to be a YAML::Emitter if i want to write to the file or ? – Hesham.K Jan 24 '20 at 13:22
  • I removed the references (`YAML::Node data` -> `YAML::Node data`), that should fix it. My idea of the `addCalibrationToConfigFile` was that it updates the `YAML::Node` value, but actual writing will be done somewhere else (so you can modify multiple values before writing the file). – flyx Jan 24 '20 at 17:18
  • That has done the trick yes. Thank you so much for your help ! but I still want to know why that has done the trick ? )) why is it wrong to initialize the constructor with a reference to yaml node and however, its okay with a copy of yaml node ? – Hesham.K Jan 27 '20 at 11:19
  • It fixed the problem since the constructors were called with results of `YAML::Node::operator[]`, which has a return type of `YAML::Node`. Since this is a temporary value, it cannot be assigned to a modifyable (non-`const`) reference type. – flyx Jan 27 '20 at 12:03