Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
66c1bda98This file is pinned to the selected release. Download the original to open it in your design tool.
#ifndef _ESPFC_MODEL_H_
#define _ESPFC_MODEL_H_
#include <cstddef>
#include <cstdint>
#include <functional>
#include <tuple>
#include <EscDriver.h>
#include "ModelConfig.h"
#include "ModelState.h"
#include "Stream/ReadWritable.hpp"
#include "Utils/Storage.hpp"
#include "Utils/Logger.hpp"
#include "Utils/Math.hpp"
namespace Espfc {
enum ModelChangeEvent
{
MODEL_CHANGE_FILTER,
MODEL_CHANGE_PID,
MODEL_CHANGE_RATES,
MODEL_CHANGE_ACCEL,
MODEL_CHANGE_INPUT,
MODEL_CHANGE_ADC,
};
class Model
{
public:
Model()
{
initialize();
}
void initialize()
{
config = ModelConfig();
#ifdef UNIT_TEST
state = ModelState(); // FIXME: causes board wdt reset
#endif
//config.brobot();
}
bool isModeActive(FlightMode mode) const
{
return state.mode.mask & (1 << mode);
}
bool hasChanged(FlightMode mode) const
{
return (state.mode.mask & (1 << mode)) != (state.mode.maskPrev & (1 << mode));
}
void clearMode(FlightMode mode)
{
state.mode.maskPrev |= state.mode.mask & (1 << mode);
state.mode.mask &= ~(1 << mode);
}
void updateModes(uint32_t mask)
{
state.mode.maskPrev = state.mode.mask;
state.mode.mask = mask;
}
bool isSwitchActive(FlightMode mode) const
{
return state.mode.maskSwitch & (1 << mode);
}
void updateSwitchActive(uint32_t mask)
{
state.mode.maskSwitch = mask;
}
void disarm(DisarmReason r)
{
state.mode.disarmReason = r;
clearMode(MODE_ARMED);
clearMode(MODE_AIRMODE);
state.appQueue.push(Event(EVENT_DISARM));
}
bool isFeatureActive(Feature feature) const
{
return config.featureMask & feature;
}
bool isAirModeActive() const
{
return isModeActive(MODE_AIRMODE);// || isFeatureActive(FEATURE_AIRMODE);
}
bool isThrottleLow() const
{
return state.input.us[AXIS_THRUST] < config.input.minCheck;
}
bool blackboxEnabled() const
{
// serial or flash
return (config.blackbox.dev == BLACKBOX_DEV_SERIAL || config.blackbox.dev == BLACKBOX_DEV_FLASH);
}
bool gyroActive() const /* IRAM_ATTR */
{
return state.gyro.present && config.gyro.dev != GYRO_NONE;
}
bool gpsActive() const /* IRAM_ATTR */
{
return state.gps.present;
}
bool accelActive() const
{
return state.accel.present && config.accel.dev != GYRO_NONE;
}
bool magActive() const
{
return state.mag.present && config.mag.dev != MAG_NONE;
}
bool baroActive() const
{
return state.baro.present && config.baro.dev != BARO_NONE;
}
bool calibrationActive() const
{
return state.accel.calibrationState != CALIBRATION_IDLE || state.gyro.calibrationState != CALIBRATION_IDLE || state.mag.calibrationState != CALIBRATION_IDLE;
}
void calibrateGyro()
{
state.gyro.calibrationState = CALIBRATION_START;
if(accelActive())
{
state.accel.calibrationState = CALIBRATION_START;
}
}
void calibrateMag()
{
state.mag.calibrationState = CALIBRATION_START;
}
void finishCalibration()
{
if(state.gyro.calibrationState == CALIBRATION_SAVE)
{
//save();
state.buzzer.push(BUZZER_GYRO_CALIBRATED);
logger.info().log("GYRO BIAS").log(Utils::toDeg(state.gyro.bias.x)).log(Utils::toDeg(state.gyro.bias.y)).logln(Utils::toDeg(state.gyro.bias.z));
}
if(state.accel.calibrationState == CALIBRATION_SAVE)
{
save();
logger.info().log("ACCEL BIAS").log(state.accel.bias.x).log(state.accel.bias.y).logln(state.accel.bias.z);
}
if(state.mag.calibrationState == CALIBRATION_SAVE)
{
save();
logger.info().log("MAG BIAS").log(state.mag.calibrationOffset.x).log(state.mag.calibrationOffset.y).logln(state.mag.calibrationOffset.z);
logger.info().log("MAG SCALE").log(state.mag.calibrationScale.x).log(state.mag.calibrationScale.y).logln(state.mag.calibrationScale.z);
}
}
bool armingDisabled() const /* IRAM_ATTR */
{
#if defined(ESPFC_DEV_PRESET_UNSAFE_ARMING)
return false;
#warning "Danger macro used ESPFC_DEV_PRESET_UNSAFE_ARMING"
#else
return state.mode.armingDisabledFlags != 0;
#endif
}
void setArmingDisabled(ArmingDisabledFlags flag, bool value)
{
if(value) state.mode.armingDisabledFlags |= flag;
else state.mode.armingDisabledFlags &= ~flag;
}
bool getArmingDisabled(ArmingDisabledFlags flag)
{
return state.mode.armingDisabledFlags & flag;
}
void setOutputSaturated(bool val)
{
state.output.saturated = val;
for(size_t i = 0; i < AXIS_COUNT_RPY; i++)
{
state.innerPid[i].outputSaturated = val;
state.outerPid[i].outputSaturated = val;
}
}
bool areMotorsRunning() const
{
size_t count = state.currentMixer.count;
for(size_t i = 0; i < count; i++)
{
if(config.output.channel[i].servo) continue;
if(state.output.disarmed[i] != config.output.minCommand) return true;
//if(state.output.us[i] != config.output.minCommand) return true;
}
return false;
}
void inline setDebug(DebugMode mode, size_t index, int16_t value)
{
if(index >= 8) return;
if(config.debug.mode != mode) return;
state.debug[index] = value;
}
void setGpsHome(bool force = false)
{
if(force || (state.gps.fix && state.gps.numSats >= config.gps.minSats))
{
if(!state.gps.homeSet || !config.gps.setHomeOnce)
{
state.gps.location.home = state.gps.location.raw;
state.gps.homeSet = true;
}
}
}
Stream::ReadWritable * getSerialStream(SerialPort i)
{
return state.serial[i].stream;
}
Stream::ReadWritable * getSerialStream(SerialFunction sf)
{
for(size_t i = 0; i < SERIAL_UART_COUNT; i++)
{
if(config.serial[i].functionMask & sf) return state.serial[i].stream;
}
return nullptr;
}
int getSerialIndex(SerialPortId id)
{
switch(id)
{
#ifdef ESPFC_SERIAL_0
case SERIAL_ID_UART_1: return SERIAL_UART_0;
#endif
#ifdef ESPFC_SERIAL_1
case SERIAL_ID_UART_2: return SERIAL_UART_1;
#endif
#ifdef ESPFC_SERIAL_2
case SERIAL_ID_UART_3: return SERIAL_UART_2;
#endif
#ifdef ESPFC_SERIAL_USB
case SERIAL_ID_USB_VCP: return SERIAL_USB;
#endif
#ifdef ESPFC_SERIAL_SOFT_0
case SERIAL_ID_SOFTSERIAL_1: return SERIAL_SOFT_0;
#endif
default: break;
}
return -1;
}
uint16_t getRssi() const
{
size_t channel = config.input.rssiChannel;
if(channel < 4 || channel > state.input.channelCount) return 0;
float value = state.input.ch[channel - 1];
return std::clamp<uint16_t>(lrintf(Utils::map(value, -1.0f, 1.0f, 0.0f, 1023.0f)), 0, 1023);
}
int load()
{
logger.begin();
_storage.begin();
logger.info().log("F_CPU").logln(F_CPU);
_storageResult = _storage.load(config);
logStorageResult();
postLoad();
return 1;
}
void save()
{
preSave();
_storageResult = _storage.save(config);
logStorageResult();
}
void reload()
{
begin();
}
void setRebootRequired()
{
state.rebootRequired = true;
setArmingDisabled(ARMING_DISABLED_REBOOT_REQUIRED, true);
}
bool getRebootRequired() const
{
return state.rebootRequired;
}
void calculateSimplifiedPids(const SimplifiedTuningConfig& s, PidConfig out[3]) const
{
// ESP-FC compile-time PID defaults for roll/pitch/yaw (no D-Max on this target)
static const PidConfig def[3] = {
{ 45, 80, 30, 110 },
{ 47, 84, 34, 115 },
{ 45, 80, 0, 110 },
};
if (s.pidsMode == SIMPLIFIED_TUNING_OFF) return;
const float master = s.masterMultiplier * 0.01f;
const float pi = s.piGain * 0.01f;
const float d = s.dGain * 0.01f;
const float ff = s.ffGain * 0.01f;
const float ig = s.iGain * 0.01f;
for (int axis = FC_PID_ROLL; axis <= std::clamp<int>(s.pidsMode, FC_PID_ROLL, FC_PID_YAW); axis++)
{
const float pitchD = (axis == FC_PID_PITCH) ? s.rollPitchRatio * 0.01f : 1.0f;
const float pitchPi = (axis == FC_PID_PITCH) ? s.pitchPiGain * 0.01f : 1.0f;
out[axis].P = std::clamp<long>(lrintf(def[axis].P * master * pi * pitchPi), 0L, SIMPLIFIED_PID_GAIN_MAX);
out[axis].I = std::clamp<long>(lrintf(def[axis].I * master * pi * ig * pitchPi), 0L, SIMPLIFIED_PID_GAIN_MAX);
out[axis].D = std::clamp<long>(lrintf(def[axis].D * master * d * pitchD), 0L, SIMPLIFIED_PID_GAIN_MAX);
out[axis].F = std::clamp<long>(lrintf(def[axis].F * master * pitchPi * ff), 0L, SIMPLIFIED_F_GAIN_MAX);
}
}
void calculateSimplifiedDtermFilters(uint8_t mult, int16_t& lpf1, int16_t& lpf2, int16_t& dynMin, int16_t& dynMax) const
{
if (dynMin)
{
dynMin = std::clamp<int>(SIMPLIFIED_DTERM_LPF1_DYN_MIN_HZ * mult / 100, 0, SIMPLIFIED_DYN_LPF_MAX_HZ);
dynMax = std::clamp<int>(SIMPLIFIED_DTERM_LPF1_DYN_MAX_HZ * mult / 100, 0, SIMPLIFIED_DYN_LPF_MAX_HZ);
}
if (lpf1) lpf1 = std::clamp<int>(SIMPLIFIED_DTERM_LPF1_DYN_MIN_HZ * mult / 100, 0, SIMPLIFIED_DYN_LPF_MAX_HZ);
if (lpf2) lpf2 = std::clamp<int>(SIMPLIFIED_DTERM_LPF2_HZ * mult / 100, 0, SIMPLIFIED_LPF_MAX_HZ);
}
void calculateSimplifiedGyroFilters(uint8_t mult, int16_t& lpf1, int16_t& lpf2, int16_t& dynMin, int16_t& dynMax) const
{
if (dynMin)
{
dynMin = std::clamp<int>(SIMPLIFIED_GYRO_LPF1_DYN_MIN_HZ * mult / 100, 0, SIMPLIFIED_DYN_LPF_MAX_HZ);
dynMax = std::clamp<int>(SIMPLIFIED_GYRO_LPF1_DYN_MAX_HZ * mult / 100, 0, SIMPLIFIED_DYN_LPF_MAX_HZ);
}
if (lpf1) lpf1 = std::clamp<int>(SIMPLIFIED_GYRO_LPF1_DYN_MIN_HZ * mult / 100, 0, SIMPLIFIED_DYN_LPF_MAX_HZ);
if (lpf2) lpf2 = std::clamp<int>(SIMPLIFIED_GYRO_LPF2_HZ * mult / 100, 0, SIMPLIFIED_LPF_MAX_HZ);
}
std::tuple<bool, bool, bool> validateSimplifiedTuning() const
{
const auto& s = config.simplifiedTuning;
const auto& pids = config.pid;
PidConfig tmp[3] = {pids[0], pids[1], pids[2]};
calculateSimplifiedPids(s, tmp);
bool pidOk = tmp[0].P == pids[0].P && tmp[0].I == pids[0].I &&
tmp[0].D == pids[0].D && tmp[0].F == pids[0].F &&
tmp[1].P == pids[1].P && tmp[1].I == pids[1].I &&
tmp[1].D == pids[1].D && tmp[1].F == pids[1].F &&
tmp[2].P == pids[2].P && tmp[2].I == pids[2].I &&
tmp[2].D == pids[2].D && tmp[2].F == pids[2].F;
const auto& gyro = config.gyro;
int16_t glpf1 = gyro.filter.freq;
int16_t glpf2 = gyro.filter2.freq;
int16_t gmin = gyro.dynLpfFilter.cutoff;
int16_t gmax = gyro.dynLpfFilter.freq;
if (s.gyroFilter) calculateSimplifiedGyroFilters(s.gyroFilterMultiplier, glpf1, glpf2, gmin, gmax);
bool gyroOk = glpf1 == gyro.filter.freq && glpf2 == gyro.filter2.freq &&
gmin == gyro.dynLpfFilter.cutoff && gmax == gyro.dynLpfFilter.freq;
const auto& dterm = config.dterm;
int16_t dlpf1 = dterm.filter.freq;
int16_t dlpf2 = dterm.filter2.freq;
int16_t dmin = dterm.dynLpfFilter.cutoff;
int16_t dmax = dterm.dynLpfFilter.freq;
if (s.dtermFilter) calculateSimplifiedDtermFilters(s.dtermFilterMultiplier, dlpf1, dlpf2, dmin, dmax);
bool dtermOk = dlpf1 == dterm.filter.freq && dlpf2 == dterm.filter2.freq &&
dmin == dterm.dynLpfFilter.cutoff && dmax == dterm.dynLpfFilter.freq;
return std::make_tuple(pidOk, gyroOk, dtermOk);
}
void reset()
{
initialize();
//save();
reload();
}
void sanitize()
{
// for spi gyro allow full speed mode
if (state.gyro.dev && state.gyro.dev->getBus()->isSPI())
{
state.gyro.rate = Utils::alignToClock(state.gyro.clock, ESPFC_GYRO_SPI_RATE_MAX);
}
else
{
state.gyro.rate = Utils::alignToClock(state.gyro.clock, ESPFC_GYRO_I2C_RATE_MAX);
// first usage
if(_storageResult == STORAGE_ERR_BAD_MAGIC || _storageResult == STORAGE_ERR_BAD_SIZE || _storageResult == STORAGE_ERR_BAD_VERSION)
{
config.loopSync = 1;
}
}
int loopSyncMax = 1;
//if(config.mag.dev != MAG_NONE || config.baro.dev != BARO_NONE) loopSyncMax /= 2;
config.loopSync = std::max((int)config.loopSync, loopSyncMax);
state.loopRate = state.gyro.rate / config.loopSync;
config.output.protocol = ESC_PROTOCOL_SANITIZE(config.output.protocol);
switch(config.output.protocol)
{
case ESC_PROTOCOL_BRUSHED:
config.output.async = true;
break;
case ESC_PROTOCOL_DSHOT150:
case ESC_PROTOCOL_DSHOT300:
case ESC_PROTOCOL_DSHOT600:
case ESC_PROTOCOL_PROSHOT:
config.output.async = false;
break;
}
if(config.output.async)
{
// for async limit pwm rate
switch(config.output.protocol)
{
case ESC_PROTOCOL_PWM:
config.output.rate = std::clamp<int16_t>(config.output.rate, 50, 480);
break;
case ESC_PROTOCOL_ONESHOT125:
config.output.rate = std::clamp<int16_t>(config.output.rate, 50, 2000);
break;
case ESC_PROTOCOL_ONESHOT42:
config.output.rate = std::clamp<int16_t>(config.output.rate, 50, 4000);
break;
case ESC_PROTOCOL_BRUSHED:
case ESC_PROTOCOL_MULTISHOT:
config.output.rate = std::clamp<int16_t>(config.output.rate, 50, 8000);
break;
default:
config.output.rate = std::clamp<int16_t>(config.output.rate, 50, 2000);
break;
}
}
else
{
// for synced and standard PWM limit loop rate and pwm pulse width
if(config.output.protocol == ESC_PROTOCOL_PWM && state.loopRate > 500)
{
config.loopSync = std::max(config.loopSync, (int8_t)((state.gyro.rate + 499) / 500)); // align loop rate to lower than 500Hz
state.loopRate = state.gyro.rate / config.loopSync;
if(state.loopRate > 480 && config.output.maxThrottle > 1940)
{
config.output.maxThrottle = 1940;
}
}
// for onshot125 limit loop rate to 2kHz
if(config.output.protocol == ESC_PROTOCOL_ONESHOT125 && state.loopRate > 2000)
{
config.loopSync = std::max(config.loopSync, (int8_t)((state.gyro.rate + 1999) / 2000)); // align loop rate to lower than 2000Hz
state.loopRate = state.gyro.rate / config.loopSync;
}
}
// sanitize throttle and motor limits
if(config.output.throttleLimitType < 0 || config.output.throttleLimitType >= THROTTLE_LIMIT_TYPE_MAX) {
config.output.throttleLimitType = THROTTLE_LIMIT_TYPE_NONE;
}
if(config.output.throttleLimitPercent < 1 || config.output.throttleLimitPercent > 100) {
config.output.throttleLimitPercent = 100;
}
if(config.output.motorLimit < 1 || config.output.motorLimit > 100) {
config.output.motorLimit = 100;
}
// configure serial ports
constexpr uint32_t serialFunctionAllowedMask = SERIAL_FUNCTION_MSP | SERIAL_FUNCTION_RX_SERIAL | SERIAL_FUNCTION_BLACKBOX |
SERIAL_FUNCTION_GPS | SERIAL_FUNCTION_TELEMETRY_FRSKY | SERIAL_FUNCTION_TELEMETRY_HOTT | SERIAL_FUNCTION_TELEMETRY_IBUS | SERIAL_FUNCTION_VTX_SMARTAUDIO;
uint32_t featureAllowMask = FEATURE_RX_PPM | FEATURE_RX_SERIAL | FEATURE_MOTOR_STOP | FEATURE_SOFTSERIAL | FEATURE_GPS |
FEATURE_TELEMETRY | FEATURE_RX_SPI;// | FEATURE_AIRMODE;
config.featureMask &= featureAllowMask;
for(int i = 0; i < SERIAL_UART_COUNT; i++)
{
config.serial[i].functionMask &= serialFunctionAllowedMask;
}
if (config.fusion.mode >= FUSION_MAX)
{
config.fusion.mode = FUSION_MAHONY;
}
// only few beeper modes allowed
config.buzzer.beeperMask &=
1 << (BUZZER_GYRO_CALIBRATED - 1) |
1 << (BUZZER_SYSTEM_INIT - 1) |
1 << (BUZZER_RX_LOST - 1) |
1 << (BUZZER_RX_SET - 1) |
1 << (BUZZER_DISARMING - 1) |
1 << (BUZZER_ARMING - 1) |
1 << (BUZZER_BAT_LOW - 1);
if(config.gyro.dynamicFilter.count > DYN_NOTCH_COUNT_MAX)
{
config.gyro.dynamicFilter.count = DYN_NOTCH_COUNT_MAX;
}
}
void begin()
{
sanitize();
// init timers
// sample rate = clock / ( divider + 1)
state.gyro.timer.setRate(state.gyro.rate);
int accelRate = Utils::alignToClock(state.gyro.timer.rate, 500);
state.accel.timer.setRate(state.gyro.timer.rate, state.gyro.timer.rate / accelRate);
state.loopTimer.setRate(state.gyro.timer.rate, config.loopSync);
state.mixer.timer.setRate(state.loopTimer.rate, config.mixerSync);
int inputRate = Utils::alignToClock(state.gyro.timer.rate, 1000);
state.input.timer.setRate(state.gyro.timer.rate, state.gyro.timer.rate / inputRate);
state.actuatorTimer.setRate(50);
state.gyro.dynamicFilterTimer.setRate(50);
state.telemetryTimer.setInterval(config.telemetryInterval * 1000);
state.stats.timer.setRate(3);
if(magActive())
{
state.mag.timer.setRate(state.mag.rate);
}
// ensure disarmed pulses
for(size_t i = 0; i < OUTPUT_CHANNELS; i++)
{
state.output.disarmed[i] = config.output.channel[i].servo ? config.output.channel[i].neutral : config.output.minCommand; // ROBOT
}
state.buzzer.beeperMask = config.buzzer.beeperMask;
state.customMixer = MixerConfig(config.customMixerCount, config.customMixes);
// override temporary
//state.telemetryTimer.setRate(100);
}
void postLoad()
{
// load current sensor calibration
for(size_t i = 0; i < AXIS_COUNT_RPY; i++)
{
state.gyro.bias.set(i, config.gyro.bias[i] / 1000.0f);
state.accel.bias.set(i, config.accel.bias[i] / 1000.0f);
state.mag.calibrationOffset.set(i, config.mag.offset[i] / 10.0f);
state.mag.calibrationScale.set(i, config.mag.scale[i] / 1000.0f);
}
}
void preSave()
{
// store current sensor calibration
for(size_t i = 0; i < AXIS_COUNT_RPY; i++)
{
config.gyro.bias[i] = lrintf(state.gyro.bias[i] * 1000.0f);
config.accel.bias[i] = lrintf(state.accel.bias[i] * 1000.0f);
config.mag.offset[i] = lrintf(state.mag.calibrationOffset[i] * 10.0f);
config.mag.scale[i] = lrintf(state.mag.calibrationScale[i] * 1000.0f);
}
}
ModelState state;
ModelConfig config;
Utils::Logger logger;
void logStorageResult()
{
switch(_storageResult)
{
case STORAGE_LOAD_SUCCESS: logger.info().logln("EEPROM load ok"); break;
case STORAGE_SAVE_SUCCESS: logger.info().logln("EEPROM save ok"); break;
case STORAGE_SAVE_ERROR: logger.err().logln("EEPROM save failed"); break;
case STORAGE_ERR_BAD_MAGIC: logger.err().logln("EEPROM wrong magic"); break;
case STORAGE_ERR_BAD_VERSION: logger.err().logln("EEPROM wrong version"); break;
case STORAGE_ERR_BAD_SIZE: logger.err().logln("EEPROM wrong size"); break;
case STORAGE_NONE:
default:
logger.err().logln("EEPROM uninitialized"); break;
}
}
void notifyConfigChange(ModelChangeEvent event)
{
if (_onConfigChange) _onConfigChange(event);
}
void setConfigChangeListener(std::function<void(ModelChangeEvent)> listener)
{
_onConfigChange = listener;
}
private:
Utils::Storage _storage;
StorageResult _storageResult;
std::function<void(ModelChangeEvent)> _onConfigChange{};
};
}
#endif