From 24c9dd9f3eddac16963d3748f985aa132c83f6f1 Mon Sep 17 00:00:00 2001 From: Gonezo <118452807+Fusion-Lightguns@users.noreply.github.com> Date: Sun, 7 Jan 2024 18:28:43 -0700 Subject: [PATCH] Add files via upload --- .../Player1/FusionLightgun/FusionColours.h | 51 + .../Player1/FusionLightgun/FusionLightgun.ino | 1691 ++++++++++++++++ .../FusionLightgun/FusionPreferences.cpp | 138 ++ .../FusionLightgun/FusionPreferences.h | 104 + .../Version-3.2/Player1/Pin-Button-Layout.txt | 35 + .../Player2/FusionLightgun/FusionColours.h | 51 + .../Player2/FusionLightgun/FusionLightgun.ino | 1695 +++++++++++++++++ .../FusionLightgun/FusionPreferences.cpp | 139 ++ .../FusionLightgun/FusionPreferences.h | 104 + .../Version-3.2/Player2/Pin-Button-Layout.txt | 35 + .../Pro-Micro/Version-3.2/Version-3.2.zip | Bin 0 -> 31136 bytes 11 files changed, 4043 insertions(+) create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionColours.h create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionLightgun.ino create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.cpp create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.h create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player1/Pin-Button-Layout.txt create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionColours.h create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionLightgun.ino create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.cpp create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.h create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Player2/Pin-Button-Layout.txt create mode 100644 DIY/Code It/Pro-Micro/Version-3.2/Version-3.2.zip diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionColours.h b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionColours.h new file mode 100644 index 0000000..15df9bc --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionColours.h @@ -0,0 +1,51 @@ +/*! + * @file FusionColours.h + * + * @copyright Gonezo Fusion Lightguns + * @copyright GNU Lesser General Public License + * + * @author Mike Lynch + * @author Gonezo + * @version V2.00 + * @date 2023 + */ + +#ifndef _FUSIONCOLOURS_H_ +#define _FUSIONCOLOURS_H_ + +#include + +// macro to scale an 8 bit colour value by an 8 bit value +// as seen by the math, 255 means full value +#define COLOR_BRI_ADJ_COLOR(brightness, color) ((((brightness) * ((color) & 0xFF)) / 255) & 0xFF) +//#define COLOR_BRI_ADJ_COLOR(brightness, color) (color) + +// macro to scale a 32-bit RGBW word with an 8 bit brightness value +#define COLOR_BRI_ADJ_RGB(brightness, rgb) COLOR_BRI_ADJ_COLOR(brightness, rgb) \ + | (COLOR_BRI_ADJ_COLOR(brightness, (rgb >> 8)) << 8) \ + | (COLOR_BRI_ADJ_COLOR(brightness, (rgb >> 16)) << 16) \ + | (COLOR_BRI_ADJ_COLOR(brightness, (rgb >> 24)) << 24) + +// some distinct colours from Wikipedia https://en.wikipedia.org/wiki/Lists_of_colors +// also adjusted the brightness to make them look even more distinct on an ItsyBitsy DotStar +// ... yeah I spent too much time on this +namespace WikiColor { + constexpr uint32_t Amber = COLOR_BRI_ADJ_RGB(130, 0xFFBF00); + constexpr uint32_t Blue = COLOR_BRI_ADJ_RGB(225, 0x0000FF); + constexpr uint32_t Carnation_pink = COLOR_BRI_ADJ_RGB(165, 0xFFA6C9); + constexpr uint32_t Cerulean_blue = COLOR_BRI_ADJ_RGB(255, 0x2A52BE); + constexpr uint32_t Cornflower_blue = COLOR_BRI_ADJ_RGB(175, 0x6495ED); + constexpr uint32_t Cyan = COLOR_BRI_ADJ_RGB(145, 0x00FFFF); + constexpr uint32_t Electric_indigo = COLOR_BRI_ADJ_RGB(235, 0x6F00FF); + constexpr uint32_t Ghost_white = COLOR_BRI_ADJ_RGB(135, 0xF8F8FF); + constexpr uint32_t Golden_yellow = COLOR_BRI_ADJ_RGB(135, 0xFFDF00); + constexpr uint32_t Green = COLOR_BRI_ADJ_RGB(140, 0x00FF00); + constexpr uint32_t Green_Lizard = COLOR_BRI_ADJ_RGB(145, 0xA7F432); + constexpr uint32_t Magenta = COLOR_BRI_ADJ_RGB(140, 0xFF00FF); + constexpr uint32_t Orange = COLOR_BRI_ADJ_RGB(150, 0xFF7F00); + constexpr uint32_t Red = COLOR_BRI_ADJ_RGB(145, 0xFF0000); + constexpr uint32_t Salmon = COLOR_BRI_ADJ_RGB(175, 0xFA8072); + constexpr uint32_t Yellow = COLOR_BRI_ADJ_RGB(135, 0xFFFF00); +}; + +#endif // _FUSIONCOLOURS_H_ diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionLightgun.ino b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionLightgun.ino new file mode 100644 index 0000000..d20a01f --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionLightgun.ino @@ -0,0 +1,1691 @@ + /* + @file FusionLightgun.ino + @brief Fusion Lightgun code orginally taken from Samco and Pwor7. Updated by Gon15ezo from Fusion Lightguns. + @copyright Gonezo Fusion Lightguns + @copyright GNU Lesser General Public License + @version V3.2 + @p player#1 + @date 2023 +*/ + +#include +#include +// include TinyUSB or HID depending on USB stack option +#if defined(USE_TINYUSB) +#include +#elif defined(CFG_TUSB_MCU) +#error Incompatible USB stack. Use Arduino or Adafruit TinyUSB. +#else +// Arduino USB stack +#include +#endif + +#include +#include +#ifdef DOTSTAR_ENABLE +#include +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_PIN +#include +#endif +#ifdef FUSION_FLASH_ENABLE +#include +#endif // FUSION_FLASH_ENABLE +#include +#include +#include +#include +#include +#include "FusionColours.h" +#include "FusionPreferences.h" + +#ifdef ARDUINO_ARCH_RP2040 +#include +#include + +// declare PWM ISR +void rp2040pwmIrq(void); +#endif + +// enable extra serial debug during run mode +//#define PRINT_VERBOSE 1 +//#define DEBUG_SERIAL 1 +//#define DEBUG_SERIAL 2 + +// numbered index of buttons, must match ButtonDesc[] order +enum ButtonIndex_e { + BtnIdx_Trigger = 0, + BtnIdx_A, + BtnIdx_B, + BtnIdx_Start, + BtnIdx_Select, + BtnIdx_Calibrate, + BtnIdx_Bomb, + BtnIdx_Up, + BtnIdx_Down, + BtnIdx_Left, + BtnIdx_Right, + BtnIdx_Reload +}; + +// bit mask for each button, must match ButtonDesc[] order to match the proper button events +enum ButtonMask_e { + BtnMask_Trigger = 1 << BtnIdx_Trigger, + BtnMask_A = 1 << BtnIdx_A, + BtnMask_B = 1 << BtnIdx_B, + BtnMask_Start = 1 << BtnIdx_Start, + BtnMask_Select = 1 << BtnIdx_Select, + BtnMask_Calibrate = 1 << BtnIdx_Calibrate, + BtnMask_Bomb = 1 << BtnIdx_Bomb, + BtnMask_Up = 1 << BtnIdx_Up, + BtnMask_Down = 1 << BtnIdx_Down, + BtnMask_Left = 1 << BtnIdx_Left, + BtnMask_Right = 1 << BtnIdx_Right, + BtnMask_Reload = 1 << BtnIdx_Reload +}; + +// Button descriptor +// The order of the buttons is the order of the button bitmask +const LightgunButtons::Desc_t LightgunButtons::ButtonDesc[] = { + {10, LightgunButtons::ReportType_Mouse, MOUSE_LEFT, 25, BTN_AG_MASK, "Trigger"}, + {9, LightgunButtons::ReportType_Keyboard, KEY_RETURN, 25, BTN_AG_MASK2, "A"}, + {8, LightgunButtons::ReportType_Keyboard, KEY_ESC, 25, BTN_AG_MASK2, "B"}, + {7, LightgunButtons::ReportType_Keyboard, KEY_1, 20, BTN_AG_MASK2, "Start"}, + {6, LightgunButtons::ReportType_Keyboard, KEY_5, 25, BTN_AG_MASK2, "Select"}, + {14, LightgunButtons::ReportType_Mouse, MOUSE_BUTTON5, 20, BTN_AG_MASK2, "Calibrate"}, + {4, LightgunButtons::ReportType_Mouse, MOUSE_MIDDLE, 20, BTN_AG_MASK2, "Bomb"}, + {A0, LightgunButtons::ReportType_Keyboard, KEY_UP_ARROW, 25, BTN_AG_MASK2, "Up"}, + {A1, LightgunButtons::ReportType_Keyboard, KEY_DOWN_ARROW, 25, BTN_AG_MASK2, "Down"}, + {A2, LightgunButtons::ReportType_Keyboard, KEY_LEFT_ARROW, 25, BTN_AG_MASK2, "Left"}, + {A3, LightgunButtons::ReportType_Keyboard, KEY_RIGHT_ARROW, 25, BTN_AG_MASK2, "Right"}, + {16, LightgunButtons::ReportType_Mouse, MOUSE_RIGHT, 20, BTN_AG_MASK2, "Reload"} +}; + +#define mot_pin 15 +#define recoil_pin 5 + +// button count constant + + +constexpr unsigned int ButtonCount = sizeof(LightgunButtons::ButtonDesc) / sizeof(LightgunButtons::ButtonDesc[0]); + +// button runtime data arrays +LightgunButtonsStatic lgbData; + +// button object instance +LightgunButtons buttons(lgbData, ButtonCount); + + +// button combo to enter pause mode +constexpr uint32_t EnterPauseModeBtnMask = BtnMask_Calibrate; + +// press any button to enter pause mode from Processing mode (this is not a button combo) +constexpr uint32_t EnterPauseModeProcessingBtnMask = BtnMask_A | BtnMask_B | BtnMask_Calibrate; + +// button combo to exit pause mode back to run mode +constexpr uint32_t ExitPauseModeBtnMask = BtnMask_Calibrate; + +// press any button to cancel the calibration (this is not a button combo) +constexpr uint32_t CancelCalBtnMask = BtnMask_Calibrate; + +// button combo to skip the center calibration step +constexpr uint32_t SkipCalCenterBtnMask = BtnMask_A; + +// button combo to save preferences to non-volatile memory +constexpr uint32_t SaveBtnMask = BtnMask_A | BtnMask_Select; + +// button combo to increase IR sensitivity +constexpr uint32_t IRSensitivityUpBtnMask = BtnMask_B | BtnMask_Select; + +// button combo to decrease IR sensitivity +constexpr uint32_t IRSensitivityDownBtnMask = BtnMask_B | BtnMask_A; + +// button combinations to select a run mode +constexpr uint32_t RunModeNormalBtnMask = BtnMask_Start | BtnMask_Select; +constexpr uint32_t RunModeAverageBtnMask = BtnMask_Start | BtnMask_A; +constexpr uint32_t RunModeProcessingBtnMask = BtnMask_Start | BtnMask_B; + +// colour when no IR points are seen +constexpr uint32_t IRSeen0Color = WikiColor::Red; + +// colour when calibrating +constexpr uint32_t CalModeColor = WikiColor::Cornflower_blue; + +// number of profiles +constexpr unsigned int ProfileCount = 8; + +// run modes +// note that this is a 5 bit value when stored in the profiles +enum RunMode_e { + RunMode_Normal = 0, ///< Normal gun mode, no averaging + RunMode_Average = 1, ///< 2 frame moving average + RunMode_Average2 = 2, ///< weighted average with 3 frames + RunMode_ProfileMax = 2, ///< maximum mode allowed for profiles + RunMode_Processing = 3, ///< Processing test mode + RunMode_Count +}; + +// profiles +// defaults can be populated here, or not worry about these values and just save to flash/EEPROM +// if you have original Fusion calibration values, multiply by 4 for the center position and +// scale is multiplied by 1000 and stored as an unsigned integer, see FusionPreferences::Calibration_t +FusionPreferences::ProfileData_t profileData[ProfileCount] = { + {1619, 950, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {1233, 950, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {1538, 855, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {1147, 855, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0} +}; +/*FusionPreferences::ProfileData_t profileData[profileCount] = { + {1619, 950, 1899, 1531, DFRobotIRPositionEx::Sensitivity_Max, RunMode_Average}, + {1233, 943, 1864, 1538, DFRobotIRPositionEx::Sensitivity_Max, RunMode_Average}, + {1538, 855, 1878, 1515, DFRobotIRPositionEx::Sensitivity_High, RunMode_Average}, + {1147, 855, 1889, 1507, DFRobotIRPositionEx::Sensitivity_High, RunMode_Average}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal} + };*/ + +// profile descriptor +typedef struct ProfileDesc_s { + // button(s) to select the profile + uint32_t buttonMask; + + // LED colour + uint32_t color; + + // button label + const char* buttonLabel; + + // optional profile label + const char* profileLabel; +} ProfileDesc_t; + +// profile descriptor +static const ProfileDesc_t profileDesc[ProfileCount] = { + {BtnMask_A, WikiColor::Cerulean_blue, "A", "TV"}, + {BtnMask_B, WikiColor::Cornflower_blue, "B", "TV 4:3"}, + {BtnMask_Start, WikiColor::Green, "Start", "Monitor"}, + {BtnMask_Select, WikiColor::Green_Lizard, "Select", "Monitor 4:3"}, +}; + +// overall calibration defaults, no need to change if data saved to NV memory or populate the profile table +// see profileData[] array below for specific profile defaults +int xCenter = MouseMaxX / 2; +int yCenter = MouseMaxY / 2; +float xScale = 1.64; +float yScale = 0.95; + +// step size for adjusting the scale +constexpr float ScaleStep = 0.001; + +int finalX = 0; // Values after tilt correction +int finalY = 0; + +int moveXAxis = 0; // Unconstrained mouse postion +int moveYAxis = 0; +int moveXAxisArr[3] = {0, 0, 0}; +int moveYAxisArr[3] = {0, 0, 0}; +int moveIndex = 0; + +int conMoveXAxis = 0; // Constrained mouse postion +int conMoveYAxis = 0; + +unsigned int lastSeen = 0; + +#ifdef EXTRA_POS_GLITCH_FILTER +int badFinalTick = 0; +int badMoveTick = 0; +int badFinalCount = 0; +int badMoveCount = 0; + +// number of consecutive bad move values to filter +constexpr unsigned int BadMoveCountThreshold = 3; + +// Used to filter out large jumps/glitches +constexpr int BadMoveThreshold = 49 * CamToMouseMult; +#endif // EXTRA_POS_GLITCH_FILTER + +// profile in use +unsigned int selectedProfile = 0; + +// IR positioning camera +#ifdef ARDUINO_ADAFRUIT_ITSYBITSY_RP2040 +DFRobotIRPositionEx dfrIRPos(Wire1); +#else +//DFRobotIRPosition myDFRobotIRPosition; +DFRobotIRPositionEx dfrIRPos(Wire); +#endif + +// Fusion positioning +FusionPositionEnhanced myFusion; + +// operating modes +enum GunMode_e { + GunMode_Init = -1, + GunMode_Run = 0, + GunMode_CalHoriz = 1, + GunMode_CalVert = 2, + GunMode_CalCenter = 3, + GunMode_Pause = 4 +}; +GunMode_e gunMode = GunMode_Init; // initial mode + +// run mode +RunMode_e runMode = RunMode_Normal; + +// IR camera sensitivity +DFRobotIRPositionEx::Sensitivity_e irSensitivity = DFRobotIRPositionEx::Sensitivity_Default; + +static const char* RunModeLabels[RunMode_Count] = { + "Normal", + "Averaging", + "Averaging2", + "Processing" +}; + +// preferences saved in non-volatile memory, populated with defaults +FusionPreferences::Preferences_t FusionPreferences::preferences = { + profileData, ProfileCount, // profiles + 0, // default profile +}; + +enum StateFlag_e { + // print selected profile once per pause state when the COM port is open + StateFlag_PrintSelectedProfile = (1 << 0), + + // report preferences once per pause state when the COM port is open + StateFlag_PrintPreferences = (1 << 1), + + // enable save (allow save once per pause state) + StateFlag_SavePreferencesEn = (1 << 2), + + // print preferences storage + StateFlag_PrintPreferencesStorage = (1 << 3) +}; + +// when serial connection resets, these flags are set +constexpr uint32_t StateFlagsDtrReset = StateFlag_PrintSelectedProfile | StateFlag_PrintPreferences | StateFlag_PrintPreferencesStorage; + +// state flags, see StateFlag_e +uint32_t stateFlags = StateFlagsDtrReset; + + +#ifdef DOTSTAR_ENABLE +// note if the colours don't match then change the colour format from BGR +// apparently different lots of DotStars may have different colour ordering ¯\_(ツ)_/¯ +Adafruit_DotStar dotstar(1, DOTSTAR_DATAPIN, DOTSTAR_CLOCKPIN, DOTSTAR_BGR); +#endif // DOTSTAR_ENABLE + +#ifdef NEOPIXEL_PIN +Adafruit_NeoPixel neopixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); +#endif // NEOPIXEL_PIN + +// flash transport instance +#if defined(EXTERNAL_FLASH_USE_QSPI) +Adafruit_FlashTransport_QSPI flashTransport; +#elif defined(EXTERNAL_FLASH_USE_SPI) +Adafruit_FlashTransport_SPI flashTransport(EXTERNAL_FLASH_USE_CS, EXTERNAL_FLASH_USE_SPI); +#endif + +#ifdef FUSION_FLASH_ENABLE +// Adafruit_SPIFlashBase non-volatile storage +// flash instance +Adafruit_SPIFlashBase flash(&flashTransport); + +static const char* NVRAMlabel = "Flash"; + +// flag to indicate if non-volatile storage is available +// this will enable in setup() +bool nvAvailable = false; +#endif // FUSION_FLASH_ENABLE + +#ifdef FUSION_EEPROM_ENABLE +// EEPROM non-volatile storage +static const char* NVRAMlabel = "EEPROM"; + +// flag to indicate if non-volatile storage is available +// unconditional for EEPROM +bool nvAvailable = true; +#endif + +// non-volatile preferences error code +int nvPrefsError = FusionPreferences::Error_NoStorage; + +// preferences instance +FusionPreferences elitePreferences; + +// number of times the IR camera will update per second +constexpr unsigned int IRCamUpdateRate = 209; + +#ifdef FUSION_NO_HW_TIMER +// use the millis() or micros() counter instead +unsigned long irPosUpdateTime = 0; +// will set this to 1 when the IR position can update +unsigned int irPosUpdateTick = 0; + +#define FUSION_NO_HW_TIMER_UPDATE() NoHardwareTimerCamTickMillis() +//define FUSION_NO_HW_TIMER_UPDATE() NoHardwareTimerCamTickMicros() + +#else +#define FUSION_NO_HW_TIMER_UPDATE() +// timer will set this to 1 when the IR position can update +volatile unsigned int irPosUpdateTick = 0; +#endif // FUSION_NO_HW_TIMER + +#ifdef DEBUG_SERIAL +static unsigned long serialDbMs = 0; +static unsigned long frameCount = 0; +static unsigned long irPosCount = 0; +#endif + +// used for periodic serial prints +unsigned long lastPrintMillis = 0; + +#ifdef USE_TINYUSB + +// USB HID Report ID +enum HID_RID_e { + HID_RID_KEYBOARD = 1, + HID_RID_MOUSE +}; + +// HID report descriptor using TinyUSB's template +uint8_t const desc_hid_report[] = { + TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(HID_RID_KEYBOARD)), + TUD_HID_REPORT_DESC_ABSMOUSE5(HID_REPORT_ID(HID_RID_MOUSE)) +}; + +Adafruit_USBD_HID usbHid; + +int __USBGetKeyboardReportID() +{ + return HID_RID_KEYBOARD; +} + +// AbsMouse5 instance +AbsMouse5_ AbsMouse5(HID_RID_MOUSE); + +#else +// AbsMouse5 instance +AbsMouse5_ AbsMouse5(1); +#endif + +void setup() { + // Initialize Button Pins + pinMode(9, INPUT_PULLUP); + pinMode(10, INPUT_PULLUP); + pinMode(11, INPUT_PULLUP); + pinMode(12, INPUT_PULLUP); + pinMode(recoil_pin, OUTPUT); + + // init DotStar and/or NeoPixel to red during setup() +#ifdef DOTSTAR_ENABLE + dotstar.begin(); + dotstar.setPixelColor(0, 150, 0, 0); + dotstar.show(); +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_ENABLEPIN + pinMode(NEOPIXEL_ENABLEPIN, OUTPUT); + digitalWrite(NEOPIXEL_ENABLEPIN, HIGH); +#endif // NEOPIXEL_ENABLEPIN +#ifdef NEOPIXEL_PIN + neopixel.begin(); + neopixel.setPixelColor(0, 255, 0, 0); + neopixel.show(); +#endif // NEOPIXEL_PIN + +#ifdef ARDUINO_ADAFRUIT_ITSYBITSY_RP2040 + // ensure Wire1 SDA and SCL are correct + Wire1.setSDA(2); + Wire1.setSCL(3); +#endif + + // initialize buttons + buttons.Begin(); + +#ifdef FUSION_FLASH_ENABLE + // init flash and load saved preferences + nvAvailable = flash.begin(); +#endif // FUSION_FLASH_ENABLE + + if (nvAvailable) { + LoadPreferences(); + } + + // use values from preferences + ApplyInitialPrefs(); + + // Start IR Camera with basic data format + dfrIRPos.begin(DFROBOT_IR_IIC_CLOCK, DFRobotIRPositionEx::DataFormat_Basic, irSensitivity); + +#ifdef USE_TINYUSB + usbHid.setPollInterval(2); + usbHid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report)); + //usb_hid.setStringDescriptor("TinyUSB HID Composite"); + + usbHid.begin(); +#endif + + Serial.begin(115200); + + AbsMouse5.init(MouseMaxX, MouseMaxY, true); + + // sanity to ensure the cal prefs is populated with at least 1 entry + // in case the table is zero'd out + if (profileData[selectedProfile].xCenter == 0) { + profileData[selectedProfile].xCenter = xCenter; + } + if (profileData[selectedProfile].yCenter == 0) { + profileData[selectedProfile].yCenter = yCenter; + } + if (profileData[selectedProfile].xScale == 0) { + profileData[selectedProfile].xScale = CalScaleFloatToPref(xScale); + } + if (profileData[selectedProfile].yScale == 0) { + profileData[selectedProfile].yScale = CalScaleFloatToPref(yScale); + } + + // fetch the calibration data, other values already handled in ApplyInitialPrefs() + SelectCalPrefs(selectedProfile); + +#ifdef USE_TINYUSB + // wait until device mounted + while (!USBDevice.mounted()) { + yield(); + } +#else + // was getting weird hangups... maybe nothing, or maybe related to dragons, so wait a bit + delay(100); +#endif + + // IR camera maxes out motion detection at ~300Hz, and millis() isn't good enough + startIrCamTimer(IRCamUpdateRate); + + // this will turn off the DotStar/RGB LED and ensure proper transition to Run + SetMode(GunMode_Run); + pinMode(mot_pin, OUTPUT); + digitalWrite(mot_pin, LOW); //turn off motor initially +} + +void startIrCamTimer(int frequencyHz) +{ +#if defined(FUSION_SAMD21) + startTimerEx(&TC4->COUNT16, GCLK_CLKCTRL_ID_TC4_TC5, TC4_IRQn, frequencyHz); +#elif defined(FUSION_SAMD51) + startTimerEx(&TC3->COUNT16, TC3_GCLK_ID, TC3_IRQn, frequencyHz); +#elif defined(FUSION_ATMEGA32U4) + startTimer3(frequencyHz); +#elif defined(FUSION_RP2040) + rp2040EnablePWMTimer(0, frequencyHz); + irq_set_exclusive_handler(PWM_IRQ_WRAP, rp2040pwmIrq); + irq_set_enabled(PWM_IRQ_WRAP, true); +#endif +} + +#if defined(FUSION_SAMD21) +void startTimerEx(TcCount16* ptc, uint16_t gclkCtrlId, IRQn_Type irqn, int frequencyHz) +{ + // use Generic clock generator 0 + GCLK->CLKCTRL.reg = (uint16_t)(GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | gclkCtrlId); + while (GCLK->STATUS.bit.SYNCBUSY == 1); // wait for sync + + ptc->CTRLA.bit.ENABLE = 0; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + // Use the 16-bit timer + ptc->CTRLA.reg |= TC_CTRLA_MODE_COUNT16; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + // Use match mode so that the timer counter resets when the count matches the compare register + ptc->CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + // Set prescaler + ptc->CTRLA.reg |= TIMER_TC_CTRLA_PRESCALER_DIV; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + setTimerFrequency(ptc, frequencyHz); + + // Enable the compare interrupt + ptc->INTENSET.reg = 0; + ptc->INTENSET.bit.MC0 = 1; + + NVIC_EnableIRQ(irqn); + + ptc->CTRLA.bit.ENABLE = 1; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync +} + +void TC4_Handler() +{ + // If this interrupt is due to the compare register matching the timer count + if (TC4->COUNT16.INTFLAG.bit.MC0 == 1) { + // clear interrupt + TC4->COUNT16.INTFLAG.bit.MC0 = 1; + + irPosUpdateTick = 1; + } +} +#endif // FUSION_SAMD21 + +#if defined(FUSION_SAMD51) +void startTimerEx(TcCount16* ptc, uint16_t gclkCtrlId, IRQn_Type irqn, int frequencyHz) +{ + // use Generic clock generator 0 + GCLK->PCHCTRL[gclkCtrlId].reg = GCLK_PCHCTRL_GEN_GCLK0 | GCLK_PCHCTRL_CHEN; + while (GCLK->SYNCBUSY.reg); // wait for sync + + ptc->CTRLA.bit.ENABLE = 0; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + // Use the 16-bit timer + ptc->CTRLA.reg |= TC_CTRLA_MODE_COUNT16; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + // Use match mode so that the timer counter resets when the count matches the compare register + ptc->WAVE.bit.WAVEGEN = TC_WAVE_WAVEGEN_MFRQ; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + // Set prescaler + ptc->CTRLA.reg |= TIMER_TC_CTRLA_PRESCALER_DIV; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + setTimerFrequency(ptc, frequencyHz); + + // Enable the compare interrupt + ptc->INTENSET.reg = 0; + ptc->INTENSET.bit.MC0 = 1; + + NVIC_EnableIRQ(irqn); + + ptc->CTRLA.bit.ENABLE = 1; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync +} + +void TC3_Handler() +{ + // If this interrupt is due to the compare register matching the timer count + if (TC3->COUNT16.INTFLAG.bit.MC0 == 1) { + // clear interrupt + TC3->COUNT16.INTFLAG.bit.MC0 = 1; + + irPosUpdateTick = 1; + } +} +#endif // FUSION_SAMD51 + +#if defined(FUSION_SAMD21) || defined(FUSION_SAMD51) +void setTimerFrequency(TcCount16* ptc, int frequencyHz) +{ + int compareValue = (F_CPU / (TIMER_PRESCALER_DIV * frequencyHz)); + + // Make sure the count is in a proportional position to where it was + // to prevent any jitter or disconnect when changing the compare value. + ptc->COUNT.reg = map(ptc->COUNT.reg, 0, ptc->CC[0].reg, 0, compareValue); + ptc->CC[0].reg = compareValue; + +#if defined(FUSION_SAMD21) + while (ptc->STATUS.bit.SYNCBUSY == 1); +#elif defined(FUSION_SAMD51) + while (ptc->SYNCBUSY.bit.STATUS == 1); +#endif +} +#endif + +#ifdef FUSION_ATMEGA32U4 +void startTimer3(unsigned long frequencyHz) +{ + // disable comapre output mode + TCCR3A = 0; + + //set the pre-scalar to 8 and set Clear on Compare + TCCR3B = (1 << CS31) | (1 << WGM32); + + // set compare value + OCR3A = F_CPU / (8UL * frequencyHz); + + // enable Timer 3 Compare A interrupt + TIMSK3 = 1 << OCIE3A; +} + +// Timer3 compare A interrupt +ISR(TIMER3_COMPA_vect) +{ + irPosUpdateTick = 1; +} +#endif // FUSION_ATMEGA32U4 + +#ifdef ARDUINO_ARCH_RP2040 +void rp2040EnablePWMTimer(unsigned int slice_num, unsigned int frequency) +{ + pwm_config pwmcfg = pwm_get_default_config(); + float clkdiv = (float)clock_get_hz(clk_sys) / (float)(65535 * frequency); + if (clkdiv < 1.0f) { + clkdiv = 1.0f; + } else { + // really just need to round up 1 lsb + clkdiv += 2.0f / (float)(1u << PWM_CH1_DIV_INT_LSB); + } + + // set the clock divider in the config and fetch the actual value that is used + pwm_config_set_clkdiv(&pwmcfg, clkdiv); + clkdiv = (float)pwmcfg.div / (float)(1u << PWM_CH1_DIV_INT_LSB); + + // calculate wrap value that will trigger the IRQ for the target frequency + pwm_config_set_wrap(&pwmcfg, (float)clock_get_hz(clk_sys) / (frequency * clkdiv)); + + // initialize and start the slice and enable IRQ + pwm_init(slice_num, &pwmcfg, true); + pwm_set_irq_enabled(slice_num, true); +} + +void rp2040pwmIrq(void) +{ + pwm_hw->intr = 0xff; + irPosUpdateTick = 1; +} +#endif + +#ifdef FUSION_NO_HW_TIMER +void NoHardwareTimerCamTickMicros() +{ + unsigned long us = micros(); + if (us - irPosUpdateTime >= 1000000UL / IRCamUpdateRate) { + irPosUpdateTime = us; + irPosUpdateTick = 1; + } +} + +void NoHardwareTimerCamTickMillis() +{ + unsigned long ms = millis(); + if (ms - irPosUpdateTime >= (1000UL + (IRCamUpdateRate / 2)) / IRCamUpdateRate) { + irPosUpdateTime = ms; + irPosUpdateTick = 1; + } +} +#endif // FUSION_NO_HW_TIMER + + +///// Motor & Recoil //// + +void move_motor() +{ + digitalWrite(mot_pin, HIGH); +} + +void stop_motor() +{ + digitalWrite(mot_pin, LOW); +} + + +void recoil(){ + digitalWrite(recoil_pin, HIGH); + delay(30); + digitalWrite(recoil_pin, LOW); + delay(30); +} + +//// + +void loop() { + FUSION_NO_HW_TIMER_UPDATE(); + + // poll/update button states with 1ms interval so debounce mask is more effective + buttons.Poll(1); + buttons.Repeat(); + if (buttons.pressed & BtnMask_Trigger) move_motor(); //if trigger button pressed, vibrate motor + switch (gunMode) { + case GunMode_Pause: + if (buttons.pressedReleased == ExitPauseModeBtnMask) { + SetMode(GunMode_Run); + } else if (buttons.pressedReleased == BtnMask_Trigger) { + stop_motor(); SetMode(GunMode_CalCenter); + } else if (buttons.pressedReleased == RunModeNormalBtnMask) { + SetRunMode(RunMode_Normal); + } else if (buttons.pressedReleased == RunModeAverageBtnMask) { + SetRunMode(runMode == RunMode_Average ? RunMode_Average2 : RunMode_Average); + } else if (buttons.pressedReleased == RunModeProcessingBtnMask) { + SetRunMode(RunMode_Processing); + } else if (buttons.pressedReleased == IRSensitivityUpBtnMask) { + IncreaseIrSensitivity(); + } else if (buttons.pressedReleased == IRSensitivityDownBtnMask) { + DecreaseIrSensitivity(); + } else if (buttons.pressedReleased == SaveBtnMask) { + SavePreferences(); + } else { + SelectCalProfileFromBtnMask(buttons.pressedReleased); + } + + PrintResults(); + + break; + case GunMode_CalCenter: + AbsMouse5.move(MouseMaxX / 2, MouseMaxY / 2); + if (buttons.pressedReleased & CancelCalBtnMask) { + CancelCalibration(); + } else if (buttons.pressedReleased == SkipCalCenterBtnMask) { + Serial.println("Calibrate Center skipped"); + SetMode(GunMode_CalVert); + } else if (buttons.pressed & BtnMask_Trigger) { + // trigger pressed, begin center cal + move_motor(); + CalCenter(); + // extra delay to wait for trigger to release (though not required) + SetModeWaitNoButtons(GunMode_CalVert, 500); + } + break; + case GunMode_CalVert: + if (buttons.pressedReleased & CancelCalBtnMask) { + CancelCalibration(); + } else { + if (buttons.pressed & BtnMask_Trigger) { + move_motor(); + SetMode(GunMode_CalHoriz); + } else { + CalVert(); + } + } + break; + case GunMode_CalHoriz: + if (buttons.pressedReleased & CancelCalBtnMask) { + CancelCalibration(); + } else { + if (buttons.pressed & BtnMask_Trigger) { + move_motor(); + ApplyCalToProfile(); + SetMode(GunMode_Run); + } else { + CalHoriz(); + } + } + break; + default: + /* ---------------------- LET'S GO --------------------------- */ + switch (runMode) { + case RunMode_Processing: + ExecRunModeProcessing(); + break; + case RunMode_Average: + case RunMode_Average2: + case RunMode_Normal: + default: + ExecRunMode(); + break; + } + break; + } +#ifdef DEBUG_SERIAL + PrintDebugSerial(); +#endif // DEBUG_SERIAL +} + +/* ----------------------------------------------- */ +/* --------------------------- METHODS ------------------------- */ +/* ----------------------------------------------- */ + +void check_trigger() +{ + if (buttons.pressedReleased == BtnMask_Trigger) stop_motor(); + if (buttons.debounced & BtnMask_Trigger) recoil(); +} + +void ExecRunMode() +{ +#ifdef DEBUG_SERIAL + Serial.print("exec run mode "); + Serial.println(RunModeLabels[runMode]); +#endif + moveIndex = 0; + buttons.ReportEnable(); + for (;;) { + buttons.Poll(0); + check_trigger(); + FUSION_NO_HW_TIMER_UPDATE(); + if (irPosUpdateTick) { + irPosUpdateTick = 0; + GetPosition(); + + int halfHscale = (int)(myFusion.h() * xScale + 0.5f) / 2; + moveXAxis = map(finalX, xCenter + halfHscale, xCenter - halfHscale, 0, MouseMaxX); + halfHscale = (int)(myFusion.h() * yScale + 0.5f) / 2; + moveYAxis = map(finalY, yCenter + halfHscale, yCenter - halfHscale, 0, MouseMaxY); + + switch (runMode) { + case RunMode_Average: + // 2 position moving average + moveIndex ^= 1; + moveXAxisArr[moveIndex] = moveXAxis; + moveYAxisArr[moveIndex] = moveYAxis; + moveXAxis = (moveXAxisArr[0] + moveXAxisArr[1]) / 2; + moveYAxis = (moveYAxisArr[0] + moveYAxisArr[1]) / 2; + break; + case RunMode_Average2: + // weighted average of current position and previous 2 + if (moveIndex < 2) { + ++moveIndex; + } else { + moveIndex = 0; + } + moveXAxisArr[moveIndex] = moveXAxis; + moveYAxisArr[moveIndex] = moveYAxis; + moveXAxis = (moveXAxis + moveXAxisArr[0] + moveXAxisArr[1] + moveXAxisArr[1] + 2) / 4; + moveYAxis = (moveYAxis + moveYAxisArr[0] + moveYAxisArr[1] + moveYAxisArr[1] + 2) / 4; + break; + case RunMode_Normal: + default: + break; + } + + conMoveXAxis = constrain(moveXAxis, 0, MouseMaxX); + conMoveYAxis = constrain(moveYAxis, 0, MouseMaxY); + AbsMouse5.move(conMoveXAxis, conMoveYAxis); + +#ifdef DEBUG_SERIAL + ++irPosCount; +#endif // DEBUG_SERIAL + } + + if (buttons.pressedReleased == EnterPauseModeBtnMask) { + SetMode(GunMode_Pause); + buttons.ReportDisable(); + return; + } + +#ifdef DEBUG_SERIAL + ++frameCount; + PrintDebugSerial(); +#endif // DEBUG_SERIAL + } +} + +// from Fusion_4IR_Test_BETA sketch +// for use with the Fusion_4IR_Processing_Sketch_BETA Processing sketch +void ExecRunModeProcessing() +{ + // constant offset added to output values + const int processingOffset = 100; + + buttons.ReportDisable(); + for (;;) { + buttons.Poll(1); + if (buttons.pressedReleased & EnterPauseModeProcessingBtnMask) { + SetMode(GunMode_Pause); + return; + } + check_trigger(); + FUSION_NO_HW_TIMER_UPDATE(); + if (irPosUpdateTick) { + irPosUpdateTick = 0; + + int error = dfrIRPos.basicAtomic(DFRobotIRPositionEx::Retry_2); + if (error == DFRobotIRPositionEx::Error_Success) { + myFusion.begin(dfrIRPos.xPositions(), dfrIRPos.yPositions(), dfrIRPos.seen(), MouseMaxX / 2, MouseMaxY / 2); + UpdateLastSeen(); + for (int i = 0; i < 4; i++) { + Serial.print(map(myFusion.testX(i), 0, MouseMaxX, CamMaxX, 0) + processingOffset); + Serial.print(","); + Serial.print(map(myFusion.testY(i), 0, MouseMaxY, CamMaxY, 0) + processingOffset); + Serial.print(","); + } + Serial.print(map(myFusion.x(), 0, MouseMaxX, CamMaxX, 0) + processingOffset); + Serial.print(","); + Serial.print(map(myFusion.y(), 0, MouseMaxY, CamMaxY, 0) + processingOffset); + Serial.print(","); + Serial.print(map(myFusion.testMedianX(), 0, MouseMaxX, CamMaxX, 0) + processingOffset); + Serial.print(","); + Serial.println(map(myFusion.testMedianY(), 0, MouseMaxY, CamMaxY, 0) + processingOffset); + } else if (error == DFRobotIRPositionEx::Error_IICerror) { + Serial.println("Device not available!"); + } + } + } +} + +// center calibration with a bit of averaging +void CalCenter() +{ + unsigned int xAcc = 0; + unsigned int yAcc = 0; + unsigned int count = 0; + unsigned long ms = millis(); + + // accumulate center position over a bit of time for some averaging + while (millis() - ms < 333) { + // center pointer + AbsMouse5.move(MouseMaxX / 2, MouseMaxY / 2); + + // get position + if (GetPositionIfReady()) { + xAcc += finalX; + yAcc += finalY; + count++; + + xCenter = finalX; + yCenter = finalY; + PrintCalInterval(); + } + + // poll buttons + buttons.Poll(1); + + // if trigger not pressed then break out of loop early + if (!(buttons.debounced & BtnMask_Trigger)) { + break; + } + } + + // unexpected, but make sure x and y positions are accumulated + if (count) { + xCenter = xAcc / count; + yCenter = yAcc / count; + } else { + Serial.print("Unexpected Center calibration failure, no center position was acquired!"); + // just continue anyway + } + + PrintCalInterval(); +} + +// vertical calibration +void CalVert() +{ + if (GetPositionIfReady()) { + int halfH = (int)(myFusion.h() * yScale + 0.5f) / 2; + moveYAxis = map(finalY, yCenter + halfH, yCenter - halfH, 0, MouseMaxY); + conMoveXAxis = MouseMaxX / 2; + conMoveYAxis = constrain(moveYAxis, 0, MouseMaxY); + AbsMouse5.move(conMoveXAxis, conMoveYAxis); + } + + if (buttons.repeat & BtnMask_B) { + yScale = yScale + ScaleStep; + } + + if (buttons.repeat & BtnMask_A) { + if (yScale > 0.005f) { + yScale = yScale - ScaleStep; + } + } + + if (buttons.pressedReleased == BtnMask_Start) { + yCenter--; + } else if (buttons.pressedReleased == BtnMask_Select) { + yCenter++; + } + + PrintCalInterval(); +} + +// horizontal calibration +void CalHoriz() +{ + if (GetPositionIfReady()) { + int halfH = (int)(myFusion.h() * xScale + 0.5f) / 2; + moveXAxis = map(finalX, xCenter + halfH, xCenter - halfH, 0, MouseMaxX); + conMoveXAxis = constrain(moveXAxis, 0, MouseMaxX); + conMoveYAxis = MouseMaxY / 2; + AbsMouse5.move(conMoveXAxis, conMoveYAxis); + } + + if (buttons.repeat & BtnMask_B) { + xScale = xScale + ScaleStep; + } + + if (buttons.repeat & BtnMask_A) { + if (xScale > 0.005f) { + xScale = xScale - ScaleStep; + } + } + + if (buttons.pressedReleased == BtnMask_Start) { + xCenter--; + } else if (buttons.pressedReleased == BtnMask_Select) { + xCenter++; + } + + PrintCalInterval(); +} + +// Helper to get position if the update tick is set +bool GetPositionIfReady() +{ + if (irPosUpdateTick) { + irPosUpdateTick = 0; + GetPosition(); + return true; + } + return false; +} + +// Get tilt adjusted position from IR postioning camera +// Updates finalX and finalY values +void GetPosition() +{ + int error = dfrIRPos.basicAtomic(DFRobotIRPositionEx::Retry_2); + if (error == DFRobotIRPositionEx::Error_Success) { + myFusion.begin(dfrIRPos.xPositions(), dfrIRPos.yPositions(), dfrIRPos.seen(), xCenter, yCenter); +#ifdef EXTRA_POS_GLITCH_FILTER + if ((abs(myFusion.X() - finalX) > BadMoveThreshold || abs(myFusion.Y() - finalY) > BadMoveThreshold) && badFinalTick < BadMoveCountThreshold) { + ++badFinalTick; + } else { + if (badFinalTick) { + badFinalCount++; + badFinalTick = 0; + } + finalX = myFusion.X(); + finalY = myFusion.Y(); + } +#else + finalX = myFusion.x(); + finalY = myFusion.y(); +#endif // EXTRA_POS_GLITCH_FILTER + + UpdateLastSeen(); +#if DEBUG_SERIAL == 2 + Serial.print(finalX); + Serial.print(' '); + Serial.print(finalY); + Serial.print(" "); + Serial.println(myFusion.h()); +#endif + } else if (error != DFRobotIRPositionEx::Error_DataMismatch) { + Serial.println("Device not available!"); + } +} + +// wait up to given amount of time for no buttons to be pressed before setting the mode +void SetModeWaitNoButtons(GunMode_e newMode, unsigned long maxWait) +{ + unsigned long ms = millis(); + while (buttons.debounced && (millis() - ms < maxWait)) { + buttons.Poll(1); + } + SetMode(newMode); +} + +// update the last seen value +// only to be called during run mode since this will modify the LED colour +void UpdateLastSeen() { + if (lastSeen != myFusion.seen()) { + if (!lastSeen && myFusion.seen()) { + LedOff(); + } else if (lastSeen && !myFusion.seen()) { + SetLedPackedColor(IRSeen0Color); + } + lastSeen = myFusion.seen(); + } +} + +void SetMode(GunMode_e newMode) +{ + if (gunMode == newMode) { + return; + } + + // exit current mode + switch (gunMode) { + case GunMode_Run: + stateFlags |= StateFlag_PrintPreferences; + break; + case GunMode_CalHoriz: + break; + case GunMode_CalVert: + break; + case GunMode_CalCenter: + break; + case GunMode_Pause: + break; + } + + // enter new mode + gunMode = newMode; + switch (newMode) { + case GunMode_Run: + // begin run mode with all 4 points seen + lastSeen = 0x0F; + break; + case GunMode_CalHoriz: + break; + case GunMode_CalVert: + break; + case GunMode_CalCenter: + break; + case GunMode_Pause: + stateFlags |= StateFlag_SavePreferencesEn | StateFlag_PrintSelectedProfile; + break; + } + + SetLedColorFromMode(); +} + +// set new run mode and apply it to the selected profile +void SetRunMode(RunMode_e newMode) +{ + if (newMode >= RunMode_Count) { + return; + } + + // block Processing/test modes being applied to a profile + if (newMode <= RunMode_ProfileMax && profileData[selectedProfile].runMode != newMode) { + profileData[selectedProfile].runMode = newMode; + stateFlags |= StateFlag_SavePreferencesEn; + } + + if (runMode != newMode) { + runMode = newMode; + if (!(stateFlags & StateFlag_PrintSelectedProfile)) { + PrintRunMode(); + } + } +} + +void PrintResults() +{ + if (millis() - lastPrintMillis < 100) { + return; + } + + if (!Serial.dtr()) { + stateFlags |= StateFlagsDtrReset; + return; + } + + PrintPreferences(); + /* + Serial.print(finalX); + Serial.print(" ("); + Serial.print(MoveXAxis); + Serial.print("), "); + Serial.print(finalY); + Serial.print(" ("); + Serial.print(MoveYAxis); + Serial.print("), H "); + Serial.println(myFusion.H());*/ + + //Serial.print("conMove "); + //Serial.print(conMoveXAxis); + //Serial.println(conMoveYAxis); + + if (stateFlags & StateFlag_PrintSelectedProfile) { + stateFlags &= ~StateFlag_PrintSelectedProfile; + PrintSelectedProfile(); + PrintIrSensitivity(); + PrintRunMode(); + PrintCal(); + } + + lastPrintMillis = millis(); +} + +void PrintCalInterval() +{ + if (millis() - lastPrintMillis < 100) { + return; + } + PrintCal(); + lastPrintMillis = millis(); +} + +void PrintCal() +{ + Serial.print("Calibration: Center x,y: "); + Serial.print(xCenter); + Serial.print(","); + Serial.print(yCenter); + Serial.print(" Scale x,y: "); + Serial.print(xScale, 3); + Serial.print(","); + Serial.println(yScale, 3); +} + +void PrintRunMode() +{ + if (runMode < RunMode_Count) { + Serial.print("Mode: "); + Serial.println(RunModeLabels[runMode]); + } +} + +// helper in case this changes +float CalScalePrefToFloat(uint16_t scale) +{ + return (float)scale / 1000.0f; +} + +// helper in case this changes +uint16_t CalScaleFloatToPref(float scale) +{ + return (uint16_t)(scale * 1000.0f); +} + +void PrintPreferences() +{ + if (!(stateFlags & StateFlag_PrintPreferences) || !Serial.dtr()) { + return; + } + + stateFlags &= ~StateFlag_PrintPreferences; + + PrintNVPrefsError(); + + if (stateFlags & StateFlag_PrintPreferencesStorage) { + stateFlags &= ~StateFlag_PrintPreferencesStorage; + PrintNVStorage(); + } + + Serial.print("Default Profile: "); + Serial.println(profileDesc[FusionPreferences::preferences.profile].profileLabel); + + Serial.println("Profiles:"); + for (unsigned int i = 0; i < FusionPreferences::preferences.profileCount; ++i) { + // report if a profile has been cal'd + if (profileData[i].xCenter && profileData[i].yCenter) { + size_t len = strlen(profileDesc[i].buttonLabel) + 2; + Serial.print(profileDesc[i].buttonLabel); + Serial.print(": "); + if (profileDesc[i].profileLabel && profileDesc[i].profileLabel[0]) { + Serial.print(profileDesc[i].profileLabel); + len += strlen(profileDesc[i].profileLabel); + } + while (len < 26) { + Serial.print(' '); + ++len; + } + Serial.print("Center: "); + Serial.print(profileData[i].xCenter); + Serial.print(","); + Serial.print(profileData[i].yCenter); + Serial.print(" Scale: "); + Serial.print(CalScalePrefToFloat(profileData[i].xScale), 3); + Serial.print(","); + Serial.print(CalScalePrefToFloat(profileData[i].yScale), 3); + Serial.print(" IR: "); + Serial.print((unsigned int)profileData[i].irSensitivity); + Serial.print(" Mode: "); + Serial.println((unsigned int)profileData[i].runMode); + } + } +} + +void PrintNVStorage() +{ +#ifdef FUSION_FLASH_ENABLE + unsigned int required = FusionPreferences::Size(); +#ifndef PRINT_VERBOSE + if (required < flash.size()) { + return; + } +#endif + Serial.print("NV Storage capacity: "); + Serial.print(flash.size()); + Serial.print(", required size: "); + Serial.println(required); +#ifdef PRINT_VERBOSE + Serial.print("Profile struct size: "); + Serial.print((unsigned int)sizeof(FusionPreferences::ProfileData_t)); + Serial.print(", Profile data array size: "); + Serial.println((unsigned int)sizeof(profileData)); +#endif +#endif // FUSION_FLASH_ENABLE +} + +void PrintNVPrefsError() +{ + if (nvPrefsError != FusionPreferences::Error_Success) { + Serial.print(NVRAMlabel); + Serial.print(" error: "); +#ifdef FUSION_FLASH_ENABLE + Serial.println(FusionPreferences::ErrorCodeToString(nvPrefsError)); +#else + Serial.println(nvPrefsError); +#endif // FUSION_FLASH_ENABLE + } +} + +void LoadPreferences() +{ + if (!nvAvailable) { + return; + } + +#ifdef FUSION_FLASH_ENABLE + nvPrefsError = FusionPreferences::Load(flash); +#else + nvPrefsError = elitePreferences.Load(); +#endif // FUSION_FLASH_ENABLE + VerifyPreferences(); +} + +void VerifyPreferences() +{ + // center 0 is used as "no cal data" + for (unsigned int i = 0; i < ProfileCount; ++i) { + if (profileData[i].xCenter >= MouseMaxX || profileData[i].yCenter >= MouseMaxY || profileData[i].xScale == 0 || profileData[i].yScale == 0) { + profileData[i].xCenter = 0; + profileData[i].yCenter = 0; + } + + // if the scale values are large, assign 0 so the values will be ignored + if (profileData[i].xScale >= 30000) { + profileData[i].xScale = 0; + } + if (profileData[i].yScale >= 30000) { + profileData[i].yScale = 0; + } + + if (profileData[i].irSensitivity > DFRobotIRPositionEx::Sensitivity_Max) { + profileData[i].irSensitivity = DFRobotIRPositionEx::Sensitivity_Default; + } + + if (profileData[i].runMode >= RunMode_Count) { + profileData[i].runMode = RunMode_Normal; + } + } + + // if default profile is not valid, use current selected profile instead + if (FusionPreferences::preferences.profile >= ProfileCount) { + FusionPreferences::preferences.profile = (uint8_t)selectedProfile; + } +} + +// Apply initial preferences, intended to be called only in setup() after LoadPreferences() +// this will apply the preferences data as the initial values +void ApplyInitialPrefs() +{ + // if default profile is valid then use it + if (FusionPreferences::preferences.profile < ProfileCount) { + // note, just set the value here not call the function to do the set + selectedProfile = FusionPreferences::preferences.profile; + + // set the current IR camera sensitivity + if (profileData[selectedProfile].irSensitivity <= DFRobotIRPositionEx::Sensitivity_Max) { + irSensitivity = (DFRobotIRPositionEx::Sensitivity_e)profileData[selectedProfile].irSensitivity; + } + + // set the run mode + if (profileData[selectedProfile].runMode < RunMode_Count) { + runMode = (RunMode_e)profileData[selectedProfile].runMode; + } + } +} + +void SavePreferences() +{ + if (!nvAvailable || !(stateFlags & StateFlag_SavePreferencesEn)) { + return; + } + + // Only allow one write per pause state until something changes. + // Extra protection to ensure the same data can't write a bunch of times. + stateFlags &= ~StateFlag_SavePreferencesEn; + + // use selected profile as the default + FusionPreferences::preferences.profile = (uint8_t)selectedProfile; + +#ifdef FUSION_FLASH_ENABLE + nvPrefsError = FusionPreferences::Save(flash); +#else + nvPrefsError = FusionPreferences::Save(); +#endif // FUSION_FLASH_ENABLE + if (nvPrefsError == FusionPreferences::Error_Success) { + Serial.print("Settings saved to "); + Serial.println(NVRAMlabel); + } else { + Serial.println("Error saving Preferences."); + PrintNVPrefsError(); + } +} + +void SelectCalProfileFromBtnMask(uint32_t mask) +{ + // only check if buttons are set in the mask + if (!mask) { + return; + } + for (unsigned int i = 0; i < ProfileCount; ++i) { + if (profileDesc[i].buttonMask == mask) { + SelectCalProfile(i); + return; + } + } +} + +void CycleIrSensitivity() +{ + uint8_t sens = irSensitivity; + if (irSensitivity < DFRobotIRPositionEx::Sensitivity_Max) { + sens++; + } else { + sens = DFRobotIRPositionEx::Sensitivity_Min; + } + SetIrSensitivity(sens); +} + +void IncreaseIrSensitivity() +{ + uint8_t sens = irSensitivity; + if (irSensitivity < DFRobotIRPositionEx::Sensitivity_Max) { + sens++; + SetIrSensitivity(sens); + } +} + +void DecreaseIrSensitivity() +{ + uint8_t sens = irSensitivity; + if (irSensitivity > DFRobotIRPositionEx::Sensitivity_Min) { + sens--; + SetIrSensitivity(sens); + } +} + +// set a new IR camera sensitivity and apply to the selected profile +void SetIrSensitivity(uint8_t sensitivity) +{ + if (sensitivity > DFRobotIRPositionEx::Sensitivity_Max) { + return; + } + + if (profileData[selectedProfile].irSensitivity != sensitivity) { + profileData[selectedProfile].irSensitivity = sensitivity; + stateFlags |= StateFlag_SavePreferencesEn; + } + + if (irSensitivity != (DFRobotIRPositionEx::Sensitivity_e)sensitivity) { + irSensitivity = (DFRobotIRPositionEx::Sensitivity_e)sensitivity; + dfrIRPos.sensitivityLevel(irSensitivity); + if (!(stateFlags & StateFlag_PrintSelectedProfile)) { + PrintIrSensitivity(); + } + } +} + +void PrintIrSensitivity() +{ + Serial.print("IR Camera Sensitivity: "); + Serial.println((int)irSensitivity); +} + +void CancelCalibration() +{ + Serial.println("Calibration cancelled"); + // re-print the profile + stateFlags |= StateFlag_PrintSelectedProfile; + // re-apply the cal stored in the profile + RevertToCalProfile(selectedProfile); + // return to pause mode + SetMode(GunMode_Pause); +} + +void PrintSelectedProfile() +{ + Serial.print("Profile: "); + Serial.println(profileDesc[selectedProfile].profileLabel); +} + +// select a profile +bool SelectCalProfile(unsigned int profile) +{ + if (profile >= ProfileCount) { + return false; + } + + if (selectedProfile != profile) { + stateFlags |= StateFlag_PrintSelectedProfile; + selectedProfile = profile; + } + + bool valid = SelectCalPrefs(profile); + + // set IR sensitivity + if (profileData[profile].irSensitivity <= DFRobotIRPositionEx::Sensitivity_Max) { + SetIrSensitivity(profileData[profile].irSensitivity); + } + + // set run mode + if (profileData[profile].runMode < RunMode_Count) { + SetRunMode((RunMode_e)profileData[profile].runMode); + } + + SetLedColorFromMode(); + + // enable save to allow setting new default profile + stateFlags |= StateFlag_SavePreferencesEn; + return valid; +} + +bool SelectCalPrefs(unsigned int profile) +{ + if (profile >= ProfileCount) { + return false; + } + + // if center values are set, assume profile is populated + if (profileData[profile].xCenter && profileData[profile].yCenter) { + xCenter = profileData[profile].xCenter; + yCenter = profileData[profile].yCenter; + + // 0 scale will be ignored + if (profileData[profile].xScale) { + xScale = CalScalePrefToFloat(profileData[profile].xScale); + } + if (profileData[profile].yScale) { + yScale = CalScalePrefToFloat(profileData[profile].yScale); + } + return true; + } + return false; +} + +// revert back to useable settings, even if not cal'd +void RevertToCalProfile(unsigned int profile) +{ + // if the current profile isn't valid + if (!SelectCalProfile(profile)) { + // revert to settings from any valid profile + for (unsigned int i = 0; i < ProfileCount; ++i) { + if (SelectCalProfile(i)) { + break; + } + } + + // stay selected on the specified profile + SelectCalProfile(profile); + } +} + +// apply current cal to the selected profile +void ApplyCalToProfile() +{ + profileData[selectedProfile].xCenter = xCenter; + profileData[selectedProfile].yCenter = yCenter; + profileData[selectedProfile].xScale = CalScaleFloatToPref(xScale); + profileData[selectedProfile].yScale = CalScaleFloatToPref(yScale); + + stateFlags |= StateFlag_PrintSelectedProfile; +} + +void SetLedPackedColor(uint32_t color) +{ +#ifdef DOTSTAR_ENABLE + dotstar.setPixelColor(0, Adafruit_DotStar::gamma32(color & 0x00FFFFFF)); + dotstar.show(); +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_PIN + neopixel.setPixelColor(0, Adafruit_NeoPixel::gamma32(color & 0x00FFFFFF)); + neopixel.show(); +#endif // NEOPIXEL_PIN +} + +void LedOff() +{ +#ifdef DOTSTAR_ENABLE + dotstar.setPixelColor(0, 0); + dotstar.show(); +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_PIN + neopixel.setPixelColor(0, 0); + neopixel.show(); +#endif // NEOPIXEL_PIN +} + +void SetLedColorFromMode() +{ + switch (gunMode) { + case GunMode_CalHoriz: + case GunMode_CalVert: + case GunMode_CalCenter: + SetLedPackedColor(CalModeColor); + break; + case GunMode_Pause: + SetLedPackedColor(profileDesc[selectedProfile].color); + break; + case GunMode_Run: + if (lastSeen) { + LedOff(); + } else { + SetLedPackedColor(IRSeen0Color); + } + break; + default: + break; + } +} + +#ifdef DEBUG_SERIAL +void PrintDebugSerial() +{ + // only print every second + if (millis() - serialDbMs >= 1000 && Serial.dtr()) { +#ifdef EXTRA_POS_GLITCH_FILTER + Serial.print("bad final count "); + Serial.print(badFinalCount); + Serial.print(", bad move count "); + Serial.println(badMoveCount); +#endif // EXTRA_POS_GLITCH_FILTER + Serial.print("mode "); + Serial.print(gunMode); + Serial.print(", IR pos fps "); + Serial.print(irPosCount); + Serial.print(", loop/sec "); + Serial.print(frameCount); + + Serial.print(", Mouse X,Y "); + Serial.print(conMoveXAxis); + Serial.print(","); + Serial.println(conMoveYAxis); + + frameCount = 0; + irPosCount = 0; + serialDbMs = millis(); + } +} +#endif // DEBUG_SERIAL + diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.cpp b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.cpp new file mode 100644 index 0000000..5a746aa --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.cpp @@ -0,0 +1,138 @@ +/*! + * @file FusionPreferences.cpp + * @brief Fusion Prow Enhanced light gun preferences to save in non-volatile memory. + * + * @copyright Mike Lynch, 2021 + * @copyright GNU Lesser General Public License + * + * @author Mike Lynch + * @author Gonezo + * @version V1.03 + * @date 2022 + */ + +#include "FusionPreferences.h" +#ifdef FUSION_FLASH_ENABLE +#include +#endif // FUSION_FLASH_ENABLE +#ifdef FUSION_EEPROM_ENABLE +#include +#endif // FUSION_EEPROM_ENABLE + +// 4 byte header ID +const FusionPreferences::HeaderId_t FusionPreferences::HeaderId = {'P', 'r', 'o', 'w'}; + +#if defined(FUSION_FLASH_ENABLE) + +// must match Errors_e order +const char* FusionPreferences::ErrorText[6] = { + "Success", + "No storage memory", + "Read error", + "No preferences saved", + "Write error", + "Erase failed" +}; + +const char* FusionPreferences::ErrorCodeToString(int error) +{ + if(error >= 0) { + // all positive values are success + error = 0; + } else { + error = -error; + } + if(error < sizeof(ErrorText) / sizeof(ErrorText[0])) { + return ErrorText[error]; + } + return ""; +} + +int FusionPreferences::Load(Adafruit_SPIFlashBase& flash) +{ + uint32_t u32; + uint32_t fr = flash.readBuffer(0, (uint8_t*)&u32, sizeof(u32)); + if(fr != 4) { + return Error_Read; + } + + if(u32 != HeaderId.u32) { + return Error_NoData; + } + + fr = flash.readBuffer(4, &preferences.profile, 1); + if(fr != 1) { + return Error_Read; + } + + const uint32_t length = sizeof(ProfileData_t) * preferences.profileCount; + fr = flash.readBuffer(5, (uint8_t*)preferences.pProfileData, length); + + return fr == length ? Error_Success : Error_Read; +} + +int FusionPreferences::Save(Adafruit_SPIFlashBase& flash) +{ + bool success = flash.eraseSector(0); + if(!success) { + return Error_Erase; + } + + uint32_t fw = flash.writeBuffer(0, (uint8_t*)&HeaderId.u32, sizeof(HeaderId.u32)); + if(fw != sizeof(HeaderId.u32)) { + return Error_Write; + } + + fw = flash.writeBuffer(4, &preferences.profile, 1); + if(fw != 1) { + return Error_Write; + } + + const uint32_t length = sizeof(ProfileData_t) * preferences.profileCount; + fw = flash.writeBuffer(5, (uint8_t*)preferences.pProfileData, length); + + return fw == length ? Error_Success : Error_Write; +} + +#elif defined(FUSION_EEPROM_ENABLE) + +int FusionPreferences::Load() +{ + uint32_t u32; + EEPROM.get(0, u32); + if(u32 != HeaderId.u32) { + return Error_NoData; + } + + preferences.profile = EEPROM.read(4); + uint8_t* p = ((uint8_t*)preferences.pProfileData); + for(unsigned int i = 0; i < sizeof(ProfileData_t) * preferences.profileCount; ++i) { + p[i] = EEPROM.read(5 + i); + } + return Error_Success; +} + +int FusionPreferences::Save() +{ + EEPROM.put(0, HeaderId.u32); + EEPROM.write(4, preferences.profile); + uint8_t* p = ((uint8_t*)preferences.pProfileData); + for(unsigned int i = 0; i < sizeof(ProfileData_t) * preferences.profileCount; ++i) { + EEPROM.write(5 + i, p[i]); + } + return Error_Success; +} + +#else + +int FusionPreferences::Load() +{ + return Error_NoStorage; +} + +int FusionPreferences::Save() +{ + return Error_NoStorage; +} + +#endif diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.h b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.h new file mode 100644 index 0000000..155e427 --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player1/FusionLightgun/FusionPreferences.h @@ -0,0 +1,104 @@ +/*! + * @file FusionPreferences.h + * + * @copyright Gonezo Fusion Lightguns + * @copyright GNU Lesser General Public License + * + * @author Mike Lynch + * @author Gonezo + * @version V2.00 + * @date 2023 + */ + +#ifndef _FUSIONPREFERENCES_H_ +#define _FUSIONPREFERENCES_H_ + +#include +#include + +#ifdef FUSION_FLASH_ENABLE +class Adafruit_SPIFlashBase; +#endif // FUSION_FLASH_ENABLE + +/// @brief Static instance of preferences to save in non-volatile memory +class FusionPreferences +{ +public: + /// @brief Error codes + enum Errors_e { + Error_Success = 0, + Error_NoStorage = -1, + Error_Read = -2, + Error_NoData = -3, + Error_Write = -4, + Error_Erase = -5 + }; + + /// @brief Header ID + typedef union HeaderId_u { + uint8_t bytes[4]; + uint32_t u32; + } __attribute__ ((packed)) HeaderId_t; + + /// @brief Profile data + typedef struct ProfileData_s { + uint16_t xScale; ///< X Scale * 1000 + uint16_t yScale; ///< Y Scale * 1000 + uint32_t xCenter : 12; + uint32_t yCenter : 12; + uint32_t irSensitivity : 3; + uint32_t runMode : 5; + uint32_t reserved; + uint32_t reserved2; + } __attribute__ ((packed)) ProfileData_t; + + /// @brief Preferences that can be stored in flash + typedef struct Preferences_s { + // pointer to ProfileData_t array + FusionPreferences::ProfileData_t* pProfileData; + + // number of ProfileData_t entries + uint8_t profileCount; + + // default profile + uint8_t profile; + } __attribute__ ((packed)) Preferences_t; + + // single instance of the preference data + static Preferences_t preferences; + + // header ID to ensure junk isn't loaded if preferences aren't saved + static const HeaderId_t HeaderId; + + /// @brief Required size for the preferences + static unsigned int Size() { return sizeof(ProfileData_t) * preferences.profileCount + sizeof(HeaderId_u) + sizeof(preferences.profile); } + +#ifdef FUSION_FLASH_ENABLE + + /// @brief Load preferences + /// @return An error code from Errors_e + static int Load(Adafruit_SPIFlashBase& flash); + + /// @brief Save preferences + /// @return An error code from Errors_e + static int Save(Adafruit_SPIFlashBase& flash); + + /// @brief Get a string for a given error code + static const char* ErrorCodeToString(int error); + +private: + // error text table + static const char* ErrorText[6]; + +#else + /// @brief Load preferences + /// @return An error code from Errors_e + static int Load(); + + /// @brief Save preferences + /// @return An error code from Errors_e + static int Save(); +#endif +}; + +#endif // _FUSIONPREFERENCES_H_ diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player1/Pin-Button-Layout.txt b/DIY/Code It/Pro-Micro/Version-3.2/Player1/Pin-Button-Layout.txt new file mode 100644 index 0000000..2de54fa --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player1/Pin-Button-Layout.txt @@ -0,0 +1,35 @@ +Pro Micro Pin & Button Layout + +Pin #-- On Gun -- Keyboard/Mouse Button-- In Code + +___TRIGGER, CALIBRATE & RELOAD____ +10 ---- Trigger -------- Mouse Left +16 ---- Reload -------- Mouse Right +4 ---- Bomb -------- Mouse Middle +14 ---- Mouse5 ----------- Calibrate + +___BUTTONS +6 --- Enter ---- A Button +7 --- Escape ------ B Button +8 --- 5 --------------- Select +9 --- 1 --------------- Start + + +____DPAD BUTTONS____ +A0 -- Up Arrow ---------- Up +A1 -- Down Arrow -------- Down +A2 -- Left Arrow -------- Left +A3 -- Right Arrow ------- Right + +____OTHER NEEDED PINS____ + +2 ---- SDA on Camera +3 ---- SDL on Camera +VCC -- POWER for Camera +GND -- Ground Pins +15 --- Motor +5 ---- Solenoid + +Your Buttons & Camera Need To Be Grounded + + \ No newline at end of file diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionColours.h b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionColours.h new file mode 100644 index 0000000..15df9bc --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionColours.h @@ -0,0 +1,51 @@ +/*! + * @file FusionColours.h + * + * @copyright Gonezo Fusion Lightguns + * @copyright GNU Lesser General Public License + * + * @author Mike Lynch + * @author Gonezo + * @version V2.00 + * @date 2023 + */ + +#ifndef _FUSIONCOLOURS_H_ +#define _FUSIONCOLOURS_H_ + +#include + +// macro to scale an 8 bit colour value by an 8 bit value +// as seen by the math, 255 means full value +#define COLOR_BRI_ADJ_COLOR(brightness, color) ((((brightness) * ((color) & 0xFF)) / 255) & 0xFF) +//#define COLOR_BRI_ADJ_COLOR(brightness, color) (color) + +// macro to scale a 32-bit RGBW word with an 8 bit brightness value +#define COLOR_BRI_ADJ_RGB(brightness, rgb) COLOR_BRI_ADJ_COLOR(brightness, rgb) \ + | (COLOR_BRI_ADJ_COLOR(brightness, (rgb >> 8)) << 8) \ + | (COLOR_BRI_ADJ_COLOR(brightness, (rgb >> 16)) << 16) \ + | (COLOR_BRI_ADJ_COLOR(brightness, (rgb >> 24)) << 24) + +// some distinct colours from Wikipedia https://en.wikipedia.org/wiki/Lists_of_colors +// also adjusted the brightness to make them look even more distinct on an ItsyBitsy DotStar +// ... yeah I spent too much time on this +namespace WikiColor { + constexpr uint32_t Amber = COLOR_BRI_ADJ_RGB(130, 0xFFBF00); + constexpr uint32_t Blue = COLOR_BRI_ADJ_RGB(225, 0x0000FF); + constexpr uint32_t Carnation_pink = COLOR_BRI_ADJ_RGB(165, 0xFFA6C9); + constexpr uint32_t Cerulean_blue = COLOR_BRI_ADJ_RGB(255, 0x2A52BE); + constexpr uint32_t Cornflower_blue = COLOR_BRI_ADJ_RGB(175, 0x6495ED); + constexpr uint32_t Cyan = COLOR_BRI_ADJ_RGB(145, 0x00FFFF); + constexpr uint32_t Electric_indigo = COLOR_BRI_ADJ_RGB(235, 0x6F00FF); + constexpr uint32_t Ghost_white = COLOR_BRI_ADJ_RGB(135, 0xF8F8FF); + constexpr uint32_t Golden_yellow = COLOR_BRI_ADJ_RGB(135, 0xFFDF00); + constexpr uint32_t Green = COLOR_BRI_ADJ_RGB(140, 0x00FF00); + constexpr uint32_t Green_Lizard = COLOR_BRI_ADJ_RGB(145, 0xA7F432); + constexpr uint32_t Magenta = COLOR_BRI_ADJ_RGB(140, 0xFF00FF); + constexpr uint32_t Orange = COLOR_BRI_ADJ_RGB(150, 0xFF7F00); + constexpr uint32_t Red = COLOR_BRI_ADJ_RGB(145, 0xFF0000); + constexpr uint32_t Salmon = COLOR_BRI_ADJ_RGB(175, 0xFA8072); + constexpr uint32_t Yellow = COLOR_BRI_ADJ_RGB(135, 0xFFFF00); +}; + +#endif // _FUSIONCOLOURS_H_ diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionLightgun.ino b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionLightgun.ino new file mode 100644 index 0000000..56ae245 --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionLightgun.ino @@ -0,0 +1,1695 @@ + + /*! + @file FusionLightgun.ino + @brief Fusion Lightgun code orginally taken from Samco and Pwor7. Updated by Gonezo from Fusion Lightguns. + + + @copyright Gonezo Fusion Lightguns + @copyright GNU Lesser General Public License + + @version V3.0 + @p player#2 + @date 2023 +*/ + +#include +#include + +// include TinyUSB or HID depending on USB stack option +#if defined(USE_TINYUSB) +#include +#elif defined(CFG_TUSB_MCU) +#error Incompatible USB stack. Use Arduino or Adafruit TinyUSB. +#else +// Arduino USB stack +#include +#endif + +#include +#include +#ifdef DOTSTAR_ENABLE +#include +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_PIN +#include +#endif +#ifdef FUSION_FLASH_ENABLE +#include +#endif // FUSION_FLASH_ENABLE +#include +#include +#include +#include +#include +#include "FusionColours.h" +#include "FusionPreferences.h" + +#ifdef ARDUINO_ARCH_RP2040 +#include +#include + +// declare PWM ISR +void rp2040pwmIrq(void); +#endif + +// enable extra serial debug during run mode +//#define PRINT_VERBOSE 1 +//#define DEBUG_SERIAL 1 +//#define DEBUG_SERIAL 2 + +// numbered index of buttons, must match ButtonDesc[] order +enum ButtonIndex_e { + BtnIdx_Trigger = 0, + BtnIdx_A, + BtnIdx_B, + BtnIdx_Start, + BtnIdx_Select, + BtnIdx_Calibrate, + BtnIdx_Bomb, + BtnIdx_Up, + BtnIdx_Down, + BtnIdx_Left, + BtnIdx_Right, + BtnIdx_Reload +}; + +// bit mask for each button, must match ButtonDesc[] order to match the proper button events +enum ButtonMask_e { + BtnMask_Trigger = 1 << BtnIdx_Trigger, + BtnMask_A = 1 << BtnIdx_A, + BtnMask_B = 1 << BtnIdx_B, + BtnMask_Start = 1 << BtnIdx_Start, + BtnMask_Select = 1 << BtnIdx_Select, + BtnMask_Calibrate = 1 << BtnIdx_Calibrate, + BtnMask_Bomb = 1 << BtnIdx_Bomb, + BtnMask_Up = 1 << BtnIdx_Up, + BtnMask_Down = 1 << BtnIdx_Down, + BtnMask_Left = 1 << BtnIdx_Left, + BtnMask_Right = 1 << BtnIdx_Right, + BtnMask_Reload = 1 << BtnIdx_Reload +}; + +// Button descriptor +// The order of the buttons is the order of the button bitmask +const LightgunButtons::Desc_t LightgunButtons::ButtonDesc[] = { + {10, LightgunButtons::ReportType_Mouse, MOUSE_LEFT, 25, BTN_AG_MASK, "Trigger"}, + {9, LightgunButtons::ReportType_Keyboard, KEY_LEFT_SHIFT, 25, BTN_AG_MASK2, "A"}, + {8, LightgunButtons::ReportType_Keyboard, KEY_LEFT_CTRL, 25, BTN_AG_MASK2, "B"}, + {7, LightgunButtons::ReportType_Keyboard, KEY_2, 20, BTN_AG_MASK2, "Start"}, + {6, LightgunButtons::ReportType_Keyboard, KEY_6, 25, BTN_AG_MASK2, "Select"}, + {14, LightgunButtons::ReportType_Mouse, MOUSE_BUTTON5, 20, BTN_AG_MASK2, "Calibrate"}, + {4, LightgunButtons::ReportType_Mouse, MOUSE_MIDDLE, 20, BTN_AG_MASK2, "Bomb"}, + {A0, LightgunButtons::ReportType_Keyboard, KEY_UP_ARROW, 25, BTN_AG_MASK2, "Up"}, + {A1, LightgunButtons::ReportType_Keyboard, KEY_DOWN_ARROW, 25, BTN_AG_MASK2, "Down"}, + {A2, LightgunButtons::ReportType_Keyboard, KEY_LEFT_ARROW, 25, BTN_AG_MASK2, "Left"}, + {A3, LightgunButtons::ReportType_Keyboard, KEY_RIGHT_ARROW, 25, BTN_AG_MASK2, "Right"}, + {16, LightgunButtons::ReportType_Mouse, MOUSE_RIGHT, 20, BTN_AG_MASK2, "Reload"} +}; + +#define mot_pin 15 +#define recoil_pin 5 + +// button count constant + + +constexpr unsigned int ButtonCount = sizeof(LightgunButtons::ButtonDesc) / sizeof(LightgunButtons::ButtonDesc[0]); + +// button runtime data arrays +LightgunButtonsStatic lgbData; + +// button object instance +LightgunButtons buttons(lgbData, ButtonCount); + + +// button combo to enter pause mode +constexpr uint32_t EnterPauseModeBtnMask = BtnMask_Calibrate; + +// press any button to enter pause mode from Processing mode (this is not a button combo) +constexpr uint32_t EnterPauseModeProcessingBtnMask = BtnMask_A | BtnMask_B | BtnMask_Calibrate; + +// button combo to exit pause mode back to run mode +constexpr uint32_t ExitPauseModeBtnMask = BtnMask_Calibrate; + +// press any button to cancel the calibration (this is not a button combo) +constexpr uint32_t CancelCalBtnMask = BtnMask_Calibrate; + +// button combo to skip the center calibration step +constexpr uint32_t SkipCalCenterBtnMask = BtnMask_A; + +// button combo to save preferences to non-volatile memory +constexpr uint32_t SaveBtnMask = BtnMask_A | BtnMask_Select; + +// button combo to increase IR sensitivity +constexpr uint32_t IRSensitivityUpBtnMask = BtnMask_B | BtnMask_Select; + +// button combo to decrease IR sensitivity +constexpr uint32_t IRSensitivityDownBtnMask = BtnMask_B | BtnMask_A; + +// button combinations to select a run mode +constexpr uint32_t RunModeNormalBtnMask = BtnMask_Start | BtnMask_Select; +constexpr uint32_t RunModeAverageBtnMask = BtnMask_Start | BtnMask_A; +constexpr uint32_t RunModeProcessingBtnMask = BtnMask_Start | BtnMask_B; + +// colour when no IR points are seen +constexpr uint32_t IRSeen0Color = WikiColor::Red; + +// colour when calibrating +constexpr uint32_t CalModeColor = WikiColor::Cornflower_blue; + +// number of profiles +constexpr unsigned int ProfileCount = 8; + +// run modes +// note that this is a 5 bit value when stored in the profiles +enum RunMode_e { + RunMode_Normal = 0, ///< Normal gun mode, no averaging + RunMode_Average = 1, ///< 2 frame moving average + RunMode_Average2 = 2, ///< weighted average with 3 frames + RunMode_ProfileMax = 2, ///< maximum mode allowed for profiles + RunMode_Processing = 3, ///< Processing test mode + RunMode_Count +}; + +// profiles +// defaults can be populated here, or not worry about these values and just save to flash/EEPROM +// if you have original Fusion calibration values, multiply by 4 for the center position and +// scale is multiplied by 1000 and stored as an unsigned integer, see FusionPreferences::Calibration_t +FusionPreferences::ProfileData_t profileData[ProfileCount] = { + {1619, 950, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {1233, 950, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {1538, 855, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {1147, 855, MouseMaxX / 2, MouseMaxY / 2, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal, 0, 0} +}; +/*FusionPreferences::ProfileData_t profileData[profileCount] = { + {1619, 950, 1899, 1531, DFRobotIRPositionEx::Sensitivity_Max, RunMode_Average}, + {1233, 943, 1864, 1538, DFRobotIRPositionEx::Sensitivity_Max, RunMode_Average}, + {1538, 855, 1878, 1515, DFRobotIRPositionEx::Sensitivity_High, RunMode_Average}, + {1147, 855, 1889, 1507, DFRobotIRPositionEx::Sensitivity_High, RunMode_Average}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal}, + {0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Normal} + };*/ + +// profile descriptor +typedef struct ProfileDesc_s { + // button(s) to select the profile + uint32_t buttonMask; + + // LED colour + uint32_t color; + + // button label + const char* buttonLabel; + + // optional profile label + const char* profileLabel; +} ProfileDesc_t; + +// profile descriptor +static const ProfileDesc_t profileDesc[ProfileCount] = { + {BtnMask_A, WikiColor::Cerulean_blue, "A", "TV"}, + {BtnMask_B, WikiColor::Cornflower_blue, "B", "TV 4:3"}, + {BtnMask_Start, WikiColor::Green, "Start", "Monitor"}, + {BtnMask_Select, WikiColor::Green_Lizard, "Select", "Monitor 4:3"}, +}; + +// overall calibration defaults, no need to change if data saved to NV memory or populate the profile table +// see profileData[] array below for specific profile defaults +int xCenter = MouseMaxX / 2; +int yCenter = MouseMaxY / 2; +float xScale = 1.64; +float yScale = 0.95; + +// step size for adjusting the scale +constexpr float ScaleStep = 0.001; + +int finalX = 0; // Values after tilt correction +int finalY = 0; + +int moveXAxis = 0; // Unconstrained mouse postion +int moveYAxis = 0; +int moveXAxisArr[3] = {0, 0, 0}; +int moveYAxisArr[3] = {0, 0, 0}; +int moveIndex = 0; + +int conMoveXAxis = 0; // Constrained mouse postion +int conMoveYAxis = 0; + +unsigned int lastSeen = 0; + +#ifdef EXTRA_POS_GLITCH_FILTER +int badFinalTick = 0; +int badMoveTick = 0; +int badFinalCount = 0; +int badMoveCount = 0; + +// number of consecutive bad move values to filter +constexpr unsigned int BadMoveCountThreshold = 3; + +// Used to filter out large jumps/glitches +constexpr int BadMoveThreshold = 49 * CamToMouseMult; +#endif // EXTRA_POS_GLITCH_FILTER + +// profile in use +unsigned int selectedProfile = 0; + +// IR positioning camera +#ifdef ARDUINO_ADAFRUIT_ITSYBITSY_RP2040 +DFRobotIRPositionEx dfrIRPos(Wire1); +#else +//DFRobotIRPosition myDFRobotIRPosition; +DFRobotIRPositionEx dfrIRPos(Wire); +#endif + +// Fusion positioning +FusionPositionEnhanced myFusion; + +// operating modes +enum GunMode_e { + GunMode_Init = -1, + GunMode_Run = 0, + GunMode_CalHoriz = 1, + GunMode_CalVert = 2, + GunMode_CalCenter = 3, + GunMode_Pause = 4 +}; +GunMode_e gunMode = GunMode_Init; // initial mode + +// run mode +RunMode_e runMode = RunMode_Normal; + +// IR camera sensitivity +DFRobotIRPositionEx::Sensitivity_e irSensitivity = DFRobotIRPositionEx::Sensitivity_Default; + +static const char* RunModeLabels[RunMode_Count] = { + "Normal", + "Averaging", + "Averaging2", + "Processing" +}; + +// preferences saved in non-volatile memory, populated with defaults +FusionPreferences::Preferences_t FusionPreferences::preferences = { + profileData, ProfileCount, // profiles + 0, // default profile +}; + +enum StateFlag_e { + // print selected profile once per pause state when the COM port is open + StateFlag_PrintSelectedProfile = (1 << 0), + + // report preferences once per pause state when the COM port is open + StateFlag_PrintPreferences = (1 << 1), + + // enable save (allow save once per pause state) + StateFlag_SavePreferencesEn = (1 << 2), + + // print preferences storage + StateFlag_PrintPreferencesStorage = (1 << 3) +}; + +// when serial connection resets, these flags are set +constexpr uint32_t StateFlagsDtrReset = StateFlag_PrintSelectedProfile | StateFlag_PrintPreferences | StateFlag_PrintPreferencesStorage; + +// state flags, see StateFlag_e +uint32_t stateFlags = StateFlagsDtrReset; + + +#ifdef DOTSTAR_ENABLE +// note if the colours don't match then change the colour format from BGR +// apparently different lots of DotStars may have different colour ordering ¯\_(ツ)_/¯ +Adafruit_DotStar dotstar(1, DOTSTAR_DATAPIN, DOTSTAR_CLOCKPIN, DOTSTAR_BGR); +#endif // DOTSTAR_ENABLE + +#ifdef NEOPIXEL_PIN +Adafruit_NeoPixel neopixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); +#endif // NEOPIXEL_PIN + +// flash transport instance +#if defined(EXTERNAL_FLASH_USE_QSPI) +Adafruit_FlashTransport_QSPI flashTransport; +#elif defined(EXTERNAL_FLASH_USE_SPI) +Adafruit_FlashTransport_SPI flashTransport(EXTERNAL_FLASH_USE_CS, EXTERNAL_FLASH_USE_SPI); +#endif + +#ifdef FUSION_FLASH_ENABLE +// Adafruit_SPIFlashBase non-volatile storage +// flash instance +Adafruit_SPIFlashBase flash(&flashTransport); + +static const char* NVRAMlabel = "Flash"; + +// flag to indicate if non-volatile storage is available +// this will enable in setup() +bool nvAvailable = false; +#endif // FUSION_FLASH_ENABLE + +#ifdef FUSION_EEPROM_ENABLE +// EEPROM non-volatile storage +static const char* NVRAMlabel = "EEPROM"; + +// flag to indicate if non-volatile storage is available +// unconditional for EEPROM +bool nvAvailable = true; +#endif + +// non-volatile preferences error code +int nvPrefsError = FusionPreferences::Error_NoStorage; + +// preferences instance +FusionPreferences elitePreferences; + +// number of times the IR camera will update per second +constexpr unsigned int IRCamUpdateRate = 209; + +#ifdef FUSION_NO_HW_TIMER +// use the millis() or micros() counter instead +unsigned long irPosUpdateTime = 0; +// will set this to 1 when the IR position can update +unsigned int irPosUpdateTick = 0; + +#define FUSION_NO_HW_TIMER_UPDATE() NoHardwareTimerCamTickMillis() +//define FUSION_NO_HW_TIMER_UPDATE() NoHardwareTimerCamTickMicros() + +#else +#define FUSION_NO_HW_TIMER_UPDATE() +// timer will set this to 1 when the IR position can update +volatile unsigned int irPosUpdateTick = 0; +#endif // FUSION_NO_HW_TIMER + +#ifdef DEBUG_SERIAL +static unsigned long serialDbMs = 0; +static unsigned long frameCount = 0; +static unsigned long irPosCount = 0; +#endif + +// used for periodic serial prints +unsigned long lastPrintMillis = 0; + +#ifdef USE_TINYUSB + +// USB HID Report ID +enum HID_RID_e { + HID_RID_KEYBOARD = 1, + HID_RID_MOUSE +}; + +// HID report descriptor using TinyUSB's template +uint8_t const desc_hid_report[] = { + TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(HID_RID_KEYBOARD)), + TUD_HID_REPORT_DESC_ABSMOUSE5(HID_REPORT_ID(HID_RID_MOUSE)) +}; + +Adafruit_USBD_HID usbHid; + +int __USBGetKeyboardReportID() +{ + return HID_RID_KEYBOARD; +} + +// AbsMouse5 instance +AbsMouse5_ AbsMouse5(HID_RID_MOUSE); + +#else +// AbsMouse5 instance +AbsMouse5_ AbsMouse5(1); +#endif + +void setup() { + // Initialize Button Pins + pinMode(9, INPUT_PULLUP); + pinMode(10, INPUT_PULLUP); + pinMode(11, INPUT_PULLUP); + pinMode(12, INPUT_PULLUP); + pinMode(recoil_pin, OUTPUT); + + // init DotStar and/or NeoPixel to red during setup() +#ifdef DOTSTAR_ENABLE + dotstar.begin(); + dotstar.setPixelColor(0, 150, 0, 0); + dotstar.show(); +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_ENABLEPIN + pinMode(NEOPIXEL_ENABLEPIN, OUTPUT); + digitalWrite(NEOPIXEL_ENABLEPIN, HIGH); +#endif // NEOPIXEL_ENABLEPIN +#ifdef NEOPIXEL_PIN + neopixel.begin(); + neopixel.setPixelColor(0, 255, 0, 0); + neopixel.show(); +#endif // NEOPIXEL_PIN + +#ifdef ARDUINO_ADAFRUIT_ITSYBITSY_RP2040 + // ensure Wire1 SDA and SCL are correct + Wire1.setSDA(2); + Wire1.setSCL(3); +#endif + + // initialize buttons + buttons.Begin(); + +#ifdef FUSION_FLASH_ENABLE + // init flash and load saved preferences + nvAvailable = flash.begin(); +#endif // FUSION_FLASH_ENABLE + + if (nvAvailable) { + LoadPreferences(); + } + + // use values from preferences + ApplyInitialPrefs(); + + // Start IR Camera with basic data format + dfrIRPos.begin(DFROBOT_IR_IIC_CLOCK, DFRobotIRPositionEx::DataFormat_Basic, irSensitivity); + +#ifdef USE_TINYUSB + usbHid.setPollInterval(2); + usbHid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report)); + //usb_hid.setStringDescriptor("TinyUSB HID Composite"); + + usbHid.begin(); +#endif + + Serial.begin(115200); + + AbsMouse5.init(MouseMaxX, MouseMaxY, true); + + // sanity to ensure the cal prefs is populated with at least 1 entry + // in case the table is zero'd out + if (profileData[selectedProfile].xCenter == 0) { + profileData[selectedProfile].xCenter = xCenter; + } + if (profileData[selectedProfile].yCenter == 0) { + profileData[selectedProfile].yCenter = yCenter; + } + if (profileData[selectedProfile].xScale == 0) { + profileData[selectedProfile].xScale = CalScaleFloatToPref(xScale); + } + if (profileData[selectedProfile].yScale == 0) { + profileData[selectedProfile].yScale = CalScaleFloatToPref(yScale); + } + + // fetch the calibration data, other values already handled in ApplyInitialPrefs() + SelectCalPrefs(selectedProfile); + +#ifdef USE_TINYUSB + // wait until device mounted + while (!USBDevice.mounted()) { + yield(); + } +#else + // was getting weird hangups... maybe nothing, or maybe related to dragons, so wait a bit + delay(100); +#endif + + // IR camera maxes out motion detection at ~300Hz, and millis() isn't good enough + startIrCamTimer(IRCamUpdateRate); + + // this will turn off the DotStar/RGB LED and ensure proper transition to Run + SetMode(GunMode_Run); + pinMode(mot_pin, OUTPUT); + digitalWrite(mot_pin, LOW); //turn off motor initially +} + +void startIrCamTimer(int frequencyHz) +{ +#if defined(FUSION_SAMD21) + startTimerEx(&TC4->COUNT16, GCLK_CLKCTRL_ID_TC4_TC5, TC4_IRQn, frequencyHz); +#elif defined(FUSION_SAMD51) + startTimerEx(&TC3->COUNT16, TC3_GCLK_ID, TC3_IRQn, frequencyHz); +#elif defined(FUSION_ATMEGA32U4) + startTimer3(frequencyHz); +#elif defined(FUSION_RP2040) + rp2040EnablePWMTimer(0, frequencyHz); + irq_set_exclusive_handler(PWM_IRQ_WRAP, rp2040pwmIrq); + irq_set_enabled(PWM_IRQ_WRAP, true); +#endif +} + +#if defined(FUSION_SAMD21) +void startTimerEx(TcCount16* ptc, uint16_t gclkCtrlId, IRQn_Type irqn, int frequencyHz) +{ + // use Generic clock generator 0 + GCLK->CLKCTRL.reg = (uint16_t)(GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | gclkCtrlId); + while (GCLK->STATUS.bit.SYNCBUSY == 1); // wait for sync + + ptc->CTRLA.bit.ENABLE = 0; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + // Use the 16-bit timer + ptc->CTRLA.reg |= TC_CTRLA_MODE_COUNT16; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + // Use match mode so that the timer counter resets when the count matches the compare register + ptc->CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + // Set prescaler + ptc->CTRLA.reg |= TIMER_TC_CTRLA_PRESCALER_DIV; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync + + setTimerFrequency(ptc, frequencyHz); + + // Enable the compare interrupt + ptc->INTENSET.reg = 0; + ptc->INTENSET.bit.MC0 = 1; + + NVIC_EnableIRQ(irqn); + + ptc->CTRLA.bit.ENABLE = 1; + while (ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync +} + +void TC4_Handler() +{ + // If this interrupt is due to the compare register matching the timer count + if (TC4->COUNT16.INTFLAG.bit.MC0 == 1) { + // clear interrupt + TC4->COUNT16.INTFLAG.bit.MC0 = 1; + + irPosUpdateTick = 1; + } +} +#endif // FUSION_SAMD21 + +#if defined(FUSION_SAMD51) +void startTimerEx(TcCount16* ptc, uint16_t gclkCtrlId, IRQn_Type irqn, int frequencyHz) +{ + // use Generic clock generator 0 + GCLK->PCHCTRL[gclkCtrlId].reg = GCLK_PCHCTRL_GEN_GCLK0 | GCLK_PCHCTRL_CHEN; + while (GCLK->SYNCBUSY.reg); // wait for sync + + ptc->CTRLA.bit.ENABLE = 0; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + // Use the 16-bit timer + ptc->CTRLA.reg |= TC_CTRLA_MODE_COUNT16; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + // Use match mode so that the timer counter resets when the count matches the compare register + ptc->WAVE.bit.WAVEGEN = TC_WAVE_WAVEGEN_MFRQ; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + // Set prescaler + ptc->CTRLA.reg |= TIMER_TC_CTRLA_PRESCALER_DIV; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync + + setTimerFrequency(ptc, frequencyHz); + + // Enable the compare interrupt + ptc->INTENSET.reg = 0; + ptc->INTENSET.bit.MC0 = 1; + + NVIC_EnableIRQ(irqn); + + ptc->CTRLA.bit.ENABLE = 1; + while (ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync +} + +void TC3_Handler() +{ + // If this interrupt is due to the compare register matching the timer count + if (TC3->COUNT16.INTFLAG.bit.MC0 == 1) { + // clear interrupt + TC3->COUNT16.INTFLAG.bit.MC0 = 1; + + irPosUpdateTick = 1; + } +} +#endif // FUSION_SAMD51 + +#if defined(FUSION_SAMD21) || defined(FUSION_SAMD51) +void setTimerFrequency(TcCount16* ptc, int frequencyHz) +{ + int compareValue = (F_CPU / (TIMER_PRESCALER_DIV * frequencyHz)); + + // Make sure the count is in a proportional position to where it was + // to prevent any jitter or disconnect when changing the compare value. + ptc->COUNT.reg = map(ptc->COUNT.reg, 0, ptc->CC[0].reg, 0, compareValue); + ptc->CC[0].reg = compareValue; + +#if defined(FUSION_SAMD21) + while (ptc->STATUS.bit.SYNCBUSY == 1); +#elif defined(FUSION_SAMD51) + while (ptc->SYNCBUSY.bit.STATUS == 1); +#endif +} +#endif + +#ifdef FUSION_ATMEGA32U4 +void startTimer3(unsigned long frequencyHz) +{ + // disable comapre output mode + TCCR3A = 0; + + //set the pre-scalar to 8 and set Clear on Compare + TCCR3B = (1 << CS31) | (1 << WGM32); + + // set compare value + OCR3A = F_CPU / (8UL * frequencyHz); + + // enable Timer 3 Compare A interrupt + TIMSK3 = 1 << OCIE3A; +} + +// Timer3 compare A interrupt +ISR(TIMER3_COMPA_vect) +{ + irPosUpdateTick = 1; +} +#endif // FUSION_ATMEGA32U4 + +#ifdef ARDUINO_ARCH_RP2040 +void rp2040EnablePWMTimer(unsigned int slice_num, unsigned int frequency) +{ + pwm_config pwmcfg = pwm_get_default_config(); + float clkdiv = (float)clock_get_hz(clk_sys) / (float)(65535 * frequency); + if (clkdiv < 1.0f) { + clkdiv = 1.0f; + } else { + // really just need to round up 1 lsb + clkdiv += 2.0f / (float)(1u << PWM_CH1_DIV_INT_LSB); + } + + // set the clock divider in the config and fetch the actual value that is used + pwm_config_set_clkdiv(&pwmcfg, clkdiv); + clkdiv = (float)pwmcfg.div / (float)(1u << PWM_CH1_DIV_INT_LSB); + + // calculate wrap value that will trigger the IRQ for the target frequency + pwm_config_set_wrap(&pwmcfg, (float)clock_get_hz(clk_sys) / (frequency * clkdiv)); + + // initialize and start the slice and enable IRQ + pwm_init(slice_num, &pwmcfg, true); + pwm_set_irq_enabled(slice_num, true); +} + +void rp2040pwmIrq(void) +{ + pwm_hw->intr = 0xff; + irPosUpdateTick = 1; +} +#endif + +#ifdef FUSION_NO_HW_TIMER +void NoHardwareTimerCamTickMicros() +{ + unsigned long us = micros(); + if (us - irPosUpdateTime >= 1000000UL / IRCamUpdateRate) { + irPosUpdateTime = us; + irPosUpdateTick = 1; + } +} + +void NoHardwareTimerCamTickMillis() +{ + unsigned long ms = millis(); + if (ms - irPosUpdateTime >= (1000UL + (IRCamUpdateRate / 2)) / IRCamUpdateRate) { + irPosUpdateTime = ms; + irPosUpdateTick = 1; + } +} +#endif // FUSION_NO_HW_TIMER + + +///// Motor & Recoil //// + +void move_motor() +{ + digitalWrite(mot_pin, HIGH); +} + +void stop_motor() +{ + digitalWrite(mot_pin, LOW); +} + + +void recoil(){ + digitalWrite(recoil_pin, HIGH); + delay(30); + digitalWrite(recoil_pin, LOW); + delay(30); +} + +//// + +void loop() { + FUSION_NO_HW_TIMER_UPDATE(); + + // poll/update button states with 1ms interval so debounce mask is more effective + buttons.Poll(1); + buttons.Repeat(); + if (buttons.pressed & BtnMask_Trigger) move_motor(); //if trigger button pressed, vibrate motor + switch (gunMode) { + case GunMode_Pause: + if (buttons.pressedReleased == ExitPauseModeBtnMask) { + SetMode(GunMode_Run); + } else if (buttons.pressedReleased == BtnMask_Trigger) { + stop_motor(); SetMode(GunMode_CalCenter); + } else if (buttons.pressedReleased == RunModeNormalBtnMask) { + SetRunMode(RunMode_Normal); + } else if (buttons.pressedReleased == RunModeAverageBtnMask) { + SetRunMode(runMode == RunMode_Average ? RunMode_Average2 : RunMode_Average); + } else if (buttons.pressedReleased == RunModeProcessingBtnMask) { + SetRunMode(RunMode_Processing); + } else if (buttons.pressedReleased == IRSensitivityUpBtnMask) { + IncreaseIrSensitivity(); + } else if (buttons.pressedReleased == IRSensitivityDownBtnMask) { + DecreaseIrSensitivity(); + } else if (buttons.pressedReleased == SaveBtnMask) { + SavePreferences(); + } else { + SelectCalProfileFromBtnMask(buttons.pressedReleased); + } + + PrintResults(); + + break; + case GunMode_CalCenter: + AbsMouse5.move(MouseMaxX / 2, MouseMaxY / 2); + if (buttons.pressedReleased & CancelCalBtnMask) { + CancelCalibration(); + } else if (buttons.pressedReleased == SkipCalCenterBtnMask) { + Serial.println("Calibrate Center skipped"); + SetMode(GunMode_CalVert); + } else if (buttons.pressed & BtnMask_Trigger) { + // trigger pressed, begin center cal + move_motor(); + CalCenter(); + // extra delay to wait for trigger to release (though not required) + SetModeWaitNoButtons(GunMode_CalVert, 500); + } + break; + case GunMode_CalVert: + if (buttons.pressedReleased & CancelCalBtnMask) { + CancelCalibration(); + } else { + if (buttons.pressed & BtnMask_Trigger) { + move_motor(); + SetMode(GunMode_CalHoriz); + } else { + CalVert(); + } + } + break; + case GunMode_CalHoriz: + if (buttons.pressedReleased & CancelCalBtnMask) { + CancelCalibration(); + } else { + if (buttons.pressed & BtnMask_Trigger) { + move_motor(); + ApplyCalToProfile(); + SetMode(GunMode_Run); + } else { + CalHoriz(); + } + } + break; + default: + /* ---------------------- LET'S GO --------------------------- */ + switch (runMode) { + case RunMode_Processing: + ExecRunModeProcessing(); + break; + case RunMode_Average: + case RunMode_Average2: + case RunMode_Normal: + default: + ExecRunMode(); + break; + } + break; + } +#ifdef DEBUG_SERIAL + PrintDebugSerial(); +#endif // DEBUG_SERIAL +} + +/* ----------------------------------------------- */ +/* --------------------------- METHODS ------------------------- */ +/* ----------------------------------------------- */ + +void check_trigger() +{ + if (buttons.pressedReleased == BtnMask_Trigger) stop_motor(); + if (buttons.debounced == BtnMask_Trigger) recoil(); +} + +void ExecRunMode() +{ +#ifdef DEBUG_SERIAL + Serial.print("exec run mode "); + Serial.println(RunModeLabels[runMode]); +#endif + moveIndex = 0; + buttons.ReportEnable(); + for (;;) { + buttons.Poll(0); + check_trigger(); + FUSION_NO_HW_TIMER_UPDATE(); + if (irPosUpdateTick) { + irPosUpdateTick = 0; + GetPosition(); + + int halfHscale = (int)(myFusion.h() * xScale + 0.5f) / 2; + moveXAxis = map(finalX, xCenter + halfHscale, xCenter - halfHscale, 0, MouseMaxX); + halfHscale = (int)(myFusion.h() * yScale + 0.5f) / 2; + moveYAxis = map(finalY, yCenter + halfHscale, yCenter - halfHscale, 0, MouseMaxY); + + switch (runMode) { + case RunMode_Average: + // 2 position moving average + moveIndex ^= 1; + moveXAxisArr[moveIndex] = moveXAxis; + moveYAxisArr[moveIndex] = moveYAxis; + moveXAxis = (moveXAxisArr[0] + moveXAxisArr[1]) / 2; + moveYAxis = (moveYAxisArr[0] + moveYAxisArr[1]) / 2; + break; + case RunMode_Average2: + // weighted average of current position and previous 2 + if (moveIndex < 2) { + ++moveIndex; + } else { + moveIndex = 0; + } + moveXAxisArr[moveIndex] = moveXAxis; + moveYAxisArr[moveIndex] = moveYAxis; + moveXAxis = (moveXAxis + moveXAxisArr[0] + moveXAxisArr[1] + moveXAxisArr[1] + 2) / 4; + moveYAxis = (moveYAxis + moveYAxisArr[0] + moveYAxisArr[1] + moveYAxisArr[1] + 2) / 4; + break; + case RunMode_Normal: + default: + break; + } + + conMoveXAxis = constrain(moveXAxis, 0, MouseMaxX); + conMoveYAxis = constrain(moveYAxis, 0, MouseMaxY); + AbsMouse5.move(conMoveXAxis, conMoveYAxis); + +#ifdef DEBUG_SERIAL + ++irPosCount; +#endif // DEBUG_SERIAL + } + + if (buttons.pressedReleased == EnterPauseModeBtnMask) { + SetMode(GunMode_Pause); + buttons.ReportDisable(); + return; + } + +#ifdef DEBUG_SERIAL + ++frameCount; + PrintDebugSerial(); +#endif // DEBUG_SERIAL + } +} + +// from Fusion_4IR_Test_BETA sketch +// for use with the Fusion_4IR_Processing_Sketch_BETA Processing sketch +void ExecRunModeProcessing() +{ + // constant offset added to output values + const int processingOffset = 100; + + buttons.ReportDisable(); + for (;;) { + buttons.Poll(1); + if (buttons.pressedReleased & EnterPauseModeProcessingBtnMask) { + SetMode(GunMode_Pause); + return; + } + check_trigger(); + FUSION_NO_HW_TIMER_UPDATE(); + if (irPosUpdateTick) { + irPosUpdateTick = 0; + + int error = dfrIRPos.basicAtomic(DFRobotIRPositionEx::Retry_2); + if (error == DFRobotIRPositionEx::Error_Success) { + myFusion.begin(dfrIRPos.xPositions(), dfrIRPos.yPositions(), dfrIRPos.seen(), MouseMaxX / 2, MouseMaxY / 2); + UpdateLastSeen(); + for (int i = 0; i < 4; i++) { + Serial.print(map(myFusion.testX(i), 0, MouseMaxX, CamMaxX, 0) + processingOffset); + Serial.print(","); + Serial.print(map(myFusion.testY(i), 0, MouseMaxY, CamMaxY, 0) + processingOffset); + Serial.print(","); + } + Serial.print(map(myFusion.x(), 0, MouseMaxX, CamMaxX, 0) + processingOffset); + Serial.print(","); + Serial.print(map(myFusion.y(), 0, MouseMaxY, CamMaxY, 0) + processingOffset); + Serial.print(","); + Serial.print(map(myFusion.testMedianX(), 0, MouseMaxX, CamMaxX, 0) + processingOffset); + Serial.print(","); + Serial.println(map(myFusion.testMedianY(), 0, MouseMaxY, CamMaxY, 0) + processingOffset); + } else if (error == DFRobotIRPositionEx::Error_IICerror) { + Serial.println("Device not available!"); + } + } + } +} + +// center calibration with a bit of averaging +void CalCenter() +{ + unsigned int xAcc = 0; + unsigned int yAcc = 0; + unsigned int count = 0; + unsigned long ms = millis(); + + // accumulate center position over a bit of time for some averaging + while (millis() - ms < 333) { + // center pointer + AbsMouse5.move(MouseMaxX / 2, MouseMaxY / 2); + + // get position + if (GetPositionIfReady()) { + xAcc += finalX; + yAcc += finalY; + count++; + + xCenter = finalX; + yCenter = finalY; + PrintCalInterval(); + } + + // poll buttons + buttons.Poll(1); + + // if trigger not pressed then break out of loop early + if (!(buttons.debounced & BtnMask_Trigger)) { + break; + } + } + + // unexpected, but make sure x and y positions are accumulated + if (count) { + xCenter = xAcc / count; + yCenter = yAcc / count; + } else { + Serial.print("Unexpected Center calibration failure, no center position was acquired!"); + // just continue anyway + } + + PrintCalInterval(); +} + +// vertical calibration +void CalVert() +{ + if (GetPositionIfReady()) { + int halfH = (int)(myFusion.h() * yScale + 0.5f) / 2; + moveYAxis = map(finalY, yCenter + halfH, yCenter - halfH, 0, MouseMaxY); + conMoveXAxis = MouseMaxX / 2; + conMoveYAxis = constrain(moveYAxis, 0, MouseMaxY); + AbsMouse5.move(conMoveXAxis, conMoveYAxis); + } + + if (buttons.repeat & BtnMask_B) { + yScale = yScale + ScaleStep; + } + + if (buttons.repeat & BtnMask_A) { + if (yScale > 0.005f) { + yScale = yScale - ScaleStep; + } + } + + if (buttons.pressedReleased == BtnMask_Start) { + yCenter--; + } else if (buttons.pressedReleased == BtnMask_Select) { + yCenter++; + } + + PrintCalInterval(); +} + +// horizontal calibration +void CalHoriz() +{ + if (GetPositionIfReady()) { + int halfH = (int)(myFusion.h() * xScale + 0.5f) / 2; + moveXAxis = map(finalX, xCenter + halfH, xCenter - halfH, 0, MouseMaxX); + conMoveXAxis = constrain(moveXAxis, 0, MouseMaxX); + conMoveYAxis = MouseMaxY / 2; + AbsMouse5.move(conMoveXAxis, conMoveYAxis); + } + + if (buttons.repeat & BtnMask_B) { + xScale = xScale + ScaleStep; + } + + if (buttons.repeat & BtnMask_A) { + if (xScale > 0.005f) { + xScale = xScale - ScaleStep; + } + } + + if (buttons.pressedReleased == BtnMask_Start) { + xCenter--; + } else if (buttons.pressedReleased == BtnMask_Select) { + xCenter++; + } + + PrintCalInterval(); +} + +// Helper to get position if the update tick is set +bool GetPositionIfReady() +{ + if (irPosUpdateTick) { + irPosUpdateTick = 0; + GetPosition(); + return true; + } + return false; +} + +// Get tilt adjusted position from IR postioning camera +// Updates finalX and finalY values +void GetPosition() +{ + int error = dfrIRPos.basicAtomic(DFRobotIRPositionEx::Retry_2); + if (error == DFRobotIRPositionEx::Error_Success) { + myFusion.begin(dfrIRPos.xPositions(), dfrIRPos.yPositions(), dfrIRPos.seen(), xCenter, yCenter); +#ifdef EXTRA_POS_GLITCH_FILTER + if ((abs(myFusion.X() - finalX) > BadMoveThreshold || abs(myFusion.Y() - finalY) > BadMoveThreshold) && badFinalTick < BadMoveCountThreshold) { + ++badFinalTick; + } else { + if (badFinalTick) { + badFinalCount++; + badFinalTick = 0; + } + finalX = myFusion.X(); + finalY = myFusion.Y(); + } +#else + finalX = myFusion.x(); + finalY = myFusion.y(); +#endif // EXTRA_POS_GLITCH_FILTER + + UpdateLastSeen(); +#if DEBUG_SERIAL == 2 + Serial.print(finalX); + Serial.print(' '); + Serial.print(finalY); + Serial.print(" "); + Serial.println(myFusion.h()); +#endif + } else if (error != DFRobotIRPositionEx::Error_DataMismatch) { + Serial.println("Device not available!"); + } +} + +// wait up to given amount of time for no buttons to be pressed before setting the mode +void SetModeWaitNoButtons(GunMode_e newMode, unsigned long maxWait) +{ + unsigned long ms = millis(); + while (buttons.debounced && (millis() - ms < maxWait)) { + buttons.Poll(1); + } + SetMode(newMode); +} + +// update the last seen value +// only to be called during run mode since this will modify the LED colour +void UpdateLastSeen() { + if (lastSeen != myFusion.seen()) { + if (!lastSeen && myFusion.seen()) { + LedOff(); + } else if (lastSeen && !myFusion.seen()) { + SetLedPackedColor(IRSeen0Color); + } + lastSeen = myFusion.seen(); + } +} + +void SetMode(GunMode_e newMode) +{ + if (gunMode == newMode) { + return; + } + + // exit current mode + switch (gunMode) { + case GunMode_Run: + stateFlags |= StateFlag_PrintPreferences; + break; + case GunMode_CalHoriz: + break; + case GunMode_CalVert: + break; + case GunMode_CalCenter: + break; + case GunMode_Pause: + break; + } + + // enter new mode + gunMode = newMode; + switch (newMode) { + case GunMode_Run: + // begin run mode with all 4 points seen + lastSeen = 0x0F; + break; + case GunMode_CalHoriz: + break; + case GunMode_CalVert: + break; + case GunMode_CalCenter: + break; + case GunMode_Pause: + stateFlags |= StateFlag_SavePreferencesEn | StateFlag_PrintSelectedProfile; + break; + } + + SetLedColorFromMode(); +} + +// set new run mode and apply it to the selected profile +void SetRunMode(RunMode_e newMode) +{ + if (newMode >= RunMode_Count) { + return; + } + + // block Processing/test modes being applied to a profile + if (newMode <= RunMode_ProfileMax && profileData[selectedProfile].runMode != newMode) { + profileData[selectedProfile].runMode = newMode; + stateFlags |= StateFlag_SavePreferencesEn; + } + + if (runMode != newMode) { + runMode = newMode; + if (!(stateFlags & StateFlag_PrintSelectedProfile)) { + PrintRunMode(); + } + } +} + +void PrintResults() +{ + if (millis() - lastPrintMillis < 100) { + return; + } + + if (!Serial.dtr()) { + stateFlags |= StateFlagsDtrReset; + return; + } + + PrintPreferences(); + /* + Serial.print(finalX); + Serial.print(" ("); + Serial.print(MoveXAxis); + Serial.print("), "); + Serial.print(finalY); + Serial.print(" ("); + Serial.print(MoveYAxis); + Serial.print("), H "); + Serial.println(myFusion.H());*/ + + //Serial.print("conMove "); + //Serial.print(conMoveXAxis); + //Serial.println(conMoveYAxis); + + if (stateFlags & StateFlag_PrintSelectedProfile) { + stateFlags &= ~StateFlag_PrintSelectedProfile; + PrintSelectedProfile(); + PrintIrSensitivity(); + PrintRunMode(); + PrintCal(); + } + + lastPrintMillis = millis(); +} + +void PrintCalInterval() +{ + if (millis() - lastPrintMillis < 100) { + return; + } + PrintCal(); + lastPrintMillis = millis(); +} + +void PrintCal() +{ + Serial.print("Calibration: Center x,y: "); + Serial.print(xCenter); + Serial.print(","); + Serial.print(yCenter); + Serial.print(" Scale x,y: "); + Serial.print(xScale, 3); + Serial.print(","); + Serial.println(yScale, 3); +} + +void PrintRunMode() +{ + if (runMode < RunMode_Count) { + Serial.print("Mode: "); + Serial.println(RunModeLabels[runMode]); + } +} + +// helper in case this changes +float CalScalePrefToFloat(uint16_t scale) +{ + return (float)scale / 1000.0f; +} + +// helper in case this changes +uint16_t CalScaleFloatToPref(float scale) +{ + return (uint16_t)(scale * 1000.0f); +} + +void PrintPreferences() +{ + if (!(stateFlags & StateFlag_PrintPreferences) || !Serial.dtr()) { + return; + } + + stateFlags &= ~StateFlag_PrintPreferences; + + PrintNVPrefsError(); + + if (stateFlags & StateFlag_PrintPreferencesStorage) { + stateFlags &= ~StateFlag_PrintPreferencesStorage; + PrintNVStorage(); + } + + Serial.print("Default Profile: "); + Serial.println(profileDesc[FusionPreferences::preferences.profile].profileLabel); + + Serial.println("Profiles:"); + for (unsigned int i = 0; i < FusionPreferences::preferences.profileCount; ++i) { + // report if a profile has been cal'd + if (profileData[i].xCenter && profileData[i].yCenter) { + size_t len = strlen(profileDesc[i].buttonLabel) + 2; + Serial.print(profileDesc[i].buttonLabel); + Serial.print(": "); + if (profileDesc[i].profileLabel && profileDesc[i].profileLabel[0]) { + Serial.print(profileDesc[i].profileLabel); + len += strlen(profileDesc[i].profileLabel); + } + while (len < 26) { + Serial.print(' '); + ++len; + } + Serial.print("Center: "); + Serial.print(profileData[i].xCenter); + Serial.print(","); + Serial.print(profileData[i].yCenter); + Serial.print(" Scale: "); + Serial.print(CalScalePrefToFloat(profileData[i].xScale), 3); + Serial.print(","); + Serial.print(CalScalePrefToFloat(profileData[i].yScale), 3); + Serial.print(" IR: "); + Serial.print((unsigned int)profileData[i].irSensitivity); + Serial.print(" Mode: "); + Serial.println((unsigned int)profileData[i].runMode); + } + } +} + +void PrintNVStorage() +{ +#ifdef FUSION_FLASH_ENABLE + unsigned int required = FusionPreferences::Size(); +#ifndef PRINT_VERBOSE + if (required < flash.size()) { + return; + } +#endif + Serial.print("NV Storage capacity: "); + Serial.print(flash.size()); + Serial.print(", required size: "); + Serial.println(required); +#ifdef PRINT_VERBOSE + Serial.print("Profile struct size: "); + Serial.print((unsigned int)sizeof(FusionPreferences::ProfileData_t)); + Serial.print(", Profile data array size: "); + Serial.println((unsigned int)sizeof(profileData)); +#endif +#endif // FUSION_FLASH_ENABLE +} + +void PrintNVPrefsError() +{ + if (nvPrefsError != FusionPreferences::Error_Success) { + Serial.print(NVRAMlabel); + Serial.print(" error: "); +#ifdef FUSION_FLASH_ENABLE + Serial.println(FusionPreferences::ErrorCodeToString(nvPrefsError)); +#else + Serial.println(nvPrefsError); +#endif // FUSION_FLASH_ENABLE + } +} + +void LoadPreferences() +{ + if (!nvAvailable) { + return; + } + +#ifdef FUSION_FLASH_ENABLE + nvPrefsError = FusionPreferences::Load(flash); +#else + nvPrefsError = elitePreferences.Load(); +#endif // FUSION_FLASH_ENABLE + VerifyPreferences(); +} + +void VerifyPreferences() +{ + // center 0 is used as "no cal data" + for (unsigned int i = 0; i < ProfileCount; ++i) { + if (profileData[i].xCenter >= MouseMaxX || profileData[i].yCenter >= MouseMaxY || profileData[i].xScale == 0 || profileData[i].yScale == 0) { + profileData[i].xCenter = 0; + profileData[i].yCenter = 0; + } + + // if the scale values are large, assign 0 so the values will be ignored + if (profileData[i].xScale >= 30000) { + profileData[i].xScale = 0; + } + if (profileData[i].yScale >= 30000) { + profileData[i].yScale = 0; + } + + if (profileData[i].irSensitivity > DFRobotIRPositionEx::Sensitivity_Max) { + profileData[i].irSensitivity = DFRobotIRPositionEx::Sensitivity_Default; + } + + if (profileData[i].runMode >= RunMode_Count) { + profileData[i].runMode = RunMode_Normal; + } + } + + // if default profile is not valid, use current selected profile instead + if (FusionPreferences::preferences.profile >= ProfileCount) { + FusionPreferences::preferences.profile = (uint8_t)selectedProfile; + } +} + +// Apply initial preferences, intended to be called only in setup() after LoadPreferences() +// this will apply the preferences data as the initial values +void ApplyInitialPrefs() +{ + // if default profile is valid then use it + if (FusionPreferences::preferences.profile < ProfileCount) { + // note, just set the value here not call the function to do the set + selectedProfile = FusionPreferences::preferences.profile; + + // set the current IR camera sensitivity + if (profileData[selectedProfile].irSensitivity <= DFRobotIRPositionEx::Sensitivity_Max) { + irSensitivity = (DFRobotIRPositionEx::Sensitivity_e)profileData[selectedProfile].irSensitivity; + } + + // set the run mode + if (profileData[selectedProfile].runMode < RunMode_Count) { + runMode = (RunMode_e)profileData[selectedProfile].runMode; + } + } +} + +void SavePreferences() +{ + if (!nvAvailable || !(stateFlags & StateFlag_SavePreferencesEn)) { + return; + } + + // Only allow one write per pause state until something changes. + // Extra protection to ensure the same data can't write a bunch of times. + stateFlags &= ~StateFlag_SavePreferencesEn; + + // use selected profile as the default + FusionPreferences::preferences.profile = (uint8_t)selectedProfile; + +#ifdef FUSION_FLASH_ENABLE + nvPrefsError = FusionPreferences::Save(flash); +#else + nvPrefsError = FusionPreferences::Save(); +#endif // FUSION_FLASH_ENABLE + if (nvPrefsError == FusionPreferences::Error_Success) { + Serial.print("Settings saved to "); + Serial.println(NVRAMlabel); + } else { + Serial.println("Error saving Preferences."); + PrintNVPrefsError(); + } +} + +void SelectCalProfileFromBtnMask(uint32_t mask) +{ + // only check if buttons are set in the mask + if (!mask) { + return; + } + for (unsigned int i = 0; i < ProfileCount; ++i) { + if (profileDesc[i].buttonMask == mask) { + SelectCalProfile(i); + return; + } + } +} + +void CycleIrSensitivity() +{ + uint8_t sens = irSensitivity; + if (irSensitivity < DFRobotIRPositionEx::Sensitivity_Max) { + sens++; + } else { + sens = DFRobotIRPositionEx::Sensitivity_Min; + } + SetIrSensitivity(sens); +} + +void IncreaseIrSensitivity() +{ + uint8_t sens = irSensitivity; + if (irSensitivity < DFRobotIRPositionEx::Sensitivity_Max) { + sens++; + SetIrSensitivity(sens); + } +} + +void DecreaseIrSensitivity() +{ + uint8_t sens = irSensitivity; + if (irSensitivity > DFRobotIRPositionEx::Sensitivity_Min) { + sens--; + SetIrSensitivity(sens); + } +} + +// set a new IR camera sensitivity and apply to the selected profile +void SetIrSensitivity(uint8_t sensitivity) +{ + if (sensitivity > DFRobotIRPositionEx::Sensitivity_Max) { + return; + } + + if (profileData[selectedProfile].irSensitivity != sensitivity) { + profileData[selectedProfile].irSensitivity = sensitivity; + stateFlags |= StateFlag_SavePreferencesEn; + } + + if (irSensitivity != (DFRobotIRPositionEx::Sensitivity_e)sensitivity) { + irSensitivity = (DFRobotIRPositionEx::Sensitivity_e)sensitivity; + dfrIRPos.sensitivityLevel(irSensitivity); + if (!(stateFlags & StateFlag_PrintSelectedProfile)) { + PrintIrSensitivity(); + } + } +} + +void PrintIrSensitivity() +{ + Serial.print("IR Camera Sensitivity: "); + Serial.println((int)irSensitivity); +} + +void CancelCalibration() +{ + Serial.println("Calibration cancelled"); + // re-print the profile + stateFlags |= StateFlag_PrintSelectedProfile; + // re-apply the cal stored in the profile + RevertToCalProfile(selectedProfile); + // return to pause mode + SetMode(GunMode_Pause); +} + +void PrintSelectedProfile() +{ + Serial.print("Profile: "); + Serial.println(profileDesc[selectedProfile].profileLabel); +} + +// select a profile +bool SelectCalProfile(unsigned int profile) +{ + if (profile >= ProfileCount) { + return false; + } + + if (selectedProfile != profile) { + stateFlags |= StateFlag_PrintSelectedProfile; + selectedProfile = profile; + } + + bool valid = SelectCalPrefs(profile); + + // set IR sensitivity + if (profileData[profile].irSensitivity <= DFRobotIRPositionEx::Sensitivity_Max) { + SetIrSensitivity(profileData[profile].irSensitivity); + } + + // set run mode + if (profileData[profile].runMode < RunMode_Count) { + SetRunMode((RunMode_e)profileData[profile].runMode); + } + + SetLedColorFromMode(); + + // enable save to allow setting new default profile + stateFlags |= StateFlag_SavePreferencesEn; + return valid; +} + +bool SelectCalPrefs(unsigned int profile) +{ + if (profile >= ProfileCount) { + return false; + } + + // if center values are set, assume profile is populated + if (profileData[profile].xCenter && profileData[profile].yCenter) { + xCenter = profileData[profile].xCenter; + yCenter = profileData[profile].yCenter; + + // 0 scale will be ignored + if (profileData[profile].xScale) { + xScale = CalScalePrefToFloat(profileData[profile].xScale); + } + if (profileData[profile].yScale) { + yScale = CalScalePrefToFloat(profileData[profile].yScale); + } + return true; + } + return false; +} + +// revert back to useable settings, even if not cal'd +void RevertToCalProfile(unsigned int profile) +{ + // if the current profile isn't valid + if (!SelectCalProfile(profile)) { + // revert to settings from any valid profile + for (unsigned int i = 0; i < ProfileCount; ++i) { + if (SelectCalProfile(i)) { + break; + } + } + + // stay selected on the specified profile + SelectCalProfile(profile); + } +} + +// apply current cal to the selected profile +void ApplyCalToProfile() +{ + profileData[selectedProfile].xCenter = xCenter; + profileData[selectedProfile].yCenter = yCenter; + profileData[selectedProfile].xScale = CalScaleFloatToPref(xScale); + profileData[selectedProfile].yScale = CalScaleFloatToPref(yScale); + + stateFlags |= StateFlag_PrintSelectedProfile; +} + +void SetLedPackedColor(uint32_t color) +{ +#ifdef DOTSTAR_ENABLE + dotstar.setPixelColor(0, Adafruit_DotStar::gamma32(color & 0x00FFFFFF)); + dotstar.show(); +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_PIN + neopixel.setPixelColor(0, Adafruit_NeoPixel::gamma32(color & 0x00FFFFFF)); + neopixel.show(); +#endif // NEOPIXEL_PIN +} + +void LedOff() +{ +#ifdef DOTSTAR_ENABLE + dotstar.setPixelColor(0, 0); + dotstar.show(); +#endif // DOTSTAR_ENABLE +#ifdef NEOPIXEL_PIN + neopixel.setPixelColor(0, 0); + neopixel.show(); +#endif // NEOPIXEL_PIN +} + +void SetLedColorFromMode() +{ + switch (gunMode) { + case GunMode_CalHoriz: + case GunMode_CalVert: + case GunMode_CalCenter: + SetLedPackedColor(CalModeColor); + break; + case GunMode_Pause: + SetLedPackedColor(profileDesc[selectedProfile].color); + break; + case GunMode_Run: + if (lastSeen) { + LedOff(); + } else { + SetLedPackedColor(IRSeen0Color); + } + break; + default: + break; + } +} + +#ifdef DEBUG_SERIAL +void PrintDebugSerial() +{ + // only print every second + if (millis() - serialDbMs >= 1000 && Serial.dtr()) { +#ifdef EXTRA_POS_GLITCH_FILTER + Serial.print("bad final count "); + Serial.print(badFinalCount); + Serial.print(", bad move count "); + Serial.println(badMoveCount); +#endif // EXTRA_POS_GLITCH_FILTER + Serial.print("mode "); + Serial.print(gunMode); + Serial.print(", IR pos fps "); + Serial.print(irPosCount); + Serial.print(", loop/sec "); + Serial.print(frameCount); + + Serial.print(", Mouse X,Y "); + Serial.print(conMoveXAxis); + Serial.print(","); + Serial.println(conMoveYAxis); + + frameCount = 0; + irPosCount = 0; + serialDbMs = millis(); + } +} +#endif // DEBUG_SERIAL diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.cpp b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.cpp new file mode 100644 index 0000000..8fcce90 --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.cpp @@ -0,0 +1,139 @@ +/*! + * @file FusionPreferences.cpp + * @brief Fusion Prow Enhanced light gun preferences to save in non-volatile memory. + * + * @copyright Mike Lynch, 2021 + * @copyright GNU Lesser General Public License + * + * @author Mike Lynch + * @author Gonezo + * @version V1.03 + * @date 2022 + */ + +#include "FusionPreferences.h" + +#ifdef FUSION_FLASH_ENABLE +#include +#endif // FUSION_FLASH_ENABLE +#ifdef FUSION_EEPROM_ENABLE +#include +#endif // FUSION_EEPROM_ENABLE + +// 4 byte header ID +const FusionPreferences::HeaderId_t FusionPreferences::HeaderId = {'P', 'r', 'o', 'w'}; + +#if defined(FUSION_FLASH_ENABLE) + +// must match Errors_e order +const char* FusionPreferences::ErrorText[6] = { + "Success", + "No storage memory", + "Read error", + "No preferences saved", + "Write error", + "Erase failed" +}; + +const char* FusionPreferences::ErrorCodeToString(int error) +{ + if(error >= 0) { + // all positive values are success + error = 0; + } else { + error = -error; + } + if(error < sizeof(ErrorText) / sizeof(ErrorText[0])) { + return ErrorText[error]; + } + return ""; +} + +int FusionPreferences::Load(Adafruit_SPIFlashBase& flash) +{ + uint32_t u32; + uint32_t fr = flash.readBuffer(0, (uint8_t*)&u32, sizeof(u32)); + if(fr != 4) { + return Error_Read; + } + + if(u32 != HeaderId.u32) { + return Error_NoData; + } + + fr = flash.readBuffer(4, &preferences.profile, 1); + if(fr != 1) { + return Error_Read; + } + + const uint32_t length = sizeof(ProfileData_t) * preferences.profileCount; + fr = flash.readBuffer(5, (uint8_t*)preferences.pProfileData, length); + + return fr == length ? Error_Success : Error_Read; +} + +int FusionPreferences::Save(Adafruit_SPIFlashBase& flash) +{ + bool success = flash.eraseSector(0); + if(!success) { + return Error_Erase; + } + + uint32_t fw = flash.writeBuffer(0, (uint8_t*)&HeaderId.u32, sizeof(HeaderId.u32)); + if(fw != sizeof(HeaderId.u32)) { + return Error_Write; + } + + fw = flash.writeBuffer(4, &preferences.profile, 1); + if(fw != 1) { + return Error_Write; + } + + const uint32_t length = sizeof(ProfileData_t) * preferences.profileCount; + fw = flash.writeBuffer(5, (uint8_t*)preferences.pProfileData, length); + + return fw == length ? Error_Success : Error_Write; +} + +#elif defined(FUSION_EEPROM_ENABLE) + +int FusionPreferences::Load() +{ + uint32_t u32; + EEPROM.get(0, u32); + if(u32 != HeaderId.u32) { + return Error_NoData; + } + + preferences.profile = EEPROM.read(4); + uint8_t* p = ((uint8_t*)preferences.pProfileData); + for(unsigned int i = 0; i < sizeof(ProfileData_t) * preferences.profileCount; ++i) { + p[i] = EEPROM.read(5 + i); + } + return Error_Success; +} + +int FusionPreferences::Save() +{ + EEPROM.put(0, HeaderId.u32); + EEPROM.write(4, preferences.profile); + uint8_t* p = ((uint8_t*)preferences.pProfileData); + for(unsigned int i = 0; i < sizeof(ProfileData_t) * preferences.profileCount; ++i) { + EEPROM.write(5 + i, p[i]); + } + return Error_Success; +} + +#else + +int FusionPreferences::Load() +{ + return Error_NoStorage; +} + +int FusionPreferences::Save() +{ + return Error_NoStorage; +} + +#endif diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.h b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.h new file mode 100644 index 0000000..155e427 --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player2/FusionLightgun/FusionPreferences.h @@ -0,0 +1,104 @@ +/*! + * @file FusionPreferences.h + * + * @copyright Gonezo Fusion Lightguns + * @copyright GNU Lesser General Public License + * + * @author Mike Lynch + * @author Gonezo + * @version V2.00 + * @date 2023 + */ + +#ifndef _FUSIONPREFERENCES_H_ +#define _FUSIONPREFERENCES_H_ + +#include +#include + +#ifdef FUSION_FLASH_ENABLE +class Adafruit_SPIFlashBase; +#endif // FUSION_FLASH_ENABLE + +/// @brief Static instance of preferences to save in non-volatile memory +class FusionPreferences +{ +public: + /// @brief Error codes + enum Errors_e { + Error_Success = 0, + Error_NoStorage = -1, + Error_Read = -2, + Error_NoData = -3, + Error_Write = -4, + Error_Erase = -5 + }; + + /// @brief Header ID + typedef union HeaderId_u { + uint8_t bytes[4]; + uint32_t u32; + } __attribute__ ((packed)) HeaderId_t; + + /// @brief Profile data + typedef struct ProfileData_s { + uint16_t xScale; ///< X Scale * 1000 + uint16_t yScale; ///< Y Scale * 1000 + uint32_t xCenter : 12; + uint32_t yCenter : 12; + uint32_t irSensitivity : 3; + uint32_t runMode : 5; + uint32_t reserved; + uint32_t reserved2; + } __attribute__ ((packed)) ProfileData_t; + + /// @brief Preferences that can be stored in flash + typedef struct Preferences_s { + // pointer to ProfileData_t array + FusionPreferences::ProfileData_t* pProfileData; + + // number of ProfileData_t entries + uint8_t profileCount; + + // default profile + uint8_t profile; + } __attribute__ ((packed)) Preferences_t; + + // single instance of the preference data + static Preferences_t preferences; + + // header ID to ensure junk isn't loaded if preferences aren't saved + static const HeaderId_t HeaderId; + + /// @brief Required size for the preferences + static unsigned int Size() { return sizeof(ProfileData_t) * preferences.profileCount + sizeof(HeaderId_u) + sizeof(preferences.profile); } + +#ifdef FUSION_FLASH_ENABLE + + /// @brief Load preferences + /// @return An error code from Errors_e + static int Load(Adafruit_SPIFlashBase& flash); + + /// @brief Save preferences + /// @return An error code from Errors_e + static int Save(Adafruit_SPIFlashBase& flash); + + /// @brief Get a string for a given error code + static const char* ErrorCodeToString(int error); + +private: + // error text table + static const char* ErrorText[6]; + +#else + /// @brief Load preferences + /// @return An error code from Errors_e + static int Load(); + + /// @brief Save preferences + /// @return An error code from Errors_e + static int Save(); +#endif +}; + +#endif // _FUSIONPREFERENCES_H_ diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Player2/Pin-Button-Layout.txt b/DIY/Code It/Pro-Micro/Version-3.2/Player2/Pin-Button-Layout.txt new file mode 100644 index 0000000..0093608 --- /dev/null +++ b/DIY/Code It/Pro-Micro/Version-3.2/Player2/Pin-Button-Layout.txt @@ -0,0 +1,35 @@ +Pro Micro Pin & Button Layout + +Pin #-- Keyboard/Mouse Button-- In Code + +___TRIGGER, CALIBRATE & RELOAD____ +10 ---- Trigger -------- Mouse Left +16 ---- Reload -------- Mouse Right +4 ---- Bomb -------- Mouse Middle +14 ---- Mouse5 ----------- Calibrate + +___BUTTONS +6 --- Left Control ---- A Button +7 --- Left Shift ------ B Button +8 --- 6 --------------- Select +9 --- 2 --------------- Start + + +____DPAD BUTTONS____ +A0 -- Up Arrow ---------- Up +A1 -- Down Arrow -------- Down +A2 -- Left Arrow -------- Left +A3 -- Right Arrow ------- Right + +____OTHER NEEDED PINS____ + +2 ---- SDA on Camera +3 ---- SDL on Camera +VCC -- POWER for Camera +GND -- Ground Pins +15 --- Motor +5 ---- Solenoid + +Your Buttons & Camera Need To Be Grounded + + \ No newline at end of file diff --git a/DIY/Code It/Pro-Micro/Version-3.2/Version-3.2.zip b/DIY/Code It/Pro-Micro/Version-3.2/Version-3.2.zip new file mode 100644 index 0000000000000000000000000000000000000000..05415b1aba385bd7e7aa67ce8177d5ba212f1ccc GIT binary patch literal 31136 zcmeF&Qd3^P@Sl#t{12nVT%0WIY-KFW z%$?0#Z2xT%)IXQDV3@5z0tEu<`nxXqe?IAdtRZY?ZRg_XL~ov>YAC&d!d$wvKzX=W_E~_KvvRS}`5=w-*^?1mgrrFscm+y$l^+jS3#55(YK2SRev+ z#vmB0VLyEE3TC^RWGG|$Pr7MRu0)IZa1wo1CX|E-w*f0wB2jWyxTYT&cD{)zm*%BqH0xvK3jJZPGIyjiI06;Cx=?F`8Pci zG}msMZj33^fm9s#lL6?u7CvBq=$dfgD{+t4W%X$U<`CC*n!U1Ght8>3ClZoS&3l15 z-3%I7dx5U5wHafaC|YEYNowKQHXB3AQIlo%W8>bzNN0)I0foEV2$37k*wvD3jt?12 zq|FFNjP;PkHbrDwAW^-h8wMs`HYH^It;sP=h-L8-ToB!5r7|U+r6Q5PpXj4=Te!Xk zGFUl6E3&)Qss`XR;V| zb?T1u=`aPqHrcg8v=5`Y?oAm)XZPZzh0Jl3kR~^=JwZeTI_MqMBtNHKkq}2ZL#Tobw|dzj;gTSe1kk1{_wr`~86bH4veI7-1bv;#=~?L<=q%b+JrSboH!qB(Wb$PEV*>5yZ$>1d+Z?9rN@%t zc=vj^E+2H!YU|pv_Yv85_%`jj1Ejn^joZ^t$%}-PhR+!yPxba)Ebvvchk^Jbt#>+K%nQpafoAqE`9*%$UCk1vDP?hAaNm^I1M_?X`v z{J7vv+7MU?X{S#fW@3lB@|H()6nau5>s@eCE{B@fgxXv`5XY~s}mRmoFeE0ZnE`P>@ z*mgD1M`mOAzSy@tNW|4c5zETCOP=3N79&=Jd8|LM7{y0tAl0&uq>JKQp2zsfw-1U3 zoQTk#EYQl#L23*J@@yYe@hVy|W~y%jIRfkAb@nSHVQUv8W)Hhnx@OpQP}QMe-S#!B zJglRu$YayU6T}${2p8kSttKac!#m&7x-%S={W){0@TC$4NT*BySO{hFf~L-(n1Mo zTkqBcAx;v9Y#D!I_@#}ag3aawONsvc`e9w)wj+hy3P`mson)=q3R1ZV$e22}P1~~o zQ1M|^81XJpmxYbfS1!#gvbwqoN;OXE%v__y8=NdbZIV$=&fm+iAM*hFjRgMqBbu_s?-X_iYHS?cv9f_EbZ(mWh|i?=G!M7%&6vvK<0Na` zF(}$Oig@?L`&qY0&q><#%L9w->v)SWKD@4>307xOepCDlXLG%(A?W)(--DOa>Sd>{ z0e-*_m;V>k3GN8dv7?spZCf4Ww(FltuN#4F;Cfl;yJr`mR<9Zvb4Zj8t4jeZpz2Q0 zDDt4-*9-2u7iNbd%_1OnJxya7gxZgrGiRF`9xCj_v`_C(v9+ zyK|%vsj~Jwq|3y}D0QOfI{=5f8p2o_aHWdLR_WxA=5G&|-BGGQjKGSy{+Ii7Owk<_ zj72o4^ae>r@2UQCeD;nWu3%~^^Zl8$_G<87Lj|`2i6osQm8;XSB$gI|vUWl`?}k2* zihQpI^%5RUXAn;t+rS0eqe5vKCvcJB=%GE=wIwL$9$;#u>6Ow8dTP>JDm~GI#L3`a z2^4CHUH&T>n(2&lLTy?g9PqOvjT5~btTDGv4Q>|Bgg?j+I1EYzxnu2tXYv=D60O69 ze-@~^j!zZcH}J)1fk9@{28ee)-m4E}8w-G%6N6k&v*2C{lKF2HQe%@QY^J}$m*;Pw z1hSqTSwCjkgV4$Oe}A(^r>@%wZ664pUTW%X8nBD@LK<(K2YRpw3*TlDO<2Q5J~>BJ zb!=V+PqCI|plXC}q0s!2U2Qa4N%ll@1=%u*}~t7BWcC;_%UVD-hC zJ{+|sr0CqsDq?&ICAbMNGx?j}*!CDGJ_H~-=^hD)VE1Mo!C}?h8@J}i-e}(F^L9G- zV%Yi1w7^G>fFrQ3c;I?;v8MYU-Us4ASrdc19Mk%X9ScIjZARH$gSrnOjuBqE{t88S zhnSNHgeNU&j8w9`t>WxQ90bi%UC2SA0t$*CP@f-aI+U^13uzSnnle4`?w zwFAW9&u@`wkWyH|W(iHwmuaDR>H+S`$%DHB=!)xe<(;4|ZkB*$3clL3ZwZyaZ}18H z&0`>A)L^3B3^#cJ*6<29LK#fuUI=$oydvcFjZAu+NPwJwqm=*?GYY>PRHkM^sI0a* zO#9t-bkE3-2nSOG4n~rFZzf{GJA8?094tUHxxW{u7V|B+&JRxehPg3T^3(gYJvbu4 zC=!Ecz1C~j&qgl?F*^Sw8*KVD{3Fn2|E>8!dFx1D1SB5@e>uiH+6Cw@7^NW{nv+4@ z&})<@EP=Ovt46K;>M2c1t*Uw9;d9$lB;EB95`_cE|NrXOhq@sq)B6Sm^`Ql?)yz>C%ag)UUeiA3FWnoHASE~UNF1)z){sH9cbrO4-) zE?M|(@u_eY3Q&}a6rrjqlJCa#3zXc7;STJtcsiP z1-3DIkhM2(rK7Cvb;M988Ca#=UWvm%-OWk>NY|>o>HiTvS5lhvtPvm<6DO=2)$?#t zS8I9lt@>~P%j|ND9gJ(T#}I(b$z-j!T^_KwqjMi`=1~cXhcY*%8$RdGNY6}MT$k9* zdH)Wl^;+Zaeiqy$c}1!;=KcCDYnEX_uuCG`jouResX)GTf z@*C_(gtWRFxjf$-+ptf9tQk%oA*}x?pCF)e6&EP#kWDP*$yjEG;|lhrVBUT#oaGJ1 zK2UUzX_$$TTF+P%+zJNt#o>`JoJw_|GZUFRA$JET~%dbEmNumHp$suy2@mAas>D;3>N90FI8+(f(GIg z+MXrasW)lWf42{GZbO$7yMUM+XAmkTAvg%0J{|{odYl1s$)nqc2iidRSL5H0De`gk z9Rr3SF8=E<@67(E4UPl!hj0s6z2^FY#-DsfHTU8|!zd)XHQ5mO^&p0G89g4~9ajdh z=80yM7Cd2c6oW3+TPS0x+Co0A0N}5fgP$W^+JXlhVi<_gGH$Qk@u>eF_(?4?r#MNH~`ftTcM@P(%KSD8O9 z>0wrnt`>MuYRgW33Q_=}y3m0y5L9xinG3B!)%~oua;NKN*mJ*j^nH!G$_d|w4U`&8f@LkL@opb>S8%_k(YGi zm?h%t>Kf-s#fIG^i~1w{vxfS}yJ$B9Uft%coyox8ZcYW1ryE#SAuAve$)?W8#H%|V zq(gi8N&*b2K#7wOpo0tiF~WBiG|0vQQNQ?=ZNe4ha(Qov;?ls(+Ge!X%RqAXTE+9D zQveK|)A3JMEp)OXp5(Ln;tZ8c7i<*@;=J1* z1bA-H$sBoJ^C@87iS?5TZ3YgANm$D;$QM|kYh+wpF4(|HkZpT~50?yi@YH=@PfjlN zUzZGX{AmbjCZzt*e(E@E%SA(gK4^yo?gAIj135vSaOSK|355C1M^l5>D>=8!wXSF7L;mvQX@r7q9g)#t*%9d(M%>w9(i8Rfe zv`XGa9>p1L!8!gb_PO!?u=f^01@@Qc{_RBwa%Z!%1>Q-K*cbQS`@o21qW9UkBm-vT z19X9WB&}&0fsZUkFoMrgDl35@W5izrUnvXi7J(1f;7NtNWT;ap+ovsr`F~*RAM;UO zi}D$f!%bZYbVPimJX$gvRuCc)%QSqM-GR^|*V<(#?99%QlEHDZ>HO&OGfPx_wuv-o zPpD6|RIs9j_Pw;2j*R>IjlsN;V|;!{maFD5*hEmGQ#Y0#BB$(f?6-)VzbZx0HeYa$#;HW{~Soh6@*H*7q=y& zAHBDu3>jjLv#*MFW3th+7@xFm5sKfM70Abucw;B>A;`VkvlOBRX(S3~S>PYO#r6j> zgbJ_klQJAWWeN149;76v7bO1qsq4+kprN>BsibX8qf#`K#d4EBPA{t6tm8&cIpUyN zvWV(}Jk_IR&J4yyym4ZwpKZkb5FKwcEY5mO#DtIt^>!B}SLEDbxy5x<$x{md_vzHP zmMKIw@pb%D=w)Pb1os`af`*+dY^`=_wez=0<&(O>jIZQR8GVI_y?pwG_uW$lbQy8% zk+}O?{aiTn$v4w5x4-w03YF32&`FCx^{JApmbD0iPXy!ccBg;^GgK>xE?tnh1^e^$ zIVc7s-g_X@ovtXDr4mpvf&NCL7?_nRt5bQTudN`z3*InNF11a5lQQQ`)kGUXa%hG6 zT@t#ZplYginD*-)e9FRV8c>S$s6X5xj5UVKEJNK#&{WmZQk8UY1q<%;;Fg%-jhtdF zLS1!3R23iE;rm_Qla$04HpgXK0CEYv)PnuDL^fE0?yTxt#;=?`JVD(P*s_ZznpakL#F5$-v$ z7H1Rc{R`?Hn4+uFDbNXax(Y||6-Pi%W7QMIZz>)cV z5Uo?$a=cM<4nO+0zn>Xn!%0GCd`R>q*k?E9ABI-n&sb*L(68P6ZY=l7&A`Al)_3o{ zx;A;B42#FBDDRa73q}2C0u(`4+-Nd~wCBuQPBf#XilXAf(<6e>5m@#+KI z_V0rDgj&NnqERKtqec{#4**J=@*T$SpNNuEqC_d1!D=dQYK1nCM%Uz?j2P@;)vh)u zwzQ*Ip2oI4zm7I>Y8f^{;f;U0&Y)0i!FS6~R&f}OFA^{3QshCMR9XT;Jtd)~@B8CnO? zZR2sp9M^OG(?0~M+vV~s>V7`->Gi|3@xEiExNL?|Vyx*sUfGu^s+tsSTz^g$`Cg{X zxM^XkpZf5cIB#+L_yPTC`Pg0pHb0Ap{S$x@%+I}z2wb`BRhDlkTu=rSQwv!@&Z@T^?70PL(kAum?c2uS z6xb&y!E22GD%Fn>&%7uUa%`-CS98zFyEsYS+K0*yP5x9h#3|AC7mx)cC!|ATBMl)N zzxCmG4!#S&dwlJa_XhZIeR20_$Ej?A{W|!m_#;{(0uLb{4?@G->3l*qBoqbPD|)Mr zm_{&c{QQDZF6GfaOz4Y8-M0Aei0f=Tq1$Rzald3B-)~|Kcs$!u{B-m@sQm^4w64pJ4>f8sRL*AWeN z3T@f3{PbE6G`c&*-a;H3=l!LeD;Doz!Wf#sONK;Uge(g7;1CGl2m`H9$gce?U^&x= zfF2nUGaw!1b%vIzBEn-jDnuOY>LAldi%ZLfQ?}DFkmv z4t7WHMVGmU&Z2br+4*qp;z(zp7j=SfFBVl^HGo%M-l}GYSr+v6arS2ICjLkS(!d40 z!~jMRS)<>_E!%MK%iS3_-tS6AQf(m(nvT`;>Nb)hHZM@tv&s7M!HJBF+QzKoJbJoJ zUp-@%HZ#D{Kc8K>UO1GBb908SKR-Igz8Tvl+)c4ZjoUBhFtg#YYLdtqc+KbU!HHmd ze`*ic6tHMJ5Q^DQM3Tp|Hp~R0FfKO0!5RCcOGXw;z3rnNFfG^}>?FI%-1Ts=vvZTn zB?BZg?g)HA$9bzrDBV$mldnQ=kNxrN-V22(8i$<2qjsuKf}}zk&gWl{J20l5BC;ae0@o)lV+NU{k(~8iDMf??T z35LjkWt|kjH>f;bcLE6=(L>7vqtUtPrV}afDB^KoCUvNs8K2xfPaipO2ZDKSgE9i8 zt`J&mCbc>bZaDl{OUHUTwVgEMX286#?E89n?;Fvfhcz|4se|1re7#xdjeICIE`5d# zUXdl|y~N|MnDg!5y(Jf>GvRR>Dk_NkY065x6OjTegPoJEMBxs-9bq9yK@GnXGDdq1 zHl}Q{e4*u&m3pb|V1n}96yvvs&DBKMKrhP6D1#cj4;ce-bWP5D)G={f+jk9xcv?~! z7^eH{SF}vIN~JAbu5oKc8=LFp!BkDYR^wEaYmJ=zph9Rl8hC~UFk{&EjtvY!67Cx> z81G3}Cj4HcfHg$WNaSm9U_ar9Ge6n(AEZH;YvH43BzgA?uRrZ^(}(8HI9CLpdPa;n z)A`a#{``a`b%oVhhvjlFg1W?lWGf&-F+QZWLB-T2tyM`YU{UkK}XX zA~lymuHb$_vFRRUXpqu=4{i>7aXRYi&HIajL2YkN`fMlJMuFYW8_sr6j-O-ere%qp_4Xsq_!;=>B#sgW|ral zyvOV=XZ#|{?phSEo)>6wuRxm`@+lG$p}8Z$$dpRXP?9fvu>~I_^iNX-DKaWZAyx>1A0OD;pwU@`3!0KNO@;MK zrY`4m9PArFME2Sn>0Htq;T*3N12hhIU~{WWxJDka)K@ANUpn)XOv5}XA!cs7PNpcZ z=qF3avwj$dhKsvl8sP!y4g_B~Apz@ip}j#7WrrPXjIwfBTG#1um@9?u*Xvb~ovm|; zr_lGnDaW8UuTCfM1x`tGQI_);zM@xcX$=84#Lx5W;v+JV;lduLq|jYX&tA z!WNi!d@*lM{|0rS`%3e?dJ8-~Mf3E?0$q&MNLCvDW#0*Vo(0g~c>SK&%u*d y#q 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'zEit0`%+<9!qex4g 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 KN!$JQ?Y{sRV6!v; literal 0 HcmV?d00001