feat(cpn): save model images with models and settings (#7234)

This commit is contained in:
Neil Horne
2026-06-07 12:20:33 +10:00
committed by GitHub
parent d754abf2c1
commit a672c3c5c6
16 changed files with 343 additions and 71 deletions
+12
View File
@@ -232,6 +232,18 @@ int main(int argc, char *argv[])
registerOpenTxFirmwares();
SimulatorLoader::registerSimulators();
QTemporaryDir tempDir(QDir::tempPath() % "/etx-cpn-XXXXXX");
if (!tempDir.isValid()) {
qDebug() << "Unable to create application temporary directory";
gAppTempPath = "";
} else {
gAppTempPath = tempDir.path();
if (!QDir(gAppTempPath).mkdir("IMAGES"))
qDebug() << "Unable to create images cache directory:" << Helpers::getImagesCacheDir();
}
Profile & profile = g.currentProfile();
if (profile.fwType().isEmpty()){
profile.fwType(Firmware::getDefaultVariant()->getId());
@@ -78,7 +78,7 @@ static void writeYamlToByteArray(const YAML::Node& node, QByteArray& data, bool
data_ostream << node;
data = QByteArray::fromStdString(data_ostream.str());
qDebug() << "Saving YAML:";
//qDebug() << "Saving YAML:";
if(addChecksum) {
uint16_t checksum = 0xFFFF;
@@ -88,7 +88,7 @@ static void writeYamlToByteArray(const YAML::Node& node, QByteArray& data, bool
data.prepend(checksum_ostream.str().c_str());
}
qDebug() << data.toStdString().c_str();
//qDebug() << data.toStdString().c_str();
}
bool loadLabelsListFromYaml(RadioData::ModelLabels& labels,
+39
View File
@@ -2334,3 +2334,42 @@ void ModelData::initTopBar()
zone.widgetName = "Internal GPS";
}
}
QString ModelData::getImageFilename() const
{
if (!isBitmapEmpty()) {
QString extn;
if (!getCurrentFirmware()->getCapability(ModelImageKeepExtn))
extn = "." % getDefaultImageFileExtn();
return QString(bitmap).append(extn);
} else {
return QString();
}
}
QString ModelData::getImageFileExtn() const
{
if (getCurrentFirmware()->getCapability(ModelImageKeepExtn)) {
QStringList strl = QString(bitmap).split(".");
return strl.count() > 1 ? strl.at(strl.count() - 1) : "";
} else {
return getDefaultImageFileExtn();
}
}
QString ModelData::getDefaultImageFileExtn()
{
QString ret;
if (!getCurrentFirmware()->getCapability(ModelImageKeepExtn))
ret = getCurrentFirmware()->getCapabilityStr(ModelImageFilters).replace("*.", "");
return ret;
}
bool ModelData::isBitmapEmpty() const
{
return bitmap[0] == '\0';
}
+4
View File
@@ -293,6 +293,10 @@ class ModelData {
const Board::SwitchType getSwitchType(int sw, const GeneralSettings & gs) const;
QString getChecklistFilename() const;
QString getImageFilename() const;
QString getImageFileExtn() const;
static QString getDefaultImageFileExtn();
bool isBitmapEmpty() const;
enum ReferenceUpdateAction {
REF_UPD_ACT_CLEAR,
@@ -107,7 +107,8 @@ int OpenTxFirmware::getCapability(::Capability capability)
case HasMixerNames:
return (IS_TARANIS_X9(board) ? 8 : 6);
case HasModelImage:
return (board == BOARD_TARANIS_X9D || IS_TARANIS_PLUS(board) || board == BOARD_TARANIS_X9DP_2019 || IS_FAMILY_HORUS_OR_T16(board));
return (board == BOARD_TARANIS_X9D || IS_TARANIS_PLUS(board) || board == BOARD_TARANIS_X9DP_2019 ||
IS_FAMILY_HORUS_OR_T16(board) || Boards::getCapability(board, Board::HasColorLcd));
case HasModelLabels:
return IS_FAMILY_HORUS_OR_T16(board);
case HasModelsList:
+30
View File
@@ -44,6 +44,8 @@
#include <QtGlobal>
#include <QFile>
QString gAppTempPath;
using namespace Helpers;
Stopwatch gStopwatch("global");
@@ -366,6 +368,34 @@ QString Helpers::concatPath(QString & str1, const QString & str2, bool onlyonese
return (str1 % ((!str1.endsWith("/") || !onlyonesep) ? "/" : "") % str2);
}
QString Helpers::getImagesCacheDir()
{
if (gAppTempPath.isEmpty())
return QString();
return QDir(gAppTempPath).filePath("IMAGES");
}
QString Helpers::getImagePath(const QString & filename)
{
if (filename.isEmpty())
return QString();
QString srcpath(Helpers::getImagesCacheDir() % "/" % filename);
if (srcpath.isEmpty() || !QFileInfo::exists(srcpath) || QFileInfo(srcpath).isDir()) {
srcpath = g.currentProfile().sdPath() % "/IMAGES/" % filename;
if (!QFileInfo::exists(srcpath) || QFileInfo(srcpath).isDir()) {
qDebug() << filename << "not found in image cache or sd path.";
srcpath = "";
}
}
return srcpath;
}
#ifdef __APPLE__
// Flag when simulator is running
static bool simulatorRunning = false;
+3
View File
@@ -34,6 +34,7 @@
#include <QComboBox>
extern const QColor colors[CPN_MAX_CURVES];
extern QString gAppTempPath;
#define TMR_NUM_OPTION (TMRMODE_COUNT+2*9+2*getCurrentFirmware()->getCapability(LogicalSwitches)-1)
@@ -118,6 +119,8 @@ namespace Helpers
QString concatPath(const QString & str1, const QString & str2, bool onlyonesep = true);
QString concatPath(const QString & str1, QString & str2, bool onlyonesep = true);
QString concatPath(QString & str1, const QString & str2, bool onlyonesep = true);
QString getImagesCacheDir();
QString getImagePath(const QString & filename);
} // namespace Helpers
// TODO : move globals to Helpers namespace
+44 -51
View File
@@ -83,46 +83,23 @@ SetupPanel::SetupPanel(QWidget * parent, ModelData & model, GeneralSettings & ge
else {
ui->imagePreview->setFixedSize(QSize(64, 32));
}
QStringList items;
items.append("");
QString path = g.profile[g.id()].sdPath();
path.append("/IMAGES/");
QDir qd(path);
if (qd.exists()) {
QStringList filters = firmware->getCapabilityStr(ModelImageFilters).split("|");
foreach ( QString file, qd.entryList(filters, QDir::Files) ) {
QFileInfo fi(file);
QString temp;
if (firmware->getCapability(ModelImageKeepExtn))
temp = fi.fileName();
else
temp = fi.completeBaseName();
if (!items.contains(temp) && temp.length() <= firmware->getCapability(ModelImageNameLen))
items.append(temp);
}
}
if (!items.contains(model.bitmap)) {
loadImageList(items, Helpers::getImagesCacheDir());
loadImageList(items, g.currentProfile().sdPath() % "/IMAGES/");
if (!items.contains(model.bitmap))
items.append(model.bitmap);
}
items.sort(Qt::CaseInsensitive);
foreach (QString file, items) {
ui->image->addItem(file);
if (file == model.bitmap) {
ui->image->setCurrentIndex(ui->image->count() - 1);
if (!file.isEmpty()) {
QString fileName = path;
fileName.append(model.bitmap);
if (!firmware->getCapability(ModelImageKeepExtn)) {
QString extn = firmware->getCapabilityStr(ModelImageFilters);
if (extn.size() > 0)
extn.remove(0, 1); // remove *
fileName.append(extn);
}
QImage image(fileName);
if (!image.isNull()) {
ui->imagePreview->setPixmap(QPixmap::fromImage(image.scaled(ui->imagePreview->size())));
}
}
loadImagePreview();
}
}
}
@@ -392,25 +369,7 @@ void SetupPanel::on_image_currentIndexChanged(int index)
if (!lock) {
memset(model->bitmap, 0, CPN_MAX_BITMAP_LEN);
strncpy(model->bitmap, ui->image->currentText().toLatin1(), CPN_MAX_BITMAP_LEN);
if (model->bitmap[0] != '\0') {
QString path = g.profile[g.id()].sdPath();
path.append("/IMAGES/");
path.append(model->bitmap);
if (!firmware->getCapability(ModelImageKeepExtn)) {
QString extn = firmware->getCapabilityStr(ModelImageFilters);
if (extn.size() > 0)
extn.remove(0, 1); // remove *
path.append(extn);
}
QImage image(path);
if (!image.isNull())
ui->imagePreview->setPixmap(QPixmap::fromImage(image.scaled(ui->imagePreview->size())));
else
ui->imagePreview->clear();
}
else {
ui->imagePreview->clear();
}
loadImagePreview();
emit modified();
}
}
@@ -829,3 +788,37 @@ void SetupPanel::onFunctionSwitchesUpdateItemModels()
updateStartupSwitches();
sharedItemModels->update(AbstractItemModel::IMUE_FunctionSwitches);
}
void SetupPanel::loadImageList(QStringList &list, const QString & path)
{
QDir d(path);
const int maxnamelen = firmware->getCapability(ModelImageNameLen);
const bool keepextn = firmware->getCapability(ModelImageKeepExtn);
if (d.exists()) {
QStringList filters = firmware->getCapabilityStr(ModelImageFilters).split("|");
foreach ( QString file, d.entryList(filters, QDir::Files) ) {
QFileInfo fi(file);
QString temp = keepextn ? fi.fileName() : fi.completeBaseName();
if (!list.contains(temp) && temp.length() <= maxnamelen)
list.append(temp);
}
}
}
void SetupPanel::loadImagePreview()
{
if (!model->isBitmapEmpty()) {
QImage image;
QString filename(model->getImageFilename());
QString path(Helpers::getImagePath(filename));
if (!path.isEmpty() && image.load(path))
ui->imagePreview->setPixmap(QPixmap::fromImage(image.scaled(ui->imagePreview->size())));
else
ui->imagePreview->clear();
}
}
+2
View File
@@ -115,4 +115,6 @@ class SetupPanel : public ModelPanel
void connectItemModelEvents(const FilteredItemModel * itemModel);
CompoundItemModelFactory * panelItemModels;
FilteredItemModelFactory * panelFilteredModels;
void loadImageList(QStringList &list, const QString & dir);
void loadImagePreview();
};
+16
View File
@@ -45,6 +45,7 @@
#include "translations.h"
#include "version.h"
#include "boardfactories.h"
#include "helpers.h"
using namespace Simulator;
@@ -367,6 +368,21 @@ int main(int argc, char *argv[])
Firmware::setCurrentVariant(Firmware::getFirmwareForId(simOptions.firmwareId));
//qDebug() << "current firmware:" << getCurrentFirmware()->getId();
QString imgDir = Helpers::getImagesCacheDir();
QDir dir(imgDir);
if (dir.exists()) {
if (!dir.removeRecursively())
qDebug() << "Unable to delete images cache directory" << imgDir;
else
qDebug() << "Deleted images cache directory" << imgDir;
}
if (!QDir::temp().mkpath(imgDir))
qDebug() << "Unable to create images cache directory" << imgDir;
else
qDebug() << "Created images cache directory" << imgDir;
int result = 0;
SimulatorMainWindow * mainWindow = new SimulatorMainWindow(nullptr, simOptions.simulatorId, (simOptions.flags ? simOptions.flags : SIMULATOR_FLAGS_STANDALONE));
if ((result = mainWindow->getExitStatus(&resultMsg))) {
+68 -11
View File
@@ -20,6 +20,8 @@
*/
#include "etx.h"
#include "helpers.h"
#include <QFile>
#define MZ_ALLOCATION_SIZE (32*1024)
@@ -94,14 +96,8 @@ bool EtxFormat::loadFile(QByteArray & filedata, const QString & filename, bool o
{
size_t size;
void * data = mz_zip_reader_extract_file_to_heap(&zip_archive, qPrintable(filename), &size, 0);
if (!data) {
if (optional) {
//qDebug() << "File not found:" << filename;
return true;
} else {
return false;
}
}
if (!data) return optional;
qDebug() << QString("Extracted file %1, size=%2").arg(filename).arg(size);
filedata.clear();
@@ -110,16 +106,77 @@ bool EtxFormat::loadFile(QByteArray & filedata, const QString & filename, bool o
return true;
}
bool EtxFormat::writeFile(const QByteArray & filedata, const QString & filename)
bool EtxFormat::loadImageFile(const QString & filename, bool optional)
{
if (!mz_zip_writer_add_mem(&zip_archive, filename.toStdString().c_str(), filedata.data(), filedata.size(), MZ_DEFAULT_LEVEL)) {
setError(tr("Error adding %1 to EdgeTX archive").arg(filename));
QString srcpath("IMAGES/" % filename);
size_t size;
void * data = mz_zip_reader_extract_file_to_heap(&zip_archive, qPrintable(srcpath), &size, 0);
if (!data) return optional;
QByteArray ba;
ba.append((char *)data, size);
mz_free(data);
QFile imgfile(Helpers::getImagesCacheDir() % "/" % filename);
if (imgfile.open(QIODevice::WriteOnly)) {
if (imgfile.write(ba) != (qint64)size) {
qDebug() << "Image data written does not match expected";
return false;
}
imgfile.close();
} else {
setError(tr("Unable to write %1 to cache").arg(filename));
return false;
}
qDebug() << QString("Extracted file %1, size=%2").arg(srcpath).arg(size);
return true;
}
bool EtxFormat::writeFile(const QByteArray & filedata, const QString & filename)
{
if (!mz_zip_writer_add_mem(&zip_archive, filename.toStdString().c_str(), filedata.data(), filedata.size(), MZ_DEFAULT_LEVEL)) {
setError(tr("Error adding %1 to archive").arg(filename));
return false;
}
qDebug() << "File" << filename << "written, size:" << filedata.size();
return true;
}
bool EtxFormat::writeImageFile(const QString & filename)
{
QString srcpath(Helpers::getImagePath(filename));
if (!srcpath.isEmpty()) {
QByteArray ba;
QFile imgfile(srcpath);
if (imgfile.open(QIODevice::ReadOnly)) {
ba.append(imgfile.readAll());
imgfile.close();
} else {
setError(tr("Unable to open image %1").arg(filename));
return false;
}
QString destpath("IMAGES/" % filename);
if (!mz_zip_writer_add_mem(&zip_archive, destpath.toStdString().c_str(), ba.data(), ba.size(), MZ_DEFAULT_LEVEL)) {
setError(tr("Error adding file %1").arg(destpath));
return false;
}
qDebug() << "File" << destpath << "written, size:" << ba.size();
return true;
}
return false;
}
bool EtxFormat::getFileList(std::list<std::string>& filelist)
{
int count = (int)mz_zip_reader_get_num_files(&zip_archive);
+2
View File
@@ -41,7 +41,9 @@ class EtxFormat : public LabelsStorageFormat
protected:
virtual bool loadFile(QByteArray & fileData, const QString & fileName, bool optional = false);
virtual bool loadImageFile(const QString & fileName, bool optional = false);
virtual bool writeFile(const QByteArray & fileData, const QString & fileName);
virtual bool writeImageFile(const QString & fileName);
virtual bool getFileList(std::list<std::string>& filelist);
virtual bool deleteFile(const QString & fileName) { return false; }
+34 -4
View File
@@ -71,6 +71,7 @@ bool LabelsStorageFormat::load(RadioData & radioData)
QByteArray labelslistBuffer;
int sortOrder = 0;
if (loadFile(labelslistBuffer, "MODELS/labels.yml")) {
try {
if (!loadLabelsListFromYaml(radioData.labels, sortOrder, modelFiles, labelslistBuffer)) {
@@ -108,6 +109,8 @@ bool LabelsStorageFormat::load(RadioData & radioData)
if (hasLabels)
radioData.models.resize(modelFiles.size());
QList<QString> modelImages;
for (const auto& mc : modelFiles) {
qDebug() << "Filename: " << mc.filename.c_str();
@@ -127,6 +130,7 @@ bool LabelsStorageFormat::load(RadioData & radioData)
QByteArray modelBuffer;
QString filename = "MODELS/" + QString::fromStdString(mc.filename);
if (!loadFile(modelBuffer, filename)) {
setError(tr("Cannot extract ") + filename);
return false;
@@ -150,6 +154,14 @@ bool LabelsStorageFormat::load(RadioData & radioData)
if (!loadChecklist(model))
return false;
if (!model.isBitmapEmpty()) {
const QString fname(model.getImageFilename());
if (!modelImages.contains(fname)) {
modelImages.append(fname);
}
}
model.modelIndex = modelIdx;
strncpy(model.filename, mc.filename.c_str(), sizeof(model.filename)-1);
@@ -160,6 +172,11 @@ bool LabelsStorageFormat::load(RadioData & radioData)
modelIdx++;
}
for (const auto &fname : modelImages) {
if (!loadImageFile(fname, true))
return false;
}
// Add the labels in the models
if (hasLabels) {
radioData.addLabelsFromModels();
@@ -232,8 +249,9 @@ bool LabelsStorageFormat::write(RadioData & radioData)
}
EtxModelfiles modelFiles;
for (const auto& model : radioData.models) {
QList<QString> modelImages;
for (const auto& model : radioData.models) {
if (model.isEmpty())
continue;
@@ -251,10 +269,23 @@ bool LabelsStorageFormat::write(RadioData & radioData)
if (!writeFile(modelData, modelFilename))
return false;
if (!model.isBitmapEmpty()) {
const QString fname(model.getImageFilename());
if (!modelImages.contains(fname)) {
modelImages.append(fname);
}
}
if (!writeChecklist(model))
return false;
}
for (const auto &fname : modelImages) {
if (!writeImageFile(fname))
return false;
}
if (hasLabels) {
QByteArray labelsListBuffer;
if (!writeLabelsListToYaml(radioData, labelsListBuffer) || !writeFile(labelsListBuffer, "MODELS/labels.yml"))
@@ -279,11 +310,10 @@ bool LabelsStorageFormat::loadChecklist(ModelData & model)
bool LabelsStorageFormat::writeChecklist(const ModelData & model)
{
const QString fname("MODELS/" + model.getChecklistFilename());
// not every model has a checklist
if (!model.checklistData.isEmpty()) {
const QString fname("MODELS/" + model.getChecklistFilename());
//qDebug() << "Writing checklist file:" << fname;
if (!writeFile(model.checklistData, fname)) {
setError(tr("Cannot write ") + fname);
return false;
+2
View File
@@ -46,7 +46,9 @@ class LabelsStorageFormat : public StorageFormat
protected:
virtual bool loadFile(QByteArray & fileData, const QString & fileName, bool optional = false) = 0;
virtual bool loadImageFile(const QString & fileName, bool optional = false) = 0;
virtual bool writeFile(const QByteArray & fileData, const QString & fileName) = 0;
virtual bool writeImageFile(const QString & fileName) = 0;
virtual bool getFileList(std::list<std::string>& filelist) = 0;
virtual bool deleteFile(const QString & fileName) = 0;
+81 -2
View File
@@ -20,6 +20,8 @@
*/
#include "sdcard.h"
#include "helpers.h"
#include <QFile>
#include <QDir>
@@ -28,6 +30,7 @@ bool SdcardFormat::write(RadioData & radioData)
QDir dir(filename);
dir.mkdir("RADIO");
dir.mkdir("MODELS");
dir.mkdir("IMAGES");
return LabelsStorageFormat::write(radioData);
}
@@ -35,6 +38,7 @@ bool SdcardFormat::loadFile(QByteArray & filedata, const QString & filename, boo
{
QString path = this->filename + "/" + filename;
QFile file(path);
if (!file.exists()) {
if (optional) {
//qDebug() << "File not found:" << filename;
@@ -45,11 +49,47 @@ bool SdcardFormat::loadFile(QByteArray & filedata, const QString & filename, boo
}
if (!file.open(QFile::ReadOnly)) {
setError(tr("Error opening file %1:\n%2.").arg(path).arg(file.errorString()));
setError(tr("Error opening file %1:\n%2.").arg(filename).arg(file.errorString()));
return false;
}
filedata = file.readAll();
qDebug() << "File" << path << "read, size:" << filedata.size();
qDebug() << "File" << filename << "read, size:" << filedata.size();
return true;
}
bool SdcardFormat::loadImageFile(const QString & filename, bool optional)
{
QString srcpath = this->filename + "/IMAGES/" + filename;
QFile file(srcpath);
if (!file.exists()) return optional;
if (QDir(Helpers::getImagesCacheDir()).exists()) {
QString destpath(Helpers::getImagesCacheDir() % "/" % filename);
QFile destfile(destpath);
// QFile::copy fails if destination file exists
if (destfile.exists()) {
if (destfile.remove()) {
qDebug() << "Deleted file from image cache" << filename;
} else {
qDebug() << "Unable to delete cached" << filename;
return false;
}
}
if (file.copy(Helpers::getImagesCacheDir() % "/" % filename)) {
qDebug() << "Cached image" << filename;
} else {
qDebug() << "Failed to cache image" << filename;
return false;
}
} else {
qDebug() << "Failed to cache" << filename << ". Cache directory" << Helpers::getImagesCacheDir()
<< "does not exist.";
}
return true;
}
@@ -57,25 +97,59 @@ bool SdcardFormat::writeFile(const QByteArray & data, const QString & filename)
{
QString path = this->filename + "/" + filename;
QFile file(path);
if (!file.open(QFile::WriteOnly)) {
setError(tr("Error opening file %1 in write mode:\n%2.").arg(path).arg(file.errorString()));
return false;
}
file.write(data.data(), data.size());
file.close();
qDebug() << "File" << path << "written, size:" << data.size();
return true;
}
bool SdcardFormat::writeImageFile(const QString & filename)
{
QString srcpath(Helpers::getImagePath(filename));
if (!srcpath.isEmpty()) {
QFileInfo srcinfo(srcpath);
QString destpath(this->filename + "/IMAGES/" + filename);
QFileInfo destinfo(destpath);
if (destinfo.exists()) {
// for performance reasons avoid overwriting the same image
if (destinfo.size() == srcinfo.size()) {
qDebug() << "Image file" << filename << "skipped as same size";
return true;
}
}
if (!QFile(srcpath).copy(destpath)) {
setError(tr("Error writing image file: %1").arg(filename));
return false;
}
qDebug() << "File" << destpath << "written, size:" << srcinfo.size();
return true;
}
return false;
}
bool SdcardFormat::getFileList(std::list<std::string>& filelist)
{
QDir dir(filename);
if (!dir.cd("MODELS")) return false;
QStringList ql = dir.entryList();
for (const auto& str : ql) {
filelist.push_back("MODELS/" + str.toStdString());
}
return true;
}
@@ -92,6 +166,11 @@ bool SdcardFormat::deleteFile(const QString & filename)
return true;
}
/*
class SdcardStorageFactory
*/
bool SdcardStorageFactory::probe(const QString & path)
{
return QDir(path).exists();
+2
View File
@@ -40,7 +40,9 @@ class SdcardFormat : public LabelsStorageFormat
protected:
virtual bool loadFile(QByteArray & fileData, const QString & fileName, bool optional = false);
virtual bool loadImageFile(const QString & fileName, bool optional = false);
virtual bool writeFile(const QByteArray & fileData, const QString & fileName);
virtual bool writeImageFile(const QString & fileName);
virtual bool getFileList(std::list<std::string>& filelist);
virtual bool deleteFile(const QString & fileName);
};