Open FFBoard
Open source force feedback firmware
Loading...
Searching...
No Matches
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};
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
60
61
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
77void EffectsCalculator::updateSamplerate(float newSamplerate){
78 this->calcfrequency = newSamplerate;
79 for(FFB_Effect &effect : this->effects){
80 if(effect.filter[0]){ // Update filters if effect has filters
81 setFilters(&effect);
82 }
83 }
84}
85
86
87/*
88If the metric is less than CP Offset - Dead Band, then the resulting force is given by the following formula:
89 force = Negative Coefficient * (q - (CP Offset – Dead Band))
90Similarly, if the metric is greater than CP Offset + Dead Band, then the resulting force is given by the
91following formula:
92 force = Positive Coefficient * (q - (CP Offset + Dead Band))
93A spring condition uses axis position as the metric.
94A damper condition uses axis velocity as the metric.
95An inertia condition uses axis acceleration as the metric.
96
97 */
98
104void EffectsCalculator::calculateEffects(std::vector<std::unique_ptr<Axis>> &axes)
105{
106 for (auto &axis : axes) {
107 axis->setEffectTorque(0);
108 axis->calculateAxisEffects(isActive());
109 }
110
111 if(!isActive()){
112 return;
113 }
114
115 int32_t force = 0;
116 int axisCount = axes.size();
117 int32_t forces[MAX_AXIS] = {0};
118
119 for (uint8_t i = 0; i < effects_stats.size(); i++)
120 {
121 effects_stats[i].current = {0}; // Reset active effect forces
122 }
123
124 for (uint8_t fxi = 0; fxi < MAX_EFFECTS; fxi++)
125 {
126 FFB_Effect *effect = &effects[fxi];
127
128 // Effect activated and not infinite (0 or 0xffff)
129 if (effect->state != EFFECT_STATE_INACTIVE && effect->duration != FFB_EFFECT_DURATION_INFINITE && effect->duration != 0){
130 // Start delay not yet reached
131 if(HAL_GetTick() < effect->startTime){
132 continue;
133 }
134 // If effect has expired make inactive
135 if (HAL_GetTick() - effect->startTime > effect->duration)
136 {
137 effect->state = EFFECT_STATE_INACTIVE;
138 for(uint8_t axis=0 ; axis < axisCount ; axis++)
139 calcStatsEffectType(effect->type, 0,axis); // record a 0 on the ended force
140 }
141 }
142
143 // Filter out inactive effects
144 if (effect->state == EFFECT_STATE_INACTIVE)
145 {
146 continue;
147 }
148
149 force = calcNonConditionEffectForce(effect);
150
151 for(uint8_t axis=0 ; axis < axisCount ; axis++) // Calculate effects for all axes
152 {
153 int32_t axisforce = calcComponentForce(effect, force, axes, axis);
154 calcStatsEffectType(effect->type, axisforce,axis);
155 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
156 }
157 }
158
159 // Apply summed force to axes
160 for(uint8_t i=0 ; i < axisCount ; i++)
161 {
162 int32_t force = clip<int32_t, int32_t>(forces[i], -0x7fff, 0x7fff); // Clip
163 axes[i]->setEffectTorque(force);
164 }
165
167}
168
174 int32_t force_vector = 0;
175 int32_t magnitude = effect->magnitude;
176
177 // If using an envelope modulate the magnitude based on time
178 if(effect->useEnvelope){
179 magnitude = getEnvelopeMagnitude(effect);
180 }
181 switch (effect->type){
182
183 case FFB_EFFECT_CONSTANT:
184 { // Constant force is just the force
185 force_vector = (int32_t)magnitude;
186 break;
187 }
188
189 case FFB_EFFECT_RAMP:
190 {
191 float elapsed_time = (micros()/1000.0) - (float)effect->startTime;
192 int32_t duration = effect->duration;
193 force_vector = (int32_t)effect->startLevel + (elapsed_time * (effect->endLevel - effect->startLevel)) / duration;
194 break;
195 }
196
197 case FFB_EFFECT_SQUARE:
198 {
199 uint32_t elapsed_time = HAL_GetTick() - effect->startTime; // Square is ms aligned
200 int32_t force = ((elapsed_time + effect->phase) % ((uint32_t)effect->period + 2)) < (uint32_t)(effect->period + 2) / 2 ? -magnitude : magnitude;
201 force_vector = force + effect->offset;
202 break;
203 }
204
205 case FFB_EFFECT_TRIANGLE:
206 {
207 int32_t force = 0;
208 int32_t offset = effect->offset;
209 float elapsed_time = micros() - ((float)effect->startTime*1000.0);
210 uint32_t phase = effect->phase;
211 uint32_t period = effect->period;
212 float periodF = period;
213
214 int32_t maxMagnitude = offset + magnitude;
215 int32_t minMagnitude = offset - magnitude;
216 float phasetime = (phase * period) / 35999.0;
217 uint32_t timeTemp = elapsed_time + (phasetime*1000); // timetemp in µs
218 float remainder = (timeTemp % (period*1000)) / 1000;
219 float slope = ((maxMagnitude - minMagnitude) * 2) / periodF;
220 if (remainder > (periodF / 2))
221 force = slope * (periodF - remainder);
222 else
223 force = slope * remainder;
224 force += minMagnitude;
225 force_vector = force;
226 break;
227 }
228
229 case FFB_EFFECT_SAWTOOTHUP:
230 {
231 float offset = effect->offset;
232 float elapsed_time = micros() - ((float)effect->startTime*1000.0);
233 uint32_t phase = effect->phase;
234 uint32_t period = effect->period;
235 float periodF = effect->period;
236
237 float maxMagnitude = offset + magnitude;
238 float minMagnitude = offset - magnitude;
239 float phasetime = (phase * period) / 35999.0;
240 uint32_t timeTemp = elapsed_time + (phasetime*1000); // timetemp in µs
241 float remainder = (timeTemp % (period*1000)) / 1000;
242 float slope = (maxMagnitude - minMagnitude) / periodF;
243 force_vector = (int32_t)(minMagnitude + slope * (period - remainder));
244 break;
245 }
246
247 case FFB_EFFECT_SAWTOOTHDOWN:
248 {
249 float offset = effect->offset;
250 float elapsed_time = micros() - ((float)effect->startTime*1000.0);
251 float phase = effect->phase;
252 uint32_t period = effect->period;
253 float periodF = effect->period;
254
255 float maxMagnitude = offset + magnitude;
256 float minMagnitude = offset - magnitude;
257 float phasetime = (phase * period) / 35999.0;
258 uint32_t timeTemp = elapsed_time + (phasetime*1000); // timetemp in µs
259 float remainder = (timeTemp % (period*1000)) / 1000;
260 float slope = (maxMagnitude - minMagnitude) / periodF;
261 force_vector = (int32_t)(minMagnitude + slope * (remainder)); // reverse time
262 break;
263 }
264
265 case FFB_EFFECT_SINE:
266 {
267 float t = (micros()/1000.0) - (float)effect->startTime;
268 float freq = 1.0f / (float)(std::max<uint16_t>(effect->period, 2));
269 float phase = (float)effect->phase / (float)35999; //degrees
270 float sine = sinf(2.0 * M_PI * (t * freq + phase)) * magnitude;
271 force_vector = (int32_t)(effect->offset + sine);
272 break;
273 }
274 default:
275 return 0;
276 break;
277 }
278
279 return (force_vector * effect->gain) / 255;
280}
281
282
283
287
288int32_t EffectsCalculator::calcComponentForce(FFB_Effect *effect, int32_t forceVector, std::vector<std::unique_ptr<Axis>> &axes, uint8_t axis)
289{
290 int32_t result_torque = 0;
291// uint16_t direction;
292 uint8_t con_idx = effect->useSingleCondition? 0 : axis; // condition block index
293
294 metric_t *metrics = axes[axis]->getMetrics();
295
296 float angle_ratio = effect->axisMagnitudes[axis];
297
298 switch (effect->type)
299 {
300 case FFB_EFFECT_CONSTANT:
301 {
302 // Optional filtering to reduce spikes
303 if(effect->filter[axis] != nullptr) {
304 // if the filter is enabled we apply it
305 if (effect->filter[axis]->getFc() < 0.5 && effect->filter[0]->getFc() != 0.0)
306 {
307 forceVector = effect->filter[axis]->process(forceVector);
308 }
309 }
310 }
311 // No break required here. The filter is a special preprocessing case for the constant force effect.
312 case FFB_EFFECT_RAMP:
313 case FFB_EFFECT_SQUARE:
314 case FFB_EFFECT_TRIANGLE:
315 case FFB_EFFECT_SAWTOOTHUP:
316 case FFB_EFFECT_SAWTOOTHDOWN:
317 case FFB_EFFECT_SINE:
318 {
319 result_torque = -forceVector * angle_ratio;
320 break;
321 }
322
323 case FFB_EFFECT_SPRING:
324 {
325 float pos = metrics->pos_scaled_16b;
326 result_torque -= calcConditionEffectForce(effect, pos, gain.spring, con_idx, scaler.spring, angle_ratio);
327 break;
328 }
329
330
343 case FFB_EFFECT_FRICTION: // TODO sometimes unstable.
344 {
345 float speed = metrics->speed * INTERNAL_SCALER_FRICTION;
346
347 int16_t offset = effect->conditions[con_idx].cpOffset;
348 int16_t deadBand = effect->conditions[con_idx].deadBand;
349 int32_t force = 0;
350
351 float speedRampupCeil = speedRampupPct();
352
353 // Effect is only active outside deadband + offset
354 if (abs((int32_t)speed - offset) > deadBand){
355
356 // remove offset/deadband from metric to compute force
357 speed -= (offset + (deadBand * (speed < offset ? -1 : 1)) );
358
359 // 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
360 float rampupFactor = 1.0;
361 if (fabs (speed) < speedRampupCeil) { // if speed in the range to rampup we apply a sinus curbe to ramup
362
363 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
364 rampupFactor = ( 1 + sin(phaseRad ) ) / 2; // sin value is -1..1 range, we translate it to 0..2 and we scale it by 2
365
366 }
367
368 int8_t sign = speed >= 0 ? 1 : -1;
369 uint16_t coeff = speed < 0 ? effect->conditions[con_idx].negativeCoefficient : effect->conditions[con_idx].positiveCoefficient;
370 force = coeff * rampupFactor * sign;
371
372 //if there is a saturation, used it to clip result
373 if (effect->conditions[con_idx].negativeSaturation !=0 || effect->conditions[con_idx].positiveSaturation !=0) {
374 force = clip<int32_t, int32_t>(force, -effect->conditions[con_idx].negativeSaturation, effect->conditions[con_idx].positiveSaturation);
375 }
376
377 result_torque -= effect->filter[axis]->process( (((gain.friction + 1) * force) >> 8) * angle_ratio * scaler.friction);
378 }
379
380 break;
381 }
382 case FFB_EFFECT_DAMPER:
383 {
384
385 float speed = metrics->speed * INTERNAL_SCALER_DAMPER;
386 result_torque -= effect->filter[axis]->process(calcConditionEffectForce(effect, speed, gain.damper, con_idx, scaler.damper, angle_ratio));
387
388 break;
389 }
390
391 case FFB_EFFECT_INERTIA:
392 {
393 float accel = metrics->accel * INTERNAL_SCALER_INERTIA;
394 result_torque -= effect->filter[axis]->process(calcConditionEffectForce(effect, accel, gain.inertia, con_idx, scaler.inertia, angle_ratio)); // Bump *60 the inertia feedback
395
396 break;
397 }
398
399 default:
400 // Unsupported effect
401 break;
402 }
403 return (result_torque * global_gain) / 255; // Apply global gain
404}
405
407 return (frictionPctSpeedToRampup / 100.0) * 32767; // compute the normalizedSpeed of pctToRampup factor
408}
409
410
416int32_t EffectsCalculator::calcConditionEffectForce(FFB_Effect *effect, float metric, uint8_t gain,
417 uint8_t idx, float scale, float angle_ratio)
418{
419 int16_t offset = effect->conditions[idx].cpOffset;
420 int16_t deadBand = effect->conditions[idx].deadBand;
421 int32_t force = 0;
422 float gainfactor = (float)(gain+1) / 256.0;
423
424 // Effect is only active outside deadband + offset
425 if (abs(metric - offset) > deadBand){
426 float coefficient = effect->conditions[idx].negativeCoefficient;
427 if(metric > offset){
428 coefficient = effect->conditions[idx].positiveCoefficient;
429 }
430 coefficient /= 0x7fff; // rescale the coefficient of effect
431
432 // remove offset/deadband from metric to compute force
433 metric = metric - (offset + (deadBand * (metric < offset ? -1 : 1)) );
434
435 force = clip<int32_t, int32_t>((coefficient * gainfactor * scale * (float)(metric)),
436 -effect->conditions[idx].negativeSaturation,
437 effect->conditions[idx].positiveSaturation);
438 }
439
440
441 return force * angle_ratio;
442}
443
451{
452 if(effect->duration == FFB_EFFECT_DURATION_INFINITE || effect->duration == 0){
453 return effect->magnitude; // Effect is infinite. envelope is invalid
454 }
455 int32_t scaler = abs(effect->magnitude);
456 uint32_t elapsed_time = HAL_GetTick() - effect->startTime;
457 if (elapsed_time < effect->attackTime && effect->attackTime != 0)
458 {
459 scaler = (scaler - effect->attackLevel) * elapsed_time;
460 scaler /= (int32_t)effect->attackTime;
461 scaler += effect->attackLevel;
462 }
463 if (elapsed_time > (effect->duration - effect->fadeTime) && effect->fadeTime != 0)
464 {
465 scaler = (scaler - effect->fadeLevel) * (effect->duration - elapsed_time); // Reversed
466 scaler /= (int32_t)effect->fadeTime;
467 scaler += effect->fadeLevel;
468 }
469 scaler = signbit(effect->magnitude) ? -scaler : scaler; // Follow original sign of magnitude because envelope has no sign (important for constant force)
470 return scaler;
471}
472
474
475 std::function<void(std::unique_ptr<Biquad> &)> fnptr = [=](std::unique_ptr<Biquad> &filter){};
476
477 switch (effect->type)
478 {
479 case FFB_EFFECT_DAMPER:
480 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
481 if (filter != nullptr)
482 filter->setBiquad(BiquadType::lowpass, this->filter[filterProfileId].damper.freq/ (float)calcfrequency, this->filter[filterProfileId].damper.q * qfloatScaler , (float)0.0);
483 else
484 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[filterProfileId].damper.freq / (float)calcfrequency, this->filter[filterProfileId].damper.q * qfloatScaler, (float)0.0);
485 };
486 break;
487 case FFB_EFFECT_FRICTION:
488 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
489 if (filter != nullptr)
490 filter->setBiquad(BiquadType::lowpass, this->filter[filterProfileId].friction.freq / (float)calcfrequency, this->filter[filterProfileId].friction.q * qfloatScaler, (float)0.0);
491 else
492 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[filterProfileId].friction.freq / (float)calcfrequency, this->filter[filterProfileId].friction.q * qfloatScaler, (float)0.0);
493 };
494 break;
495 case FFB_EFFECT_INERTIA:
496 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
497 if (filter != nullptr)
498 filter->setBiquad(BiquadType::lowpass, this->filter[filterProfileId].inertia.freq / (float)calcfrequency, this->filter[filterProfileId].inertia.q * qfloatScaler, (float)0.0);
499 else
500 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[filterProfileId].inertia.freq / (float)calcfrequency, this->filter[filterProfileId].inertia.q * qfloatScaler, (float)0.0);
501 };
502 break;
503 case FFB_EFFECT_CONSTANT:
504 fnptr = [=, this](std::unique_ptr<Biquad> &filter){
505 if (filter != nullptr)
506 filter->setBiquad(BiquadType::lowpass, this->filter[0].constant.freq / (float)calcfrequency, this->filter[0].constant.q * qfloatScaler, (float)0.0);
507 else
508 filter = std::make_unique<Biquad>(BiquadType::lowpass, this->filter[0].constant.freq / (float)calcfrequency, this->filter[0].constant.q * qfloatScaler, (float)0.0);
509 };
510 break;
511 }
512
513
514 for (int i=0; i<MAX_AXIS; i++) {
515 fnptr(effect->filter[i]);
516 }
517}
518
519
521{
523}
524
526
527
528
529/*
530 * Read parameters from flash and restore settings
531 */
533{
534 uint16_t filterStorage;
535 if (Flash_Read(ADR_FFB_CF_FILTER, &filterStorage))
536 {
537 uint32_t freq = filterStorage & 0x1FF;
538 uint8_t q = (filterStorage >> 9) & 0x7F;
539 checkFilterCoeff(&(this->filter[0].constant), freq, q);
540 updateFilterSettingsForEffects(FFB_EFFECT_CONSTANT);
541 }
542
543 if (Flash_Read(ADR_FFB_FR_FILTER, &filterStorage))
544 {
545 uint32_t freq = filterStorage & 0x1FF;
546 uint8_t q = (filterStorage >> 9) & 0x7F;
547 checkFilterCoeff(&(this->filter[CUSTOM_PROFILE_ID].friction), freq, q);
548 updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
549 }
550
551 if (Flash_Read(ADR_FFB_DA_FILTER, &filterStorage))
552 {
553 uint32_t freq = filterStorage & 0x1FF;
554 uint8_t q = (filterStorage >> 9) & 0x7F;
555 checkFilterCoeff(&(this->filter[CUSTOM_PROFILE_ID].damper), freq, q);
556 updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
557 }
558
559 if (Flash_Read(ADR_FFB_IN_FILTER, &filterStorage))
560 {
561 uint32_t freq = filterStorage & 0x1FF;
562 uint8_t q = (filterStorage >> 9) & 0x7F;
563 checkFilterCoeff(&(this->filter[CUSTOM_PROFILE_ID].inertia), freq, q);
564 updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
565 }
566
567 uint16_t effects = 0;
568 if(Flash_Read(ADR_FFB_EFFECTS1, &effects)){
569 gain.friction = (effects >> 8) & 0xff;
570 gain.inertia = (effects & 0xff);
571 }
572 if(Flash_Read(ADR_FFB_EFFECTS2, &effects)){
573 gain.damper = (effects >> 8) & 0xff;
574 gain.spring = (effects & 0xff);
575 }
576 if(Flash_Read(ADR_FFB_EFFECTS3, &effects)){
577 filterProfileId = (effects >> 8) & 0x03;
579 }
580
581}
582
583// Saves parameters to flash
585{
586 uint16_t filterStorage;
587
588 // save CF biquad
589 filterStorage = (uint16_t)filter[0].constant.freq & 0x1FF;
590 filterStorage |= ( (uint16_t)filter[0].constant.q & 0x7F ) << 9 ;
591 Flash_Write(ADR_FFB_CF_FILTER, filterStorage);
592
593 if(filterProfileId == CUSTOM_PROFILE_ID){ // Only attempt saving if custom profile active
594 // save Friction biquad
595 filterStorage = (uint16_t)filter[CUSTOM_PROFILE_ID].friction.freq & 0x1FF;
596 filterStorage |= ( (uint16_t)filter[CUSTOM_PROFILE_ID].friction.q & 0x7F ) << 9 ;
597 Flash_Write(ADR_FFB_FR_FILTER, filterStorage);
598
599 // save Damper biquad
600 filterStorage = (uint16_t)filter[CUSTOM_PROFILE_ID].damper.freq & 0x1FF;
601 filterStorage |= ( (uint16_t)filter[CUSTOM_PROFILE_ID].damper.q & 0x7F ) << 9 ;
602 Flash_Write(ADR_FFB_DA_FILTER, filterStorage);
603
604 // save Inertia biquad
605 filterStorage = (uint16_t)filter[CUSTOM_PROFILE_ID].inertia.freq & 0x1FF;
606 filterStorage |= ( (uint16_t)filter[CUSTOM_PROFILE_ID].inertia.q & 0x7F ) << 9 ;
607 Flash_Write(ADR_FFB_IN_FILTER, filterStorage);
608 }
609
610 // save the effect gain
611 uint16_t effects = gain.inertia | (gain.friction << 8);
612 Flash_Write(ADR_FFB_EFFECTS1, effects);
613
614 effects = gain.spring | (gain.damper << 8);
615 Flash_Write(ADR_FFB_EFFECTS2, effects);
616
617 // save the friction rampup zone
619 Flash_Write(ADR_FFB_EFFECTS3, effects);
620
621}
622
624{
625 if(q == 0) {
626 q = 1;
627 }
628
629 if(freq == 0){
630 freq = calcfrequency / 2;
631 }
632
633 filter->freq = clip<uint32_t, uint32_t>(freq, 1, (calcfrequency / 2));
634 filter->q = clip<uint8_t, uint8_t>(q,0,127);
635}
636
638
639 // loop on all effect in memory and setup new constant filter
640 for (uint8_t i = 0; i < MAX_EFFECTS; i++)
641 {
642 if (effects[i].type == type_effect)
643 {
644 setFilters(&effects[i]);
645 }
646 }
647}
648
649
650void EffectsCalculator::logEffectType(uint8_t type,bool remove){
651 if(type > 0 && type < 32){
652
653 if(remove){
654 if(effects_stats[type-1].nb > 0)
655 effects_stats[type-1].nb--;
656
657 if(!effects_stats[type-1].nb){
658 //effects_used &= ~(1<<(type-1)); // Only manual reset
659 //effects_stats[type-1].max = 0;
660 effects_stats[type-1].current = {0};
661 }
662 }else{
663 effects_used |= 1<<(type-1);
664 if( effects_stats[type-1].nb < 65535 ) {
665 effects_stats[type-1].nb ++;
666 }
667 }
668 }
669}
670
671void EffectsCalculator::logEffectState(uint8_t type,uint8_t state){
672 if(type > 0 && type < 32){
673 if(!state){
674 // effects_stats[type-1].max = 0;
675 effects_stats[type-1].current = {0};
676 }
677 }
678}
679
680
681void EffectsCalculator::calcStatsEffectType(uint8_t type, int32_t force,uint8_t axis){
682 if(axis >= MAX_AXIS)
683 return;
684 if(type > 0 && type < 13) {
685 uint8_t arrayLocation = type - 1;
686 effects_stats[arrayLocation].current[axis] = clip<int32_t,int32_t>(effects_stats[arrayLocation].current[axis] + force, -0x7fff, 0x7fff);
687 effects_stats[arrayLocation].max[axis] = std::max(effects_stats[arrayLocation].max[axis], (int16_t)abs(force));
688 }
689}
690
696std::string EffectsCalculator::listEffectsUsed(bool details,uint8_t axis){
697 std::string effects_list = "";
698 if(axis >= MAX_AXIS)
699 return "";
700
701 if (!details) {
702 if(effects_used == 0){
703 return "None";
704 }
705
706 static const char *effects[12] = {"Constant,","Ramp,","Square,","Sine,","Triangle,","Sawtooth Up,","Sawtooth Down,","Spring,","Damper,","Inertia,","Friction,","Custom,"};
707
708 for (int i=0;i < 12; i++) {
709 if((effects_used >> i) & 1) {
710 effects_list += effects[i];
711 }
712 }
713
714 effects_list.pop_back();
715 } else {
716
717 bool firstItem = true;
718 for (int i=0;i < 12; i++) {
719 if (!firstItem) effects_list += ", ";
720 effects_list += "{\"max\":" + std::to_string(effects_stats[i].max[axis]);
721 effects_list += ", \"curr\":" + std::to_string(effects_stats[i].current[axis]);
722 effects_list += ", \"nb\":" + std::to_string(effects_stats[i].nb) + "}";
723 firstItem = false;
724 }
725
726 }
727
728 return effects_list.c_str();
729}
730
736 effects_used = 0;
737 if(reinit){
738 for (int i=0;i < 12; i++) {
739 if(effects_stats[i].nb > 0) {
740 effects_used |= 1<<(i);
741 }
742 }
743 }
744}
745
746CommandStatus EffectsCalculator::command(const ParsedCommand& cmd,std::vector<CommandReply>& replies){
747 switch(static_cast<EffectsCalculator_commands>(cmd.cmdId)){
748
750 if (cmd.type == CMDtype::get)
751 {
752 replies.emplace_back(filter[0].constant.freq);
753 }
754 else if (cmd.type == CMDtype::set)
755 {
756 checkFilterCoeff(&filter[0].constant, cmd.val, filter[0].constant.q);
757 updateFilterSettingsForEffects(FFB_EFFECT_CONSTANT);
758 }
759 break;
761 if(cmd.type == CMDtype::info){
762 replies.emplace_back("scale:"+std::to_string(this->qfloatScaler));
763 }
764 else if (cmd.type == CMDtype::get)
765 {
766 replies.emplace_back(filter[0].constant.q);
767 }
768 else if (cmd.type == CMDtype::set)
769 {
770 checkFilterCoeff(&filter[0].constant, filter[0].constant.freq, cmd.val);
771 updateFilterSettingsForEffects(FFB_EFFECT_CONSTANT);
772 }
773 break;
775 if (cmd.type == CMDtype::get)
776 {
777 replies.emplace_back(effects_used); //listEffectsUsed(cmd.val)
778 }
779 else if (cmd.type == CMDtype::set)
780 {
782 }
783 else if (cmd.type == CMDtype::info)
784 {
785 replies.emplace_back(listEffectsUsed(false));
786 }
787 break;
789 if (cmd.type == CMDtype::get || cmd.type == CMDtype::getat)
790 {
791 replies.emplace_back(listEffectsUsed(true,cmd.adr));
792 }
793 else if (cmd.type == CMDtype::set && cmd.val >= 0)
794 {
795 for (int i=0; i<12; i++) {
796 effects_stats[i].max = {0};
797 if(cmd.val > 0){
798 effects_stats[i].current = {0};
799 effects_stats[i].nb = 0;
800 }
801 }
803 }
804 break;
806 {
807 uint8_t axis = 0;
808 if(cmd.type == CMDtype::getat){
809 axis = std::min<uint8_t>(cmd.adr,MAX_AXIS);
810 }
811 if (cmd.type == CMDtype::get || cmd.type == CMDtype::getat)
812 {
813 for (size_t i=0; i < effects_statslast.size(); i++) {
814 replies.emplace_back(effects_statslast[i].current[axis],effects_statslast[i].nb);
815 }
816 }
817 break;
818 }
820 if(cmd.type == CMDtype::info){
821 replies.emplace_back("scale:"+std::to_string(this->scaler.spring));
822 }else
823 return handleGetSet(cmd, replies, this->gain.spring);
824 break;
826 if(cmd.type == CMDtype::info){
827 replies.emplace_back("scale:"+std::to_string(this->scaler.friction)+",factor:"+std::to_string(INTERNAL_SCALER_FRICTION));
828 }else
829 return handleGetSet(cmd, replies, this->gain.friction);
830 break;
832 if(cmd.type == CMDtype::info){
833 replies.emplace_back("scale:"+std::to_string(this->scaler.damper)+",factor:"+std::to_string(INTERNAL_SCALER_DAMPER));
834 }else
835 return handleGetSet(cmd, replies, this->gain.damper);
836 break;
838 if(cmd.type == CMDtype::info){
839 replies.emplace_back("scale:"+std::to_string(this->scaler.inertia)+",factor:"+std::to_string(INTERNAL_SCALER_INERTIA));
840 }else
841 return handleGetSet(cmd, replies, this->gain.inertia);
842 break;
844 if (cmd.type == CMDtype::get)
845 {
846 replies.emplace_back(filter[filterProfileId].damper.freq);
847 }
848 else if (cmd.type == CMDtype::set)
849 {
850 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].damper, cmd.val, filter[CUSTOM_PROFILE_ID].damper.q);
851 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
852 }
853 break;
855 if (cmd.type == CMDtype::get)
856 {
857 replies.emplace_back(filter[filterProfileId].damper.q);
858 }
859 else if (cmd.type == CMDtype::set)
860 {
861 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].damper, filter[CUSTOM_PROFILE_ID].damper.freq, cmd.val);
862 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
863 }
864 break;
866 if (cmd.type == CMDtype::get)
867 {
868 replies.emplace_back(filter[filterProfileId].friction.freq);
869 }
870 else if (cmd.type == CMDtype::set)
871 {
872 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].friction, cmd.val, filter[CUSTOM_PROFILE_ID].friction.q);
873 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
874 }
875 break;
877 if (cmd.type == CMDtype::get)
878 {
879 replies.emplace_back(filter[filterProfileId].friction.q);
880 }
881 else if (cmd.type == CMDtype::set)
882 {
883 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].friction, filter[CUSTOM_PROFILE_ID].friction.freq, cmd.val);
884 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
885 }
886 break;
888 if (cmd.type == CMDtype::get)
889 {
890 replies.emplace_back(filter[filterProfileId].inertia.freq);
891 }
892 else if (cmd.type == CMDtype::set)
893 {
894 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].inertia, cmd.val, filter[CUSTOM_PROFILE_ID].inertia.q);
895 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
896 }
897 break;
899 if (cmd.type == CMDtype::get)
900 {
901 replies.emplace_back(filter[filterProfileId].inertia.q);
902 }
903 else if (cmd.type == CMDtype::set)
904 {
905 checkFilterCoeff(&filter[CUSTOM_PROFILE_ID].inertia, filter[CUSTOM_PROFILE_ID].inertia.freq, cmd.val);
906 if (filterProfileId == CUSTOM_PROFILE_ID) updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
907 }
908 break;
909
911 if (cmd.type == CMDtype::get)
912 {
913 replies.emplace_back(frictionPctSpeedToRampup);
914 }
915 else if (cmd.type == CMDtype::set)
916 {
917 uint8_t pct = clip<uint8_t, uint8_t>(cmd.val, 0, 100);
919 }
920 break;
922 if (cmd.type == CMDtype::get)
923 {
924 replies.emplace_back(this->filterProfileId);
925 }
926 else if (cmd.type == CMDtype::set)
927 {
928 uint32_t value = clip<uint32_t, uint32_t>(cmd.val, 0, 1);
929 this->filterProfileId = value;
930 updateFilterSettingsForEffects(FFB_EFFECT_INERTIA);
931 updateFilterSettingsForEffects(FFB_EFFECT_DAMPER);
932 updateFilterSettingsForEffects(FFB_EFFECT_FRICTION);
933 }
934 break;
936 if (cmd.type == CMDtype::get)
937 {
938 replies.emplace_back(isMonitorEffect);
939 }
940 else if (cmd.type == CMDtype::set)
941 {
943 }
944 break;
945
946 default:
948 }
949 return CommandStatus::OK;
950}
951
952/*
953 *
954 */
956 std::vector<CommandReply> replies;
957 Delay(500);
958 while (true) {
959 Delay(3000);
960
961 if(isMonitorEffect) {
962
963 continue; // TODO uncomment when stream is ok
964 replies.push_back(CommandReply(listEffectsUsed(true)));
966 this,
969
970 }
971
972 }
973
974}
975
980 if(idx < this->effects.size()){
981 logEffectType(effects[idx].type, true); // Effect off
982 effects[idx] = FFB_Effect(); // Reset all settings
983 for(int i=0; i< MAX_AXIS; i++) {
984 if(effects[idx].filter[i] != nullptr){
985 effects[idx].filter[i].reset(nullptr);
986 }
987 }
988 }
989}
990
995 if(type > FFB_EFFECT_NONE && type < FFB_EFFECT_CUSTOM+1){ // Check if it is a valid effect type
996 for(uint8_t i=0;i<effects.size();i++){
997 if(effects[i].type == FFB_EFFECT_NONE){
998 return(i);
999 }
1000 }
1001 }
1002 return -1;
1003}
1004
1005
1006
1007
1012 float periodAvg = fxPeriodAvg.getAverage();
1013 if((micros() - lastFxUpdate) > 1000000 || periodAvg == 0){
1014 // Reset average
1015 fxPeriodAvg.clear();
1016 return 0;
1017 }else{
1018 return (1000000.0/periodAvg);
1019 }
1020}
1021
1026 float periodAvg = cfUpdatePeriodAvg.getAverage();
1027 if((micros() - lastCfUpdate) > 1000000 || periodAvg == 0){
1028 // Reset average
1029 cfUpdatePeriodAvg.clear();
1030 return 0;
1031 }else{
1032 return (1000000.0/periodAvg);
1033 }
1034}
1035
1036
1038 cfUpdatePeriodAvg.addValue((uint32_t)(micros() - lastCfUpdate));
1039 lastCfUpdate = micros();
1040}
1041
1043 fxPeriodAvg.addValue((uint32_t)(micros() - lastFxUpdate));
1044 lastFxUpdate = micros();
1045}
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)
CommandHandler(const char *clsname, uint16_t clsid, uint8_t instance=0)
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 updateSamplerate(float newSamplerate)
void setActive(bool active)
int32_t getEnvelopeMagnitude(FFB_Effect *effect)
void checkFilterCoeff(biquad_constant_t *filter, uint32_t freq, uint8_t q)
void calculateEffects(std::vector< std::unique_ptr< Axis > > &axes)
int32_t calcConditionEffectForce(FFB_Effect *effect, float metric, uint8_t gain, uint8_t idx, float scale, float angle_ratio)
void setGain(uint8_t gain)
CommandStatus command(const ParsedCommand &cmd, std::vector< CommandReply > &replies)
std::array< effect_stat_t, 12 > effects_stats
void calcStatsEffectType(uint8_t type, int32_t force, uint8_t axis)
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 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
Thread(const std::string Name, uint16_t StackDepth, UBaseType_t Priority)
Definition cthread.cpp:56
T clip(T v, C l, C h)
Definition cppmain.h:58
uint32_t micros()
Definition cppmain.cpp:124
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)
uint16_t negativeSaturation
Definition ffb_defs.h:295
int16_t negativeCoefficient
Definition ffb_defs.h:293
int16_t positiveCoefficient
Definition ffb_defs.h:292
uint16_t positiveSaturation
Definition ffb_defs.h:294
uint16_t period
Definition ffb_defs.h:325
bool useEnvelope
Definition ffb_defs.h:334
int16_t startLevel
Definition ffb_defs.h:319
uint8_t type
Definition ffb_defs.h:315
bool useSingleCondition
Definition ffb_defs.h:335
uint8_t gain
Definition ffb_defs.h:317
std::unique_ptr< Biquad > filter[MAX_AXIS]
Definition ffb_defs.h:330
uint32_t duration
Definition ffb_defs.h:326
int16_t magnitude
Definition ffb_defs.h:318
uint32_t attackTime
Definition ffb_defs.h:328
float axisMagnitudes[MAX_AXIS]
Definition ffb_defs.h:321
FFB_Effect_Condition conditions[MAX_AXIS]
Definition ffb_defs.h:323
uint16_t attackLevel
Definition ffb_defs.h:327
int16_t endLevel
Definition ffb_defs.h:320
volatile uint8_t state
Definition ffb_defs.h:314
uint32_t startTime
Definition ffb_defs.h:332
uint32_t fadeTime
Definition ffb_defs.h:328
int16_t offset
Definition ffb_defs.h:316
uint16_t fadeLevel
Definition ffb_defs.h:327
int16_t phase
Definition ffb_defs.h:324
int32_t pos_scaled_16b
Definition Axis.h:74
float accel
Definition Axis.h:72
float speed
Definition Axis.h:73