// // Copyright 2020 Ettus Research, a National Instruments Brand // // SPDX-License-Identifier: GPL-3.0-or-later // #include #include #include #include #include #include #include #include #include #include #include #include CMRC_DECLARE(rc); using namespace uhd::usrp::cal; namespace rc = cmrc::rc; namespace fs = boost::filesystem; namespace { constexpr char LOG_ID[] = "CAL::DATABASE"; constexpr char CAL_EXT[] = ".cal"; //! This value is just for sanity checking. We pick a value (in bytes) that we // are guaranteed to never exceed. Its only purpose is to avoid loading files // that can't possibly be valid cal data based on the filesize. This can avoid // someone bringing down a UHD session by trying to import a huge file, because // we first load it entirely into heap space, and then deserialize it from there. constexpr size_t CALDATA_MAX_SIZE = 10 * 1024 * 1024; // 10 MiB /****************************************************************************** * RC implementation *****************************************************************************/ //! Helper: Map a cal resource key into a source::RC path name std::string get_cal_path_rc(const std::string& key) { return std::string("cal/") + key + CAL_EXT; } //! Return true if a cal data resource with given key exists // // The serial parameter is ignored, as serial numbers should, by definition, not // matter for RC data bool has_cal_data_rc(const std::string& key, const std::string&) { auto fs = rc::get_filesystem(); const std::string cal_path(get_cal_path_rc(key)); UHD_LOG_TRACE(LOG_ID, "Checking for resource " << cal_path); return fs.is_file(cal_path); } //! Return a byte array for a given cal resource std::vector get_cal_data_rc(const std::string& key, const std::string&) { try { auto fs = rc::get_filesystem(); auto file = fs.open(get_cal_path_rc(key)); return std::vector(file.cbegin(), file.cend()); } catch (const std::system_error&) { throw uhd::key_error(std::string("Unable to open resource with key: ") + key); } } /****************************************************************************** * Filesystem implementation *****************************************************************************/ //! Helper: Check a path exists, or create it if not. Does not create recursively, // think mkdir, not mkdir -p. void check_or_create_dir(fs::path dir) { if (fs::exists(dir)) { if (fs::is_directory(dir)) { return; } UHD_LOG_ERROR(LOG_ID, "Path exists, but is not a directory: " << dir); throw uhd::runtime_error("Path exists, but is not a directory!"); } if (!fs::create_directory(dir)) { UHD_LOG_ERROR(LOG_ID, "Cannot create cal data directory: " << dir); throw uhd::runtime_error("Cannot create cal data directory!"); } UHD_LOG_DEBUG(LOG_ID, "Created directory: " << dir); } //! Make sure the calibration storage directory exists. // // The path returned by uhd::get_cal_data_path() might not exist (e.g., when run // for the first time). This directory must be created before we try writing to // it, or we won't be able to open the file. // // C++ doesn't have a mkdir -p equivalent, so we check the parent directory and // the directory itself, in that order. Most of the time, the cal data path is // in $XDG_DATA_HOME/uhd/cal_data. We assume that $XDG_DATA_HOME exists, and // then first check $XDG_DATA_HOME/uhd, then $XDG_DATA_HOME/uhd/cal_data. // // This will not work if the user sets $UHD_CAL_DATA_PATH to an arbitrary path // that requires multiple levels of directories to be created, but they will get // a clear error message in that case. void assert_cal_dir_exists() { const auto cal_path = fs::path(uhd::get_cal_data_path()); if (!cal_path.parent_path().empty()) { check_or_create_dir(cal_path.parent_path()); } check_or_create_dir(cal_path); } //! Helper: Map a cal resource key into a filesystem path name // (relative to get_cal_data_path()) std::string get_cal_path_fs(const std::string& key, const std::string& serial) { return key + "_" + serial + CAL_EXT; } //! Return true if a cal data resource with given key exists bool has_cal_data_fs(const std::string& key, const std::string& serial) { auto const cal_file_path = fs::path(uhd::get_cal_data_path()) / get_cal_path_fs(key, serial); UHD_LOG_TRACE(LOG_ID, "Checking for file at " << cal_file_path.string()); // We might want to check readability also return fs::exists(cal_file_path) && fs::is_regular_file(cal_file_path); } //! Return a byte array for a given filesystem resource std::vector get_cal_data_fs(const std::string& key, const std::string& serial) { if (!has_cal_data_fs(key, serial)) { throw uhd::key_error( std::string("Cannot find cal file for key=") + key + ", serial=" + serial); } const auto cal_file_path = fs::path(uhd::get_cal_data_path()) / get_cal_path_fs(key, serial); // We read the filesize first to do a sanity check (is this file small // enough to reasonably be cal data?) and also to pre-allocate heap space in // which we'll load the full data for future deserialization. const size_t filesize = fs::file_size(cal_file_path); if (filesize > CALDATA_MAX_SIZE) { throw uhd::key_error( std::string("The following cal data file exceeds maximum size limitations: ") + cal_file_path.string()); } std::vector result(filesize, 0); std::ifstream file(cal_file_path.string(), std::ios::binary); UHD_LOG_TRACE(LOG_ID, "Reading " << filesize << " bytes from " << cal_file_path); file.read(reinterpret_cast(&result[0]), filesize); return result; } } // namespace /****************************************************************************** * Flash/EEPROM implementation *****************************************************************************/ // Access to non-volatile memory is device-specific. Instead of implementing // anything here, we allow devices to register callbacks to look up cal data // from their EEPROMs / flash memories. using lookup_registry_type = std::vector>; UHD_SINGLETON_FCN(lookup_registry_type, get_flash_lookup_registry); bool has_cal_data_flash(const std::string& key, const std::string& serial) { for (auto& data_fn_pair : get_flash_lookup_registry()) { if (data_fn_pair.first(key, serial)) { return true; } } return false; } std::vector get_cal_data_flash(const std::string& key, const std::string& serial) { for (auto& data_fn_pair : get_flash_lookup_registry()) { if (data_fn_pair.first(key, serial)) { return data_fn_pair.second(key, serial); } } // No data? Then throw: throw uhd::key_error( std::string("Cannot find flash cal data for key=") + key + ", serial=" + serial); } /****************************************************************************** * Function lookup *****************************************************************************/ typedef bool (*has_cal_data_fn)(const std::string&, const std::string&); typedef std::vector (*get_cal_data_fn)(const std::string&, const std::string&); typedef std::tuple cal_data_fn_tuple; // These are in order of priority! // clang-format off constexpr std::array data_fns{{ cal_data_fn_tuple{source::FILESYSTEM, &has_cal_data_fs, &get_cal_data_fs }, cal_data_fn_tuple{source::FLASH, &has_cal_data_flash, &get_cal_data_flash}, cal_data_fn_tuple{source::RC, &has_cal_data_rc, &get_cal_data_rc } }}; // clang-format on /****************************************************************************** * cal::database implementation *****************************************************************************/ std::vector database::read_cal_data( const std::string& key, const std::string& serial, const source source_type) { for (auto& data_fn : data_fns) { if (source_type == source::ANY || source_type == std::get<0>(data_fn)) { if (std::get<1>(data_fn)(key, serial)) { return std::get<2>(data_fn)(key, serial); } } } const std::string err_msg = std::string("Calibration Data not found for: key=") + key + ", serial=" + serial; UHD_LOG_ERROR(LOG_ID, err_msg); throw uhd::key_error(err_msg); } bool database::has_cal_data( const std::string& key, const std::string& serial, const source source_type) { for (auto& data_fn : data_fns) { if (source_type == source::ANY || source_type == std::get<0>(data_fn)) { if (std::get<1>(data_fn)(key, serial)) { return true; } } } return false; } void database::write_cal_data(const std::string& key, const std::string& serial, const std::vector& cal_data, const std::string& backup_ext) { assert_cal_dir_exists(); const auto cal_file_path = (fs::path(uhd::get_cal_data_path()) / get_cal_path_fs(key, serial)).string(); if (fs::exists(cal_file_path)) { const auto ext = backup_ext.empty() ? std::to_string(time(NULL)) : backup_ext; const auto cal_file_path_backup = fs::path(uhd::get_cal_data_path()) / (get_cal_path_fs(key, serial) + "." + ext); UHD_LOG_WARNING(LOG_ID, "Calibration data already exists for key: `" << key << "' serial: `" << serial << "'. Backing up to: " << cal_file_path_backup); fs::rename(fs::path(cal_file_path), cal_file_path_backup); } std::ofstream file(cal_file_path, std::ios::binary); UHD_LOG_DEBUG(LOG_ID, "Writing to " << cal_file_path); file.write(reinterpret_cast(cal_data.data()), cal_data.size()); } void database::register_lookup(has_data_fn_type has_cal_data, get_data_fn_type get_cal_data, const source source_type) { UHD_ASSERT_THROW(source_type == source::FLASH); get_flash_lookup_registry().push_back({has_cal_data, get_cal_data}); }