diff --git a/src/config/model.c b/src/config/model.c
index 1b9a3dd2c8..7e3c09485e 100644
--- a/src/config/model.c
+++ b/src/config/model.c
@@ -32,7 +32,7 @@ struct Model Model;
/*set this to write all model data even if it is the same as the default */
static u32 crc32;
-const char * const MODEL_TYPE_VAL[MODELTYPE_LAST] = { "heli", "plane", "multi" };
+static const char * const MODEL_TYPE_VAL[MODELTYPE_LAST] = { "heli", "plane", "multi" };
const u8 RADIO_TX_POWER_COUNT[TX_MODULE_LAST] = { // number of power settings
8, // CYRF6936,
7, // A7105,
diff --git a/src/config/model.h b/src/config/model.h
index 3a2c28bf18..ec9c6f8bd7 100644
--- a/src/config/model.h
+++ b/src/config/model.h
@@ -20,7 +20,9 @@
extern const char MODEL_NAME[];
extern const char MODEL_ICON[];
extern const char MODEL_TYPE[];
+extern const char MODEL_MIXERMODE[];
extern const char MODEL_TEMPLATE[];
+extern const char MODEL_AUTOMAP[];
#define UNKNOWN_ICON ("media/noicon" IMG_EXT)
//This cannot be computed, and must be manually updated
diff --git a/src/target/tx/other/test/old_model.c b/src/target/tx/other/test/old_model.c
new file mode 100644
index 0000000000..d1feb298bd
--- /dev/null
+++ b/src/target/tx/other/test/old_model.c
@@ -0,0 +1,1475 @@
+/*
+ This project is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Deviation 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.
+
+ You should have received a copy of the GNU General Public License
+ along with Deviation. If not, see .
+ */
+
+#include "common.h"
+#include "config/model.h"
+#include "telemetry.h"
+#include "config/tx.h"
+#include "music.h"
+#include "extended_audio.h"
+
+#include
+#include
+extern const u8 EATRG0[PROTO_MAP_LEN];
+
+extern struct Model Model;
+/*set this to write all model data even if it is the same as the default */
+static u32 crc32;
+static const char * const MODEL_TYPE_VAL[MODELTYPE_LAST] = { "heli", "plane", "multi" };
+
+#define MATCH_SECTION(s) (strcasecmp(section, s) == 0)
+#define MATCH_START(x,y) (strncasecmp(x, y, sizeof(y)-1) == 0)
+#define MATCH_KEY(s) (strcasecmp(name, s) == 0)
+#define MATCH_VALUE(s) (strcasecmp(value, s) == 0)
+#define NUM_STR_ELEMS(s) (sizeof(s) / sizeof(char *))
+
+#define WRITE_FULL_MODEL 0
+static u8 auto_map;
+
+/* Section: Radio */
+static const char SECTION_RADIO[] = "radio";
+
+static const char RADIO_PROTOCOL[] = "protocol";
+#if HAS_VIDEO
+static const char RADIO_VIDEOSRC[] = "videosrc";
+static const char RADIO_VIDEOCH[] = "videoch";
+static const char RADIO_VIDEOCONTRAST[] = "videocontrast";
+static const char RADIO_VIDEOBRIGHTNESS[] = "videobrightness";
+#endif
+
+static const char RADIO_NUM_CHANNELS[] = "num_channels";
+static const char RADIO_FIXED_ID[] = "fixed_id";
+
+static const char RADIO_TX_POWER[] = "tx_power";
+#if HAS_EXTENDED_TELEMETRY
+static const char RADIO_GROUND_LEVEL[] = "ground_level";
+#endif // HAS_EXTENDED_TELEMETRY
+
+static const char SECTION_PROTO_OPTS[] = "protocol_opts";
+/* Section: Mixer */
+static const char SECTION_MIXER[] = "mixer";
+
+static const char MIXER_SOURCE[] = "src";
+static const char MIXER_DEST[] = "dest";
+static const char MIXER_SWITCH[] = "switch";
+static const char MIXER_SCALAR[] = "scalar";
+static const char MIXER_OFFSET[] = "offset";
+static const char MIXER_USETRIM[] = "usetrim";
+
+static const char MIXER_MUXTYPE[] = "muxtype";
+static const char * const MIXER_MUXTYPE_VAL[MUX_LAST] = {
+ "replace", "multiply", "add", "max", "min", "delay",
+#if HAS_EXTENDED_AUDIO
+ "beep","voice",
+#endif
+};
+
+static const char MIXER_CURVETYPE[] = "curvetype";
+static const char * const MIXER_CURVETYPE_VAL[CURVE_MAX+1] = {
+ "none", "fixed", "min/max", "zero/max", "greater-than-0", "less-than-0", "absval",
+ "expo", "deadband", "3point", "5point", "7point", "9point", "11point", "13point" };
+static const char MIXER_CURVE_POINTS[] = "points";
+static const char MIXER_CURVE_SMOOTH[] = "smooth";
+
+/* Section: Channel */
+static const char SECTION_CHANNEL[] = "channel";
+
+static const char CHAN_DISPLAY_FORMAT[] = "display-format";
+static const char CHAN_DISPLAY_SCALE[] = "display-scale";
+static const char CHAN_LIMIT_REVERSE[] = "reverse";
+static const char CHAN_LIMIT_SAFETYSW[] = "safetysw";
+static const char CHAN_LIMIT_SAFETYVAL[] = "safetyval";
+static const char CHAN_LIMIT_FAILSAFE[] = "failsafe";
+static const char CHAN_LIMIT_MAX[] = "max";
+static const char CHAN_LIMIT_MIN[] = "min";
+static const char CHAN_LIMIT_SPEED[] = "speed";
+static const char CHAN_SUBTRIM[] = "subtrim";
+static const char CHAN_SCALAR_NEG[] = "scalar-";
+#define CHAN_SCALAR MIXER_SCALAR
+#define CHAN_TEMPLATE MODEL_TEMPLATE
+static const char * const CHAN_TEMPLATE_VAL[MIXERTEMPLATE_MAX+1] =
+ { "none", "simple", "expo_dr", "complex", "cyclic1", "cyclic2", "cyclic3" };
+
+/* Section: Virtual Channel */
+static const char SECTION_VIRTCHAN[] = "virtchan";
+#define VCHAN_TEMPLATE CHAN_TEMPLATE
+#define VCHAN_TEMPLATE_VAL CHAN_TEMPLATE_VAL
+#define VCHAN_NAME MODEL_NAME
+
+/* Section: PPM-In */
+static const char SECTION_PPMIN[] = "ppm-in";
+static const char PPMIN_MAP[] = "map";
+static const char PPMIN_MODE[] = "mode";
+static const char * const PPMIN_MODE_VALUE[4] = {"none", "channel", "stick", "extend"};
+static const char PPMIN_CENTERPW[] = "centerpw";
+static const char PPMIN_DELTAPW[] = "deltapw";
+#define PPMIN_NUM_CHANNELS RADIO_NUM_CHANNELS
+#define PPMIN_SWITCH MIXER_SWITCH
+
+/* Section: Trim */
+static const char SECTION_TRIM[] = "trim";
+
+#define TRIM_SOURCE MIXER_SOURCE
+static const char TRIM_POS[] = "pos";
+static const char TRIM_NEG[] = "neg";
+static const char TRIM_STEP[] = "step";
+static const char TRIM_VALUE[] = "value";
+#define TRIM_SWITCH MIXER_SWITCH
+
+/* Section: Heli */
+static const char SECTION_SWASH[] = "swash";
+#define SWASH_TYPE MODEL_TYPE
+static const char SWASH_AIL_INV[] = "ail_inv";
+static const char SWASH_ELE_INV[] = "ele_inv";
+static const char SWASH_COL_INV[] = "col_inv";
+static const char SWASH_AILMIX[] = "ail_mix";
+static const char SWASH_ELEMIX[] = "ele_mix";
+static const char SWASH_COLMIX[] = "col_mix";
+
+/* Section: Timer */
+static const char SECTION_TIMER[] = "timer";
+
+#define TIMER_SOURCE MIXER_SOURCE
+#define TIMER_TYPE MODEL_TYPE
+static const char * const TIMER_TYPE_VAL[TIMER_LAST] = {
+ [TIMER_STOPWATCH] = "stopwatch",
+ [TIMER_STOPWATCH_PROP] = "stop-prop",
+ [TIMER_COUNTDOWN] = "countdown",
+ [TIMER_COUNTDOWN_PROP] = "cntdn-prop",
+ [TIMER_PERMANENT] = "permanent",
+ };
+static const char TIMER_TIME[] = "time";
+static const char TIMER_RESETSRC[] = "resetsrc";
+#if HAS_PERMANENT_TIMER
+static const char PERMANENT_TIMER[] = "permanent_timer";
+#endif
+static const char TIMER_VAL[] = "val";
+
+/* Section: Safety */
+static const char SECTION_SAFETY[] = "safety";
+static const char * const SAFETY_VAL[SAFE_MAX+1] = { "none", "min", "zero", "max" };
+
+/* Section: Telemetry */
+static const char SECTION_TELEMALARM[] = "telemalarm";
+static const char TELEM_SRC[] = "source";
+static const char TELEM_ABOVE[] = "above";
+static const char TELEM_VALUE[] = "value";
+static const char TELEM_THRESHOLD[] ="threshold";
+
+#if HAS_DATALOG
+/* Section: Datalog */
+static const char SECTION_DATALOG[] = "datalog";
+static const char DATALOG_RATE[] = "rate";
+#define DATALOG_SWITCH MIXER_SWITCH
+#define DATALOG_SOURCE TELEM_SRC
+#endif
+
+/* Section: Gui-QVGA */
+#define STRINGIFY(x) _STRINGIFY(x)
+#define _STRINGIFY(x) #x
+static const char SECTION_GUI[] = "gui-" STRINGIFY(LCD_WIDTH) "x" STRINGIFY(LCD_HEIGHT);
+static const char GUI_QUICKPAGE[] = "quickpage";
+
+#if HAS_EXTENDED_AUDIO
+/* Section: Music */
+static const char SECTION_VOICE[] = "voice";
+static const char * const VOICE_TELEMALARM[TELEM_NUM_ALARMS] =
+ { "telemalarm1", "telemalarm2", "telemalarm3", "telemalarm4", "telemalarm5", "telemalarm6" };
+static const char * const VOICE_TIMER[NUM_TIMERS] =
+ { "timer1", "timer2", "timer3", "timer4" };
+
+#endif
+
+
+static s8 mapstrcasecmp(const char *s1, const char *s2)
+{
+ int i = 0;
+ while(1) {
+ if(s1[i] == s2[i]
+ || (s2[i] >= 'a' && s1[i] + ('a'-'A') == s2[i])
+ || (s1[i] >= 'a' && s2[i] + ('a'-'A') == s1[i])
+ || (s2[i] == ' ' && s1[i] == '_')
+ || (s1[i] == ' ' && s2[i] == '_'))
+ {
+ if(s1[i] == '\0')
+ return 0;
+ i++;
+ continue;
+ }
+ return(s1[i] < s2[i] ? -1 : 1);
+ }
+}
+static u8 get_source(const char *section, const char *value)
+{
+ unsigned i;
+ unsigned val;
+ const char *ptr = (value[0] == '!') ? value + 1 : value;
+ const char *tmp;
+ char cmp[10];
+ for (i = 0; i <= NUM_SOURCES; i++) {
+ if(mapstrcasecmp(INPUT_SourceNameReal(cmp, i), ptr) == 0) {
+ #if defined(HAS_SWITCHES_NOSTOCK) && HAS_SWITCHES_NOSTOCK
+ #define SWITCH_NOSTOCK ((1 << INP_HOLD0) | (1 << INP_HOLD1) | \
+ (1 << INP_FMOD0) | (1 << INP_FMOD1))
+ if ((Transmitter.ignore_src & SWITCH_NOSTOCK) == SWITCH_NOSTOCK) {
+ if(mapstrcasecmp("FMODE0", ptr) == 0 ||
+ mapstrcasecmp("FMODE1", ptr) == 0 ||
+ mapstrcasecmp("HOLD0", ptr) == 0 ||
+ mapstrcasecmp("HOLD1", ptr) == 0)
+ break;
+ }
+ #endif //HAS_SWITCHES_NOSTOCK
+ return ((ptr == value) ? 0 : 0x80) | i;
+ }
+ }
+ for (i = 0; i < 4; i++) {
+ if(mapstrcasecmp(tx_stick_names[i], ptr) == 0) {
+ return ((ptr == value) ? 0 : 0x80) | (i + 1);
+ }
+ }
+ i = 0;
+ while((tmp = INPUT_MapSourceName(i++, &val))) {
+ if(mapstrcasecmp(tmp, ptr) == 0) {
+ return ((ptr == value) ? 0 : 0x80) | val;
+ }
+ }
+ printf("%s: Could not parse Source %s\n", section, value);
+ return 0;
+}
+
+static u8 get_button(const char *section, const char *value)
+{
+ u8 i;
+ for (i = 0; i <= NUM_TX_BUTTONS; i++) {
+ if(strcasecmp(INPUT_ButtonName(i), value) == 0) {
+ return i;
+ }
+ }
+ printf("%s: Could not parse Button %s\n", section, value);
+ return 0;
+}
+
+static int handle_proto_opts(struct Model *m, const char* key, const char* value, const char **opts)
+{
+ const char **popts = opts;
+ int idx = 0;
+ while(*popts) {
+ if(mapstrcasecmp(*popts, key) == 0) {
+ popts++;
+ int start = exact_atoi(popts[0]);
+ int end = exact_atoi(popts[1]);
+ int is_num = ((start != 0 || end != 0) && (popts[2] == 0 || (popts[3] == 0 && exact_atoi(popts[2]) != 0))) ? 1 : 0;
+ if(is_num) {
+ m->proto_opts[idx] = atoi(value);
+ return 1;
+ }
+ int val = 0;
+ while(popts[val]) {
+ if(mapstrcasecmp(popts[val], value) == 0) {
+ m->proto_opts[idx] = val;
+ return 1;
+ }
+ val++;
+ }
+ printf("Unknown protocol option '%s' for '%s'\n", value, key);
+ return 1;
+ }
+ //Find end of options
+ while(*popts) {
+ popts++;
+ }
+ popts++; //Go to next option
+ idx++;
+ }
+ return 0;
+}
+
+enum {
+ S8,
+ U8,
+ S16
+};
+static const char * parse_partial_int_list(const char *ptr, void *vals, int *max_count, int type)
+{
+ //const char *origptr = ptr;
+ int value_int = 0;
+ int idx = 0;
+ int sign = 0;
+
+ while(1) {
+ if(*ptr == ',' || *ptr == '\0') {
+ value_int = value_int * (sign ? -1 : 1);
+ if (type == S8) {
+ if (value_int > 127)
+ value_int = 127;
+ else if (value_int < -127)
+ value_int = -127;
+ ((s8 *)vals)[idx] = value_int;
+ } else if (type == U8) {
+ if (value_int > 255)
+ value_int = 255;
+ else if (value_int < 0)
+ value_int = 0;
+ ((u8 *)vals)[idx] = value_int;
+ } else {
+ ((s16 *)vals)[idx] = value_int;
+ }
+ sign = 0;
+ value_int = 0;
+ idx++;
+ --*max_count;
+ if (*max_count == 0 || *ptr == '\0')
+ return ptr;
+ } else if(*ptr == '-') {
+ sign = 1;
+ } else if (*ptr >= '0' && *ptr <= '9') {
+ value_int = value_int * 10 + (*ptr - '0');
+ } else {
+ //printf("Bad value '%c' in '%s'\n", *ptr, origptr);
+ return ptr;
+ }
+ ptr++;
+ }
+}
+
+static int parse_int_list(const char *ptr, void *vals, int max_count, int type)
+{
+ int count = max_count;
+ parse_partial_int_list(ptr, vals, &count, type);
+ return max_count - count;
+}
+
+static void create_element(struct elem *elem, int type, s16 *data)
+{
+ //int x, int y, int src, int e0, int e1, int e2)
+ ELEM_SET_X(*elem, data[0]);
+ ELEM_SET_Y(*elem, data[1]);
+ ELEM_SET_TYPE(*elem, type);
+ elem->src = data[5];
+ elem->extra[0] = data[2];
+ elem->extra[1] = data[3];
+ elem->extra[2] = data[4];
+}
+
+static int layout_ini_handler(void* user, const char* section, const char* name, const char* value)
+{
+ struct Model *m = (struct Model *)user;
+ u16 i;
+ int offset_x = 0, offset_y = 0;
+ CLOCK_ResetWatchdog();
+ int idx;
+ if (MATCH_START(name, GUI_QUICKPAGE)) {
+ u8 idx = name[9] - '1';
+ if (idx >= NUM_QUICKPAGES) {
+ printf("%s: Only %d quickpages are supported\n", section, NUM_QUICKPAGES);
+ return 1;
+ }
+ int max = PAGE_GetNumPages();
+ for(i = 0; i < max; i++) {
+ if(mapstrcasecmp(PAGE_GetName(i), value) == 0) {
+ m->pagecfg2.quickpage[idx] = i;
+ return 1;
+ }
+ }
+ printf("%s: Unknown page '%s' for quickpage%d\n", section, value, idx+1);
+ return 1;
+ }
+#ifdef ENABLE_320x240_GUI
+ static u8 seen_res = 0;
+ enum {
+ LOWRES = 1,
+ HIRES,
+ };
+ if (! MATCH_SECTION(SECTION_GUI)) {
+ if(MATCH_SECTION("gui-320x240")
+ && (! ELEM_USED(Model.pagecfg2.elem[0]) || seen_res != HIRES))
+ {
+ seen_res = LOWRES;
+ offset_x = (LCD_WIDTH - 320) / 2;
+ offset_y = (LCD_HEIGHT - 240) / 2;
+ } else
+ return 1;
+ } else {
+ if (seen_res == LOWRES) {
+ memset(&Model.pagecfg2.elem, 0, sizeof(Model.pagecfg2.elem));
+ }
+ seen_res = HIRES;
+ }
+#else
+ if (! MATCH_SECTION(SECTION_GUI))
+ return 1;
+#endif
+ for (idx = 0; idx < NUM_ELEMS; idx++) {
+ if (! ELEM_USED(Model.pagecfg2.elem[idx]))
+ break;
+ }
+
+ if (idx == NUM_ELEMS) {
+ printf("No free element available (max = %d)\n", NUM_ELEMS);
+ return 1;
+ }
+ int type;
+ for (type = 0; type < ELEM_LAST; type++)
+ if(mapstrcasecmp(name, GetElemName(type)) == 0)
+ break;
+ if (type == ELEM_LAST)
+ return 1;
+ int count = 5;
+ s16 data[6] = {0};
+ const char *ptr = parse_partial_int_list(value, data, &count, S16);
+ data[0] += offset_x;
+ data[1] += offset_y;
+ if (count > 3) {
+ printf("Could not parse coordinates from %s=%s\n", name,value);
+ return 1;
+ }
+ switch(type) {
+ //case ELEM_MODEL: //x, y
+ case ELEM_VTRIM: //x, y, src
+ case ELEM_HTRIM: //x, y, src
+ data[5] = data[2];
+ data[2] = 0;
+ break;
+ case ELEM_SMALLBOX: //x, y, src
+ case ELEM_BIGBOX: //x. y. src
+ {
+ s16 src = -1;
+ char str[20];
+ if (count != 3)
+ return 1;
+#if HAS_RTC
+ for(i = 0; i < NUM_RTC; i++) {
+ if(mapstrcasecmp(ptr, RTC_Name(str, i)) == 0) {
+ src = i + 1;
+ break;
+ }
+ }
+#endif
+ if (src == -1) {
+ for(i = 0; i < NUM_TIMERS; i++) {
+ if(mapstrcasecmp(ptr, TIMER_Name(str, i)) == 0) {
+ src = i + 1 + NUM_RTC;
+ break;
+ }
+ }
+ }
+ if (src == -1) {
+ for(i = 0; i < NUM_TELEM; i++) {
+ if(mapstrcasecmp(ptr, TELEMETRY_Name(str, i+1)) == 0) {
+ src = i + 1 + NUM_RTC + NUM_TIMERS;
+ break;
+ }
+ }
+ }
+ if (src == -1) {
+ u8 newsrc = get_source(section, ptr);
+ if(newsrc >= NUM_INPUTS) {
+ src = newsrc - (NUM_INPUTS + 1 - (NUM_RTC + NUM_TIMERS + NUM_TELEM + 1));
+ }
+ }
+ if (src == -1)
+ src = 0;
+ data[5] = src;
+ break;
+ }
+ case ELEM_BAR: //x, y, src
+ {
+ if (count != 3)
+ return 1;
+ u8 src = get_source(section, ptr);
+ if (src < NUM_INPUTS)
+ src = 0;
+ data[5] = src - NUM_INPUTS;
+ break;
+ }
+ case ELEM_TOGGLE: //x, y, tgl0, tgl1, tgl2, src
+ {
+ if(count)
+ return 1;
+ for (int j = 0; j <= NUM_SOURCES; j++) {
+ char cmp[10];
+ if(mapstrcasecmp(INPUT_SourceNameAbbrevSwitchReal(cmp, j), ptr+1) == 0) {
+ data[5] = j;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ create_element(&m->pagecfg2.elem[idx], type, data);
+ return 1;
+}
+
+struct struct_map {const char *str; u16 offset; u16 defval;};
+#define MAPSIZE(x) (sizeof(x) / sizeof(struct struct_map))
+#define OFFSET(s,v) (((long)(&s.v) - (long)(&s)) | ((sizeof(s.v)-1) << 13))
+#define OFFSETS(s,v) (((long)(&s.v) - (long)(&s)) | ((sizeof(s.v)+3) << 13))
+#define OFFSET_SRC(s,v) (((long)(&s.v) - (long)(&s)) | (2 << 13))
+#define OFFSET_BUT(s,v) (((long)(&s.v) - (long)(&s)) | (6 << 13))
+#if HAS_PERMANENT_TIMER
+static const struct struct_map _secnone[] =
+{
+ {PERMANENT_TIMER, OFFSET(Model, permanent_timer), 0},
+};
+#endif
+static const struct struct_map _secradio[] = {
+ {RADIO_NUM_CHANNELS, OFFSET(Model, num_channels), 0},
+ {RADIO_FIXED_ID, OFFSET(Model, fixed_id), 0},
+#if HAS_VIDEO
+ {RADIO_VIDEOSRC, OFFSET_SRC(Model, videosrc), 0},
+ {RADIO_VIDEOCH, OFFSET(Model, videoch), 0},
+ {RADIO_VIDEOCONTRAST,OFFSET(Model, video_contrast), 0},
+ {RADIO_VIDEOBRIGHTNESS,OFFSET(Model, video_brightness), 0},
+#endif
+#if HAS_EXTENDED_TELEMETRY
+ {RADIO_GROUND_LEVEL, OFFSET(Model, ground_level), 0},
+#endif
+};
+static const struct struct_map _secmixer[] = {
+ {MIXER_SWITCH, OFFSET_SRC(Model.mixers[0], sw), 0},
+ {MIXER_SCALAR, OFFSETS(Model.mixers[0], scalar), 100},
+ {MIXER_OFFSET, OFFSETS(Model.mixers[0], offset), 0},
+};
+static const struct struct_map _seclimit[] = {
+ {CHAN_LIMIT_SAFETYSW, OFFSET_SRC(Model.limits[0], safetysw), 0},
+ {CHAN_LIMIT_SAFETYVAL, OFFSETS(Model.limits[0], safetyval), 0},
+ {CHAN_LIMIT_MAX, OFFSET(Model.limits[0], max), DEFAULT_SERVO_LIMIT},
+ {CHAN_LIMIT_SPEED, OFFSET(Model.limits[0], speed), 0},
+ {CHAN_SCALAR, OFFSET(Model.limits[0], servoscale), 100},
+ {CHAN_SCALAR_NEG, OFFSET(Model.limits[0], servoscale_neg), 0},
+ {CHAN_SUBTRIM, OFFSETS(Model.limits[0], subtrim), 0},
+ {CHAN_DISPLAY_SCALE, OFFSETS(Model.limits[0], displayscale), DEFAULT_DISPLAY_SCALE},
+};
+static const struct struct_map _sectrim[] = {
+ {TRIM_SOURCE, OFFSET_SRC(Model.trims[0], src), 0xFFFF},
+ {TRIM_POS, OFFSET_BUT(Model.trims[0], pos), 0},
+ {TRIM_NEG, OFFSET_BUT(Model.trims[0], neg), 0},
+ {TRIM_STEP, OFFSET(Model.trims[0], step), 1},
+};
+static const struct struct_map _secswash[] = {
+ {SWASH_AILMIX, OFFSET(Model, swashmix[0]), 60},
+ {SWASH_ELEMIX, OFFSET(Model, swashmix[1]), 60},
+ {SWASH_COLMIX, OFFSET(Model, swashmix[2]), 60},
+};
+static const struct struct_map _sectimer[] = {
+ {TIMER_TIME, OFFSET(Model.timer[0], timer), 0xFFFF},
+ {TIMER_VAL, OFFSET(Model.timer[0], val), 0xFFFF},
+ {TIMER_SOURCE, OFFSET_SRC(Model.timer[0], src), 0},
+ {TIMER_RESETSRC, OFFSET_SRC(Model.timer[0], resetsrc), 0},
+};
+static const struct struct_map _secppm[] = {
+ {PPMIN_CENTERPW, OFFSET(Model, ppmin_centerpw), 0},
+ {PPMIN_DELTAPW, OFFSET(Model, ppmin_deltapw), 0},
+ {PPMIN_SWITCH, OFFSET_SRC(Model, train_sw), 0xFFFF},
+};
+static int ini_handler(void* user, const char* section, const char* name, const char* value)
+{
+ int value_int = atoi(value);
+ struct Model *m = (struct Model *)user;
+int assign_int(void* ptr, const struct struct_map *map, int map_size)
+{
+ for(int i = 0; i < map_size; i++) {
+ if(MATCH_KEY(map[i].str)) {
+ int size = map[i].offset >> 13;
+ int offset = map[i].offset & 0x1FFF;
+ switch(size) {
+ case 0:
+ *((u8 *)((long)ptr + offset)) = value_int; break;
+ case 1:
+ *((u16 *)((long)ptr + offset)) = value_int; break;
+ case 2:
+ *((u8 *)((long)ptr + offset)) = get_source(section, value); break;
+ case 3:
+ *((u32 *)((long)ptr + offset)) = value_int; break;
+ case 4:
+ *((s8 *)((long)ptr + offset)) = value_int; break;
+ case 5:
+ *((s16 *)((long)ptr + offset)) = value_int; break;
+ case 6:
+ *((u8 *)((long)ptr + offset)) = get_button(section, value); break;
+ case 7:
+ *((s32 *)((long)ptr + offset)) = value_int; break;
+ }
+ return 1;
+ }
+ }
+ return 0;
+}
+ CLOCK_ResetWatchdog();
+ unsigned i;
+ if (MATCH_SECTION("")) {
+ if(MATCH_KEY(MODEL_NAME)) {
+ strlcpy(m->name, value, sizeof(m->name)-1);
+ return 1;
+ }
+ if(MATCH_KEY(MODEL_TEMPLATE)) {
+ //A dummy rule
+ return 1;
+ }
+ if (MATCH_KEY(MODEL_ICON)) {
+ CONFIG_ParseIconName(m->icon, value);
+ return 1;
+ }
+#if HAS_PERMANENT_TIMER
+ if(assign_int(&Model, _secnone, MAPSIZE(_secnone)))
+ return 1;
+#endif
+ if (MATCH_KEY(MODEL_TYPE)) {
+ for (i = 0; i < NUM_STR_ELEMS(MODEL_TYPE_VAL); i++) {
+ if (MATCH_VALUE(MODEL_TYPE_VAL[i])) {
+ m->type = i;
+ return 1;
+ }
+ }
+ return 0;
+ }
+ if (MATCH_KEY(MODEL_AUTOMAP)) {
+ auto_map = atoi(value);
+ return 1;
+ }
+ if (MATCH_KEY(MODEL_MIXERMODE)) {
+ for(i = 1; i < 3; i++) {
+ if(MATCH_VALUE(STDMIXER_ModeName(i)))
+ m->mixer_mode = i;
+ }
+ return 1;
+ }
+ }
+ if (MATCH_SECTION(SECTION_RADIO)) {
+ if (MATCH_KEY(RADIO_PROTOCOL)) {
+ for (i = 0; i < PROTOCOL_COUNT; i++) {
+ if (MATCH_VALUE(PROTOCOL_GetName(i))) {
+ m->protocol = i;
+ m->radio = PROTOCOL_GetRadio(i);
+ PROTOCOL_Load(1);
+ return 1;
+ }
+ }
+ printf("Unknown protocol: %s\n", value);
+ return 1;
+ }
+ if(assign_int(&Model, _secradio, MAPSIZE(_secradio)))
+ return 1;
+ if (MATCH_KEY(RADIO_TX_POWER)) {
+ if (m->radio == TX_MODULE_LAST) {
+ m->tx_power = TXPOWER_150mW;
+ return 1;
+ }
+ for (i = 0; i < RADIO_TX_POWER_COUNT[m->radio]; i++) {
+ if (MATCH_VALUE(radio_tx_power_val(m->radio, i))) {
+ m->tx_power = i;
+ return 1;
+ }
+ }
+ printf("Unknown Tx power: %s\n", value);
+ m->tx_power = RADIO_TX_POWER_COUNT[m->radio]-1; // default to radio maximum
+ return 1;
+ }
+ printf("Unknown Radio Key: %s\n", name);
+ return 0;
+ }
+ if (MATCH_SECTION(SECTION_PROTO_OPTS)) {
+ const char **opts = PROTOCOL_GetOptions();
+ if (!opts || ! *opts)
+ return 1;
+ return handle_proto_opts(m, name, value, opts);
+ }
+ if (MATCH_START(section, SECTION_MIXER)) {
+ int idx;
+ for (idx = 0; idx < NUM_MIXERS; idx++) {
+ if(m->mixers[idx].src == 0)
+ break;
+ }
+ if (MATCH_KEY(MIXER_SOURCE)) {
+ if (idx == NUM_MIXERS) {
+ printf("%s: Only %d mixers are supported\n", section, NUM_MIXERS);
+ return 1;
+ }
+ m->mixers[idx].src = get_source(section, value);
+ return 1;
+ }
+ idx--;
+ if (MATCH_KEY(MIXER_DEST)) {
+ m->mixers[idx].dest = get_source(section, value) - NUM_INPUTS - 1;
+ return 1;
+ }
+ if(assign_int(&m->mixers[idx], _secmixer, MAPSIZE(_secmixer)))
+ return 1;
+ if (MATCH_KEY(MIXER_USETRIM)) {
+ MIXER_SET_APPLY_TRIM(&m->mixers[idx], value_int);
+ return 1;
+ }
+ if (MATCH_KEY(MIXER_MUXTYPE)) {
+ for (i = 0; i < NUM_STR_ELEMS(MIXER_MUXTYPE_VAL); i++) {
+ if (MATCH_VALUE(MIXER_MUXTYPE_VAL[i])) {
+ MIXER_SET_MUX(&m->mixers[idx], i);
+ return 1;
+ }
+ }
+ printf("%s: Unknown Mux type: %s\n", section, value);
+ return 1;
+ }
+ if (MATCH_KEY(MIXER_CURVETYPE)) {
+ for (i = 0; i < NUM_STR_ELEMS(MIXER_CURVETYPE_VAL); i++) {
+ if (MATCH_VALUE(MIXER_CURVETYPE_VAL[i])) {
+ CURVE_SET_TYPE(&m->mixers[idx].curve, i);
+ return 1;
+ }
+ }
+ printf("%s: Unknown Curve type: %s\n", section, value);
+ return 1;
+ }
+ if (MATCH_KEY(MIXER_CURVE_POINTS)) {
+ int count = parse_int_list(value, m->mixers[idx].curve.points, MAX_POINTS, S8);
+ if (count > MAX_POINTS) {
+ printf("%s: Too many points (max points = %d\n", section, MAX_POINTS);
+ return 0;
+ }
+ return 1;
+ }
+ if (MATCH_KEY(MIXER_CURVE_SMOOTH)) {
+ CURVE_SET_SMOOTHING(&m->mixers[idx].curve, value_int);
+ return 1;
+ }
+ printf("%s: Couldn't parse key: %s\n", section, name);
+ return 0;
+ }
+ if (MATCH_START(section, SECTION_CHANNEL)) {
+ u8 idx = atoi(section + sizeof(SECTION_CHANNEL)-1);
+ if (idx == 0) {
+ printf("Unknown Channel: %s\n", section);
+ return 0;
+ }
+ if (idx > NUM_OUT_CHANNELS) {
+ printf("%s: Only %d channels are supported\n", section, NUM_OUT_CHANNELS);
+ return 1;
+ }
+ idx--;
+ if (MATCH_KEY(CHAN_LIMIT_REVERSE)) {
+ if (value_int)
+ m->limits[idx].flags |= CH_REVERSE;
+ else
+ m->limits[idx].flags &= ~CH_REVERSE;
+ return 1;
+ }
+ if (MATCH_KEY(CHAN_LIMIT_FAILSAFE)) {
+ if(strcasecmp("off", value) == 0) {
+ m->limits[idx].flags &= ~CH_FAILSAFE_EN;
+ } else {
+ m->limits[idx].failsafe = value_int;
+ m->limits[idx].flags |= CH_FAILSAFE_EN;
+ }
+ return 1;
+ }
+ if (MATCH_KEY(CHAN_DISPLAY_FORMAT)) {
+ strcpy(m->limits[idx].displayformat, value);
+ return 1;
+ }
+
+ if(assign_int(&m->limits[idx], _seclimit, MAPSIZE(_seclimit)))
+ return 1;
+ if (MATCH_KEY(CHAN_LIMIT_MIN)) {
+ m->limits[idx].min = -value_int;
+ return 1;
+ }
+ if (MATCH_KEY(CHAN_TEMPLATE)) {
+ for (i = 0; i < NUM_STR_ELEMS(CHAN_TEMPLATE_VAL); i++) {
+ if (MATCH_VALUE(CHAN_TEMPLATE_VAL[i])) {
+ m->templates[idx] = i;
+ return 1;
+ }
+ }
+ printf("%s: Unknown template: %s\n", section, value);
+ return 1;
+ }
+ printf("%s: Unknown key: %s\n", section, name);
+ return 0;
+ }
+ if (MATCH_START(section, SECTION_VIRTCHAN)) {
+ u8 idx = atoi(section + sizeof(SECTION_VIRTCHAN)-1);
+ if (idx == 0) {
+ printf("Unknown Virtual Channel: %s\n", section);
+ return 0;
+ }
+ if (idx > NUM_VIRT_CHANNELS) {
+ printf("%s: Only %d virtual channels are supported\n", section, NUM_VIRT_CHANNELS);
+ return 1;
+ }
+ idx = idx + NUM_OUT_CHANNELS - 1;
+ if (MATCH_KEY(VCHAN_TEMPLATE)) {
+ for (i = 0; i < NUM_STR_ELEMS(VCHAN_TEMPLATE_VAL); i++) {
+ if (MATCH_VALUE(VCHAN_TEMPLATE_VAL[i])) {
+ m->templates[idx] = i;
+ return 1;
+ }
+ }
+ printf("%s: Unknown template: %s\n", section, value);
+ return 1;
+ }
+ if (MATCH_KEY(VCHAN_NAME)) {
+ strlcpy(m->virtname[idx - NUM_OUT_CHANNELS], value, sizeof(m->virtname[0]));
+ return 1;
+ }
+ printf("%s: Unknown key: %s\n", section, name);
+ return 0;
+ }
+ if (MATCH_START(section, SECTION_TRIM)) {
+ u8 idx = atoi(section + sizeof(SECTION_TRIM)-1);
+ if (idx == 0) {
+ printf("Unknown Trim: %s\n", section);
+ return 0;
+ }
+ if (idx > NUM_TRIMS) {
+ printf("%s: Only %d trims are supported\n", section, NUM_TRIMS);
+ return 1;
+ }
+ idx--;
+ if(assign_int(&m->trims[idx], _sectrim, MAPSIZE(_sectrim)))
+ return 1;
+ if (MATCH_KEY(TRIM_SWITCH)) {
+ for (int i = 0; i <= NUM_SOURCES; i++) {
+ char cmp[10];
+ if(mapstrcasecmp(INPUT_SourceNameAbbrevSwitchReal(cmp, i), value) == 0) {
+ m->trims[idx].sw = i;
+ return 1;
+ }
+ }
+ return 1;
+ }
+ if (MATCH_KEY(TRIM_VALUE)) {
+ parse_int_list(value, m->trims[idx].value, 6, S8);
+ return 1;
+ }
+ printf("%s: Unknown trim setting: %s\n", section, name);
+ return 0;
+ }
+ if (MATCH_SECTION(SECTION_SWASH)) {
+ if (MATCH_KEY(SWASH_TYPE)) {
+ for (i = SWASH_TYPE_NONE; i <= SWASH_TYPE_90; i++) {
+ if(strcasecmp(MIXER_SwashType(i), value) == 0) {
+ m->swash_type = i;
+ return 1;
+ }
+ }
+ printf("%s: Unknown swash_type: %s\n", section, value);
+ return 1;
+ }
+ if (MATCH_KEY(SWASH_ELE_INV)) {
+ if (value_int)
+ m->swash_invert |= 0x01;
+ return 1;
+ }
+ if (MATCH_KEY(SWASH_AIL_INV)) {
+ if (value_int)
+ m->swash_invert |= 0x02;
+ return 1;
+ }
+ if (MATCH_KEY(SWASH_COL_INV)) {
+ if (value_int)
+ m->swash_invert |= 0x04;
+ return 1;
+ }
+ if(assign_int(m, _secswash, MAPSIZE(_secswash)))
+ return 1;
+ }
+ if (MATCH_START(section, SECTION_TIMER)) {
+ u8 idx = atoi(section + sizeof(SECTION_TIMER)-1);
+ if (idx == 0) {
+ printf("Unknown Timer: %s\n", section);
+ return 0;
+ }
+ if (idx > NUM_TIMERS) {
+ printf("%s: Only %d timers are supported\n", section, NUM_TIMERS);
+ return 1;
+ }
+ idx--;
+ if (MATCH_KEY(TIMER_TYPE)) {
+ for (i = 0; i < NUM_STR_ELEMS(TIMER_TYPE_VAL); i++) {
+ if (MATCH_VALUE(TIMER_TYPE_VAL[i])) {
+ m->timer[idx].type = i;
+ return 1;
+ }
+ }
+ printf("%s: Unknown timer type: %s\n", section, value);
+ return 1;
+ }
+ if(assign_int(&m->timer[idx], _sectimer, MAPSIZE(_sectimer)))
+ return 1;
+ }
+ if (MATCH_START(section, SECTION_TELEMALARM)) {
+ u8 idx = atoi(section + sizeof(SECTION_TELEMALARM)-1);
+ if (idx == 0) {
+ printf("Unknown Telem-alarm: %s\n", section);
+ return 0;
+ }
+ if (idx > TELEM_NUM_ALARMS) {
+ printf("%s: Only %d timers are supported\n", section, TELEM_NUM_ALARMS);
+ return 1;
+ }
+ struct TelemetryAlarm *alarm = &Model.alarms[idx - 1]; // idx is 1 based
+ if (MATCH_KEY(TELEM_SRC)) {
+ char str[20];
+ unsigned last = TELEMETRY_GetNumTelemSrc();
+ for(i = 1; i <= last; i++) {
+ if (strcasecmp(TELEMETRY_ShortName(str, i), value) == 0) {
+ alarm->src = i;
+ return 1;
+ }
+ }
+ printf("%s: Unknown telemetry src: %s\n", section, value);
+ return 0;
+ }
+ if (MATCH_KEY(TELEM_ABOVE)) {
+ if (atoi(value))
+ alarm->above = 1;
+ else
+ alarm->above = 0;
+ return 1;
+ }
+ if (MATCH_KEY(TELEM_VALUE)) {
+ alarm->value = atoi(value);
+ return 1;
+ }
+ if (MATCH_KEY(TELEM_THRESHOLD)) {
+ alarm->threshold = atoi(value);
+ return 1;
+ }
+ }
+#if HAS_DATALOG
+ if (MATCH_SECTION(SECTION_DATALOG)) {
+ if (MATCH_KEY(DATALOG_SWITCH)) {
+ m->datalog.enable = get_source(section, value);
+ } else if (MATCH_KEY(DATALOG_RATE)) {
+ for (i = 0; i < DLOG_RATE_LAST; i++) {
+ if(mapstrcasecmp(DATALOG_RateString(i), value) == 0) {
+ m->datalog.rate = i;
+ break;
+ }
+ }
+ } else if (MATCH_KEY(DATALOG_SOURCE)) {
+ char cmp[10];
+ for (i = 0; i < DLOG_LAST; i++) {
+ if(mapstrcasecmp(DATALOG_Source(cmp, i), value) == 0) {
+ m->datalog.source[DATALOG_BYTE(i)] |= 1 << DATALOG_POS(i);
+ break;
+ }
+ }
+ }
+ return 1;
+ }
+#endif //HAS_DATALOG
+ if (MATCH_START(section, SECTION_SAFETY)) {
+ int found = 0;
+ u8 src;
+ if (MATCH_KEY("auto")) {
+ src = 0;
+ found = 1;
+ } else {
+ src = get_source(section, name);
+ }
+ if(found || src) {
+ u32 i;
+ for (i = 0; i < NUM_STR_ELEMS(SAFETY_VAL); i++) {
+ if (MATCH_VALUE(SAFETY_VAL[i])) {
+ m->safety[src] = i;
+ return 1;
+ }
+ }
+ }
+ }
+ if (MATCH_START(section, "gui-")) {
+ return layout_ini_handler(user, section, name, value);
+ }
+ if (MATCH_SECTION(SECTION_PPMIN)) {
+ if (MATCH_KEY(PPMIN_NUM_CHANNELS)) {
+ m->num_ppmin_channels = atoi(value);
+ return 1;
+ }
+ if (MATCH_KEY(PPMIN_MODE)) {
+ for(i = 0; i < 4; i++) {
+ if(mapstrcasecmp(PPMIN_MODE_VALUE[i], value) == 0) {
+ m->ppmin_mode = i;
+ return 1;
+ }
+ }
+ return 1;
+ }
+ if(assign_int(m, _secppm, MAPSIZE(_secppm)))
+ return 1;
+ if (MATCH_START(name, PPMIN_MAP)) {
+ u8 idx = atoi(name + sizeof(PPMIN_MAP)-1) -1;
+ if (idx < MAX_PPM_IN_CHANNELS) {
+ m->ppm_map[idx] = get_source(section, value);
+ if (PPMin_Mode() == PPM_IN_TRAIN1) {
+ m->ppm_map[idx] = (m->ppm_map[idx] <= NUM_INPUTS)
+ ? -1
+ : m->ppm_map[idx] - (NUM_INPUTS + 1);
+ }
+ }
+ return 1;
+ }
+ }
+#if HAS_EXTENDED_AUDIO
+ char src_name[20];
+
+ if (MATCH_SECTION(SECTION_VOICE)) {
+ u16 val = atoi(value);
+ if(val>MAX_VOICEMAP_ENTRIES-1 || voice_map[val].duration == 0 || val < CUSTOM_ALARM_ID) {
+ printf("%s: Music %s not found in voice.ini or below ID %d\n", section, value, CUSTOM_ALARM_ID);
+ return 0;
+ }
+ for (int i = INP_HAS_CALIBRATION+1; i <= NUM_INPUTS; i++) {
+ INPUT_SourceName(src_name, i);
+ if (MATCH_KEY(src_name)) {
+ m->voice.switches[i - INP_HAS_CALIBRATION - 1].music = val;
+ return 1;
+ }
+ }
+#if NUM_AUX_KNOBS
+ for (int i = 0; i < NUM_AUX_KNOBS; i++) {
+ INPUT_SourceName(src_name, i + NUM_STICKS + 1);
+ strcat(src_name, "_UP");
+ if (MATCH_KEY(src_name)) {
+ m->voice.aux[i * 2 + 1].music = val;
+ return 1;
+ }
+ INPUT_SourceName(src_name, i + NUM_STICKS + 1);
+ strcat(src_name, "_DOWN");
+ if (MATCH_KEY(src_name)) {
+ m->voice.aux[i * 2].music = val;
+ return 1;
+ }
+ }
+#endif
+ for (int i = 0; i < NUM_TIMERS; i++) {
+ if (MATCH_KEY(VOICE_TIMER[i])) {
+ m->voice.timer[i].music = val;
+ return 1;
+ }
+ }
+ for (int i = 0; i < TELEM_NUM_ALARMS; i++) {
+ if (MATCH_KEY(VOICE_TELEMALARM[i])) {
+ m->voice.telemetry[i].music = val;
+ return 1;
+ }
+ }
+ for (int i = 0; i < (NUM_OUT_CHANNELS + NUM_VIRT_CHANNELS); i++) {
+ INPUT_SourceNameReal(src_name, i + NUM_INPUTS + 1);
+ if (MATCH_KEY(src_name)) {
+ m->voice.mixer[i].music = val;
+ return 1;
+ }
+ }
+ printf("%s: unknown source name '%s'\n", section, name);
+ return 0;
+ }
+#endif
+ printf("Unknown Section: '%s'\n", section);
+ return 0;
+}
+
+static void get_model_file(char *file, u8 model_num)
+{
+ if (model_num == 0)
+ sprintf(file, "models/default.ini");
+ else
+ sprintf(file, "models/model%d.ini", model_num);
+}
+
+static void write_int(FILE *fh, void* ptr, const struct struct_map *map, int map_size)
+{
+ char tmpstr[20];
+ for(int i = 0; i < map_size; i++) {
+ int size = map[i].offset >> 13;
+ int offset = map[i].offset & 0x1FFF;
+ int value;
+ if (map[i].defval == 0xffff)
+ continue;
+ switch(size) {
+ case 0:
+ case 2: //SRC
+ case 6: //BUTTON
+ value = *((u8 *)((long)ptr + offset)); break;
+ case 1: value = *((u16 *)((long)ptr + offset)); break;
+ case 3: value = *((u32 *)((long)ptr + offset)); break;
+ case 4: value = *((s8 *)((long)ptr + offset)); break;
+ case 5: value = *((s16 *)((long)ptr + offset)); break;
+ case 7: value = *((s32 *)((long)ptr + offset)); break;
+ default: continue;
+ }
+ if(WRITE_FULL_MODEL || value != map[i].defval) {
+ if (2 == (size & 0x03)) //2, 6
+ fprintf(fh, "%s=%s\n", map[i].str, size == 2 ? INPUT_SourceNameReal(tmpstr, value) : INPUT_ButtonName(value));
+ else
+ fprintf(fh, "%s=%d\n", map[i].str, value);
+ }
+ }
+}
+
+static u8 write_mixer(FILE *fh, struct Model *m, u8 channel)
+{
+ int idx;
+ int i;
+ char tmpstr[20];
+ u8 changed = 0;
+ for(idx = 0; idx < NUM_MIXERS; idx++) {
+ if (! WRITE_FULL_MODEL && (m->mixers[idx].src == 0 || m->mixers[idx].dest != channel))
+ continue;
+ changed = 1;
+ fprintf(fh, "[%s]\n", SECTION_MIXER);
+ fprintf(fh, "%s=%s\n", MIXER_SOURCE, INPUT_SourceNameReal(tmpstr, m->mixers[idx].src));
+ fprintf(fh, "%s=%s\n", MIXER_DEST, INPUT_SourceNameReal(tmpstr, m->mixers[idx].dest + NUM_INPUTS + 1));
+ write_int(fh, &m->mixers[idx], _secmixer, MAPSIZE(_secmixer));
+ if(WRITE_FULL_MODEL || ! MIXER_APPLY_TRIM(&m->mixers[idx]))
+ fprintf(fh, "%s=%d\n", MIXER_USETRIM, MIXER_APPLY_TRIM(&m->mixers[idx]) ? 1 : 0);
+ if(WRITE_FULL_MODEL || MIXER_MUX(&m->mixers[idx]))
+ fprintf(fh, "%s=%s\n", MIXER_MUXTYPE, MIXER_MUXTYPE_VAL[MIXER_MUX(&m->mixers[idx])]);
+ if(WRITE_FULL_MODEL || CURVE_TYPE(&m->mixers[idx].curve)) {
+ fprintf(fh, "%s=%s\n", MIXER_CURVETYPE, MIXER_CURVETYPE_VAL[CURVE_TYPE(&m->mixers[idx].curve)]);
+ u8 num_points = CURVE_NumPoints(&m->mixers[idx].curve);
+ if (num_points > 0) {
+ fprintf(fh, "%s=", MIXER_CURVE_POINTS);
+ for (i = 0; i < num_points; i++) {
+ fprintf(fh, "%d", m->mixers[idx].curve.points[i]);
+ if (i != num_points - 1)
+ fprintf(fh, ",");
+ }
+ fprintf(fh, "\n");
+ }
+ if (CURVE_SMOOTHING(&m->mixers[idx].curve))
+ fprintf(fh, "%s=%d\n", MIXER_CURVE_SMOOTH, CURVE_SMOOTHING(&m->mixers[idx].curve) ? 1 : 0);
+ }
+ }
+ return changed;
+}
+
+static void write_proto_opts(FILE *fh, struct Model *m)
+{
+ const char **opts = PROTOCOL_GetOptions();
+ if (!opts || ! *opts) // bug fix: must check NULL ptr
+ return;
+ int idx = 0;
+ fprintf(fh, "[%s]\n", SECTION_PROTO_OPTS);
+ while(*opts) {
+ int start = exact_atoi(opts[1]);
+ int end = exact_atoi(opts[2]);
+ int is_num = ((start != 0 || end != 0) && (opts[3] == 0 || (opts[4] == 0 && exact_atoi(opts[3]) != 0))) ? 1 : 0;
+ if (is_num) {
+ fprintf(fh, "%s=%d\n",*opts, m->proto_opts[idx]);
+ } else {
+ fprintf(fh, "%s=%s\n",*opts, opts[m->proto_opts[idx]+1]);
+ }
+ opts++;
+ while(*opts) {
+ opts++;
+ }
+ opts++;
+ idx++;
+ }
+ fprintf(fh, "\n");
+}
+
+u8 CONFIG_WriteModel_old(u8 model_num) {
+ char file[20];
+ FILE *fh;
+ u8 idx;
+ struct Model *m = &Model;
+
+
+ get_model_file(file, model_num);
+ fh = fopen(file, "w");
+ if (! fh) {
+ printf("Couldn't open file: %s\n", file);
+ return 0;
+ }
+ CONFIG_EnableLanguage(0);
+ fprintf(fh, "%s=%s\n", MODEL_NAME, m->name);
+#if HAS_PERMANENT_TIMER
+ write_int(fh, m, _secnone, MAPSIZE(_secnone));
+#endif
+ fprintf(fh, "%s=%s\n", MODEL_MIXERMODE, STDMIXER_ModeName(m->mixer_mode));
+ if(m->icon[0] != 0)
+ fprintf(fh, "%s=%s\n", MODEL_ICON, m->icon + 9);
+ if(WRITE_FULL_MODEL || m->type != 0)
+ fprintf(fh, "%s=%s\n", MODEL_TYPE, MODEL_TYPE_VAL[m->type]);
+ fprintf(fh, "[%s]\n", SECTION_RADIO);
+ fprintf(fh, "%s=%s\n", RADIO_PROTOCOL, PROTOCOL_GetName(m->protocol));
+ write_int(fh, m, _secradio, MAPSIZE(_secradio));
+ fprintf(fh, "%s=%s\n", RADIO_TX_POWER, radio_tx_power_val(m->radio, m->tx_power));
+ fprintf(fh, "\n");
+ write_proto_opts(fh, m);
+ struct Limit default_limit;
+ memset(&default_limit, 0, sizeof(default_limit));
+ MIXER_SetDefaultLimit(&default_limit);
+ for(idx = 0; idx < NUM_OUT_CHANNELS; idx++) {
+ if(!WRITE_FULL_MODEL
+ && memcmp(&m->limits[idx], &default_limit, sizeof(default_limit)) == 0
+ && m->templates[idx] == 0)
+ {
+ if (write_mixer(fh, m, idx))
+ fprintf(fh, "\n");
+ continue;
+ }
+ fprintf(fh, "[%s%d]\n", SECTION_CHANNEL, idx+1);
+ if(WRITE_FULL_MODEL || (m->limits[idx].flags & CH_REVERSE))
+ fprintf(fh, "%s=%d\n", CHAN_LIMIT_REVERSE, (m->limits[idx].flags & CH_REVERSE) ? 1 : 0);
+ write_int(fh, &m->limits[idx], _seclimit, MAPSIZE(_seclimit));
+ if(WRITE_FULL_MODEL || (m->limits[idx].flags & CH_FAILSAFE_EN)) {
+ if(m->limits[idx].flags & CH_FAILSAFE_EN) {
+ fprintf(fh, "%s=%d\n", CHAN_LIMIT_FAILSAFE, m->limits[idx].failsafe);
+ } else {
+ fprintf(fh, "%s=Off\n", CHAN_LIMIT_FAILSAFE);
+ }
+ }
+ if(WRITE_FULL_MODEL || m->limits[idx].min != DEFAULT_SERVO_LIMIT)
+ fprintf(fh, "%s=%d\n", CHAN_LIMIT_MIN, -(int)m->limits[idx].min);
+ if(WRITE_FULL_MODEL || strcmp(m->limits[idx].displayformat, DEFAULT_DISPLAY_FORMAT) != 0)
+ fprintf(fh, "%s=%s\n", CHAN_DISPLAY_FORMAT, m->limits[idx].displayformat);
+ if(WRITE_FULL_MODEL || m->templates[idx] != 0)
+ fprintf(fh, "%s=%s\n", CHAN_TEMPLATE, CHAN_TEMPLATE_VAL[m->templates[idx]]);
+ write_mixer(fh, m, idx);
+ fprintf(fh, "\n");
+ }
+ for(idx = 0; idx < NUM_VIRT_CHANNELS; idx++) {
+ if(WRITE_FULL_MODEL || m->templates[idx+NUM_OUT_CHANNELS] != 0 || m->virtname[idx][0]) {
+ fprintf(fh, "[%s%d]\n", SECTION_VIRTCHAN, idx+1);
+ if(m->virtname[idx][0])
+ fprintf(fh, "%s=%s\n", VCHAN_NAME, m->virtname[idx]);
+ if(WRITE_FULL_MODEL || m->templates[idx+NUM_OUT_CHANNELS] != 0)
+ fprintf(fh, "%s=%s\n", VCHAN_TEMPLATE, VCHAN_TEMPLATE_VAL[m->templates[idx+NUM_OUT_CHANNELS]]);
+ }
+ if (write_mixer(fh, m, idx+NUM_OUT_CHANNELS))
+ fprintf(fh, "\n");
+ }
+ if (PPMin_Mode()) {
+ fprintf(fh, "[%s]\n", SECTION_PPMIN);
+ fprintf(fh, "%s=%s\n", PPMIN_MODE, PPMIN_MODE_VALUE[PPMin_Mode()]);
+ fprintf(fh, "%s=%d\n", PPMIN_NUM_CHANNELS, m->num_ppmin_channels);
+ if (PPMin_Mode() != PPM_IN_SOURCE) {
+ fprintf(fh, "%s=%s\n", PPMIN_SWITCH, INPUT_SourceNameReal(file, m->train_sw));
+ }
+ write_int(fh, m, _secppm, MAPSIZE(_secppm));
+ //fprintf(fh, "%s=%d\n", PPMIN_CENTERPW, m->ppmin_centerpw);
+ //fprintf(fh, "%s=%d\n", PPMIN_DELTAPW, m->ppmin_deltapw);
+ if (PPMin_Mode() != PPM_IN_SOURCE) {
+ int offset = (PPMin_Mode() == PPM_IN_TRAIN1) ? NUM_INPUTS + 1: 0;
+ for(idx = 0; idx < MAX_PPM_IN_CHANNELS; idx++) {
+ if (m->ppm_map[idx] == -1)
+ continue;
+ fprintf(fh, "%s%d=%s\n", PPMIN_MAP, idx + 1, INPUT_SourceNameReal(file, m->ppm_map[idx] + offset));
+ }
+ }
+ fprintf(fh, "\n");
+ }
+ for(idx = 0; idx < NUM_TRIMS; idx++) {
+ if (! WRITE_FULL_MODEL && m->trims[idx].src == 0)
+ continue;
+ fprintf(fh, "[%s%d]\n", SECTION_TRIM, idx+1);
+ fprintf(fh, "%s=%s\n", TRIM_SOURCE,
+ m->trims[idx].src >= 1 && m->trims[idx].src <= 4
+ ? tx_stick_names[m->trims[idx].src-1]
+ : INPUT_SourceNameReal(file, m->trims[idx].src));
+ write_int(fh, &m->trims[idx], _sectrim, MAPSIZE(_sectrim));
+ if(WRITE_FULL_MODEL || m->trims[idx].sw)
+ fprintf(fh, "%s=%s\n", TRIM_SWITCH, INPUT_SourceNameAbbrevSwitchReal(file, m->trims[idx].sw));
+ if(WRITE_FULL_MODEL || m->trims[idx].value[0] || m->trims[idx].value[1] || m->trims[idx].value[2]
+ || m->trims[idx].value[3] || m->trims[idx].value[4] || m->trims[idx].value[5])
+ fprintf(fh, "%s=%d,%d,%d,%d,%d,%d\n", TRIM_VALUE,
+ m->trims[idx].value[0], m->trims[idx].value[1], m->trims[idx].value[2],
+ m->trims[idx].value[3], m->trims[idx].value[4], m->trims[idx].value[5]);
+ }
+ if (WRITE_FULL_MODEL || m->swash_type) {
+ fprintf(fh, "[%s]\n", SECTION_SWASH);
+ fprintf(fh, "%s=%s\n", SWASH_TYPE, MIXER_SwashType(m->swash_type));
+ if (WRITE_FULL_MODEL || m->swash_invert & 0x01)
+ fprintf(fh, "%s=1\n", SWASH_ELE_INV);
+ if (WRITE_FULL_MODEL || m->swash_invert & 0x02)
+ fprintf(fh, "%s=1\n", SWASH_AIL_INV);
+ if (WRITE_FULL_MODEL || m->swash_invert & 0x04)
+ fprintf(fh, "%s=1\n", SWASH_COL_INV);
+ write_int(fh, m, _secswash, MAPSIZE(_secswash));
+ }
+ for(idx = 0; idx < NUM_TIMERS; idx++) {
+ if (! WRITE_FULL_MODEL && m->timer[idx].src == 0 && m->timer[idx].type == TIMER_STOPWATCH)
+ continue;
+ fprintf(fh, "[%s%d]\n", SECTION_TIMER, idx+1);
+ if (WRITE_FULL_MODEL || m->timer[idx].type != TIMER_STOPWATCH)
+ fprintf(fh, "%s=%s\n", TIMER_TYPE, TIMER_TYPE_VAL[m->timer[idx].type]);
+ write_int(fh, &m->timer[idx], _sectimer, MAPSIZE(_sectimer));
+ if (WRITE_FULL_MODEL || ((m->timer[idx].type == TIMER_COUNTDOWN || m->timer[idx].type == TIMER_COUNTDOWN_PROP) && m->timer[idx].timer))
+ fprintf(fh, "%s=%d\n", TIMER_TIME, m->timer[idx].timer);
+ if (WRITE_FULL_MODEL || (m->timer[idx].val != 0 && m->timer[idx].type == TIMER_PERMANENT))
+ fprintf(fh, "%s=%d\n", TIMER_VAL, m->timer[idx].val);
+ }
+ for(idx = 0; idx < TELEM_NUM_ALARMS; idx++) {
+ struct TelemetryAlarm *alarm = &m->alarms[idx];
+ if (!WRITE_FULL_MODEL && !alarm->src)
+ continue;
+ fprintf(fh, "[%s%d]\n", SECTION_TELEMALARM, idx+1);
+ fprintf(fh, "%s=%s\n", TELEM_SRC, TELEMETRY_ShortName(file, alarm->src));
+ if (WRITE_FULL_MODEL || alarm->above)
+ fprintf(fh, "%s=%d\n", TELEM_ABOVE, alarm->above);
+ fprintf(fh, "%s=%d\n", TELEM_VALUE, alarm->value);
+ fprintf(fh, "%s=%d\n", TELEM_THRESHOLD, alarm->threshold);
+ }
+#if HAS_DATALOG
+ fprintf(fh, "[%s]\n", SECTION_DATALOG);
+ fprintf(fh, "%s=%s\n", DATALOG_SWITCH, INPUT_SourceNameReal(file, m->datalog.enable));
+ fprintf(fh, "%s=%s\n", DATALOG_RATE, DATALOG_RateString(m->datalog.rate));
+ for(idx = 0; idx < DLOG_LAST; idx++) {
+ if(m->datalog.source[DATALOG_BYTE(idx)] & (1 << DATALOG_POS(idx)))
+ fprintf(fh, "%s=%s\n", DATALOG_SOURCE, DATALOG_Source(file, idx));
+ }
+#endif //HAS_DATALOG
+ fprintf(fh, "[%s]\n", SECTION_SAFETY);
+ for(int i = 0; i < NUM_SOURCES + 1; i++) {
+ if (WRITE_FULL_MODEL || m->safety[i]) {
+ fprintf(fh, "%s=%s\n", i == 0 ? "Auto" : INPUT_SourceNameReal(file, i), SAFETY_VAL[m->safety[i]]);
+ }
+ }
+ fprintf(fh, "[%s]\n", SECTION_GUI);
+ for(idx = 0; idx < NUM_ELEMS; idx++) {
+ if (! ELEM_USED(Model.pagecfg2.elem[idx]))
+ break;
+ int src = Model.pagecfg2.elem[idx].src;
+ int x = ELEM_X(Model.pagecfg2.elem[idx]);
+ int y = ELEM_Y(Model.pagecfg2.elem[idx]);
+ int type = ELEM_TYPE(Model.pagecfg2.elem[idx]);
+ const char *elename = GetElemName(type);
+ switch(type) {
+ case ELEM_SMALLBOX:
+ case ELEM_BIGBOX:
+ fprintf(fh, "%s=%d,%d,%s\n", elename, x, y, GetBoxSourceReal(file, src));
+ break;
+ case ELEM_BAR:
+ src += NUM_INPUTS;
+ fprintf(fh, "%s=%d,%d,%s\n", elename, x, y, INPUT_SourceNameReal(file, src));
+ break;
+ case ELEM_TOGGLE:
+ fprintf(fh, "%s=%d,%d,%d,%d,%d,%s\n", elename, x, y,
+ Model.pagecfg2.elem[idx].extra[0],
+ Model.pagecfg2.elem[idx].extra[1],
+ INPUT_NumSwitchPos(src) == 2 ? 0 : Model.pagecfg2.elem[idx].extra[2],
+ INPUT_SourceNameAbbrevSwitchReal(file, src));
+ break;
+ case ELEM_HTRIM:
+ case ELEM_VTRIM:
+ fprintf(fh, "%s=%d,%d,%d\n", elename, x, y, src);
+ break;
+ default:
+ fprintf(fh, "%s=%d,%d\n", elename, x, y);
+ break;
+ }
+ }
+ for(idx = 0; idx < NUM_QUICKPAGES; idx++) {
+ if (WRITE_FULL_MODEL || m->pagecfg2.quickpage[idx]) {
+ u8 val = m->pagecfg2.quickpage[idx];
+ fprintf(fh, "%s%d=%s\n", GUI_QUICKPAGE, idx+1, PAGE_GetName(val));
+ }
+ }
+#if HAS_EXTENDED_AUDIO
+ fprintf(fh, "[%s]\n", SECTION_VOICE);
+ for (idx = 0; idx < NUM_SWITCHES; idx++) {
+ if (m->voice.switches[idx].music)
+ fprintf(fh, "%s=%d\n", INPUT_SourceName(file,idx + INP_HAS_CALIBRATION + 1), m->voice.switches[idx].music);
+ }
+#if NUM_AUX_KNOBS
+ for (idx = 0; idx < NUM_AUX_KNOBS * 2; idx++) {
+ if (m->voice.aux[idx].music) {
+ if (idx % 2)
+ fprintf(fh, "%s_UP=%d\n", INPUT_SourceName(tempstring,(idx-1) / 2 + NUM_STICKS + 1), m->voice.aux[idx].music);
+ else
+ fprintf(fh, "%s_DOWN=%d\n", INPUT_SourceName(tempstring,idx / 2 + NUM_STICKS + 1), m->voice.aux[idx].music);
+ }
+ }
+#endif
+ for (idx = 0; idx < NUM_TIMERS; idx++) {
+ if (m->voice.timer[idx].music)
+ fprintf(fh, "timer%d=%d\n", idx + 1, m->voice.timer[idx].music);
+ }
+ for (idx = 0; idx < TELEM_NUM_ALARMS; idx++) {
+ if (m->voice.telemetry[idx].music)
+ fprintf(fh, "telemalarm%d=%d\n", idx + 1, m->voice.telemetry[idx].music);
+ }
+ for (idx = 0; idx < (NUM_OUT_CHANNELS + NUM_VIRT_CHANNELS); idx++) {
+ if (m->voice.mixer[idx].music)
+ fprintf(fh, "%s=%d\n", INPUT_SourceNameReal(tempstring, idx + NUM_INPUTS + 1), m->voice.mixer[idx].music);
+ }
+#endif
+ CONFIG_EnableLanguage(1);
+ fclose(fh);
+ return 1;
+}
+
+static void clear_model(u8 full)
+{
+ u8 i;
+ if (full) {
+ memset(&Model, 0, sizeof(Model));
+ } else {
+ memset(Model.mixers, 0, sizeof(Model.mixers));
+ memset(Model.templates, 0, sizeof(Model.templates));
+ memset(Model.trims, 0, sizeof(Model.trims));
+ Model.swash_type = SWASH_TYPE_NONE;
+ Model.swash_invert = 0;
+ }
+ Model.mixer_mode = MIXER_ADVANCED;
+ Model.swashmix[0] = 60;
+ Model.swashmix[1] = 60;
+ Model.swashmix[2] = 60;
+ for(i = 0; i < NUM_MIXERS; i++) {
+ Model.mixers[i].scalar = 100;
+ MIXER_SET_APPLY_TRIM(&Model.mixers[i], 1);
+ }
+ for(i = 0; i < NUM_OUT_CHANNELS; i++) {
+ MIXER_SetDefaultLimit(&Model.limits[i]);
+ }
+ for (i = 0; i < NUM_TRIMS; i++) {
+ Model.trims[i].step = 1;
+ }
+ for (i = 0; i < MAX_PPM_IN_CHANNELS; i++) {
+ Model.ppm_map[i] = -1;
+ }
+ Model.ppmin_centerpw = 1500;
+ Model.ppmin_deltapw = 400;
+}
+
+u8 CONFIG_ReadLayout_old(const char *filename) {
+ memset(&Model.pagecfg2, 0, sizeof(Model.pagecfg2));
+ if (CONFIG_IniParse(filename, layout_ini_handler, &Model)) {
+ printf("Failed to parse Layout file: %s\n", filename);
+ return 0;
+ }
+ return 1;
+}
+
+u8 CONFIG_ReadModel_old(const char* file) {
+ clear_model(1);
+
+ auto_map = 0;
+ if (CONFIG_IniParse(file, ini_handler, &Model)) {
+ printf("Failed to parse Model file: %s\n", file);
+ }
+ if (! ELEM_USED(Model.pagecfg2.elem[0]))
+ CONFIG_ReadLayout_old("layout/default.ini");
+ if(! PROTOCOL_HasPowerAmp(Model.protocol))
+ Model.tx_power = TXPOWER_150mW;
+ MIXER_SetMixers(NULL, 0);
+ if(auto_map)
+ RemapChannelsForProtocol(EATRG0);
+ if(! Model.name[0])
+ sprintf(Model.name, "Model%d", 1);
+ return 1;
+}
diff --git a/src/tests/models/280qav.ini b/src/tests/models/280qav.ini
new file mode 100644
index 0000000000..060ff47855
--- /dev/null
+++ b/src/tests/models/280qav.ini
@@ -0,0 +1,100 @@
+name=280qav
+mixermode=Advanced
+type=multi
+[radio]
+protocol=DSMX
+num_channels=7
+tx_power=150mW
+
+[channel1]
+template=simple
+[mixer]
+src=THR
+dest=Ch1
+curvetype=expo
+points=0,0
+
+[channel2]
+reverse=1
+template=simple
+[mixer]
+src=AIL
+dest=Ch2
+curvetype=expo
+points=0,0
+
+[channel3]
+template=simple
+[mixer]
+src=ELE
+dest=Ch3
+curvetype=expo
+points=0,0
+
+[channel4]
+reverse=1
+template=simple
+[mixer]
+src=RUD
+dest=Ch4
+curvetype=expo
+points=0,0
+
+[channel5]
+template=expo_dr
+[mixer]
+src=AIL
+dest=Ch5
+scalar=125
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=SW A1
+scalar=50
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=SW A2
+scalar=0
+curvetype=fixed
+
+[channel6]
+template=expo_dr
+[mixer]
+src=SW B1
+dest=Ch6
+scalar=125
+curvetype=fixed
+[mixer]
+src=SW B1
+dest=Ch6
+switch=SW B1
+scalar=50
+curvetype=fixed
+[mixer]
+src=SW B1
+dest=Ch6
+switch=SW B0
+scalar=0
+curvetype=fixed
+
+[safety]
+[gui-128x64]
+V-trim=59,10,1
+H-trim=5,59,3
+V-trim=65,10,2
+H-trim=74,59,4
+Small-box=2,22,Ch1
+Small-box=2,31,Timer1
+Small-box=2,40,Timer2
+Model=75,20
+Battery=102,1
+Toggle=4,10,0,3,0,None
+Toggle=13,10,0,5,0,None
+Toggle=22,10,0,4,0,None
+Toggle=31,10,0,0,0,None
+Toggle=40,10,0,0,0,None
+TxPower=102,7
+quickpage1=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/models/4g6s.ini b/src/tests/models/4g6s.ini
new file mode 100644
index 0000000000..e8207b802c
--- /dev/null
+++ b/src/tests/models/4g6s.ini
@@ -0,0 +1,169 @@
+name=4G6
+mixermode=Advanced
+icon=4G6S.BMP
+[radio]
+protocol=WK2601
+num_channels=7
+tx_power=30mW
+
+[protocol_opts]
+Chan mode=6+1
+COL Inv=Normal
+COL Limit=0
+
+[channel1]
+template=cyclic1
+
+[channel2]
+reverse=1
+template=cyclic2
+
+[channel3]
+template=complex
+[mixer]
+src=THR
+dest=Ch3
+usetrim=0
+curvetype=3point
+points=-100,48,100
+[mixer]
+src=THR
+dest=Ch3
+switch=FMODE1
+usetrim=0
+curvetype=3point
+points=100,50,100
+[mixer]
+src=THR
+dest=Ch3
+switch=FMODE2
+usetrim=0
+curvetype=3point
+points=100,75,100
+[mixer]
+src=THR
+dest=Ch3
+switch=GEAR1
+usetrim=0
+curvetype=3point
+points=-100,-100,-100
+
+[channel4]
+reverse=1
+template=simple
+[mixer]
+src=RUD
+dest=Ch4
+usetrim=0
+curvetype=expo
+points=0,0
+
+[channel6]
+reverse=1
+template=cyclic3
+
+[channel7]
+template=expo_dr
+[mixer]
+src=Ch7
+dest=Ch7
+scalar=82
+usetrim=0
+curvetype=fixed
+[mixer]
+src=Ch7
+dest=Ch7
+switch=FMODE1
+scalar=82
+usetrim=0
+curvetype=fixed
+[mixer]
+src=Ch7
+dest=Ch7
+switch=FMODE2
+scalar=82
+usetrim=0
+curvetype=fixed
+
+[virtchan1]
+template=expo_dr
+[mixer]
+src=AIL
+dest=Virt1
+usetrim=0
+curvetype=expo
+points=40,40
+
+[virtchan2]
+template=expo_dr
+[mixer]
+src=ELE
+dest=Virt2
+usetrim=0
+curvetype=expo
+points=40,40
+
+[virtchan3]
+template=expo_dr
+[mixer]
+src=THR
+dest=Virt3
+usetrim=0
+curvetype=3point
+points=-25,0,100
+[mixer]
+src=THR
+dest=Virt3
+switch=FMODE1
+usetrim=0
+[mixer]
+src=THR
+dest=Virt3
+switch=FMODE2
+usetrim=0
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+value=-100
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[swash]
+type=120
+ail_inv=1
+[timer2]
+type=countdown
+time=10
+[safety]
+Auto=min
+Ch3=min
+[gui-qvga]
+trim=4in
+barsize=half
+box1=Ch3
+box2=Timer1
+box3=Timer2
+bar1=Ch1
+bar2=Ch2
+bar3=Ch3
+bar4=Ch4
+toggle1=ELE DR
+tglico1=0,1,0
+toggle2=AIL DR
+tglico2=0,0,0
+toggle3=RUD DR
+tglico3=0,2,0
+toggle4=GEAR
+tglico4=0,4,0
+quickpage1=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/models/apm.ini b/src/tests/models/apm.ini
new file mode 100644
index 0000000000..773e57fadc
--- /dev/null
+++ b/src/tests/models/apm.ini
@@ -0,0 +1,185 @@
+name=APM
+mixermode=Advanced
+icon=V212.BMP
+type=plane
+[radio]
+protocol=DSM2
+num_channels=8
+fixed_id=636699
+tx_power=100mW
+
+[protocol_opts]
+Telemetry=Off
+
+[channel1]
+max=100
+min=-100
+template=complex
+[mixer]
+src=THR
+dest=Ch1
+switch=RUD DR1
+[mixer]
+src=AUX4
+dest=Ch1
+switch=RUD DR0
+scalar=-100
+usetrim=0
+curvetype=fixed
+
+[channel2]
+reverse=1
+template=simple
+[mixer]
+src=AIL
+dest=Ch2
+
+[channel3]
+reverse=1
+template=simple
+[mixer]
+src=ELE
+dest=Ch3
+
+[channel4]
+reverse=1
+template=complex
+[mixer]
+src=RUD
+dest=Ch4
+switch=RUD DR1
+
+[channel5]
+max=100
+min=-100
+template=complex
+[mixer]
+src=AUX5
+dest=Ch5
+switch=FMODE0
+scalar=-125
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=FMODE1
+scalar=-40
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=FMODE2
+scalar=-20
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=GEAR1
+scalar=10
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=MIX1
+scalar=40
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=MIX2
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=RUD DR0
+scalar=-100
+usetrim=0
+curvetype=fixed
+
+[channel6]
+max=100
+min=-100
+template=complex
+[mixer]
+src=AUX5
+dest=Ch6
+usetrim=0
+curvetype=expo
+points=0,0
+
+[channel7]
+max=100
+min=-100
+template=complex
+[mixer]
+src=ELE DR0
+dest=Ch7
+switch=ELE DR0
+scalar=-100
+usetrim=0
+[mixer]
+src=AIL
+dest=Ch7
+switch=ELE DR1
+usetrim=0
+curvetype=fixed
+
+[channel8]
+max=100
+min=-100
+template=complex
+[mixer]
+src=AUX4
+dest=Ch8
+usetrim=0
+curvetype=expo
+points=0,0
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[timer1]
+type=countdown
+src=Ch1
+resetsrc=AIL DR1
+time=420
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-128x64]
+V-trim=59,10,1
+H-trim=5,59,3
+V-trim=65,10,2
+H-trim=74,59,4
+Small-box=2,22,Ch1
+Small-box=2,31,Timer1
+Model=75,20
+Battery=102,1
+Toggle=42,10,72,0,0,RUD DR
+Toggle=12,10,0,68,0,ELE DR
+Toggle=2,10,0,193,194,FMODE
+Toggle=32,10,0,196,197,MIX
+Toggle=22,10,0,71,0,GEAR
+TxPower=102,7
+quickpage1=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/models/ardrone2.ini b/src/tests/models/ardrone2.ini
new file mode 100644
index 0000000000..e65157d97d
--- /dev/null
+++ b/src/tests/models/ardrone2.ini
@@ -0,0 +1,108 @@
+name=ArDrone2
+mixermode=Advanced
+icon=DRONE2-2.BMP
+type=plane
+[radio]
+protocol=DSM2
+num_channels=7
+fixed_id=123456
+tx_power=150mW
+
+[protocol_opts]
+Telemetry=Off
+
+[channel1]
+template=simple
+[mixer]
+src=THR
+dest=Ch1
+
+[channel2]
+reverse=1
+template=simple
+[mixer]
+src=AIL
+dest=Ch2
+scalar=45
+curvetype=expo
+points=0,0
+
+[channel3]
+reverse=1
+template=simple
+[mixer]
+src=ELE
+dest=Ch3
+scalar=45
+curvetype=expo
+points=0,0
+
+[channel4]
+reverse=1
+template=simple
+[mixer]
+src=RUD
+dest=Ch4
+
+[channel5]
+reverse=1
+template=simple
+[mixer]
+src=GEAR0
+dest=Ch5
+curvetype=min/max
+points=0
+
+[virtchan1]
+name=Virt1
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[trim5]
+src=GEAR0
+pos=None
+neg=None
+switch=GEAR
+[trim6]
+src=MIX0
+pos=None
+neg=None
+[timer1]
+type=countdown
+src=GEAR1
+time=600
+[timer2]
+type=stop-prop
+src=THR
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-320x240]
+V-trim=131,75,1
+H-trim=6,220,3
+V-trim=181,75,2
+H-trim=191,220,4
+Big-box=9,90,Timer1
+Small-box=9,150,Timer2
+Bargraph=205,150,Ch1
+Bargraph=235,150,Ch2
+Bargraph=265,150,Ch3
+Bargraph=295,150,Ch4
+Model=206,40
+Toggle=144,44,68,5,0,GEAR
+Big-box=9,37,Ch1
\ No newline at end of file
diff --git a/src/tests/models/bixler2.ini b/src/tests/models/bixler2.ini
new file mode 100644
index 0000000000..55d514e79f
--- /dev/null
+++ b/src/tests/models/bixler2.ini
@@ -0,0 +1,241 @@
+name=Bixler 2
+mixermode=Advanced
+icon=BIXLER2.BMP
+type=plane
+[radio]
+protocol=DEVO
+num_channels=10
+fixed_id=870249
+tx_power=150mW
+
+[protocol_opts]
+Telemetry=Off
+
+[channel1]
+subtrim=-165
+template=complex
+[mixer]
+src=ELE
+dest=Ch1
+switch=FMODE0
+scalar=80
+curvetype=expo
+points=20,20
+[mixer]
+src=ELE
+dest=Ch1
+switch=FMODE1
+curvetype=expo
+points=20,20
+[mixer]
+src=AIL
+dest=Ch1
+switch=MIX1
+scalar=20
+muxtype=add
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch1
+switch=MIX2
+scalar=20
+muxtype=add
+curvetype=fixed
+[mixer]
+src=ELE
+dest=Ch1
+switch=FMODE2
+curvetype=expo
+points=20,20
+[mixer]
+src=ELE
+dest=Ch1
+switch=FMODE2
+curvetype=expo
+points=20,20
+
+[channel2]
+template=complex
+[mixer]
+src=AIL
+dest=Ch2
+switch=FMODE0
+scalar=80
+curvetype=expo
+points=20,20
+[mixer]
+src=AIL
+dest=Ch2
+switch=FMODE1
+curvetype=expo
+points=20,20
+[mixer]
+src=AIL
+dest=Ch2
+switch=FMODE2
+curvetype=expo
+points=20,20
+
+[channel3]
+safetysw=RUD DR0
+failsafe=-125
+safetyval=-150
+template=simple
+[mixer]
+src=THR
+dest=Ch3
+
+[channel4]
+template=complex
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE0
+scalar=80
+curvetype=expo
+points=20,20
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE1
+curvetype=expo
+points=20,20
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE2
+curvetype=expo
+points=20,20
+[mixer]
+src=AIL
+dest=Ch4
+switch=GEAR1
+curvetype=expo
+points=20,20
+
+[channel5]
+template=complex
+[mixer]
+src=AIL
+dest=Ch5
+switch=FMODE0
+scalar=80
+curvetype=expo
+points=20,20
+[mixer]
+src=AIL
+dest=Ch5
+switch=FMODE1
+usetrim=0
+curvetype=expo
+points=20,20
+[mixer]
+src=AIL
+dest=Ch5
+switch=FMODE2
+usetrim=0
+curvetype=expo
+points=20,20
+
+[channel6]
+template=complex
+[mixer]
+src=ELE
+dest=Ch6
+switch=MIX0
+scalar=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch6
+switch=MIX1
+scalar=40
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch6
+switch=MIX2
+scalar=60
+usetrim=0
+curvetype=fixed
+[mixer]
+src=!AIL
+dest=Ch6
+switch=FMODE2
+usetrim=0
+
+[channel7]
+template=complex
+[mixer]
+src=MIX0
+dest=Ch7
+switch=MIX0
+scalar=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch7
+switch=MIX1
+scalar=-40
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch7
+switch=MIX2
+scalar=-60
+usetrim=0
+curvetype=fixed
+[mixer]
+src=!AIL
+dest=Ch7
+switch=FMODE2
+usetrim=0
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+step=10
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+step=10
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+step=10
+[timer1]
+type=countdown
+src=Ch3
+resetsrc=AIL DR1
+time=630
+[timer2]
+type=permanent
+src=Ch3
+val=2709794
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-128x64]
+V-trim=59,10,1
+H-trim=5,59,3
+V-trim=65,10,2
+H-trim=74,59,4
+Small-box=2,22,Ch3
+Small-box=2,31,Timer1
+Small-box=2,39,Timer2
+Model=75,20
+Toggle=2,11,0,193,194,FMODE
+Toggle=23,11,0,196,197,MIX
+Toggle=42,11,72,0,0,RUD DR
+quickpage1=Mixer
\ No newline at end of file
diff --git a/src/tests/models/blade130x.ini b/src/tests/models/blade130x.ini
new file mode 100644
index 0000000000..d4968f48a5
--- /dev/null
+++ b/src/tests/models/blade130x.ini
@@ -0,0 +1,198 @@
+name=Blade130X
+mixermode=Advanced
+icon=HELI.BMP
+[radio]
+protocol=DSMX
+num_channels=7
+tx_power=100mW
+
+[protocol_opts]
+Telemetry=Off
+
+[channel1]
+safetysw=RUD DR1
+safetyval=-100
+template=complex
+[mixer]
+src=THR
+dest=Ch1
+curvetype=7point
+points=-100,-20,22,41,48,50,50
+[mixer]
+src=THR
+dest=Ch1
+switch=FMODE1
+curvetype=7point
+points=100,87,73,60,73,87,100
+[mixer]
+src=THR
+dest=Ch1
+switch=FMODE2
+curvetype=fixed
+
+[channel2]
+template=expo_dr
+[mixer]
+src=AIL
+dest=Ch2
+curvetype=expo
+points=30,30
+[mixer]
+src=AIL
+dest=Ch2
+switch=AIL DR1
+scalar=125
+curvetype=expo
+points=0,0
+
+[channel3]
+template=expo_dr
+[mixer]
+src=ELE
+dest=Ch3
+curvetype=expo
+points=30,30
+[mixer]
+src=ELE
+dest=Ch3
+switch=ELE DR1
+scalar=125
+curvetype=expo
+points=0,0
+
+[channel4]
+template=simple
+[mixer]
+src=RUD
+dest=Ch4
+curvetype=expo
+points=-6,-6
+
+[channel6]
+template=complex
+[mixer]
+src=THR
+dest=Ch6
+usetrim=0
+curvetype=7point
+points=-40,-26,-13,0,33,66,100
+[mixer]
+src=THR
+dest=Ch6
+switch=FMODE1
+usetrim=0
+curvetype=7point
+points=-100,-66,-33,0,33,66,100
+[mixer]
+src=THR
+dest=Ch6
+switch=FMODE2
+usetrim=0
+curvetype=7point
+points=-100,-66,-33,0,33,66,100
+[mixer]
+src=AIL
+dest=Ch6
+switch=RUD DR1
+scalar=0
+usetrim=0
+curvetype=fixed
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[timer1]
+type=countdown
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-480x272]
+V-trim=213,91,1
+H-trim=86,236,3
+V-trim=263,91,2
+H-trim=271,236,4
+Big-box=89,56,Ch1
+Big-box=89,106,Timer1
+Small-box=89,166,Timer2
+Small-box=89,197,Clock
+Bargraph=285,166,Ch1
+Bargraph=298,166,Ch2
+Bargraph=312,166,Ch3
+Bargraph=326,166,Ch4
+Bargraph=340,166,Ch5
+Bargraph=354,166,Ch6
+Bargraph=368,166,Ch7
+Bargraph=382,166,Ch8
+Toggle=210,54,1,64,128,FMODE
+Toggle=248,54,2,65,129,MIX
+Toggle=227,92,8,71,0,GEAR
+Toggle=227,131,5,132,68,ELE DR
+Toggle=227,169,4,131,67,AIL DR
+Toggle=227,208,3,130,66,RUD DR
+Model=290,56
+
+[gui-320x240]
+V-trim=129,75,1
+H-trim=4,220,3
+V-trim=181,75,2
+H-trim=191,220,4
+Big-box=8,40,Ch1
+Small-box=8,95,Timer1
+Small-box=8,133,Timer2
+Small-box=8,172,Timer3
+Bargraph=200,150,Ch1
+Bargraph=214,150,Ch2
+Bargraph=228,150,Ch3
+Bargraph=242,150,Ch4
+Bargraph=256,150,Ch5
+Bargraph=270,150,Ch6
+Bargraph=284,150,Ch7
+Bargraph=298,150,Ch8
+Toggle=144,36,1,64,128,FMODE
+Toggle=144,69,2,65,129,MIX
+Toggle=144,102,5,68,0,ELE DR
+Toggle=144,135,4,67,0,AIL DR
+Toggle=144,168,3,66,0,RUD DR
+Toggle=144,201,8,71,0,GEAR
+Model=207,40
+
+[gui-128x64]
+V-trim=55,10,1
+H-trim=1,59,3
+V-trim=69,10,2
+H-trim=78,59,4
+Big-box=2,12,Ch1
+Small-box=2,28,Timer1
+Small-box=2,38,Timer2
+Small-box=2,48,Timer3
+Bargraph=79,30,Ch1
+Bargraph=85,30,Ch2
+Bargraph=91,30,Ch3
+Bargraph=97,30,Ch4
+Bargraph=103,30,Ch5
+Bargraph=109,30,Ch6
+Bargraph=115,30,Ch7
+Bargraph=121,30,Ch8
+Toggle=75,13,1,64,128,FMODE
+Toggle=84,13,2,65,129,MIX
+Toggle=93,13,0,5,0,ELE DR
+Toggle=102,13,0,4,0,AIL DR
+Toggle=111,13,0,8,0,GEAR
+Toggle=120,13,0,3,0,RUD DR
+Battery=102,1
+TxPower=75,1
\ No newline at end of file
diff --git a/src/tests/models/deltaray.ini b/src/tests/models/deltaray.ini
new file mode 100644
index 0000000000..67fe54b38b
--- /dev/null
+++ b/src/tests/models/deltaray.ini
@@ -0,0 +1,116 @@
+name=DeltaRay 1
+mixermode=Advanced
+icon=DELTARAY.BMP
+type=plane
+[radio]
+protocol=DSM2
+num_channels=7
+tx_power=150mW
+
+[protocol_opts]
+Telemetry=Off
+
+[channel1]
+template=simple
+[mixer]
+src=THR
+dest=Ch1
+
+[channel2]
+template=simple
+[mixer]
+src=AIL
+dest=Ch2
+curvetype=expo
+points=0,0
+
+[channel3]
+template=simple
+[mixer]
+src=ELE
+dest=Ch3
+curvetype=expo
+points=0,0
+
+[channel4]
+template=simple
+[mixer]
+src=RUD
+dest=Ch4
+curvetype=expo
+points=40,40
+
+[channel5]
+max=100
+min=-100
+template=expo_dr
+[mixer]
+src=FMODE0
+dest=Ch5
+curvetype=fixed
+[mixer]
+src=FMODE0
+dest=Ch5
+switch=FMODE1
+scalar=0
+curvetype=fixed
+[mixer]
+src=FMODE0
+dest=Ch5
+switch=FMODE2
+scalar=-100
+curvetype=fixed
+
+[channel6]
+reverse=1
+template=expo_dr
+[mixer]
+src=RUD DR0
+dest=Ch6
+curvetype=fixed
+[mixer]
+src=RUD DR0
+dest=Ch6
+switch=RUD DR1
+scalar=-100
+curvetype=fixed
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+value=-18
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[timer1]
+src=THR
+[timer2]
+type=countdown
+src=THR
+time=585
+[safety]
+Ch1=min
+[gui-qvga]
+trim=4in
+barsize=half
+box1=Timer1
+box2=Timer2
+box3=Timer2
+bar1=Ch1
+bar2=Ch2
+bar3=Ch3
+bar4=Ch4
+toggle1=FMODE
+tglico1=3,1,2
+toggle2=RUD DR
+tglico2=3,2,0
\ No newline at end of file
diff --git a/src/tests/models/fx071.ini b/src/tests/models/fx071.ini
new file mode 100644
index 0000000000..d3d68a35f6
--- /dev/null
+++ b/src/tests/models/fx071.ini
@@ -0,0 +1,219 @@
+name=FX071-Kiwi.Craig
+mixermode=Advanced
+icon=FX071.BMP
+[radio]
+protocol=KNFX
+num_channels=10
+fixed_id=123456
+tx_power=100mW
+
+[protocol_opts]
+Re-bind=No
+1Mbps=No
+
+[channel1]
+safetysw=RUD DR1
+safetyval=-100
+scalar-=100
+failsafe=-100
+template=simple
+[mixer]
+src=THR
+dest=Ch1
+curvetype=5point
+points=-100,-50,0,50,100
+smooth=1
+
+[channel2]
+scalar-=100
+template=complex
+[mixer]
+src=AIL
+dest=Ch2
+scalar=30
+curvetype=expo
+points=10,10
+[mixer]
+src=AIL
+dest=Ch2
+switch=MIX0
+scalar=5
+muxtype=add
+curvetype=expo
+points=0,0
+[mixer]
+src=AIL
+dest=Ch2
+switch=FMODE1
+scalar=55
+curvetype=expo
+points=15,15
+[mixer]
+src=AIL
+dest=Ch2
+switch=MIX0
+scalar=5
+muxtype=add
+curvetype=expo
+points=0,0
+[mixer]
+src=AIL
+dest=Ch2
+switch=FMODE2
+scalar=70
+curvetype=expo
+points=30,30
+[mixer]
+src=AIL
+dest=Ch2
+switch=MIX0
+scalar=10
+muxtype=add
+curvetype=expo
+points=0,0
+
+[channel3]
+scalar-=100
+template=complex
+[mixer]
+src=ELE
+dest=Ch3
+scalar=30
+curvetype=expo
+points=10,10
+[mixer]
+src=ELE
+dest=Ch3
+switch=MIX0
+scalar=5
+muxtype=add
+curvetype=expo
+points=0,0
+[mixer]
+src=ELE
+dest=Ch3
+switch=FMODE1
+scalar=55
+curvetype=expo
+points=15,15
+[mixer]
+src=ELE
+dest=Ch3
+switch=MIX0
+scalar=5
+muxtype=add
+curvetype=expo
+points=0,0
+[mixer]
+src=ELE
+dest=Ch3
+switch=FMODE2
+scalar=70
+curvetype=expo
+points=30,30
+[mixer]
+src=ELE
+dest=Ch3
+switch=MIX0
+scalar=10
+muxtype=add
+curvetype=expo
+points=0,0
+
+[channel4]
+safetysw=RUD DR1
+safetyval=-100
+failsafe=-100
+template=complex
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE0
+scalar=80
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE1
+curvetype=expo
+points=10,10
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE2
+scalar=120
+curvetype=expo
+points=20,20
+
+[channel5]
+template=simple
+[mixer]
+src=GEAR0
+dest=Ch5
+curvetype=expo
+points=0,0
+
+[channel6]
+template=expo_dr
+[mixer]
+src=RUD DR1
+dest=Ch6
+curvetype=min/max
+points=0
+
+[channel8]
+template=simple
+[mixer]
+src=MIX1
+dest=Ch8
+curvetype=expo
+points=0,0
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+step=2
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+step=2
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+step=2
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+step=2
+[timer1]
+type=countdown
+src=THR
+resetsrc=AIL DR1
+time=390
+[timer2]
+type=countdown
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-128x64]
+V-trim=59,10,1
+H-trim=5,59,3
+V-trim=65,10,2
+H-trim=74,59,4
+Small-box=2,22,Ch1
+Small-box=2,31,Timer1
+Small-box=2,40,None
+Model=75,20
+Battery=102,1
+Toggle=4,10,9,72,0,RUD DR
+Toggle=13,10,8,71,0,MIX
+Toggle=22,10,2,65,129,FMODE
+Toggle=31,10,6,69,0,GEAR
+Toggle=40,10,0,0,0,None
+TxPower=102,7
+quickpage1=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/models/geniuscp.ini b/src/tests/models/geniuscp.ini
new file mode 100644
index 0000000000..72e6d85caa
--- /dev/null
+++ b/src/tests/models/geniuscp.ini
@@ -0,0 +1,149 @@
+name=Genius CP V2
+mixermode=Standard
+[radio]
+protocol=DEVO
+num_channels=7
+tx_power=150mW
+
+[protocol_opts]
+Telemetry=On
+
+[channel1]
+template=cyclic2
+
+[channel2]
+template=cyclic1
+
+[channel3]
+safetysw=!HOLD0
+failsafe=-125
+safetyval=-110
+template=complex
+[mixer]
+src=THR
+dest=Ch3
+curvetype=9point
+points=-100,-50,-4,23,40,58,74,87,100
+[mixer]
+src=THR
+dest=Ch3
+switch=FMODE1
+curvetype=9point
+points=100,84,69,58,50,58,69,84,100
+
+[channel4]
+template=expo_dr
+[mixer]
+src=RUD
+dest=Ch4
+curvetype=expo
+points=0,0
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE1
+curvetype=expo
+points=0,0
+
+[channel6]
+template=cyclic3
+
+[channel7]
+template=expo_dr
+[mixer]
+src=FMODE0
+dest=Ch7
+scalar=50
+curvetype=fixed
+[mixer]
+src=FMODE0
+dest=Ch7
+switch=FMODE1
+scalar=0
+curvetype=fixed
+
+[virtchan1]
+template=expo_dr
+[mixer]
+src=AIL
+dest=Virt1
+scalar=90
+curvetype=expo
+points=0,0
+[mixer]
+src=AIL
+dest=Virt1
+switch=FMODE1
+curvetype=expo
+points=0,0
+
+[virtchan2]
+template=expo_dr
+[mixer]
+src=ELE
+dest=Virt2
+scalar=90
+curvetype=expo
+points=0,0
+[mixer]
+src=ELE
+dest=Virt2
+switch=FMODE1
+curvetype=expo
+points=0,0
+
+[virtchan3]
+template=complex
+[mixer]
+src=THR
+dest=Virt3
+curvetype=9point
+points=-20,-10,0,10,20,29,38,47,56
+[mixer]
+src=THR
+dest=Virt3
+switch=FMODE1
+curvetype=9point
+points=-60,-45,-30,-15,0,15,30,45,60
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[timer1]
+type=countdown
+src=Ch3
+time=180
+[timer2]
+src=Ch3
+[safety]
+Auto=min
+[gui-128x64]
+V-trim=59,10,1
+H-trim=5,59,3
+V-trim=65,10,2
+H-trim=74,59,4
+Small-box=2,22,Ch3
+Small-box=2,31,Timer1
+Small-box=2,39,Timer2
+Model=75,20
+Battery=102,1
+Toggle=4,10,0,3,0,None
+Toggle=13,10,0,5,0,None
+Toggle=22,10,0,4,0,None
+Toggle=31,10,0,0,0,None
+Toggle=40,10,0,0,0,None
+TxPower=102,7
+quickpage1=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/models/nazath.ini b/src/tests/models/nazath.ini
new file mode 100644
index 0000000000..4424cd4098
--- /dev/null
+++ b/src/tests/models/nazath.ini
@@ -0,0 +1,135 @@
+name=Naza TH
+mixermode=Advanced
+icon=HUBSANX4.BMP
+[radio]
+protocol=DEVO
+num_channels=10
+fixed_id=611642
+tx_power=150mW
+
+[protocol_opts]
+Telemetry=Off
+
+[channel1]
+template=simple
+[mixer]
+src=ELE
+dest=Ch1
+curvetype=expo
+points=0,0
+
+[channel2]
+template=simple
+[mixer]
+src=AIL
+dest=Ch2
+curvetype=expo
+points=0,0
+
+[channel3]
+template=simple
+[mixer]
+src=THR
+dest=Ch3
+curvetype=expo
+points=0,0
+
+[channel4]
+template=simple
+[mixer]
+src=RUD
+dest=Ch4
+curvetype=expo
+points=0,0
+
+[channel5]
+template=expo_dr
+[mixer]
+src=MIX0
+dest=Ch5
+scalar=95
+curvetype=absval
+points=0
+[mixer]
+src=MIX0
+dest=Ch5
+switch=MIX1
+scalar=5
+curvetype=absval
+points=0
+[mixer]
+src=MIX0
+dest=Ch5
+switch=MIX2
+scalar=-80
+curvetype=absval
+points=0
+
+[channel6]
+template=simple
+[mixer]
+src=PPM5
+dest=Ch6
+curvetype=expo
+points=0,0
+
+[channel7]
+template=simple
+[mixer]
+src=PPM7
+dest=Ch7
+curvetype=expo
+points=0,0
+
+[channel8]
+template=simple
+[mixer]
+src=PPM8
+dest=Ch8
+curvetype=expo
+points=0,0
+
+[ppm-in]
+mode=extend
+num_channels=8
+centerpw=1500
+deltapw=400
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-128x64]
+V-trim=59,10,1
+H-trim=5,59,3
+V-trim=65,10,2
+H-trim=74,59,4
+Small-box=2,22,Ch3
+Small-box=2,31,Timer1
+Small-box=2,39,Timer2
+Model=75,20
+Battery=102,1
+Toggle=4,10,0,3,0,RUD DR
+Toggle=13,10,0,5,0,ELE DR
+Toggle=22,10,0,4,0,AIL DR
+Toggle=31,10,0,0,0,None
+Toggle=40,10,0,0,0,None
+TxPower=102,7
+quickpage1=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/models/trex150dfc.ini b/src/tests/models/trex150dfc.ini
new file mode 100644
index 0000000000..37842b8cb3
--- /dev/null
+++ b/src/tests/models/trex150dfc.ini
@@ -0,0 +1,242 @@
+name=TREX150DFC
+mixermode=Advanced
+icon=HELI.BMP
+[radio]
+protocol=DSMX
+num_channels=7
+tx_power=100mW
+
+[protocol_opts]
+Telemetry=Off
+
+[channel1]
+safetysw=RUD DR1
+safetyval=-100
+template=complex
+[mixer]
+src=THR
+dest=Ch1
+curvetype=7point
+points=-100,-40,0,8,13,17,20
+[mixer]
+src=THR
+dest=Ch1
+switch=FMODE1
+curvetype=7point
+points=40,36,33,28,33,36,40
+[mixer]
+src=THR
+dest=Ch1
+switch=FMODE2
+curvetype=7point
+points=60,55,50,46,50,55,60
+
+[channel2]
+reverse=1
+template=expo_dr
+[mixer]
+src=AIL
+dest=Ch2
+scalar=50
+curvetype=expo
+points=30,30
+[mixer]
+src=AIL
+dest=Ch2
+switch=FMODE1
+scalar=70
+curvetype=expo
+points=30,30
+[mixer]
+src=AIL
+dest=Ch2
+switch=FMODE2
+scalar=125
+
+[channel3]
+reverse=1
+template=expo_dr
+[mixer]
+src=ELE
+dest=Ch3
+scalar=50
+curvetype=expo
+points=30,30
+[mixer]
+src=ELE
+dest=Ch3
+switch=FMODE1
+scalar=70
+curvetype=expo
+points=30,30
+[mixer]
+src=ELE
+dest=Ch3
+switch=FMODE2
+scalar=125
+
+[channel4]
+reverse=1
+template=expo_dr
+[mixer]
+src=RUD
+dest=Ch4
+curvetype=expo
+points=15,15
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE1
+curvetype=expo
+points=15,15
+[mixer]
+src=RUD
+dest=Ch4
+switch=FMODE2
+
+[channel5]
+template=complex
+[mixer]
+src=AIL
+dest=Ch5
+switch=MIX0
+scalar=30
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=MIX1
+scalar=40
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch5
+switch=MIX2
+scalar=50
+curvetype=fixed
+
+[channel6]
+reverse=1
+template=complex
+[mixer]
+src=THR
+dest=Ch6
+scalar=60
+usetrim=0
+curvetype=7point
+points=0,0,0,0,33,66,100
+[mixer]
+src=THR
+dest=Ch6
+switch=FMODE1
+scalar=70
+usetrim=0
+curvetype=7point
+points=-30,-20,-10,0,33,66,100
+[mixer]
+src=THR
+dest=Ch6
+switch=FMODE2
+scalar=75
+usetrim=0
+curvetype=7point
+points=-100,-66,-33,0,33,66,100
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[timer1]
+type=countdown
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-480x272]
+V-trim=213,91,1
+H-trim=86,236,3
+V-trim=263,91,2
+H-trim=271,236,4
+Big-box=89,56,Ch1
+Big-box=89,106,Timer1
+Small-box=89,166,Timer2
+Small-box=89,197,Clock
+Bargraph=285,166,Ch1
+Bargraph=298,166,Ch2
+Bargraph=312,166,Ch3
+Bargraph=326,166,Ch4
+Bargraph=340,166,Ch5
+Bargraph=354,166,Ch6
+Bargraph=368,166,Ch7
+Bargraph=382,166,Ch8
+Toggle=210,54,1,64,128,FMODE
+Toggle=248,54,2,65,129,MIX
+Toggle=227,92,8,71,0,GEAR
+Toggle=227,131,5,132,68,ELE DR
+Toggle=227,169,4,131,67,AIL DR
+Toggle=227,208,3,130,66,RUD DR
+Model=290,56
+
+[gui-320x240]
+V-trim=129,75,1
+H-trim=4,220,3
+V-trim=181,75,2
+H-trim=191,220,4
+Big-box=8,40,Ch1
+Small-box=8,95,Timer1
+Small-box=8,133,Timer2
+Small-box=8,172,Timer3
+Bargraph=200,150,Ch1
+Bargraph=214,150,Ch2
+Bargraph=228,150,Ch3
+Bargraph=242,150,Ch4
+Bargraph=256,150,Ch5
+Bargraph=270,150,Ch6
+Bargraph=284,150,Ch7
+Bargraph=298,150,Ch8
+Toggle=144,36,1,64,128,FMODE
+Toggle=144,69,2,65,129,MIX
+Toggle=144,102,5,68,0,ELE DR
+Toggle=144,135,4,67,0,AIL DR
+Toggle=144,168,3,66,0,RUD DR
+Toggle=144,201,8,71,0,GEAR
+Model=207,40
+
+[gui-128x64]
+V-trim=55,10,1
+H-trim=1,59,3
+V-trim=69,10,2
+H-trim=78,59,4
+Big-box=2,12,Ch1
+Small-box=2,28,Timer1
+Small-box=2,38,Timer2
+Small-box=2,48,Timer3
+Bargraph=79,30,Ch1
+Bargraph=85,30,Ch2
+Bargraph=91,30,Ch3
+Bargraph=97,30,Ch4
+Bargraph=103,30,Ch5
+Bargraph=109,30,Ch6
+Bargraph=115,30,Ch7
+Bargraph=121,30,Ch8
+Toggle=75,13,1,64,128,FMODE
+Toggle=84,13,2,65,129,MIX
+Toggle=93,13,0,5,0,ELE DR
+Toggle=102,13,0,4,0,AIL DR
+Toggle=111,13,0,8,0,GEAR
+Toggle=120,13,0,3,0,RUD DR
+Battery=102,1
+TxPower=75,1
\ No newline at end of file
diff --git a/src/tests/models/wltoys931.ini b/src/tests/models/wltoys931.ini
new file mode 100644
index 0000000000..26520b706c
--- /dev/null
+++ b/src/tests/models/wltoys931.ini
@@ -0,0 +1,206 @@
+name=V931
+mixermode=Advanced
+icon=V931.BMP
+type=plane
+[radio]
+protocol=KN
+num_channels=11
+tx_power=100mW
+
+[protocol_opts]
+Re-bind=No
+1Mbps=Yes
+Format=WLToys
+
+[channel1]
+template=complex
+[mixer]
+src=THR
+dest=Ch1
+switch=FMODE1
+scalar=95
+[mixer]
+src=THR
+dest=Ch1
+switch=!FMODE1
+usetrim=0
+
+[channel2]
+template=expo_dr
+[mixer]
+src=AIL
+dest=Ch2
+scalar=60
+curvetype=expo
+points=50,50
+[mixer]
+src=AIL
+dest=Ch2
+switch=MIX1
+scalar=80
+curvetype=expo
+points=25,25
+[mixer]
+src=AIL
+dest=Ch2
+switch=MIX2
+curvetype=expo
+points=0,0
+
+[channel3]
+template=expo_dr
+[mixer]
+src=ELE
+dest=Ch3
+scalar=60
+curvetype=expo
+points=50,50
+[mixer]
+src=ELE
+dest=Ch3
+switch=MIX1
+scalar=80
+curvetype=expo
+points=25,25
+[mixer]
+src=ELE
+dest=Ch3
+switch=MIX2
+curvetype=expo
+points=0,0
+
+[channel4]
+template=simple
+[mixer]
+src=RUD
+dest=Ch4
+
+[channel5]
+template=simple
+[mixer]
+src=!AIL DR0
+dest=Ch5
+curvetype=min/max
+points=0
+
+[channel6]
+template=simple
+[mixer]
+src=RUD DR1
+dest=Ch6
+curvetype=min/max
+points=0
+
+[channel7]
+template=complex
+[mixer]
+src=FMODE0
+dest=Ch7
+switch=FMODE0
+scalar=-100
+usetrim=0
+curvetype=fixed
+[mixer]
+src=AIL
+dest=Ch7
+switch=!FMODE0
+usetrim=0
+curvetype=fixed
+
+[channel8]
+template=simple
+[mixer]
+src=!GEAR0
+dest=Ch8
+curvetype=min/max
+points=0
+
+[channel9]
+template=simple
+
+[channel10]
+template=simple
+
+[channel11]
+template=simple
+
+[virtchan1]
+template=simple
+[mixer]
+src=Ch1
+dest=Virt1
+scalar=50
+offset=50
+
+[virtchan2]
+template=complex
+[mixer]
+src=THR
+dest=Virt2
+curvetype=expo
+points=0,0
+[mixer]
+src=THR
+dest=Virt2
+switch=RUD DR1
+scalar=-100
+curvetype=fixed
+
+[trim1]
+src=Ch9
+pos=TRIMLV+
+neg=TRIMLV-
+step=10
+switch=GEAR
+value=52,-18,0
+[trim2]
+src=Ch11
+pos=TRIMRV+
+neg=TRIMRV-
+step=10
+switch=GEAR
+value=26,0,0
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+switch=GEAR
+[trim4]
+src=Ch10
+pos=TRIMRH+
+neg=TRIMRH-
+step=10
+switch=GEAR
+value=33,0,0
+[timer1]
+src=Virt2
+[timer2]
+type=permanent
+src=Virt2
+val=1733598
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-320x240]
+V-trim=133,75,1
+H-trim=6,220,3
+V-trim=183,75,2
+H-trim=191,220,4
+Big-box=9,40,Ch1
+Big-box=9,90,Timer1
+Small-box=9,140,Timer2
+Bargraph=205,150,Ch2
+Bargraph=235,150,Ch3
+Bargraph=265,150,Ch1
+Bargraph=295,150,Ch4
+Model=206,40
+Toggle=130,38,1,64,128,None
+Toggle=168,38,2,65,129,None
+Toggle=147,76,0,66,0,RUD DR
+Toggle=147,113,0,67,0,AIL DR
+Toggle=147,153,0,68,0,ELE DR
+Toggle=147,192,8,71,0,None
+Big-box=9,177,Virt1
+quickpage1=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/models/yacht.ini b/src/tests/models/yacht.ini
new file mode 100644
index 0000000000..46d85c47c3
--- /dev/null
+++ b/src/tests/models/yacht.ini
@@ -0,0 +1,85 @@
+name=Joysway Orion
+mixermode=Advanced
+icon=YACHT.BMP
+type=multi
+[radio]
+protocol=DSM2
+num_channels=2
+fixed_id=859231
+tx_power=100mW
+
+[protocol_opts]
+Telemetry=On
+
+[channel1]
+reverse=1
+template=complex
+[mixer]
+src=THR
+dest=Ch1
+[mixer]
+src=AIL
+dest=Ch1
+switch=AIL DR1
+scalar=20
+usetrim=0
+muxtype=add
+curvetype=fixed
+
+[channel2]
+template=complex
+[mixer]
+src=RUD
+dest=Ch2
+[mixer]
+src=AIL
+dest=Ch2
+usetrim=0
+muxtype=add
+
+[trim1]
+src=LEFT_V
+pos=TRIMLV+
+neg=TRIMLV-
+[trim2]
+src=RIGHT_V
+pos=TRIMRV+
+neg=TRIMRV-
+[trim3]
+src=LEFT_H
+pos=TRIMLH+
+neg=TRIMLH-
+value=1,0,0
+[trim4]
+src=RIGHT_H
+pos=TRIMRH+
+neg=TRIMRH-
+[timer1]
+src=Ch3
+resetsrc=ELE DR1
+[timer2]
+type=permanent
+src=Ch3
+val=19187434
+[telemalarm1]
+source=RxV
+above=1
+value=500
+[datalog]
+switch=None
+rate=1 sec
+[safety]
+Auto=min
+[gui-128x64]
+V-trim=59,10,1
+H-trim=5,59,3
+Small-box=2,35,Timer1
+Small-box=2,45,Timer2
+Model=75,20
+Battery=102,1
+Toggle=22,10
+Switch=198,AIL DR1
+TxPower=102,7
+Big-box=2,21,Volt1
+quickpage1=Channel monitor
+quickpage2=Telemetry monitor
\ No newline at end of file
diff --git a/src/tests/test_model.c b/src/tests/test_model.c
index f5884c35db..f4754db55e 100644
--- a/src/tests/test_model.c
+++ b/src/tests/test_model.c
@@ -1,5 +1,65 @@
#include "CuTest.h"
+extern u8 CONFIG_ReadModel_old(const char* file);
+extern u8 CONFIG_WriteModel_old(u8 model_num);
+
+u8 CONFIG_ReadModel_new(const char* file) {
+ clear_model(1);
+
+ auto_map = 0;
+ if (CONFIG_IniParse(file, ini_handler, &Model)) {
+ printf("Failed to parse Model file: %s\n", file);
+ }
+ if (! ELEM_USED(Model.pagecfg2.elem[0]))
+ CONFIG_ReadLayout("layout/default.ini");
+ if(! PROTOCOL_HasPowerAmp(Model.protocol))
+ Model.tx_power = TXPOWER_150mW;
+ MIXER_SetMixers(NULL, 0);
+ if(auto_map)
+ RemapChannelsForProtocol(EATRG0);
+ if(! Model.name[0])
+ sprintf(Model.name, "Model%d", 1);
+ return 1;
+}
+
+const char* const names[] = {
+"tests/models/280qav.ini",
+"tests/models/bixler2.ini",
+"tests/models/geniuscp.ini",
+"tests/models/yacht.ini",
+
+"tests/models/4g6s.ini",
+"tests/models/blade130x.ini",
+"tests/models/nazath.ini",
+
+"tests/models/apm.ini",
+"tests/models/deltaray.ini",
+"tests/models/trex150dfc.ini",
+
+"tests/models/ardrone2.ini",
+"tests/models/fx071.ini",
+"tests/models/wltoys931.ini",
+};
+
+void TestNewAndOld(CuTest *t)
+{
+ struct Model ValidateModel;
+
+ for (unsigned i = 0; i < ARRAYSIZE(names); i++) {
+ const char *filename = names[i];
+ printf("Test model: %s\n", filename);
+ CONFIG_ReadModel_old(filename);
+ memcpy(&ValidateModel, &Model, sizeof(Model));
+ CONFIG_ReadModel_new(filename);
+
+ CuAssertTrue(t, memcmp(&ValidateModel, &Model, sizeof(Model)) == 0);
+
+ CONFIG_WriteModel(1);
+ CONFIG_ReadModel(1);
+ CuAssertTrue(t, memcmp(&ValidateModel, &Model, sizeof(Model)) == 0);
+ }
+}
+
void TestModelLoadSave(CuTest *t)
{
struct Model ValidateModel;