// calib_dump — M3 step 6.1: Watchman flash config backup tool. // // Thin CLI over device/tundra_config.h ReadWatchmanConfig (the protocol, // retry behavior, and read-twice-identical discipline live there; this // tool adds files, checksums, and archive verification). // // calib_dump [--imu-serial LHR-...] [--out dir] [--verify-json file.json] // // Writes /.config.z (raw zlib stream) and .config.json // (inflated). --verify-json semantically compares against an archived dev // JSON — formatting differs by source and the SteamVR lighthouse cache is // LOSSY (observed: cache ipd block kept only default_mm; flash carries // enable/high/low/minstep too), so a flash-side superset with equal common // values passes with a note. // // Exit codes: 0 ok, 2 device/protocol, 5 verify mismatch. #include "device/tundra_config.h" #include #include #include #include #include #include using namespace sauna; namespace { uint64_t fnv1a(const uint8_t* p, size_t n) { uint64_t h = 1469598103934665603ull; for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 1099511628211ull; } return h; } bool writeFile(const std::string& path, const void* data, size_t n) { FILE* f = fopen(path.c_str(), "wb"); if (!f) { fprintf(stderr, "cannot write %s\n", path.c_str()); return false; } fwrite(data, 1, n, f); fclose(f); return true; } } // namespace int main(int argc, char** argv) { setbuf(stdout, nullptr); const char* imuSerial = ""; std::string outDir = "."; std::string verifyJson; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--imu-serial") && i + 1 < argc) imuSerial = argv[++i]; else if (!strcmp(argv[i], "--out") && i + 1 < argc) outDir = argv[++i]; else if (!strcmp(argv[i], "--verify-json") && i + 1 < argc) verifyJson = argv[++i]; } std::string text, err, serial; std::vector rawZ; if (!ReadWatchmanConfig(imuSerial, &text, &err, &serial, &rawZ)) { fprintf(stderr, "%s\n", err.c_str()); return 2; } printf("unit: %s\n", serial.c_str()); printf("dump stable: %zu bytes compressed, fnv1a %016llx (x2 identical)\n", rawZ.size(), (unsigned long long)fnv1a(rawZ.data(), rawZ.size())); printf("inflated: %zu bytes JSON, fnv1a %016llx\n", text.size(), (unsigned long long)fnv1a((const uint8_t*)text.data(), text.size())); const std::string base = outDir + "\\" + serial; if (!writeFile(base + ".config.z", rawZ.data(), rawZ.size())) return 2; if (!writeFile(base + ".config.json", text.data(), text.size())) return 2; printf("wrote %s.config.z and %s.config.json\n", base.c_str(), base.c_str()); if (!verifyJson.empty()) { nlohmann::json jf, ja; try { jf = nlohmann::json::parse(text); FILE* f = fopen(verifyJson.c_str(), "rb"); if (!f) { fprintf(stderr, "cannot read %s\n", verifyJson.c_str()); return 5; } std::string a; char buf[4096]; size_t n; while ((n = fread(buf, 1, sizeof(buf), f)) > 0) a.append(buf, n); fclose(f); ja = nlohmann::json::parse(a); } catch (const std::exception& e) { fprintf(stderr, "VERIFY parse error: %s\n", e.what()); return 5; } if (jf == ja) { printf("VERIFY: semantically identical to %s\n", verifyJson.c_str()); } else { bool fail = false; for (auto it = ja.begin(); it != ja.end(); ++it) { if (!jf.contains(it.key())) { fprintf(stderr, "VERIFY: archive key '%s' missing from flash\n", it.key().c_str()); fail = true; } else if (jf[it.key()] != it.value()) { bool superset = it.value().is_object() && jf[it.key()].is_object(); if (superset) for (auto s = it.value().begin(); s != it.value().end(); ++s) if (!jf[it.key()].contains(s.key()) || jf[it.key()][s.key()] != s.value()) superset = false; if (superset) { printf("VERIFY note: '%s' flash superset of archive (cache " "lossy), common values equal\n", it.key().c_str()); } else { fprintf(stderr, "VERIFY: key '%s' DIFFERS\n", it.key().c_str()); fail = true; } } } if (fail) { fprintf(stderr, "VERIFY FAILED vs %s\n", verifyJson.c_str()); return 5; } printf("VERIFY: values match %s (flash superset)\n", verifyJson.c_str()); } } printf("BACKUP OK\n"); return 0; }