Files
edgetx-gx12/radio/src/storage/modelslist.cpp
T

1632 lines
44 KiB
C++

/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "modelslist.h"
#include <algorithm>
using std::list;
#if defined(SDCARD_YAML)
#include "opentx.h"
#include "storage/sdcard_yaml.h"
#include "yaml/yaml_datastructs.h"
#include "yaml/yaml_labelslist.h"
#include "yaml/yaml_modelslist.h"
#include "yaml/yaml_parser.h"
#endif
#if defined(USBJ_EX)
#include "usb_joystick.h"
#endif
#include <cstring>
#include "datastructs.h"
#include "myeeprom.h"
#include "pulses/modules_helpers.h"
#include "strhelpers.h"
#ifdef DEBUG_LABELS
#define TRACE_LABELS(...) TRACE(__VA_ARGS__)
#else
#define TRACE_LABELS(...)
#endif
ModelsList modelslist;
ModelMap modelslabels;
ModelCell::ModelCell(const char *fileName) : valid_rfData(false)
{
strncpy(modelFilename, fileName, sizeof(modelFilename) - 1);
modelFilename[sizeof(modelFilename) - 1] = '\0';
}
ModelCell::ModelCell(const char *fileName, uint8_t len) : valid_rfData(false)
{
if (len > sizeof(modelFilename) - 1) len = sizeof(modelFilename) - 1;
memcpy(modelFilename, fileName, len);
modelFilename[len] = '\0';
}
void ModelCell::setModelName(char *name)
{
strncpy(modelName, name, LEN_MODEL_NAME);
modelName[LEN_MODEL_NAME] = '\0';
if (modelName[0] == '\0') {
char *tmp;
strncpy(modelName, modelFilename, LEN_MODEL_NAME);
tmp = (char *)memchr(modelName, '.', LEN_MODEL_NAME);
if (tmp != nullptr) *tmp = 0;
}
}
void ModelCell::setModelName(char *name, uint8_t len)
{
if (len > LEN_MODEL_NAME - 1) len = LEN_MODEL_NAME - 1;
memcpy(modelName, name, len);
modelName[len] = '\0';
if (modelName[0] == '\0') {
char *tmp;
strncpy(modelName, modelFilename, LEN_MODEL_NAME);
tmp = (char *)memchr(modelName, '.', LEN_MODEL_NAME);
if (tmp != nullptr) *tmp = 0;
}
}
void ModelCell::setModelId(uint8_t moduleIdx, uint8_t id)
{
modelId[moduleIdx] = id;
}
void ModelCell::setRfData(ModelData *model)
{
for (uint8_t i = 0; i < NUM_MODULES; i++) {
modelId[i] = model->header.modelId[i];
setRfModuleData(i, &(model->moduleData[i]));
TRACE("<%s/%i> : %X,%X,%X", strlen(modelName) ? modelName : modelFilename,
i, moduleData[i].type, moduleData[i].subType, modelId[i]);
}
valid_rfData = true;
}
void ModelCell::setRfModuleData(uint8_t moduleIdx, ModuleData *modData)
{
moduleData[moduleIdx].type = modData->type;
if (modData->type != MODULE_TYPE_MULTIMODULE) {
moduleData[moduleIdx].subType = (uint8_t)modData->subType;
} else {
// do we care here about MM_RF_CUSTOM_SELECTED? probably not...
moduleData[moduleIdx].subType = modData->multi.rfProtocol;
}
}
//-----------------------------------------------------------------------------
/**
* @brief Gets all models which don't have any labels selected
*
* @param sortby
* @return ModelsVector vector<ModelCell>
*/
ModelsVector ModelMap::getUnlabeledModels()
{
ModelsVector unlabeledModels;
for (auto model : modelslist) {
if (modelslabels.getLabelsByModel(model).size() == 0)
unlabeledModels.emplace_back(model);
}
sortModelsBy(unlabeledModels, _sortOrder);
return unlabeledModels;
}
/**
* @brief Returns a sorted list of all models
*/
ModelsVector ModelMap::getAllModels()
{
ModelsVector all = modelslist;
sortModelsBy(all, _sortOrder);
return all;
}
/**
* @brief Returns all models that have a sepecified label
*
* @param lbl Label to search
* @return ModelsVector aka vector<ModelCell*> of all models belonging to a
* label
*/
ModelsVector ModelMap::getModelsByLabel(const std::string &lbl)
{
int index = getIndexByLabel(lbl);
if (index < 0) return ModelsVector();
ModelsVector rv;
for (auto it = begin(); it != end(); ++it) {
if (it->first == index) rv.push_back(it->second);
}
sortModelsBy(rv, _sortOrder);
return rv;
}
/**
* @brief Returns all models that are in multiple labels (OR function)
*
* @param lbls Labels to search
* @return ModelsVector aka vector<ModelCell*> of all models belonging to a
* label
*/
ModelsVector ModelMap::getModelsByLabels(const LabelsVector &lbls)
{
bool addunlabeled = false;
// Build a list of the requested indexes
std::vector<int> idxvect;
for (const auto &lbl : lbls) {
if (lbl == STR_UNLABELEDMODEL) addunlabeled = true;
int index = getIndexByLabel(lbl);
if (index >= 0) idxvect.push_back(index);
}
ModelsVector rv;
for (auto it = begin(); it != end(); ++it) {
for (auto idx : idxvect) {
if (it->first == idx) rv.push_back(it->second);
}
}
if (addunlabeled) {
ModelsVector unlabeled = getUnlabeledModels();
rv.insert(rv.end(), unlabeled.begin(), unlabeled.end());
}
sortModelsBy(rv, _sortOrder);
return rv;
}
/**
* @brief Returns all models that match the selected labels
*
* @param lbls Labels to search
* @return ModelsVector aka vector<ModelCell*> of all models belonging to a
* label
*/
ModelsVector ModelMap::getModelsInLabels(const LabelsVector &lbls)
{
if (lbls.size() == 0) return ModelsVector();
// Requesting only Unlabeled models
if (lbls.size() == 1 && lbls.at(0) == STR_UNLABELEDMODEL)
return getUnlabeledModels();
ModelsVector rv;
for (const auto &mdl : modelslist) {
bool hasAllLabels = true;
bool hasAnyLabels = false;
bool favLabelIncluded = false;
bool hasFavLabel = false;
LabelsVector mdllables = getLabelsByModel(mdl);
for (const auto &lbl : lbls) {
if (lbl == STR_UNLABELEDMODEL) // If requesting unlabeled model ignore it
break;
bool hasLabel = std::find(mdllables.begin(), mdllables.end(), lbl) != mdllables.end();
if (lbl == STR_FAVORITE_LABEL) {
favLabelIncluded = true;
hasFavLabel = hasLabel;
} else {
if (hasLabel) {
hasAnyLabels = true;
} else {
hasAllLabels = false;
}
}
}
if (favLabelIncluded) {
if (g_eeGeneral.favMultiMode == 0) {
hasAnyLabels = hasAnyLabels && hasFavLabel;
hasAllLabels = hasAllLabels && hasFavLabel;
} else if (g_eeGeneral.favMultiMode == 1) {
hasAnyLabels = hasAnyLabels || hasFavLabel;
hasAllLabels = hasAllLabels && hasFavLabel;
}
}
if (((g_eeGeneral.labelMultiMode == 0) && hasAllLabels) ||
((g_eeGeneral.labelMultiMode == 1) && hasAnyLabels))
rv.push_back(mdl);
}
sortModelsBy(rv, _sortOrder);
return rv;
}
/**
* @brief Gets all labels that a model currently has selected
*
* @param mdl Model to search
* @return LabelsVector aka vector<string> of all labels selected by a model
*/
LabelsVector ModelMap::getLabelsByModel(ModelCell *mdl)
{
if (mdl == nullptr) return LabelsVector();
LabelsVector rv;
for (auto it = begin(); it != end(); ++it) {
if (it->second == mdl) {
rv.push_back(getLabelByIndex(it->first));
}
}
return rv;
}
/**
* @brief Get a map of all labels and their selection status
* @details Returns a map of all the labels and if they are selected in a model.
*
* @param cell ModelCell to search
* @return std::map<std::string, bool> Map Containing all labels with a boolean
* if they are selected
*/
std::map<std::string, bool> ModelMap::getSelectedLabels(ModelCell *cell)
{
std::map<std::string, bool> rval;
for (auto lbl : labels) { // Add all labels
if (lbl == "") continue;
rval[lbl] = false;
}
LabelsVector modlbl = getLabelsByModel(cell);
for (const auto &ml : modlbl) { // Set to true if selected by the model
rval[ml] = true;
}
return rval;
}
/**
* @brief Returns if a label is selected in the model
*
* @param label Label to search
* @param cell Model to search
* @return true Label is selected
* @return false Label not seleced OR label not found
*/
bool ModelMap::isLabelSelected(const std::string &label, ModelCell *cell)
{
auto lbm = getLabelsByModel(cell);
if (std::find(lbm.begin(), lbm.end(), label) == lbm.end()) {
return false;
}
return true;
}
/**
* @brief Returns a list of the current labels
*
* @return LabelsVector vector<string> of all labels
*/
LabelsVector ModelMap::getLabels()
{
LabelsVector rv;
for (auto &label : labels) {
// Skip the internal empty (removed) ones
if (label.size() > 0) rv.emplace_back(label);
}
return rv;
}
/**
* @brief Adds a label
* @details Checks if the label already exists. If it does it returns the
* index to it. If label doesn't exist it adds it at the end of the
* list and returns the new index
* Won't allow creation of the special case label "Unlabeled" STR_UNLABELEDMODEL
*
* @param lbl Adds a label to the list
* @return int -1 on failure, label index on success
*/
int ModelMap::addLabel(std::string lbl)
{
if (lbl == STR_UNLABELEDMODEL) return -1;
// Limit maximum label length, TODO... Truncate UTF8 Properly
lbl = lbl.substr(0, LABEL_LENGTH);
removeYAMLChars(lbl);
if (lbl.size() == 0) return -1;
// Add a new label if it doesn't already exist in the list
// Returns the index to the label
int ind = getIndexByLabel(lbl);
if (ind < 0) {
labels.push_back(lbl);
setDirty();
TRACE_LABELS("Added a label %s", lbl.c_str());
return labels.size() - 1;
}
return ind;
}
/**
* @brief Adds a label to a model
*
* @param lbl Label to be added
* @param cell Model to add the label
* @param update If true will update the model file
* @return true Couldn't add label, not enough memory available in labels string
* @return false Success
*/
bool ModelMap::addLabelToModel(const std::string &lbl, ModelCell *cell,
bool update)
{
// First check that there aren't too many labels on this model
LabelsVector lbs = getLabelsByModel(cell);
lbs.push_back(lbl);
if(ModelMap::toCSV(lbs).size() > LABELS_LENGTH - 1) {
TRACE("Cannot add the %s label to the model. Too many labels", lbl.c_str());
return true;
}
setDirty();
int labelindex = addLabel(lbl);
insert(std::pair<int, ModelCell *>(labelindex, cell));
if (update) updateModelFile(cell); // Write labels into model
return false;
}
/**
* @brief Adds a label to the filter, used in yaml_labelslist on load
*
* @param label Label to be added
*/
void ModelMap::addFilteredLabel(const std::string &lbl)
{
int ind = getIndexByLabel(lbl);
if (ind >= 0) filtlbls.insert(ind);
}
bool ModelMap::isLabelFiltered(const std::string &lbl)
{
if (filtlbls.find(getIndexByLabel(lbl)) != filtlbls.end()) return true;
return false;
}
void ModelMap::setSortOrder(ModelsSortBy sortby)
{
if (sortby < SORT_COUNT && sortby >= NO_SORT) {
_sortOrder = sortby;
setDirty();
}
}
/**
* @brief Convert a vector of labels to a comma separated list
*
* @param labels Vector of the labels
* @return labels list in CSV format
*/
std::string ModelMap::toCSV(const LabelsVector &labels)
{
std::string csv;
bool comma=false;
for(auto lbl: labels) {
if(lbl == "")
continue;
if(comma) {
csv += ',';
}
// Escape out commas
escapeCSV(lbl);
csv += lbl;
comma = true;
}
return csv;
}
/**
* @brief Convert a Comma separated string list to LabelsVector
*
* @param str Comma separated string of labels
* @return vector of all labels
*/
LabelsVector ModelMap::fromCSV(const char* str)
{
LabelsVector lbls;
const char* prev_c = str;
const char* c = strchr(prev_c, ',');
while(c != nullptr) {
std::string lbl(prev_c, c - prev_c);
unEscapeCSV(lbl);
lbls.push_back(lbl);
prev_c = ++c;
c = strchr(c, ',');
}
std::string lbl(prev_c);
if (!lbl.empty()) {
unEscapeCSV(lbl);
lbls.push_back(lbl);
}
return lbls;
}
// TODO - Fix me, ideally there should be no limitations
void ModelMap::escapeCSV(std::string &str)
{
replace_all(str, "/", "//");
replace_all(str, ",", "/c");
}
// TODO - Fix me, ideally there should be no limitations
void ModelMap::unEscapeCSV(std::string &str)
{
replace_all(str, "//", "/");
replace_all(str, "/c", ",");
}
// TODO - Fix me, ideally there should be no limitations
void ModelMap::removeYAMLChars(std::string &str)
{
replace_all(str, "\\", "");
replace_all(str, "\"", "");
replace_all(str, ":", "");
replace_all(str, "\'", "");
replace_all(str, "-", "");
}
/**
* @brief Replace all occurances of <from> to <to> in <str>
*
* @param str String
* @param from String to search
* @param to String to replace
*/
void ModelMap::replace_all(std::string &str,
const std::string &from,
const std::string &to)
{
if(from.empty()) return;
size_t pos = 0;
while((pos = str.find(from, pos)) != std::string::npos) {
str.replace(pos, from.length(), to);
pos += to.length();
}
}
/**
* @brief Removes a label from a model
*
* @param label Label to be removed
* @param cell Model to remove the label from
* @return true Couldn't find the label
* @return false Success
*/
bool ModelMap::removeLabelFromModel(const std::string &label, ModelCell *cell,
bool update)
{
int lblind = getIndexByLabel(label);
if (lblind < 0) return true;
bool rv = true;
// Erase items that match in the map
for (ModelMap::const_iterator itr = cbegin(); itr != cend();) {
itr = (itr->first == lblind && itr->second == cell) ? erase(itr)
: std::next(itr);
setDirty();
rv = false;
}
if (update) updateModelFile(cell); // Write labels into model
return rv;
}
/**
* @brief Removes a label
* @details Remove a label from the list, only if there are no models that have
* the label selected. The label is set blank, when writing this will be
* skipped. Labels are re-synced
*
* @param label Label to be removed
* @return true Label wasn't found or not empty
* @return false Success
*/
bool ModelMap::removeLabel(
const std::string &label,
std::function<void(const char *file, int progress)> progress)
{
bool renameFault=true;
renameLabel(label, "", std::move(progress));
for (auto &lbl : labels) {
if (lbl == label && getModelsByLabel(lbl).size() == 0) {
lbl = "";
setDirty();
renameFault = false;
}
}
// If no more labels, add a favorite
if (!renameFault && getLabels().size() == 0) {
addLabel(STR_FAVORITE_LABEL);
}
// Save and rescan
modelslist.save();
modelslist.clear();
modelslist.load();
return renameFault;
}
/**
* @brief Moves a labels order
*
* @param current Index of the label to move
* @param toindex Index where to place the new label
* @return 1 Failure
* @return 0 Success
*/
bool ModelMap::moveLabelTo(unsigned curind, unsigned newind)
{
if (curind == newind || curind >= labels.size() || newind >= labels.size())
return true;
if (labels.at(curind) == "") return true;
std::swap(labels[curind], labels[newind]);
ModelMap newmap;
newmap.labels = labels;
for (auto &mm : modelslabels) {
uint16_t ind = mm.first;
if (ind == curind)
ind = newind;
else if (ind == newind)
ind = curind;
newmap.insert(std::make_pair(ind, mm.second));
}
modelslabels = newmap;
modelslist.save(labels);
setDirty();
return false;
}
/**
* @brief Rename a label
* @details Opens all models which have a label that matches the <from> string.
* Renames the label with <to> string and saves file.
* If working on the current model it replaces the label string in
* g_model
*
* @param from Label to search
* @param to Replacement label
* @return true failure Label couldn't be found, YAML couldn't be read
* @return false success
*/
bool ModelMap::renameLabel(const std::string &from, std::string to,
std::function<void(const char *file, int progress)> progress)
{
if (from == "") return true;
DEBUG_TIMER_START(debugTimerYamlScan);
if(to.size() > 0) { // Ignore check if deleting a label, size will be zero
to = to.substr(0, LABEL_LENGTH); // Limit max label name. TODO: Warn user they entered too long of a string
removeYAMLChars(to); // Remove special chars
if(to.size() == 0 || from == to) { // Abort rename if no chars left or same
if (progress != nullptr) progress("", 100); // Kill progress dialog
return true;
}
}
ModelData *modeldata = (ModelData *)malloc(sizeof(ModelData));
if (!modeldata) {
TRACE("Labels: Out Of Memory");
if (progress != nullptr) progress("", 100); // Kill progress dialog
return true;
}
// Force a write of any changes in memory
storageCheck(true);
bool fault = false;
ModelsVector mods = getModelsByLabel(from); // Find all models to be renamed
// Scan all these models first, recombine their labels to a csv,
// make sure re-size is going to fit before starting. Otherwise new partial
// labels would be created on next scan.
for(const auto &model: mods) {
int curlen = toCSV(getLabelsByModel(model)).size();
std::string csvto = to;
escapeCSV(csvto);
std::string csvfrom = from;
escapeCSV(csvfrom);
if(curlen + csvto.size() - csvfrom.size() > LABELS_LENGTH - 1) {
TRACE("Labels: Rename Error! Labels too long on %s", model->modelName);
if (progress != nullptr) progress("", 100); // Kill progress dialog
free(modeldata);
return true;
}
}
int i = 0;
for (const auto &modcell : mods) {
if (progress != nullptr) {
progress(modcell->modelFilename, (i++) * 100 / mods.size());
}
readModelYaml(modcell->modelFilename, (uint8_t *)modeldata,
sizeof(ModelData));
// Separate Curent CSV
LabelsVector lbls = ModelMap::fromCSV(modeldata->header.labels);
// Replace from->to strings
for (auto &lbl : lbls) {
if (lbl == from) lbl = to;
}
// Remove possible duplicates
std::sort(lbls.begin(), lbls.end());
auto last = std::unique(lbls.begin(), lbls.end());
lbls.erase(last, lbls.end());
lbls.resize(std::distance(lbls.begin(), last));
// Write back
strncpy(modeldata->header.labels, ModelMap::toCSV(lbls).c_str(), LABELS_LENGTH);
modeldata->header.labels[LABELS_LENGTH-1] = '\0';
char path[256];
getModelPath(path, modcell->modelFilename);
if (modcell == modelslist.getCurrentModel()) {
// If working on the current model, write current data to file instead
memcpy(g_model.header.labels, modeldata->header.labels, LABELS_LENGTH);
fault = (writeFileYaml(path, get_modeldata_nodes(),
(uint8_t *)&g_model, 0) != NULL);
} else {
fault = (writeFileYaml(path, get_modeldata_nodes(),
(uint8_t *)modeldata, 0) != NULL);
}
#if defined(SIMU)
if (SIMU_SLEEP_OR_EXIT_MS(100)) break;
#endif
}
// Rename the label
for (auto &lbl : labels) {
if (lbl == from) {
lbl = to;
setDirty(true);
}
}
// Make sure to leave at 100, to kill rename dialog
if (progress != nullptr) progress("", 100);
free(modeldata);
// Issue a rescan all of all models.
modelslist.clear();
modelslist.load();
#if defined(DEBUG_TIMERS)
DEBUG_TIMER_SAMPLE(debugTimerYamlScan);
TRACE("Labels: Time to rename %d labels %luus", mods.size(),
debugTimers[debugTimerYamlScan].getLast());
#endif
return fault;
}
/**
* @brief Returns a bullet separated list of the labels, used in model_setup
*
* @param curmod Model Cell
* @param noresults String to return when no labels found
* @return std::string of all Labels, if no results return
*/
std::string ModelMap::getBulletLabelString(ModelCell *curmod, const char *noresults)
{
std::string lbls = ModelMap::toCSV(getLabelsByModel(curmod));
replace_all(lbls, ",", "\u2022");
unEscapeCSV(lbls);
if(lbls.size() == 0) {
return noresults;
}
if(lbls.size() > LABEL_TRUNCATE_LENGTH) {
lbls = lbls.substr(0, LABEL_TRUNCATE_LENGTH);
lbls += "...";
}
return lbls;
}
/**
* @brief Removes all models from the map
*
* @param cell Model to remove
* @return true Failure finding a model
* @return false Success
*/
bool ModelMap::removeModels(ModelCell *cell)
{
bool rv = true;
// Erase items that match in the map
for (ModelMap::const_iterator itr = cbegin(); itr != cend();) {
if (itr->second == cell) {
itr = erase(itr);
setDirty();
rv = false;
} else {
itr = std::next(itr);
}
}
return rv;
}
/**
* @brief Opens a model.yml File and writes labels data into it.
* @details If the cell is current model then write the labels data to g_model
* and mark as dirty
*
* @param cell
* @return true
* @return false
*/
bool ModelMap::updateModelFile(ModelCell *cell)
{
// Update memory copy if on current model
if (cell == modelslist.getCurrentModel()) {
strncpy(g_model.header.labels, ModelMap::toCSV(getLabelsByModel(cell)).c_str(),
LABELS_LENGTH - 1);
g_model.header.labels[LABELS_LENGTH - 1] = '\0';
storageDirty(EE_MODEL);
return false;
}
ModelData *modeldata = (ModelData *)malloc(sizeof(ModelData));
if (!modeldata) {
TRACE("Labels: Out Of Memory");
return true;
}
bool fault = false;
readModelYaml(cell->modelFilename, (uint8_t *)modeldata, sizeof(ModelData));
strncpy(modeldata->header.labels, ModelMap::toCSV(getLabelsByModel(cell)).c_str(),
LABELS_LENGTH - 1);
modeldata->header.labels[LABELS_LENGTH - 1] = '\0';
char path[256];
getModelPath(path, cell->modelFilename);
fault = (writeFileYaml(path, get_modeldata_nodes(), (uint8_t *)modeldata, 0) !=
NULL);
free(modeldata);
#if defined(DEBUG_TIMERS)
DEBUG_TIMER_SAMPLE(debugTimerYamlScan);
TRACE("Labels: Time to add/remove labels %luus",
debugTimers[debugTimerYamlScan].getLast());
#endif
return fault;
}
/**
* @brief Sorts a ModelsVector by sortby
*
* @param mv ModelsVector to sort
* @param sortby NO_SORT, NAME_ASC, NAME_DES, DATE_ASC, DATE_DES,
*/
void ModelMap::sortModelsBy(ModelsVector &mv, ModelsSortBy sortby)
{
if (sortby == DATE_DES) {
std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool {
return a->lastOpened > b->lastOpened;
});
} else if (sortby == DATE_ASC) {
std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool {
return a->lastOpened < b->lastOpened;
});
} else if (sortby == NAME_ASC) {
std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool {
return strcasecmp(a->modelName, b->modelName) < 0;
});
} else if (sortby == NAME_DES) {
std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool {
return strcasecmp(a->modelName, b->modelName) > 0;
});
}
}
/**
* @brief Sets the ModelMap to dirty.
* @details Causes labels.yml to be written after a delay in
* sdcard_common.cpp->storageCheck()
*/
void ModelMap::setDirty(bool save)
{
_isDirty = true;
storageDirty(EE_LABELS);
if (save) storageCheck(true);
}
//-----------------------------------------------------------------------------
ModelsList::ModelsList() { init(); }
ModelsList::~ModelsList() { clear(); }
void ModelsList::init()
{
loaded = false;
currentModel = nullptr;
}
void ModelsList::clear()
{
for(ModelCell *mdl: *this) {
delete(mdl);
}
std::vector<ModelCell *>::clear();
init();
}
/**
* @brief Load and parse the models.txt file
*
* @return true On Success
* @return false On Failure
*/
bool ModelsList::loadTxt()
{
char line[LEN_MODELS_IDX_LINE + 1];
ModelCell *model = nullptr;
FRESULT result =
f_open(&file, RADIO_MODELSLIST_PATH, FA_OPEN_EXISTING | FA_READ);
if (result == FR_OK) {
// TXT reader
while (readNextLine(line, LEN_MODELS_IDX_LINE)) {
int len = strlen(line); // TODO could be returned by readNextLine
if (len > 2 && line[0] == '[' && line[len - 1] == ']') {
line[len - 1] = '\0';
} else if (len > 0) {
model = new ModelCell(line);
push_back(model);
if (!strncmp(line, g_eeGeneral.currModelFilename, LEN_MODEL_FILENAME)) {
currentModel = model;
}
}
}
f_close(&file);
return true;
}
return false;
}
#if defined(SDCARD_YAML)
/**
* @brief Opens a YAML file, reads the data and updates the ModelCell
*
* @param cell Model to update
*/
void ModelMap::updateModelCell(ModelCell *cell)
{
modelslabels.removeModels(cell);
ModelData *model = (ModelData *)malloc(sizeof(ModelData));
if (!model) {
TRACE("Labels: Out Of Memory");
return;
}
TRACE("Labels: Updating model %s", cell->modelFilename);
readModelYaml(cell->modelFilename, (uint8_t *)model, sizeof(ModelData));
strncpy(cell->modelName, model->header.name, LEN_MODEL_NAME);
cell->modelName[LEN_MODEL_NAME] = '\0';
strncpy(cell->modelBitmap, model->header.bitmap, LEN_BITMAP_NAME);
cell->modelBitmap[LEN_BITMAP_NAME] = '\0';
LabelsVector labels = ModelMap::fromCSV(model->header.labels);
for(const auto &lbl : labels ) {
modelslabels.addLabelToModel(lbl,cell);
}
// Save Module Data
cell->setRfData(model);
cell->_isDirty = false;
free(model);
}
/**
* @brief Creates a Hash based on the file information
*
* @param buffer Buffer to store the output data
* @param finfo Files info handle
* @return char* Pointer to buffer supplied
*/
char *FILInfoToHexStr(char buffer[17], FILINFO *finfo)
{
char *str = buffer;
for (unsigned int i = 0; i < sizeof(FInfoH); i++) {
sprintf(str, "%02x", *((uint8_t *)finfo + i));
str += 2;
}
return buffer;
}
/**
* @brief Loads the Labels and Models from the labels.yml file
*
* @return true On success
* @return false On failure
*/
bool ModelsList::loadYaml()
{
// Clear labels + map
modelslist.clear();
modelslabels.clear();
fileHashInfo.clear();
DEBUG_TIMER_START(debugTimerYamlScan);
// Scan all models in folder
DIR moddir;
FILINFO finfo;
if (f_opendir(&moddir, MODELS_PATH) == FR_OK) {
for (;;) {
FRESULT res = f_readdir(&moddir, &finfo);
if (res != FR_OK || finfo.fname[0] == 0) break;
if (finfo.fattrib & AM_DIR) continue;
unsigned int len = strlen(finfo.fname);
// Only open model###.yml files
bool modelNameInvalid = false;
if (strncasecmp(finfo.fname, MODEL_FILENAME_PREFIX, sizeof(MODEL_FILENAME_PREFIX) - 1) == 0) {
for (unsigned int i = sizeof(MODEL_FILENAME_PREFIX) - 1; i < len - 4; i++) {
if(finfo.fname[i] < '0' || finfo.fname[i] > '9') {
modelNameInvalid = true;
break;
}
}
} else {
modelNameInvalid = true;
}
if (modelNameInvalid ||
strcasecmp(finfo.fname + len - 4, YAML_EXT) || // Skip non .yml files
(finfo.fattrib & AM_DIR)) { // Skip sub dirs
continue;
}
// Store hash & filename
filedat cf;
FILInfoToHexStr(cf.hash, &finfo);
cf.name = finfo.fname;
cf.celladded = false;
if (!strncmp(finfo.fname, g_eeGeneral.currModelFilename,
LEN_MODEL_FILENAME))
cf.curmodel = true;
else
cf.curmodel = false;
fileHashInfo.push_back(cf);
TRACE_LABELS("File - %s \r\n HASH - %s - CM: %s", finfo.fname, cf.hash,
cf.curmodel ? "Y" : "N");
}
f_closedir(&moddir);
}
// Check if models.yml exists
// Any files found above that are not listed in the file will be moved into
// /MDOELS/UNUSED and removed from the discovered file hash list
char line[LEN_MODELS_IDX_LINE + 1];
FILINFO fno;
FRESULT result;
bool foundInModels = f_stat(MODELSLIST_YAML_PATH, &fno) == FR_OK;
bool foundInRadio = f_stat(FALLBACK_MODELSLIST_YAML_PATH, &fno) == FR_OK;
if(foundInModels) { // Default to /Models copy
result = f_open(&file, MODELSLIST_YAML_PATH, FA_OPEN_EXISTING | FA_READ);
} else if (foundInRadio) {
result = f_open(&file, FALLBACK_MODELSLIST_YAML_PATH, FA_OPEN_EXISTING | FA_READ);
}
if((foundInModels || foundInRadio) && result == FR_OK) {
// Create /Models/Unused if it doesn't exist
bool moveRequired = false;
DIR unusedFolder;
FRESULT result = f_opendir(&unusedFolder, UNUSED_MODELS_PATH);
if (result != FR_OK) {
if (result == FR_NO_PATH) result = f_mkdir(UNUSED_MODELS_PATH);
if (result != FR_OK) {
TRACE("Unable to create unused models folder");
f_close(&file);
return false;
}
} else f_closedir(&unusedFolder);
YamlParser ymp;
std::vector<std::string> modfiles;
void *ctx = get_modelslist_iter(&modfiles);
ymp.init(get_modelslist_parser_calls(), ctx);
UINT bytes_read = 0;
while (f_read(&file, line, sizeof(line), &bytes_read) == FR_OK) {
if (bytes_read == 0) break;
if (f_eof(&file)) ymp.set_eof();
if (ymp.parse(line, bytes_read) != YamlParser::CONTINUE_PARSING) break;
}
f_close(&file);
// Loop through file hases, move any files found that don't exists to /unused
std::vector<filedat> newFileHash;
for(const auto &fhas: fileHashInfo) {
bool found = false;
for(const auto &file : modfiles) {
if (file == fhas.name) {
TRACE_LABELS("Found file %s in models.yml.. OK!", file);
found = true;
break;
}
}
if(!found) {
moveRequired = true;
TRACE_LABELS("Model %s not in models.yml, moving to /UNUSED", fhas.name.c_str());
// Move model into unused folder.
const char *warning = sdMoveFile(fhas.name.c_str(), MODELS_PATH, fhas.name.c_str(), UNUSED_MODELS_PATH);
if(warning)
POPUP_WARNING(warning);
} else {
newFileHash.push_back(fhas); // File exists, keep it
}
}
if(foundInRadio) {
const char *warning = sdMoveFile(MODELS_FILENAME, RADIO_PATH, MODELS_FILENAME ".old", UNUSED_MODELS_PATH);
if(warning)
POPUP_WARNING(warning);
}
if(foundInModels) { // Will overwrite the copy from /radio if both existed, do last
const char *warning = sdMoveFile(MODELS_FILENAME, MODELS_PATH, MODELS_FILENAME ".old", UNUSED_MODELS_PATH);
if(warning)
POPUP_WARNING(warning);
}
if(moveRequired) {
fileHashInfo = newFileHash; // Update the new file list
POPUP_WARNING(TR_MODELS_MOVED "\n" UNUSED_MODELS_PATH, "\n" TR_PRESS_ANY_KEY_TO_SKIP);
}
}
#if defined(DEBUG_TIMERS)
DEBUG_TIMER_SAMPLE(debugTimerYamlScan);
TRACE("Lables: Time to scan models folder %luus",
debugTimers[debugTimerYamlScan].getLast());
#endif
// Scan labels.yml
result = f_open(&file, LABELSLIST_YAML_PATH, FA_OPEN_EXISTING | FA_READ);
if (result == FR_OK) {
YamlParser yp;
void *ctx = get_labelslist_iter();
yp.init(get_labelslist_parser_calls(), ctx);
UINT bytes_read = 0;
while (f_read(&file, line, sizeof(line), &bytes_read) == FR_OK) {
if (bytes_read == 0) break;
if (f_eof(&file)) yp.set_eof();
if (yp.parse(line, bytes_read) != YamlParser::CONTINUE_PARSING) break;
}
f_close(&file);
}
#if defined(DEBUG_TIMERS)
DEBUG_TIMER_SAMPLE(debugTimerYamlScan);
TRACE("Lables: Time to scan labels.yml %luus",
debugTimers[debugTimerYamlScan].getLast());
#endif
// Add modelcells for any remaining models that weren't in labels.yml
for (auto &filehash : fileHashInfo) {
ModelCell *model = NULL;
if (filehash.celladded == false) {
TRACE_LABELS(" Created a modelcell for %s, not in labels.yml",
filehash.name.c_str());
model = new ModelCell(filehash.name.c_str());
strncpy(model->modelFinfoHash, filehash.hash, FILE_HASH_LENGTH);
model->modelFinfoHash[FILE_HASH_LENGTH] = '\0';
modelslist.push_back(model);
filehash.celladded = true;
model->_isDirty = true;
if(filehash.curmodel == true)
modelslist.setCurrentModel(model);
}
}
// Scan all models, to see if update needed
bool updatelabelsyml = false;
for (auto &model : modelslist) {
// Open and read each model if it's marked as dirty
if (model->_isDirty) {
updatelabelsyml = true;
modelslabels.updateModelCell(model);
}
}
fileHashInfo.clear();
// If any items differed save the file
if (updatelabelsyml == true) {
TRACE_LABELS("LABELS.YML Wasn't in sync. Needs to be saved");
modelslist.save();
} else {
TRACE_LABELS("LABELS.YML Is in Sync! No models were read");
}
// If no labels found. Add a favorites label
if (modelslabels.getLabels().size() == 0) {
modelslabels.addLabel(STR_FAVORITE_LABEL);
}
return true;
}
#endif
/**
* @brief Called to load the model data from file
*
* @param fmt Format::txt - Opens models.txt file, Format::yaml_txt - Opens
* labels.yml
* @return true on success
* @return false on failure
*/
bool ModelsList::load(Format fmt)
{
if (loaded) return true;
bool res = false;
#if !defined(SDCARD_YAML)
(void)fmt;
res = loadTxt();
#else
FILINFO fno;
if (fmt == Format::txt ||
(fmt == Format::yaml_txt && f_stat(MODELSLIST_YAML_PATH, &fno) != FR_OK &&
f_stat(FALLBACK_MODELSLIST_YAML_PATH, &fno) != FR_OK)) {
res = loadTxt();
} else {
res = loadYaml();
}
#endif
if (!currentModel) {
TRACE("ERROR no Current Model Found");
if (modelslist.size()) {
modelslist.setCurrentModel(modelslist.at(0));
strncpy(g_eeGeneral.currModelFilename, modelslist.at(0)->modelFilename,
sizeof(g_eeGeneral.currModelFilename));
g_eeGeneral.currModelFilename[sizeof(g_eeGeneral.currModelFilename) - 1] = '\0';
TRACE(" - Set current model to first available");
} else {
TRACE(" - No Models Found, making a new one");
// No models found, make a new one
auto model = modelslist.addModel(createModel(), true);
modelslist.setCurrentModel(model);
updateCurrentModelCell();
}
}
loaded = true;
return res;
}
/**
* @brief Writes labels.yml file
* @param newOrder vector<string> - Forces a save of this label order. leave empty to use current
* order.
* @return const char* NULL on success
* @return const char* Error String on failure
*/
const char *ModelsList::save(LabelsVector newOrder)
{
#if !defined(SDCARD_YAML)
FRESULT result =
f_open(&file, RADIO_MODELSLIST_PATH, FA_CREATE_ALWAYS | FA_WRITE);
#else
FRESULT result =
f_open(&file, LABELSLIST_YAML_PATH, FA_CREATE_ALWAYS | FA_WRITE);
#endif
if (result != FR_OK) return "Couldn't open labels.yml for writing";
// Save current selection
f_puts("Labels:\r\n", &file);
std::string cursel = modelslabels.getCurrentLabel();
if(newOrder.empty())
newOrder = modelslabels.getLabels();
for (auto &lbl : newOrder) {
f_printf(&file, " \"%s\":\r\n", lbl.c_str());
if (modelslabels.isLabelFiltered(lbl))
f_printf(&file, " selected: true\r\n", lbl.c_str());
}
// Save current sort order
f_printf( &file, "Sort: %d\r\n", modelslabels.sortOrder());
f_puts("Models:\r\n", &file);
for (auto &model : modelslist) {
f_puts(" ", &file);
f_puts(model->modelFilename, &file);
f_puts(":\r\n", &file);
f_puts(" hash: \"", &file);
f_puts(model->modelFinfoHash, &file);
f_puts("\"\r\n", &file);
f_puts(" name: \"", &file);
f_puts(model->modelName, &file);
f_puts("\"\r\n", &file);
for (int i = 0; i < NUM_MODULES; i++) {
if (model->modelId[i])
f_printf(&file, " " MODULE_ID_STR ": %u\r\n", i,
(unsigned int)model->modelId[i]);
if (model->moduleData[i].type)
f_printf(&file, " " MODULE_TYPE_STR ": %u\r\n", i,
(unsigned int)model->moduleData[i].type);
if (model->moduleData[i].subType)
f_printf(&file, " " MODULE_RFPROTOCOL_STR ": %u\r\n", i,
(unsigned int)model->moduleData[i].subType);
}
f_printf(&file, " labels: \"%s\"\r\n", ModelMap::toCSV(modelslabels.getLabelsByModel(model)).c_str());
#if LEN_BITMAP_NAME > 0
f_puts(" bitmap: \"", &file);
f_puts(model->modelBitmap, &file);
f_puts("\"\r\n", &file);
#endif
f_puts(" lastopen: ", &file);
f_puts(std::to_string(model->lastOpened).c_str(), &file);
f_puts("\r\n", &file);
}
f_puts("\r\n", &file);
f_close(&file);
modelslabels._isDirty = false;
return NULL;
}
/**
* @brief set the currently loaded model.
*
* @param cell Model to set as current
*/
void ModelsList::setCurrentModel(ModelCell *cell)
{
currentModel = cell;
struct gtm t;
gettime(&t);
cell->lastOpened = gmktime(&t);
modelslabels.setDirty();
#if defined(USBJ_EX) && defined(STM32) && !defined(SIMU)
onUSBJoystickModelChanged();
#endif
}
/**
* @brief Takes the current g_model + filename data in memory and update
* the ModelCell data to match.
*/
void ModelsList::updateCurrentModelCell()
{
if (currentModel) {
#if LEN_BITMAP_NAME > 0
strncpy(currentModel->modelBitmap, g_model.header.bitmap, LEN_BITMAP_NAME);
currentModel->modelBitmap[LEN_BITMAP_NAME] = '\0';
#endif
strncpy(currentModel->modelFilename, g_eeGeneral.currModelFilename, LEN_MODEL_FILENAME);
currentModel->modelFilename[LEN_MODEL_FILENAME] = '\0';
currentModel->setModelName(g_model.header.name);
currentModel->setRfData(&g_model);
modelslabels.setDirty();
} else {
TRACE("ModelList Error - No Current Model");
}
}
/**
* @brief Reads a line from a file. Used by loadTxt
*
* @param line Storage for the read line
* @param maxlen maximum read length
* @return true Success
* @return false Failure
*/
bool ModelsList::readNextLine(char *line, int maxlen)
{
if (f_gets(line, maxlen, &file) != NULL) {
int curlen = strlen(line) - 1;
if (line[curlen] ==
'\n') { // remove unwanted chars if file was edited using windows
if (line[curlen - 1] == '\r') {
line[curlen - 1] = 0;
} else {
line[curlen] = 0;
}
}
return true;
}
return false;
}
/**
* @brief Creates a new ModelCell
*
* @param name Model File Name
* @param save True=Update labels.yml right away
* @param copyCell If duplicating copy the data from this cell, otherwise leave null
* @return ModelCell* New Model
*/
ModelCell *ModelsList::addModel(const char *fileName, bool save, ModelCell *copyCell)
{
ModelCell *result = new ModelCell(fileName);
if (copyCell != nullptr) { // Duplicate all data
memcpy(result, copyCell, sizeof(ModelCell));
}
// Set the new File Name
strncpy(result->modelFilename, fileName, LEN_MODEL_FILENAME);
result->modelFilename[LEN_MODEL_FILENAME] = '\0';
// Add to the ModelsList
push_back(result);
// Force save to labels.yml
if (save) this->save();
return result;
}
/**
* @brief Removes a model from the list and the modelmap.
* @details The models filename is moved into the sub folder DELETED_MODELS_PATH
* rather than deleting the model. If left in current location would
* be re-scanned on boot
*
* @param model Model to be deleted
* @return false Successfully removed
* @return true Failure
*/
bool ModelsList::removeModel(ModelCell *model)
{
erase(std::remove(begin(), end(), model), end());
modelslabels.removeModels(model);
// Create deleted folder if it doesn't exist
DIR deletedFolder;
FRESULT result = f_opendir(&deletedFolder, DELETED_MODELS_PATH);
if (result != FR_OK) {
if (result == FR_NO_PATH) result = f_mkdir(DELETED_MODELS_PATH);
if (result != FR_OK) {
TRACE("Unable to create deleted models folder");
return true;
}
} else f_closedir(&deletedFolder);
// Move model into deleted folder. If not moved will be re-added on next
// reboot
TRACE_LABELS("Deleting Model %s", model->modelFilename);
const char *warning = sdMoveFile(model->modelFilename, MODELS_PATH, model->modelFilename, DELETED_MODELS_PATH);
if (warning) {
TRACE("Labels: Unable to move file");
return true;
}
// Free memory
delete(model);
return false;
}
/**
* @brief Moves a model in the list for customization of order
*
* @param curindex Index of the model to move
* @param toindex Destination index of the model
* @return true Failure
* @return false Success
*/
bool ModelsList::moveModelTo(unsigned curindex, unsigned toindex)
{
if (curindex == toindex || curindex >= size() || toindex >= size())
return true;
if (curindex < toindex) { // Move forward
std::rotate(rend() - curindex - 1, rend() - curindex, rend() - toindex);
} else { // Move back
std::rotate(begin() + curindex, begin() + curindex + 1,
begin() + toindex + 1);
}
modelslabels.setDirty();
return false;
}
/**
* @brief Checks if a models RF module has a unique id
*
* @param moduleIdx Module number 0 <-> NUM_MODULES
* @param warn_buf Buffer to store the warning string
* @param warn_buf_len Length of buffer
* @return true ID is unique
* @return false ID is used
*/
bool ModelsList::isModelIdUnique(uint8_t moduleIdx, char *warn_buf,
size_t warn_buf_len)
{
ModelCell *modelCell = modelslist.getCurrentModel();
if (!modelCell || !modelCell->valid_rfData) {
// in doubt, pretend it's unique
return true;
}
uint8_t modelId = modelCell->modelId[moduleIdx];
uint8_t type = modelCell->moduleData[moduleIdx].type;
uint8_t subType = modelCell->moduleData[moduleIdx].subType;
uint8_t additionalOnes = 0;
char *curr = warn_buf;
curr[0] = 0;
bool hit_found = false;
for (auto it = begin(); it != end(); ++it) {
if (modelCell == *it) continue;
if (!(*it)->valid_rfData) continue;
if ((type != MODULE_TYPE_NONE) &&
(type == (*it)->moduleData[moduleIdx].type) &&
(subType == (*it)->moduleData[moduleIdx].subType) &&
(modelId == (*it)->modelId[moduleIdx])) {
// Hit found!
hit_found = true;
const char *modelName = (*it)->modelName;
const char *modelFilename = (*it)->modelFilename;
// you cannot rely exactly on WARNING_LINE_LEN so using WARNING_LINE_LEN-2
// (-2 for the ",")
if ((int)(warn_buf_len - 2 - (curr - warn_buf)) > LEN_MODEL_NAME) {
if (warn_buf[0] != 0) curr = strAppend(curr, ", ");
if (modelName[0] == 0) {
size_t len = min<size_t>(strlen(modelFilename), LEN_MODEL_NAME);
curr = strAppendFilename(curr, modelFilename, len);
} else
curr = strAppend(curr, modelName, LEN_MODEL_NAME);
} else {
additionalOnes++;
}
}
}
if (additionalOnes && (warn_buf_len - (curr - warn_buf) >= 7)) {
curr = strAppend(curr, " (+");
curr = strAppendUnsigned(curr, additionalOnes);
curr = strAppend(curr, ")");
}
return !hit_found;
}
/**
* @brief Finds the next free RF Module ID
*
* @param moduleIdx RF Module to search (Internal/External) 0 <-> NUM_MODULES
* @return 0 No unused ID found
* @return uint8_t Next free ID
*/
uint8_t ModelsList::findNextUnusedModelId(uint8_t moduleIdx)
{
ModelCell *modelCell = modelslist.getCurrentModel();
if (!modelCell || !modelCell->valid_rfData) {
return 0;
}
uint8_t type = modelCell->moduleData[moduleIdx].type;
uint8_t subType = modelCell->moduleData[moduleIdx].subType;
uint8_t usedModelIds[(MAX_RXNUM + 7) / 8];
memset(usedModelIds, 0, sizeof(usedModelIds));
for (auto it = begin(); it != end(); it++) {
if (modelCell == *it) continue;
if (!(*it)->valid_rfData) continue;
// match module type and RF protocol
if ((type != MODULE_TYPE_NONE) &&
(type == (*it)->moduleData[moduleIdx].type) &&
(subType == (*it)->moduleData[moduleIdx].subType)) {
uint8_t id = (*it)->modelId[moduleIdx];
uint8_t mask = 1 << (id & 7u);
usedModelIds[id >> 3u] |= mask;
}
}
for (uint8_t id = 1; id <= getMaxRxNum(moduleIdx); id++) {
uint8_t mask = 1u << (id & 7u);
if (!(usedModelIds[id >> 3u] & mask)) {
// found free ID
return id;
}
}
// failed finding something...
return 0;
}