2018-12-27 23:11:32 +01:00
// proj
# include <hyperion/SettingsManager.h>
// util
# include <utils/JsonUtils.h>
2019-07-12 16:54:26 +02:00
# include <db/SettingsTable.h>
2021-04-24 19:37:29 +02:00
# include "HyperionConfig.h"
2018-12-27 23:11:32 +01:00
// json schema process
# include <utils/jsonschema/QJsonFactory.h>
# include <utils/jsonschema/QJsonSchemaChecker.h>
// write config to filesystem
# include <utils/JsonUtils.h>
2021-04-24 19:37:29 +02:00
# include <utils/version.hpp>
using namespace semver ;
// Constants
namespace {
const char DEFAULT_VERSION [ ] = " 2.0.0-alpha.8 " ;
} //End of constants
2018-12-27 23:11:32 +01:00
QJsonObject SettingsManager : : schemaJson ;
2020-11-01 19:47:30 +01:00
SettingsManager : : SettingsManager ( quint8 instance , QObject * parent , bool readonlyMode )
2019-07-14 22:43:22 +02:00
: QObject ( parent )
2020-07-19 15:37:47 +02:00
, _log ( Logger : : getInstance ( " SETTINGSMGR " ) )
2021-04-24 19:37:29 +02:00
, _instance ( instance )
2019-07-12 16:54:26 +02:00
, _sTable ( new SettingsTable ( instance , this ) )
2021-04-24 19:37:29 +02:00
, _configVersion ( DEFAULT_VERSION )
, _previousVersion ( DEFAULT_VERSION )
2020-11-01 19:47:30 +01:00
, _readonlyMode ( readonlyMode )
2018-12-27 23:11:32 +01:00
{
2020-11-01 19:47:30 +01:00
_sTable - > setReadonlyMode ( _readonlyMode ) ;
2018-12-27 23:11:32 +01:00
// get schema
if ( schemaJson . isEmpty ( ) )
{
2018-12-30 22:07:53 +01:00
Q_INIT_RESOURCE ( resource ) ;
2018-12-27 23:11:32 +01:00
try
{
schemaJson = QJsonFactory : : readSchema ( " :/hyperion-schema " ) ;
}
catch ( const std : : runtime_error & error )
{
throw std : : runtime_error ( error . what ( ) ) ;
}
}
2019-07-12 16:54:26 +02:00
2018-12-27 23:11:32 +01:00
// get default config
QJsonObject defaultConfig ;
if ( ! JsonUtils : : readFile ( " :/hyperion_default.config " , defaultConfig , _log ) )
2021-04-24 19:37:29 +02:00
{
2018-12-27 23:11:32 +01:00
throw std : : runtime_error ( " Failed to read default config " ) ;
2021-04-24 19:37:29 +02:00
}
2018-12-27 23:11:32 +01:00
2019-07-12 16:54:26 +02:00
// transform json to string lists
QStringList keyList = defaultConfig . keys ( ) ;
QStringList defValueList ;
2020-08-02 22:35:09 +02:00
for ( const auto & key : keyList )
2018-12-27 23:11:32 +01:00
{
2019-07-12 16:54:26 +02:00
if ( defaultConfig [ key ] . isObject ( ) )
2018-12-27 23:11:32 +01:00
{
2019-07-12 16:54:26 +02:00
defValueList < < QString ( QJsonDocument ( defaultConfig [ key ] . toObject ( ) ) . toJson ( QJsonDocument : : Compact ) ) ;
2018-12-27 23:11:32 +01:00
}
2019-07-12 16:54:26 +02:00
else if ( defaultConfig [ key ] . isArray ( ) )
2018-12-27 23:11:32 +01:00
{
2019-07-12 16:54:26 +02:00
defValueList < < QString ( QJsonDocument ( defaultConfig [ key ] . toArray ( ) ) . toJson ( QJsonDocument : : Compact ) ) ;
2018-12-27 23:11:32 +01:00
}
}
2019-07-12 16:54:26 +02:00
// fill database with default data if required
2020-08-02 22:35:09 +02:00
for ( const auto & key : keyList )
2019-07-12 16:54:26 +02:00
{
QString val = defValueList . takeFirst ( ) ;
// prevent overwrite
if ( ! _sTable - > recordExist ( key ) )
_sTable - > createSettingsRecord ( key , val ) ;
}
2018-12-27 23:11:32 +01:00
2021-04-24 19:37:29 +02:00
// need to validate all data in database construct the entire data object
2019-07-12 16:54:26 +02:00
// TODO refactor schemaChecker to accept QJsonArray in validate(); QJsonDocument container? To validate them per entry...
QJsonObject dbConfig ;
2020-08-02 22:35:09 +02:00
for ( const auto & key : keyList )
2019-07-12 16:54:26 +02:00
{
QJsonDocument doc = _sTable - > getSettingsRecord ( key ) ;
if ( doc . isArray ( ) )
dbConfig [ key ] = doc . array ( ) ;
else
dbConfig [ key ] = doc . object ( ) ;
}
2018-12-27 23:11:32 +01:00
2021-04-24 19:37:29 +02:00
//Check, if database requires migration
bool isNewRelease = false ;
// Use instance independent SettingsManager to track migration status
if ( instance = = GLOABL_INSTANCE_ID )
{
if ( resolveConfigVersion ( dbConfig ) )
{
QJsonObject newGeneralConfig = dbConfig [ " general " ] . toObject ( ) ;
semver : : version BUILD_VERSION ( HYPERION_VERSION ) ;
if ( _configVersion > BUILD_VERSION )
{
Error ( _log , " Database version [%s] is greater that current Hyperion version [%s] " , _configVersion . getVersion ( ) . c_str ( ) , BUILD_VERSION . getVersion ( ) . c_str ( ) ) ;
// TODO: Remove version checking and Settingsmanager from components' constructor to be able to stop hyperion.
}
else
{
if ( _previousVersion < BUILD_VERSION )
{
if ( _configVersion = = BUILD_VERSION )
{
newGeneralConfig [ " previousVersion " ] = BUILD_VERSION . getVersion ( ) . c_str ( ) ;
dbConfig [ " general " ] = newGeneralConfig ;
isNewRelease = true ;
Info ( _log , " Migration completed to version [%s] " , BUILD_VERSION . getVersion ( ) . c_str ( ) ) ;
}
else
{
Info ( _log , " Migration from current version [%s] to new version [%s] started " , _previousVersion . getVersion ( ) . c_str ( ) , BUILD_VERSION . getVersion ( ) . c_str ( ) ) ;
newGeneralConfig [ " previousVersion " ] = _configVersion . getVersion ( ) . c_str ( ) ;
newGeneralConfig [ " configVersion " ] = BUILD_VERSION . getVersion ( ) . c_str ( ) ;
dbConfig [ " general " ] = newGeneralConfig ;
isNewRelease = true ;
}
}
}
}
}
2020-02-26 18:54:56 +01:00
// possible data upgrade steps to prevent data loss
2021-04-24 19:37:29 +02:00
bool migrated = handleConfigUpgrade ( dbConfig ) ;
if ( isNewRelease | | migrated )
2020-02-26 18:54:56 +01:00
{
saveSettings ( dbConfig , true ) ;
}
2019-07-12 16:54:26 +02:00
// validate full dbconfig against schema, on error we need to rewrite entire table
QJsonSchemaChecker schemaChecker ;
schemaChecker . setSchema ( schemaJson ) ;
QPair < bool , bool > valid = schemaChecker . validate ( dbConfig ) ;
// check if our main schema syntax is IO
if ( ! valid . second )
2018-12-27 23:11:32 +01:00
{
2020-07-12 09:19:59 +02:00
for ( auto & schemaError : schemaChecker . getMessages ( ) )
2018-12-27 23:11:32 +01:00
Error ( _log , " Schema Syntax Error: %s " , QSTRING_CSTR ( schemaError ) ) ;
2019-07-12 16:54:26 +02:00
throw std : : runtime_error ( " The config schema has invalid syntax. This should never happen! Go fix it! " ) ;
2018-12-27 23:11:32 +01:00
}
2019-07-12 16:54:26 +02:00
if ( ! valid . first )
2018-12-27 23:11:32 +01:00
{
2019-07-12 16:54:26 +02:00
Info ( _log , " Table upgrade required... " ) ;
dbConfig = schemaChecker . getAutoCorrectedConfig ( dbConfig ) ;
2018-12-27 23:11:32 +01:00
2020-07-12 09:19:59 +02:00
for ( auto & schemaError : schemaChecker . getMessages ( ) )
2018-12-27 23:11:32 +01:00
Warning ( _log , " Config Fix: %s " , QSTRING_CSTR ( schemaError ) ) ;
2020-11-14 16:22:21 +01:00
saveSettings ( dbConfig , true ) ;
2018-12-27 23:11:32 +01:00
}
2019-07-12 16:54:26 +02:00
else
_qconfig = dbConfig ;
2018-12-27 23:11:32 +01:00
2020-02-10 15:21:58 +01:00
Debug ( _log , " Settings database initialized " ) ;
2018-12-27 23:11:32 +01:00
}
2020-08-08 13:09:15 +02:00
QJsonDocument SettingsManager : : getSetting ( settings : : type type ) const
2018-12-27 23:11:32 +01:00
{
2019-07-12 16:54:26 +02:00
return _sTable - > getSettingsRecord ( settings : : typeToString ( type ) ) ;
2018-12-27 23:11:32 +01:00
}
2021-02-11 19:45:22 +01:00
QJsonObject SettingsManager : : getSettings ( ) const
{
QJsonObject config ;
for ( const auto & key : _qconfig . keys ( ) )
{
//Read all records from database to ensure that global settings are read across instances
2021-02-17 12:29:53 +01:00
QJsonDocument doc = _sTable - > getSettingsRecord ( key ) ;
if ( doc . isArray ( ) )
{
config . insert ( key , doc . array ( ) ) ;
}
else
{
config . insert ( key , doc . object ( ) ) ;
}
2021-02-11 19:45:22 +01:00
}
return config ;
}
2020-08-08 13:09:15 +02:00
bool SettingsManager : : saveSettings ( QJsonObject config , bool correct )
2018-12-27 23:11:32 +01:00
{
2020-02-26 18:54:56 +01:00
// optional data upgrades e.g. imported legacy/older configs
// handleConfigUpgrade(config);
2018-12-27 23:11:32 +01:00
// we need to validate data against schema
QJsonSchemaChecker schemaChecker ;
schemaChecker . setSchema ( schemaJson ) ;
if ( ! schemaChecker . validate ( config ) . first )
{
if ( ! correct )
{
Error ( _log , " Failed to save configuration, errors during validation " ) ;
return false ;
}
Warning ( _log , " Fixing json data! " ) ;
config = schemaChecker . getAutoCorrectedConfig ( config ) ;
2020-08-08 23:12:43 +02:00
for ( const auto & schemaError : schemaChecker . getMessages ( ) )
2018-12-27 23:11:32 +01:00
Warning ( _log , " Config Fix: %s " , QSTRING_CSTR ( schemaError ) ) ;
}
2019-07-12 16:54:26 +02:00
// store the new config
_qconfig = config ;
2018-12-28 18:12:45 +01:00
2019-07-12 16:54:26 +02:00
// extract keys and data
QStringList keyList = config . keys ( ) ;
QStringList newValueList ;
2020-08-02 22:35:09 +02:00
for ( const auto & key : keyList )
2019-07-12 16:54:26 +02:00
{
if ( config [ key ] . isObject ( ) )
{
newValueList < < QString ( QJsonDocument ( config [ key ] . toObject ( ) ) . toJson ( QJsonDocument : : Compact ) ) ;
}
else if ( config [ key ] . isArray ( ) )
{
newValueList < < QString ( QJsonDocument ( config [ key ] . toArray ( ) ) . toJson ( QJsonDocument : : Compact ) ) ;
}
2018-12-28 18:12:45 +01:00
}
2020-11-01 19:47:30 +01:00
int rc = true ;
2019-07-12 16:54:26 +02:00
// compare database data with new data to emit/save changes accordingly
2020-08-02 22:35:09 +02:00
for ( const auto & key : keyList )
2019-07-12 16:54:26 +02:00
{
QString data = newValueList . takeFirst ( ) ;
if ( _sTable - > getSettingsRecordString ( key ) ! = data )
{
2020-11-01 19:47:30 +01:00
if ( ! _sTable - > createSettingsRecord ( key , data ) )
{
rc = false ;
}
else
{
emit settingsChanged ( settings : : stringToType ( key ) , QJsonDocument : : fromJson ( data . toLocal8Bit ( ) ) ) ;
}
2019-07-12 16:54:26 +02:00
}
}
2020-11-01 19:47:30 +01:00
return rc ;
2018-12-27 23:11:32 +01:00
}
2020-02-26 18:54:56 +01:00
2021-04-24 19:37:29 +02:00
bool SettingsManager : : resolveConfigVersion ( QJsonObject & config )
{
bool isValid = false ;
if ( config . contains ( " general " ) )
{
QJsonObject generalConfig = config [ " general " ] . toObject ( ) ;
QString configVersion = generalConfig [ " configVersion " ] . toString ( ) ;
QString previousVersion = generalConfig [ " previousVersion " ] . toString ( ) ;
if ( ! configVersion . isEmpty ( ) )
{
isValid = _configVersion . setVersion ( configVersion . toStdString ( ) ) ;
}
else
{
_configVersion . setVersion ( DEFAULT_VERSION ) ;
isValid = true ;
}
if ( ! previousVersion . isEmpty ( ) & & isValid )
{
isValid = _previousVersion . setVersion ( previousVersion . toStdString ( ) ) ;
}
else
{
_previousVersion . setVersion ( DEFAULT_VERSION ) ;
isValid = true ;
}
}
return isValid ;
}
2020-02-26 18:54:56 +01:00
bool SettingsManager : : handleConfigUpgrade ( QJsonObject & config )
{
bool migrated = false ;
2021-04-24 19:37:29 +02:00
resolveConfigVersion ( config ) ;
2020-02-26 18:54:56 +01:00
2021-04-24 19:37:29 +02:00
//Do only migrate, if configuration is not up to date
if ( _previousVersion < _configVersion )
{
//Migration steps for versions <= alpha 9
semver : : version targetVersion { " 2.0.0-alpha.9 " } ;
if ( _previousVersion < = targetVersion )
2020-02-26 18:54:56 +01:00
{
2021-04-24 19:37:29 +02:00
Info ( _log , " Instance [%u]: Migrate LED Layout from current version [%s] to version [%s] or later " , _instance , _previousVersion . getVersion ( ) . c_str ( ) , targetVersion . getVersion ( ) . c_str ( ) ) ;
2020-02-26 18:54:56 +01:00
2021-04-24 19:37:29 +02:00
// LED LAYOUT UPGRADE
// from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } }
// from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } }
// to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3}
if ( config . contains ( " leds " ) )
2020-02-26 18:54:56 +01:00
{
2021-04-24 19:37:29 +02:00
const QJsonArray ledarr = config [ " leds " ] . toArray ( ) ;
const QJsonObject led = ledarr [ 0 ] . toObject ( ) ;
if ( led . contains ( " hscan " ) | | led . contains ( " h " ) )
2020-02-26 18:54:56 +01:00
{
2021-04-24 19:37:29 +02:00
const bool whscan = led . contains ( " hscan " ) ;
QJsonArray newLedarr ;
for ( const auto & entry : ledarr )
{
const QJsonObject led = entry . toObject ( ) ;
QJsonObject hscan ;
QJsonObject vscan ;
QJsonValue hmin ;
QJsonValue hmax ;
QJsonValue vmin ;
QJsonValue vmax ;
QJsonObject nL ;
if ( whscan )
{
hscan = led [ " hscan " ] . toObject ( ) ;
vscan = led [ " vscan " ] . toObject ( ) ;
hmin = hscan [ " minimum " ] ;
hmax = hscan [ " maximum " ] ;
vmin = vscan [ " minimum " ] ;
vmax = vscan [ " maximum " ] ;
}
else
{
hscan = led [ " h " ] . toObject ( ) ;
vscan = led [ " v " ] . toObject ( ) ;
hmin = hscan [ " min " ] ;
hmax = hscan [ " max " ] ;
vmin = vscan [ " min " ] ;
vmax = vscan [ " max " ] ;
}
// append to led object
nL [ " hmin " ] = hmin ;
nL [ " hmax " ] = hmax ;
nL [ " vmin " ] = vmin ;
nL [ " vmax " ] = vmax ;
newLedarr . append ( nL ) ;
}
// replace
config [ " leds " ] = newLedarr ;
migrated = true ;
Info ( _log , " Instance [%u]: LED Layout migrated " , _instance ) ;
2020-02-26 18:54:56 +01:00
}
2021-04-24 19:37:29 +02:00
}
if ( config . contains ( " ledConfig " ) )
{
QJsonObject oldLedConfig = config [ " ledConfig " ] . toObject ( ) ;
if ( ! oldLedConfig . contains ( " classic " ) )
2020-02-26 18:54:56 +01:00
{
2021-04-24 19:37:29 +02:00
QJsonObject newLedConfig ;
newLedConfig . insert ( " classic " , oldLedConfig ) ;
QJsonObject defaultMatrixConfig { { " ledshoriz " , 1 }
, { " ledsvert " , 1 }
, { " cabling " , " snake " }
, { " start " , " top-left " }
} ;
newLedConfig . insert ( " matrix " , defaultMatrixConfig ) ;
config [ " ledConfig " ] = newLedConfig ;
migrated = true ;
Info ( _log , " Instance [%u]: LED-Config migrated " , _instance ) ;
2020-02-26 18:54:56 +01:00
}
}
2021-02-17 12:29:53 +01:00
2021-04-24 19:37:29 +02:00
// LED Hardware count is leading for versions after alpha 9
// Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs
if ( config . contains ( " device " ) )
{
QJsonObject newDeviceConfig = config [ " device " ] . toObject ( ) ;
2021-02-17 12:29:53 +01:00
2021-04-24 19:37:29 +02:00
if ( newDeviceConfig . contains ( " hardwareLedCount " ) )
{
int hwLedcount = newDeviceConfig [ " hardwareLedCount " ] . toInt ( ) ;
if ( config . contains ( " leds " ) )
{
const QJsonArray ledarr = config [ " leds " ] . toArray ( ) ;
int layoutLedCount = ledarr . size ( ) ;
if ( hwLedcount < layoutLedCount )
{
Warning ( _log , " Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout " , _instance ) ;
hwLedcount = layoutLedCount ;
newDeviceConfig [ " hardwareLedCount " ] = hwLedcount ;
config [ " device " ] = newDeviceConfig ;
migrated = true ;
}
}
}
}
2021-02-17 12:29:53 +01:00
}
}
2020-02-26 18:54:56 +01:00
return migrated ;
}