Files
edgetx-gx12/radio/src/opentx.cpp
T
kkoenig1@optonline.comandRaphael Coeffic fd276bb2fc 1. Included mroe fonts
2. change order of initialization.  Lvgl has to be before ViewMain
3. Basic support for layers, but they are still broken when Menu's are used
4. Register input driver in lvglwrapper
2022-06-11 15:38:08 +02:00

2224 lines
56 KiB
C++

/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "opentx.h"
#include "io/frsky_firmware_update.h"
#include "hal/adc_driver.h"
#include "aux_serial_driver.h"
#if defined(LIBOPENUI)
#include "libopenui.h"
// #include "shutdown_animation.h"
#include "radio_calibration.h"
#include "view_main.h"
#include "view_text.h"
#include "lvgl/lvgl.h"
#include "gui/colorlcd/LvglWrapper.h"
#endif
extern uint16_t get_flysky_hall_adc_value(uint8_t ch);
RadioData g_eeGeneral;
ModelData g_model;
#if defined(SDCARD)
Clipboard clipboard;
#endif
GlobalData globalData;
uint16_t maxMixerDuration; // step = 0.01ms
uint8_t heartbeat;
#if defined(OVERRIDE_CHANNEL_FUNCTION)
safetych_t safetyCh[MAX_OUTPUT_CHANNELS];
#endif
// __DMA for the MSC_BOT_Data member
union ReusableBuffer reusableBuffer __DMA;
uint8_t* MSC_BOT_Data = reusableBuffer.MSC_BOT_Data;
#if defined(DEBUG_LATENCY)
uint8_t latencyToggleSwitch = 0;
#endif
const uint8_t bchout_ar[] = {
0x1B, 0x1E, 0x27, 0x2D, 0x36, 0x39,
0x4B, 0x4E, 0x63, 0x6C, 0x72, 0x78,
0x87, 0x8D, 0x93, 0x9C, 0xB1, 0xB4,
0xC6, 0xC9, 0xD2, 0xD8, 0xE1, 0xE4 };
uint8_t channelOrder(uint8_t setup, uint8_t x)
{
return ((*(bchout_ar + setup) >> (6 - (x - 1) * 2)) & 3) + 1;
}
uint8_t channelOrder(uint8_t x)
{
return channelOrder(g_eeGeneral.templateSetup, x);
}
/*
mode1 rud ele thr ail
mode2 rud thr ele ail
mode3 ail ele thr rud
mode4 ail thr ele rud
*/
const uint8_t modn12x3[] = {
0, 1, 2, 3,
0, 2, 1, 3,
3, 1, 2, 0,
3, 2, 1, 0 };
volatile uint8_t rtc_count = 0;
uint32_t watchdogTimeout = 0;
void watchdogSuspend(uint32_t timeout)
{
watchdogTimeout = timeout;
}
#if defined(DEBUG_LATENCY)
void toggleLatencySwitch()
{
latencyToggleSwitch ^= 1;
#if defined(PCBHORUS)
if (latencyToggleSwitch)
GPIO_ResetBits(EXTMODULE_TX_GPIO, EXTMODULE_TX_GPIO_PIN);
else
GPIO_SetBits(EXTMODULE_TX_GPIO, EXTMODULE_TX_GPIO_PIN);
#else
if (latencyToggleSwitch)
sportUpdatePowerOn();
else
sportUpdatePowerOff();
#endif
}
#endif
void per10ms()
{
g_tmr10ms++;
if (watchdogTimeout) {
watchdogTimeout -= 1;
WDG_RESET(); // Retrigger hardware watchdog
}
#if defined(GUI)
if (lightOffCounter) lightOffCounter--;
if (flashCounter) flashCounter--;
#if !defined(LIBOPENUI)
if (noHighlightCounter) noHighlightCounter--;
#endif
#endif
if (trimsCheckTimer) trimsCheckTimer--;
if (ppmInputValidityTimer) ppmInputValidityTimer--;
if (trimsDisplayTimer)
trimsDisplayTimer--;
else
trimsDisplayMask = 0;
#if defined(DEBUG_LATENCY_END_TO_END)
static tmr10ms_t lastLatencyToggle = 0;
if (g_tmr10ms - lastLatencyToggle == 10) {
lastLatencyToggle = g_tmr10ms;
toggleLatencySwitch();
}
#endif
#if defined(RTCLOCK)
/* Update global Date/Time every 100 per10ms cycles */
if (++g_ms100 == 100) {
g_rtcTime++; // inc global unix timestamp one second
g_ms100 = 0;
}
#endif
readKeysAndTrims();
#if defined(FUNCTION_SWITCHES)
evalFunctionSwitches();
#endif
#if defined(ROTARY_ENCODER_NAVIGATION)
if (IS_ROTARY_ENCODER_NAVIGATION_ENABLE()) {
static rotenc_t rePreviousValue;
static bool cw = false;
rotenc_t reNewValue = (ROTARY_ENCODER_NAVIGATION_VALUE / ROTARY_ENCODER_GRANULARITY);
rotenc_t scrollRE = reNewValue - rePreviousValue;
if (scrollRE) {
static uint32_t lastEvent;
rePreviousValue = reNewValue;
bool new_cw = (scrollRE < 0) ? false : true;
if ((g_tmr10ms - lastEvent >= 10) || (cw == new_cw)) { // 100ms
pushEvent(new_cw ? EVT_ROTARY_RIGHT : EVT_ROTARY_LEFT);
// rotary encoder navigation speed (acceleration) detection/calculation
static uint32_t delay = 2*ROTENC_DELAY_MIDSPEED;
if (new_cw == cw) {
// Modified moving average filter used for smoother change of speed
delay = (((g_tmr10ms - lastEvent) << 3) + delay) >> 1;
}
else {
delay = 2*ROTENC_DELAY_MIDSPEED;
}
if (delay < ROTENC_DELAY_HIGHSPEED)
rotencSpeed = ROTENC_HIGHSPEED;
else if (delay < ROTENC_DELAY_MIDSPEED)
rotencSpeed = ROTENC_MIDSPEED;
else
rotencSpeed = ROTENC_LOWSPEED;
cw = new_cw;
lastEvent = g_tmr10ms;
}
}
}
#endif
telemetryInterrupt10ms();
// These moved here from evalFlightModeMixes() to improve beep trigger reliability.
#if defined(PWM_BACKLIGHT)
if ((g_tmr10ms&0x03) == 0x00)
backlightFade(); // increment or decrement brightness until target brightness is reached
#endif
#if !defined(AUDIO)
if (mixWarning & 1) if(((g_tmr10ms&0xFF)== 0)) AUDIO_MIX_WARNING(1);
if (mixWarning & 2) if(((g_tmr10ms&0xFF)== 64) || ((g_tmr10ms&0xFF)== 72)) AUDIO_MIX_WARNING(2);
if (mixWarning & 4) if(((g_tmr10ms&0xFF)==128) || ((g_tmr10ms&0xFF)==136) || ((g_tmr10ms&0xFF)==144)) AUDIO_MIX_WARNING(3);
#endif
#if defined(SDCARD)
sdPoll10ms();
#endif
outputTelemetryBuffer.per10ms();
heartbeat |= HEART_TIMER_10MS;
}
FlightModeData *flightModeAddress(uint8_t idx)
{
return &g_model.flightModeData[idx];
}
ExpoData *expoAddress(uint8_t idx )
{
return &g_model.expoData[idx];
}
MixData *mixAddress(uint8_t idx)
{
return &g_model.mixData[idx];
}
LimitData *limitAddress(uint8_t idx)
{
return &g_model.limitData[idx];
}
void memswap(void * a, void * b, uint8_t size)
{
uint8_t * x = (uint8_t *)a;
uint8_t * y = (uint8_t *)b;
uint8_t temp ;
while (size--) {
temp = *x;
*x++ = *y;
*y++ = temp;
}
}
#if defined(PXX2)
void setDefaultOwnerId()
{
uint8_t ch;
for (uint8_t i = 0; i < PXX2_LEN_REGISTRATION_ID; i++) {
ch = ((uint8_t *)cpu_uid)[4+i]&0x7f;
if(ch<0x20 || ch==0x7f) ch='-';
g_eeGeneral.ownerRegistrationID[PXX2_LEN_REGISTRATION_ID-1-i] = ch;
}
}
#endif
void generalDefault()
{
memclear(&g_eeGeneral, sizeof(g_eeGeneral));
#if defined(PCBHORUS)
g_eeGeneral.blOffBright = 20;
#endif
#if defined(LCD_CONTRAST_DEFAULT)
g_eeGeneral.contrast = LCD_CONTRAST_DEFAULT;
#endif
#if defined(DEFAULT_INTERNAL_MODULE)
g_eeGeneral.internalModule = DEFAULT_INTERNAL_MODULE;
#endif
#if defined(DEFAULT_POTS_CONFIG)
g_eeGeneral.potsConfig = DEFAULT_POTS_CONFIG;
#endif
#if defined(DEFAULT_SWITCH_CONFIG)
g_eeGeneral.switchConfig = DEFAULT_SWITCH_CONFIG;
#endif
#if defined(DEFAULT_SLIDERS_CONFIG)
g_eeGeneral.slidersConfig = DEFAULT_SLIDERS_CONFIG;
#endif
#if defined(STICK_DEAD_ZONE)
g_eeGeneral.stickDeadZone = DEFAULT_STICK_DEADZONE;
#endif
// vBatWarn is voltage in 100mV, vBatMin is in 100mV but with -9V offset,
// vBatMax has a -12V offset
g_eeGeneral.vBatWarn = BATTERY_WARN;
if (BATTERY_MIN != 90)
g_eeGeneral.vBatMin = BATTERY_MIN - 90;
if (BATTERY_MAX != 120)
g_eeGeneral.vBatMax = BATTERY_MAX - 120;
#if defined(DEFAULT_MODE)
g_eeGeneral.stickMode = DEFAULT_MODE - 1;
#endif
g_eeGeneral.templateSetup = DEFAULT_TEMPLATE_SETUP;
g_eeGeneral.backlightMode = e_backlight_mode_all;
g_eeGeneral.lightAutoOff = 2;
g_eeGeneral.inactivityTimer = 10;
g_eeGeneral.ttsLanguage[0] = 'e';
g_eeGeneral.ttsLanguage[1] = 'n';
g_eeGeneral.wavVolume = 2;
g_eeGeneral.backgroundVolume = 1;
for (int i=0; i<NUM_STICKS; ++i) {
g_eeGeneral.trainer.mix[i].mode = 2;
g_eeGeneral.trainer.mix[i].srcChn = channelOrder(i+1) - 1;
g_eeGeneral.trainer.mix[i].studWeight = 100;
}
#if defined(PCBX9E)
const int8_t defaultName[] = { 20, -1, -18, -1, -14, -9, -19 };
memcpy(g_eeGeneral.bluetoothName, defaultName, sizeof(defaultName));
#endif
#if defined(STORAGE_MODELSLIST)
strcpy(g_eeGeneral.currModelFilename, DEFAULT_MODEL_FILENAME);
#endif
#if defined(COLORLCD)
strcpy(g_eeGeneral.themeName, static_cast<OpenTxTheme *>(theme)->getName());
static_cast<OpenTxTheme *>(theme)->init();
#endif
#if defined(PXX2)
setDefaultOwnerId();
#endif
#if defined(RADIOMASTER_RTF_RELEASE)
// Those settings are for headless radio
g_eeGeneral.USBMode = USB_JOYSTICK_MODE;
g_eeGeneral.disableRtcWarning = 1;
g_eeGeneral.splashMode = 3; // Disable splash
g_eeGeneral.pwrOnSpeed = 1; // 1 second
#endif
g_eeGeneral.chkSum = 0xFFFF;
}
uint16_t evalChkSum()
{
uint16_t sum = 0;
const int16_t * calibValues = (const int16_t *) &g_eeGeneral.calib[0];
for (int i=0; i<12; i++)
sum += calibValues[i];
return sum;
}
bool isInputRecursive(int index)
{
ExpoData * line = expoAddress(0);
for (int i=0; i<MAX_EXPOS; i++, line++) {
if (line->chn > index)
break;
else if (line->chn < index)
continue;
else if (line->srcRaw >= MIXSRC_FIRST_LOGICAL_SWITCH)
return true;
}
return false;
}
#if defined(AUTOSOURCE)
constexpr int MULTIPOS_STEP_SIZE = (2 * RESX) / XPOTS_MULTIPOS_COUNT;
int8_t getMovedSource(uint8_t min)
{
int8_t result = 0;
static tmr10ms_t s_move_last_time = 0;
static int16_t inputsStates[MAX_INPUTS];
if (min <= MIXSRC_FIRST_INPUT) {
for (uint8_t i=0; i<MAX_INPUTS; i++) {
if (abs(anas[i] - inputsStates[i]) > MULTIPOS_STEP_SIZE) {
if (!isInputRecursive(i)) {
result = MIXSRC_FIRST_INPUT+i;
break;
}
}
}
}
static int16_t sourcesStates[NUM_STICKS+NUM_POTS+NUM_SLIDERS+NUM_MOUSE_ANALOGS];
if (result == 0) {
for (uint8_t i=0; i<NUM_STICKS+NUM_POTS+NUM_SLIDERS; i++) {
if (abs(calibratedAnalogs[i] - sourcesStates[i]) > MULTIPOS_STEP_SIZE) {
result = MIXSRC_Rud+i;
break;
}
}
}
bool recent = ((tmr10ms_t)(get_tmr10ms() - s_move_last_time) > 10);
if (recent) {
result = 0;
}
if (result || recent) {
memcpy(inputsStates, anas, sizeof(inputsStates));
memcpy(sourcesStates, calibratedAnalogs, sizeof(sourcesStates));
}
s_move_last_time = get_tmr10ms();
return result;
}
#endif
#if defined(FLIGHT_MODES)
uint8_t getFlightMode()
{
for (uint8_t i=1; i<MAX_FLIGHT_MODES; i++) {
FlightModeData *phase = &g_model.flightModeData[i];
if (phase->swtch && getSwitch(phase->swtch)) {
return i;
}
}
return 0;
}
#endif
trim_t getRawTrimValue(uint8_t phase, uint8_t idx)
{
FlightModeData * p = flightModeAddress(phase);
return p->trim[idx];
}
int getTrimValue(uint8_t phase, uint8_t idx)
{
int result = 0;
for (uint8_t i=0; i<MAX_FLIGHT_MODES; i++) {
trim_t v = getRawTrimValue(phase, idx);
if (v.mode == TRIM_MODE_NONE) {
return result;
}
else {
unsigned int p = v.mode >> 1;
if (p == phase || phase == 0) {
return result + v.value;
}
else {
phase = p;
if (v.mode % 2 != 0) {
result += v.value;
}
}
}
}
return 0;
}
bool setTrimValue(uint8_t phase, uint8_t idx, int trim)
{
for (uint8_t i=0; i<MAX_FLIGHT_MODES; i++) {
trim_t & v = flightModeAddress(phase)->trim[idx];
if (v.mode == TRIM_MODE_NONE)
return false;
unsigned int p = v.mode >> 1;
if (p == phase || phase == 0) {
v.value = trim;
break;
}
else if (v.mode % 2 == 0) {
phase = p;
}
else {
v.value = limit<int>(TRIM_EXTENDED_MIN, trim - getTrimValue(p, idx), TRIM_EXTENDED_MAX);
break;
}
}
storageDirty(EE_MODEL);
return true;
}
getvalue_t convert16bitsTelemValue(source_t channel, ls_telemetry_value_t value)
{
return value;
}
ls_telemetry_value_t maxTelemValue(source_t channel)
{
return 30000;
}
#define INAC_STICKS_SHIFT 6
#define INAC_SWITCHES_SHIFT 8
bool inputsMoved()
{
uint8_t sum = 0;
for (uint8_t i=0; i<NUM_STICKS+NUM_POTS+NUM_SLIDERS; i++)
sum += anaIn(i) >> INAC_STICKS_SHIFT;
for (uint8_t i=0; i<NUM_SWITCHES; i++)
sum += getValue(MIXSRC_FIRST_SWITCH+i) >> INAC_SWITCHES_SHIFT;
#if defined(GYRO)
for (uint8_t i=0; i<2; i++)
sum += getValue(MIXSRC_GYRO1+i) >> INAC_STICKS_SHIFT;
#endif
if (abs((int8_t)(sum-inactivity.sum)) > 1) {
inactivity.sum = sum;
return true;
}
else {
return false;
}
}
void checkBacklight()
{
static uint8_t tmr10ms ;
uint8_t x = g_blinkTmr10ms;
if (tmr10ms != x) {
tmr10ms = x;
if (inputsMoved()) {
inactivity.counter = 0;
if (g_eeGeneral.backlightMode & e_backlight_mode_sticks) {
resetBacklightTimeout();
}
}
#if defined(HARDWARE_TOUCH)
if (MainWindow::instance()->touchEventOccured()) {
inactivity.counter = 0;
if (g_eeGeneral.backlightMode & e_backlight_mode_keys) {
resetBacklightTimeout();
}
}
#endif
if (requiredBacklightBright == BACKLIGHT_FORCED_ON) {
currentBacklightBright = g_eeGeneral.backlightBright;
BACKLIGHT_ENABLE();
}
else {
bool backlightOn = ((g_eeGeneral.backlightMode == e_backlight_mode_on) ||
(g_eeGeneral.backlightMode != e_backlight_mode_off && lightOffCounter) ||
(g_eeGeneral.backlightMode == e_backlight_mode_off && isFunctionActive(FUNCTION_BACKLIGHT)));
if (flashCounter) {
backlightOn = !backlightOn;
}
if (backlightOn) {
currentBacklightBright = requiredBacklightBright;
#if defined(COLORLCD)
// force backlight on for color lcd radios
if(currentBacklightBright > BACKLIGHT_LEVEL_MAX - BACKLIGHT_LEVEL_MIN)
currentBacklightBright = BACKLIGHT_LEVEL_MAX - BACKLIGHT_LEVEL_MIN;
#endif
BACKLIGHT_ENABLE();
}
else {
BACKLIGHT_DISABLE();
}
}
}
}
void resetBacklightTimeout()
{
uint16_t autoOff = g_eeGeneral.lightAutoOff;
#if defined(COLORLCD)
autoOff = std::max<uint16_t>(1, autoOff); // prevent the timeout from being 0 seconds on color lcd radios
#endif
lightOffCounter = (autoOff*250) << 1;
}
#if defined(SPLASH)
void doSplash()
{
#if defined(PWR_BUTTON_PRESS)
bool refresh = false;
#endif
if (SPLASH_NEEDED()) {
resetBacklightTimeout();
drawSplash();
getADC(); // init ADC array
inputsMoved();
tmr10ms_t tgtime = get_tmr10ms() + SPLASH_TIMEOUT;
while (tgtime > get_tmr10ms()) {
RTOS_WAIT_TICKS(1);
getADC();
if (getEvent() || inputsMoved())
return;
#if defined(PWR_BUTTON_PRESS)
uint32_t pwr_check = pwrCheck();
if (pwr_check == e_power_off) {
break;
}
else if (pwr_check == e_power_press) {
refresh = true;
}
else if (pwr_check == e_power_on && refresh) {
drawSplash();
refresh = false;
}
#else
if (pwrCheck() == e_power_off) {
return;
}
#endif
checkBacklight();
}
}
}
#else
#define Splash()
#define doSplash()
#endif
#if defined(MULTIMODULE)
void checkMultiLowPower()
{
if (isModuleMultimodule(EXTERNAL_MODULE) && g_model.moduleData[EXTERNAL_MODULE].multi.lowPowerMode) {
ALERT("MULTI", STR_WARN_MULTI_LOWPOWER, AU_ERROR);
return;
}
#if defined(INTERNAL_MODULE_MULTI)
if (isModuleMultimodule(INTERNAL_MODULE) && g_model.moduleData[INTERNAL_MODULE].multi.lowPowerMode) {
ALERT("MULTI", STR_WARN_MULTI_LOWPOWER, AU_ERROR);
}
#endif
}
#endif
static void checkRTCBattery()
{
GET_ADC_IF_MIXER_NOT_RUNNING();
if (getRTCBatteryVoltage() < 200) {
ALERT(STR_BATTERY, STR_WARN_RTC_BATTERY_LOW, AU_ERROR);
}
}
#if defined(PCBFRSKY) || defined(PCBFLYSKY)
static void checkFailsafe()
{
for (int i=0; i<NUM_MODULES; i++) {
#if defined(MULTIMODULE)
// use delayed check for MPM
if (isModuleMultimodule(i)) break;
#endif
if (isModuleFailsafeAvailable(i)) {
ModuleData & moduleData = g_model.moduleData[i];
if (moduleData.failsafeMode == FAILSAFE_NOT_SET) {
ALERT(STR_FAILSAFEWARN, STR_NO_FAILSAFE, AU_ERROR);
break;
}
}
}
}
#else
#define checkFailsafe()
#endif
#if defined(GUI)
void checkAll()
{
#if defined(EEPROM_RLC) && !defined(SDCARD_RAW) && !defined(SDCARD_YAML)
checkLowEEPROM();
#endif
// we don't check the throttle stick if the radio is not calibrated
if (g_eeGeneral.chkSum == evalChkSum()) {
checkThrottleStick();
}
checkSwitches();
checkFailsafe();
if (isVBatBridgeEnabled() && !g_eeGeneral.disableRtcWarning) {
// only done once at board start
checkRTCBattery();
}
disableVBatBridge();
if (g_model.displayChecklist && modelHasNotes()) {
readModelNotes();
}
#if defined(MULTIMODULE)
checkMultiLowPower();
#endif
#if defined(COLORLCD)
if (!waitKeysReleased()) {
auto dlg = new FullScreenDialog(WARNING_TYPE_ALERT, STR_KEYSTUCK);
LED_ERROR_BEGIN();
AUDIO_ERROR_MESSAGE(AU_ERROR);
tmr10ms_t tgtime = get_tmr10ms() + 500;
uint32_t keys = readKeys();
std::string strKeys("");
const char STR_VKEYS[] = TR_VKEYS;
const int len = int(LEN_VKEYS[0]);
char s[6];
s[5] = 0;
for (int i = 0; i < (int)TRM_BASE; i++) {
if (keys & (1 << i)) {
strncpy(s, &STR_VKEYS[i * len], len);
strKeys += s;
}
}
dlg->setMessage(strKeys.c_str());
dlg->setCloseCondition([tgtime]() {
if (tgtime >= get_tmr10ms() && keyDown()) {
return false;
} else {
return true;
}
});
dlg->runForever();
LED_ERROR_END();
}
#else
if (!waitKeysReleased()) {
showMessageBox(STR_KEYSTUCK);
tmr10ms_t tgtime = get_tmr10ms() + 500;
while (tgtime != get_tmr10ms()) {
RTOS_WAIT_MS(1);
WDG_RESET();
}
}
#endif
#if defined(EXTERNAL_ANTENNA) && defined(INTERNAL_MODULE_PXX1)
checkExternalAntenna();
#endif
START_SILENCE_PERIOD();
}
#endif // GUI
#if defined(EEPROM_RLC) && !defined(SDCARD_RAW) && !defined(SDCARD_YAML)
void checkLowEEPROM()
{
if (g_eeGeneral.disableMemoryWarning) return;
if (EeFsGetFree() < 100) {
ALERT(STR_STORAGE_WARNING, STR_EEPROMLOWMEM, AU_ERROR);
}
}
#endif
bool isThrottleWarningAlertNeeded()
{
if (g_model.disableThrottleWarning) {
return false;
}
// throttle channel is either the stick according stick mode (already handled in evalInputs)
// or P1 to P3;
// in case an output channel is choosen as throttle source (thrTraceSrc>NUM_POTS+NUM_SLIDERS) we assume the throttle stick is the input
// no other information available at the moment, and good enough to my option (otherwise too much exceptions...)
uint8_t thrchn = ((g_model.thrTraceSrc==0) || (g_model.thrTraceSrc>NUM_POTS+NUM_SLIDERS)) ? THR_STICK : g_model.thrTraceSrc+NUM_STICKS-1;
GET_ADC_IF_MIXER_NOT_RUNNING();
evalInputs(e_perout_mode_notrainer); // let do evalInputs do the job
int16_t v = calibratedAnalogs[thrchn];
if (g_model.thrTraceSrc && g_model.throttleReversed) { // TODO : proper review of THR source definition and handling
v = -v;
}
if (g_model.enableCustomThrottleWarning) {
int16_t idleValue = (int32_t)RESX * (int32_t)g_model.customThrottleWarningPosition / (int32_t)100;
return abs(v - idleValue) > THRCHK_DEADBAND;
}
else {
return v > THRCHK_DEADBAND - RESX;
}
}
#if defined(COLORLCD)
void checkThrottleStick()
{
char throttleNotIdle[strlen(STR_THROTTLE_NOT_IDLE) + 9];
if (isThrottleWarningAlertNeeded()) {
if (g_model.enableCustomThrottleWarning) {
sprintf(throttleNotIdle, "%s (%d%%)", STR_THROTTLE_NOT_IDLE, g_model.customThrottleWarningPosition);
}
else {
strcpy(throttleNotIdle, STR_THROTTLE_NOT_IDLE);
}
LED_ERROR_BEGIN();
AUDIO_ERROR_MESSAGE(AU_THROTTLE_ALERT);
auto dialog =
new FullScreenDialog(WARNING_TYPE_ALERT, TR_THROTTLE_UPPERCASE,
throttleNotIdle, STR_PRESS_ANY_KEY_TO_SKIP);
dialog->setCloseCondition([]() { return !isThrottleWarningAlertNeeded(); });
dialog->runForever();
LED_ERROR_END();
}
}
#else
void checkThrottleStick()
{
char throttleNotIdle[strlen(STR_THROTTLE_NOT_IDLE) + 9];
if (!isThrottleWarningAlertNeeded()) {
return;
}
if (g_model.enableCustomThrottleWarning) {
sprintf(throttleNotIdle, "%s (%d%%)", STR_THROTTLE_NOT_IDLE, g_model.customThrottleWarningPosition);
}
else {
strcpy(throttleNotIdle, STR_THROTTLE_NOT_IDLE);
}
// first - display warning; also deletes inputs if any have been before
LED_ERROR_BEGIN();
RAISE_ALERT(TR_THROTTLE_UPPERCASE, throttleNotIdle, STR_PRESS_ANY_KEY_TO_SKIP, AU_THROTTLE_ALERT);
#if defined(PWR_BUTTON_PRESS)
bool refresh = false;
#endif
while (!keyDown()) {
if (!isThrottleWarningAlertNeeded()) {
return;
}
#if defined(PWR_BUTTON_PRESS)
uint32_t power = pwrCheck();
if (power == e_power_off) {
drawSleepBitmap();
boardOff();
break;
}
else if (power == e_power_press) {
refresh = true;
}
else if (power == e_power_on && refresh) {
RAISE_ALERT(TR_THROTTLE_UPPERCASE, throttleNotIdle, STR_PRESS_ANY_KEY_TO_SKIP, AU_NONE);
refresh = false;
}
#else
if (pwrCheck() == e_power_off) {
break;
}
#endif
checkBacklight();
WDG_RESET();
RTOS_WAIT_MS(10);
}
LED_ERROR_END();
}
#endif
void checkAlarm() // added by Gohst
{
if (g_eeGeneral.disableAlarmWarning) {
return;
}
if (IS_SOUND_OFF()) {
ALERT(STR_ALARMSWARN, STR_ALARMSDISABLED, AU_ERROR);
}
}
void alert(const char * title, const char * msg , uint8_t sound)
{
LED_ERROR_BEGIN();
TRACE("ALERT %s: %s", title, msg);
RAISE_ALERT(title, msg, STR_PRESSANYKEY, sound);
#if defined(PWR_BUTTON_PRESS)
bool refresh = false;
#endif
while (true) {
RTOS_WAIT_MS(10);
if (getEvent()) // wait for key release
break;
checkBacklight();
WDG_RESET();
const uint32_t pwr_check = pwrCheck();
if (pwr_check == e_power_off) {
drawSleepBitmap();
boardOff();
return; // only happens in SIMU, required for proper shutdown
}
#if defined(PWR_BUTTON_PRESS)
else if (pwr_check == e_power_press) {
refresh = true;
}
else if (pwr_check == e_power_on && refresh) {
RAISE_ALERT(title, msg, STR_PRESSANYKEY, AU_NONE);
refresh = false;
}
#endif
}
LED_ERROR_END();
}
#if defined(GVARS)
#if NUM_TRIMS == 6
int8_t trimGvar[NUM_TRIMS] = { -1, -1, -1, -1, -1, -1 };
#elif NUM_TRIMS == 4
int8_t trimGvar[NUM_TRIMS] = { -1, -1, -1, -1 };
#elif NUM_TRIMS == 2
int8_t trimGvar[NUM_TRIMS] = { -1, -1 };
#endif
#endif
void checkTrims()
{
event_t event = getEvent(true);
if (event && !IS_KEY_BREAK(event)) {
int8_t k = EVT_KEY_MASK(event) - TRM_BASE;
// LH_DWN LH_UP LV_DWN LV_UP RV_DWN RV_UP RH_DWN RH_UP
uint8_t idx = CONVERT_MODE_TRIMS((uint8_t)k/2);
uint8_t phase;
int before;
bool thro;
trimsDisplayTimer = 200; // 2 seconds
trimsDisplayMask |= (1<<idx);
#if defined(GVARS)
if (TRIM_REUSED(idx)) {
phase = getGVarFlightMode(mixerCurrentFlightMode, trimGvar[idx]);
before = GVAR_VALUE(trimGvar[idx], phase);
thro = false;
}
else {
phase = getTrimFlightMode(mixerCurrentFlightMode, idx);
before = getTrimValue(phase, idx);
thro = (idx == (g_model.getThrottleStickTrimSource() - MIXSRC_FIRST_TRIM) && g_model.thrTrim);
}
#else
phase = getTrimFlightMode(mixerCurrentFlightMode, idx);
before = getTrimValue(phase, idx);
thro = (idx==THR_STICK && g_model.thrTrim);
#endif
int8_t trimInc = g_model.trimInc + 1;
int8_t v = (trimInc==-1) ? min(32, abs(before)/4+1) : (1 << trimInc); // TODO flash saving if (trimInc < 0)
if (thro) v = 4; // if throttle trim and trim trottle then step=4
#if defined(GVARS)
if (TRIM_REUSED(idx)) v = 1;
#endif
int16_t after = (k&1) ? before + v : before - v; // positive = k&1
bool beepTrim = false;
if (!thro && before!=0 && ((!(after < 0) == (before < 0)) || after==0)) { //forcing a stop at centerered trim when changing sides
after = 0;
beepTrim = true;
AUDIO_TRIM_MIDDLE();
pauseEvents(event);
}
#if defined(GVARS)
if (TRIM_REUSED(idx)) {
int8_t gvar = trimGvar[idx];
int16_t vmin = GVAR_MIN + g_model.gvars[gvar].min;
int16_t vmax = GVAR_MAX - g_model.gvars[gvar].max;
if (after < vmin) {
after = vmin;
beepTrim = true;
AUDIO_TRIM_MIN();
killEvents(event);
}
else if (after > vmax) {
after = vmax;
beepTrim = true;
AUDIO_TRIM_MAX();
killEvents(event);
}
SET_GVAR_VALUE(gvar, phase, after);
}
else
#endif
{
int16_t tMax = g_model.extendedTrims ? TRIM_EXTENDED_MAX : TRIM_MAX;
int16_t tMin = g_model.extendedTrims ? TRIM_EXTENDED_MIN : TRIM_MIN;
if (before > tMin && after <= tMin) {
beepTrim = true;
AUDIO_TRIM_MIN();
killEvents(event);
}
else if (before < tMax && after >= tMax) {
beepTrim = true;
AUDIO_TRIM_MAX();
killEvents(event);
}
// Allow only move into the right direction if over the limits
if ((before < after && after > tMax) || // increasing over tMax
(before > after && after < tMin)) { // decreasing under tMin
after = before;
}
// Clip at hard limits
if (after < TRIM_EXTENDED_MIN) {
after = TRIM_EXTENDED_MIN;
}
else if (after > TRIM_EXTENDED_MAX) {
after = TRIM_EXTENDED_MAX;
}
if (!setTrimValue(phase, idx, after)) {
// we don't play a beep, so we exit now the function
return;
}
}
if (!beepTrim) {
AUDIO_TRIM_PRESS(after);
}
}
}
#if !defined(SIMU)
uint32_t s_anaFilt[NUM_ANALOGS];
#endif
#if defined(JITTER_MEASURE)
JitterMeter<uint16_t> rawJitter[NUM_ANALOGS];
JitterMeter<uint16_t> avgJitter[NUM_ANALOGS];
tmr10ms_t jitterResetTime = 0;
#endif
#define ANA_FILT(chan) (s_anaFilt[chan] / (JITTER_ALPHA * ANALOG_MULTIPLIER))
#if !defined(SIMU)
uint16_t anaIn(uint8_t chan)
{
return ANA_FILT(chan);
}
void getADC()
{
#if defined(JITTER_MEASURE)
if (JITTER_MEASURE_ACTIVE() && jitterResetTime < get_tmr10ms()) {
// reset jitter measurement every second
for (uint32_t x=0; x<NUM_ANALOGS; x++) {
rawJitter[x].reset();
avgJitter[x].reset();
}
jitterResetTime = get_tmr10ms() + 100; //every second
}
#endif
DEBUG_TIMER_START(debugTimerAdcRead);
if (!adcRead())
TRACE("adcRead failed");
DEBUG_TIMER_STOP(debugTimerAdcRead);
for (uint8_t x=0; x<NUM_ANALOGS; x++) {
uint32_t v;
#if defined(RADIO_FAMILY_T16) || defined(PCBNV14)
if (globalData.flyskygimbals)
{
if (x < 4) {
v = get_flysky_hall_adc_value(x) >> (1 - ANALOG_SCALE);
} else {
v = getAnalogValue(x) >> (1 - ANALOG_SCALE);
}
}
else
#endif
{
v = getAnalogValue(x) >> (1 - ANALOG_SCALE);
}
// Jitter filter:
// * pass trough any big change directly
// * for small change use Modified moving average (MMA) filter
//
// Explanation:
//
// Normal MMA filter has this formula:
// <out> = ((ALPHA-1)*<out> + <in>)/ALPHA
//
// If calculation is done this way with integer arithmetics, then any small change in
// input signal is lost. One way to combat that, is to rearrange the formula somewhat,
// to store a more precise (larger) number between iterations. The basic idea is to
// store undivided value between iterations. Therefore an new variable <filtered> is
// used. The new formula becomes:
// <filtered> = <filtered> - <filtered>/ALPHA + <in>
// <out> = <filtered>/ALPHA (use only when out is needed)
//
// The above formula with a maximum allowed ALPHA value (we are limited by
// the 16 bit s_anaFilt[]) was tested on the radio. The resulting signal still had
// some jitter (a value of 1 was observed). The jitter might be bigger on other
// radios.
//
// So another idea is to use larger input values for filtering. So instead of using
// input in a range from 0 to 2047, we use twice larger number (temp[x] is divided less)
//
// This also means that ALPHA must be lowered (remember 16 bit limit), but test results
// have proved that this kind of filtering gives better results. So the recommended values
// for filter are:
// JITTER_FILTER_STRENGTH 4
// ANALOG_SCALE 1
//
// Variables mapping:
// * <in> = v
// * <out> = s_anaFilt[x]
uint32_t previous = s_anaFilt[x] / JITTER_ALPHA;
uint32_t diff = (v > previous) ? (v - previous) : (previous - v);
// Combine ADC jitter filter setting form radio and model.
// Model can override (on or off) or use setting from radio setup.
// Model setting is active when 1, radio setting is active when 0
uint8_t useJitterFilter = 0;
if (g_model.jitterFilter == OVERRIDE_GLOBAL) {
// Use radio setting - which is inverted
useJitterFilter = !g_eeGeneral.noJitterFilter;
} else {
// Enable if value is "On", disable if "Off"
useJitterFilter = (g_model.jitterFilter == OVERRIDE_ON)?1:0;
}
if (useJitterFilter && diff < (10*ANALOG_MULTIPLIER)) {
// apply jitter filter
s_anaFilt[x] = (s_anaFilt[x] - previous) + v;
}
else {
// use unfiltered value
s_anaFilt[x] = v * JITTER_ALPHA;
}
#if defined(JITTER_MEASURE)
if (JITTER_MEASURE_ACTIVE()) {
avgJitter[x].measure(ANA_FILT(x));
}
#endif
#define ANAFILT_MAX (2 * RESX * JITTER_ALPHA * ANALOG_MULTIPLIER - 1)
StepsCalibData * calib = (StepsCalibData *) &g_eeGeneral.calib[x];
if (IS_POT_MULTIPOS(x) && IS_MULTIPOS_CALIBRATED(calib)) {
// TODO: consider adding another low pass filter to eliminate multipos switching glitches
uint8_t vShifted = ANA_FILT(x) >> 4;
s_anaFilt[x] = ANAFILT_MAX;
for (uint32_t i=0; i<calib->count; i++) {
if (vShifted < calib->steps[i]) {
s_anaFilt[x] = (i * ANAFILT_MAX) / calib->count;
break;
}
}
}
}
}
#endif // SIMU
uint8_t g_vbat100mV = 0;
uint16_t lightOffCounter;
uint8_t flashCounter = 0;
uint16_t sessionTimer;
uint16_t s_timeCumThr; // THR in 1/16 sec
uint16_t s_timeCum16ThrP; // THR% in 1/16 sec
uint8_t trimsCheckTimer = 0;
uint8_t trimsDisplayTimer = 0;
uint8_t trimsDisplayMask = 0;
void flightReset(uint8_t check)
{
// we don't reset the whole audio here (the hello.wav would be cut, if a prompt is queued before FlightReset, it should be played)
// TODO check if the vario / background music are stopped correctly if switching to a model which doesn't have these functions enabled
if (!IS_MANUAL_RESET_TIMER(0)) {
timerReset(0);
}
#if TIMERS > 1
if (!IS_MANUAL_RESET_TIMER(1)) {
timerReset(1);
}
#endif
#if TIMERS > 2
if (!IS_MANUAL_RESET_TIMER(2)) {
timerReset(2);
}
#endif
telemetryReset();
s_mixer_first_run_done = false;
START_SILENCE_PERIOD();
RESET_THR_TRACE();
logicalSwitchesReset();
if (check) {
checkAll();
}
}
#if defined(THRTRACE)
uint8_t s_traceBuf[MAXTRACE];
uint16_t s_traceWr;
uint8_t s_cnt_10s;
uint16_t s_cnt_samples_thr_10s;
uint16_t s_sum_samples_thr_10s;
#endif
void evalTrims()
{
uint8_t phase = mixerCurrentFlightMode;
for (uint8_t i=0; i<NUM_TRIMS; i++) {
// do trim -> throttle trim if applicable
int16_t trim = getTrimValue(phase, i);
if (trimsCheckTimer > 0) {
trim = 0;
}
trims[i] = trim*2;
}
}
uint8_t s_mixer_first_run_done = false;
void doMixerCalculations()
{
static tmr10ms_t lastTMR = 0;
tmr10ms_t tmr10ms = get_tmr10ms();
#if defined(DEBUG_LATENCY_MIXER_RF) || defined(DEBUG_LATENCY_RF_ONLY)
static tmr10ms_t lastLatencyToggle = 0;
if (tmr10ms - lastLatencyToggle >= 10) {
lastLatencyToggle = tmr10ms;
toggleLatencySwitch();
}
#endif
uint8_t tick10ms = (tmr10ms >= lastTMR ? tmr10ms - lastTMR : 1);
// handle tick10ms overrun
// correct overflow handling costs a lot of code; happens only each 11 min;
// therefore forget the exact calculation and use only 1 instead; good compromise
lastTMR = tmr10ms;
DEBUG_TIMER_START(debugTimerGetAdc);
getADC();
DEBUG_TIMER_STOP(debugTimerGetAdc);
DEBUG_TIMER_START(debugTimerGetSwitches);
getSwitchesPosition(!s_mixer_first_run_done);
DEBUG_TIMER_STOP(debugTimerGetSwitches);
DEBUG_TIMER_START(debugTimerEvalMixes);
evalMixes(tick10ms);
DEBUG_TIMER_STOP(debugTimerEvalMixes);
}
void doMixerPeriodicUpdates()
{
static tmr10ms_t lastTMR = 0;
tmr10ms_t tmr10ms = get_tmr10ms();
uint8_t tick10ms = (tmr10ms >= lastTMR ? tmr10ms - lastTMR : 1);
// handle tick10ms overrun
// correct overflow handling costs a lot of code; happens only each 11 min;
// therefore forget the exact calculation and use only 1 instead; good compromise
lastTMR = tmr10ms;
DEBUG_TIMER_START(debugTimerMixes10ms);
if (tick10ms) {
/* Throttle trace */
int16_t val;
if (g_model.thrTraceSrc > NUM_POTS+NUM_SLIDERS) {
uint8_t ch = g_model.thrTraceSrc-NUM_POTS-NUM_SLIDERS-1;
val = channelOutputs[ch];
LimitData * lim = limitAddress(ch);
int16_t gModelMax = LIMIT_MAX_RESX(lim);
int16_t gModelMin = LIMIT_MIN_RESX(lim);
if (lim->revert)
val = -val + gModelMax;
else
val = val - gModelMin;
#if defined(PPM_LIMITS_SYMETRICAL)
if (lim->symetrical) {
val -= calc1000toRESX(lim->offset);
}
#endif
gModelMax -= gModelMin; // we compare difference between Max and Mix for recaling needed; Max and Min are shifted to 0 by default
// usually max is 1024 min is -1024 --> max-min = 2048 full range
if (gModelMax != 0 && gModelMax != 2048)
val = (int32_t) (val << 11) / (gModelMax); // rescaling only needed if Min, Max differs
if (val < 0)
val=0; // prevent val be negative, which would corrupt throttle trace and timers; could occur if safetyswitch is smaller than limits
}
else {
val = RESX + calibratedAnalogs[g_model.thrTraceSrc == 0 ? THR_STICK : g_model.thrTraceSrc+NUM_STICKS-1];
}
val >>= (RESX_SHIFT-6); // calibrate it (resolution increased by factor 4)
evalTimers(val, tick10ms);
static uint8_t s_cnt_100ms;
static uint8_t s_cnt_1s;
static uint8_t s_cnt_samples_thr_1s;
static uint16_t s_sum_samples_thr_1s;
s_cnt_samples_thr_1s++;
s_sum_samples_thr_1s+=val;
if ((s_cnt_100ms += tick10ms) >= 10) { // 0.1sec
s_cnt_100ms -= 10;
s_cnt_1s += 1;
logicalSwitchesTimerTick();
checkTrainerSignalWarning();
if (s_cnt_1s >= 10) { // 1sec
s_cnt_1s -= 10;
sessionTimer += 1;
inactivity.counter++;
if ((((uint8_t)inactivity.counter) & 0x07) == 0x01 && g_eeGeneral.inactivityTimer && inactivity.counter > ((uint16_t)g_eeGeneral.inactivityTimer * 60))
AUDIO_INACTIVITY();
#if defined(AUDIO)
if (mixWarning & 1) if ((sessionTimer&0x03)==0) AUDIO_MIX_WARNING(1);
if (mixWarning & 2) if ((sessionTimer&0x03)==1) AUDIO_MIX_WARNING(2);
if (mixWarning & 4) if ((sessionTimer&0x03)==2) AUDIO_MIX_WARNING(3);
#endif
val = s_sum_samples_thr_1s / s_cnt_samples_thr_1s;
s_timeCum16ThrP += (val>>3); // s_timeCum16ThrP would overrun if we would store throttle value with higher accuracy; therefore stay with 16 steps
if (val)
s_timeCumThr += 1;
s_sum_samples_thr_1s >>= 2; // correct better accuracy now, because trace graph can show this information; in case thrtrace is not active, the compile should remove this
#if defined(THRTRACE)
// throttle trace is done every 10 seconds; Tracebuffer is adjusted to screen size.
// in case buffer runs out, it wraps around
// resolution for y axis is only 32, therefore no higher value makes sense
s_cnt_samples_thr_10s += s_cnt_samples_thr_1s;
s_sum_samples_thr_10s += s_sum_samples_thr_1s;
if (++s_cnt_10s >= 10) { // 10s
s_cnt_10s -= 10;
val = s_sum_samples_thr_10s / s_cnt_samples_thr_10s;
s_sum_samples_thr_10s = 0;
s_cnt_samples_thr_10s = 0;
s_traceBuf[s_traceWr % MAXTRACE] = val;
s_traceWr++;
}
#endif
s_cnt_samples_thr_1s = 0;
s_sum_samples_thr_1s = 0;
}
}
#if defined(PXX) || defined(DSM2)
static uint8_t countRangecheck = 0;
for (uint8_t i = 0; i < NUM_MODULES; ++i) {
if (isModuleBeeping(i)) {
if (++countRangecheck >= 250) {
countRangecheck = 0;
AUDIO_PLAY(AU_SPECIAL_SOUND_CHEEP);
}
}
}
#endif
checkTrims();
}
DEBUG_TIMER_STOP(debugTimerMixes10ms);
s_mixer_first_run_done = true;
}
#if !defined(OPENTX_START_DEFAULT_ARGS)
#define OPENTX_START_DEFAULT_ARGS 0
#endif
void opentxStart(const uint8_t startOptions = OPENTX_START_DEFAULT_ARGS)
{
TRACE("opentxStart(%u)", startOptions);
uint8_t calibration_needed = !(startOptions & OPENTX_START_NO_CALIBRATION) && (g_eeGeneral.chkSum != evalChkSum());
#if defined(GUI)
if (!calibration_needed && !(startOptions & OPENTX_START_NO_SPLASH)) {
AUDIO_HELLO();
doSplash();
}
#endif
#if defined(DEBUG_TRACE_BUFFER)
trace_event(trace_start, 0x12345678);
#endif
#if defined(TEST_BUILD_WARNING)
ALERT(STR_TEST_WARNING, TR_TEST_NOTSAFE, AU_ERROR);
#endif
#if defined(FUNCTION_SWITCHES)
if (!UNEXPECTED_SHUTDOWN()) {
setFSStartupPosition();
}
#endif
#if defined(GUI)
if (calibration_needed) {
#if defined(LIBOPENUI)
startCalibration();
#else
chainMenu(menuFirstCalib);
#endif
}
else if (!(startOptions & OPENTX_START_NO_CHECKS)) {
checkAlarm();
checkAll();
PLAY_MODEL_NAME();
}
#endif
}
void opentxClose(uint8_t shutdown)
{
TRACE("opentxClose");
watchdogSuspend(2000/*20s*/);
if (shutdown) {
pausePulses(); // stop mixer task to disable trims processing while in shutdown
AUDIO_BYE();
// TODO needed? telemetryEnd();
#if defined(LUA)
luaClose(&lsScripts);
#endif
#if defined(HAPTIC)
hapticOff();
#endif
}
#if defined(SDCARD)
logsClose();
#endif
storageFlushCurrentModel();
if (sessionTimer > 0) {
g_eeGeneral.globalTimer += sessionTimer;
sessionTimer = 0;
}
g_eeGeneral.unexpectedShutdown = 0;
storageDirty(EE_GENERAL);
storageCheck(true);
while (IS_PLAYING(ID_PLAY_PROMPT_BASE + AU_BYE)) {
RTOS_WAIT_MS(10);
}
RTOS_WAIT_MS(100);
#if defined(COLORLCD)
MainWindow::instance()->deleteChildren();
// this is necessary as the custom screens are not deleted
// by using deleteCustomScreens(), but here through it's parent window
memset(customScreens, 0, sizeof(customScreens));
//TODO: In fact we want only to empty the trash (private method)
MainWindow::instance()->run();
#if defined(LUA)
luaUnregisterWidgets();
luaClose(&lsWidgets);
lsWidgets = 0;
#endif
#endif
#if defined(SDCARD)
sdDone();
#endif
}
void opentxResume()
{
TRACE("opentxResume");
sdMount();
#if defined(COLORLCD) && defined(LUA)
// reload widgets
luaInitThemesAndWidgets();
#endif
storageReadAll();
#if defined(COLORLCD)
//TODO: needs to go into storageReadAll()
TRACE("reloading theme");
loadTheme();
// Force redraw
ViewMain::instance()->invalidate();
TRACE("theme reloaded & ViewMain invalidated");
#endif
// removed to avoid the double warnings (throttle, switch, etc.)
// opentxStart(OPENTX_START_NO_SPLASH | OPENTX_START_NO_CALIBRATION | OPENTX_START_NO_CHECKS);
referenceSystemAudioFiles();
if (!g_eeGeneral.unexpectedShutdown) {
g_eeGeneral.unexpectedShutdown = 1;
storageDirty(EE_GENERAL);
}
}
#define INSTANT_TRIM_MARGIN 10 /* around 1% */
void instantTrim()
{
int16_t anas_0[MAX_INPUTS];
evalInputs(e_perout_mode_notrainer | e_perout_mode_nosticks);
memcpy(anas_0, anas, sizeof(anas_0));
evalInputs(e_perout_mode_notrainer);
for (uint8_t stick = 0; stick < NUM_STICKS; stick++) {
if (stick != THR_STICK) { // don't instant trim the throttle stick
bool addTrim = false;
int16_t delta = 0;
uint8_t trimFlightMode = getTrimFlightMode(mixerCurrentFlightMode, stick);
for (uint8_t i = 0; i < MAX_EXPOS; i++) {
ExpoData * expo = expoAddress(i);
if (!EXPO_VALID(expo))
break; // end of list
if (stick == expo->srcRaw - MIXSRC_FIRST_STICK) {
if (expo->carryTrim < 0) {
// only default trims will be taken into account
addTrim = false;
break;
}
auto newDelta = anas[expo->chn] - anas_0[expo->chn];
if (addTrim && delta != newDelta) {
// avoid 2 different delta values
addTrim = false;
break;
}
addTrim = true;
delta = newDelta;
}
}
if (addTrim && abs(delta) >= INSTANT_TRIM_MARGIN) {
int16_t trim = limit<int16_t>(
TRIM_EXTENDED_MIN, (delta + trims[stick]) / 2, TRIM_EXTENDED_MAX);
setTrimValue(trimFlightMode, stick, trim);
}
}
}
storageDirty(EE_MODEL);
AUDIO_WARNING2();
}
void copySticksToOffset(uint8_t ch)
{
pauseMixerCalculations();
int32_t zero = (int32_t)channelOutputs[ch];
evalFlightModeMixes(e_perout_mode_nosticks+e_perout_mode_notrainer, 0);
int32_t val = chans[ch];
LimitData *ld = limitAddress(ch);
limit_min_max_t lim = LIMIT_MIN(ld);
if (val < 0) {
val = -val;
lim = LIMIT_MIN(ld);
}
zero = (zero*256000 - val*lim) / (1024*256-val);
ld->offset = (ld->revert ? -zero : zero);
resumeMixerCalculations();
storageDirty(EE_MODEL);
}
void copyTrimsToOffset(uint8_t ch)
{
int16_t zero;
pauseMixerCalculations();
evalFlightModeMixes(e_perout_mode_noinput, 0); // do output loop - zero input sticks and trims
zero = applyLimits(ch, chans[ch]);
evalFlightModeMixes(e_perout_mode_noinput-e_perout_mode_notrims, 0); // do output loop - only trims
int16_t output = applyLimits(ch, chans[ch]) - zero;
int16_t v = g_model.limitData[ch].offset;
if (g_model.limitData[ch].revert)
output = -output;
v += (output * 125) / 128;
g_model.limitData[ch].offset = limit((int16_t)-1000, (int16_t)v, (int16_t)1000); // make sure the offset doesn't go haywire
resumeMixerCalculations();
storageDirty(EE_MODEL);
}
void copyMinMaxToOutputs(uint8_t ch)
{
LimitData *ld = limitAddress(ch);
int16_t min = ld->min;
int16_t max = ld->max;
int16_t center = ld->ppmCenter;
pauseMixerCalculations();
for (uint8_t chan = 0; chan < MAX_OUTPUT_CHANNELS; chan++) {
ld = limitAddress(chan);
ld->min = min;
ld->max = max;
ld->ppmCenter = center;
}
resumeMixerCalculations();
storageDirty(EE_MODEL);
}
#if defined(STARTUP_ANIMATION)
inline uint32_t PWR_PRESS_DURATION_MIN()
{
return (2 - g_eeGeneral.pwrOnSpeed) * 100;
}
constexpr uint32_t PWR_PRESS_DURATION_MAX = 500; // 5s
void runStartupAnimation()
{
tmr10ms_t start = get_tmr10ms();
tmr10ms_t duration = 0;
bool isPowerOn = false;
while (pwrPressed()) {
duration = get_tmr10ms() - start;
if (duration < PWR_PRESS_DURATION_MIN()) {
drawStartupAnimation(duration, PWR_PRESS_DURATION_MIN());
}
else if (duration >= PWR_PRESS_DURATION_MAX) {
drawSleepBitmap();
backlightDisable();
}
else if (!isPowerOn) {
isPowerOn = true;
pwrOn();
haptic.play(15, 3, PLAY_NOW);
}
}
if (duration < PWR_PRESS_DURATION_MIN() || duration >= PWR_PRESS_DURATION_MAX) {
boardOff();
}
}
#endif
void moveTrimsToOffsets() // copy state of 3 primary to subtrim
{
int16_t zeros[MAX_OUTPUT_CHANNELS];
pauseMixerCalculations();
evalFlightModeMixes(e_perout_mode_noinput, 0); // do output loop - zero input sticks and trims
for (uint8_t i = 0; i < MAX_OUTPUT_CHANNELS; i++) {
zeros[i] = applyLimits(i, chans[i]);
}
evalFlightModeMixes(e_perout_mode_noinput-e_perout_mode_notrims, 0); // do output loop - only trims
for (uint8_t i = 0; i < MAX_OUTPUT_CHANNELS; i++) {
int16_t diff = applyLimits(i, chans[i]) - zeros[i];
int16_t v = g_model.limitData[i].offset;
if (g_model.limitData[i].revert)
diff = -diff;
v += (diff * 125) / 128;
g_model.limitData[i].offset = limit((int16_t) -1000, (int16_t) v, (int16_t) 1000); // make sure the offset doesn't go haywire
}
// reset all trims, except throttle (if throttle trim)
for (uint8_t i=0; i<NUM_TRIMS; i++) {
auto thrStick = g_model.getThrottleStickTrimSource() - MIXSRC_FIRST_TRIM;
if (i != thrStick || !g_model.thrTrim) {
int16_t original_trim = getTrimValue(mixerCurrentFlightMode, i);
for (uint8_t fm=0; fm<MAX_FLIGHT_MODES; fm++) {
trim_t trim = getRawTrimValue(fm, i);
if (trim.mode / 2 == fm)
setTrimValue(fm, i, trim.value - original_trim);
}
}
}
resumeMixerCalculations();
storageDirty(EE_MODEL);
AUDIO_WARNING2();
}
#if defined(ROTARY_ENCODER_NAVIGATION)
volatile rotenc_t rotencValue = 0;
uint8_t rotencSpeed;
#endif
void opentxInit()
{
TRACE("opentxInit");
#if defined(LIBOPENUI)
// create ViewMain
LvglWrapper::instance();
ViewMain::instance();
#elif defined(GUI)
// TODO add a function for this (duplicated)
menuHandlers[0] = menuMainView;
menuHandlers[1] = menuModelSelect;
#endif
#if defined(STARTUP_ANIMATION)
lcdRefreshWait();
lcdClear();
lcdRefresh();
lcdRefreshWait();
bool radioSettingsValid = storageReadRadioSettings(false);
(void)radioSettingsValid;
#endif
BACKLIGHT_ENABLE(); // we start the backlight during the startup animation
#if defined(STARTUP_ANIMATION)
if (WAS_RESET_BY_WATCHDOG_OR_SOFTWARE()) {
pwrOn();
}
else {
runStartupAnimation();
}
#else // defined(PWR_BUTTON_PRESS)
pwrOn();
#endif
// Radios handle UNEXPECTED_SHUTDOWN() differently:
// * radios with WDT and EEPROM and CPU controlled power use Reset status register
// and eeGeneral.unexpectedShutdown
// * radios with SDCARD model storage use Reset status register and special
// variables in RAM. They can not use eeGeneral.unexpectedShutdown
// * radios without CPU controlled power can only use Reset status register (if available)
if (UNEXPECTED_SHUTDOWN()) {
TRACE("Unexpected Shutdown detected");
globalData.unexpectedShutdown = 1;
}
#if defined(RTC_BACKUP_RAM)
SET_POWER_REASON(0);
#endif
#if defined(SDCARD)
// SDCARD related stuff, only done if not unexpectedShutdown
if (!globalData.unexpectedShutdown) {
if (!sdMounted())
sdInit();
#if !defined(COLORLCD)
if (!sdMounted()) {
g_eeGeneral.pwrOffSpeed = 2;
runFatalErrorScreen(STR_NO_SDCARD);
}
#endif
#if defined(AUTOUPDATE)
sportStopSendByteLoop();
if (f_stat(AUTOUPDATE_FILENAME, nullptr) == FR_OK) {
FrSkyFirmwareInformation information;
if (readFrSkyFirmwareInformation(AUTOUPDATE_FILENAME, information) == nullptr) {
#if defined(BLUETOOTH)
if (information.productFamily == FIRMWARE_FAMILY_BLUETOOTH_CHIP) {
if (bluetooth.flashFirmware(AUTOUPDATE_FILENAME) == nullptr)
f_unlink(AUTOUPDATE_FILENAME);
}
#endif
#if defined(HARDWARE_POWER_MANAGEMENT_UNIT)
if (information.productFamily == FIRMWARE_FAMILY_POWER_MANAGEMENT_UNIT) {
FrskyChipFirmwareUpdate device;
if (device.flashFirmware(AUTOUPDATE_FILENAME, false) == nullptr)
f_unlink(AUTOUPDATE_FILENAME);
}
#endif
}
}
#endif
logsInit();
}
#endif
#if defined(EEPROM)
if (!radioSettingsValid)
storageReadRadioSettings();
storageReadCurrentModel();
#endif
#if defined(COLORLCD) && defined(LUA)
if (!globalData.unexpectedShutdown) {
// ??? lua widget state must be prepared before the call to storageReadAll()
luaInitThemesAndWidgets();
}
#endif
// handling of storage for radios that have no EEPROM
#if !defined(EEPROM)
#if defined(RTC_BACKUP_RAM) && !defined(SIMU)
if (globalData.unexpectedShutdown) {
// SDCARD not available, try to restore last model from RAM
TRACE("rambackupRestore");
rambackupRestore();
}
else {
storageReadAll();
}
#else
storageReadAll();
#endif
#endif // #if !defined(EEPROM)
initSerialPorts();
currentSpeakerVolume = requiredSpeakerVolume = g_eeGeneral.speakerVolume + VOLUME_LEVEL_DEF;
currentBacklightBright = requiredBacklightBright = g_eeGeneral.backlightBright;
#if !defined(SOFTWARE_VOLUME)
setScaledVolume(currentSpeakerVolume);
#endif
referenceSystemAudioFiles();
audioQueue.start();
BACKLIGHT_ENABLE();
#if defined(SPORT_UPDATE_PWR_GPIO)
SPORT_UPDATE_POWER_INIT();
#endif
#if defined(COLORLCD)
loadTheme();
if (g_eeGeneral.backlightMode == e_backlight_mode_off) {
// no backlight mode off on color lcd radios
g_eeGeneral.backlightMode = e_backlight_mode_keys;
}
if (g_eeGeneral.backlightBright > BACKLIGHT_LEVEL_MAX - BACKLIGHT_LEVEL_MIN)
g_eeGeneral.backlightBright = BACKLIGHT_LEVEL_MAX - BACKLIGHT_LEVEL_MIN;
if (g_eeGeneral.lightAutoOff < 1)
g_eeGeneral.lightAutoOff = 1;
#endif
if (g_eeGeneral.backlightMode != e_backlight_mode_off) {
// on Tx start turn the light on
resetBacklightTimeout();
}
if (!globalData.unexpectedShutdown) {
opentxStart();
}
#if !defined(RTC_BACKUP_RAM)
if (!g_eeGeneral.unexpectedShutdown) {
g_eeGeneral.unexpectedShutdown = 1;
storageDirty(EE_GENERAL);
}
#endif
#if defined(GUI)
lcdSetContrast();
#endif
resetBacklightTimeout();
startPulses();
WDG_ENABLE(WDG_DURATION);
}
#if defined(SEMIHOSTING)
extern "C" void initialise_monitor_handles();
#endif
#if defined(SIMU)
void simuMain()
#else
int main()
#endif
{
#if defined(SEMIHOSTING)
initialise_monitor_handles();
#endif
#if !defined(SIMU)
/* Ensure all priority bits are assigned as preemption priority bits. */
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
#endif
TRACE("reusableBuffer: modelSel=%d, moduleSetup=%d, calib=%d, sdManager=%d, hardwareAndSettings=%d, spectrumAnalyser=%d, usb=%d",
sizeof(reusableBuffer.modelsel),
sizeof(reusableBuffer.moduleSetup),
sizeof(reusableBuffer.calib),
sizeof(reusableBuffer.sdManager),
sizeof(reusableBuffer.hardwareAndSettings),
sizeof(reusableBuffer.spectrumAnalyser),
sizeof(reusableBuffer.MSC_BOT_Data));
// G: The WDT remains active after a WDT reset -- at maximum clock speed. So it's
// important to disable it before commencing with system initialisation (or
// we could put a bunch more WDG_RESET()s in. But I don't like that approach
// during boot up.)
#if defined(LCD_CONTRAST_DEFAULT)
g_eeGeneral.contrast = LCD_CONTRAST_DEFAULT;
#endif
boardInit();
#if defined(COLORLCD)
extern void loadFonts(); // TODO ?
loadFonts();
#endif
#if !defined(SIMU)
stackPaint();
#endif
#if defined(PCBHORUS)
if (!IS_FIRMWARE_COMPATIBLE_WITH_BOARD()) {
runFatalErrorScreen(STR_WRONG_PCBREV);
}
#endif
#if defined(COLORLCD)
// SD_CARD_PRESENT() does not work properly on most
// B&W targets, so that we need to delay the detection
// until the SD card is mounted (requires RTOS scheduler running)
if (!SD_CARD_PRESENT() && !UNEXPECTED_SHUTDOWN()) {
runFatalErrorScreen(STR_NO_SDCARD);
}
#endif
tasksStart();
}
#if !defined(SIMU)
#if defined(PWR_BUTTON_PRESS)
inline uint32_t PWR_PRESS_SHUTDOWN_DELAY()
{
// Instant off when both power button are pressed
if (pwrForcePressed())
return 0;
return (2 - g_eeGeneral.pwrOffSpeed) * 100;
}
uint32_t pwr_press_time = 0;
uint32_t pwrPressedDuration()
{
if (pwr_press_time == 0) {
return 0;
}
else {
return get_tmr10ms() - pwr_press_time;
}
}
#if defined(COLORLCD)
inline tmr10ms_t getTicks() { return g_tmr10ms; }
#endif
uint32_t pwrCheck()
{
const char * message = nullptr;
enum PwrCheckState {
PWR_CHECK_ON,
PWR_CHECK_OFF,
PWR_CHECK_PAUSED,
};
static uint8_t pwr_check_state = PWR_CHECK_ON;
if (pwr_check_state == PWR_CHECK_OFF) {
return e_power_off;
}
else if (pwrPressed()) {
if (g_eeGeneral.backlightMode == e_backlight_mode_keys ||
g_eeGeneral.backlightMode == e_backlight_mode_all)
resetBacklightTimeout();
if (TELEMETRY_STREAMING()) {
message = STR_MODEL_STILL_POWERED;
}
if (pwr_check_state == PWR_CHECK_PAUSED) {
// nothing
}
else if (pwr_press_time == 0) {
pwr_press_time = get_tmr10ms();
if (message && !g_eeGeneral.disableRssiPoweroffAlarm) {
audioEvent(AU_MODEL_STILL_POWERED);
}
}
else {
inactivity.counter = 0;
if (g_eeGeneral.backlightMode != e_backlight_mode_off) {
BACKLIGHT_ENABLE();
}
if (get_tmr10ms() - pwr_press_time > PWR_PRESS_SHUTDOWN_DELAY()) {
#if defined(COLORLCD)
bool usbConfirmed = !usbPlugged() || getSelectedUsbMode() == USB_UNSELECTED_MODE;
bool modelConnectedConfirmed = !TELEMETRY_STREAMING() || g_eeGeneral.disableRssiPoweroffAlarm;
#endif
#if defined(SHUTDOWN_CONFIRMATION)
while (1)
#else
while ((usbPlugged() && getSelectedUsbMode() != USB_UNSELECTED_MODE) ||
(TELEMETRY_STREAMING() && !g_eeGeneral.disableRssiPoweroffAlarm))
#endif
{
#if !defined(COLORLCD)
lcdRefreshWait();
lcdClear();
POPUP_CONFIRMATION(STR_MODEL_SHUTDOWN, nullptr);
const char* msg = STR_MODEL_STILL_POWERED;
uint8_t msg_len = sizeof(TR_MODEL_STILL_POWERED);
if (usbPlugged() && getSelectedUsbMode() != USB_UNSELECTED_MODE) {
msg = STR_USB_STILL_CONNECTED;
msg_len = sizeof(TR_USB_STILL_CONNECTED);
}
event_t evt = getEvent(false);
SET_WARNING_INFO(msg, msg_len, 0);
DISPLAY_WARNING(evt);
LED_ERROR_BEGIN();
WDG_RESET();
lcdRefresh();
if (warningResult) {
pwr_check_state = PWR_CHECK_OFF;
return e_power_off;
}
else if (!warningText) {
// shutdown has been cancelled
pwr_check_state = PWR_CHECK_PAUSED;
LED_ERROR_END();
return e_power_on;
}
#else // COLORLCD
const char* message = nullptr;
std::function<bool(void)> closeCondition = nullptr;
if (!usbConfirmed) {
message = STR_USB_STILL_CONNECTED;
closeCondition = [](){
return !usbPlugged();
};
}
else if (!modelConnectedConfirmed) {
message = STR_MODEL_STILL_POWERED;
closeCondition = []() {
tmr10ms_t startTime = getTicks();
while (!TELEMETRY_STREAMING()) {
if (getTicks() - startTime > TELEMETRY_CHECK_DELAY10ms) break;
}
return !TELEMETRY_STREAMING() || g_eeGeneral.disableRssiPoweroffAlarm;
};
}
// TODO: abort dialog condition (here, RSSI lost / USB connected)
if (confirmationDialog(STR_MODEL_SHUTDOWN, message, false, closeCondition)) {
if( message == STR_USB_STILL_CONNECTED)
usbConfirmed=true;
if( message == STR_MODEL_STILL_POWERED)
modelConnectedConfirmed = true;
if(!usbConfirmed || !modelConnectedConfirmed)
continue;
pwr_check_state = PWR_CHECK_OFF;
return e_power_off;
} else {
// shutdown has been cancelled
pwr_check_state = PWR_CHECK_PAUSED;
LED_ERROR_END();
return e_power_on;
}
#endif // COLORLCD
}
haptic.play(15, 3, PLAY_NOW);
pwr_check_state = PWR_CHECK_OFF;
return e_power_off;
}
else {
drawShutdownAnimation(pwrPressedDuration(), PWR_PRESS_SHUTDOWN_DELAY(), message);
return e_power_press;
}
}
}
else {
#if defined(COLORLCD)
if (pwr_press_time != 0)
MainWindow::instance()->invalidate();
#endif
pwr_check_state = PWR_CHECK_ON;
pwr_press_time = 0;
}
return e_power_on;
}
#else
uint32_t pwrCheck()
{
#if defined(SOFT_PWR_CTRL)
if (pwrPressed()) {
return e_power_on;
}
#endif
if (usbPlugged()) {
return e_power_usb;
}
#if defined(TRAINER_PWR)
if (TRAINER_CONNECTED()) {
return e_power_trainer;
}
#endif
if (!g_eeGeneral.disableRssiPoweroffAlarm) {
if (TELEMETRY_STREAMING()) {
RAISE_ALERT(STR_MODEL, STR_MODEL_STILL_POWERED, STR_PRESS_ENTER_TO_CONFIRM, AU_MODEL_STILL_POWERED);
while (TELEMETRY_STREAMING()) {
resetForcePowerOffRequest();
RTOS_WAIT_MS(20);
if (pwrPressed()) {
return e_power_on;
}
else if (readKeys() == (1 << KEY_ENTER)) {
return e_power_off;
}
}
}
}
return e_power_off;
}
#endif // defined(PWR_BUTTON_PRESS)
#endif // !defined(SIMU)