C
Qt Quick Ultralite Automotive Cluster Demo
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "simulation/drivestates.h"
#include "simulation/drivetrain.h"
#include "simulation/speedlimits.h"
#include "simulation/lights.h"
#include "mathutils.h"
#include "Automotive/TellTalesModel.h"
#include "Automotive/NaviModel.h"
using namespace Automotive;
namespace {
MainModel &mainModel = MainModel::instance();
TellTalesModel &tellTalesModel = TellTalesModel::instance();
NaviModel &naviModel = NaviModel::instance();
static Simulation::Drivetrain::Config drivetrainConfig = {
200, // float maxSpeed;
7000, // float maxRpm;
800, // float minRpm;
6000, // float shiftUpAtRpm;
2500, // float shiftDownAtRpm;
207.3f, // float tireCircumference; // cm for 225/45/R18
7.5f, // float diffRatio;
6, // int gearNum;
{2.97f, 2.07f, 1.43f, 1.00f, 0.84f, 0.56f}, // float gearRatios[MAX_GEARS];
0.000025f, // float batteryFillRatio;
0.00002f, // float batteryDrainRatio;
0.000055f, // float coolantHeatRatio;
0.00002f, // float coolantCooldownRatio;
90.f, // float optimalTemp;
180.f, // float maxTemp;
(float) mainModel.fullRange.value() // float maxRange;
};
Simulation::Drivetrain drivetrain(drivetrainConfig);
Simulation::SpeedLimits speedLimits(20000, 10000);
Simulation::Lights lights;
} // namespace
namespace CommonDriveConstants {
const uint16_t DelayDriveAfterModeChange = 1500;
const float AcceChangeFilteringFactor = 0.15f;
const uint8_t StateUpdatePeriod = 100;
} // namespace CommonDriveConstants
namespace LightConstants {
const uint8_t LowBeamHeadlightsMinimalSpeed = 100;
const uint16_t LowBeamHeadlightsRetryPeriod = 5000;
const uint16_t LowBeamHeadlightsTurnOnTimeMin = 5000;
const uint16_t LowBeamHeadlightsTurnOnTimeMax = 15000;
const uint16_t TurnSignalShort = 1500;
const uint16_t TurnSignalLong = 5000;
const uint16_t TurnSingalTurnOnTimeScaleMin = 1;
const uint16_t TurnSingalTurnOnTimeScaleMax = 2;
const uint8_t TurnSignalShortMinSpeed = 70;
const uint16_t TurnSignalRetryPeriodMin = 5000;
const uint16_t TurnSignalRetryPeriodMax = 25000;
const uint16_t TurnSignalInitialDelay = 10000;
} // namespace LightConstants
namespace LaneAssistConstants {
const uint16_t InitialDelay = 10000;
const uint16_t RetryPeriodMin = 5000;
const uint16_t RetryPeriodMax = 15000;
} // namespace LaneAssistConstants
namespace ArrowGuidesConstants {
const uint8_t HighSpeedForwardOnly = 80;
const uint8_t MediumSpeedLightTurns = 50;
const float HighSpeedDistanceMin = 0.5f; // 500m
const float HighSpeedDistanceMax = 1.f; // 1km
const float DistanceMin = 0.1f; // 100m
const float DistanceMax = 0.3f; // 300m
const float InitialDistance = 0.05f;
} // namespace ArrowGuidesConstants
namespace NormalDriveConstants {
const uint16_t AccChangeRandomizePeriod = 3000;
const float AccChangeMin = -0.1f;
const float AccChangeMax = 1.0f;
const float AccMin = -0.3f;
const float AccMax = 0.4f;
const float AccInitial = AccMax;
const float AccChangeSpeedFactor = 10000.f;
const uint8_t DefaultSpeedLimit = 160;
const uint8_t ExcessiveSpeedingThreshold = 50;
const float ExcessiveSpeedingBraking = -0.5f;
const uint8_t LightSpeedingThreshold = 15;
const float LightSpeedingBraking = -0.2f;
const float LightSpeedingBrakingRelaxValue = -0.4f;
} // namespace NormalDriveConstants
namespace SportDriveConstants {
const float MinSpeed = 10.f;
const float SpeedBoostFactor = 20.f;
const float AccMin = 0.2f;
const float AccMax = 2.0f;
const float CooldownSpeedBoostFactor = 2.f;
const float CooldownSpeedReductionFactor = 5.f;
const float CooldownAccMin = 0.05f;
const float CooldownAccMax = 0.15f;
const float CooldownAccReductionMin = 0.1f;
const float CooldownAccReductionMax = 1.f;
const float CooldownStartMaxTempRatio = 0.9f;
const float CooldownStopOptimalTempRatio = 1.1f;
const float SpeedFactorMin = 0.f;
const float SpeedFactorMax = 1.f;
} // namespace SportDriveConstants
namespace Simulation {
bool isDriveState(const StateId &state)
{
return state == State_NormalDrive || state == State_SportDrive;
}
DriveState::DriveState(MainModel::ClusterMode mode_, char gearLabel)
: ClusterModeState(mode_)
, _gearLabel(gearLabel)
, _cumulatedTime(0)
, _lastGear(std::numeric_limits<int>::min())
, _lowBeamHeadlightsTimestamp(0)
, _turnSignalTimestamp(0)
, _laneAssistTimestamp(0)
, _guideArrowNextChange(0)
, _acceleration(0)
, _comesFromDriveState(false)
{}
void DriveState::onEnter(const StateId &prevState, const Layer &layer, Machine &sm)
{
ClusterModeState::onEnter(prevState, layer, sm);
_lastGear = std::numeric_limits<int>::min();
_cumulatedTime = 0;
_comesFromDriveState = isDriveState(prevState);
if (!_comesFromDriveState) {
mainModel.laneAssistCarMoving.setValue(true);
drivetrain.reset();
drivetrain.resetOdo(mainModel.odo.value());
drivetrain.resetBattery(mainModel.batteryLevel.value());
speedLimits.reset((SpeedLimitValues::Limit) mainModel.speedLimitWarning.value());
lights.setLightState(Lights::ParkingLights, true);
naviModel.triggerInstruction(NaviModel::ArrowForward);
sm.requestUpdate(CommonDriveConstants::StateUpdatePeriod, true, layer);
}
_turnSignalTimestamp
= LightConstants::TurnSignalInitialDelay; // Don't turn on turn signals for at least 10s from state begin
_laneAssistTimestamp = LaneAssistConstants::InitialDelay; // Same for lane assist
// Use previous value from MainModel as odo can be changed via SimulatorController
_guideArrowNextChange = mainModel.odo.value() + ArrowGuidesConstants::InitialDistance;
_lowBeamHeadlightsTimestamp = 0;
}
void DriveState::onUpdate(uint32_t tick, const Layer &, Machine &)
{
_cumulatedTime += tick;
// When switching drive states, don't start to drive immediately,
// keep prev acc value for a while
if (getStateTime() > CommonDriveConstants::DelayDriveAfterModeChange || !_comesFromDriveState) {
drive(tick);
} else {
updateDrivetrain(tick, _acceleration);
}
updateLights(tick);
updateLaneAssist();
updateGuides();
updateModels();
}
void DriveState::updateModels()
{
if (mainModel.showSimulatedDriveData.value()) {
const Drivetrain::DriveData &data = drivetrain.getDriveData();
mainModel.speed.setValue(data.speed);
mainModel.rpm.setValue(data.rpm);
mainModel.odo.setValue(data.odo);
mainModel.batteryLevel.setValue(data.battery);
mainModel.temp.setValue(data.coolantTemp);
mainModel.range.setValue(int(data.range));
mainModel.fuelLevel.setValue(data.fuel);
mainModel.gear.setValue(data.gear + 1);
}
if (tellTalesModel.showSimulatedLightIndicators.value()) {
tellTalesModel.turnLeftBlinking.setValue(lights.getLightState(Lights::TurnLeft));
tellTalesModel.turnRightBlinking.setValue(lights.getLightState(Lights::TurnRight));
tellTalesModel.parkingLightsActive.setValue(lights.getLightState(Lights::ParkingLights));
tellTalesModel.lowBeamHeadlightsActive.setValue(lights.getLightState(Lights::LowBeamHeadlights));
}
const int currentGear = mainModel.gear.value();
if (currentGear != _lastGear) {
// This is workaround for QUL string using static buffer underneath
// (no value change could be detected then)
static char gearText[3] = {'\0', '\0', '\0'};
if (currentGear > 0) {
gearText[0] = _gearLabel;
gearText[1] = '0' + currentGear;
} else {
gearText[0] = 'P';
gearText[1] = '\0';
}
mainModel.gearShiftText.setValue(gearText);
mainModel.gearShiftText.markDependencyDirty();
_lastGear = currentGear;
}
}
void DriveState::onLeave(const StateId &nextState, const Layer &layer, Machine &sm)
{
if (!isDriveState(nextState)) {
sm.dismissUpdate(layer);
}
}
void DriveState::updateLights(uint32_t tick)
{
using namespace LightConstants;
lights.update(tick);
if (drivetrain.getDriveData().speed > LowBeamHeadlightsMinimalSpeed
&& lights.getLightState(Lights::LowBeamHeadlights) == false
&& int64_t(getStateTime() - _lowBeamHeadlightsTimestamp) > LowBeamHeadlightsRetryPeriod) {
const bool turnOn = randomChoice(1u) == 0;
if (turnOn) {
const uint32_t turnOnTime = randomize(LowBeamHeadlightsTurnOnTimeMin, LowBeamHeadlightsTurnOnTimeMax);
lights.setLightState(Lights::LowBeamHeadlights, turnOn, turnOnTime);
_lowBeamHeadlightsTimestamp = getStateTime() + turnOnTime;
} else {
_lowBeamHeadlightsTimestamp = getStateTime();
}
}
if (int64_t(getStateTime() - _turnSignalTimestamp) > 0) {
// Choices:
// 0 - turn left
// 1 - turn right
// 2 - off
uint8_t choice = randomChoice(2u);
if (choice == 0 || choice == 1) {
// When going fast simulate lane changes by shorter period of signal enabled
const uint16_t turnOnBaseTime = (drivetrain.getDriveData().speed > TurnSignalShortMinSpeed
? TurnSignalShort
: TurnSignalLong);
const uint16_t turnOnTime = randomScale(turnOnBaseTime,
TurnSingalTurnOnTimeScaleMin,
TurnSingalTurnOnTimeScaleMax);
lights.setLightState(choice == 0 ? Lights::TurnLeft : Lights::TurnRight, true, turnOnTime);
} else {
lights.setLightState(Lights::TurnLeft, false);
lights.setLightState(Lights::TurnRight, false);
}
_turnSignalTimestamp = getStateTime() + randomize(TurnSignalRetryPeriodMin, TurnSignalRetryPeriodMax);
}
}
void DriveState::updateLaneAssist()
{
using namespace LaneAssistConstants;
if (int64_t(getStateTime() - _laneAssistTimestamp) > 0) {
// Choices:
// 0 - left
// 1 - right
// 2 - off
uint8_t choice = mainModel.laneAssistEnabled.value() ? randomChoice(2u) : 2;
// Don't signal same side as current turn indicator
if ((choice == 0 && lights.getLightState(Lights::TurnLeft))
|| (choice == 1 && lights.getLightState(Lights::TurnRight))) {
choice = (choice + 1) % 2;
}
mainModel.triggerLaneAssist(choice);
_laneAssistTimestamp = getStateTime() + randomize(RetryPeriodMin, RetryPeriodMax);
}
}
void DriveState::updateGuides()
{
using namespace ArrowGuidesConstants;
// Use previous values from MainModel as odo and speed can be changed via SimulatorController
const float odo = mainModel.odo.value();
const float speed = mainModel.speed.value();
if (odo >= _guideArrowNextChange) {
// Tight turns and roundabuts possible only at low speeds
NaviModel::ArrowType maxAllowedManoeuver = NaviModel::ArrowNone;
const bool highSpeed = speed > HighSpeedForwardOnly;
if (highSpeed) {
maxAllowedManoeuver = NaviModel::ArrowForward;
} else if (speed > MediumSpeedLightTurns) {
maxAllowedManoeuver = NaviModel::ArrowRight;
} else {
maxAllowedManoeuver = NaviModel::ArrowRound;
}
NaviModel::ArrowType choice = randomChoice(maxAllowedManoeuver, NaviModel::ArrowForward);
naviModel.triggerInstruction(choice);
// All manoeuvers should change quickly, just forward direction should stay longer
float distanceToChange = highSpeed ? randomize(HighSpeedDistanceMin, HighSpeedDistanceMax)
: randomize(DistanceMin, DistanceMax);
_guideArrowNextChange = odo + distanceToChange;
}
const uint32_t distanceInMeters = uint32_t((_guideArrowNextChange - odo) * 1000);
naviModel.distanceToNextManoeuver.setValue(distanceInMeters);
}
void DriveState::updateDrivetrain(uint32_t tick, float acceleration)
{
using namespace CommonDriveConstants;
_acceleration = AcceChangeFilteringFactor * acceleration + (1 - AcceChangeFilteringFactor) * _acceleration;
drivetrain.update(tick, _acceleration);
}
NormalDriveState::NormalDriveState()
: DriveState(MainModel::ModeNormal, 'D')
, _accChangeTimestamp(0)
, _targetAcc(0)
, _accChange(0)
{}
void NormalDriveState::onEnter(const StateId &prevState, const Layer &layer, Machine &sm)
{
DriveState::onEnter(prevState, layer, sm);
_targetAcc = NormalDriveConstants::AccInitial;
randomizeAccChange();
}
void NormalDriveState::onUpdate(uint32_t tick, const Layer &layer, Machine &sm)
{
speedLimits.update(tick);
DriveState::onUpdate(tick, layer, sm);
}
void NormalDriveState::randomizeAccChange()
{
using namespace NormalDriveConstants;
_accChange = randomize(AccChangeMin, AccChangeMax);
_accChangeTimestamp = 0;
}
void NormalDriveState::drive(uint32_t tick)
{
using namespace NormalDriveConstants;
_accChangeTimestamp += tick;
float limit = speedLimits.getCurrentSpeedLimitInKmh();
if (limit <= 10e-5f) {
// There is not speed limit, but in normal mode don't go to fast
limit = DefaultSpeedLimit;
}
if (limit > 0 && drivetrain.getDriveData().speed > limit) {
if (drivetrain.getDriveData().speed - limit > ExcessiveSpeedingThreshold) {
// Excessive speeding slow down fast
_targetAcc = ExcessiveSpeedingBraking;
} else if (_targetAcc < LightSpeedingBrakingRelaxValue
&& drivetrain.getDriveData().speed - limit < LightSpeedingThreshold) {
// Lot of braking, but not so much speeding any more, relax
_targetAcc = LightSpeedingBraking;
} else if (_targetAcc > LightSpeedingBraking) {
// Slight speeding, slow down gently
_targetAcc -= tick / AccChangeSpeedFactor;
}
} else {
// Just randomize acc change
if (_accChangeTimestamp >= AccChangeRandomizePeriod) {
randomizeAccChange();
}
_targetAcc += tick / AccChangeSpeedFactor * _accChange;
_targetAcc = clamp(_targetAcc, AccMin, AccMax);
}
DriveState::updateDrivetrain(tick, _targetAcc);
}
void NormalDriveState::updateModels()
{
DriveState::updateModels();
mainModel.speedLimitWarning.setValue(speedLimits.getCurrentSpeedLimit());
}
SportDriveState::SportDriveState()
: DriveState(MainModel::ModeSport, 'S')
, _targetSpeed(0)
, _cooldown(false)
{}
void SportDriveState::onEnter(const StateId &prevState, const Layer &layer, Machine &sm)
{
DriveState::onEnter(prevState, layer, sm);
mainModel.speedLimitWarning.setValue(SpeedLimitValues::None);
randomizeTargetSpeed();
}
void SportDriveState::onUpdate(uint32_t tick, const Layer &layer, Machine &sm)
{
updateCooldown();
DriveState::onUpdate(tick, layer, sm);
}
void SportDriveState::drive(uint32_t tick)
{
using namespace SportDriveConstants;
const Drivetrain::DriveData &data = drivetrain.getDriveData();
const int dir = _targetSpeed - data.speed > 0 ? 1 : -1;
const float speedFactor = std::abs(_targetSpeed - data.speed) / drivetrainConfig.maxSpeed;
float targetAcc = 0;
if (_cooldown) {
if (dir > 0) {
targetAcc = linearPartitionSelect(clamp(speedFactor * CooldownSpeedBoostFactor,
SpeedFactorMin,
SpeedFactorMax),
CooldownAccMin,
CooldownAccMax);
} else {
targetAcc = linearPartitionSelect(clamp(speedFactor * CooldownSpeedReductionFactor,
SpeedFactorMin,
SpeedFactorMax),
CooldownAccReductionMin,
CooldownAccReductionMax);
}
} else {
targetAcc = sqrtPartitionSelect(clamp(speedFactor * SpeedBoostFactor, SpeedFactorMin, SpeedFactorMax),
AccMin,
AccMax);
}
targetAcc *= dir;
DriveState::updateDrivetrain(tick, targetAcc);
if ((dir > 0 && data.speed >= _targetSpeed) || (dir < 0 && data.speed <= _targetSpeed)) {
// Target speed boundary just crossed
randomizeTargetSpeed();
}
}
void SportDriveState::updateCooldown()
{
using namespace SportDriveConstants;
if (_cooldown
&& drivetrain.getDriveData().coolantTemp <= drivetrainConfig.optimalTemp * CooldownStopOptimalTempRatio) {
_cooldown = false;
} else if (!_cooldown
&& drivetrain.getDriveData().coolantTemp > drivetrainConfig.maxTemp * CooldownStartMaxTempRatio) {
_cooldown = true;
randomizeTargetSpeed();
}
}
void SportDriveState::randomizeTargetSpeed()
{
const float halfSpeed = drivetrainConfig.maxSpeed * 0.5f;
if (drivetrain.getDriveData().speed > halfSpeed || _cooldown) {
_targetSpeed = randomize(SportDriveConstants::MinSpeed, halfSpeed);
} else {
_targetSpeed = randomize(halfSpeed, drivetrainConfig.maxSpeed);
}
}
} // namespace Simulation