From 84194de595754c397148aa233b16bd4595824088 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Thu, 31 Aug 2023 21:45:36 +0100 Subject: [PATCH 01/29] Create .gitignore --- .gitignore | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3f071d --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# AVR objects +*.lst +*.eep +*.lss +*.sym + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf \ No newline at end of file From 7952a0f30542cae77a49ce50d1f573d718da17ed Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:26:26 +0100 Subject: [PATCH 02/29] Fix based on #46 --- ffb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffb.c b/ffb.c index 9c3d7dd..705825b 100644 --- a/ffb.c +++ b/ffb.c @@ -216,8 +216,8 @@ int16_t UsbInt8ToMidiInt14(int8_t inUsbValue) // Returns MIDI value (i.e. max 0..7f). uint8_t CalcGain(uint8_t usbValue, uint8_t gain) { - int16_t v = usbValue; - return (((v * gain) / 256) >> 2 ) & 0x7f; + uint16_t v = usbValue; + return (((v * gain) / 255) >> 1 ) & 0x7f; } // Lengths of each report type From 7c63076c5aa9b92c0bc8d3c41f3615a5b95a38e9 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Thu, 31 Aug 2023 23:13:23 +0100 Subject: [PATCH 03/29] Update FFP MIDI structures including frequency to uint14 Separated type conversion functions as existing one contained div/2 for Time units Update FFP structures. wavelength -> frequency Update FFP MIDI data structures to include useful known parameters. Change references to waveLength => frequency and units 1/Hz => Hz to avoid confusion. frequency to MIDI uint14 and new USB period => MIDI frequency conversion. --- ffb-pro.c | 85 ++++++++++++++++++++++++++++--------------------------- ffb-pro.h | 19 +++++++------ ffb.c | 14 +++++++-- ffb.h | 1 + 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index fb2c6b6..7dfbd84 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -5,6 +5,7 @@ Copyright 2012 Tero Loimuneva (tloimu [at] gmail [dot] com) Copyright 2013 Saku Kekkonen + Copyright 2023 Ed Wilkinson Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted @@ -238,20 +239,21 @@ void FfbproSetEnvelope( MIDI effect data: uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 2=sine, 5=Square, 6=RampUp, 7=RampDown, 8=Triange, 0x12=Constant - uint8_t unknown1; // ? always 0x7F + uint8_t unknown1; // Overwrite an allocated effect uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t direction; - uint8_t unknown3[5]; // ? always 7f 64 00 10 4e + uint8_t gain; + uint16_t sampleRate; //default 0x64 0x00 = 100Hz + uint16_t truncate; //default 0x10 0x4e = 10000 for full waveform uint8_t attackLevel; uint16_t attackTime; uint8_t magnitude; uint16_t fadeTime; uint8_t fadeLevel; - uint8_t waveLength; // 0x6F..0x01 => 1/Hz - uint8_t unknown5; // ? always 0x00 - uint16_t param1; // Constant: positive=7f 00, negative=01 01, Other effects: 01 01 - uint16_t param2; // Constant: 00 00, Other effects 01 01 + uint16_t frequency; // unit=Hz; 1 for constant and ramps + uint16_t param1; // Varies by effect type; Constant: positive=7f 00, negative=01 01, Other effects: 01 01 + uint16_t param2; // Varies by effect type; Constant: 00 00, Other effects 01 01 */ if (DoDebug(DEBUG_DETAIL)) @@ -275,12 +277,12 @@ void FfbproSetEnvelope( midi_data->attackLevel = CalcGain(data->attackLevel, effect->usb_gain); midi_data->fadeLevel = CalcGain(data->fadeLevel, effect->usb_gain); - midi_data->attackTime = UsbUint16ToMidiUint14(data->attackTime); + midi_data->attackTime = UsbUint16ToMidiUint14_Time(data->attackTime); if (data->fadeTime == USB_DURATION_INFINITE) midi_data->fadeTime = MIDI_DURATION_INFINITE; else - midi_data->fadeTime = UsbUint16ToMidiUint14(effect->usb_duration - effect->usb_fadeTime); + midi_data->fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); if (effect->state & MEffectState_SentToJoystick) { FfbproSendModify(eid, 0x60, midi_data->fadeTime); @@ -418,13 +420,13 @@ void FfbproSetPeriodic( midi_data->param1 = 0x007f; midi_data->param2 = 0x0101; - // Calculate waveLength (in MIDI it is in units of 1/Hz and can have value 0x6F..0x01) + // Calculate frequency (in MIDI it is in units of Hz and can have value from 1 to 169Hz) if (data->period >= 1000) - midi_data->waveLength = 0x01; - else if (data->period <= 9) - midi_data->waveLength = 0x6F; + midi_data->frequency = 0x0001; //1Hz + else if (data->period <= 5) + midi_data->frequency = 0x0129; //169Hz else - midi_data->waveLength = (1000 / data->period) & 0x7F; + midi_data->frequency = UsbUint16ToMidiUint14(1000 / data->period); // Check phase if relevant (+90 phase for sine makes it a cosine) if (midi_data->waveForm == 2 || midi_data->waveForm == 3) // sine @@ -447,7 +449,7 @@ void FfbproSetPeriodic( if (effect->state & MEffectState_SentToJoystick) { // FfbProSendModify(eid, 0x74, midi_data->magnitude); // FFP does not actually support changing magnitude on-fly here - FfbproSendModify(eid, 0x70, midi_data->waveLength); + FfbproSendModify(eid, 0x70, midi_data->frequency); } } @@ -465,20 +467,22 @@ void FfbproSetConstantForce( MIDI effect data: uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 2=sine, 5=Square, 6=RampUp, 7=RampDown, 8=Triange, 0x12=Constant - uint8_t unknown1; // ? always 0x7F + uint8_t unknown1; // Overwrite an allocated effect uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t direction; - uint8_t unknown3[5]; // ? always 7f 64 00 10 4e + uint8_t gain; + uint16_t sampleRate; //default 0x64 0x00 = 100Hz + uint16_t truncate; //default 0x10 0x4e = 10000 for full waveform uint8_t attackLevel; uint16_t attackTime; uint8_t magnitude; uint16_t fadeTime; uint8_t fadeLevel; - uint8_t waveLength; // 0x6F..0x01 => 1/Hz - uint8_t unknown5; // ? always 0x00 - uint16_t param1; // Constant: positive=7f 00, negative=01 01, Other effects: 01 01 - uint16_t param2; // Constant: 00 00, Other effects 01 01 + uint16_t frequency; // unit=Hz; 1 for constant and ramps + uint16_t param1; // Varies by effect type; Constant: positive=7f 00, negative=01 01, Other effects: 01 01 + uint16_t param2; // Varies by effect type; Constant: 00 00, Other effects 01 01 + */ if (DoDebug(DEBUG_DETAIL)) @@ -591,20 +595,22 @@ int FfbproSetEffect( MIDI effect data: uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 2=sine, 5=Square, 6=RampUp, 7=RampDown, 8=Triange, 0x12=Constant - uint8_t unknown1; // ? always 0x7F + uint8_t unknown1; // Overwrite an allocated effect uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t direction; - uint8_t unknown3[5]; // ? always 7f 64 00 10 4e + uint8_t gain; + uint16_t sampleRate; //default 0x64 0x00 = 100Hz + uint16_t truncate; //default 0x10 0x4e = 10000 for full waveform uint8_t attackLevel; uint16_t attackTime; uint8_t magnitude; uint16_t fadeTime; uint8_t fadeLevel; - uint8_t waveLength; // 0x6F..0x01 => 1/Hz - uint8_t unknown5; // ? always 0x00 - uint16_t param1; // Constant: positive=7f 00, negative=01 01, Other effects: 01 01 - uint16_t param2; // Constant: 00 00, Other effects 01 01 + uint16_t frequency; // unit=Hz; 1 for constant and ramps + uint16_t param1; // Varies by effect type; Constant: positive=7f 00, negative=01 01, Other effects: 01 01 + uint16_t param2; // Varies by effect type; Constant: 00 00, Other effects 01 01 + */ // Convert direction @@ -622,7 +628,7 @@ int FfbproSetEffect( } else { if (effect->usb_duration > effect->usb_fadeTime) { // add some safety and special case handling - midi_data->fadeTime = UsbUint16ToMidiUint14(effect->usb_duration - effect->usb_fadeTime); + midi_data->fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); } else { midi_data->fadeTime = midi_data->duration; } @@ -682,7 +688,7 @@ int FfbproSetEffect( uint8_t waveForm; // 2=sine, 5=Square, 6=RampUp, 7=RampDown, 8=Triange, 0x12=Constant uint8_t unknown1; // ? always 0x7F uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t coeffAxis0; uint16_t coeffAxis1; uint16_t offsetAxis0; @@ -705,7 +711,7 @@ int FfbproSetEffect( uint8_t waveForm; // 2=sine, 5=Square, 6=RampUp, 7=RampDown, 8=Triange, 0x12=Constant uint8_t unknown1; // ? always 0x7F uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t coeffAxis0; uint16_t coeffAxis1; */ @@ -746,22 +752,19 @@ void FfbproCreateNewEffect( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; midi_data->magnitude = 0x7f; - midi_data->waveLength = 0x01; + midi_data->frequency = 0x0001; midi_data->attackLevel = 0x00; midi_data->attackTime = 0x0000; midi_data->fadeLevel = 0x00; midi_data->fadeTime = 0x0000; - + // Constants midi_data->command = 0x23; midi_data->unknown1 = 0x7F; - midi_data->unknown2 = 0x0000; - midi_data->unknown3[0] = 0x7F; - midi_data->unknown3[1] = 0x64; - midi_data->unknown3[2] = 0x00; - midi_data->unknown3[3] = 0x10; - midi_data->unknown3[4] = 0x4E; - + midi_data->triggerButton = 0x0000; + midi_data->gain = 0x7F; + midi_data->sampleRate = 0x0064; + midi_data->truncate = 0x4E10; if (inData->effectType == 0x01) // constant midi_data->param2 = 0x0000; else diff --git a/ffb-pro.h b/ffb-pro.h index e6f5abd..9788a63 100644 --- a/ffb-pro.h +++ b/ffb-pro.h @@ -12,18 +12,19 @@ typedef struct { uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 2=sine, 5=Square, 6=RampUp, 7=RampDown, 8=Triange, 0x12=Constant - uint8_t unknown1; // ? always 0x7F + uint8_t unknown1; // Overwrite an allocated effect uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t direction; - uint8_t unknown3[5]; // ? always 7f 64 00 10 4e + uint8_t gain; + uint16_t sampleRate; //default 0x64 0x00 = 100Hz + uint16_t truncate; //default 0x10 0x4e = 10000 for full waveform uint8_t attackLevel; uint16_t attackTime; uint8_t magnitude; uint16_t fadeTime; uint8_t fadeLevel; - uint8_t waveLength; // 0x6F..0x01 => 1/Hz - uint8_t unknown5; // ? always 0x00 + uint16_t frequency; // unit=Hz; 1 for constant and ramps uint16_t param1; // Varies by effect type; Constant: positive=7f 00, negative=01 01, Other effects: 01 01 uint16_t param2; // Varies by effect type; Constant: 00 00, Other effects 01 01 } FFP_MIDI_Effect_Basic; @@ -32,9 +33,9 @@ typedef struct { uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 0xd=Spring, 0x0e=Damper, 0xf=Inertia - uint8_t unknown1; // ? always 0x7F + uint8_t unknown1; // Overwrite an allocated effect uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t coeffAxis0; uint16_t coeffAxis1; uint16_t offsetAxis0; @@ -45,9 +46,9 @@ typedef struct { uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 0x10=Friction - uint8_t unknown1; // ? always 0x7F + uint8_t unknown1; // Overwrite an allocated effect uint16_t duration; // unit=2ms - uint16_t unknown2; // ? always 0x0000 + uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t coeffAxis0; uint16_t coeffAxis1; } FFP_MIDI_Effect_Friction; diff --git a/ffb.c b/ffb.c index 705825b..87571e8 100644 --- a/ffb.c +++ b/ffb.c @@ -190,13 +190,21 @@ void FfbSendSysEx(const uint8_t* midi_data, uint8_t len) FfbSendData(&mark, 1); } +uint16_t UsbUint16ToMidiUint14_Time(uint16_t inUsbValue) + { //Only use for Time conversion from ms. Includes /2 as MIDI duration is in units of 2ms + if (inUsbValue == 0xFFFF) + return 0x0000; + + return (inUsbValue & 0x7F00) + ((inUsbValue & 0x00FF) >> 1); + } + uint16_t UsbUint16ToMidiUint14(uint16_t inUsbValue) { if (inUsbValue == 0xFFFF) return 0x0000; - return (inUsbValue & 0x7F00) + ((inUsbValue & 0x00FF) >> 1); // loss of the MSB-bit! - } + return ((inUsbValue << 1) & 0x7F00) + ((inUsbValue & 0x007F)); + } int16_t UsbInt8ToMidiInt14(int8_t inUsbValue) { @@ -380,7 +388,7 @@ void FfbHandle_SetEffect(USB_FFBReport_SetEffect_Output_Data_t *data) if (data->duration == USB_DURATION_INFINITE) { midi_data->duration = MIDI_DURATION_INFINITE; } else { - midi_data->duration = UsbUint16ToMidiUint14(data->duration); // MIDI unit is 2ms + midi_data->duration = UsbUint16ToMidiUint14_Time(data->duration); // MIDI unit is 2ms } effect->usb_duration = data->duration; // store for later calculation of diff --git a/ffb.h b/ffb.h index 1d4a684..b49c643 100644 --- a/ffb.h +++ b/ffb.h @@ -247,6 +247,7 @@ typedef struct extern volatile TDisabledEffectTypes gDisabledEffects; void FfbSendSysEx(const uint8_t* midi_data, uint8_t len); +uint16_t UsbUint16ToMidiUint14_Time(uint16_t inUsbValue); uint16_t UsbUint16ToMidiUint14(uint16_t inUsbValue); int16_t UsbInt8ToMidiInt14(int8_t inUsbValue); uint8_t CalcGain(uint8_t usbValue, uint8_t gain); From 6f29dce8684bba3492f524bb9c353bd9ec9e880e Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Wed, 6 Sep 2023 22:54:22 +0100 Subject: [PATCH 04/29] Send Modify messages only for altered parameters Functions that check if a) MIDI parameter has changed and if so update the SysEx data and b) check if already sent to device and Send Modify if so. This is needed because future improvements may send more updates to the stick. Also makes code more concise. Also #define all MIDI modify offset codes Wheel code only minimum update to not break compatibility. Will have altered behaviour in some cases where previously address and send-modify code were not targeting the same parameter. Corrected definition of USB_DURATION_INFINITE Fix compiler errors --- ffb-pro.c | 225 +++++++++++++++++++++++----------------------------- ffb-pro.h | 24 +++++- ffb-wheel.c | 7 +- ffb-wheel.h | 4 +- ffb.c | 43 ++++++++-- ffb.h | 11 ++- 6 files changed, 174 insertions(+), 140 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 7dfbd84..82c9867 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -216,9 +216,11 @@ void FfbproSendModify(uint8_t effectId, uint8_t address, uint16_t value) FfbSendData(midi_cmd, 3); } -void FfbproModifyDuration(uint8_t effectId, uint16_t duration) +void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration) { - FfbproSendModify(effectId, 0x40, duration); + FfbSetParamMidi_14bit(effectState, midi_data_param, effectId, + FFP_MIDI_MODIFY_DURATION, duration); + //FfbproSendModify(effectId, 0x40, duration); } void FfbproSetEnvelope( @@ -227,6 +229,7 @@ void FfbproSetEnvelope( { uint8_t eid = data->effectBlockIndex; + uint16_t midi_fadeTime; /* USB effect data: uint8_t reportId; // =2 @@ -274,22 +277,19 @@ void FfbproSetEnvelope( effect->usb_fadeLevel = data->fadeLevel; effect->usb_fadeTime = data->fadeTime; - midi_data->attackLevel = CalcGain(data->attackLevel, effect->usb_gain); - midi_data->fadeLevel = CalcGain(data->fadeLevel, effect->usb_gain); - - midi_data->attackTime = UsbUint16ToMidiUint14_Time(data->attackTime); - - if (data->fadeTime == USB_DURATION_INFINITE) - midi_data->fadeTime = MIDI_DURATION_INFINITE; + if (data->fadeTime == USB_DURATION_INFINITE) // is this check needed? Only if duration is not INF but fadeTime is INF - can this occur? + midi_fadeTime = MIDI_DURATION_INFINITE; else - midi_data->fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); - - if (effect->state & MEffectState_SentToJoystick) { - FfbproSendModify(eid, 0x60, midi_data->fadeTime); - FfbproSendModify(eid, 0x5C, midi_data->attackTime); - FfbproSendModify(eid, 0x6C, midi_data->fadeLevel); - FfbproSendModify(eid, 0x64, midi_data->attackLevel); - } + midi_fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); + + FfbSetParamMidi_14bit(effect->state, &(midi_data->fadeTime), eid, + FFP_MIDI_MODIFY_FADETIME, midi_fadeTime); + FfbSetParamMidi_14bit(effect->state, &(midi_data->attackTime), eid, + FFP_MIDI_MODIFY_ATTACKTIME, UsbUint16ToMidiUint14_Time(data->attackTime)); + FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, + FFP_MIDI_MODIFY_FADE, CalcGain(data->fadeLevel, effect->usb_gain)); + FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, + FFP_MIDI_MODIFY_ATTACK, CalcGain(data->attackLevel, effect->usb_gain)); } void FfbproSetCondition( @@ -331,28 +331,25 @@ void FfbproSetCondition( { volatile FFP_MIDI_Effect_Spring_Inertia_Damper *midi_data = (FFP_MIDI_Effect_Spring_Inertia_Damper *)&effect->data; - + + uint16_t midi_offsetAxis1; + if (data->parameterBlockOffset == 0) { - midi_data->coeffAxis0 = UsbInt8ToMidiInt14(data->positiveCoefficient); - midi_data->offsetAxis0 = UsbInt8ToMidiInt14(data->cpOffset); + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(data->positiveCoefficient)); + FfbSetParamMidi_14bit(effect->state, &(midi_data->offsetAxis0), eid, + FFP_MIDI_MODIFY_OFFSETAXIS0, UsbInt8ToMidiInt14(data->cpOffset)); } else { - midi_data->coeffAxis1 = UsbInt8ToMidiInt14(data->positiveCoefficient); + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(data->positiveCoefficient)); if (data->cpOffset == 0x80) - midi_data->offsetAxis1 = 0x007f; + midi_offsetAxis1 = 0x007f; else - midi_data->offsetAxis1 = UsbInt8ToMidiInt14(-data->cpOffset); + midi_offsetAxis1 = UsbInt8ToMidiInt14(-data->cpOffset); + FfbSetParamMidi_14bit(effect->state, &(midi_data->offsetAxis1), eid, + FFP_MIDI_MODIFY_OFFSETAXIS1, midi_offsetAxis1); } - // Send data to MIDI - if (effect->state & MEffectState_SentToJoystick) { - if (data->parameterBlockOffset == 0) { - FfbproSendModify(eid, 0x48, midi_data->coeffAxis0); - FfbproSendModify(eid, 0x50, midi_data->offsetAxis0); - } else { - FfbproSendModify(eid, 0x4C, midi_data->coeffAxis1); - FfbproSendModify(eid, 0x54, midi_data->offsetAxis1); - } - } } break; @@ -362,17 +359,11 @@ void FfbproSetCondition( (FFP_MIDI_Effect_Friction *)&effect->data; if (data->parameterBlockOffset == 0) - midi_data->coeffAxis0 = UsbInt8ToMidiInt14(data->positiveCoefficient); + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(data->positiveCoefficient)); else - midi_data->coeffAxis1 = UsbInt8ToMidiInt14(data->positiveCoefficient); - - // Send data to MIDI - if (effect->state & MEffectState_SentToJoystick) { // Send update - if (data->parameterBlockOffset == 0) - FfbproSendModify(eid, 0x48, midi_data->coeffAxis0); - else - FfbproSendModify(eid, 0x4C, midi_data->coeffAxis1); - } + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(data->positiveCoefficient)); } break; @@ -415,42 +406,41 @@ void FfbproSetPeriodic( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + uint16_t midi_param1 = 0x007f, midi_param2 = 0x0101, midi_frequency = 0x0001; + effect->usb_magnitude = data->magnitude; - midi_data->param1 = 0x007f; - midi_data->param2 = 0x0101; - // Calculate frequency (in MIDI it is in units of Hz and can have value from 1 to 169Hz) - if (data->period >= 1000) - midi_data->frequency = 0x0001; //1Hz - else if (data->period <= 5) - midi_data->frequency = 0x0129; //169Hz - else - midi_data->frequency = UsbUint16ToMidiUint14(1000 / data->period); - + if (data->period <= 5) + midi_frequency = 0x0129; //169Hz + else if (data->period < 1000) + midi_frequency = UsbUint16ToMidiUint14(1000 / data->period); + + FfbSetParamMidi_14bit(effect->state, &(midi_data->frequency), eid, + FFP_MIDI_MODIFY_FREQUENCY, midi_frequency); + // Check phase if relevant (+90 phase for sine makes it a cosine) if (midi_data->waveForm == 2 || midi_data->waveForm == 3) // sine { if (data->phase >= 32 && data->phase <= 224) { - midi_data->waveForm = 3; // cosine + midi_data->waveForm = 3; // cosine. Can't be modified } else { - midi_data->waveForm = 2; // sine + midi_data->waveForm = 2; // sine. Can't be modified } // Calculate min-max from magnitude and offset uint8_t magnitude = CalcGain(data->magnitude, effect->usb_gain); // already at MIDI-level i.e. 1/2 of USB level! - midi_data->param1 = UsbInt8ToMidiInt14(data->offset / 2 + magnitude); // max - midi_data->param2 = UsbInt8ToMidiInt14(data->offset / 2 - magnitude); // min - if (effect->state & MEffectState_SentToJoystick) { - FfbproSendModify(eid, 0x74, midi_data->param1); - FfbproSendModify(eid, 0x78, midi_data->param2); - } + + FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, + FFP_MIDI_MODIFY_PARAM1, UsbInt8ToMidiInt14(data->offset / 2 + magnitude)); // max + FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), eid, + FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(data->offset / 2 - magnitude)); // min + } else { + midi_data->param1 = midi_param1; //never again changed + midi_data->param2 = midi_param2; //never again changed } + - if (effect->state & MEffectState_SentToJoystick) { - // FfbProSendModify(eid, 0x74, midi_data->magnitude); // FFP does not actually support changing magnitude on-fly here - FfbproSendModify(eid, 0x70, midi_data->frequency); - } } void FfbproSetConstantForce( @@ -498,26 +488,29 @@ void FfbproSetConstantForce( effect->usb_magnitude = data->magnitude; + uint8_t midi_magnitude; + uint16_t midi_param1; + if (data->magnitude >= 0) { - midi_data->magnitude = CalcGain(data->magnitude, effect->usb_gain); - midi_data->param1 = 0x007f; + midi_magnitude = CalcGain(data->magnitude, effect->usb_gain); + midi_param1 = 0x007f; } else { - midi_data->magnitude = CalcGain(-(data->magnitude+1), effect->usb_gain); - midi_data->param1 = 0x0101; - } - - midi_data->param2 = 0x0000; - - if (effect->state & MEffectState_SentToJoystick) { - FfbproSendModify(eid, 0x74, midi_data->magnitude); - FfbproSendModify(eid, 0x7C, midi_data->param1); + midi_magnitude = CalcGain(-(data->magnitude+1), effect->usb_gain); + midi_param1 = 0x0101; } + + FfbSetParamMidi_7bit(effect->state, &(midi_data->magnitude), eid, + FFP_MIDI_MODIFY_MAGNITUDE, midi_magnitude); + FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, + FFP_MIDI_MODIFY_PARAM1, midi_param1); + midi_data->param2 = 0x0000; // never again modified } void FfbproSetRampForce( USB_FFBReport_SetRampForce_Output_Data_t* data, volatile TEffectState* effect) { + uint8_t eid = data->effectBlockIndex; if (DoDebug(DEBUG_DETAIL)) { uint8_t eid = data->effectBlockIndex; @@ -540,17 +533,18 @@ void FfbproSetRampForce( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + uint16_t midi_param1; + if (data->start < 0) - midi_data->param1 = 0x0100 | (-(data->start+1)); + midi_param1 = 0x0100 | (-(data->start+1)); else - midi_data->param1 = data->start; - - midi_data->param2 = UsbInt8ToMidiInt14(data->end); + midi_param1 = data->start; + + FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, + FFP_MIDI_MODIFY_PARAM1, midi_param1); - if (effect->state & MEffectState_SentToJoystick) { - FfbproSendModify(data->reportId, 0x78, midi_data->param1); - FfbproSendModify(data->reportId, 0x74, midi_data->param2); - } + FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), eid, + FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(data->end)); } int FfbproSetEffect( @@ -616,25 +610,28 @@ int FfbproSetEffect( // Convert direction uint16_t usbdir = data->directionX; usbdir = usbdir * 2; - uint16_t dir = (usbdir & 0x7F) + ( (usbdir & 0x0180) << 1 ); - midi_data->direction = dir; - + FfbSetParamMidi_14bit(effect->state, &(midi_data->direction), eid, + FFP_MIDI_MODIFY_DIRECTION, (usbdir & 0x7F) + ( (usbdir & 0x0180) << 1 )); + // Recalculate fadeTime for MIDI since change to duration changes the fadeTime too + uint16_t midi_fadeTime; if (data->duration == USB_DURATION_INFINITE) { - midi_data->fadeTime = MIDI_DURATION_INFINITE; + midi_fadeTime = MIDI_DURATION_INFINITE; } else { if (effect->usb_fadeTime == USB_DURATION_INFINITE) { - midi_data->fadeTime = MIDI_DURATION_INFINITE; + midi_fadeTime = MIDI_DURATION_INFINITE; } else { if (effect->usb_duration > effect->usb_fadeTime) { // add some safety and special case handling - midi_data->fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); + midi_fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); } else { - midi_data->fadeTime = midi_data->duration; + midi_fadeTime = midi_data->duration; } } - } - + } + FfbSetParamMidi_14bit(effect->state, &(midi_data->fadeTime), eid, + FFP_MIDI_MODIFY_FADETIME, midi_fadeTime); + // Gain and its effects (magnitude and envelope levels) bool gain_changed = (effect->usb_gain != data->gain); if (gain_changed) { @@ -642,39 +639,25 @@ int FfbproSetEffect( // LogBinary(&data->gain, 1); effect->usb_gain = data->gain; - midi_data->attackLevel = CalcGain(effect->usb_attackLevel, data->gain); - midi_data->fadeLevel = CalcGain(effect->usb_fadeLevel, data->gain); + FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, + FFP_MIDI_MODIFY_FADE, CalcGain(effect->usb_fadeLevel, data->gain)); + FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, + FFP_MIDI_MODIFY_ATTACK, CalcGain(effect->usb_attackLevel, data->gain)); + if (is_periodic) { // Calculate min-max from magnitude and offset, since magnitude may be affected by gain we must calc them here too for periodic effects uint8_t magnitude = CalcGain(effect->usb_magnitude, effect->usb_gain); // already at MIDI-level i.e. 1/2 of USB level! - midi_data->param1 = UsbInt8ToMidiInt14(effect->usb_offset + magnitude); // max - midi_data->param2 = UsbInt8ToMidiInt14(effect->usb_offset - magnitude); // min - if (effect->state & MEffectState_SentToJoystick) { - FfbproSendModify(eid, 0x74, midi_data->param1); // TODO - FfbproSendModify(eid, 0x78, midi_data->param2); - } + FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, + FFP_MIDI_MODIFY_PARAM1, UsbInt8ToMidiInt14(effect->usb_offset + magnitude)); // max + FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), eid, + FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(effect->usb_offset - magnitude)); // min } else { - midi_data->magnitude = CalcGain(effect->usb_magnitude, data->gain); + FfbSetParamMidi_7bit(effect->state, &(midi_data->magnitude), eid, + FFP_MIDI_MODIFY_MAGNITUDE, CalcGain(effect->usb_magnitude, data->gain)); } } - // Send data to MIDI - if (effect->state & MEffectState_SentToJoystick) - { - FfbproSendModify(eid, 0x48, midi_data->direction); // TODO - FfbproSendModify(eid, 0x60, midi_data->fadeTime); - if (gain_changed) { - FfbproSendModify(eid, 0x6C, midi_data->fadeLevel); // might have changed due gain - FfbproSendModify(eid, 0x64, midi_data->attackLevel); // might have changed due gain - if (!is_periodic) { - FfbproSendModify(eid, 0x74, midi_data->magnitude); // might have changed due gain - } - } - } else { - FfbSendSysEx((uint8_t*)midi_data, sizeof(FFP_MIDI_Effect_Basic)); - effect->state |= MEffectState_SentToJoystick; - } } break; @@ -697,9 +680,6 @@ int FfbproSetEffect( // volatile FFP_MIDI_Effect_Spring_Inertia_Damper *midi_data = (FFP_MIDI_Effect_Spring_Inertia_Damper *) &gEffectStates[eid].data; midi_data_len = sizeof(FFP_MIDI_Effect_Spring_Inertia_Damper); - // Send data to MIDI - if (effect->state & MEffectState_SentToJoystick) { - } } break; @@ -718,9 +698,6 @@ int FfbproSetEffect( // volatile FFP_MIDI_Effect_Friction *midi_data = (FFP_MIDI_Effect_Friction *) &gEffectStates[eid].data; midi_data_len = sizeof(FFP_MIDI_Effect_Friction); - // Send data to MIDI - if (effect->state & MEffectState_SentToJoystick) { - } } break; diff --git a/ffb-pro.h b/ffb-pro.h index 9788a63..7cfd5fc 100644 --- a/ffb-pro.h +++ b/ffb-pro.h @@ -61,7 +61,9 @@ void FfbproStartEffect(uint8_t id); void FfbproStopEffect(uint8_t id); void FfbproFreeEffect(uint8_t id); -void FfbproModifyDuration(uint8_t effectId, uint16_t duration); +void FfbproSendModify(uint8_t effectId, uint8_t address, uint16_t value); + +void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration); void FfbproSetEnvelope(USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* effect); void FfbproSetCondition(USB_FFBReport_SetCondition_Output_Data_t* data, volatile TEffectState* effect); @@ -73,4 +75,24 @@ void FfbproCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, uint8_t FfbproUsbToMidiEffectType(uint8_t usb_effect_type); +#define FFP_MIDI_MODIFY_DURATION 0x40 +#define FFP_MIDI_MODIFY_TRIGGERBUTTON 0x44 +#define FFP_MIDI_MODIFY_DIRECTION 0x48 +#define FFP_MIDI_MODIFY_GAIN 0x4C +#define FFP_MIDI_MODIFY_SAMPLERATE 0x50 +#define FFP_MIDI_MODIFY_ATTACK 0x64 +#define FFP_MIDI_MODIFY_ATTACKTIME 0x5C +#define FFP_MIDI_MODIFY_MAGNITUDE 0x68 //byte 22 i.e. sustain +#define FFP_MIDI_MODIFY_FADETIME 0x60 +#define FFP_MIDI_MODIFY_FADE 0x6C +#define FFP_MIDI_MODIFY_FREQUENCY 0x70 +#define FFP_MIDI_MODIFY_PARAM1 0x74 +#define FFP_MIDI_MODIFY_PARAM2 0x78 +#define FFP_MIDI_MODIFY_COEFFAXIS0 0x48 //roll +#define FFP_MIDI_MODIFY_COEFFAXIS1 0x4C //pitch +#define FFP_MIDI_MODIFY_OFFSETAXIS0 0x50 //roll +#define FFP_MIDI_MODIFY_OFFSETAXIS1 0x54 //pitch + +#define FFP_MIDI_MODIFY_DEVICEGAIN 0x7C + #endif // _FFB_PRO_ \ No newline at end of file diff --git a/ffb-wheel.c b/ffb-wheel.c index c614a70..43e26ee 100644 --- a/ffb-wheel.c +++ b/ffb-wheel.c @@ -140,7 +140,7 @@ void FfbwheelFreeEffect(uint8_t effectId) // modify operations --------------------------------------------------------- -static void FfbwheelSendModify(uint8_t effectId, uint8_t address, uint16_t value) +void FfbwheelSendModify(uint8_t effectId, uint8_t address, uint16_t value) { cmd_f1_t op; @@ -156,9 +156,10 @@ static void FfbwheelSendModify(uint8_t effectId, uint8_t address, uint16_t value FfbSendData(d, sizeof(op)); } -void FfbwheelModifyDuration(uint8_t effectId, uint16_t duration) +void FfbwheelModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration) { - FfbwheelSendModify(effectId, 0x00, duration); + //FfbwheelSendModify(effectId, 0x00, duration); + FfbSetParamMidi_14bit(effectState, midi_data_param, effectId, 0x00, duration); //Changed for compatibility but behaviour not checked for wheel } void FfbwheelSetEnvelope( diff --git a/ffb-wheel.h b/ffb-wheel.h index 9335d3e..f6d1951 100644 --- a/ffb-wheel.h +++ b/ffb-wheel.h @@ -105,7 +105,9 @@ void FfbwheelStartEffect(uint8_t effectId); void FfbwheelStopEffect(uint8_t effectId); void FfbwheelFreeEffect(uint8_t effectId); -void FfbwheelModifyDuration(uint8_t effectId, uint16_t duration); +void FfbwheelSendModify(uint8_t effectId, uint8_t address, uint16_t value); + +void FfbwheelModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration); void FfbwheelSetEnvelope(USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* e); void FfbwheelSetCondition(USB_FFBReport_SetCondition_Output_Data_t* data, volatile TEffectState* e); diff --git a/ffb.c b/ffb.c index 87571e8..96cd4b8 100644 --- a/ffb.c +++ b/ffb.c @@ -59,6 +59,7 @@ const FFB_Driver ffb_drivers[2] = .SetRampForce = FfbproSetRampForce, .SetEffect = FfbproSetEffect, .ModifyDuration = FfbproModifyDuration, + .SendModify = FfbproSendModify, }, { .EnableInterrupts = FfbwheelEnableInterrupts, @@ -76,6 +77,7 @@ const FFB_Driver ffb_drivers[2] = .SetRampForce = FfbwheelSetRampForce, .SetEffect = FfbwheelSetEffect, .ModifyDuration = FfbwheelModifyDuration, + .SendModify = FfbwheelSendModify, } }; @@ -190,8 +192,34 @@ void FfbSendSysEx(const uint8_t* midi_data, uint8_t len) FfbSendData(&mark, 1); } +uint8_t FfbSetParamMidi_14bit(uint8_t effectState, volatile uint16_t* midi_data_param, uint8_t effectId, uint8_t address, uint16_t value) + { // why does midi data need to be volatile? What else can change it?? Are the USB FFB messages not processed sequentially? + if (value == *midi_data_param) + return 0; + else + { + *midi_data_param = value; + if (effectState & MEffectState_SentToJoystick) + ffb->SendModify(effectId, address, value); + return 1; + } + } + +uint8_t FfbSetParamMidi_7bit(uint8_t effectState, volatile uint8_t* midi_data_param, uint8_t effectId, uint8_t address, uint8_t value) + { // why does midi data need to be volatile? What else can change it?? Are the USB FFB messages not processed sequentially? + if (value == *midi_data_param) + return 0; + else + { + *midi_data_param = value; + if (effectState & MEffectState_SentToJoystick) + ffb->SendModify(effectId, address, value); + return 1; + } + } + uint16_t UsbUint16ToMidiUint14_Time(uint16_t inUsbValue) - { //Only use for Time conversion from ms. Includes /2 as MIDI duration is in units of 2ms + { //Only use for Time conversion from ms. Includes /2 as MIDI duration is in units of 2ms and USB 1ms if (inUsbValue == 0xFFFF) return 0x0000; @@ -382,18 +410,19 @@ void FfbHandle_SetEffect(USB_FFBReport_SetEffect_Output_Data_t *data) } FlushDebugBuffer(); } - + + uint16_t midi_duration; + midi_data_common_t* midi_data = (midi_data_common_t*)effect->data; if (data->duration == USB_DURATION_INFINITE) { - midi_data->duration = MIDI_DURATION_INFINITE; + midi_duration = MIDI_DURATION_INFINITE; } else { - midi_data->duration = UsbUint16ToMidiUint14_Time(data->duration); // MIDI unit is 2ms + midi_duration = UsbUint16ToMidiUint14_Time(data->duration); // MIDI unit is 2ms } effect->usb_duration = data->duration; // store for later calculation of - - if (effect->state & MEffectState_SentToJoystick) - ffb->ModifyDuration(data->effectBlockIndex, midi_data->duration); + + ffb->ModifyDuration(effect->state, &(midi_data->duration), data->effectBlockIndex, midi_duration); uint8_t midi_data_len = ffb->SetEffect((USB_FFBReport_SetEffect_Output_Data_t *) data, effect); diff --git a/ffb.h b/ffb.h index b49c643..3688db3 100644 --- a/ffb.h +++ b/ffb.h @@ -247,6 +247,8 @@ typedef struct extern volatile TDisabledEffectTypes gDisabledEffects; void FfbSendSysEx(const uint8_t* midi_data, uint8_t len); +uint8_t FfbSetParamMidi_14bit(uint8_t effectState, volatile uint16_t *midi_data_param, uint8_t effectId, uint8_t address, uint16_t value); +uint8_t FfbSetParamMidi_7bit(uint8_t effectState, volatile uint8_t *midi_data_param, uint8_t effectId, uint8_t address, uint8_t value); uint16_t UsbUint16ToMidiUint14_Time(uint16_t inUsbValue); uint16_t UsbUint16ToMidiUint14(uint16_t inUsbValue); int16_t UsbInt8ToMidiInt14(int8_t inUsbValue); @@ -264,8 +266,8 @@ void FfbEnableEffectId(uint8_t inId, uint8_t inEnable); #define MEffectState_Playing 0x02 #define MEffectState_SentToJoystick 0x04 -#define USB_DURATION_INFINITE 0x7FFF -#define MIDI_DURATION_INFINITE 0 +#define USB_DURATION_INFINITE 0xFFFF +#define MIDI_DURATION_INFINITE 0x0000 #define USB_EFFECT_CONSTANT 0x01 #define USB_EFFECT_RAMP 0x02 @@ -310,7 +312,8 @@ typedef struct void (*StopEffect)(uint8_t eid); void (*FreeEffect)(uint8_t eid); - void (*ModifyDuration)(uint8_t effectId, uint16_t duration); + void (*SendModify)(uint8_t effectId, uint8_t address, uint16_t value); + void (*ModifyDuration)(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration); void (*CreateNewEffect)(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, volatile TEffectState* effect); void (*SetEnvelope)(USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* effect); @@ -321,4 +324,4 @@ typedef struct int (*SetEffect)(USB_FFBReport_SetEffect_Output_Data_t* data, volatile TEffectState* effect); } FFB_Driver; -#endif // _FFB_PRO_ \ No newline at end of file +#endif // _FFB_ \ No newline at end of file From 67d2e443714039766c903bdf9ce16537a608498a Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:19:49 +0100 Subject: [PATCH 05/29] Set MIDI effect gain parameter directly for waveforms While removing gain compensated magnitudes from periodic and constant force (otherwise double accounting) --- ffb-pro.c | 47 ++++++++++++----------------------------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 82c9867..4d2b740 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -287,9 +287,9 @@ void FfbproSetEnvelope( FfbSetParamMidi_14bit(effect->state, &(midi_data->attackTime), eid, FFP_MIDI_MODIFY_ATTACKTIME, UsbUint16ToMidiUint14_Time(data->attackTime)); FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, - FFP_MIDI_MODIFY_FADE, CalcGain(data->fadeLevel, effect->usb_gain)); + FFP_MIDI_MODIFY_FADE, ((data->fadeLevel) >> 1) & 0x7f); FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, - FFP_MIDI_MODIFY_ATTACK, CalcGain(data->attackLevel, effect->usb_gain)); + FFP_MIDI_MODIFY_ATTACK, ((data->attackLevel) >> 1) & 0x7f); } void FfbproSetCondition( @@ -429,7 +429,7 @@ void FfbproSetPeriodic( } // Calculate min-max from magnitude and offset - uint8_t magnitude = CalcGain(data->magnitude, effect->usb_gain); // already at MIDI-level i.e. 1/2 of USB level! + uint8_t magnitude = data->magnitude / 2; FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, FFP_MIDI_MODIFY_PARAM1, UsbInt8ToMidiInt14(data->offset / 2 + magnitude)); // max @@ -492,10 +492,10 @@ void FfbproSetConstantForce( uint16_t midi_param1; if (data->magnitude >= 0) { - midi_magnitude = CalcGain(data->magnitude, effect->usb_gain); + midi_magnitude = (data->magnitude >> 1) & 0x7f; midi_param1 = 0x007f; } else { - midi_magnitude = CalcGain(-(data->magnitude+1), effect->usb_gain); + midi_magnitude = (( -(data->magnitude + 1)) >> 1) & 0x7f; midi_param1 = 0x0101; } @@ -606,7 +606,10 @@ int FfbproSetEffect( uint16_t param2; // Varies by effect type; Constant: 00 00, Other effects 01 01 */ - + // Effect Gain + FfbSetParamMidi_7bit(effect->state, &(midi_data->gain), eid, + FFP_MIDI_MODIFY_GAIN, (data->gain >> 1) & 0x7f); + // Convert direction uint16_t usbdir = data->directionX; usbdir = usbdir * 2; @@ -631,33 +634,7 @@ int FfbproSetEffect( } FfbSetParamMidi_14bit(effect->state, &(midi_data->fadeTime), eid, FFP_MIDI_MODIFY_FADETIME, midi_fadeTime); - - // Gain and its effects (magnitude and envelope levels) - bool gain_changed = (effect->usb_gain != data->gain); - if (gain_changed) { -// LogTextP(PSTR(" New gain:")); -// LogBinary(&data->gain, 1); - - effect->usb_gain = data->gain; - FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, - FFP_MIDI_MODIFY_FADE, CalcGain(effect->usb_fadeLevel, data->gain)); - FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, - FFP_MIDI_MODIFY_ATTACK, CalcGain(effect->usb_attackLevel, data->gain)); - - - if (is_periodic) { - // Calculate min-max from magnitude and offset, since magnitude may be affected by gain we must calc them here too for periodic effects - uint8_t magnitude = CalcGain(effect->usb_magnitude, effect->usb_gain); // already at MIDI-level i.e. 1/2 of USB level! - FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, - FFP_MIDI_MODIFY_PARAM1, UsbInt8ToMidiInt14(effect->usb_offset + magnitude)); // max - FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), eid, - FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(effect->usb_offset - magnitude)); // min - } else { - FfbSetParamMidi_7bit(effect->state, &(midi_data->magnitude), eid, - FFP_MIDI_MODIFY_MAGNITUDE, CalcGain(effect->usb_magnitude, data->gain)); - } - } - + } break; @@ -734,12 +711,12 @@ void FfbproCreateNewEffect( midi_data->attackTime = 0x0000; midi_data->fadeLevel = 0x00; midi_data->fadeTime = 0x0000; + midi_data->gain = 0x7F; // Constants midi_data->command = 0x23; midi_data->unknown1 = 0x7F; - midi_data->triggerButton = 0x0000; - midi_data->gain = 0x7F; + midi_data->triggerButton = 0x0000; midi_data->sampleRate = 0x0064; midi_data->truncate = 0x4E10; if (inData->effectType == 0x01) // constant From c7be4b234041bb781466fccf3058a9ad4d085e43 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:09:48 +0100 Subject: [PATCH 06/29] Set TriggerButton for all effects --- ffb-pro.c | 8 ++++++++ ffb.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/ffb-pro.c b/ffb-pro.c index 4d2b740..e3f573d 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -572,6 +572,14 @@ int FfbproSetEffect( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; uint8_t midi_data_len = sizeof(FFP_MIDI_Effect_Basic); // default MIDI data size bool is_periodic = false; + + // Data applying to all effects + uint16_t buttonBits = 0; + if (data->triggerButton != USB_TRIGGERBUTTON_NULL) + buttonBits = (1 << data->triggerButton); + //Buttons 1-9 from LSB + FfbSetParamMidi_14bit(effect->state, &(midi_data->triggerButton), eid, + FFP_MIDI_MODIFY_TRIGGERBUTTON, (buttonBits & 0x7F) + ( (buttonBits & 0x0180) << 1 )); // Fill in the effect type specific data switch (data->effectType) diff --git a/ffb.h b/ffb.h index 3688db3..076bf5f 100644 --- a/ffb.h +++ b/ffb.h @@ -269,6 +269,8 @@ void FfbEnableEffectId(uint8_t inId, uint8_t inEnable); #define USB_DURATION_INFINITE 0xFFFF #define MIDI_DURATION_INFINITE 0x0000 +#define USB_TRIGGERBUTTON_NULL 0xFF + #define USB_EFFECT_CONSTANT 0x01 #define USB_EFFECT_RAMP 0x02 #define USB_EFFECT_SQUARE 0x03 From 0b29a93737c2aab58eb0031e175387313db68aae Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:40:47 +0100 Subject: [PATCH 07/29] Improve Constant Force behaviour Never modify param1 as this triggers an FFP bug so that envelopes are not applied Instead set reciprocal direction if magnitude is negative --- ffb-pro.c | 33 +++++++++++++++++++++++---------- ffb.c | 2 ++ ffb.h | 4 ++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index e3f573d..2d24ce2 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -223,6 +223,18 @@ void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_ //FfbproSendModify(effectId, 0x40, duration); } +uint16_t FfbproConvertDirection(uint8_t usbdir, uint8_t reciprocal) +{ + //Convert from USB 0..179 i.e. unit 2deg to MIDI uint_14 0..359 unit deg + //Take reciprocal direction if arg not 0 + uint16_t direction = usbdir * 2; + + if (reciprocal) + direction = (direction + 180) % 360; + + return (direction & 0x7F) + ( (direction & 0x0180) << 1 ); +} + void FfbproSetEnvelope( USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* effect) @@ -407,8 +419,6 @@ void FfbproSetPeriodic( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; uint16_t midi_param1 = 0x007f, midi_param2 = 0x0101, midi_frequency = 0x0001; - - effect->usb_magnitude = data->magnitude; // Calculate frequency (in MIDI it is in units of Hz and can have value from 1 to 169Hz) if (data->period <= 5) @@ -489,20 +499,22 @@ void FfbproSetConstantForce( effect->usb_magnitude = data->magnitude; uint8_t midi_magnitude; - uint16_t midi_param1; if (data->magnitude >= 0) { midi_magnitude = (data->magnitude >> 1) & 0x7f; - midi_param1 = 0x007f; + } else { midi_magnitude = (( -(data->magnitude + 1)) >> 1) & 0x7f; - midi_param1 = 0x0101; + } FfbSetParamMidi_7bit(effect->state, &(midi_data->magnitude), eid, FFP_MIDI_MODIFY_MAGNITUDE, midi_magnitude); - FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, - FFP_MIDI_MODIFY_PARAM1, midi_param1); + FfbSetParamMidi_14bit(effect->state, &(midi_data->direction), eid, + FFP_MIDI_MODIFY_DIRECTION, FfbproConvertDirection(effect->usb_direction, (data->magnitude < 0))); + //reciprocal direction if -ve + + midi_data->param1 = 0x007F; // never again modified midi_data->param2 = 0x0000; // never again modified } @@ -619,10 +631,11 @@ int FfbproSetEffect( FFP_MIDI_MODIFY_GAIN, (data->gain >> 1) & 0x7f); // Convert direction - uint16_t usbdir = data->directionX; - usbdir = usbdir * 2; + effect->usb_direction = data->directionX; FfbSetParamMidi_14bit(effect->state, &(midi_data->direction), eid, - FFP_MIDI_MODIFY_DIRECTION, (usbdir & 0x7F) + ( (usbdir & 0x0180) << 1 )); + FFP_MIDI_MODIFY_DIRECTION, FfbproConvertDirection(data->directionX, (effect->usb_magnitude < 0))); + //reciprocal only if -ve constant force + // Recalculate fadeTime for MIDI since change to duration changes the fadeTime too uint16_t midi_fadeTime; diff --git a/ffb.c b/ffb.c index 96cd4b8..8b2b524 100644 --- a/ffb.c +++ b/ffb.c @@ -361,6 +361,8 @@ void FfbOnCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, effect->usb_offset = 0; effect->usb_attackLevel = 0xFF; effect->usb_fadeLevel = 0xFF; + effect->usb_magnitude = 0; + effect->usb_direction = 0; ((midi_data_common_t*)effect->data)->waveForm = ffb->UsbToMidiEffectType(inData->effectType - 1); diff --git a/ffb.h b/ffb.h index 076bf5f..f8f8875 100644 --- a/ffb.h +++ b/ffb.h @@ -298,8 +298,8 @@ typedef struct { uint8_t state; // see constants uint16_t usb_duration, usb_fadeTime; // used to calculate fadeTime to MIDI, since in USB it is given as time difference from the end while in MIDI it is given as time from start // These are used to calculate effects of USB gain to MIDI data - uint8_t usb_gain, usb_offset, usb_attackLevel, usb_fadeLevel; - uint8_t usb_magnitude; + uint8_t usb_gain, usb_offset, usb_attackLevel, usb_fadeLevel, usb_direction; + int16_t usb_magnitude; //Signed for Constant Force use only volatile uint8_t data[MAX_MIDI_MSG_LEN]; } TEffectState; From 07c1f7a507847bb979c92701102174793230e111 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:26:45 +0100 Subject: [PATCH 08/29] Apply offset and envelope combination for all periodic waveforms Resolve definition in excess of force limit (i.e. magnitude + offset > stick force max) by respecting offset and squeezing available range for envelope. Will now allow attack and fade levels to exceed sustain level --- ffb-pro.c | 129 ++++++++++++++++++++++++++++++++++++++++++++---------- ffb.c | 3 +- ffb.h | 4 +- 3 files changed, 111 insertions(+), 25 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 2d24ce2..99a22e1 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -223,7 +223,7 @@ void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_ //FfbproSendModify(effectId, 0x40, duration); } -uint16_t FfbproConvertDirection(uint8_t usbdir, uint8_t reciprocal) +static uint16_t FfbproConvertDirection(uint8_t usbdir, uint8_t reciprocal) { //Convert from USB 0..179 i.e. unit 2deg to MIDI uint_14 0..359 unit deg //Take reciprocal direction if arg not 0 @@ -235,6 +235,51 @@ uint16_t FfbproConvertDirection(uint8_t usbdir, uint8_t reciprocal) return (direction & 0x7F) + ( (direction & 0x0180) << 1 ); } +static uint8_t FfbproModifyParamRange(volatile TEffectState* effect, uint8_t effectId, int8_t offset) +{ + + volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + + int8_t param1, param2; + uint8_t range; + if (offset >= 0) { + param1 = 127; + param2 = -128 + offset * 2; + } else { + param1 = 127 + (offset + 1) * 2; // avoid overflow, but offset -1 has same effect as 0 + param2 = -128; + } // Note range of 0 should not occur - this would cause /div0 in FFbproCalcLevel + range = param1 - param2; + + if (effect->invert) //param1 is always set > param2 by MS drivers? Possible this inversion could cause some unexpected behaviour + { + param2 = param1; + param1 = param2 - range; + } + + FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), effectId, + FFP_MIDI_MODIFY_PARAM1, UsbInt8ToMidiInt14(param1)); + FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), effectId, + FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(param2)); + + return range; +} + +static uint8_t FfbproCalcLevel(uint8_t range, uint8_t usb_level) +{ + // Initial levels assume full range - but range is reduced by application of offset + // So compensate by increasing levels (attack, magnitude or fade) + + uint16_t v = ((usb_level * 255) / range) >> 1; + + if (v > 255) { + return 0x7f; //saturated + } else { + return (v >> 1) & 0x7f; + } +} + + void FfbproSetEnvelope( USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* effect) @@ -299,9 +344,9 @@ void FfbproSetEnvelope( FfbSetParamMidi_14bit(effect->state, &(midi_data->attackTime), eid, FFP_MIDI_MODIFY_ATTACKTIME, UsbUint16ToMidiUint14_Time(data->attackTime)); FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, - FFP_MIDI_MODIFY_FADE, ((data->fadeLevel) >> 1) & 0x7f); + FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(effect->range, data->fadeLevel)); FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, - FFP_MIDI_MODIFY_ATTACK, ((data->attackLevel) >> 1) & 0x7f); + FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(effect->range, data->attackLevel)); } void FfbproSetCondition( @@ -401,7 +446,6 @@ void FfbproSetPeriodic( MIDI effect data: - Offset values other than zero do not work and thus it is ignored on FFP */ if (DoDebug(DEBUG_DETAIL)) @@ -418,7 +462,7 @@ void FfbproSetPeriodic( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; - uint16_t midi_param1 = 0x007f, midi_param2 = 0x0101, midi_frequency = 0x0001; + uint16_t midi_frequency = 0x0001; // Calculate frequency (in MIDI it is in units of Hz and can have value from 1 to 169Hz) if (data->period <= 5) @@ -429,28 +473,69 @@ void FfbproSetPeriodic( FfbSetParamMidi_14bit(effect->state, &(midi_data->frequency), eid, FFP_MIDI_MODIFY_FREQUENCY, midi_frequency); - // Check phase if relevant (+90 phase for sine makes it a cosine) - if (midi_data->waveForm == 2 || midi_data->waveForm == 3) // sine + // Check phase and set closest waveform and sign only before effect is sent + // - don't allow changes on the fly - even where possible this would result in harsh steps + if (!(effect->state & MEffectState_SentToJoystick)) { - if (data->phase >= 32 && data->phase <= 224) { - midi_data->waveForm = 3; // cosine. Can't be modified + if (midi_data->waveForm == 2 || midi_data->waveForm == 3) // sine or cosine + { + switch (data->phase / 32) //USB 255 = 2*pi or 360deg so 32 is 45deg + { + case 0: //0-44deg + case 7: + { + midi_data->waveForm = 2; + effect->invert = 0; + break; + } + case 1: + case 2: + { + midi_data->waveForm = 3; + effect->invert = 0; + break; + } + case 3: + case 4: + { + midi_data->waveForm = 2; + effect->invert = 1; //i.e. -sine + break; + } + case 5: + case 6: + { + midi_data->waveForm = 3; + effect->invert = 1; + break; + } + } } else { - midi_data->waveForm = 2; // sine. Can't be modified + if ((data->phase > 64) && (data->phase < 192)) { //for square, tri, sawtooth + effect->invert = 1; + } else { + effect->invert = 0; + } } - - // Calculate min-max from magnitude and offset - uint8_t magnitude = data->magnitude / 2; - - FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, - FFP_MIDI_MODIFY_PARAM1, UsbInt8ToMidiInt14(data->offset / 2 + magnitude)); // max - FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), eid, - FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(data->offset / 2 - magnitude)); // min - } else { - midi_data->param1 = midi_param1; //never again changed - midi_data->param2 = midi_param2; //never again changed } - + // Calculate min max and available range from offset. Invert if needed. + uint8_t range = FfbproModifyParamRange(effect, eid, data->offset); + + // Calculate magnitude relative to available range + FfbSetParamMidi_7bit(effect->state, &(midi_data->magnitude), eid, + FFP_MIDI_MODIFY_MAGNITUDE, FfbproCalcLevel(range, data->magnitude)); + + // Check whether envelope levels need to be updated too + if (range != effect->range) + { + effect->range = range; + FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, + FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(range, effect->usb_fadeLevel)); + FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, + FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(range, effect->usb_attackLevel)); + } + } void FfbproSetConstantForce( diff --git a/ffb.c b/ffb.c index 8b2b524..bf93d0c 100644 --- a/ffb.c +++ b/ffb.c @@ -358,11 +358,12 @@ void FfbOnCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, effect->usb_duration = USB_DURATION_INFINITE; effect->usb_fadeTime = USB_DURATION_INFINITE; effect->usb_gain = 0xFF; - effect->usb_offset = 0; effect->usb_attackLevel = 0xFF; effect->usb_fadeLevel = 0xFF; effect->usb_magnitude = 0; effect->usb_direction = 0; + effect->invert = 0; + effect->range = 255; ((midi_data_common_t*)effect->data)->waveForm = ffb->UsbToMidiEffectType(inData->effectType - 1); diff --git a/ffb.h b/ffb.h index f8f8875..940702f 100644 --- a/ffb.h +++ b/ffb.h @@ -297,8 +297,8 @@ typedef struct { typedef struct { uint8_t state; // see constants uint16_t usb_duration, usb_fadeTime; // used to calculate fadeTime to MIDI, since in USB it is given as time difference from the end while in MIDI it is given as time from start - // These are used to calculate effects of USB gain to MIDI data - uint8_t usb_gain, usb_offset, usb_attackLevel, usb_fadeLevel, usb_direction; + // These are used to calculate effect levels and signs + uint8_t usb_gain, usb_attackLevel, usb_fadeLevel, usb_direction, invert, range; int16_t usb_magnitude; //Signed for Constant Force use only volatile uint8_t data[MAX_MIDI_MSG_LEN]; } TEffectState; From 583a7197b14f592f2fdac7f7589f7b7c55919397 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Fri, 22 Sep 2023 22:25:40 +0100 Subject: [PATCH 09/29] Improve ramp representation Support for ramp up and down and envelopes all changing on the fly Uses same code as periodics, respecting start and end where attack or fade are out of range Fix for bug added in periodic mod where amplitudes were halved --- ffb-pro.c | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 99a22e1..f037253 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -270,7 +270,7 @@ static uint8_t FfbproCalcLevel(uint8_t range, uint8_t usb_level) // Initial levels assume full range - but range is reduced by application of offset // So compensate by increasing levels (attack, magnitude or fade) - uint16_t v = ((usb_level * 255) / range) >> 1; + uint16_t v = ((usb_level * 255) / range); if (v > 255) { return 0x7f; //saturated @@ -519,7 +519,7 @@ void FfbproSetPeriodic( } } - // Calculate min max and available range from offset. Invert if needed. + // Calculate min max and available range from offset. Then Modify. Invert if needed. uint8_t range = FfbproModifyParamRange(effect, eid, data->offset); // Calculate magnitude relative to available range @@ -619,7 +619,6 @@ void FfbproSetRampForce( FlushDebugBuffer(); } - // FFP supports only ramp up from MIN to MAX and ramp down from MAX to MIN? /* USB effect data: uint8_t reportId; // =6 @@ -630,6 +629,39 @@ void FfbproSetRampForce( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + // Same approach as periodic waveforms + int8_t offset = ((int16_t)data->start + (int16_t)data->end)/2; //Could be done more efficiently without casting + uint8_t magnitude; + + if (data->start > data->end) { + effect->invert = 1; //Ramp Down + magnitude = data->start - data->end; + } else { + effect->invert = 0; //Ramp Up + magnitude = data->end - data->start; + } + + + // Calculate min max and available range from offset. Then Modify. Invert if needed. + uint8_t range = FfbproModifyParamRange(effect, eid, offset); + + + // Calculate magnitude relative to available range + FfbSetParamMidi_7bit(effect->state, &(midi_data->magnitude), eid, + FFP_MIDI_MODIFY_MAGNITUDE, FfbproCalcLevel(range, magnitude)); + + // Check whether envelope levels need to be updated too + if (range != effect->range) + { + effect->range = range; + FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, + FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(range, effect->usb_fadeLevel)); + FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, + FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(range, effect->usb_attackLevel)); + } + + +/* uint16_t midi_param1; if (data->start < 0) @@ -642,6 +674,7 @@ void FfbproSetRampForce( FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), eid, FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(data->end)); + */ } int FfbproSetEffect( From 99f121785601e0b76ce98fc07367a6ddbe865537 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sat, 23 Sep 2023 21:34:06 +0100 Subject: [PATCH 10/29] Fix bug in calculation of magnitudes --- ffb-pro.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffb-pro.c b/ffb-pro.c index f037253..4741822 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -270,7 +270,7 @@ static uint8_t FfbproCalcLevel(uint8_t range, uint8_t usb_level) // Initial levels assume full range - but range is reduced by application of offset // So compensate by increasing levels (attack, magnitude or fade) - uint16_t v = ((usb_level * 255) / range); + uint16_t v = ((uint16_t) usb_level * 255) / range; //explicit cast was necessary here to avoid implicit to int16_t and overflow if (v > 255) { return 0x7f; //saturated From d549e9b9cf94ba7a9267aa3c9351b3b06084e262 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sat, 30 Sep 2023 21:00:22 +0100 Subject: [PATCH 11/29] Send Device Control and Device Gain commands to FFP. Init with auto spring centre. Implement all device control in response to USB: Pause/Continue, Enable/Disable Actuators, Stop All, Reset Set PID state flags accordingly - some changes here Device Gain command in response to USB only. Not changed automatically on Reset. Remove SetAutoCentre - this should be handled by Reset then Stop All. Changed in initialisation as well i.e. Auto spring centre persists after FFB init. Update wheel code for compatibility only using known commands - not tested --- ffb-pro.c | 50 +++++++++++++++-------- ffb-pro.h | 3 +- ffb-wheel.c | 47 +++++++++++++-------- ffb-wheel.h | 3 +- ffb.c | 115 +++++++++++++++++++++++++++++----------------------- ffb.h | 12 +++++- 6 files changed, 142 insertions(+), 88 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 4741822..9e40046 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -141,27 +141,38 @@ void FfbproEnableInterrupts(void) FfbSendData(startupFfbData_2, sizeof(startupFfbData_2)); // Initialize effects data memory FfbSendData(startupFfbData_3, sizeof(startupFfbData_3)); // Initialize effects data memory - FfbproSetAutoCenter(0); + FfbproDeviceControl(USB_DCTRL_RESET); // Leave auto centre on WaitMs(70); + } -void FfbproSetAutoCenter(uint8_t enable) +uint8_t FfbproDeviceControl(uint8_t usb_control) { - const uint8_t ac_enable[] = { - 0xc5, 0x01 - }; - - const uint8_t ac_disable[] = { - 0xb5, 0x7c, 0x7f, - 0xa5, 0x7f, 0x00, - 0xc5, 0x06, - }; - - FfbSendData(ac_enable, sizeof(ac_enable)); - if (!enable) { - WaitMs(70); - FfbSendData(ac_disable, sizeof(ac_disable)); - } + /* + USB_DCTRL_ACTUATORS_DISABLE 0x01 + USB_DCTRL_ACTUATORS_ENABLE 0x02 + USB_DCTRL_STOPALL 0x03 + USB_DCTRL_RESET 0x04 + USB_DCTRL_PAUSE 0x05 + USB_DCTRL_CONTINUE 0x06 + */ + const uint8_t usbToMidiControl[] = { + 0x03, // Disable Actuators (time stepping continues in background) + 0x02, // Enable Actuators + 0x06, // Stop All (including stop auto centre) + 0x01, // Reset (stop all effects; free all effects; reset device gain to max; enable actuators; continue; enable auto spring centre) + 0x05, // Pause (time stepping is paused) + 0x04, // Continue + }; + + if (usb_control < 1 || usb_control > 6) + return 0; //not supported + + uint8_t command[2] = {0xc5}; + command[1] = usbToMidiControl[usb_control-1]; + FfbSendData(command, sizeof(command)); + //Is a wait needed here? + return 1; //supported command } const uint8_t* FfbproGetSysExHeader(uint8_t* hdr_len) @@ -223,6 +234,11 @@ void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_ //FfbproSendModify(effectId, 0x40, duration); } +void FfbproModifyDeviceGain(uint8_t gain) +{ + FfbproSendModify(0x7f, FFP_MIDI_MODIFY_DEVICEGAIN, (gain >> 1) & 0x7f); +} + static uint16_t FfbproConvertDirection(uint8_t usbdir, uint8_t reciprocal) { //Convert from USB 0..179 i.e. unit 2deg to MIDI uint_14 0..359 unit deg diff --git a/ffb-pro.h b/ffb-pro.h index 7cfd5fc..e982200 100644 --- a/ffb-pro.h +++ b/ffb-pro.h @@ -54,8 +54,8 @@ typedef struct } FFP_MIDI_Effect_Friction; void FfbproEnableInterrupts(void); +uint8_t FfbproDeviceControl(uint8_t usb_control); const uint8_t* FfbproGetSysExHeader(uint8_t* hdr_len); -void FfbproSetAutoCenter(uint8_t enable); void FfbproStartEffect(uint8_t id); void FfbproStopEffect(uint8_t id); @@ -64,6 +64,7 @@ void FfbproFreeEffect(uint8_t id); void FfbproSendModify(uint8_t effectId, uint8_t address, uint16_t value); void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration); +void FfbproModifyDeviceGain(uint8_t gain); void FfbproSetEnvelope(USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* effect); void FfbproSetCondition(USB_FFBReport_SetCondition_Output_Data_t* data, volatile TEffectState* effect); diff --git a/ffb-wheel.c b/ffb-wheel.c index 43e26ee..106576f 100644 --- a/ffb-wheel.c +++ b/ffb-wheel.c @@ -74,27 +74,34 @@ void FfbwheelEnableInterrupts(void) FfbSendData(startupFfbWheelData_0, sizeof(startupFfbWheelData_0)); FfbSendData(startupFfbWheelData_1, sizeof(startupFfbWheelData_1)); - FfbwheelSetAutoCenter(0); + FfbwheelDeviceControl(USB_DCTRL_RESET); // Leave auto centre on WaitMs(100); } -void FfbwheelSetAutoCenter(uint8_t enable) -{ - const uint8_t ac_enable[] = { - 0xf3, 0x1d - }; - - const uint8_t ac_disable[] = { - 0xf1, 0x10, 0x40, 0x00, 0x7f, 0x00, - 0xf3, 0x6a - }; - - FfbSendData(ac_enable, sizeof(ac_enable)); +uint8_t FfbwheelDeviceControl(uint8_t usb_control) +{ // CHANGED FOR COMPATIBILITY - NOT TESTED FOR WHEEL + /* + USB_DCTRL_ACTUATORS_DISABLE 0x01 + USB_DCTRL_ACTUATORS_ENABLE 0x02 + USB_DCTRL_STOPALL 0x03 + USB_DCTRL_RESET 0x04 + USB_DCTRL_PAUSE 0x05 + USB_DCTRL_CONTINUE 0x06 + */ + uint8_t command[2] = {0xf3}; - if (!enable) { - FfbSendData(ac_disable, sizeof(ac_disable)); + if (usb_control == USB_DCTRL_RESET) { + command[1] = 0x1d; + } else if (usb_control == USB_DCTRL_STOPALL) { + command[1] = 0x6a; + } else { + return 0; //not supported } + + FfbSendData(command, sizeof(command)); + //Is a wait needed? + return 1; //supported command } const uint8_t* FfbwheelGetSysExHeader(uint8_t* hdr_len) @@ -159,9 +166,17 @@ void FfbwheelSendModify(uint8_t effectId, uint8_t address, uint16_t value) void FfbwheelModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration) { //FfbwheelSendModify(effectId, 0x00, duration); - FfbSetParamMidi_14bit(effectState, midi_data_param, effectId, 0x00, duration); //Changed for compatibility but behaviour not checked for wheel + FfbSetParamMidi_14bit(effectState, midi_data_param, effectId, 0x00, duration); // CHANGED FOR COMPATIBILITY - NOT TESTED FOR WHEEL +} + +void FfbwheelModifyDeviceGain(uint8_t gain) +{ // TO IMPLEMENT: CHANGED FOR COMPATIBILITY - NOT TESTED FOR WHEEL + static const uint8_t gainCommand[] = {0xf1, 0x10, 0x40, 0x00, 0x7f, 0x00}; // only sends max gain for now + FfbSendData(gainCommand, sizeof(gainCommand)); + } + void FfbwheelSetEnvelope( USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* effect) diff --git a/ffb-wheel.h b/ffb-wheel.h index f6d1951..a61f814 100644 --- a/ffb-wheel.h +++ b/ffb-wheel.h @@ -98,8 +98,8 @@ typedef struct } cmd_f0_constant_force_t; void FfbwheelEnableInterrupts(void); +uint8_t FfbwheelDeviceControl(uint8_t usb_control); const uint8_t* FfbwheelGetSysExHeader(uint8_t* hdr_len); -void FfbwheelSetAutoCenter(uint8_t enable); void FfbwheelStartEffect(uint8_t effectId); void FfbwheelStopEffect(uint8_t effectId); @@ -108,6 +108,7 @@ void FfbwheelFreeEffect(uint8_t effectId); void FfbwheelSendModify(uint8_t effectId, uint8_t address, uint16_t value); void FfbwheelModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration); +void FfbwheelModifyDeviceGain(uint8_t gain); void FfbwheelSetEnvelope(USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* e); void FfbwheelSetCondition(USB_FFBReport_SetCondition_Output_Data_t* data, volatile TEffectState* e); diff --git a/ffb.c b/ffb.c index bf93d0c..494dd4d 100644 --- a/ffb.c +++ b/ffb.c @@ -5,6 +5,7 @@ Copyright 2012 Tero Loimuneva (tloimu [at] gmail [dot] com) Copyright 2013 Saku Kekkonen + Copyright 2023 Ed Wilkinson Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted @@ -46,7 +47,7 @@ const FFB_Driver ffb_drivers[2] = { .EnableInterrupts = FfbproEnableInterrupts, .GetSysExHeader = FfbproGetSysExHeader, - .SetAutoCenter = FfbproSetAutoCenter, + .DeviceControl = FfbproDeviceControl, .UsbToMidiEffectType = FfbproUsbToMidiEffectType, .StartEffect = FfbproStartEffect, .StopEffect = FfbproStopEffect, @@ -59,12 +60,13 @@ const FFB_Driver ffb_drivers[2] = .SetRampForce = FfbproSetRampForce, .SetEffect = FfbproSetEffect, .ModifyDuration = FfbproModifyDuration, + .ModifyDeviceGain = FfbproModifyDeviceGain, .SendModify = FfbproSendModify, }, { .EnableInterrupts = FfbwheelEnableInterrupts, .GetSysExHeader = FfbwheelGetSysExHeader, - .SetAutoCenter = FfbwheelSetAutoCenter, + .DeviceControl = FfbwheelDeviceControl, .UsbToMidiEffectType = FfbwheelUsbToMidiEffectType, .StartEffect = FfbwheelStartEffect, .StopEffect = FfbwheelStopEffect, @@ -77,6 +79,7 @@ const FFB_Driver ffb_drivers[2] = .SetRampForce = FfbwheelSetRampForce, .SetEffect = FfbwheelSetEffect, .ModifyDuration = FfbwheelModifyDuration, + .ModifyDeviceGain = FfbwheelModifyDeviceGain, .SendModify = FfbwheelSendModify, } }; @@ -546,11 +549,13 @@ void FfbHandle_BlockFree(USB_FFBReport_BlockFree_Output_Data_t *data) } void FfbHandle_DeviceControl(USB_FFBReport_DeviceControl_Output_Data_t *data) - { +{ // LogTextP(PSTR("Device Control: ")); uint8_t control = data->control; // 1=Enable Actuators, 2=Disable Actuators, 3=Stop All Effects, 4=Reset, 5=Pause, 6=Continue + + uint8_t success; // PID State Report: // uint8_t reportId; // =2 @@ -558,69 +563,77 @@ void FfbHandle_DeviceControl(USB_FFBReport_DeviceControl_Output_Data_t *data) // uint8_t effectBlockIndex; // Bit7=Effect Playing, Bit0..7=EffectId (1..40) pidState.reportId = 2; - pidState.status |= 1 << 2; - pidState.status |= 1 << 4; + pidState.status |= 1 << 2; //Safety Switch: device usable + pidState.status |= 1 << 4; //Actuator Power: on pidState.effectBlockIndex = 0; - if (control == 0x01) - { - LogTextLf("Disable Actuators"); - pidState.status = (pidState.status & 0xFE); - } - else if (control == 0x02) - { - LogTextLf("Enable Actuators"); - pidState.status |= 1 << 2; - } - else if (control == 0x03) - { - // Stop all effects (e.g. FFB-application to foreground) - LogTextLf("Stop All Effects"); + success = ffb->DeviceControl(control); - // Disable auto-center spring and stop all effects -// ???? The below would take too long? - ffb->SetAutoCenter(0); - pidState.effectBlockIndex = 0; - } - else if (control == 0x04) - { - LogTextLf("Reset"); - // Reset (e.g. FFB-application out of focus) - // Enable auto-center spring and stop all effects - ffb->SetAutoCenter(1); - WaitMs(75); - FreeAllEffects(); - } - else if (control == 0x05) - { - LogTextLf("Pause"); - } - else if (control == 0x06) - { - LogTextLf("Continue"); - } - else if (control & (0xFF-0x3F)) - { - LogTextP(PSTR("Other ")); - LogBinaryLf(&data->control, 1); - } + switch (control) + { + case USB_DCTRL_ACTUATORS_DISABLE: + LogTextLf("Disable Actuators"); + if (success) + pidState.status &= ~(1 << 1); + break; + case USB_DCTRL_ACTUATORS_ENABLE: + LogTextLf("Enable Actuators"); + if (success) + pidState.status |= (1 << 1); + break; + case USB_DCTRL_STOPALL: + LogTextLf("Stop All Effects"); + if (success) + pidState.effectBlockIndex = 0; + //need to update all effect states to not playing? Maybe not needed since adapter doesn't track when effects finish anyway + break; + case USB_DCTRL_RESET: + LogTextLf("Reset"); + // Reset (e.g. FFB-application out of focus) + //Enables auto centre, continues, enables actuators, stop and free all effects, resets device gain (for FFP at least) + if (success) + { + WaitMs(75); + FreeAllEffects(); + pidState.status |= (1 << 1); //actuators + pidState.status &= ~1; //continue + } + break; + case USB_DCTRL_PAUSE: + LogTextLf("Pause"); + if (success) + pidState.status |= 1; + break; + case USB_DCTRL_CONTINUE: + LogTextLf("Continue"); + if (success) + pidState.status &= ~1; + break; + default: + if (control & (0xFF-0x3F)) + { + LogTextP(PSTR("Other ")); + LogBinaryLf(&data->control, 1); + } + } + // Send response - } +} -void -FfbHandle_DeviceGain(USB_FFBReport_DeviceGain_Output_Data_t *data) +void FfbHandle_DeviceGain(USB_FFBReport_DeviceGain_Output_Data_t *data) { LogTextP(PSTR("Device Gain: ")); LogBinaryLf(&data->gain, 1); + + ffb->ModifyDeviceGain(data->gain); } -void -FfbHandle_SetCustomForce(USB_FFBReport_SetCustomForce_Output_Data_t *data) +void FfbHandle_SetCustomForce(USB_FFBReport_SetCustomForce_Output_Data_t *data) { LogTextLf("Set Custom Force"); // LogBinary(&data, sizeof(USB_FFBReport_SetCustomForce_Output_Data_t)); diff --git a/ffb.h b/ffb.h index 940702f..2a9eb18 100644 --- a/ffb.h +++ b/ffb.h @@ -284,6 +284,13 @@ void FfbEnableEffectId(uint8_t inId, uint8_t inEnable); #define USB_EFFECT_FRICTION 0x0B #define USB_EFFECT_CUSTOM 0x0C +#define USB_DCTRL_ACTUATORS_DISABLE 0x01 +#define USB_DCTRL_ACTUATORS_ENABLE 0x02 +#define USB_DCTRL_STOPALL 0x03 +#define USB_DCTRL_RESET 0x04 +#define USB_DCTRL_PAUSE 0x05 +#define USB_DCTRL_CONTINUE 0x06 + #define MAX_MIDI_MSG_LEN 27 /* enough to hold longest midi message data part, FFP_MIDI_Effect_Basic */ /* start of midi data common for both pro and wheel protocols */ @@ -307,7 +314,7 @@ typedef struct { void (*EnableInterrupts)(void); const uint8_t* (*GetSysExHeader)(uint8_t* hdr_len); - void (*SetAutoCenter)(uint8_t enable); + uint8_t (*DeviceControl)(uint8_t usb_control); uint8_t (*UsbToMidiEffectType)(uint8_t usb_effect_type); void (*StartEffect)(uint8_t eid); @@ -316,7 +323,8 @@ typedef struct void (*SendModify)(uint8_t effectId, uint8_t address, uint16_t value); void (*ModifyDuration)(uint8_t effectState, uint16_t* midi_data_param, uint8_t effectId, uint16_t duration); - + void (*ModifyDeviceGain)(uint8_t gain); + void (*CreateNewEffect)(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, volatile TEffectState* effect); void (*SetEnvelope)(USB_FFBReport_SetEnvelope_Output_Data_t* data, volatile TEffectState* effect); void (*SetCondition)(USB_FFBReport_SetCondition_Output_Data_t* data, volatile TEffectState* effect); From 50bf5f719f36b61774eda430ca9b2a0cf4698f6f Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sat, 30 Sep 2023 23:33:54 +0100 Subject: [PATCH 12/29] Flip USB Actuator Enable/Disable commands They were reversed in new implementation --- ffb.c | 10 +++++----- ffb.h | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ffb.c b/ffb.c index 494dd4d..7796b3f 100644 --- a/ffb.c +++ b/ffb.c @@ -571,16 +571,16 @@ void FfbHandle_DeviceControl(USB_FFBReport_DeviceControl_Output_Data_t *data) switch (control) { - case USB_DCTRL_ACTUATORS_DISABLE: - LogTextLf("Disable Actuators"); - if (success) - pidState.status &= ~(1 << 1); - break; case USB_DCTRL_ACTUATORS_ENABLE: LogTextLf("Enable Actuators"); if (success) pidState.status |= (1 << 1); break; + case USB_DCTRL_ACTUATORS_DISABLE: + LogTextLf("Disable Actuators"); + if (success) + pidState.status &= ~(1 << 1); + break; case USB_DCTRL_STOPALL: LogTextLf("Stop All Effects"); if (success) diff --git a/ffb.h b/ffb.h index 2a9eb18..ce23a42 100644 --- a/ffb.h +++ b/ffb.h @@ -284,8 +284,8 @@ void FfbEnableEffectId(uint8_t inId, uint8_t inEnable); #define USB_EFFECT_FRICTION 0x0B #define USB_EFFECT_CUSTOM 0x0C -#define USB_DCTRL_ACTUATORS_DISABLE 0x01 -#define USB_DCTRL_ACTUATORS_ENABLE 0x02 +#define USB_DCTRL_ACTUATORS_ENABLE 0x01 +#define USB_DCTRL_ACTUATORS_DISABLE 0x02 #define USB_DCTRL_STOPALL 0x03 #define USB_DCTRL_RESET 0x04 #define USB_DCTRL_PAUSE 0x05 From c009f67d2bcbe334cd8489a0b1bf8b2d4947d60b Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sat, 30 Sep 2023 23:40:47 +0100 Subject: [PATCH 13/29] Flip USB Actuator Enable/Disable commands Part 2 Missed one... --- ffb-pro.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffb-pro.c b/ffb-pro.c index 9e40046..b6e9732 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -157,8 +157,8 @@ uint8_t FfbproDeviceControl(uint8_t usb_control) USB_DCTRL_CONTINUE 0x06 */ const uint8_t usbToMidiControl[] = { - 0x03, // Disable Actuators (time stepping continues in background) 0x02, // Enable Actuators + 0x03, // Disable Actuators (time stepping continues in background) 0x06, // Stop All (including stop auto centre) 0x01, // Reset (stop all effects; free all effects; reset device gain to max; enable actuators; continue; enable auto spring centre) 0x05, // Pause (time stepping is paused) From 8e896d3ca96b2e6fb73825202d5aa5863f7359eb Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sun, 1 Oct 2023 20:22:00 +0100 Subject: [PATCH 14/29] Flip USB Actuator Enable/Disable commands Part 3 Missed some comments --- ffb-pro.c | 4 ++-- ffb-wheel.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index b6e9732..0384604 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -149,8 +149,8 @@ void FfbproEnableInterrupts(void) uint8_t FfbproDeviceControl(uint8_t usb_control) { /* - USB_DCTRL_ACTUATORS_DISABLE 0x01 - USB_DCTRL_ACTUATORS_ENABLE 0x02 + USB_DCTRL_ACTUATORS_ENABLE 0x01 + USB_DCTRL_ACTUATORS_DISABLE 0x02 USB_DCTRL_STOPALL 0x03 USB_DCTRL_RESET 0x04 USB_DCTRL_PAUSE 0x05 diff --git a/ffb-wheel.c b/ffb-wheel.c index 106576f..dbd35ec 100644 --- a/ffb-wheel.c +++ b/ffb-wheel.c @@ -82,8 +82,8 @@ void FfbwheelEnableInterrupts(void) uint8_t FfbwheelDeviceControl(uint8_t usb_control) { // CHANGED FOR COMPATIBILITY - NOT TESTED FOR WHEEL /* - USB_DCTRL_ACTUATORS_DISABLE 0x01 - USB_DCTRL_ACTUATORS_ENABLE 0x02 + USB_DCTRL_ACTUATORS_ENABLE 0x01 + USB_DCTRL_ACTUATORS_DISABLE 0x02 USB_DCTRL_STOPALL 0x03 USB_DCTRL_RESET 0x04 USB_DCTRL_PAUSE 0x05 From 52767c1b678c4b665ffc9c8d2c12a7a9962bc23e Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sun, 1 Oct 2023 22:13:17 +0100 Subject: [PATCH 15/29] Apply effect gain to all conditional effects Calculate a coefficient value for each axis that is scaled by effect gain Send modify if coefficient or gain changes FFP does not natively support effect gain for conditional effects - so this is needed to enable it. --- ffb-pro.c | 49 +++++++++++++++++++++++++++++++++++++------------ ffb.c | 11 ++++++----- ffb.h | 3 ++- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 0384604..8e148a2 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -397,6 +397,8 @@ void FfbproSetCondition( FlushDebugBuffer(); } + int8_t coeff = CalcGainCoeff(data->positiveCoefficient, effect->usb_gain); + switch (common_midi_data->waveForm) { case 0x0d: // spring (midi: 0x0d) case 0x0e: // damper (midi: 0x0e) @@ -408,13 +410,16 @@ void FfbproSetCondition( uint16_t midi_offsetAxis1; if (data->parameterBlockOffset == 0) { + effect->usb_coeffAxis0 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, - FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(data->positiveCoefficient)); + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(coeff)); FfbSetParamMidi_14bit(effect->state, &(midi_data->offsetAxis0), eid, FFP_MIDI_MODIFY_OFFSETAXIS0, UsbInt8ToMidiInt14(data->cpOffset)); + } else { + effect->usb_coeffAxis1 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, - FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(data->positiveCoefficient)); + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(coeff)); if (data->cpOffset == 0x80) midi_offsetAxis1 = 0x007f; else @@ -431,12 +436,15 @@ void FfbproSetCondition( volatile FFP_MIDI_Effect_Friction *midi_data = (FFP_MIDI_Effect_Friction *)&effect->data; - if (data->parameterBlockOffset == 0) + if (data->parameterBlockOffset == 0) { + effect->usb_coeffAxis0 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, - FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(data->positiveCoefficient)); - else + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(coeff)); + } else { + effect->usb_coeffAxis1 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, - FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(data->positiveCoefficient)); + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(coeff)); + } } break; @@ -809,9 +817,18 @@ int FfbproSetEffect( uint16_t offsetAxis0; uint16_t offsetAxis1; */ -// volatile FFP_MIDI_Effect_Spring_Inertia_Damper *midi_data = (FFP_MIDI_Effect_Spring_Inertia_Damper *) &gEffectStates[eid].data; - midi_data_len = sizeof(FFP_MIDI_Effect_Spring_Inertia_Damper); + volatile FFP_MIDI_Effect_Spring_Inertia_Damper *midi_data = + (FFP_MIDI_Effect_Spring_Inertia_Damper *)&effect->data; + + effect->usb_gain = data->gain; + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis0, data->gain))); + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis1, data->gain))); + + midi_data_len = sizeof(FFP_MIDI_Effect_Spring_Inertia_Damper); + } break; @@ -827,7 +844,15 @@ int FfbproSetEffect( uint16_t coeffAxis0; uint16_t coeffAxis1; */ -// volatile FFP_MIDI_Effect_Friction *midi_data = (FFP_MIDI_Effect_Friction *) &gEffectStates[eid].data; + volatile FFP_MIDI_Effect_Friction *midi_data = + (FFP_MIDI_Effect_Friction *)&effect->data; + + effect->usb_gain = data->gain; + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis0, data->gain))); + FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis1, data->gain))); + midi_data_len = sizeof(FFP_MIDI_Effect_Friction); } @@ -867,16 +892,16 @@ void FfbproCreateNewEffect( midi_data->fadeLevel = 0x00; midi_data->fadeTime = 0x0000; midi_data->gain = 0x7F; + midi_data->triggerButton = 0x0000; // Constants midi_data->command = 0x23; midi_data->unknown1 = 0x7F; - midi_data->triggerButton = 0x0000; + midi_data->sampleRate = 0x0064; midi_data->truncate = 0x4E10; if (inData->effectType == 0x01) // constant midi_data->param2 = 0x0000; else midi_data->param2 = 0x0101; -} - +} \ No newline at end of file diff --git a/ffb.c b/ffb.c index 7796b3f..7acbeaa 100644 --- a/ffb.c +++ b/ffb.c @@ -251,12 +251,11 @@ int16_t UsbInt8ToMidiInt14(int8_t inUsbValue) return value; } -// Calculates the final value of the given when taking in given into account. -// Returns MIDI value (i.e. max 0..7f). -uint8_t CalcGain(uint8_t usbValue, uint8_t gain) +// Calculates the final value of the given coefficient when taking in given into account. +int8_t CalcGainCoeff(int8_t usbValue, uint8_t gain) { - uint16_t v = usbValue; - return (((v * gain) / 255) >> 1 ) & 0x7f; + int16_t v = usbValue; + return ((v * gain) / 255); } // Lengths of each report type @@ -367,6 +366,8 @@ void FfbOnCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, effect->usb_direction = 0; effect->invert = 0; effect->range = 255; + effect->usb_coeffAxis0 = 0; + effect->usb_coeffAxis1 = 0; ((midi_data_common_t*)effect->data)->waveForm = ffb->UsbToMidiEffectType(inData->effectType - 1); diff --git a/ffb.h b/ffb.h index ce23a42..0ec6a4e 100644 --- a/ffb.h +++ b/ffb.h @@ -252,7 +252,7 @@ uint8_t FfbSetParamMidi_7bit(uint8_t effectState, volatile uint8_t *midi_data_pa uint16_t UsbUint16ToMidiUint14_Time(uint16_t inUsbValue); uint16_t UsbUint16ToMidiUint14(uint16_t inUsbValue); int16_t UsbInt8ToMidiInt14(int8_t inUsbValue); -uint8_t CalcGain(uint8_t usbValue, uint8_t gain); +int8_t CalcGainCoeff(int8_t usbValue, uint8_t gain); void FfbEnableSprings(uint8_t inEnable); void FfbEnableConstants(uint8_t inEnable); @@ -307,6 +307,7 @@ typedef struct { // These are used to calculate effect levels and signs uint8_t usb_gain, usb_attackLevel, usb_fadeLevel, usb_direction, invert, range; int16_t usb_magnitude; //Signed for Constant Force use only + int8_t usb_coeffAxis0, usb_coeffAxis1; volatile uint8_t data[MAX_MIDI_MSG_LEN]; } TEffectState; From 45b2f066e65cdced504bf90f44197526dbdabbec Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Wed, 4 Oct 2023 22:54:10 +0100 Subject: [PATCH 16/29] Send SampleRate with default value based on waveform frequency Send SampleRate if it is set to a non default value If USB samplePeriod is set to "default" then the default 100Hz FFP value is used, except it is increased for high waveform frequencies to prevent aliasing. --- ffb-pro.c | 58 ++++++++++++++++++++++++++++++++++++++----------------- ffb-pro.h | 2 ++ ffb.c | 8 ++++++++ ffb.h | 8 ++++++-- 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 8e148a2..90f1c49 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -234,9 +234,9 @@ void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_ //FfbproSendModify(effectId, 0x40, duration); } -void FfbproModifyDeviceGain(uint8_t gain) +void FfbproModifyDeviceGain(uint8_t usb_gain) { - FfbproSendModify(0x7f, FFP_MIDI_MODIFY_DEVICEGAIN, (gain >> 1) & 0x7f); + FfbproSendModify(0x7f, FFP_MIDI_MODIFY_DEVICEGAIN, (usb_gain >> 1) & 0x7f); } static uint16_t FfbproConvertDirection(uint8_t usbdir, uint8_t reciprocal) @@ -295,6 +295,17 @@ static uint8_t FfbproCalcLevel(uint8_t range, uint8_t usb_level) } } +static uint16_t FfbproCalcSampleRate(uint16_t usb_samplePeriod, uint16_t frequency) +{ + if (usb_samplePeriod == USB_SAMPLEPERIOD_DEFAULT) { + if (frequency > (FFP_SAMPLERATE_DEFAULT / 4)) + return frequency * 4; //This is needed to avoid aliasing or attenuation of peaks + else + return FFP_SAMPLERATE_DEFAULT; + } else { + return UsbPeriodToFrequencyHz(usb_samplePeriod); + } +} void FfbproSetEnvelope( USB_FFBReport_SetEnvelope_Output_Data_t* data, @@ -397,7 +408,7 @@ void FfbproSetCondition( FlushDebugBuffer(); } - int8_t coeff = CalcGainCoeff(data->positiveCoefficient, effect->usb_gain); + int8_t coeff = CalcGainCoeff(data->positiveCoefficient, effect->usb_gain); //Scale coefficients by gain since FFP conditional effects don't have gain parameter switch (common_midi_data->waveForm) { case 0x0d: // spring (midi: 0x0d) @@ -486,16 +497,24 @@ void FfbproSetPeriodic( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; - uint16_t midi_frequency = 0x0001; - + uint16_t frequency = 0x0001; // 1Hz + // Calculate frequency (in MIDI it is in units of Hz and can have value from 1 to 169Hz) - if (data->period <= 5) - midi_frequency = 0x0129; //169Hz - else if (data->period < 1000) - midi_frequency = UsbUint16ToMidiUint14(1000 / data->period); + if (data->period <= 13) { //Can actually play up to 169Hz, but this seems a more sensible limit to avoid motor damage, plus the freq steps get quite big + frequency = 77; //Hz + } else if (data->period < 1000) { + frequency = UsbPeriodToFrequencyHz(data->period); + } + + effect->frequency = frequency; + + uint16_t sampleRate = FfbproCalcSampleRate(effect->usb_samplePeriod, frequency); //Sample rate may need to change as a result of frequency if usb value set to default FfbSetParamMidi_14bit(effect->state, &(midi_data->frequency), eid, - FFP_MIDI_MODIFY_FREQUENCY, midi_frequency); + FFP_MIDI_MODIFY_FREQUENCY, UsbUint16ToMidiUint14(frequency)); + + FfbSetParamMidi_14bit(effect->state, &(midi_data->sampleRate), eid, + FFP_MIDI_MODIFY_SAMPLERATE, UsbUint16ToMidiUint14(sampleRate)); // Check phase and set closest waveform and sign only before effect is sent // - don't allow changes on the fly - even where possible this would result in harsh steps @@ -725,7 +744,6 @@ int FfbproSetEffect( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; uint8_t midi_data_len = sizeof(FFP_MIDI_Effect_Basic); // default MIDI data size - bool is_periodic = false; // Data applying to all effects uint16_t buttonBits = 0; @@ -743,7 +761,6 @@ int FfbproSetEffect( case USB_EFFECT_TRIANGLE: case USB_EFFECT_SAWTOOTHDOWN: case USB_EFFECT_SAWTOOTHUP: - is_periodic = true; case USB_EFFECT_CONSTANT: case USB_EFFECT_RAMP: { @@ -797,7 +814,13 @@ int FfbproSetEffect( } FfbSetParamMidi_14bit(effect->state, &(midi_data->fadeTime), eid, FFP_MIDI_MODIFY_FADETIME, midi_fadeTime); - + + effect->usb_samplePeriod = data->samplePeriod; + + uint16_t sampleRate = FfbproCalcSampleRate(data->samplePeriod, effect->frequency); + + FfbSetParamMidi_14bit(effect->state, &(midi_data->sampleRate), eid, + FFP_MIDI_MODIFY_SAMPLERATE, UsbUint16ToMidiUint14(sampleRate)); } break; @@ -821,7 +844,7 @@ int FfbproSetEffect( volatile FFP_MIDI_Effect_Spring_Inertia_Damper *midi_data = (FFP_MIDI_Effect_Spring_Inertia_Damper *)&effect->data; - effect->usb_gain = data->gain; + effect->usb_gain = data->gain; //Scale coefficients by gain since FFP conditional effects don't have gain parameter FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis0, data->gain))); FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, @@ -847,7 +870,7 @@ int FfbproSetEffect( volatile FFP_MIDI_Effect_Friction *midi_data = (FFP_MIDI_Effect_Friction *)&effect->data; - effect->usb_gain = data->gain; + effect->usb_gain = data->gain; //Scale coefficients by gain since FFP conditional effects don't have gain parameter FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis0, data->gain))); FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, @@ -893,13 +916,12 @@ void FfbproCreateNewEffect( midi_data->fadeTime = 0x0000; midi_data->gain = 0x7F; midi_data->triggerButton = 0x0000; + midi_data->sampleRate = FFP_SAMPLERATE_DEFAULT; // Constants midi_data->command = 0x23; midi_data->unknown1 = 0x7F; - - midi_data->sampleRate = 0x0064; - midi_data->truncate = 0x4E10; + midi_data->truncate = 0x4E10; // 10000 if (inData->effectType == 0x01) // constant midi_data->param2 = 0x0000; else diff --git a/ffb-pro.h b/ffb-pro.h index e982200..52a8631 100644 --- a/ffb-pro.h +++ b/ffb-pro.h @@ -96,4 +96,6 @@ uint8_t FfbproUsbToMidiEffectType(uint8_t usb_effect_type); #define FFP_MIDI_MODIFY_DEVICEGAIN 0x7C +#define FFP_SAMPLERATE_DEFAULT 0x0064 //100Hz + #endif // _FFB_PRO_ \ No newline at end of file diff --git a/ffb.c b/ffb.c index 7acbeaa..67463d7 100644 --- a/ffb.c +++ b/ffb.c @@ -251,6 +251,12 @@ int16_t UsbInt8ToMidiInt14(int8_t inUsbValue) return value; } +uint16_t UsbPeriodToFrequencyHz(uint16_t period) + { + //USB Period in ms to Frequency in Hz + return ((2000 / period) + 1) / 2; //Rounds to nearest Hz i.e. 1.51Hz rounds up to 2Hz + } + // Calculates the final value of the given coefficient when taking in given into account. int8_t CalcGainCoeff(int8_t usbValue, uint8_t gain) { @@ -366,6 +372,8 @@ void FfbOnCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, effect->usb_direction = 0; effect->invert = 0; effect->range = 255; + effect->frequency = 1; // Hz + effect->usb_samplePeriod = USB_SAMPLEPERIOD_DEFAULT; effect->usb_coeffAxis0 = 0; effect->usb_coeffAxis1 = 0; diff --git a/ffb.h b/ffb.h index 0ec6a4e..8f90d33 100644 --- a/ffb.h +++ b/ffb.h @@ -252,6 +252,7 @@ uint8_t FfbSetParamMidi_7bit(uint8_t effectState, volatile uint8_t *midi_data_pa uint16_t UsbUint16ToMidiUint14_Time(uint16_t inUsbValue); uint16_t UsbUint16ToMidiUint14(uint16_t inUsbValue); int16_t UsbInt8ToMidiInt14(int8_t inUsbValue); +uint16_t UsbPeriodToFrequencyHz(uint16_t period); int8_t CalcGainCoeff(int8_t usbValue, uint8_t gain); void FfbEnableSprings(uint8_t inEnable); @@ -269,6 +270,8 @@ void FfbEnableEffectId(uint8_t inId, uint8_t inEnable); #define USB_DURATION_INFINITE 0xFFFF #define MIDI_DURATION_INFINITE 0x0000 +#define USB_SAMPLEPERIOD_DEFAULT 0x0000 + #define USB_TRIGGERBUTTON_NULL 0xFF #define USB_EFFECT_CONSTANT 0x01 @@ -304,12 +307,13 @@ typedef struct { typedef struct { uint8_t state; // see constants uint16_t usb_duration, usb_fadeTime; // used to calculate fadeTime to MIDI, since in USB it is given as time difference from the end while in MIDI it is given as time from start - // These are used to calculate effect levels and signs + // These are used to calculate effect parameters when not all data is available in the isolated output report uint8_t usb_gain, usb_attackLevel, usb_fadeLevel, usb_direction, invert, range; + uint16_t frequency, usb_samplePeriod; int16_t usb_magnitude; //Signed for Constant Force use only int8_t usb_coeffAxis0, usb_coeffAxis1; volatile uint8_t data[MAX_MIDI_MSG_LEN]; - } TEffectState; + } TEffectState; // This takes up a lot of RAM when stored for all effects. Can stored parameters be rationalised? typedef struct { From bfc655bf9abbcbc29147241cab116d946250debf Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Wed, 4 Oct 2023 22:57:07 +0100 Subject: [PATCH 17/29] Reduce MAX_EFFECTS from 20 to 18 This is based on the FFP limitations for numbers of supported effects. Also memory for stack is getting tight with all the parameters stored for later calculation. Needed to free some up. Wheel may support more effects... --- ffb.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ffb.h b/ffb.h index 8f90d33..4518ac8 100644 --- a/ffb.h +++ b/ffb.h @@ -39,7 +39,8 @@ */ // Maximum number of parallel effects in memory -#define MAX_EFFECTS 20 +#define MAX_EFFECTS 18 +//FFP can support 10 waveforms + 2 of each conditional = 18 not including other unsupported effect types // ---- Input From 2a5f03023dfe05396cbd7b22e63bef7ab3401bf4 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:55:48 +0100 Subject: [PATCH 18/29] Count allocated effect types to determine if FFP effect memory full FFP has space for 10 waveforms and 2 of each type of conditional. The adapter counts these when a new effect is requested in order to respond correctly to host (success/full) and ensure effect IDs do not get out of sync --- ffb-pro.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ ffb-pro.h | 1 + ffb-wheel.c | 5 +++++ ffb-wheel.h | 1 + ffb.c | 22 ++++++++++++++++++++-- ffb.h | 3 +++ 6 files changed, 76 insertions(+), 2 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 90f1c49..6ae0acd 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -55,6 +55,52 @@ uint8_t FfbproUsbToMidiEffectType(uint8_t usb_effect_type) return usbToMidiEffectType[usb_effect_type]; } +uint8_t FfbproEffectMemFull(uint8_t new_midi_type) +{ + uint8_t count_waveform = 0, + count_spring = 0, count_damper = 0, + count_inertia = 0, count_friction = 0; + + uint8_t midi_type = new_midi_type; //count the new one first + + for (uint8_t id = 2; id <= (MAX_EFFECTS + 1); id++) { + switch (midi_type) { + case 0x12: + case 0x06: + case 0x05: + case 0x02: + case 0x08: + case 0x0A: + case 0x0B: + count_waveform++; + break; + case 0x0D: + count_spring++; + break; + case 0x0E: + count_damper++; + break; + case 0x0F: + count_inertia++; + break; + case 0x10: + count_friction++; + break; +// case 0x01: +// count_custom++; //limit of 4 + } + midi_type = GetMidiEffectType(id); + } + + if (count_waveform > 10 || count_spring > 2 || count_damper > 2 || + count_inertia > 2 || count_friction > 2) { + return 1; + } else { + return 0; + } + //The FFP limit on all loaded effects is 32 total, but we can't get there with the USB PID supported effects only! +} + static void FfbproInitPulses(uint8_t count) { while (count--) { diff --git a/ffb-pro.h b/ffb-pro.h index 52a8631..11fc984 100644 --- a/ffb-pro.h +++ b/ffb-pro.h @@ -75,6 +75,7 @@ int FfbproSetEffect(USB_FFBReport_SetEffect_Output_Data_t *data, volatile TEffe void FfbproCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, volatile TEffectState* effect); uint8_t FfbproUsbToMidiEffectType(uint8_t usb_effect_type); +uint8_t FfbproEffectMemFull(uint8_t new_midi_type); #define FFP_MIDI_MODIFY_DURATION 0x40 #define FFP_MIDI_MODIFY_TRIGGERBUTTON 0x44 diff --git a/ffb-wheel.c b/ffb-wheel.c index dbd35ec..1ffcbac 100644 --- a/ffb-wheel.c +++ b/ffb-wheel.c @@ -49,6 +49,11 @@ uint8_t FfbwheelUsbToMidiEffectType(uint8_t usb_effect_type) return usbToMidiEffectType[usb_effect_type]; } +uint8_t FfbwheelEffectMemFull(uint8_t new_midi_type) +{ + return 0; //Supported quantities of each effect not yet known +} + /** * Initialize wheel for FF. Releases spring effect. * diff --git a/ffb-wheel.h b/ffb-wheel.h index a61f814..11ffeeb 100644 --- a/ffb-wheel.h +++ b/ffb-wheel.h @@ -119,5 +119,6 @@ int FfbwheelSetEffect(USB_FFBReport_SetEffect_Output_Data_t *data, volatile TEf void FfbwheelCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, volatile TEffectState* effect); uint8_t FfbwheelUsbToMidiEffectType(uint8_t usb_effect_type); +uint8_t FfbwheelEffectMemFull(uint8_t new_midi_type); #endif // _FFB_WHEEL_ \ No newline at end of file diff --git a/ffb.c b/ffb.c index 67463d7..10a30f3 100644 --- a/ffb.c +++ b/ffb.c @@ -49,6 +49,7 @@ const FFB_Driver ffb_drivers[2] = .GetSysExHeader = FfbproGetSysExHeader, .DeviceControl = FfbproDeviceControl, .UsbToMidiEffectType = FfbproUsbToMidiEffectType, + .EffectMemFull = FfbproEffectMemFull, .StartEffect = FfbproStartEffect, .StopEffect = FfbproStopEffect, .FreeEffect = FfbproFreeEffect, @@ -68,6 +69,7 @@ const FFB_Driver ffb_drivers[2] = .GetSysExHeader = FfbwheelGetSysExHeader, .DeviceControl = FfbwheelDeviceControl, .UsbToMidiEffectType = FfbwheelUsbToMidiEffectType, + .EffectMemFull = FfbwheelEffectMemFull, .StartEffect = FfbwheelStartEffect, .StopEffect = FfbwheelStopEffect, .FreeEffect = FfbwheelFreeEffect, @@ -177,6 +179,16 @@ void FreeAllEffects(void) // Utilities +uint8_t GetMidiEffectType(uint8_t id) +{ + if (id > MAX_EFFECTS || gEffectStates[id].state == MEffectState_Free) { + return 0xFF; //use this as null value since it can't be a valid value in MIDI + } else { + volatile TEffectState* effect = &gEffectStates[id]; + return ((midi_data_common_t*)effect->data)->waveForm; + } +} + void FfbSendSysEx(const uint8_t* midi_data, uint8_t len) { uint8_t hdr_len; @@ -354,8 +366,14 @@ void FfbOnUsbData(uint8_t *data, uint16_t len) void FfbOnCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, USB_FFBReport_PIDBlockLoad_Feature_Data_t *outData) { outData->reportId = 6; - outData->effectBlockIndex = GetNextFreeEffect(); + uint8_t midi_effect_type = ffb->UsbToMidiEffectType(inData->effectType - 1); + if (ffb->EffectMemFull(midi_effect_type)) { + outData->effectBlockIndex = 0; + } else { + outData->effectBlockIndex = GetNextFreeEffect(); // can also return 0 if adapter full + } + if (outData->effectBlockIndex == 0) { outData->loadStatus = 2; // 1=Success,2=Full,3=Error } else { @@ -377,7 +395,7 @@ void FfbOnCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, effect->usb_coeffAxis0 = 0; effect->usb_coeffAxis1 = 0; - ((midi_data_common_t*)effect->data)->waveForm = ffb->UsbToMidiEffectType(inData->effectType - 1); + ((midi_data_common_t*)effect->data)->waveForm = midi_effect_type; ffb->CreateNewEffect(inData, effect); } diff --git a/ffb.h b/ffb.h index 4518ac8..ca59476 100644 --- a/ffb.h +++ b/ffb.h @@ -6,6 +6,7 @@ with some room for additional extra controls. Copyright 2012 Tero Loimuneva (tloimu [at] gmail [dot] com) + Copyright 2023 Ed Wilkinson MIT License. Permission to use, copy, modify, distribute, and sell this @@ -247,6 +248,7 @@ typedef struct extern volatile TDisabledEffectTypes gDisabledEffects; +uint8_t GetMidiEffectType(uint8_t id); void FfbSendSysEx(const uint8_t* midi_data, uint8_t len); uint8_t FfbSetParamMidi_14bit(uint8_t effectState, volatile uint16_t *midi_data_param, uint8_t effectId, uint8_t address, uint16_t value); uint8_t FfbSetParamMidi_7bit(uint8_t effectState, volatile uint8_t *midi_data_param, uint8_t effectId, uint8_t address, uint8_t value); @@ -322,6 +324,7 @@ typedef struct const uint8_t* (*GetSysExHeader)(uint8_t* hdr_len); uint8_t (*DeviceControl)(uint8_t usb_control); uint8_t (*UsbToMidiEffectType)(uint8_t usb_effect_type); + uint8_t (*EffectMemFull)(uint8_t new_midi_type); void (*StartEffect)(uint8_t eid); void (*StopEffect)(uint8_t eid); From acb83d8377712456df78b5ff3cb9e08ae99b69e9 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sun, 8 Oct 2023 00:06:49 +0100 Subject: [PATCH 19/29] Increase MAX_EFFECTS from 18 to 19 Due to ID offset, this allows 18 effects which is the maximum number the FFP supports of the effect types that are implemented. --- ffb.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ffb.h b/ffb.h index ca59476..53ee804 100644 --- a/ffb.h +++ b/ffb.h @@ -40,8 +40,10 @@ */ // Maximum number of parallel effects in memory -#define MAX_EFFECTS 18 +#define MAX_EFFECTS 19 //Actually Max Effect ID , but effects IDs start at 0x02 so 1 less than this //FFP can support 10 waveforms + 2 of each conditional = 18 not including other unsupported effect types +//Wheel limits? + // ---- Input From a26cf8704c8de3115385037776413a066007126e Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sun, 8 Oct 2023 00:02:34 +0100 Subject: [PATCH 20/29] Reduce size of shared data between Output reports to save RAM Different effect types now use different data structures within the same block so there are fewer redundant bytes --- ffb-pro.c | 212 +++++++++++++++++++++++++++++++++++------------------- ffb-pro.h | 41 +++++++++++ ffb.c | 19 +---- ffb.h | 8 ++- 4 files changed, 187 insertions(+), 93 deletions(-) diff --git a/ffb-pro.c b/ffb-pro.c index 6ae0acd..7254026 100644 --- a/ffb-pro.c +++ b/ffb-pro.c @@ -277,7 +277,6 @@ void FfbproModifyDuration(uint8_t effectState, uint16_t* midi_data_param, uint8_ { FfbSetParamMidi_14bit(effectState, midi_data_param, effectId, FFP_MIDI_MODIFY_DURATION, duration); - //FfbproSendModify(effectId, 0x40, duration); } void FfbproModifyDeviceGain(uint8_t usb_gain) @@ -301,6 +300,8 @@ static uint8_t FfbproModifyParamRange(volatile TEffectState* effect, uint8_t eff { volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + + FFP_Share_Periodic_Ramp *effect_share = (FFP_Share_Periodic_Ramp *)&effect->share_data; int8_t param1, param2; uint8_t range; @@ -313,7 +314,7 @@ static uint8_t FfbproModifyParamRange(volatile TEffectState* effect, uint8_t eff } // Note range of 0 should not occur - this would cause /div0 in FFbproCalcLevel range = param1 - param2; - if (effect->invert) //param1 is always set > param2 by MS drivers? Possible this inversion could cause some unexpected behaviour + if (effect_share->invert) //param1 is always set > param2 by MS drivers? Possible this inversion could cause some unexpected behaviour { param2 = param1; param1 = param2 - range; @@ -402,24 +403,26 @@ void FfbproSetEnvelope( } volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + + FFP_Share_Basic_common_t *effect_share = (FFP_Share_Basic_common_t *)&effect->share_data; - effect->usb_attackLevel = data->attackLevel; - effect->usb_fadeLevel = data->fadeLevel; - effect->usb_fadeTime = data->fadeTime; + effect_share->usb_attackLevel = data->attackLevel; + effect_share->usb_fadeLevel = data->fadeLevel; + effect_share->usb_fadeTime = data->fadeTime; if (data->fadeTime == USB_DURATION_INFINITE) // is this check needed? Only if duration is not INF but fadeTime is INF - can this occur? midi_fadeTime = MIDI_DURATION_INFINITE; else - midi_fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); + midi_fadeTime = UsbUint16ToMidiUint14_Time(effect_share->usb_duration - effect_share->usb_fadeTime); FfbSetParamMidi_14bit(effect->state, &(midi_data->fadeTime), eid, FFP_MIDI_MODIFY_FADETIME, midi_fadeTime); FfbSetParamMidi_14bit(effect->state, &(midi_data->attackTime), eid, FFP_MIDI_MODIFY_ATTACKTIME, UsbUint16ToMidiUint14_Time(data->attackTime)); FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, - FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(effect->range, data->fadeLevel)); + FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(effect_share->range, data->fadeLevel)); FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, - FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(effect->range, data->attackLevel)); + FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(effect_share->range, data->attackLevel)); } void FfbproSetCondition( @@ -429,6 +432,7 @@ void FfbproSetCondition( uint8_t eid = data->effectBlockIndex; volatile FFP_MIDI_Effect_Basic *common_midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + FFP_Share_Condition *effect_share = (FFP_Share_Condition *)&effect->share_data; /* USB effect data: uint8_t effectBlockIndex; // 1..40 @@ -454,7 +458,7 @@ void FfbproSetCondition( FlushDebugBuffer(); } - int8_t coeff = CalcGainCoeff(data->positiveCoefficient, effect->usb_gain); //Scale coefficients by gain since FFP conditional effects don't have gain parameter + int8_t coeff = CalcGainCoeff(data->positiveCoefficient, effect_share->usb_gain); //Scale coefficients by gain since FFP conditional effects don't have gain parameter switch (common_midi_data->waveForm) { case 0x0d: // spring (midi: 0x0d) @@ -467,14 +471,14 @@ void FfbproSetCondition( uint16_t midi_offsetAxis1; if (data->parameterBlockOffset == 0) { - effect->usb_coeffAxis0 = data->positiveCoefficient; + effect_share->usb_coeffAxis0 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(coeff)); FfbSetParamMidi_14bit(effect->state, &(midi_data->offsetAxis0), eid, FFP_MIDI_MODIFY_OFFSETAXIS0, UsbInt8ToMidiInt14(data->cpOffset)); } else { - effect->usb_coeffAxis1 = data->positiveCoefficient; + effect_share->usb_coeffAxis1 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(coeff)); if (data->cpOffset == 0x80) @@ -494,11 +498,11 @@ void FfbproSetCondition( (FFP_MIDI_Effect_Friction *)&effect->data; if (data->parameterBlockOffset == 0) { - effect->usb_coeffAxis0 = data->positiveCoefficient; + effect_share->usb_coeffAxis0 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(coeff)); } else { - effect->usb_coeffAxis1 = data->positiveCoefficient; + effect_share->usb_coeffAxis1 = data->positiveCoefficient; FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(coeff)); } @@ -543,6 +547,8 @@ void FfbproSetPeriodic( volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + FFP_Share_Periodic_Ramp *effect_share = (FFP_Share_Periodic_Ramp *)&effect->share_data; + uint16_t frequency = 0x0001; // 1Hz // Calculate frequency (in MIDI it is in units of Hz and can have value from 1 to 169Hz) @@ -552,9 +558,9 @@ void FfbproSetPeriodic( frequency = UsbPeriodToFrequencyHz(data->period); } - effect->frequency = frequency; + effect_share->frequency = frequency; - uint16_t sampleRate = FfbproCalcSampleRate(effect->usb_samplePeriod, frequency); //Sample rate may need to change as a result of frequency if usb value set to default + uint16_t sampleRate = FfbproCalcSampleRate(effect_share->usb_samplePeriod, frequency); //Sample rate may need to change as a result of frequency if usb value set to default FfbSetParamMidi_14bit(effect->state, &(midi_data->frequency), eid, FFP_MIDI_MODIFY_FREQUENCY, UsbUint16ToMidiUint14(frequency)); @@ -574,36 +580,36 @@ void FfbproSetPeriodic( case 7: { midi_data->waveForm = 2; - effect->invert = 0; + effect_share->invert = 0; break; } case 1: case 2: { midi_data->waveForm = 3; - effect->invert = 0; + effect_share->invert = 0; break; } case 3: case 4: { midi_data->waveForm = 2; - effect->invert = 1; //i.e. -sine + effect_share->invert = 1; //i.e. -sine break; } case 5: case 6: { midi_data->waveForm = 3; - effect->invert = 1; + effect_share->invert = 1; break; } } } else { if ((data->phase > 64) && (data->phase < 192)) { //for square, tri, sawtooth - effect->invert = 1; + effect_share->invert = 1; } else { - effect->invert = 0; + effect_share->invert = 0; } } } @@ -616,13 +622,13 @@ void FfbproSetPeriodic( FFP_MIDI_MODIFY_MAGNITUDE, FfbproCalcLevel(range, data->magnitude)); // Check whether envelope levels need to be updated too - if (range != effect->range) + if (range != effect_share->range) { - effect->range = range; + effect_share->range = range; FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, - FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(range, effect->usb_fadeLevel)); + FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(range, effect_share->usb_fadeLevel)); FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, - FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(range, effect->usb_attackLevel)); + FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(range, effect_share->usb_attackLevel)); } } @@ -669,8 +675,10 @@ void FfbproSetConstantForce( } volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; - - effect->usb_magnitude = data->magnitude; + + FFP_Share_Constant *effect_share = (FFP_Share_Constant *)&effect->share_data; + + effect_share->usb_magnitude = data->magnitude; uint8_t midi_magnitude; @@ -685,7 +693,7 @@ void FfbproSetConstantForce( FfbSetParamMidi_7bit(effect->state, &(midi_data->magnitude), eid, FFP_MIDI_MODIFY_MAGNITUDE, midi_magnitude); FfbSetParamMidi_14bit(effect->state, &(midi_data->direction), eid, - FFP_MIDI_MODIFY_DIRECTION, FfbproConvertDirection(effect->usb_direction, (data->magnitude < 0))); + FFP_MIDI_MODIFY_DIRECTION, FfbproConvertDirection(effect_share->usb_direction, (data->magnitude < 0))); //reciprocal direction if -ve midi_data->param1 = 0x007F; // never again modified @@ -717,16 +725,18 @@ void FfbproSetRampForce( */ volatile FFP_MIDI_Effect_Basic *midi_data = (volatile FFP_MIDI_Effect_Basic *)&effect->data; + + FFP_Share_Periodic_Ramp *effect_share = (FFP_Share_Periodic_Ramp *)&effect->share_data; // Same approach as periodic waveforms - int8_t offset = ((int16_t)data->start + (int16_t)data->end)/2; //Could be done more efficiently without casting + int8_t offset = ((int16_t)data->start + (int16_t)data->end)/2; //Finding midpoint could be done more efficiently without casting uint8_t magnitude; if (data->start > data->end) { - effect->invert = 1; //Ramp Down + effect_share->invert = 1; //Ramp Down magnitude = data->start - data->end; } else { - effect->invert = 0; //Ramp Up + effect_share->invert = 0; //Ramp Up magnitude = data->end - data->start; } @@ -740,30 +750,15 @@ void FfbproSetRampForce( FFP_MIDI_MODIFY_MAGNITUDE, FfbproCalcLevel(range, magnitude)); // Check whether envelope levels need to be updated too - if (range != effect->range) + if (range != effect_share->range) { - effect->range = range; + effect_share->range = range; FfbSetParamMidi_7bit(effect->state, &(midi_data->fadeLevel), eid, - FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(range, effect->usb_fadeLevel)); + FFP_MIDI_MODIFY_FADE, FfbproCalcLevel(range, effect_share->usb_fadeLevel)); FfbSetParamMidi_7bit(effect->state, &(midi_data->attackLevel), eid, - FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(range, effect->usb_attackLevel)); + FFP_MIDI_MODIFY_ATTACK, FfbproCalcLevel(range, effect_share->usb_attackLevel)); } - - -/* - uint16_t midi_param1; - - if (data->start < 0) - midi_param1 = 0x0100 | (-(data->start+1)); - else - midi_param1 = data->start; - - FfbSetParamMidi_14bit(effect->state, &(midi_data->param1), eid, - FFP_MIDI_MODIFY_PARAM1, midi_param1); - FfbSetParamMidi_14bit(effect->state, &(midi_data->param2), eid, - FFP_MIDI_MODIFY_PARAM2, UsbInt8ToMidiInt14(data->end)); - */ } int FfbproSetEffect( @@ -798,18 +793,30 @@ int FfbproSetEffect( //Buttons 1-9 from LSB FfbSetParamMidi_14bit(effect->state, &(midi_data->triggerButton), eid, FFP_MIDI_MODIFY_TRIGGERBUTTON, (buttonBits & 0x7F) + ( (buttonBits & 0x0180) << 1 )); - + + uint8_t reciprocal = 0; + bool is_constant = false; + // Fill in the effect type specific data switch (data->effectType) - { + { + case USB_EFFECT_CONSTANT: + { + FFP_Share_Constant *effect_share = (FFP_Share_Constant *)&effect->share_data; + is_constant = true; + if (effect_share->usb_magnitude < 0) { + reciprocal = 1; + } + effect_share->usb_direction = data->directionX; + } case USB_EFFECT_SQUARE: case USB_EFFECT_SINE: case USB_EFFECT_TRIANGLE: case USB_EFFECT_SAWTOOTHDOWN: case USB_EFFECT_SAWTOOTHUP: - case USB_EFFECT_CONSTANT: case USB_EFFECT_RAMP: { + FFP_Share_Basic_common_t *effect_share = (FFP_Share_Basic_common_t *)&effect->share_data; /* MIDI effect data: uint8_t command; // always 0x23 -- start counting checksum from here @@ -836,23 +843,22 @@ int FfbproSetEffect( FFP_MIDI_MODIFY_GAIN, (data->gain >> 1) & 0x7f); // Convert direction - effect->usb_direction = data->directionX; FfbSetParamMidi_14bit(effect->state, &(midi_data->direction), eid, - FFP_MIDI_MODIFY_DIRECTION, FfbproConvertDirection(data->directionX, (effect->usb_magnitude < 0))); - //reciprocal only if -ve constant force - + FFP_MIDI_MODIFY_DIRECTION, FfbproConvertDirection(data->directionX, reciprocal)); //reciprocal only if -ve constant force // Recalculate fadeTime for MIDI since change to duration changes the fadeTime too + effect_share->usb_duration = data->duration; // store for later calculation of + uint16_t midi_fadeTime; if (data->duration == USB_DURATION_INFINITE) { midi_fadeTime = MIDI_DURATION_INFINITE; } else { - if (effect->usb_fadeTime == USB_DURATION_INFINITE) { + if (effect_share->usb_fadeTime == USB_DURATION_INFINITE) { midi_fadeTime = MIDI_DURATION_INFINITE; } else { - if (effect->usb_duration > effect->usb_fadeTime) { + if (data->duration > effect_share->usb_fadeTime) { // add some safety and special case handling - midi_fadeTime = UsbUint16ToMidiUint14_Time(effect->usb_duration - effect->usb_fadeTime); + midi_fadeTime = UsbUint16ToMidiUint14_Time(data->duration - effect_share->usb_fadeTime); } else { midi_fadeTime = midi_data->duration; } @@ -860,13 +866,18 @@ int FfbproSetEffect( } FfbSetParamMidi_14bit(effect->state, &(midi_data->fadeTime), eid, FFP_MIDI_MODIFY_FADETIME, midi_fadeTime); - - effect->usb_samplePeriod = data->samplePeriod; - - uint16_t sampleRate = FfbproCalcSampleRate(data->samplePeriod, effect->frequency); + + if (!is_constant) + { + FFP_Share_Periodic_Ramp *effect_share = (FFP_Share_Periodic_Ramp *)&effect->share_data; + + effect_share->usb_samplePeriod = data->samplePeriod; + + uint16_t sampleRate = FfbproCalcSampleRate(data->samplePeriod, effect_share->frequency); - FfbSetParamMidi_14bit(effect->state, &(midi_data->sampleRate), eid, - FFP_MIDI_MODIFY_SAMPLERATE, UsbUint16ToMidiUint14(sampleRate)); + FfbSetParamMidi_14bit(effect->state, &(midi_data->sampleRate), eid, + FFP_MIDI_MODIFY_SAMPLERATE, UsbUint16ToMidiUint14(sampleRate)); + } } break; @@ -889,12 +900,14 @@ int FfbproSetEffect( volatile FFP_MIDI_Effect_Spring_Inertia_Damper *midi_data = (FFP_MIDI_Effect_Spring_Inertia_Damper *)&effect->data; - - effect->usb_gain = data->gain; //Scale coefficients by gain since FFP conditional effects don't have gain parameter + + FFP_Share_Condition *effect_share = (FFP_Share_Condition *)&effect->share_data; + + effect_share->usb_gain = data->gain; //Scale coefficients by gain since FFP conditional effects don't have gain parameter FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, - FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis0, data->gain))); + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect_share->usb_coeffAxis0, data->gain))); FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, - FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis1, data->gain))); + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(CalcGainCoeff(effect_share->usb_coeffAxis1, data->gain))); midi_data_len = sizeof(FFP_MIDI_Effect_Spring_Inertia_Damper); @@ -916,11 +929,13 @@ int FfbproSetEffect( volatile FFP_MIDI_Effect_Friction *midi_data = (FFP_MIDI_Effect_Friction *)&effect->data; - effect->usb_gain = data->gain; //Scale coefficients by gain since FFP conditional effects don't have gain parameter + FFP_Share_Condition *effect_share = (FFP_Share_Condition *)&effect->share_data; + + effect_share->usb_gain = data->gain; //Scale coefficients by gain since FFP conditional effects don't have gain parameter FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis0), eid, - FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis0, data->gain))); + FFP_MIDI_MODIFY_COEFFAXIS0, UsbInt8ToMidiInt14(CalcGainCoeff(effect_share->usb_coeffAxis0, data->gain))); FfbSetParamMidi_14bit(effect->state, &(midi_data->coeffAxis1), eid, - FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(CalcGainCoeff(effect->usb_coeffAxis1, data->gain))); + FFP_MIDI_MODIFY_COEFFAXIS1, UsbInt8ToMidiInt14(CalcGainCoeff(effect_share->usb_coeffAxis1, data->gain))); midi_data_len = sizeof(FFP_MIDI_Effect_Friction); @@ -972,4 +987,55 @@ void FfbproCreateNewEffect( midi_data->param2 = 0x0000; else midi_data->param2 = 0x0101; + + //Set defaults for shared data + switch (inData->effectType) { + case USB_EFFECT_SQUARE: + case USB_EFFECT_SINE: + case USB_EFFECT_TRIANGLE: + case USB_EFFECT_SAWTOOTHDOWN: + case USB_EFFECT_SAWTOOTHUP: + case USB_EFFECT_RAMP: + { + FFP_Share_Periodic_Ramp *effect_share = (FFP_Share_Periodic_Ramp *)&effect->share_data; + + effect_share->usb_duration = USB_DURATION_INFINITE; + effect_share->usb_fadeTime = USB_DURATION_INFINITE; + effect_share->usb_attackLevel = 0xFF; + effect_share->usb_fadeLevel = 0xFF; + effect_share->usb_samplePeriod = USB_SAMPLEPERIOD_DEFAULT; + + effect_share->frequency = 1; // Hz constant for Ramp + effect_share->invert = 0; + effect_share->range = 255; + break; + } + case USB_EFFECT_CONSTANT: + { + FFP_Share_Constant *effect_share = (FFP_Share_Constant *)&effect->share_data; + + effect_share->usb_duration = USB_DURATION_INFINITE; + effect_share->usb_fadeTime = USB_DURATION_INFINITE; + effect_share->usb_attackLevel = 0xFF; + effect_share->usb_fadeLevel = 0xFF; + effect_share->usb_magnitude = 0; + effect_share->usb_direction = 0; + + effect_share->range = 255; //constant for Constant + break; + } + case USB_EFFECT_SPRING: + case USB_EFFECT_DAMPER: + case USB_EFFECT_INERTIA: + case USB_EFFECT_FRICTION: + { + FFP_Share_Condition *effect_share = (FFP_Share_Condition *)&effect->share_data; + + effect_share->usb_gain = 0xFF; + effect_share->usb_coeffAxis0 = 0; + effect_share->usb_coeffAxis1 = 0; + } + } + + } \ No newline at end of file diff --git a/ffb-pro.h b/ffb-pro.h index 11fc984..0de52f2 100644 --- a/ffb-pro.h +++ b/ffb-pro.h @@ -53,6 +53,47 @@ typedef struct uint16_t coeffAxis1; } FFP_MIDI_Effect_Friction; +// Data structures: to be shared between Output reports for calculating MIDI parameters coupled to multiple USB parameters +// Set [MAX_SHARE_DATA] bytes to largest of +typedef struct + { + uint16_t usb_duration; + uint16_t usb_fadeTime; + uint8_t usb_attackLevel; + uint8_t usb_fadeLevel; + uint8_t range; + uint16_t usb_samplePeriod; + uint16_t frequency; + uint8_t invert; + } FFP_Share_Periodic_Ramp; + +typedef struct + { + uint16_t usb_duration; + uint16_t usb_fadeTime; + uint8_t usb_attackLevel; + uint8_t usb_fadeLevel; + uint8_t range; //Not varied for constant but simplifies use of common structure + uint8_t usb_magnitude; + uint8_t usb_direction; + } FFP_Share_Constant; + +typedef struct + { + uint16_t usb_duration; + uint16_t usb_fadeTime; + uint8_t usb_attackLevel; + uint8_t usb_fadeLevel; + uint8_t range; + } FFP_Share_Basic_common_t; + +typedef struct + { + uint8_t usb_coeffAxis0; + uint8_t usb_coeffAxis1; + uint8_t usb_gain; + } FFP_Share_Condition; + void FfbproEnableInterrupts(void); uint8_t FfbproDeviceControl(uint8_t usb_control); const uint8_t* FfbproGetSysExHeader(uint8_t* hdr_len); diff --git a/ffb.c b/ffb.c index 10a30f3..e2274b2 100644 --- a/ffb.c +++ b/ffb.c @@ -380,20 +380,6 @@ void FfbOnCreateNewEffect(USB_FFBReport_CreateNewEffect_Feature_Data_t* inData, outData->loadStatus = 1; // 1=Success,2=Full,3=Error volatile TEffectState* effect = &gEffectStates[outData->effectBlockIndex]; - - effect->usb_duration = USB_DURATION_INFINITE; - effect->usb_fadeTime = USB_DURATION_INFINITE; - effect->usb_gain = 0xFF; - effect->usb_attackLevel = 0xFF; - effect->usb_fadeLevel = 0xFF; - effect->usb_magnitude = 0; - effect->usb_direction = 0; - effect->invert = 0; - effect->range = 255; - effect->frequency = 1; // Hz - effect->usb_samplePeriod = USB_SAMPLEPERIOD_DEFAULT; - effect->usb_coeffAxis0 = 0; - effect->usb_coeffAxis1 = 0; ((midi_data_common_t*)effect->data)->waveForm = midi_effect_type; @@ -453,7 +439,6 @@ void FfbHandle_SetEffect(USB_FFBReport_SetEffect_Output_Data_t *data) } else { midi_duration = UsbUint16ToMidiUint14_Time(data->duration); // MIDI unit is 2ms } - effect->usb_duration = data->duration; // store for later calculation of ffb->ModifyDuration(effect->state, &(midi_data->duration), data->effectBlockIndex, midi_duration); @@ -879,7 +864,7 @@ uint8_t FfbDebugListEffects(uint8_t *index) LogTextP(PSTR(" (Disabled)\n")); else LogTextP(PSTR(" (Enabled)\n")); - +/* //These variables don't now exist for all effects - could be accessed for some effects share_data if (e->state) { LogTextP(PSTR(" duration=")); @@ -889,7 +874,7 @@ uint8_t FfbDebugListEffects(uint8_t *index) LogTextP(PSTR("\n gain=")); LogBinary(&e->usb_gain, 1); } - +*/ *index = *index + 1; return 1; diff --git a/ffb.h b/ffb.h index 53ee804..3c777a7 100644 --- a/ffb.h +++ b/ffb.h @@ -300,6 +300,7 @@ void FfbEnableEffectId(uint8_t inId, uint8_t inEnable); #define USB_DCTRL_CONTINUE 0x06 #define MAX_MIDI_MSG_LEN 27 /* enough to hold longest midi message data part, FFP_MIDI_Effect_Basic */ +#define MAX_SHARE_DATA 12 /* enough bytes to hold all data that must be shared between Output reports for any effect type*/ /* start of midi data common for both pro and wheel protocols */ typedef struct { @@ -311,14 +312,15 @@ typedef struct { typedef struct { uint8_t state; // see constants - uint16_t usb_duration, usb_fadeTime; // used to calculate fadeTime to MIDI, since in USB it is given as time difference from the end while in MIDI it is given as time from start + uint8_t share_data[MAX_SHARE_DATA]; // All data to be shared between Output reports for calculating MIDI parameters coupled to multiple USB parameters +/* uint16_t usb_duration, usb_fadeTime; // used to calculate fadeTime to MIDI, since in USB it is given as time difference from the end while in MIDI it is given as time from start // These are used to calculate effect parameters when not all data is available in the isolated output report uint8_t usb_gain, usb_attackLevel, usb_fadeLevel, usb_direction, invert, range; uint16_t frequency, usb_samplePeriod; int16_t usb_magnitude; //Signed for Constant Force use only - int8_t usb_coeffAxis0, usb_coeffAxis1; + int8_t usb_coeffAxis0, usb_coeffAxis1; */ volatile uint8_t data[MAX_MIDI_MSG_LEN]; - } TEffectState; // This takes up a lot of RAM when stored for all effects. Can stored parameters be rationalised? + } TEffectState; typedef struct { From f436bd65f99077eaeb85f14bc870df5d9f39761f Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sun, 8 Oct 2023 21:19:56 +0100 Subject: [PATCH 21/29] Increase simultaneous effects from 10 to 16 in PID Pool Feature Report --- ffb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffb.c b/ffb.c index e2274b2..3fe3558 100644 --- a/ffb.c +++ b/ffb.c @@ -463,7 +463,7 @@ void FfbOnPIDPool(USB_FFBReport_PIDPool_Feature_Data_t *data) data->reportId = 7; data->ramPoolSize = 0xFFFF; - data->maxSimultaneousEffects = 0x0A; // FFP supports playing up to 10 simultaneous effects + data->maxSimultaneousEffects = 0x10; // FFP supports playing up to 16 simultaneous effects data->memoryManagement = 3; } From 49ebca362f02b09922a59750b90796e71eb24d48 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sun, 8 Oct 2023 22:16:06 +0100 Subject: [PATCH 22/29] Comments only --- ffb-pro.h | 8 ++++---- ffb.c | 14 -------------- ffb.h | 8 +------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/ffb-pro.h b/ffb-pro.h index 0de52f2..866c723 100644 --- a/ffb-pro.h +++ b/ffb-pro.h @@ -12,7 +12,7 @@ typedef struct { uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 2=sine, 5=Square, 6=RampUp, 7=RampDown, 8=Triange, 0x12=Constant - uint8_t unknown1; // Overwrite an allocated effect + uint8_t unknown1; // Overwrite an allocated effect - leave as unknown1 because we don't need it and don't know what it does for wheel uint16_t duration; // unit=2ms uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t direction; @@ -21,7 +21,7 @@ typedef struct uint16_t truncate; //default 0x10 0x4e = 10000 for full waveform uint8_t attackLevel; uint16_t attackTime; - uint8_t magnitude; + uint8_t magnitude; //i.e. envelope sustain uint16_t fadeTime; uint8_t fadeLevel; uint16_t frequency; // unit=Hz; 1 for constant and ramps @@ -33,7 +33,7 @@ typedef struct { uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 0xd=Spring, 0x0e=Damper, 0xf=Inertia - uint8_t unknown1; // Overwrite an allocated effect + uint8_t unknown1; // Overwrite an allocated effect - leave as unknown1 because we don't need it and don't know what it does for wheel uint16_t duration; // unit=2ms uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t coeffAxis0; @@ -46,7 +46,7 @@ typedef struct { uint8_t command; // always 0x23 -- start counting checksum from here uint8_t waveForm; // 0x10=Friction - uint8_t unknown1; // Overwrite an allocated effect + uint8_t unknown1; // Overwrite an allocated effect - leave as unknown1 because we don't need it and don't know what it does for wheel uint16_t duration; // unit=2ms uint16_t triggerButton; // Bitwise buttons 1 to 9 from LSB uint16_t coeffAxis0; diff --git a/ffb.c b/ffb.c index 3fe3558..c637e0e 100644 --- a/ffb.c +++ b/ffb.c @@ -824,20 +824,6 @@ void FfbSendDisable() { } -/* -typedef struct { - uint8_t state; // see constants - uint16_t usb_duration, usb_fadeTime; // used to calculate fadeTime to MIDI, since in USB it is given as time difference from the end while in MIDI it is given as time from start - // These are used to calculate effects of USB gain to MIDI data - uint8_t usb_gain, usb_offset, usb_attackLevel, usb_fadeLevel; - uint8_t usb_magnitude; - FFP_MIDI_Effect_Basic data; // For FFP, this is enough for all types of effects - cast for other effect types when necessary - } TEffectState; - -const uint8_t MEffectState_Allocated = 0x01; -const uint8_t MEffectState_Playing = 0x02; -const uint8_t MEffectState_SentToJoystick = 0x04; -*/ uint8_t FfbDebugListEffects(uint8_t *index) { diff --git a/ffb.h b/ffb.h index 3c777a7..afbf035 100644 --- a/ffb.h +++ b/ffb.h @@ -152,7 +152,7 @@ typedef struct typedef struct { // FFB: Device Control Output Report uint8_t reportId; // =12 - uint8_t control; // 1=Enable Actuators, 2=Disable Actuators, 4=Stop All Effects, 8=Reset, 16=Pause, 32=Continue + uint8_t control; // 1=Enable Actuators, 2=Disable Actuators, 3=Stop All Effects, 4=Reset, 5=Pause, 6=Continue } USB_FFBReport_DeviceControl_Output_Data_t; typedef struct @@ -313,12 +313,6 @@ typedef struct { typedef struct { uint8_t state; // see constants uint8_t share_data[MAX_SHARE_DATA]; // All data to be shared between Output reports for calculating MIDI parameters coupled to multiple USB parameters -/* uint16_t usb_duration, usb_fadeTime; // used to calculate fadeTime to MIDI, since in USB it is given as time difference from the end while in MIDI it is given as time from start - // These are used to calculate effect parameters when not all data is available in the isolated output report - uint8_t usb_gain, usb_attackLevel, usb_fadeLevel, usb_direction, invert, range; - uint16_t frequency, usb_samplePeriod; - int16_t usb_magnitude; //Signed for Constant Force use only - int8_t usb_coeffAxis0, usb_coeffAxis1; */ volatile uint8_t data[MAX_MIDI_MSG_LEN]; } TEffectState; From 94e03ff22b2e6360ccd9041fa070023353256e6b Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Tue, 12 Sep 2023 22:53:33 +0100 Subject: [PATCH 23/29] Allow new effect to be created over serial port Used a spoof Report ID to allow this to be triggered. Could the spoof ID 91 be triggered by anything else?? --- ffb.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ffb.c b/ffb.c index c637e0e..a5b2bdf 100644 --- a/ffb.c +++ b/ffb.c @@ -312,7 +312,9 @@ void FfbOnUsbData(uint8_t *data, uint16_t len) LogReport(PSTR("Usb =>"), OutReportSize, data, len); uint8_t effectId = data[1]; // effectBlockIndex is always the second byte. - + + USB_FFBReport_PIDBlockLoad_Feature_Data_t pidBlockLoadData; //do nothing with this + switch (data[0]) // reportID { case 1: @@ -356,6 +358,9 @@ void FfbOnUsbData(uint8_t *data, uint16_t len) case 14: FfbHandle_SetCustomForce((USB_FFBReport_SetCustomForce_Output_Data_t*) data); break; + case 91: //=0x5B This is a spoofed ID to allow CreateNewEffect to be triggered over USB virtual COM PORT, since it is a Feature Report not an Output Report + FfbOnCreateNewEffect((USB_FFBReport_CreateNewEffect_Feature_Data_t*) data, &pidBlockLoadData); + break; default: break; }; From bf9bc2cdbdb9a41ab3a86aa684ed0d09986ee05d Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sat, 30 Sep 2023 23:06:52 +0100 Subject: [PATCH 24/29] Bug Fix for crash when using Create New Effect over Serial Port Changed spoofed ID to 15 and added entry in OutReportSize Was causing pointer overflow. --- ffb.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ffb.c b/ffb.c index a5b2bdf..33c8404 100644 --- a/ffb.c +++ b/ffb.c @@ -292,6 +292,7 @@ const uint16_t OutReportSize[] = { sizeof(USB_FFBReport_DeviceControl_Output_Data_t), // 12 sizeof(USB_FFBReport_DeviceGain_Output_Data_t), // 13 sizeof(USB_FFBReport_SetCustomForce_Output_Data_t), // 14 + sizeof(USB_FFBReport_CreateNewEffect_Feature_Data_t), // 15 SPOOFED ID }; void FfbHandle_EffectOperation(USB_FFBReport_EffectOperation_Output_Data_t *data); @@ -358,7 +359,7 @@ void FfbOnUsbData(uint8_t *data, uint16_t len) case 14: FfbHandle_SetCustomForce((USB_FFBReport_SetCustomForce_Output_Data_t*) data); break; - case 91: //=0x5B This is a spoofed ID to allow CreateNewEffect to be triggered over USB virtual COM PORT, since it is a Feature Report not an Output Report + case 15: //This is a spoofed ID to allow CreateNewEffect to be triggered over USB virtual COM PORT, since it is a Feature Report not an Output Report FfbOnCreateNewEffect((USB_FFBReport_CreateNewEffect_Feature_Data_t*) data, &pidBlockLoadData); break; default: From c28f6369edecd9030f59a524c824a86aa9cd8900 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:08:17 +0100 Subject: [PATCH 25/29] Only allow spoofing USB "Create New Effect" reports when USB debug is enabled Preprocessor switch --- ffb.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ffb.c b/ffb.c index 33c8404..99991f6 100644 --- a/ffb.c +++ b/ffb.c @@ -314,7 +314,7 @@ void FfbOnUsbData(uint8_t *data, uint16_t len) uint8_t effectId = data[1]; // effectBlockIndex is always the second byte. - USB_FFBReport_PIDBlockLoad_Feature_Data_t pidBlockLoadData; //do nothing with this + switch (data[0]) // reportID { @@ -359,9 +359,14 @@ void FfbOnUsbData(uint8_t *data, uint16_t len) case 14: FfbHandle_SetCustomForce((USB_FFBReport_SetCustomForce_Output_Data_t*) data); break; - case 15: //This is a spoofed ID to allow CreateNewEffect to be triggered over USB virtual COM PORT, since it is a Feature Report not an Output Report + #ifdef DEBUG_ENABLE_USB // only want to allow this behaviour when debugging since it should not be triggered otherwise + case 15: + {// This is a spoofed ID to allow CreateNewEffect to be triggered over USB virtual COM PORT, since it is a Feature Report not an Output Report + USB_FFBReport_PIDBlockLoad_Feature_Data_t pidBlockLoadData; // do nothing with this FfbOnCreateNewEffect((USB_FFBReport_CreateNewEffect_Feature_Data_t*) data, &pidBlockLoadData); break; + } + #endif // DEBUG_ENABLE_USB default: break; }; From ea8ab410ecb1341c15b05ef655fc0739d84bf828 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:16:48 +0100 Subject: [PATCH 26/29] Disable USB Debugging & Logging --- debug.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debug.h b/debug.h index d567117..3cda918 100644 --- a/debug.h +++ b/debug.h @@ -46,7 +46,7 @@ bool DoDebug(const uint8_t type); // If below are defined, code for respective debug target is included into build //#define DEBUG_ENABLE_UART -#define DEBUG_ENABLE_USB +//#define DEBUG_ENABLE_USB #define DEBUG_BUFFER_SIZE 512 From b26fd91e220b355cc1f1fee871883dc3bfbc0f61 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:25:56 +0100 Subject: [PATCH 27/29] Disable USB COM Port descriptors --- Descriptors.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Descriptors.h b/Descriptors.h index eb90d34..6a46027 100644 --- a/Descriptors.h +++ b/Descriptors.h @@ -38,7 +38,7 @@ // Define: ENABLE_JOYSTICK_SERIAL // When defined, includes USB COM serial port to the device // in addition to joystick. -#define ENABLE_JOYSTICK_SERIAL +//#define ENABLE_JOYSTICK_SERIAL /* Includes: */ #include From 439b5d5187fa25af83499b5bade34369ed63cbf0 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Fri, 20 Oct 2023 22:57:16 +0100 Subject: [PATCH 28/29] Add hex to downloads and renamed _r54 hex. --- ...joy-r54.hex => adaptffbjoy-0.3.0(r54).hex} | 0 downloads/adaptffbjoy-0.5.0beta.hex | 1062 +++++++++++++++++ 2 files changed, 1062 insertions(+) rename downloads/{adaptffbjoy-r54.hex => adaptffbjoy-0.3.0(r54).hex} (100%) create mode 100644 downloads/adaptffbjoy-0.5.0beta.hex diff --git a/downloads/adaptffbjoy-r54.hex b/downloads/adaptffbjoy-0.3.0(r54).hex similarity index 100% rename from downloads/adaptffbjoy-r54.hex rename to downloads/adaptffbjoy-0.3.0(r54).hex diff --git a/downloads/adaptffbjoy-0.5.0beta.hex b/downloads/adaptffbjoy-0.5.0beta.hex new file mode 100644 index 0000000..dc692cb --- /dev/null +++ b/downloads/adaptffbjoy-0.5.0beta.hex @@ -0,0 +1,1062 @@ +:1000000029C7000044C7000041C700003FC70000E7 +:100010003DC700003BC7000039C7000037C70000DC +:1000200035C7000033C700000C94161C2FC7000012 +:100030002DC700002BC7000029C7000027C70000FC +:1000400025C7000023C7000021C700001FC700000C +:100050001DC700001BC7000019C7000017C700001C +:1000600015C7000013C7000011C700000FC700002C +:100070000DC700000C948F0809C7000007C70000D7 +:1000800005C7000003C7000001C70000FFC600004D +:10009000FDC60000FBC60000F9C60000F7C6000060 +:1000A000F5C60000F3C60000F1C600000501090412 +:1000B000A10185010901A1000930093109321600A9 +:1000C000FE26FF01350046FF0395037510810209E6 +:1000D000351500253F3500452E95017506810275C1 +:1000E0000295018101650009330934150025FF459A +:1000F000FF7508750895028102C009361580257FB5 +:1001000045FF750895018102050209BB15C0253F11 +:10011000457F750895018102050919012908150017 +:1001200025014501750195088102190929107501FC +:100130009508810205010939950175041500250707 +:10014000463B0165148142750495018101550065A6 +:1001500000050F0992A1028502099F09A009A409BF +:10016000A509A615002501350045017501950581F4 +:100170000295038103099415002501350045017599 +:1001800001950181020922150125283501452875AF +:100190000795018102C00921A102850109221501EB +:1001A0002528350145287508950191020925A102E8 +:1001B0000926092709300931093209330934094070 +:1001C0000941094209430928250C15013501450C4F +:1001D000750895019100C009500954095115002670 +:1001E000FF7F350046FF7F66031055FD75109503B0 +:1001F000910255006600000952150026FF003500E7 +:1002000046102775089501910209531501250835F7 +:100210000145087508950191020955A102050109DA +:1002200030093115002501750195029102C0050FB5 +:10023000095695019102950591030957A1020B01F9 +:10024000000A000B02000A0066140055FE15002685 +:10025000B400350047A08C000066000075089502C8 +:1002600091025500660000C0050F66031055FD158C +:100270000026FF7F350046FF7F7510950166000060 +:100280005500C0050F095AA1028502092215012552 +:100290002835014528750895019102095B095D150E +:1002A0000026FF00350046102795029102095C09DF +:1002B0005E66031055FD26FF7F46FF7F7510910295 +:1002C00045006600005500C0095FA10285030922B0 +:1002D0001501252835014528750895019102092346 +:1002E0001500250135004501750495019102095855 +:1002F000A1020B01000A000B02000A007502950220 +:100300009102C01580257F36F0D846102709607508 +:10031000089501910236F0D84610270961950191A0 +:1003200002150026FF003500461027096309647591 +:100330000895024610279501C0096EA1028504099F +:1003400022150125283501452875089501910209D6 +:1003500070150026FF00350046102775089501919D +:1003600002096F1580257F36F0D846102795019138 +:1003700002097166140055FE150026FF003500477E +:10038000A08C00009102097226FF7F46FF7F660362 +:100390001055FD7510950191026600005500C009C9 +:1003A00073A1028505092215012528350145287507 +:1003B000089501910209701601FF26FF0036F0D85A +:1003C000461027751095019102C00974A102850697 +:1003D0000922150125283501452875089501910246 +:1003E000097509761580257F36F0D84610277508DF +:1003F00095029102C00968A102850709221501250D +:100400002835014528750895019102096C150026CB +:1004100010273500461027751095019102096915BE +:1004200081257F350046FF007508950C920201C0BA +:100430000966A10285080501093009311581257F6A +:10044000350046FF00750895029102C0050F097737 +:10045000A102850A092215012528350145287508BC +:10046000950191020978A1020979097A097B1501A0 +:100470002503750895019100C0097C150026FF0031 +:10048000350046FF009102C00990A102850B0922A8 +:100490002528150135014528750895019102C009E7 +:1004A00096A102850C099709980999099A099B094F +:1004B0009C15012506750895019100C0097DA102D2 +:1004C000850D097E150026FF0035004610277508AA +:1004D00095019102C0096BA102850E092215012523 +:1004E0002835014528750895019102096D150026EA +:1004F000FF00350046FF007508950191020951661D +:10050000031055FD150026FF7F350046FF7F75104F +:10051000950191025500660000C009ABA102850556 +:100520000925A102092609270930093109320933B1 +:10053000093409400941094209430928250C1501DC +:100540003501450C75089501B100C00501093B1541 +:100550000026FF01350046FF01750A9501B10275BD +:1005600006B101C0050F0989A102850609222528C7 +:1005700015013501452875089501B102098BA102C5 +:10058000098C098D098E2503150135014503750870 +:100590009501B100C009AC150027FFFF0000350030 +:1005A00047FFFF000075109501B100C0097FA1024F +:1005B00085070980751095011500350027FFFF009C +:1005C0000047FFFF0000B102098326FF0046FF003D +:1005D00075089501B10209A909AA750195021500CE +:1005E000250135004501B10275069501B103C0C072 +:1005F00005010904A10185010904A100093009319F +:10060000093216000026FF03350046FF03950375E7 +:1006100010810209351500253F3500452E950175DD +:1006200006810275029501810165000933093415BF +:100630000025FF45FF7508750895028102C009363F +:100640001580257F45FF750895018102050209BBCC +:100650001500253F453F75089501810205091901DF +:100660002908150025014501750195088102190920 +:100670002910750195088102050109399501750454 +:1006800015002507463B01651481427504950181DB +:100690000155006500050F0992A1028502099F0915 +:1006A000A009A409A509A615002501350045017575 +:1006B00001950581029503810309941500250135F3 +:1006C0000045017501950181020922150125283592 +:1006D000014528750795018102C00921A102850104 +:1006E0000922150125283501452875089501910233 +:1006F0000925A102092609270930093109320933E0 +:10070000093409400941094209430928250C15010A +:100710003501450C750895019100C009500954092F +:1007200051150026FF7F350046FF7F66031055FDFB +:1007300075109503910255006600000952150026B8 +:10074000FF003500461027750895019102095315E1 +:10075000012508350145087508950191020955A143 +:100760000205010930093115002501750195029135 +:1007700002C0050F0956950191029505910309578D +:10078000A1020B01000A000B02000A0066140055CA +:10079000FE150026B400350047A08C00006600005E +:1007A0007508950291025500660000C0050F6603AA +:1007B0001055FD150026FF7F350046FF7F7510950B +:1007C000016600005500C0050F095AA10285020903 +:1007D0002215012528350145287508950191020942 +:1007E0005B095D150026FF00350046102795029134 +:1007F00002095C095E66031055FD26FF7F46FF7FF8 +:100800007510910245006600005500C0095FA10205 +:10081000850309221501252835014528750895010C +:100820009102092315002501350045017504950144 +:1008300091020958A1020B01000A000B02000A00F4 +:10084000750295029102C01580257F36F0D84610BA +:1008500027096075089501910236F0D846102709DE +:100860006195019102150026FF0035004610270909 +:10087000630964750895024610279501C0096EA1A9 +:10088000028504092215012528350145287508959A +:100890000191020970150026FF00350046102775EA +:1008A0000895019102096F1580257F36F0D8461012 +:1008B0002795019102097166140055FE150026FF67 +:1008C00000350047A08C00009102097226FF7F4688 +:1008D000FF7F66031055FD751095019102660000BB +:1008E0005500C00973A10285050922150125283587 +:1008F00001452875089501910209701601FF26FF30 +:100900000036F0D8461027751095019102C0097481 +:10091000A1028506092215012528350145287508FB +:1009200095019102097509761580257F36F0D84624 +:100930001027750895029102C00968A10285070970 +:1009400022150125283501452875089501910209D0 +:100950006C1500261027350046102775109501915B +:100960000209691581257F350046FF007508950C41 +:10097000920201C00966A10285080501093009310A +:100980001581257F350046FF00750895029102C04C +:10099000050F0977A102850A0922150125283501CD +:1009A00045287508950191020978A1020979097A0B +:1009B000097B15012503750895019100C0097C1577 +:1009C0000026FF00350046FF009102C00990A102F9 +:1009D000850B092225281501350145287508950143 +:1009E0009102C00996A102850C09970998099909F5 +:1009F0009A099B099C15012506750895019100C06F +:100A0000097DA102850D097E150026FF00350046EF +:100A10001027750895019102C0096BA102850E0986 +:100A200022150125283501452875089501910209EF +:100A30006D150026FF00350046FF007508950191F1 +:100A400002095166031055FD150026FF7F3500464B +:100A5000FF7F7510950191025500660000C009AB3B +:100A6000A10285050925A1020926092709300931B6 +:100A70000932093309340940094109420943092867 +:100A8000250C15013501450C75089501B100C0050F +:100A900001093B150026FF01350046FF01750A9547 +:100AA00001B1027506B101C0050F0989A1028506D1 +:100AB0000922252815013501452875089501B1023F +:100AC000098BA102098C098D098E250315013501B9 +:100AD000450375089501B100C009AC150027FFFF5B +:100AE0000000350047FFFF000075109501B100C000 +:100AF000097FA10285070980751095011500350051 +:100B000027FFFF000047FFFF0000B102098326FF17 +:100B10000046FF0075089501B10209A909AA7501EF +:100B200095021500250135004501B10275069501B4 +:100B3000B103C0C01201100100000008EB035620F1 +:100B40000100010200011201100100000008EB0386 +:100B50004E2001000102000109022900010100C02C +:100B60003209040000020300000009211101000104 +:100B700022440507050203400001070581034000E8 +:100B8000010403090418034400650061006E00209D +:100B900000430061006D00650072006100000026E6 +:100BA000034C0055004600410020004A006F0079C8 +:100BB0000073007400690063006B0020007700463A +:100BC0000046004200000020034C00550046004152 +:100BD00000200057006800650065006C0020007769 +:100BE0000046004600420000004374726C5265717A +:100BF0002876616C2C6964782C726571293A0053EF +:100C0000746F70206D616E75616C3A002028456EBE +:100C100061626C6564290A00202844697361626C12 +:100C20006564290A002046726565002053656E746C +:100C30000020506C6179696E670A0020416C6C6F0E +:100C4000636174656400203D3E204D6964693A002B +:100C5000446576696365204761696E3A20004F7488 +:100C60006865722000426C6F636B20467265653A5E +:100C7000200020556E6B6E6F776E206F706572610D +:100C800074696F6E002053746F700020537461722A +:100C900074536F6C6F002053746172740045666604 +:100CA000656374204F7065726174696F6E3A0047B6 +:100CB00065745265706F72742050494420506F6F94 +:100CC0006C204665617475726500202062757474CD +:100CD0006F6E3D0020207265706561743D002020BC +:100CE000593D002020583D002020647572613D0070 +:100CF00020206761696E3D002020747970653D0099 +:100D00002020696420203D00536574204566666597 +:100D100063743A002C207374617475733D004372E0 +:100D200065617465204E657720456666656374204D +:100D30003D3E2069643D0055736220203D3E002009 +:100D400020656E6420203D00202073746172743D24 +:100D500000202069643D005365742052616D70204D +:100D6000466F7263653A0020206D61676E69747525 +:100D700064653D00202069643D0053657420436F25 +:100D80006E7374616E7420466F7263653A00202042 +:100D9000706572696F642020203D002020706861BA +:100DA0007365202020203D0020206F6666736574E7 +:100DB0002020203D0020206D61676E697475646598 +:100DC0003D00202069643D005365742050657269C0 +:100DD0006F6469633A002020636F6566662B3D008F +:100DE00020206F66667365743D002020626C6F631F +:100DF0006B203D00202069642020203D0053657455 +:100E000020436F6E646974696F6E3A0020206661DA +:100E1000646554696D6520203D0020206174746113 +:100E2000636B54696D653D00202066616465202018 +:100E30003D00202061747461636B3D002020696473 +:100E4000202020203D0053657420456E76656C6F30 +:100E500070653A0011241FBECFEFDAE0DEBFCDBFD0 +:100E600012E0A0E0B1E0ECE8F0E402C005900D92E1 +:100E7000A03CB107D9F716E0A0ECB2E001C01D928A +:100E8000AB37B107E1F7B4D40C944420B9C85F93F1 +:100E90005FB75F9357EE56BD4F93AF93BF93B6E0E6 +:100EA000A0913F0650914006452F53955093400620 +:100EB00053B15295507E477069F04530B9F04230D9 +:100EC00069F04330F1F04630E9F04130E1F0443070 +:100ED000D9F01BC0529556955E931AC0550F4C9190 +:100EE000542B5C935527551F5E9312C057FB550F2B +:100EF000550F4C91542B5C935527551F51F95E9318 +:100F000007C056955695569556954C91542B5C9323 +:100F1000A0933F06BF91AF914F915F915FBF5F91EB +:100F20001895DC0154E0A50F5C91509550FB50954D +:100F3000A1503C91369557955093660642E0340F88 +:100F400033705C91A1504C91469557955C7F532B23 +:100F50005093670638E0430F4F70A2505C91550FD5 +:100F6000550F5051507F542B509368063C9142E0EE +:100F7000340F3370A3954C91407F342B3295330F4F +:100F8000330F54E0A50F5C915095569537955695C3 +:100F900037953093690656F9A3504C9145FB57F9A4 +:100FA00050936A06A1505C9138E0530F5F70440F74 +:100FB000551F440F551F50936B060895DC0145E003 +:100FC000A40F5C91A395A3953C913873330F329590 +:100FD000550F369557954EEF340F33705093660684 +:100FE0004C914770A3505C91550F469557952EEF45 +:100FF000420F4370550F441F550F441F532B5093FE +:101000006706342FA2505C91A3954C914871550FFF +:10101000440F429546955795409540FB5295452F74 +:10102000407F432B409368065F7054F9352FA350DF +:101030005C91507747E0A40F4C9146FB57F9505113 +:1010400057FB550F532B50936906332730F9AA9558 +:101050005C91A3504C9146FB57F950954427550F8E +:10106000441F532B50936A06342FA2505C91A395D2 +:10107000A3954C914770550F469557952EEF420F0B +:10108000437057FB550F532B50936B06440F40F999 +:1010900040936C060895482F362F8827F894E09ADD +:1010A000E89A2227189B2FEF51E053BD5CE956BD0B +:1010B000A89A209340065FE350933F0678942498C3 +:1010C000259850E05A95F1F7249A259A50914006B8 +:1010D000521719F4A89BFACF089550914006521B5D +:1010E000250F231728F444238AF7450F7AF3E7CF17 +:1010F00083950895D0DF882371F0509140065631D2 +:1011000051F4F89455275093400650913F0653955B +:1011100050933F067894089581E090E008951F92DF +:101120000F920FB60F9211248F9381E08093C0022B +:101130008F910F900FBE0F901F901895CF93DF9354 +:10114000EB0183E085BD0E94081A8091C0028823CC +:10115000C9F11092C0028091C102813069F08130E2 +:1011600028F0843089F08530C9F414C0809179006A +:101170008093C20281E005C0809179008093C30210 +:1011800084E08093C1020CC0809179008093C402F6 +:1011900085E0F7CF809179008093C5021092C1025B +:1011A00080917C00887F80937C0090917C0080916E +:1011B000C1028770892B80937C0080917A008064C3 +:1011C00080937A0081E0888380916D068430A1F558 +:1011D00020916906229526952695237030E080910E +:1011E000680690E0880F991F880F991F282B392BCC +:1011F0008FEF90E0282739273D872C8780916A065A +:1012000090E083709070982F882720916B06820F52 +:10121000911D9A83898380916A0690E08C7F9070FB +:10122000880F991F880F991F9C838B838091690673 +:1012300080958F738B8771C08091670690E0837073 +:101240009070382F222780916606280F311D3A832F +:1012500029838091670681FF03C03C6F3A8329830D +:1012600080916806282F30E02F70307046E0220F02 +:10127000331F4A95E1F78091670686958695280F7A +:10128000311D3C832B838091680683FF03C03C6F34 +:101290003C832B8320916906229526952695237001 +:1012A00080916A0690E08F779070880F991F880F61 +:1012B000991F820F911D9D878C878091680682956A +:1012C0008F708E87809169068F7380528F83909183 +:1012D0006B069F73990F80916A06881F8827881F65 +:1012E000980F9B8780916B0685FF02C0906C9B874F +:1012F0001E821D828091C5022091C40290E0821B53 +:10130000910962E070E00E942A2060586A8780910B +:10131000C30288878091C202898781E090E0DF91D3 +:10132000CF9108950E943F1A86E08093410683E0A2 +:101330008093810085E085BD26E02150822F90E0DA +:10134000FC01EA59F94FE081DC01AD5BB94F8C91AA +:101350008E13EC93222389F788EE93E07DD312D489 +:1013600010BA80917A00876080937A0080917C0027 +:10137000806480937C0080917C00806280937C00FC +:1013800080917A00806880937A0080917A008860EA +:1013900080937A0080917A00806480937A00089527 +:1013A000CF92DF92EF92FF920F931F93DF93CF9331 +:1013B00000D0CDB7DEB7282F6A836A01933031F1B0 +:1013C000943030F4913091F0923009F054C007C05D +:1013D000913209F439C0923209F04DC03CC068E541 +:1013E000E62E6BE0F62E09E210E049C080916D0612 +:1013F000843029F054E3E52E5BE0F52E04C046E48A +:10140000E42E4BE0F42E02E110E039C0813049F0C7 +:10141000813018F0823079F50BC0E1E8FBE084916F +:1014200003C0E5E8FBE08491082F10E07F0127C0AE +:1014300080916D06843021F4E7ECFBE08491F4CFD9 +:10144000EFE9FBE08491F0CF3AE6E32E3BE0F32EA8 +:1014500009E010E014C080916D06843029F09CEA08 +:10146000E92E90E0F92E04C080EFE82E85E0F82EFA +:1014700004E415E004C0EE24FF2400E010E029831A +:10148000CE01019661E070E00E94B11ACE01029691 +:1014900061E070E00E94BC1AF601F182E082C801AE +:1014A0000F900F90CF91DF911F910F91FF90EF90D0 +:1014B000DF90CF9008955E9A08955E9808956F9298 +:1014C0007F928F929F92AF92BF92DF92EF92FF92A4 +:1014D0000F931F93DF93CF93CDB7DEB7C054D040A7 +:1014E0000FB6F894DEBF0FBECDBF80917206843078 +:1014F00009F083C081E08093E9008091E80080FFDB +:1015000013C0809100018E010F5F1F4FB80116DEDE +:10151000C8016FE070E040E050E00E94421E809100 +:10152000E8008E778093E80082E08093E900809164 +:10153000E80082FF62C05E9ADD245E010894A11C6F +:10154000B11C22E0622E712C6C0E7D1E90EA892E59 +:101550009FE0992E3EC0C50161E070E040E050E0A0 +:101560000E94F01D8530B9F3D394E981F0E0EE0FCD +:10157000FF1FEC50FE4F0081118101501040C80147 +:101580008D0D911D8034910598F020E931E05E9831 +:1015900088EE93E0F9013197F1F70197D9F75E9A58 +:1015A00088EE93E0F9013197F1F70197D9F7EFCF82 +:1015B000C301B80140E050E00E94F01D8530C1F346 +:1015C000D00E0F5F1F4FC501B80105D6C4010197AA +:1015D000F1F72091F3008091F200F22EEE2490E0DA +:1015E0008E299F29892B21F08FE38D1508F0B3CF29 +:1015F0008091E8008B778093E800C05CDF4F0FB6E6 +:10160000F894DEBF0FBECDBFCF91DF911F910F9138 +:10161000FF90EF90DF90BF90AF909F908F907F9062 +:101620006F9008950F931F93DF93CF93CDB7DEB7DD +:1016300064970FB6F894DEBF0FBECDBF0091BE0217 +:10164000802F0E94811A8823B1F089EE9BE00E94CE +:101650008A1A85E796E062E070E00E94B11A87E797 +:1016600096E062E070E00E94B11A84E796E061E0E3 +:1016700070E00E94BC1A80917406813021F089309C +:1016800009F06CC02BC0802F0E94811A882321F0A2 +:1016900081E091E00E94A61A80917306813A09F0D8 +:1016A0005DC05E9A809175069091760623E0873042 +:1016B000920731F48E010F5F1F4FC8014CD43EC01A +:1016C0008E010A5F1F4FB80139DD8091E800877FE6 +:1016D0008093E800C8016FE070E038C0802F0E945E +:1016E000811A882321F08BE091E00E94A61A809154 +:1016F0007306813299F55E9A8091E800877F809326 +:10170000E8008E010A5F1F4F6091790670917A069A +:10171000C8010E94421D0E94671B8091750690912E +:10172000760685509340C9F480ED97E00197F1F774 +:10173000C8018E010F5F1F4FB801DDD48091E80012 +:10174000877F8093E800C80165E070E00E94DC1CA0 +:101750008091E8008B778093E8005E9864960FB6DE +:10176000F894DEBF0FBECDBFCF91DF911F910F91D7 +:1017700008950F931F9381E061EC42E30E94321BB6 +:10178000082F82E060EC42E30E94321B10921501A8 +:1017900010921601109217011092180110E090E0BB +:1017A0000823192300FF02C090E001C090E48BB130 +:1017B0008F7B982B9BB91F910F91089584B7877FDA +:1017C00084BF88E10FB6F8948093600010926000A7 +:1017D0000FBE80E090E020E80FB6F89420936100FF +:1017E000809361000FBE569A5E989CDD0C94FE1BA0 +:1017F000E5DF5E98789408C05E9A8CE291E02CD187 +:101800005E988CE291E028D187DC892BA9F357DE22 +:101810000E94311B0E94311B0E9403200E94311B39 +:10182000F3CF94E2899FC001112483559E4F90937A +:10183000C7028093C6020895843180F4282F30E0D7 +:1018400088E290E0289FF001299FF00D389FF00D6D +:101850001124E853FD4F8081826080830895282FF2 +:101860008431F8F4A82FB0E088E290E0A89FF0015E +:10187000A99FF00DB89FF00D1124E853FD4F808112 +:101880008D7F8083A25BB94F15968C91882349F494 +:10189000E091C602F091C7020484F585E02D822F05 +:1018A000099508951F9311E0812FD9DF1F5F14312F +:1018B000D9F71F910895482F8431E8F4282F30E09C +:1018C00088E290E0289FF001299FF00D389FF00DED +:1018D0001124E853FD4F10828091F501481710F450 +:1018E0004093F501E091C602F091C7020684F785A6 +:1018F000E02D842F09950895482F8431C0F4282FB6 +:1019000030E088E290E0289FF001299FF00D389F99 +:10191000F00D1124E853FD4F8081882341F0E8E267 +:101920004E9FF0011124EB52FD4F818108958FEFFE +:1019300008950F931F93382FFB0180819181081721 +:10194000190711F480E010C01183008332FF0BC02F +:10195000E091C602F091C7020088F189E02D842F42 +:10196000622FA801099581E01F910F9108950F93AF +:10197000982FFB018081081711F480E010C00083CC +:1019800092FF0CC0E091C602F091C7020088F18975 +:10199000E02D842F622F402F50E0099581E00F91B8 +:1019A00008952FEF8F3F920719F420E030E008C030 +:1019B0009C0130703695279580709F77280F391FCE +:1019C000C90108952FEF8F3F920719F420E030E00E +:1019D00009C09C01220F331F20703F778F779070D2 +:1019E000280F391FC9010895282F332727FD309567 +:1019F00087FF02C020583048C9010895BC0180ED1E +:101A000097E00E9416206F5F7F4F76956795CB0118 +:101A1000089570E0992787FD90959C01629FC00111 +:101A2000639F900D729F900D11246FEF70E00E94E4 +:101A30002A20862F089520E304C0922F9A95F1F76B +:101A400081508823D1F708952498259885E0F3DF05 +:101A5000249A259A81E0EFCF20EA3FE004C0A895C0 +:101A6000F9013197F1F701974FEF8F3F9407B9F7DD +:101A70000895982F8091C80085FFFCCF9093CE00E9 +:101A80000895EF92FF920F931F93CF93DF937C0102 +:101A90008B018091BF02882341F086E49CE00E9484 +:101AA0008A1AC701B8010E94BC1AC0E0D0E006C083 +:101AB000F701EC0FFD1F8081DCDF2196C017D107F5 +:101AC000B8F3DF91CF911F910F91FF90EF900895A0 +:101AD000FF920F931F93DF93CF9300D00F92CDB758 +:101AE000DEB78C01F62EE091C602F091C7020280AB +:101AF000F381E02DCE0101960995698170E0C1DF87 +:101B0000C8016F2D70E0BDDF1A8206C0F801819117 +:101B10008F01980F9A83FA949A81FF20B9F79195D3 +:101B20009F779A83CE01029661E070E0AADF87EF8B +:101B30008B83CE01039661E070E0A3DF0F900F90DE +:101B40000F90CF91DF911F910F91FF9008950F9308 +:101B50001F938C0180E59CE00E948A1AC8010196BF +:101B600061E070E00E94BC1AE091C602F091C702E9 +:101B70000488F589E02DD80111968C9109951F9163 +:101B80000F9108952EE436E0C90160E070E048E16D +:101B900050E00E943D20539A1092C8008FE190E0DF +:101BA0009093CD008093CC0086E88093CA0088E0B3 +:101BB0008093C9001092CE0028EC32E0C90160E0A9 +:101BC00070E040E253E00E943D202BE436E0C90182 +:101BD00060E070E043E050E00E943D2082E08093AE +:101BE000F501E091C602F091C7020190F081E02D6D +:101BF0000995089582E08093F50128EC32E0C9014F +:101C000060E070E040E253E00C943D208091F501EB +:101C1000833111F440E038C04091F5014F5F4093AB +:101C2000F501415028E230E009C08091F50183318F +:101C3000A0F48091F5018F5F8093F5018091F5010B +:101C400090E0829FF001839FF00D929FF00D112490 +:101C5000E853FD4F8081882341F7242F30E088E24C +:101C600090E0289FF001299FF00D389FF00D11247E +:101C7000DF01A853BD4F81E08C93EB52FD4F8BE108 +:101C8000DF011D928A95E9F7842F08958CE191E098 +:101C90000C94A61A0F931F93CF93DF93EC01198135 +:101CA00082E080934B0680914C06846080934C06C2 +:101CB00080914C06806180934C0610924D06E09115 +:101CC000C602F091C7020480F581E02D812F0995AD +:101CD000082F133041F1143030F4113061F012301C +:101CE00009F04EC013C01530B9F1153028F1163087 +:101CF00009F046C03BC08DE291E00E94A61A002385 +:101D000009F44AC080914C0682600AC08EE391E0DB +:101D10000E94A61A002309F43FC080914C068D7FD3 +:101D200080934C0639C080E591E00E94A61A0023FA +:101D300099F110924D0630C081E691E00E94A61AFA +:101D4000002351F18BE490E087DE54DF80914C0654 +:101D5000826080934C0610C087E691E00E94A61A2C +:101D60000023D1F080914C068160DACF8DE691E0BE +:101D70000E94A61A002381F080914C068E7FD0CF5E +:101D8000107C51F08EE59CE00E948A1ACE010196EB +:101D900061E070E00E94BC1ADF91CF911F910F911A +:101DA0000895DF93CF930F92CDB7DEB7FC01818109 +:101DB00089838091BE020E94811A882351F085E6B2 +:101DC0009CE00E948A1ACE01019661E070E00E94B8 +:101DD000BC1A89818F3F59F40DDFE091C602F09162 +:101DE000C7020684F785E02D8FE7099501C063DD02 +:101DF0000F90CF91DF9108958091BE020E94811AC9 +:101E0000882321F086E791E00C94A61A089580912A +:101E1000BE020E94811A882321F080E991E00C948F +:101E2000A61A0895EF92FF921F93DF93CF930F921C +:101E3000CDB7DEB77C01FC01818189831091BE02A0 +:101E4000812F0E94811A882351F08DE99CE00E9425 +:101E50008A1ACE01019661E070E00E94B11A898170 +:101E60008F3F11F48FE78983F70182818130F1F48C +:101E7000812F0E94811A882321F086E99CE00E942C +:101E8000961AF7018181D8DC9981E92FF0E0E25BB5 +:101E9000F94F8581882309F057C0E091C602F0917F +:101EA000C7020284F385E02D892F2EC0823071F5A0 +:101EB000812F0E94811A882321F08BE89CE00E94E8 +:101EC000961AF0DCE981F0E0E25BF94F8581882326 +:101ED00049F4E091C602F091C7020484F585E02D33 +:101EE0008FE70995F7018181A7DCE981F0E0E25BEA +:101EF000F94F8581882341F5E091C602F091C70230 +:101F00000284F385E02D8FE709951EC0833069F4C4 +:101F1000812F0E94811A882321F085E89CE00E948D +:101F2000961AF70181819BDC0FC0812F0E94811AD4 +:101F3000882351F082E79CE00E94961AC70102961E +:101F400061E070E00E94BC1A0F90CF91DF911F9169 +:101F5000FF90EF900895CF93DF93EC018091BE0244 +:101F60000E94811A882321F08FEA9CE00E94961A31 +:101F700041DE87E088838FEF9FEF9A83898380E13A +:101F80008B8383E08C83DF91CF910895EF92FF9252 +:101F90000F931F93CF93DF93EC01998188E2989F71 +:101FA0007001112488EC92E0E80EF91E8091BE02C7 +:101FB0000E94811A882309F465C088E09DE00E9490 +:101FC0008A1ACE016EE070E00E94BC1A80E09DE0AB +:101FD0000E948A1ACE01019661E070E00E94BC1A4C +:101FE00088EF9CE00E948A1ACE01029661E070E0C0 +:101FF0000E94BC1A80EF9CE00E948A1ACE010996CA +:1020000061E070E00E94BC1A88EE9CE00E948A1A8F +:10201000CE01039662E070E00E94BC1A8B85882393 +:10202000A1F083EE9CE00E948A1ACE010C9661E03A +:1020300070E00E94BC1A8EED9CE00E948A1ACE01CC +:102040000D9661E070E00E94BC1A8D819E81892B03 +:1020500051F084ED9CE00E948A1ACE01059662E060 +:1020600070E00E94BC1A8A85882351F08AEC9CE0BB +:102070000E948A1ACE010A9661E070E00E94BC1AA2 +:102080000E94311B8701035F1F4F8B819C81EFEF03 +:102090008F3F9E0719F420E030E002C082DC9C01F3 +:1020A000F7018081E091C602F091C7020D5F1F4FDA +:1020B0000288F389E02DB80149810995E091C602B3 +:1020C000F091C70202A0F3A1E02DCE01B70109955E +:1020D000682FF701808182FD07C0C7010D96F8DCEB +:1020E000F701808184608083DF91CF911F910F91F0 +:1020F000FF90EF900895FF920F931F93CF93DF937C +:102100008C01EB0186E08883E091C602F091C70262 +:10211000D80111968C910680F781E02D81500995A8 +:10212000F82EE091C602F091C7020084F185E02DFF +:102130000995882311F0198202C068DD898399818D +:10214000992319F482E08A8315C081E08A83E8E24A +:102150009E9FF0011124E853FD4FF686A091C60220 +:10216000B091C70256962D913C915797C801BF0177 +:10217000F90109958FEF9FEF9C838B838091BE02BD +:102180000E94811A8823B1F08EE19DE00E948A1A94 +:10219000CE01019661E070E00E94B11A84E19DE0F9 +:1021A0000E948A1ACE01029661E070E00E94BC1A79 +:1021B0000E94311B86EA91E06881AE0125E030E0A3 +:1021C0000E94C71A85E090E047DCDF91CF911F9114 +:1021D0000F91FF900895CF93DF93EC019B015E9ADE +:1021E00087E39DE066EF71E0AE010E94EA1A9981F3 +:1021F0008881873009F470C08830A8F48330D1F129 +:10220000843030F4813029F1823009F079C024C063 +:10221000853009F447C0E091C602F091C7028630CC +:1022200008F04EC035C08B3009F45FC08C3038F4F4 +:10223000883009F454C08A3009F062C053C08D3030 +:1022400009F459C08D3008F453C08E3009F058C0DD +:1022500055C0CE019BDE54C0E091C602F091C7028A +:1022600068E2969FB001112468537D4F008CF18D78 +:10227000E02D2FC0E091C602F091C70268E2969F60 +:10228000B001112468537D4F028CF38DE02D21C0E5 +:1022900068E2969FB001112468537D4F048CF58D40 +:1022A000E02D17C0E091C602F091C70268E2969F48 +:1022B000B001112468537D4F068CF78DE02D09C0C5 +:1022C00068E2969FB001112468537D4F00A0F1A1F0 +:1022D000E02DCE01099514C0CE0199DD11C0CE01CB +:1022E0008BDD0EC0CE019EDD0BC0CE015ADD08C0D5 +:1022F000CE01D0DC05C0CE012ADC02C0CE01C6DC96 +:102300005E98DF91CF9108958C3010F080E00895B1 +:10231000E82FF0E0E258FD4F80810895FC0185E050 +:10232000808384E192E0089590E0880F991F6623EE +:1023300031F08C549F4F68E671E00E9416209C019A +:1023400020783170220F331F8F779070280F391F3C +:10235000C9010895282F8FEF689FC0011124622FB3 +:1023600070E00E941620CB016F3F710519F010F04C +:102370008FE70895969587958F770895CF93DF938C +:10238000EC01DB01FB013D962FE7218B81E090E022 +:10239000968B858B1686108A1786148A138A128A62 +:1023A00021871682158284E690E09387828783E2F4 +:1023B0001D968C931D97228380E19EE495878487E8 +:1023C0008981813019F4128E118E04C081E091E070 +:1023D000928F818F8981883028F4823030F4813067 +:1023E00091F519C08C3078F527C0FD0131968FEF3B +:1023F0009FEF12969C938E931197938382832FEF76 +:10240000248325831086178281E090E0928781875C +:102410001386268318C0FD0131968FEF9FEF129629 +:102420009C938E931197938382838FEF848385830C +:1024300017821086868307C0FD0131968FEF828355 +:1024400011961C921182DF91CF9108950F931F93E3 +:10245000890120E46EDA1F910F910895AF92BF9227 +:10246000CF92DF92EF92FF920F931F93CF93DF9360 +:102470006C01A62EEC012D96FC013196842F992734 +:1024800087FD909547FD06C080549040E82EEE0CE5 +:102490004FE707C00196880F991F482F415870E8F1 +:1024A000E72EB42EBE188385882311F0E42E4B1935 +:1024B000F601F0806796842F97DA8C018F2DBE018C +:1024C0004A2D24E736DAF601F08022968E2D8CDA3A +:1024D0008C018F2DBE014A2D28E72BDA8B2DDF9141 +:1024E000CF911F910F91FF90EF90DF90CF90BF9011 +:1024F000AF900895009759F46A31710518F464E6B5 +:1025000070E007C0660F771F660F771F02C076DA8C +:10251000BC01CB0108957F928F929F92AF92BF92A0 +:10252000CF92DF92EF92FF920F931F93CF93DF939F +:102530005C01EB01DC011196EC9011972DE0C22EAD +:10254000D12CC60ED71E1A968C918F3F19F420E01D +:1025500030E007C021E030E002C0220F331F8A952F +:10256000E2F78881B6016B5F7F4F890100781170B7 +:10257000000F111F2F773070020F131F4E2D24E410 +:10258000D8D9F5018281883030F4823058F4813016 +:1025900009F0D4C009C08B3008F47EC08B3009F03C +:1025A000CDC0A3C0772406C0D5011C968C91898725 +:1025B000772473944E010894811C911C8881E9E072 +:1025C000F0E0CE0EDF1ED50119960C910695B601EE +:1025D0004E2D2CE4CCD9F880EEEFFFEFCE0EDF1EAF +:1025E000D5011C968C9160E09FDE8C018F2DB60189 +:1025F000E9EFFFEFCE0EDF1E4E2D28E49AD9D5016C +:1026000013968D919C9114979A83898313968D913B +:102610009C911497BFEF8F3F9B07A9F0F401228193 +:102620003381FFEF2F3F3F0771F02817390728F458 +:10263000821B930BB6D98C0108C0D60113960D915D +:102640001C91149702C000E010E08881B6016E5E14 +:102650007F4F4E2D20E66DD9772009F06FC0FE0127 +:102660003196D50117968D919C9118979087878305 +:102670006185728517968D919C9118973BDFF88044 +:10268000EAE0F0E0CE0EDF1E9DD98C018F2DB60161 +:102690004E2D20E54ED952C06E010894C11CD11CAC +:1026A000D50119968C911997F6018283F88089815A +:1026B00019966C91AED998D98C018F2D6496BE0174 +:1026C00064974E2D28E435D9F8806696D601119688 +:1026D0008C91F50161859DD987D98C018F2DBE0123 +:1026E0004E2D2CE426D98FE02AC06E010894C11C1F +:1026F000D11CD50119968C911997F6018283F88027 +:10270000898119966C9185D96FD98C018F2D64962A +:10271000BE0164974E2D28E40CD9F8806696D60148 +:1027200011968C91F501618574D95ED98C018F2D3C +:10273000BE014E2D2CE4FDD88BE001C08BE190E072 +:10274000DF91CF911F910F91FF90EF90DF90CF908D +:10275000BF90AF909F908F907F9008957F928F92BF +:102760009F92AF92BF92CF92DF92EF92FF920F9320 +:102770001F93DF93CF930F92CDB7DEB78C014B0140 +:10278000FC0171808091BE02BCD6882319F1F8014A +:102790008181898387E59DE0BDD6C80164E070E052 +:1027A000EBD681E59DE0B6D6CE01019661E070E002 +:1027B000E3D688E49DE0AED6C801029661E070E001 +:1027C000DBD68FE39DE0A6D6C801039661E070E0FA +:1027D000D3D647D73DE0E32EF12CE80CF91C540189 +:1027E0000894A11CB11CF8012281938192173CF43A +:1027F00081E0F5018387F801D280838105C0F5016E +:102800001386F801D3808281D81A892F992787FDF2 +:102810009095332727FD3095820F931F62E070E07B +:102820000E942A20462FC401672D18DEC82EF4010D +:10283000108121E130E0E20EF31E6D2D8BDD082FBB +:10284000812FB701EFEEFFEFEE0EFF1E472D28E6BA +:102850008ED8F5018681C81609F1C682F40110816F +:1028600024E130E0E20EF31E8C2DF501658172DD6E +:10287000082F812FB701472D2CE679D8F40110815C +:102880002AEF3FEFE20EF31E8C2DF501648162DD2D +:10289000082F812FB701472D24E669D80F90CF91DB +:1028A000DF911F910F91FF90EF90DF90CF90BF903D +:1028B000AF909F908F907F9008957F928F929F927C +:1028C000AF92BF92CF92DF92EF92FF920F931F933E +:1028D000DF93CF930F92CDB7DEB77C015B01FC0194 +:1028E000818189838091BE020CD68823C1F08AE75A +:1028F0009DE010D6C70164E070E03ED684E79DE01D +:1029000009D6CE01019661E070E036D687E69DE0FB +:1029100001D6C701029662E070E02ED6A2D66DE025 +:10292000C62ED12CCA0CDB1C45010894811C911CBD +:10293000F7018281F4018783F7010281138117FF78 +:1029400002C000951095159507950F77F5018081C8 +:10295000B6016F5E7F4F498128E609D8F501708086 +:1029600047E0A42EB12CAC0CBD1C60E0F7018281C5 +:10297000938197FD61E0F4018085D6DC8C01872D81 +:10298000B501498128E40E94990C8FE790E0F60197 +:10299000908F878B128E118E0F90CF91DF911F91A8 +:1029A0000F91FF90EF90DF90CF90BF90AF909F90EE +:1029B0008F907F9008954F925F926F927F928F9247 +:1029C0009F92AF92BF92CF92DF92EF92FF920F93BE +:1029D0001F93DF93CF930F92CDB7DEB73C012B014E +:1029E000FC01818189838091BE028BD5882381F18E +:1029F00088EC9DE08FD5C30167E070E0BDD582EC27 +:102A00009DE088D5CE01019661E070E0B5D585EBFB +:102A10009DE080D5C301029661E070E0ADD588EA03 +:102A20009DE078D5C301039661E070E0A5D58BE900 +:102A30009DE070D5C301049661E070E09DD58EE8FD +:102A40009DE068D5C301059662E070E095D509D692 +:102A50007DE0A72EB12CA40CB51C42010894811C6A +:102A6000911CF301858196818E30910518F40DE457 +:102A700010E00AC0F3E0883E9F0718F001E010E084 +:102A800003C00E94FE0C8C01F4011287018787812C +:102A90009085B8012FDD6C01F201F08025E130E076 +:102AA000A20EB31EE980C8010E94E20C8C018F2D9A +:102AB000B5014E2D20E70E94990CF201F08025EF20 +:102AC0003FEFA20EB31EE980C6010E94E20C8C010A +:102AD0008F2DB501E6EFFFEFAE0EBF1E4E2D20E5A8 +:102AE0000E94990CF201808182FD2EC0F501818146 +:102AF000823019F081818330E1F4F3018481829581 +:102B000086958770853028F4833068F4882329F00F +:102B100006C0873050F08730B9F482E001C083E00E +:102B2000F50181830FC082E001C083E0F50181835C +:102B300005C0F301848181548F3720F481E0F401D2 +:102B4000838702C0F4011386C2016981F3014381C6 +:102B500085DCE82EF201108121E130E0A20EB31EE7 +:102B6000F3016281F7DB082F812FB5012FEE3FEFD4 +:102B7000A20EB31E498128E60E94B70CF40186819B +:102B8000E81619F1E682F201108124E130E0A20E8C +:102B9000B31E8E2DF4016581DDDB082F812FB50179 +:102BA00049812CE60E94B70CF20110812AEF3FEF19 +:102BB000A20EB31E8E2DF4016481CCDB082F812F71 +:102BC000B501498124E60E94B70C0F90CF91DF91A7 +:102BD0001F910F91FF90EF90DF90CF90BF90AF903B +:102BE0009F908F907F906F905F904F9008958F92FD +:102BF0009F92AF92BF92CF92DF92EF92FF920F938C +:102C00001F93DF93CF930F92CDB7DEB75C016B01BB +:102C1000FC0181818983FDE0EF2EF12CE60EF71E89 +:102C20008B010F5F1F4F8091BE026BD4882341F14F +:102C30008DEF9DE06FD4C50165E070E09DD484EF19 +:102C40009DE068D4CE01019661E070E095D48AEEF3 +:102C50009DE060D4C501029661E070E08DD480EE05 +:102C60009DE058D4C501039661E070E085D486EDFF +:102C70009DE050D4C501049661E070E07DD4F1D4AC +:102C8000F5018481F80162810E94090D282FF70166 +:102C900081818D3008F48AC0803120F0803109F0C4 +:102CA00085C05CC0EDE08E2E912C8C0C9D1CF50136 +:102CB00082819481E980882331F5F6019183F08047 +:102CC00084E190E0C80ED91E822F0E94F40C8C0182 +:102CD0008F2DB601ECEEFFEFCE0EDF1E4E2D28E459 +:102CE0000E94990CF601F08028E130E0C20ED31E5C +:102CF000E980F50183810E94F40C8C018F2DB601CF +:102D00004E2D20E551C0F8019183F601F08086E157 +:102D100090E0C80ED91E822F0E94F40C8C018F2DDA +:102D2000B601EAEEFFEFCE0EDF1E4E2D2CE40E9420 +:102D3000990CF5018381803819F40FE710E004C085 +:102D400081950E94F40C8C01F60180812DE030E029 +:102D5000820E931EB401498124E526C0F5018281CB +:102D60009481E980882381F4F6019183F08084E1E5 +:102D700090E0C80ED91E822F0E94F40C8C018F2D7A +:102D8000B6014E2D28E410C0F8019183F601F080C1 +:102D900086E190E0C80ED91E822F0E94F40C8C01AF +:102DA0008F2DB6014E2D2CE40E94990C0F90CF91DF +:102DB000DF911F910F91FF90EF90DF90CF90BF9028 +:102DC000AF909F908F9008956F927F928F929F9275 +:102DD000AF92BF92CF92DF92EF92FF920F931F9329 +:102DE000DF93CF930F92CDB7DEB75C014B01FC01AF +:102DF000818189838091BE0284D3882381F186E416 +:102E00009EE088D3C50168E070E0B6D38CE39EE015 +:102E100081D3CE01019661E070E0AED382E39EE003 +:102E200079D3C501029661E070E0A6D388E29EE006 +:102E300071D3C501039661E070E09ED38AE19EE004 +:102E400069D3C501049662E070E096D38CE09EE001 +:102E500061D3C501069662E070E08ED302D4ADE086 +:102E6000CA2ED12CC80CD91C34010894611C711CC9 +:102E7000F5018281F3018483F5018381F301858368 +:102E8000F50126813781F30133832283F5018681A1 +:102E900097818F5F9F4F19F400E010E008C0F401A4 +:102EA00081819281821B930B0E94D10C8C01F401D1 +:102EB000808122E130E0C20ED31EB601498120E6B6 +:102EC0000E94990CF401F0802DEF3FEFC20ED31E4B +:102ED000E980F501848195810E94D10C8C018F2DB0 +:102EE000B6014E2D2CE50E94990CF401108125E0CD +:102EF00030E0C20ED31EF3018681F50163812ADA28 +:102F0000082F812FB60149812CE60E94B70CF401ED +:102F100010812AEF3FEFC20ED31EF3018681F50127 +:102F2000628118DA082F812FB601498124E60E94B8 +:102F3000B70C0F90CF91DF911F910F91FF90EF9001 +:102F4000DF90CF90BF90AF909F908F907F906F90C9 +:102F50000895EF92FF920F931F93DF93CF9300D0CA +:102F60000F92CDB7DEB78A0195EB99836A838B8385 +:102F70007E010894E11CF11CC70163E070E00E942F +:102F8000410D85EA8983802F8F778A8300701F77B0 +:102F90001B83C70163E070E00E94410D0F900F900A +:102FA0000F90CF91DF911F910F91FF90EF900895B7 +:102FB000482F46958FE76CE750E0CBCFDF93CF9358 +:102FC00000D00F92CDB7DEB795EB99836A838B83E0 +:102FD000CE01019663E070E00E94410D0F900F90CA +:102FE0000F90CF91DF91089560E1E8CF60E3E6CFE5 +:102FF00060E2E4CFDF93CF9300D0CDB7DEB7E82F08 +:103000008150863010F080E00EC085EC8983F0E0BE +:10301000E85EFD4F80818A83CE01019662E070E018 +:103020000E94410D81E00F900F90CF91DF910895A4 +:103030001F93182F06C00E94240D8AE00E941B0DCA +:1030400011501123C1F71F91089584E690E00E946A +:103050002C0D0E94240D87E090E00E942C0D84E04E +:10306000E7DF83E290E00E942C0D83E0E1DF8EE059 +:1030700090E00E942C0D82E0DBDF8EE490E00E9465 +:103080002C0D82E0D5DF84E090E00E942C0D83E0DF +:10309000CFDF8BE390E00E942C0D82E0C9DF8CE74C +:1030A00092E062E070E00E94410D84E190E00E94B5 +:1030B0002C0D83E792E069E070E00E94410D89E306 +:1030C00090E00E942C0D80E492E063E370E00E94A7 +:1030D000410D8FE192E061E270E00E94410D84E0D9 +:1030E00089DF86E490E00C942C0DCF92DF92EF9272 +:1030F000FF920F931F9300E0FF24EE24DD24CC24E5 +:1031000012E08C3060F48A30B8F4873028F48530CF +:1031100098F48230D1F410C08830B9F40DC08F30EB +:1031200089F0803128F48D3049F08E3071F408C078 +:10313000803151F0823149F40F5F07C0F39405C02C +:10314000E39403C0D39401C0C394812F0E947C0CEC +:103150001F5F1531B1F60B3058F482E08F1540F047 +:103160008E1530F08D1520F080E092E09C1508F46B +:1031700081E01F910F91FF90EF90DF90CF90089525 +:103180008C3010F080E00895E82FF0E0E155FD4F1D +:103190008081089580E00895FC0185E0808380E9C6 +:1031A00092E0089508950895089508950895FC0102 +:1031B0008281883028F4823030F4813031F407C0C5 +:1031C0008C3018F406C083E105C080E003C08FE0B6 +:1031D00001C087E090E00895CF93DF93EC01FB01FD +:1031E000DB011D9680E285879FE712969C931297DC +:1031F00015961C9215978981823008F441C0883059 +:1032000018F08C30E8F539C080E415968C9315974A +:1032100016969C93169719969C9319971B961C9239 +:103220001E921A971C969C931C971F969C931F970F +:103230008EE352968C9352978981823059F4189676 +:103240001C921E9217978EE69EE11E969C938E937B +:103250001D970EC080E090E418969C938E9317976C +:1032600085E692E11E969C938E931D9784E793E0EA +:1032700051969C938E93509703C08EE716968C932D +:10328000DF91CF9108958AE892E066E070E00C94B7 +:10329000410DDF93CF9300D000D000D0CDB7DEB783 +:1032A00091EF998360646B838C834D835E8341953A +:1032B000481B4157451B6F7B461B4F774A83CE0106 +:1032C000019666E070E00E94410D26960FB6F894D4 +:1032D000DEBF0FBECDBFCF91DF910895DF93CF93B7 +:1032E00000D00F92CDB7DEB792EF99838B836295B2 +:1032F000607F982F92959F702DE09227262F229520 +:103300002F7092278F709827962B9A83CE01019663 +:1033100063E070E00E94410D0F900F900F90CF91ED +:10332000DF91089561E0DACF63E0D8CF62E0D6CFD5 +:10333000DF93CF9300D0CDB7DEB7843011F48DE1A9 +:1033400005C0833011F080E00BC08AE68A8383EFEA +:103350008983CE01019662E070E00E94410D81E018 +:103360000F900F90CF91DF9108950F931F938901D4 +:1033700020E00E94990C1F910F91089584E690E03F +:103380000E942C0D8DEA92E062E070E00E94410DF7 +:1033900085E992E068E170E00E94410D84E0C8DFB9 +:1033A00084E690E00C942C0D23B5216023BD90930E +:1033B00085008093840081E086BBB09BFECF08959A +:1033C000FC01238182812827848128278581282761 +:1033D000603121F48181908189272827822F82956D +:1033E0008F708227982F969596959827292F269546 +:1033F00029276B3011F481812827822F817008954D +:103400005E9A81EC9FEFD0DF5E9883E89FEFCCCF90 +:103410001F9380916D06843011F010E101C01BE014 +:1034200080E0612F0E944B08E898882361F4809126 +:10343000E9058F5F8093E9058B30F8F0F89480E21E +:103440008093D800FFCF1092E90589E396E0612FC1 +:10345000B7DF882391F080916D06843051F4A6E6A1 +:10346000B6E0E9E3F6E086E001900D928150E1F7E5 +:1034700004C089E396E00E9491071F9108951DB84A +:1034800080E48EB910BA83EF81BB93E095BD85E0EF +:1034900080938100909369008BEC93EF85DFB0DF20 +:1034A0008BEC93EF81DF80E06EE70E944B08A8DF92 +:1034B0008091400680914006813159F489E396E07D +:1034C00060E17EDF882329F083E080936D0680E051 +:1034D0000EC0809140068C3091F489E396E06BE059 +:1034E0006FDF882361F084E080936D0681E00E94A5 +:1034F000110CE898F8948FE08093E8050895E89817 +:10350000CECF9091BF0220E09823981709F421E0D4 +:10351000822F08959C018091BF02882331F0F90128 +:103520002F5F3F4FE491EE23D1F708959C018091E6 +:10353000BF02882311F0C901EDCF0895FC018091ED +:10354000BF02882311F08191FCCF08959C018091E6 +:10355000BF02882311F0C901F1CF08958091BF0205 +:1035600008959C018091BF02882329F0662319F0F9 +:10357000C90170E0F3CF08959C018091BF028823B8 +:1035800029F0662319F0C90170E0E8CF0895EF92A1 +:10359000FF920F931F93DF93CF930F92CDB7DEB7B8 +:1035A000FC0169838A0179018091BF02882351F06F +:1035B000CF01C4DFCE01019661E070E0D2DFC80127 +:1035C000B701DADF0F90CF91DF911F910F91FF903C +:1035D000EF900895AF92BF92CF92DF92EF92FF9259 +:1035E0000F931F93CF93DF93FC016B01F42EE52E15 +:1035F00089018091BF02882349F1CF018BDF8F2D94 +:103600009E2DEC017E01E00EF11E31E0A32EB12CC7 +:103610001AC088819501281B3109C901880F991F9B +:10362000F601E81BF90B1081CE0161E070E099DF33 +:10363000123038F0612F70E061507040CE01019679 +:1036400090DFC10FD11DCE15DF0518F3DF91CF91AB +:103650001F910F91FF90EF90DF90CF90BF90AF90B0 +:1036600008950895382F282F2CC02093E9002317A0 +:1036700021F4762F942F50E006C07091EC009091C9 +:10368000ED005091F00091FF1BC08091EB008E7F08 +:103690008093EB008091ED008D7F8093ED00809111 +:1036A000EB0081608093EB007093EC009093ED0051 +:1036B0005093F0008091EE0087FD02C080E00895F5 +:1036C0002F5F273090F23093E90081E008958091D8 +:1036D000730688239CF404C0809172068823B9F095 +:1036E0008091E80082FFF8CF8091E8008B7780938B +:1036F000E800089580917206882349F08091E800DF +:1037000080FFF8CF8091E8008E778093E8000895DD +:103710004091E4005091E50024E68091EC0080FFA8 +:1037200024C08091E80080FD1EC08091720688232D +:1037300011F482E00895853011F483E008958091BA +:10374000EB0085FF02C081E008958091E400909134 +:10375000E5008417950709F3222311F484E0089506 +:103760002150AC01DACF80E008958091E80082FF1B +:10377000DCCFF9CFEF92FF920F931F934AD051D035 +:1037800008ED10E0F80180818F7780838081806868 +:10379000808380818F7D808319BC10927206109285 +:1037A0006E061092700610926F0680EEE82EF12CD5 +:1037B000F70180818B7F8083F80180818160808325 +:1037C00080E060E042E04EDFE1EEF0E080818E7F5D +:1037D0008083E2EEF0E08081816080838081886078 +:1037E0008083F70180818E7F8083F80180818061F2 +:1037F00080831F910F91FF90EF900895E7EDF0E027 +:1038000080818160808384E082BF81E08093710643 +:10381000B1CFE8EDF0E080818E7F80831092E200EE +:1038200008951092DA001092E10008951F920F920D +:103830000FB60F9211242F933F934F935F936F9383 +:103840007F938F939F93AF93BF93EF93FF93809159 +:10385000E10082FF0AC08091E20082FF06C08091F1 +:10386000E1008B7F8093E100CDD38091DA0080FF6F +:103870001FC08091D80080FF1BC08091DA008E7F2E +:103880008093DA008091D90080FF0DC080E189BD6E +:1038900082E189BD09B400FEFDCF81E0809372060C +:1038A0000E945B0A05C019BC109272060E945D0A54 +:1038B0008091E10080FF18C08091E20080FF14C079 +:1038C0008091E2008E7F8093E2008091E20080612F +:1038D0008093E2008091D80080628093D80019BC68 +:1038E00085E0809372068ED38091E10084FF2DC025 +:1038F0008091E20084FF29C080E189BD82E189BD19 +:1039000009B400FEFDCF8091D8008F7D8093D80050 +:103910008091E1008F7E8093E1008091E2008F7EB4 +:103920008093E2008091E20081608093E2008091C8 +:103930006E06882331F48091E30087FD02C081E0A8 +:1039400001C084E0809372065DD38091E10083FF23 +:1039500022C08091E20083FF1EC08091E100877F3A +:103960008093E10082E08093720610926E0680914F +:10397000E1008E7F8093E1008091E2008E7F809352 +:10398000E2008091E20080618093E20080E060E0EC +:1039900042E068DE37D3FF91EF91BF91AF919F91E5 +:1039A0008F917F916F915F914F913F912F910F90E8 +:1039B0000FBE0F901F9018959C0140917906509171 +:1039C0007A064617570718F4F90190E045C06115CB +:1039D000710511F0AB01F8CF8091E8008E778093EC +:1039E000E80040E050E0F0CF80917206882309F4AF +:1039F00045C0853009F444C08091E80083FF02C0CF +:103A000081E008958091E80082FD32C08091E80055 +:103A100080FF22C08091F3009091F200782F60E047 +:103A2000292F30E0262B372B07C081918093F1009E +:103A3000415050402F5F3F4F4115510519F028303C +:103A4000310598F390E02830310509F491E0809138 +:103A5000E8008E778093E8004115510529F69923F7 +:103A600019F606C080917206882341F0853041F036 +:103A70008091E80082FFF6CF80E0089582E008950B +:103A800083E008959C016115710529F48091E80097 +:103A90008B778093E800F90127C080917206882314 +:103AA00099F1853099F18091E80083FF02C081E0AF +:103AB00008958091E80082FFF0CF06C08091F10068 +:103AC00081936150704059F02091F3008091F20091 +:103AD000322F20E090E0822B932B892B79F7809175 +:103AE000E8008B778093E80061157105B1F606C098 +:103AF00080917206882341F0853041F08091E80082 +:103B000080FFF6CF80E0089582E0089583E0089575 +:103B10009C014091790650917A064617570718F490 +:103B2000F90190E046C06115710511F0AB01F8CFC5 +:103B30008091E8008E778093E80040E050E0F0CF7D +:103B400080917206882309F447C0853009F446C085 +:103B50008091E80083FF02C081E008958091E80031 +:103B600082FD34C08091E80080FF23C08091F30083 +:103B70009091F200782F60E0292F30E0262B372B30 +:103B800008C084918093F1003196415050402F5FDE +:103B90003F4F4115510519F02830310590F390E061 +:103BA0002830310509F491E08091E8008E77809308 +:103BB000E8004115510521F6992309F0C1CF06C04F +:103BC00080917206882341F0853041F08091E800B1 +:103BD00082FFF6CF80E0089582E0089583E00895A3 +:103BE000BF92CF92DF92EF92FF920F931F93CF93EA +:103BF000DF93182F092F7B016A018ADDB82E8823F5 +:103C0000B1F5812F902F9C01E901C114D10439F045 +:103C1000F60180819181E81AF90AC80FD91F00E0E6 +:103C200010E022C08091E80085FD16C08091E80078 +:103C30008B778093E800E7D1C114D10449F0F601F5 +:103C400080819181800F911F9183808385E010C0D6 +:103C50005FDD882349F00CC08091F10089930894BE +:103C6000E108F1080F5F1F4FE114F104D9F68B2D25 +:103C7000DF91CF911F910F91FF90EF90DF90CF9048 +:103C8000BF900895BF92CF92DF92EF92FF920F9371 +:103C90001F93CF93DF93182F092F7B016A0138DD23 +:103CA000B82E8823B1F5812F902F9C01E901C11412 +:103CB000D10439F0F60180819181E81AF90AC80F20 +:103CC000D91F00E010E022C08091E80085FD16C0F9 +:103CD0008091E8008E778093E80095D1C114D104DB +:103CE00049F0F60180819181800F911F918380833B +:103CF00085E010C00DDD882349F00CC089918093C8 +:103D0000F1000894E108F1080F5F1F4FE114F1047E +:103D1000D9F68B2DDF91CF911F910F91FF90EF90EE +:103D2000DF90CF90BF9008950F931F93DF93CF93B1 +:103D3000CDB7DEB7AC970FB6F894DEBF0FBECDBFE0 +:103D4000E3E7F6E08091F100819326E0EB37F2079C +:103D5000C9F70E94120B8091E80083FF3AC180915D +:103D6000730630917406353009F487C0363040F45C +:103D70003130C9F1313070F0333009F02AC133C02D +:103D8000383009F4F7C0393009F406C1363009F08B +:103D900020C197C0803821F0823809F01AC108C0CC +:103DA00090916F0680917006882399F0926011C0FF +:103DB000809177068F708093E9008091EB0090E00E +:103DC00025E0969587952A95E1F7982F91701092A6 +:103DD000E9008091E800877F8093E8009093F100EC +:103DE0001092F100D2C0882319F0823009F0F1C09E +:103DF00090E08F719070009721F0029709F0E9C070 +:103E00000CC080917506813009F0E3C010927006F5 +:103E1000333069F5809370062AC080917506882337 +:103E200031F5209177062F7009F4D3C02093E90073 +:103E30008091EB0080FF1BC0333021F48091EB00B8 +:103E4000806213C08091EB0080618093EB0081E081 +:103E500090E002C0880F991F2A95E2F78093EA004C +:103E60001092EA008091EB0088608093EB00109242 +:103E7000E9008091E800877F8BC0882309F0A9C002 +:103E8000109175061F770FB7F8948091E800877F2F +:103E90008093E8001CDC8091E80080FFFCCF8091DB +:103EA000E3008078812B8093E30080688093E300B7 +:103EB000112311F482E001C083E0809372060FBFEA +:103EC00088C08058823008F084C080917506909137 +:103ED000760623E08C3D920709F033C083E08C83A3 +:103EE0008AE28B837FB7F894DE0115966EE040E09E +:103EF00050E011E2E62FF0E010935700849140FF6C +:103F000003C082958F706F5F8F70282F30E08A30EA +:103F100018F0C901C79602C0C901C0968D939D9340 +:103F20004F5F5F4F4431510529F77FBF8091E80013 +:103F3000877F8093E800CE0103966AE270E03CDD63 +:103F400013C060917706AE014F5F5F4F0E94D009AA +:103F5000BC01009709F43DC08091E800877F809301 +:103F6000E80089819A81D4DD8091E8008B77809385 +:103F7000E8002FC0803869F58091E800877F809342 +:103F8000E80080916E068093F1008091E8008E77C2 +:103F90008093E8009CDB1DC08823D9F490917506BE +:103FA0009230B8F48091E800877F8093E800909386 +:103FB0006E068DDB80916E06882331F48091E300DC +:103FC00087FD02C081E001C084E0809372060E94F8 +:103FD000B90B8091E80083FF0AC08091EB008062FA +:103FE0008093EB008091E800877F8093E800AC9697 +:103FF0000FB6F894DEBF0FBECDBFCF91DF911F91FA +:104000000F91089508951F9380917206882361F09F +:104010001091E9001092E9008091E80083FF01C04F +:1040200083DE1F701093E9001F910895AA1BBB1B2C +:1040300051E107C0AA1FBB1FA617B70710F0A61BA8 +:10404000B70B881F991F5A95A9F780959095BC01C9 +:10405000CD01089597FB092E07260AD077FD04D0DD +:10406000E5DF06D000201AF4709561957F4F089522 +:10407000F6F7909581959F4F0895DC0101C06D93EF +:0C40800041505040E0F70895F894FFCF45 +:10408C00FF4765745265706F7274005365745265A6 +:10409C00706F7274000000000000000853657420FB +:1040AC00437573746F6D20466F72636500456E6166 +:1040BC00626C65204163747561746F72730044693E +:1040CC007361626C65204163747561746F72730007 +:1040DC0053746F7020416C6C204566666563747315 +:1040EC0000526573657400506175736500436F6EA3 +:1040FC0074696E75650053657420446F776E6C6FD0 +:10410C00616420466F7263652053616D706C65004D +:10411C0053657420437573746F6D20466F726365BD +:10412C00204461746100557362203C3D0025188E5B +:10413C0011FA1784117518F817F617F417A9172622 +:10414C0012D817BE11E416F715DB145D14AE138BE1 +:10415C0012BE19CC189819C018CA1896199419922D +:10416C00194919B5194319EC18D218D318D418D504 +:10417C0018D618D718020E00080005000700040016 +:10418C0004000F0003000000040002000200020003 +:10419C0005000400F000010A01020306010504A554 +:1041AC001465B5647FA57E6BB5687FA53600B56CCC +:1041BC007FA52800B5707FA5664CB5747FA57E01E0 +:1041CC00B5407FA57257B5447FA53C43B5487FA544 +:1041DC007E00B54C7FA50400B5507FA50200B554F8 +:1041EC007FA50200B5587FA5007EB55C7FA53C007D +:1041FC00B5607FF000010A0110056BF7C5011206CE +:10420C000502080A0B0D0E0F1001F11040007F0083 +:10421C00F000010A15F10E4301007DF17E04013E10 +:10422C004EF11C45013E2FF10B46017D00F31D069E +:10423C0005030204050508090A0B0000010204022B +:00000001FF From abcdae5a2a5c041f824cf9e7f26767ba070b7b13 Mon Sep 17 00:00:00 2001 From: ej113 <132016173+ej113@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:28:13 +0100 Subject: [PATCH 29/29] renamed hex beta to beta1 --- .../{adaptffbjoy-0.5.0beta.hex => adaptffbjoy-0.5.0beta1.hex} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename downloads/{adaptffbjoy-0.5.0beta.hex => adaptffbjoy-0.5.0beta1.hex} (100%) diff --git a/downloads/adaptffbjoy-0.5.0beta.hex b/downloads/adaptffbjoy-0.5.0beta1.hex similarity index 100% rename from downloads/adaptffbjoy-0.5.0beta.hex rename to downloads/adaptffbjoy-0.5.0beta1.hex