Search code examples
c++parsingmapsyamlyaml-cpp

How to emit or write a map of maps in yaml file? c++


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());
    }
}

Solution

  • 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.