fwt_software/src/core/CameraSettingsXml.cpp

220 lines
8.7 KiB
C++

#include "fgc/CameraSettingsXml.h"
#include <algorithm>
#include <ctime>
#include <fstream>
#include <map>
#include <sstream>
#include <stdexcept>
namespace fgc {
namespace {
// The input is machine-generated by the VimbaX Viewer and has a fixed, flat
// shape: <Feature Name=".." Value=".." Type=".." /> elements, some nested in
// <SelectorGroup Name=".." Value=".." >. A targeted scanner is enough for that
// and keeps the project free of an XML dependency - but it validates what it
// finds and throws rather than guessing, because a silently mis-parsed settings
// file would be worse than a missing one.
// Value of `attr` in `tag`, or "" when absent.
std::string attrValue(const std::string& tag, const std::string& attr) {
const std::string key = attr + "=\"";
const auto pos = tag.find(key);
if (pos == std::string::npos) return "";
const auto start = pos + key.size();
const auto end = tag.find('"', start);
if (end == std::string::npos) return "";
return tag.substr(start, end - start);
}
std::string slurp(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error("cannot read camera settings XML: " + path);
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
std::string nowIso8601() {
const std::time_t t = std::time(nullptr);
std::tm tm{};
localtime_r(&t, &tm);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tm);
return buf;
}
} // namespace
CameraSettings parseCameraSettingsXml(const std::string& path) {
const std::string xml = slurp(path);
CameraSettings out;
const auto info = xml.find("<CameraInfo");
if (info != std::string::npos) {
const auto close = xml.find('>', info);
const std::string tag = xml.substr(info, close - info);
out.model = attrValue(tag, "Model");
out.device_id = attrValue(tag, "Id");
}
// Only <RemoteDevice>: <LocalDevice> and the stream sections describe the
// host's transport layer, which is per-machine and must not be replayed.
const auto begin = xml.find("<RemoteDevice>");
if (begin == std::string::npos)
throw std::runtime_error("not a VimbaX settings export (no <RemoteDevice>): " + path);
const auto end = xml.find("</RemoteDevice>", begin);
if (end == std::string::npos)
throw std::runtime_error("malformed VimbaX settings export (unterminated "
"<RemoteDevice>): " + path);
const std::string body = xml.substr(begin, end - begin);
// Walk the block in order, tracking the enclosing SelectorGroup so a repeated
// feature name (BalanceRatio for Red and Blue, say) stays distinguishable.
std::string selector;
size_t i = 0;
while (i < body.size()) {
const auto lt = body.find('<', i);
if (lt == std::string::npos) break;
const auto gt = body.find('>', lt);
if (gt == std::string::npos) break;
const std::string tag = body.substr(lt, gt - lt + 1);
i = gt + 1;
if (tag.rfind("<SelectorGroup", 0) == 0) {
selector = attrValue(tag, "Name") + "=" + attrValue(tag, "Value");
} else if (tag.rfind("</SelectorGroup", 0) == 0) {
selector.clear();
} else if (tag.rfind("<Feature", 0) == 0) {
SettingEntry e;
e.name = attrValue(tag, "Name");
e.value = attrValue(tag, "Value");
e.selector = selector;
if (!e.name.empty()) out.entries.push_back(std::move(e));
}
}
if (out.entries.empty())
throw std::runtime_error("VimbaX settings export has no features: " + path);
return out;
}
std::string renderSettingsRecord(const CameraSettings& s, const std::string& xml_name,
const std::string& hash_label) {
std::ostringstream o;
o << "; ===========================================================================\n"
"; GENERATED - THIS FILE DOES NOT CONFIGURE THE CAMERA\n"
";\n"
"; A human-readable record of what " << xml_name << " contains, so the settings\n"
"; can be read and diffed without opening the Viewer. The program applies\n"
"; " << xml_name << " directly through the Vimba X SDK. Nothing here is ever\n"
"; written to the camera, and editing this file changes nothing.\n"
";\n"
"; To change the settings: open the VimbaX Viewer, configure the camera there\n"
"; (it validates combinations as you go), save the settings XML over\n"
"; " << xml_name << ", and restart. This file is then regenerated and the\n"
"; changes are listed in the log.\n"
"; ===========================================================================\n\n";
o << "[Source]\n"
<< "xml = " << xml_name << "\n"
<< "xml_hash = " << hash_label << "\n"
<< "generated = " << nowIso8601() << "\n";
if (!s.model.empty()) o << "model = " << s.model << "\n";
if (!s.device_id.empty()) o << "device_id = " << s.device_id << "\n";
o << "\n[Settings]\n";
std::string current_selector;
for (const auto& e : s.entries) {
// Reproduce the selector context as a comment: without it a repeated
// feature name reads as a duplicate rather than a per-selector value.
if (e.selector != current_selector) {
current_selector = e.selector;
o << "; --- " << (current_selector.empty() ? "(none)" : current_selector) << "\n";
}
o << e.name << " = " << e.value << "\n";
}
return o.str();
}
SettingsRecord readSettingsRecord(const std::string& path) {
SettingsRecord rec;
std::ifstream in(path);
if (!in) return rec; // absent: caller treats it as a mismatch
std::string line;
bool in_settings = false;
std::string selector;
while (std::getline(in, line)) {
// Selector context is written as a comment; recover it so a diff can tell
// BalanceRatio-for-Red from BalanceRatio-for-Blue.
if (line.rfind("; --- ", 0) == 0) {
selector = line.substr(6);
if (selector == "(none)") selector.clear();
continue;
}
if (!line.empty() && line[0] == ';') continue;
if (line == "[Settings]") { in_settings = true; continue; }
if (!line.empty() && line[0] == '[') { in_settings = false; continue; }
const auto eq = line.find('=');
if (eq == std::string::npos) continue;
auto trim = [](std::string v) {
const auto a = v.find_first_not_of(" \t\r");
if (a == std::string::npos) return std::string();
const auto b = v.find_last_not_of(" \t\r");
return v.substr(a, b - a + 1);
};
const std::string key = trim(line.substr(0, eq));
const std::string val = trim(line.substr(eq + 1));
if (in_settings) {
rec.entries.push_back({key, val, selector});
} else if (key == "xml_hash") {
rec.hash_label = val;
}
}
return rec;
}
std::vector<std::string> diffSettings(const std::vector<SettingEntry>& before,
const std::vector<SettingEntry>& after) {
// Key by selector + name + occurrence index. A real Viewer export repeats the
// same SelectorGroup name AND value (FileOperationSelector=Open appears once
// per FileSelector group), so selector+name alone is not unique - keying on it
// collapsed the duplicates and reported every one after the first as "new",
// burying a one-line change under hundreds of phantom ones.
auto keyed = [](const std::vector<SettingEntry>& v) {
std::map<std::string, const SettingEntry*> m;
std::map<std::string, int> seen;
for (const auto& e : v) {
const std::string base = e.selector + "|" + e.name;
m[base + "|" + std::to_string(seen[base]++)] = &e;
}
return m;
};
auto label = [](const SettingEntry& e) {
return e.selector.empty() ? e.name : e.name + " [" + e.selector + "]";
};
auto old_by_key = keyed(before);
auto new_by_key = keyed(after);
std::vector<std::string> out;
for (const auto& [k, e] : new_by_key) {
auto it = old_by_key.find(k);
if (it == old_by_key.end()) {
out.push_back("+ " + label(*e) + " " + e->value + " (new)");
} else if (it->second->value != e->value) {
out.push_back(label(*e) + " " + it->second->value + " -> " + e->value);
}
}
for (const auto& [k, e] : old_by_key)
if (new_by_key.find(k) == new_by_key.end())
out.push_back("- " + label(*e) + " (removed)");
return out;
}
} // namespace fgc