Open FFBoard
Open source force feedback firmware
EffectsCalculator.cpp
Go to the documentation of this file.
1/*
2 * EffectsCalculator.cpp
3 *
4 * Created on: 27.01.21
5 * Author: Jon Lidgard, Yannick Richter, Vincent Manoukian
6 */
7
8#include <stdint.h>
9#include <math.h>
10#include "EffectsCalculator.h"
11#include "Axis.h"
12#include "ledEffects.h"
13
14
15#define EFFECT_STATE_INACTIVE 0
16
18 .name = "Effects" ,
19 .id = CLSID_EFFECTSCALC,
20 .visibility = ClassVisibility::hidden
21};
23 return info;
24}
25
27 Thread("FXCalc", EFFECT_THREAD_MEM, EFFECT_THREAD_PRIO)
28{
30
32 registerCommand("filterCfFreq", EffectsCalculator_commands::ffbfiltercf, "Constant force filter frequency", CMDFLAG_GET | CMDFLAG_SET);
33 registerCommand("filterCfQ", EffectsCalculator_commands::ffbfiltercf_q, "Constant force filter Q-factor", CMDFLAG_GET | CMDFLAG_SET);
34 registerCommand("spring", EffectsCalculator_commands::spring, "Spring gain", CMDFLAG_GET | CMDFLAG_SET | CMDFLAG_INFOSTRING);
35 registerCommand("friction", EffectsCalculator_commands::friction, "Friction gain", CMDFLAG_GET | CMDFLAG_SET | CMDFLAG_INFOSTRING);
36 registerCommand("damper", EffectsCalculator_commands::damper, "Damper gain", CMDFLAG_GET | CMDFLAG_SET | CMDFLAG_INFOSTRING);
37 registerCommand("inertia", EffectsCalculator_commands::inertia, "Inertia gain", CMDFLAG_GET | CMDFLAG_SET | CMDFLAG_INFOSTRING);
38 registerCommand("effects", EffectsCalculator_commands::effects, "USed effects since reset (Info print as str). set 0 to reset", CMDFLAG_GET | CMDFLAG_SET | CMDFLAG_INFOSTRING);
39 registerCommand("effectsDetails", EffectsCalculator_commands::effectsDetails, "List effects details. set 0 to reset", CMDFLAG_GET | CMDFLAG_SET | CMDFLAG_STR_ONLY | CMDFLAG_GETADR);
40 registerCommand("effectsForces", EffectsCalculator_commands::effectsForces, "List actual effects forces.", CMDFLAG_GET | CMDFLAG_GETADR);
41// registerCommand("monitorEffect", EffectsCalculator_commands::monitorEffect, "Get monitoring status. set to 1 to enable.", CMDFLAG_GET | CMDFLAG_SET);
42
43 registerCommand("damper_f", EffectsCalculator_commands::damper_f, "Damper biquad freq", CMDFLAG_GET | CMDFLAG_SET);
44 registerCommand("damper_q", EffectsCalculator_commands::damper_q, "Damper biquad q", CMDFLAG_GET | CMDFLAG_SET);
45 registerCommand("friction_f", EffectsCalculator_commands::friction_f, "Friction biquad freq", CMDFLAG_GET | CMDFLAG_SET);
46 registerCommand("friction_q", EffectsCalculator_commands::friction_q, "Friction biquad q", CMDFLAG_GET | CMDFLAG_SET);
47 registerCommand("inertia_f", EffectsCalculator_commands::inertia_f, "Inertia biquad freq", CMDFLAG_GET | CMDFLAG_SET);
48 registerCommand("inertia_q", EffectsCalculator_commands::inertia_q, "Inertia biquad q", CMDFLAG_GET | CMDFLAG_SET);
49 registerCommand("filterProfile_id", EffectsCalculator_commands::filterProfileId, "Conditional effects filter profile: 0 default; 1 custom", CMDFLAG_GET | CMDFLAG_SET);
50
51 registerCommand("frictionPctSpeedToRampup", EffectsCalculator_commands::frictionPctSpeedToRampup, "% of max speed for gradual increase", CMDFLAG_GET | CMDFLAG_SET);
52
53 //this->Start(); // Enable if we want to periodically monitor
54}
55
57{
58
59}
60
61
63{
64 return effects_active;
65}
67{
68 effects_active = active;
69 for (uint8_t i = 0; i < effects_stats.size(); i++)
70 {
71 effects_stats[i].current = {0}; // Reset active effect forces
72 effects_statslast[i].current = {0};
73 }
74 setClipLed(active);
75}
76
77
78/*
79If the metric is less than CP Offset - Dead Band, then the resulting force is given by the following formula:
80 force = Negative Coefficient * (q - (CP Offset – Dead Band))
81Similarly, if the metric is greater than CP Offset + Dead Band, then the resulting force is given by the
82following formula:
83 force = Positive Coefficient * (q - (CP Offset + Dead Band))
84A spring condition uses axis position as the metric.
85A damper condition uses axis velocity as the metric.
86An inertia condition uses axis acceleration as the metric.
87
88 */
89
95void EffectsCalculator::calculateEffects(std::vector<std::unique_ptr<Axis>> &axes)
96{
97 for (auto &axis : axes) {
98 axis->setEffectTorque(0);
99 axis->calculateAxisEffects(isActive());
100 }
101
102 if(!isActive()){
103 return;
104 }
105
106 int32_t force = 0;
107 int axisCount = axes.size();
108 int32_t forces[MAX_AXIS] = {0};
109
110 for (uint8_t i = 0; i < effects_stats.size(); i++)
111 {
112 effects_stats[i].current = {0}; // Reset active effect forces
113 }
114
115 for (uint8_t fxi = 0; fxi < MAX_EFFECTS; fxi++)
116 {
117 FFB_Effect *effect = &effects[fxi];
118
119 // Effect activated and not infinite (0 or 0xffff)
120 if (effect->state != EFFECT_STATE_INACTIVE && effect->duration != FFB_EFFECT_DURATION_INFINITE && effect->duration != 0){
121 // Start delay not yet reached
122 if(HAL_GetTick() < effect->startTime){
123 continue;
124 }
125 // If effect has expired make inactive
126 if (HAL_GetTick() - effect->startTime > effect->duration)
127 {
128 effect->state = EFFECT_STATE_INACTIVE;
129 for(uint8_t axis=0 ; axis < axisCount ; axis++)
130 calcStatsEffectType(effect->type, 0,axis); // record a 0 on the ended force
131 }
132 }
133
134 // Filter out inactive effects
135 if (effect->state == EFFECT_STATE_INACTIVE)
136 {
137 continue;
138 }
139
140 force = calcNonConditionEffectForce(effect);
141
142 for(uint8_t axis=0 ; axis < axisCount ; axis++) // Calculate effects for all axes
143 {
144 int32_t axisforce = calcComponentForce(effect, force, axes, axis);
145 calcStatsEffectType(effect->type, axisforce,axis);
146 forces[axis] += axisforce; // Do not clip yet to allow effects to subtract force correctly. Will not overflow as maxeffects * 0x7fff is less than int32 range
147 }
148 }
149
150 // Apply summed force to axes
151 for(uint8_t i=0 ; i < axisCount ; i++)
152 {
153 int32_t force = clip<int32_t, int32_t>(forces[i], -0x7fff, 0x7fff); // Clip
154 axes[i]->setEffectTorque(force);
155 }
156
158}
159
165 int32_t force_vector = 0;
166 int32_t magnitude = effect->magnitude;
167
168 // If using an envelope modulate the magnitude based on time
169 if(effect->useEnvelope){
170 magnitude = getEnvelopeMagnitude(effect);
171 }
172 switch (effect->type){
173
174 case FFB_EFFECT_CONSTANT:
175 { // Constant force is just the force
176 force_vector = (int32_t)magnitude;
177 break;
178 }
179
180 case FFB_EFFECT_RAMP:
181 {
182 uint32_t elapsed_time = HAL_GetTick() - effect->startTime;
183 int32_t duration = effect->duration;
184 force_vector = (int32_t)effect->startLevel + ((int32_t)elapsed_time * (effect->endLevel - effect->startLevel)) / duration;
185 break;
186 }
187
188 case FFB_EFFECT_SQUARE:
189 {
190 uint32_t elapsed_time = HAL_GetTick() - effect->startTime;
191 int32_t force = ((elapsed_time + effect->phase) % ((uint32_t)effect->period + 2)) < (uint32_t)(effect->period + 2) / 2 ? -magnitude : magnitude;
192 force_vector = force + effect->offset;
193 break;
194 }
195
196 case FFB_EFFECT_TRIANGLE:
197 {
198 int32_t force = 0;
199 int32_t offset = effect->offset;
200 uint32_t elapsed_time = HAL_GetTick() - effect->startTime;
201 uint32_t phase = effect->phase;
202 uint32_t period = effect->period;
203 float periodF = period;
204
205 int32_t maxMagnitude = offset + magnitude;
206 int32_t minMagnitude = offset - magnitude;
207 uint32_t phasetime = (phase * period) / 35999;
208 uint32_t timeTemp = elapsed_time + phasetime;
209 float remainder = timeTemp % period;
210 float slope = ((maxMagnitude - minMagnitude) * 2) / periodF;
211 if (remainder > (periodF / 2))
212 force = slope * (periodF - remainder);
213 else
214 force = slope * remainder;
215 force += minMagnitude;
216 force_vector = force;
217 break;
218 }
219
220 case FFB_EFFECT_SAWTOOTHUP:
221 {
222 float offset = effect->offset;
223 uint32_t elapsed_time = HAL_GetTick() - effect->startTime;
224 uint32_t phase = effect->phase;
225 uint32_t period = effect->period;
226 float periodF = effect->period;
227
228 float maxMagnitude = offset + magnitude;
229 float minMagnitude = offset - magnitude;
230 int32_t phasetime = (phase * period) / 35999;
231 uint32_t timeTemp = elapsed_time + phasetime;
232 float remainder = timeTemp % period;
233 float slope = (maxMagnitude - minMagnitude) / periodF;
234 force_vector = (int32_t)(minMagnitude + slope * (period - remainder));
235 break;
236 }
237
238 case FFB_EFFECT_SAWTOOTHDOWN:
239 {
240 float offset = effect->offset;
241 uint32_t elapsed_time = HAL_GetTick() - effect->startTime;
242 float phase = effect->phase;
243 uint32_t period = effect->period;
244 float periodF = effect->period;
245
246 float maxMagnitude = offset + magnitude;
247 float minMagnitude = offset - magnitude;
248 int32_t phasetime = (phase * period) / 35999;
249 uint32_t timeTemp = elapsed_time + phasetime;
250 float remainder = timeTemp % period;
251 float slope = (maxMagnitude - minMagnitude) / periodF;
252 force_vector = (int32_t)(minMagnitude + slope * (remainder)); // reverse time
253 break;
254 }
255
256 case FFB_EFFECT_SINE:
257 {
258 float t = HAL_GetTick() - effect->startTime;
259 float freq = 1.0f / (float)(std::max<uint16_t>(effect->period, 2));
260 float phase = (float)effect->phase / (float)35999; //degrees
261 float sine = sinf(2.0 * M_PI * (t * freq + phase)) * magnitude;
262 force_vector = (int32_t)(effect->offset + sine);
263 break;
264 }
265 default:
266 return 0;
267 break;
268 }
269
270 return (force_vector * effect->gain) / 255;
271}
272
273
274
279int32_t EffectsCalculator::calcComponentForce(FFB_Effect *effect, int32_t forceVector, std::vector<std::unique_ptr<Axis>> &axes, uint8_t axis)
280{
281 int32_t result_torque = 0;
282// uint16_t direction;
283 uint8_t con_idx = effect->useSingleCondition? 0 : axis; // condition block index
284
285 metric_t *metrics = axes[axis]->getMetrics();
286
287 float angle_ratio = effect->axisMagnitudes[axis];
288
289 switch (effect->type)
290 {
291 case FFB_EFFECT_CONSTANT:
292 {
293 // Optional filtering to reduce spikes
294 if(effect->filter[axis] != nullptr) {
295 // if the filter is enabled we apply it
296 if (effect->filter[axis]->getFc() < 0.5 && effect->filter[0]->getFc() != 0.0)
297 {
298 forceVector = effect->filter[axis]->process(forceVector);
299 }
300 }
301 }
302 // No break required here. The filter is a special preprocessing case for the constant force effect.
303 case FFB_EFFECT_RAMP:
304 case FFB_EFFECT_SQUARE:
305 case FFB_EFFECT_TRIANGLE:
306 case FFB_EFFECT_SAWTOOTHUP:
307 case FFB_EFFECT_SAWTOOTHDOWN:
308 case FFB_EFFECT_SINE:
309 {
310 result_torque = -forceVector * angle_ratio;
311 break;
312 }
313
314 case FFB_EFFECT_SPRING:
315 {
316 float pos = metrics->pos;
317 result_torque -= calcConditionEffectForce(effect, pos, gain.spring, con_idx, scaler.spring, angle_ratio);
318 break;
319 }
320
321
334 case FFB_EFFECT_FRICTION: // TODO sometimes unstable.
335 {
336 float speed = metrics->speed * INTERNAL_SCALER_FRICTION;
337
338 int16_t offset = effect->conditions[con_idx].cpOffset;
339 int16_t deadBand = effect->conditions[con_idx].deadBand;
340 int32_t force = 0;
341
342 float speedRampupCeil = speedRampupPct();
343
344 // Effect is only active outside deadband + offset
345 if (abs((int32_t)speed - offset) > deadBand){
346
347 // remove offset/deadband from metric to compute force
348 speed -= (offset + (deadBand * (speed < offset ? -1 : 1)) );
349
350 // check if speed is in the 0..x% to rampup, if is this range, apply a sinusoidale function to smooth the torque (slow near 0, slow around the X% rampup
351 float rampupFactor = 1.0;
352 if (fabs (speed) < speedRampupCeil) { // if speed in the range to rampup we apply a sinus curbe to ramup
353
354 float phaseRad = M_PI * ((fabs (speed) / speedRampupCeil) - 0.5);// we start to compute the normalized angle (speed / normalizedSpeed@5%) and translate it of -1/2PI to translate sin on 1/2 periode
355 rampupFactor = ( 1 + sin(phaseRad ) ) / 2; // sin value is -1..1 range, we translate it to 0..2 and we scale it by 2
356
357 }
358
359 int8_t sign = speed >= 0 ? 1 : -1;
360 uint16_t coeff = speed < 0 ? effect->conditions[con_idx].negativeCoefficient : effect->conditions[con_idx].positiveCoefficient;
361 force = coeff * rampupFactor * sign;
362
363 //if there is a saturation, used it to clip result
364 if (effect->conditions[con_idx].negativeSaturation !=0 || effect->conditions[con_idx].positiveSaturation !=0) {
365 force = clip<int32_t, int32_t>(force, -effect->conditions[con_idx].negativeSaturation, effect->conditions[con_idx].positiveSaturation);
366 }
367
368 result_torque -= effect->filter[axis]->process( (((gain.friction + 1) * force) >> 8) * angle_ratio * scaler.friction);
369 }
370
371 break;
372 }
373 case FFB_EFFECT_DAMPER:
374 {
375
376 float speed = metrics->speed * INTERNAL_SCALER_DAMPER;
377 result_torque -= effect->filter[axis]->process(calcConditionEffectForce(effect, speed, gain.damper, con_idx, scaler.damper, angle_ratio));
378
379 break;
380 }
381
382 case FFB_EFFECT_INERTIA:
383 {
384 float accel = metrics->accel * INTERNAL_SCALER_INERTIA;
385 result_torque -= effect->filter[axis]->process(calcConditionEffectForce(effect, accel, gain.inertia, con_idx, scaler.inertia, angle_ratio)); // Bump *60 the inertia feedback
386
387 break;
388 }
389
390 default:
391 // Unsupported effect
392 break;
393 }
394 return (result_torque * global_gain) / 255; // Apply global gain
395}
396
398 return (frictionPctSpeedToRampup / 100.0) * 32767; // compute the normalizedSpeed of pctToRampup factor
399}
400
401
407int32_t EffectsCalculator::calcConditionEffectForce(FFB_Effect *effect, float metric, uint8_t gain,
408 uint8_t idx, float scale, float angle_ratio)
409{
410 int16_t offset = effect->conditions[idx].cpOffset;
411 int16_t deadBand = effect->conditions[idx].deadBand;
412 int32_t force = 0;
413 float gainfactor = (float)(gain+1) / 256.0;
414
415 // Effect is only active outside deadband + offset
416 if (abs(metric - offset) > deadBand){
417 float coefficient = effect->conditions[idx].negativeCoefficient;
418 if(metric > offset){
419 coefficient = effect->conditions[idx].positiveCoefficient;
420 }
421 coefficient /= 0x7fff; // rescale the coefficient of effect
422
423 // remove offset/deadband from metric to compute force
424 metric = metric - (offset + (deadBand * (metric < offset ? -1 : 1)) );
425
426 force = clip<int32_t, int32_t>((coefficient * gainfactor * scale * (float)(metric)),
427 -effect->conditions[idx].negativeSaturation,
428 effect->conditions[idx].positiveSaturation);
429 }
430
431
432 return force * angle_ratio;
433}
434
442{
443 if(effect->duration == FFB_EFFECT_DURATION_INFINITE || effect->duration == 0){
444 return effect->magnitude; // Effect is infinite. envelope is invalid
445 }
446 int32_t scaler = abs(effect->magnitude);
447 uint32_t elapsed_time = HAL_GetTick() - effect->startTime;
448 if (elapsed_time < effect->attackTime && effect->attackTime != 0)
449 {
450 scaler = (scaler - effect->attackLevel) * elapsed_time;
451 scaler /= (int32_t)effect->attackTime;
452 scaler += effect->attackLevel;
453 }
454 if (elapsed_time > (effect->duration - effect->fadeTime) && effect->fadeTime != 0)
455 {
456 scaler = (scaler - effect->fadeLevel) * (effect->duration - elapsed_time); // Reversed
457 scaler /= (int32_t)effect->fadeTime;
458 scaler += effect->fadeLevel;
459 }
460 scaler = signbit(effect->magnitude) ? -scaler : scaler; // Follow original sign of magnitude because envelope has no sign (important for constant force)
461 return scaler;
462}
463
465
466 std::function<void(std::unique_ptr<Biquad> &)> fnptr = [=](std::unique_ptr<Biquad> &filter){};
467
468 switch (effect->type)
469 {
470 case FFB_EFFECT_DAMPER:
471 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
472 if (filter != nullptr)
473 filter->setBiquad(BiquadType::lowpass, this->filter[filterProfileId].damper.freq/ (float)calcfrequency, this->filter[filterProfileId].damper.q * qfloatScaler , (float)0.0);
474 else
475 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[filterProfileId].damper.freq / (float)calcfrequency, this->filter[filterProfileId].damper.q * qfloatScaler, (float)0.0);
476 };
477 break;
478 case FFB_EFFECT_FRICTION:
479 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
480 if (filter != nullptr)
482 else
483 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[filterProfileId].friction.freq / (float)calcfrequency, this->filter[filterProfileId].friction.q * qfloatScaler, (float)0.0);
484 };
485 break;
486 case FFB_EFFECT_INERTIA:
487 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
488 if (filter != nullptr)
489 filter->setBiquad(BiquadType::lowpass, this->filter[filterProfileId].inertia.freq / (float)calcfrequency, this->filter[filterProfileId].inertia.q * qfloatScaler, (float)0.0);
490 else
491 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[filterProfileId].inertia.freq / (float)calcfrequency, this->filter[filterProfileId].inertia.q * qfloatScaler, (float)0.0);
492 };
493 break;
494 case FFB_EFFECT_CONSTANT:
495 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
496 if (filter != nullptr)
497 filter->setBiquad(BiquadType::lowpass, this->filter[0].constant.freq / (float)calcfrequency, this->filter[0].constant.q * qfloatScaler, (float)0.0);
498 else
499 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[0].constant.freq / (float)calcfrequency, this->filter[0].constant.q * qfloatScaler, (float)0.0);
500 };
501 break;
502 }
503
504
505 for (int i=0; i<MAX_AXIS; i++) {
506 fnptr(effect->filter[i]);
507 }
508}
509
510
512{
514}
515
517
518
519
520/*
521 * Read parameters from flash and restore settings
522 */
524{
525 uint16_t filterStorage;
526 if (Flash_Read(ADR_FFB_CF_FILTER, &filterStorage))
527 {
528 uint32_t freq = filterStorage & 0x1FF;
529 uint8_t q = (filterStorage >> 9) & 0x7F;
530 checkFilterCoeff(&(this->filter[0].constant), freq, q);
531 updateFilterSettingsForEffects(FFB_EFFECT_CONSTANT);
532 }
533
534 if (Flash_Read(ADR_FFB_FR_FILTER, &filterStorage))
535 {
536 uint32_t freq = filterStorage & 0x1FF;
537 uint8_t q = (filterStorage >> 9) & 0x7F;
538 checkFilterCoeff(&(this->filter[CUSTOM_PROFILE_ID].friction), freq, q);
539 updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
540 }
541
542 if (Flash_Read(ADR_FFB_DA_FILTER, &filterStorage))
543 {
544 uint32_t freq = filterStorage & 0x1FF;
545 uint8_t q = (filterStorage >> 9) & 0x7F;
546 checkFilterCoeff(&(this->filter[CUSTOM_PROFILE_ID].damper), freq, q);
547 updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
548 }
549
550 if (Flash_Read(ADR_FFB_IN_FILTER, &filterStorage))
551 {
552 uint32_t freq = filterStorage & 0x1FF;
553 uint8_t q = (filterStorage >> 9) & 0x7F;
554 checkFilterCoeff(&(this->filter[CUSTOM_PROFILE_ID].inertia), freq, q);
555 updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
556 }
557
558 uint16_t effects = 0;
559 if(Flash_Read(ADR_FFB_EFFECTS1, &effects)){
560 gain.friction = (effects >> 8) & 0xff;
561 gain.inertia = (effects & 0xff);
562 }
563 if(Flash_Read(ADR_FFB_EFFECTS2, &effects)){
564 gain.damper = (effects >> 8) & 0xff;
565 gain.spring = (effects & 0xff);
566 }
567 if(Flash_Read(ADR_FFB_EFFECTS3, &effects)){
568 filterProfileId = (effects >> 8) & 0x03;
570 }
571
572}
573
574// Saves parameters to flash
576{
577 uint16_t filterStorage;
578
579 // save CF biquad
580 filterStorage = (uint16_t)filter[0].constant.freq & 0x1FF;
581 filterStorage |= ( (uint16_t)filter[0].constant.q & 0x7F ) << 9 ;
582 Flash_Write(ADR_FFB_CF_FILTER, filterStorage);
583
584 if(filterProfileId == CUSTOM_PROFILE_ID){ // Only attempt saving if custom profile active
585 // save Friction biquad
586 filterStorage = (uint16_t)filter[CUSTOM_PROFILE_ID].friction.freq & 0x1FF;
587 filterStorage |= ( (uint16_t)filter[CUSTOM_PROFILE_ID].friction.q & 0x7F ) << 9 ;
588 Flash_Write(ADR_FFB_FR_FILTER, filterStorage);
589
590 // save Damper biquad
591 filterStorage = (uint16_t)filter[CUSTOM_PROFILE_ID].damper.freq & 0x1FF;
592 filterStorage |= ( (uint16_t)filter[CUSTOM_PROFILE_ID].damper.q & 0x7F ) << 9 ;
593 Flash_Write(ADR_FFB_DA_FILTER, filterStorage);
594
595 // save Inertia biquad
596 filterStorage = (uint16_t)filter[CUSTOM_PROFILE_ID].inertia.freq & 0x1FF;
597 filterStorage |= ( (uint16_t)filter[CUSTOM_PROFILE_ID].inertia.q & 0x7F ) << 9 ;
598 Flash_Write(ADR_FFB_IN_FILTER, filterStorage);
599 }
600
601 // save the effect gain
602 uint16_t effects = gain.inertia | (gain.friction << 8);
603 Flash_Write(ADR_FFB_EFFECTS1, effects);
604
605 effects = gain.spring | (gain.damper << 8);
606 Flash_Write(ADR_FFB_EFFECTS2, effects);
607
608 // save the friction rampup zone
610 Flash_Write(ADR_FFB_EFFECTS3, effects);
611
612}
613
614void EffectsCalculator::checkFilterCoeff(biquad_constant_t *filter, uint32_t freq,uint8_t q)
615{
616 if(q == 0) {
617 q = 1;
618 }
619
620 if(freq == 0){
621 freq = calcfrequency / 2;
622 }
623
624 filter->freq = clip<uint32_t, uint32_t>(freq, 1, (calcfrequency / 2));
625 filter->q = clip<uint8_t, uint8_t>(q,0,127);
626}
627
629
630 // loop on all effect in memory and setup new constant filter
631 for (uint8_t i = 0; i < MAX_EFFECTS; i++)
632 {
633 if (effects[i].type == type_effect)
634 {
635 setFilters(&effects[i]);
636 }
637 }
638}
639
640
641void EffectsCalculator::logEffectType(uint8_t type,bool remove){
642 if(type > 0 && type < 32){
643
644 if(remove){
645 if(effects_stats[type-1].nb > 0)
646 effects_stats[type-1].nb--;
647
648 if(!effects_stats[type-1].nb){
649 //effects_used &= ~(1<<(type-1)); // Only manual reset
650 //effects_stats[type-1].max = 0;
651 effects_stats[type-1].current = {0};
652 }
653 }else{
654 effects_used |= 1<<(type-1);
655 if( effects_stats[type-1].nb < 65535 ) {
656 effects_stats[type-1].nb ++;
657 }
658 }
659 }
660}
661
662void EffectsCalculator::logEffectState(uint8_t type,uint8_t state){
663 if(type > 0 && type < 32){
664 if(!state){
665 // effects_stats[type-1].max = 0;
666 effects_stats[type-1].current = {0};
667 }
668 }
669}
670
671
672void EffectsCalculator::calcStatsEffectType(uint8_t type, int16_t force,uint8_t axis){
673 if(axis >= MAX_AXIS)
674 return;
675 if(type > 0 && type < 13) {
676 uint8_t arrayLocation = type - 1;
677 effects_stats[arrayLocation].current[axis] = clip<int32_t,int16_t>(effects_stats[arrayLocation].current[axis] + force, -0x7fff, 0x7fff);
678 effects_stats[arrayLocation].max[axis] = std::max(effects_stats[arrayLocation].max[axis], (int16_t)abs(force));
679 }
680}
681
687std::string EffectsCalculator::listEffectsUsed(bool details,uint8_t axis){
688 std::string effects_list = "";
689 if(axis >= MAX_AXIS)
690 return "";
691
692 if (!details) {
693 if(effects_used == 0){
694 return "None";
695 }
696
697 static const char *effects[12] = {"Constant,","Ramp,","Square,","Sine,","Triangle,","Sawtooth Up,","Sawtooth Down,","Spring,","Damper,","Inertia,","Friction,","Custom,"};
698
699 for (int i=0;i < 12; i++) {
700 if((effects_used >> i) & 1) {
701 effects_list += effects[i];
702 }
703 }
704
705 effects_list.pop_back();
706 } else {
707
708 bool firstItem = true;
709 for (int i=0;i < 12; i++) {
710 if (!firstItem) effects_list += ", ";
711 effects_list += "{\"max\":" + std::to_string(effects_stats[i].max[axis]);
712 effects_list += ", \"curr\":" + std::to_string(effects_stats[i].current[axis]);
713 effects_list += ", \"nb\":" + std::to_string(effects_stats[i].nb) + "}";
714 firstItem = false;
715 }
716
717 }
718
719 return effects_list.c_str();
720}
721
727 effects_used = 0;
728 if(reinit){
729 for (int i=0;i < 12; i++) {
730 if(effects_stats[i].nb > 0) {
731 effects_used |= 1<<(i);
732 }
733 }
734 }
735}
736
737CommandStatus EffectsCalculator::command(const ParsedCommand& cmd,std::vector<CommandReply>& replies){
738 switch(static_cast<EffectsCalculator_commands>(cmd.cmdId)){
739
741 if (cmd.type == CMDtype::get)
742 {
743 replies.emplace_back(filter[0].constant.freq);
744 }
745 else if (cmd.type == CMDtype::set)
746 {
747 checkFilterCoeff(&filter[0].constant, cmd.val, filter[0].constant.q);
748 updateFilterSettingsForEffects(FFB_EFFECT_CONSTANT);
749 }
750 break;
752 if(cmd.type == CMDtype::info){
753 replies.emplace_back("scale:"+std::to_string(this->qfloatScaler));
754 }
755 else if (cmd.type == CMDtype::get)
756 {
757 replies.emplace_back(filter[0].constant.q);
758 }
759 else if (cmd.type == CMDtype::set)
760 {
761 checkFilterCoeff(&filter[0].constant, filter[0].constant.freq, cmd.val);
762 updateFilterSettingsForEffects(FFB_EFFECT_CONSTANT);
763 }
764 break;
766 if (cmd.type == CMDtype::get)
767 {
768 replies.emplace_back(effects_used); //listEffectsUsed(cmd.val)
769 }
770 else if (cmd.type == CMDtype::set)
771 {
773 }
774 else if (cmd.type == CMDtype::info)
775 {
776 replies.emplace_back(listEffectsUsed(false));
777 }
778 break;
780 if (cmd.type == CMDtype::get || cmd.type == CMDtype::getat)
781 {
782 replies.emplace_back(listEffectsUsed(true,cmd.adr));
783 }
784 else if (cmd.type == CMDtype::set && cmd.val >= 0)
785 {
786 for (int i=0; i<12; i++) {
787 effects_stats[i].max = {0};
788 if(cmd.val > 0){
789 effects_stats[i].current = {0};
790 effects_stats[i].nb = 0;
791 }
792 }
794 }
795 break;
797 {
798 uint8_t axis = 0;
799 if(cmd.type == CMDtype::getat){
800 axis = std::min<uint8_t>(cmd.adr,MAX_AXIS);
801 }
802 if (cmd.type == CMDtype::get || cmd.type == CMDtype::getat)
803 {
804 for (size_t i=0; i < effects_statslast.size(); i++) {
805 replies.emplace_back(effects_statslast[i].current[axis],effects_statslast[i].nb);
806 }
807 }
808 break;
809 }
811 if(cmd.type == CMDtype::info){
812 replies.emplace_back("scale:"+std::to_string(this->scaler.spring));
813 }else
814 return handleGetSet(cmd, replies, this->gain.spring);
815 break;
817 if(cmd.type == CMDtype::info){
818 replies.emplace_back("scale:"+std::to_string(this->scaler.friction)+",factor:"+std::to_string(INTERNAL_SCALER_FRICTION));
819 }else
820 return handleGetSet(cmd, replies, this->gain.friction);
821 break;
823 if(cmd.type == CMDtype::info){
824 replies.emplace_back("scale:"+std::to_string(this->scaler.damper)+",factor:"+std::to_string(INTERNAL_SCALER_DAMPER));
825 }else
826 return handleGetSet(cmd, replies, this->gain.damper);
827 break;
829 if(cmd.type == CMDtype::info){
830 replies.emplace_back("scale:"+std::to_string(this->scaler.inertia)+",factor:"+std::to_string(INTERNAL_SCALER_INERTIA));
831 }else
832 return handleGetSet(cmd, replies, this->gain.inertia);
833 break;
835 if (cmd.type == CMDtype::get)
836 {
837 replies.emplace_back(filter[filterProfileId].damper.freq);
838 }
839 else if (cmd.type == CMDtype::set)
840 {
841 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].damper, cmd.val, filter[CUSTOM_PROFILE_ID].damper.q);
842 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
843 }
844 break;
846 if (cmd.type == CMDtype::get)
847 {
848 replies.emplace_back(filter[filterProfileId].damper.q);
849 }
850 else if (cmd.type == CMDtype::set)
851 {
852 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].damper, filter[CUSTOM_PROFILE_ID].damper.freq, cmd.val);
853 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
854 }
855 break;
857 if (cmd.type == CMDtype::get)
858 {
859 replies.emplace_back(filter[filterProfileId].friction.freq);
860 }
861 else if (cmd.type == CMDtype::set)
862 {
863 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].friction, cmd.val, filter[CUSTOM_PROFILE_ID].friction.q);
864 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
865 }
866 break;
868 if (cmd.type == CMDtype::get)
869 {
870 replies.emplace_back(filter[filterProfileId].friction.q);
871 }
872 else if (cmd.type == CMDtype::set)
873 {
874 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].friction, filter[CUSTOM_PROFILE_ID].friction.freq, cmd.val);
875 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
876 }
877 break;
879 if (cmd.type == CMDtype::get)
880 {
881 replies.emplace_back(filter[filterProfileId].inertia.freq);
882 }
883 else if (cmd.type == CMDtype::set)
884 {
885 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].inertia, cmd.val, filter[CUSTOM_PROFILE_ID].inertia.q);
886 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
887 }
888 break;
890 if (cmd.type == CMDtype::get)
891 {
892 replies.emplace_back(filter[filterProfileId].inertia.q);
893 }
894 else if (cmd.type == CMDtype::set)
895 {
896 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].inertia, filter[CUSTOM_PROFILE_ID].inertia.freq, cmd.val);
897 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
898 }
899 break;
900
902 if (cmd.type == CMDtype::get)
903 {
904 replies.emplace_back(frictionPctSpeedToRampup);
905 }
906 else if (cmd.type == CMDtype::set)
907 {
908 uint8_t pct = clip<uint8_t, uint8_t>(cmd.val, 0, 100);
910 }
911 break;
913 if (cmd.type == CMDtype::get)
914 {
915 replies.emplace_back(this->filterProfileId);
916 }
917 else if (cmd.type == CMDtype::set)
918 {
919 uint32_t value = clip<uint32_t, uint32_t>(cmd.val, 0, 1);
920 this->filterProfileId = value;
921 updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
922 updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
923 updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
924 }
925 break;
927 if (cmd.type == CMDtype::get)
928 {
929 replies.emplace_back(isMonitorEffect);
930 }
931 else if (cmd.type == CMDtype::set)
932 {
933 isMonitorEffect = clip<uint8_t, uint8_t>(cmd.val, 0, 1);
934 }
935 break;
936
937 default:
939 }
940 return CommandStatus::OK;
941}
942
943/*
944 *
945 */
947 std::vector<CommandReply> replies;
948 Delay(500);
949 while (true) {
950 Delay(3000);
951
952 if(isMonitorEffect) {
953
954 continue; // TODO uncomment when stream is ok
955 replies.push_back(CommandReply(listEffectsUsed(true)));
957 this,
960
961 }
962
963 }
964
965}
966
971 if(idx < this->effects.size()){
972 logEffectType(effects[idx].type, true); // Effect off
973 effects[idx] = FFB_Effect(); // Reset all settings
974 for(int i=0; i< MAX_AXIS; i++) {
975 if(effects[idx].filter[i] != nullptr){
976 effects[idx].filter[i].reset(nullptr);
977 }
978 }
979 }
980}
981
986 if(type > FFB_EFFECT_NONE && type < FFB_EFFECT_CUSTOM+1){ // Check if it is a valid effect type
987 for(uint8_t i=0;i<effects.size();i++){
988 if(effects[i].type == FFB_EFFECT_NONE){
989 return(i);
990 }
991 }
992 }
993 return -1;
994}
995
996
997
998
1003 float periodAvg = fxPeriodAvg.getAverage();
1004 if((HAL_GetTick() - lastFxUpdate) > 1000 || periodAvg == 0){
1005 // Reset average
1006 fxPeriodAvg.clear();
1007 return 0;
1008 }else{
1009 return (1000.0/periodAvg);
1010 }
1011}
1012
1017 float periodAvg = cfUpdatePeriodAvg.getAverage();
1018 if((HAL_GetTick() - lastCfUpdate) > 1000 || periodAvg == 0){
1019 // Reset average
1020 cfUpdatePeriodAvg.clear();
1021 return 0;
1022 }else{
1023 return (1000.0/periodAvg);
1024 }
1025}
1026
1027
1029 cfUpdatePeriodAvg.addValue((uint32_t)(HAL_GetTick() - lastCfUpdate));
1030 lastCfUpdate = HAL_GetTick();
1031}
1032
1034 fxPeriodAvg.addValue((uint32_t)(HAL_GetTick() - lastFxUpdate));
1035 lastFxUpdate = HAL_GetTick();
1036}
CommandStatus
EffectsCalculator_commands
void registerCommand(const char *cmd, const ID cmdid, const char *help=nullptr, uint32_t flags=0)
static CommandStatus handleGetSet(const ParsedCommand &cmd, std::vector< CommandReply > &replies, TVal &value)
static void broadcastCommandReplyAsync(const std::vector< CommandReply > &reply, CommandHandler *handler, uint32_t cmdId, CMDtype type=CMDtype::get)
void logEffectState(uint8_t type, uint8_t state)
const ClassIdentifier getInfo()
Command handlers always have class infos. Works well with ChoosableClass.
std::string listEffectsUsed(bool details=false, uint8_t axis=0)
void updateFilterSettingsForEffects(uint8_t type_effect)
int32_t calcComponentForce(FFB_Effect *effect, int32_t forceVector, std::vector< std::unique_ptr< Axis > > &axes, uint8_t axis)
int32_t find_free_effect(uint8_t type)
void setActive(bool active)
int32_t getEnvelopeMagnitude(FFB_Effect *effect)
void checkFilterCoeff(biquad_constant_t *filter, uint32_t freq, uint8_t q)
const float qfloatScaler
void calculateEffects(std::vector< std::unique_ptr< Axis > > &axes)
const uint32_t calcfrequency
int32_t calcConditionEffectForce(FFB_Effect *effect, float metric, uint8_t gain, uint8_t idx, float scale, float angle_ratio)
uint8_t frictionPctSpeedToRampup
void setGain(uint8_t gain)
CommandStatus command(const ParsedCommand &cmd, std::vector< CommandReply > &replies)
std::array< effect_stat_t, 12 > effects_stats
effect_biquad_t filter[2]
std::array< effect_stat_t, 12 > effects_statslast
static ClassIdentifier info
int32_t calcNonConditionEffectForce(FFB_Effect *effect)
std::array< FFB_Effect, max_effects > effects
virtual void setFilters(FFB_Effect *effect)
void logEffectType(uint8_t type, bool remove=false)
void resetLoggedActiveEffects(bool reinit)
effect_scaler_t scaler
void calcStatsEffectType(uint8_t type, int16_t force, uint8_t axis)
void free_effect(uint16_t idx)
virtual uint32_t getConstantForceRate()
virtual void fxUpdateEvent()
virtual void cfUpdateEvent()
FastMovingAverage< float > fxPeriodAvg
FastMovingAverage< float > cfUpdatePeriodAvg
virtual uint32_t getRate()
void Delay(const TickType_t Delay)
Definition: thread.hpp:352
bool Flash_Write(uint16_t adr, uint16_t dat)
bool Flash_Read(uint16_t adr, uint16_t *buf, bool checkempty=true)
void setClipLed(uint8_t on)
Definition: ledEffects.cpp:70
const char * name
uint16_t negativeSaturation
Definition: ffb_defs.h:254
int16_t negativeCoefficient
Definition: ffb_defs.h:252
int16_t positiveCoefficient
Definition: ffb_defs.h:251
uint16_t positiveSaturation
Definition: ffb_defs.h:253
uint16_t period
Definition: ffb_defs.h:284
bool useEnvelope
Definition: ffb_defs.h:293
int16_t startLevel
Definition: ffb_defs.h:278
uint8_t type
Definition: ffb_defs.h:274
bool useSingleCondition
Definition: ffb_defs.h:294
uint8_t gain
Definition: ffb_defs.h:276
std::unique_ptr< Biquad > filter[MAX_AXIS]
Definition: ffb_defs.h:289
uint32_t duration
Definition: ffb_defs.h:285
int16_t magnitude
Definition: ffb_defs.h:277
uint32_t attackTime
Definition: ffb_defs.h:287
float axisMagnitudes[MAX_AXIS]
Definition: ffb_defs.h:280
FFB_Effect_Condition conditions[MAX_AXIS]
Definition: ffb_defs.h:282
uint16_t attackLevel
Definition: ffb_defs.h:286
int16_t endLevel
Definition: ffb_defs.h:279
volatile uint8_t state
Definition: ffb_defs.h:273
uint32_t startTime
Definition: ffb_defs.h:291
uint32_t fadeTime
Definition: ffb_defs.h:287
int16_t offset
Definition: ffb_defs.h:275
uint16_t fadeLevel
Definition: ffb_defs.h:286
int16_t phase
Definition: ffb_defs.h:283
uint32_t cmdId
biquad_constant_t constant
biquad_constant_t damper
biquad_constant_t friction
biquad_constant_t inertia
Definition: Axis.h:70
float accel
Definition: Axis.h:71
float speed
Definition: Axis.h:72
int32_t pos
Definition: Axis.h:73