From e83a62b469dce3ff5b778df1fb7787340d1043f5 Mon Sep 17 00:00:00 2001 From: Mike Parks Date: Mon, 8 May 2023 09:59:14 -0700 Subject: [PATCH] Reintroduced Wii Extension Controller support --- CMakeLists.txt | 2 + .../images/gpc-add-ons-wii-extensions.png | Bin 0 -> 62911 bytes docs/web-configurator.md | 31 + headers/addons/wiiext.h | 85 +++ headers/storagemanager.h | 5 + lib/CMakeLists.txt | 3 +- lib/WiiExtension/CMakeLists.txt | 6 + lib/WiiExtension/README.md | 6 + lib/WiiExtension/WiiExtension.cpp | 722 ++++++++++++++++++ lib/WiiExtension/WiiExtension.h | 187 +++++ src/addons/wiiext.cpp | 146 ++++ src/configs/webconfig.cpp | 10 + src/gp2040.cpp | 2 + src/storagemanager.cpp | 6 + 14 files changed, 1210 insertions(+), 1 deletion(-) create mode 100644 docs/assets/images/gpc-add-ons-wii-extensions.png create mode 100644 headers/addons/wiiext.h create mode 100644 lib/WiiExtension/CMakeLists.txt create mode 100644 lib/WiiExtension/README.md create mode 100644 lib/WiiExtension/WiiExtension.cpp create mode 100644 lib/WiiExtension/WiiExtension.h create mode 100644 src/addons/wiiext.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e47874529..5cf81d7fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,7 @@ src/addons/ps4mode.cpp src/addons/reverse.cpp src/addons/turbo.cpp src/addons/slider_socd.cpp +src/addons/wiiext.cpp src/gamepad/GamepadDebouncer.cpp src/gamepad/GamepadDescriptors.cpp ) @@ -147,6 +148,7 @@ OneBitDisplay ArduinoJson rndis hardware_adc +WiiExtension pico_mbedtls TinyUSB_Gamepad ) diff --git a/docs/assets/images/gpc-add-ons-wii-extensions.png b/docs/assets/images/gpc-add-ons-wii-extensions.png new file mode 100644 index 0000000000000000000000000000000000000000..c9f93567d604a3089ffe7c7cf94bc65c0be43c8b GIT binary patch literal 62911 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%0DP7^jR8hmF2>{Hox@dd*u9~%tXqWR;U{_mE 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 literal 0 HcmV?d00001 diff --git a/docs/web-configurator.md b/docs/web-configurator.md index 25f2a6b16..4ac507151 100644 --- a/docs/web-configurator.md +++ b/docs/web-configurator.md @@ -222,6 +222,37 @@ Enabling this add-on will allow you to use GP2040-CE on a PS4 with an 8 minute t * `Serial Number (16 Bytes in Hex Ascii)` - Choose your serial number file. * `Signature (256 Bytes in Binary)` - Choose your signature file. +### Wii Extensions + +![GP2040 Configurator - Wii Extensions](assets/images/gpc-add-ons-wii-extensions.png) + +* `I2C SDA Pin` - The GPIO pin used for Wii Extension SDA. +* `I2C SCL Pin` - The GPIO pin used for Wii Extension SCL. +* `I2C Block` - The block of I2C to use (i2c0 or i2c1). +* `I2C Speed` - Sets the speed of I2C communication. Common values are `100000` for standard, or `400000` for fast. + +Supported Extension Controllers and their mapping is as follows: + +| GP2040-CE | Nunchuck | Classic | Guitar Hero Guitar | +|-----------|----------|--------------|--------------------| +| B1 | C | B | Green | +| B2 | Z | A | Red | +| B3 | | Y | Blue | +| B4 | | X | Yellow | +| L1 | | L | | +| L2 | | ZL | | +| R1 | | R | | +| R2 | | ZR | | +| S1 | | Select | | +| S2 | | Start | | +| A1 | | Home | | +| D-Pad | | D-Pad | Strum Up/Down | +| Analog | Left | Left & Right | Left | + +Classic Controller support includes Classic, Classic Pro, and NES/SNES Mini Controllers. + +Original Classic Controller L & R triggers are analog sensitive, where Pro triggers are not. + ## Data Backup and Restoration ![GP2040-CE Configurator - Add-Ons Backup and Restore](assets/images/gpc-backup-and-restore.png) diff --git a/headers/addons/wiiext.h b/headers/addons/wiiext.h new file mode 100644 index 000000000..5fac643ac --- /dev/null +++ b/headers/addons/wiiext.h @@ -0,0 +1,85 @@ +#ifndef _WIIExtensionAddon_H +#define _WIIExtensionAddon_H + +#include +#include +#include +#include "BoardConfig.h" +#include "gpaddon.h" +#include "gamepad.h" +#include "storagemanager.h" +#include "WiiExtension.h" + +// WiiExtension Module Name +#define WiiExtensionName "WiiExtension" + +#ifndef WII_EXTENSION_ENABLED +#define WII_EXTENSION_ENABLED 1 +#endif + +#ifndef WII_EXTENSION_I2C_ADDR +#define WII_EXTENSION_I2C_ADDR 0x52 +#endif + +#ifndef WII_EXTENSION_I2C_SDA_PIN +#define WII_EXTENSION_I2C_SDA_PIN 16 +#endif + +#ifndef WII_EXTENSION_I2C_SCL_PIN +#define WII_EXTENSION_I2C_SCL_PIN 17 +#endif + +#ifndef WII_EXTENSION_I2C_BLOCK +#define WII_EXTENSION_I2C_BLOCK i2c0 +#endif + +#ifndef WII_EXTENSION_I2C_SPEED +#define WII_EXTENSION_I2C_SPEED 400000 +#endif + +class WiiExtensionInput : public GPAddon { +public: + virtual bool available(); + virtual void setup(); // WiiExtension Setup + virtual void process(); // WiiExtension Process + virtual void preprocess() {} + virtual std::string name() { return WiiExtensionName; } +private: + WiiExtension * wii; + uint32_t uIntervalMS; + uint32_t nextTimer; + + bool buttonC = false; + bool buttonZ = false; + + bool buttonA = false; + bool buttonB = false; + bool buttonX = false; + bool buttonY = false; + bool buttonL = false; + bool buttonZL = false; + bool buttonR = false; + bool buttonZR = false; + + bool buttonSelect = false; + bool buttonStart = false; + bool buttonHome = false; + + bool dpadUp = false; + bool dpadDown = false; + bool dpadLeft = false; + bool dpadRight = false; + + uint16_t triggerLeft = 0; + uint16_t triggerRight = 0; + uint16_t whammyBar = 0; + + uint16_t leftX = 0; + uint16_t leftY = 0; + uint16_t rightX = 0; + uint16_t rightY = 0; + + uint16_t map(uint16_t x, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max); +}; + +#endif // _WIIExtensionAddon_H \ No newline at end of file diff --git a/headers/storagemanager.h b/headers/storagemanager.h index 61ed89374..d60df0258 100644 --- a/headers/storagemanager.h +++ b/headers/storagemanager.h @@ -140,6 +140,10 @@ struct AddonOptions { SOCDMode sliderSOCDModeOne; SOCDMode sliderSOCDModeTwo; SOCDMode sliderSOCDModeDefault; + uint8_t wiiExtensionSDAPin; + uint8_t wiiExtensionSCLPin; + int wiiExtensionBlock; + uint32_t wiiExtensionSpeed; uint8_t AnalogInputEnabled; uint8_t BoardLedAddonEnabled; uint8_t BootselButtonAddonEnabled; @@ -156,6 +160,7 @@ struct AddonOptions { uint8_t ReverseInputEnabled; uint8_t TurboInputEnabled; uint8_t SliderSOCDInputEnabled; + uint8_t WiiExtensionAddonEnabled; uint32_t checksum; }; diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 467925e89..0eefad157 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -9,4 +9,5 @@ add_subdirectory(NeoPico) add_subdirectory(OneBitDisplay) add_subdirectory(PlayerLEDs) add_subdirectory(rndis) -add_subdirectory(TinyUSB_Gamepad) \ No newline at end of file +add_subdirectory(TinyUSB_Gamepad) +add_subdirectory(WiiExtension) \ No newline at end of file diff --git a/lib/WiiExtension/CMakeLists.txt b/lib/WiiExtension/CMakeLists.txt new file mode 100644 index 000000000..1048267f6 --- /dev/null +++ b/lib/WiiExtension/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(WiiExtension WiiExtension.cpp) +target_link_libraries(WiiExtension PUBLIC BitBang_I2C) +target_include_directories(WiiExtension INTERFACE .) +target_include_directories(WiiExtension PUBLIC +BitBang_I2C +) diff --git a/lib/WiiExtension/README.md b/lib/WiiExtension/README.md new file mode 100644 index 000000000..1321909f7 --- /dev/null +++ b/lib/WiiExtension/README.md @@ -0,0 +1,6 @@ +# WiiExtension +# Ported to BitBangI2C +# +# Written by: +# Mike Parks +# \ No newline at end of file diff --git a/lib/WiiExtension/WiiExtension.cpp b/lib/WiiExtension/WiiExtension.cpp new file mode 100644 index 000000000..16abe32de --- /dev/null +++ b/lib/WiiExtension/WiiExtension.cpp @@ -0,0 +1,722 @@ +#include "WiiExtension.h" + +#include +#include + +WiiExtension::WiiExtension(int sda, int scl, i2c_inst_t *i2cCtl, int32_t speed, uint8_t addr) { + iSDA = sda; + iSCL = scl; + picoI2C = i2cCtl; + bWire = bWire; + iSpeed = speed; + address = addr; +} + +void WiiExtension::begin() { + doI2CInit(); +#if WII_EXTENSION_DEBUG==true + printf("WiiExtension::begin\n"); +#endif + isReady = false; + reset(); +} + +void WiiExtension::start(){ + uint8_t idRead[32]; + uint8_t regWrite[16]; + int8_t result; + +#if WII_EXTENSION_DEBUG==true + printf("WiiExtension::start\n"); + printf("WiiExtension::start isReady? %1d\n", isReady); +#endif + + if (!isReady) return; + + regWrite[0] = 0xFA; + result = doI2CWrite(®Write[0], 1); + + extensionType = WII_EXTENSION_NONE; + + // continue if the write was successful + if (result > -1) { + doI2CRead(idRead, 6); + + if (idRead[5] == 0x00) { + extensionType = WII_EXTENSION_NUNCHUCK; + } else if (idRead[5] == 0x01) { + extensionType = WII_EXTENSION_CLASSIC; + if (idRead[0] == 0x01) { + extensionType = WII_EXTENSION_CLASSIC_PRO; + } + } else if (idRead[5] == 0x03) { + extensionType = WII_EXTENSION_GUITAR; + if (idRead[0] == 0x01) { + extensionType = WII_EXTENSION_DRUMS; + } + } else if (idRead[5] == 0x11) { + extensionType = WII_EXTENSION_TAIKO; + } + + // in certain situations (eg. Nunchuck), setting the data type in reset() does not affect what this value will be + dataType = idRead[4]; + if (dataType == WII_DATA_TYPE_0) dataType = WII_DATA_TYPE_1; + +#if WII_EXTENSION_DEBUG==true + printf("Extension ID: %02x%02x %02x%02x %02x%02x\n", idRead[0], idRead[1], idRead[2], idRead[3], idRead[4], idRead[5]); + printf("Data Format: %02x\n",idRead[4]); +#endif + + if (extensionType != WII_EXTENSION_NONE) { +#if WII_EXTENSION_DEBUG==true + //printf("Extension Type: %d\n", extensionType); +#endif + +#if WII_EXTENSION_DEBUG==true + printf("Calibration Data\n"); +#endif + if (extensionType == WII_EXTENSION_NUNCHUCK) { + _analogPrecision1From = WII_ANALOG_PRECISION_2; + _analogPrecision1To = WII_ANALOG_PRECISION_3; + +#if WII_EXTENSION_CALIBRATION==true + // read calibration + regWrite[0] = 0x20; + doI2CWrite(regWrite, 1); + + doI2CRead(idRead, 16); + + _maxX1 = idRead[8]; + _minX1 = idRead[9]; + _cenX1 = idRead[10]; + + _maxY1 = idRead[11]; + _minY1 = idRead[12]; + _cenY1 = idRead[13]; + + _accelX0G = ((idRead[0] << 2) | ((idRead[3] >> 2) & 0x03)); + _accelY0G = ((idRead[1] << 2) | ((idRead[3] >> 4) & 0x03)); + _accelZ0G = ((idRead[2] << 2) | ((idRead[3] >> 6) & 0x03)); + + _accelX1G = ((idRead[4] << 2) | ((idRead[7] >> 2) & 0x03)); + _accelY1G = ((idRead[5] << 2) | ((idRead[7] >> 4) & 0x03)); + _accelZ1G = ((idRead[6] << 2) | ((idRead[7] >> 6) & 0x03)); + +#if WII_EXTENSION_DEBUG==true + //printf("Calibration:\n"); + //printf("X0G: %d\n", _accelX0G); + //printf("Y0G: %d\n", _accelY0G); + //printf("Z0G: %d\n", _accelZ0G); + //printf("X1G: %d\n", _accelX1G); + //printf("Y1G: %d\n", _accelY1G); + //printf("YZG: %d\n", _accelZ1G); + //printf("X Min: %d\n", _minX); + //printf("X Max: %d\n", _maxX); + //printf("X Center: %d\n", _cenX); + //printf("Y Min: %d\n", _minY); + //printf("Y Max: %d\n", _maxY); + //printf("Y Center: %d\n", _cenY); +#endif +#endif + } else { + if (dataType == WII_DATA_TYPE_1) { + _analogPrecision1From = WII_ANALOG_PRECISION_1; + _analogPrecision1To = WII_ANALOG_PRECISION_3; + _analogPrecision2From = WII_ANALOG_PRECISION_0; + _analogPrecision2To = WII_ANALOG_PRECISION_3; + + _triggerPrecision1From = WII_ANALOG_PRECISION_0; + _triggerPrecision1To = WII_ANALOG_PRECISION_2; + _triggerPrecision2From = WII_ANALOG_PRECISION_0; + _triggerPrecision2To = WII_ANALOG_PRECISION_2; + } else if (dataType == WII_DATA_TYPE_2) { + _analogPrecision1From = WII_ANALOG_PRECISION_3; + _analogPrecision1To = WII_ANALOG_PRECISION_3; + _analogPrecision2From = WII_ANALOG_PRECISION_3; + _analogPrecision2To = WII_ANALOG_PRECISION_3; + + _triggerPrecision1From = WII_ANALOG_PRECISION_2; + _triggerPrecision1To = WII_ANALOG_PRECISION_2; + _triggerPrecision2From = WII_ANALOG_PRECISION_2; + _triggerPrecision2To = WII_ANALOG_PRECISION_2; + } else if (dataType == WII_DATA_TYPE_3) { + _analogPrecision1From = WII_ANALOG_PRECISION_2; + _analogPrecision1To = WII_ANALOG_PRECISION_3; + _analogPrecision2From = WII_ANALOG_PRECISION_2; + _analogPrecision2To = WII_ANALOG_PRECISION_3; + + _triggerPrecision1From = WII_ANALOG_PRECISION_2; + _triggerPrecision1To = WII_ANALOG_PRECISION_2; + _triggerPrecision2From = WII_ANALOG_PRECISION_2; + _triggerPrecision2To = WII_ANALOG_PRECISION_2; + } + +#if WII_EXTENSION_CALIBRATION==true + regWrite[0] = 0x20; + doI2CWrite(regWrite, 1); + + doI2CRead(idRead, 16); + + if (dataType == WII_DATA_TYPE_1) { + _calibrationPrecision1From = WII_ANALOG_PRECISION_2; + _calibrationPrecision1To = WII_ANALOG_PRECISION_1; + _calibrationPrecision2From = WII_ANALOG_PRECISION_2; + _calibrationPrecision2To = WII_ANALOG_PRECISION_0; + } else if (dataType == WII_DATA_TYPE_2) { + _calibrationPrecision1From = WII_ANALOG_PRECISION_2; + _calibrationPrecision1To = WII_ANALOG_PRECISION_3; + _calibrationPrecision2From = WII_ANALOG_PRECISION_2; + _calibrationPrecision2To = WII_ANALOG_PRECISION_3; + } else if (dataType == WII_DATA_TYPE_3) { + _calibrationPrecision1From = WII_ANALOG_PRECISION_2; + _calibrationPrecision1To = WII_ANALOG_PRECISION_2; + _calibrationPrecision2From = WII_ANALOG_PRECISION_2; + _calibrationPrecision2To = WII_ANALOG_PRECISION_2; + } + + _maxX1 = map(idRead[0],0,(_calibrationPrecision1From-1),0,(_calibrationPrecision1To-1)); + _minX1 = map(idRead[1],0,(_calibrationPrecision1From-1),0,(_calibrationPrecision1To-1)); + _cenX1 = map(idRead[2],0,(_calibrationPrecision1From-1),0,(_calibrationPrecision1To-1)); + + _maxY1 = map(idRead[3],0,(_calibrationPrecision1From-1),0,(_calibrationPrecision1To-1)); + _minY1 = map(idRead[4],0,(_calibrationPrecision1From-1),0,(_calibrationPrecision1To-1)); + _cenY1 = map(idRead[5],0,(_calibrationPrecision1From-1),0,(_calibrationPrecision1To-1)); + + _maxX2 = map(idRead[6],0,(_calibrationPrecision2From-1),0,(_calibrationPrecision2To-1)); + _minX2 = map(idRead[7],0,(_calibrationPrecision2From-1),0,(_calibrationPrecision2To-1)); + _cenX2 = map(idRead[8],0,(_calibrationPrecision2From-1),0,(_calibrationPrecision2To-1)); + + _maxY2 = map(idRead[9],0,(_calibrationPrecision2From-1),0,(_calibrationPrecision2To-1)); + _minY2 = map(idRead[10],0,(_calibrationPrecision2From-1),0,(_calibrationPrecision2To-1)); + _cenY2 = map(idRead[11],0,(_calibrationPrecision2From-1),0,(_calibrationPrecision2To-1)); + +#if WII_EXTENSION_DEBUG==true + printf("X1 Min: %d\n", _minX1); + printf("X1 Max: %d\n", _maxX1); + printf("X1 Center: %d\n", _cenX1); + printf("Y1 Min: %d\n", _minY1); + printf("Y1 Max: %d\n", _maxY1); + printf("Y1 Center: %d\n", _cenY1); + printf("X2 Min: %d\n", _minX2); + printf("X2 Max: %d\n", _maxX2); + printf("X2 Center: %d\n", _cenX2); + printf("Y2 Min: %d\n", _minY2); + printf("Y2 Max: %d\n", _maxY2); + printf("Y2 Center: %d\n", _cenY2); +#endif +#endif + } + + // reset to default input values in the event of a removal/hotswap + joy1X = 0; + joy1Y = 0; + joy2X = 0; + joy2Y = 0; + accelX = 0; + accelY = 0; + accelZ = 0; + + buttonZ = 0; + buttonC = 0; + buttonZR = 0; + buttonZL = 0; + buttonA = 0; + buttonB = 0; + buttonX = 0; + buttonY = 0; + buttonPlus = 0; + buttonHome = 0; + buttonMinus = 0; + buttonLT = 0; + buttonRT = 0; + + directionUp = 0; + directionDown = 0; + directionLeft = 0; + directionRight = 0; + + triggerLeft = 0; + triggerRight = 0; + + fretGreen = 0; + fretRed = 0; + fretYellow = 0; + fretBlue = 0; + fretOrange = 0; + pedalButton = 0; + + rimLeft = 0; + rimRight = 0; + drumLeft = 0; + drumRight = 0; + + whammyBar = 0; + } else { +#if WII_EXTENSION_DEBUG==true + printf("Unknown Extension: %02x%02x %02x%02x %02x%02x\n", idRead[0], idRead[1], idRead[2], idRead[3], idRead[4], idRead[5]); +#endif + } + + regWrite[0] = 0x00; + result = doI2CWrite(regWrite, 1); + } +} + +void WiiExtension::reset(){ + uint8_t regWrite[16]; + int8_t result; + bool canContinue = true; + + if (canContinue) { + result = doI2CTest(); + canContinue = (result == 1); + } + +#if WII_EXTENSION_DEBUG==true + printf("WiiExtension::reset\n"); +#endif + + if (canContinue) { + regWrite[0] = 0xF0; + regWrite[1] = 0x55; + result = doI2CWrite(regWrite, 2); + canContinue = (result > -1); + } + + if (canContinue) { + regWrite[0] = 0xFB; + regWrite[1] = 0x00; + result = doI2CWrite(regWrite, 2); + canContinue = (result > -1); + } + + if (canContinue) { + // set data format + regWrite[0] = 0xFE; + regWrite[1] = 0x03; + result = doI2CWrite(regWrite, 2); + canContinue = (result > -1); + } + +#if WII_EXTENSION_DEBUG==true + printf("WiiExtension::reset canContinue? %1d\n", canContinue); +#endif + + if (canContinue) { +#if WII_EXTENSION_DEBUG==true + //printf("Reset Sent\n"); +#endif + isReady = true; + } else { +#if WII_EXTENSION_DEBUG==true + //printf("Device not found\n"); +#endif + } +} + +void WiiExtension::poll() { + uint8_t reg = _u(0x08); + uint8_t regWrite[16]; + uint8_t regRead[16]; + int8_t result; + +#if WII_EXTENSION_DEBUG==true + //printf("WiiExtension::poll\n"); + //printf("WiiExtension::poll isReady? %1d\n", isReady); +#endif + + if (!isReady) return; + + if (extensionType != WII_EXTENSION_NONE) { + switch (dataType) { + case WII_DATA_TYPE_1: + result = doI2CRead(regRead, 6); + break; + case WII_DATA_TYPE_2: + result = doI2CRead(regRead, 9); + break; + case WII_DATA_TYPE_3: + result = doI2CRead(regRead, 8); + break; + default: + // unknown. TBD + result = -1; +#if WII_EXTENSION_DEBUG==true + printf("WiiExtension::poll Unknown data type: %1d\n", dataType); +#endif + break; + } + + if (result > 0) { + switch (extensionType) { + case WII_EXTENSION_NUNCHUCK: + joy1X = (regRead[0] & 0xFF); + joy1Y = (regRead[1] & 0xFF); + + accelX = (((regRead[2] << 2) | ((regRead[5] >> 2) & 0x03))); + accelY = (((regRead[3] << 2) | ((regRead[5] >> 4) & 0x03))); + accelZ = (((regRead[4] << 2) | ((regRead[5] >> 6) & 0x03))); + buttonZ = (!(regRead[5] & 0x01)); + buttonC = (!(regRead[5] & 0x02)); + +#if WII_EXTENSION_DEBUG==true + printf("Joy X=%4d Y=%4d Acc X=%4d Y=%4d Z=%4d Btn Z=%1d C=%1d\n", joy1X, joy1Y, accelX, accelY, accelZ, buttonZ, buttonC); +#endif + + break; + case WII_EXTENSION_CLASSIC: + case WII_EXTENSION_CLASSIC_PRO: + // write data format to return + // see wiki for data types + if (dataType == WII_DATA_TYPE_1) { + joy1X = (regRead[0] & 0x3F); + joy1Y = (regRead[1] & 0x3F); + joy2X = ((regRead[0] & 0xC0) >> 3) | ((regRead[1] & 0xC0) >> 5) | ((regRead[2] & 0x80) >> 7); + joy2Y = (regRead[2] & 0x1F); + + triggerLeft = (((regRead[2] & 0x60) >> 2) | ((regRead[3] & 0xE0) >> 5)); + triggerRight = ((regRead[3] & 0x1F) >> 0); + + directionRight = !((regRead[4] & 0x80) >> 7); + directionDown = !((regRead[4] & 0x40) >> 6); + buttonLT = !((regRead[4] & 0x20) >> 5); + buttonMinus = !((regRead[4] & 0x10) >> 4); + buttonHome = !((regRead[4] & 0x08) >> 3); + buttonPlus = !((regRead[4] & 0x04) >> 2); + buttonRT = !((regRead[4] & 0x02) >> 1); + + buttonZL = !((regRead[5] & 0x80) >> 7); + buttonB = !((regRead[5] & 0x40) >> 6); + buttonY = !((regRead[5] & 0x20) >> 5); + buttonA = !((regRead[5] & 0x10) >> 4); + buttonX = !((regRead[5] & 0x08) >> 3); + buttonZR = !((regRead[5] & 0x04) >> 2); + directionLeft = !((regRead[5] & 0x02) >> 1); + directionUp = !((regRead[5] & 0x01) >> 0); + } else if (dataType == WII_DATA_TYPE_2) { + joy1X = ((regRead[0] << 2) | ((regRead[4] & 0x03) >> 0)); + joy1Y = ((regRead[2] << 2) | ((regRead[4] & 0x30) >> 4)); + joy2X = ((regRead[1] << 2) | ((regRead[4] & 0x0C) >> 2)); + joy2Y = ((regRead[3] << 2) | ((regRead[4] & 0xC0) >> 6)); + + triggerLeft = (regRead[5] & 0xFF); + triggerRight = (regRead[6] & 0xFF); + + directionRight = !((regRead[7] & 0x80) >> 7); + directionDown = !((regRead[7] & 0x40) >> 6); + buttonLT = !((regRead[7] & 0x20) >> 5); + buttonMinus = !((regRead[7] & 0x10) >> 4); + buttonHome = !((regRead[7] & 0x08) >> 3); + buttonPlus = !((regRead[7] & 0x04) >> 2); + buttonRT = !((regRead[7] & 0x02) >> 1); + + buttonZL = !((regRead[8] & 0x80) >> 7); + buttonB = !((regRead[8] & 0x40) >> 6); + buttonY = !((regRead[8] & 0x20) >> 5); + buttonA = !((regRead[8] & 0x10) >> 4); + buttonX = !((regRead[8] & 0x08) >> 3); + buttonZR = !((regRead[8] & 0x04) >> 2); + directionLeft = !((regRead[8] & 0x02) >> 1); + directionUp = !((regRead[8] & 0x01) >> 0); + } else if (dataType == WII_DATA_TYPE_3) { + joy1X = (regRead[0] & 0xFF); + joy1Y = (regRead[2] & 0xFF); + joy2X = (regRead[1] & 0xFF); + joy2Y = (regRead[3] & 0xFF); + + triggerLeft = (regRead[4] & 0xFF); + triggerRight = (regRead[5] & 0xFF); + + directionRight = !((regRead[6] & 0x80) >> 7); + directionDown = !((regRead[6] & 0x40) >> 6); + buttonLT = !((regRead[6] & 0x20) >> 5); + buttonMinus = !((regRead[6] & 0x10) >> 4); + buttonHome = !((regRead[6] & 0x08) >> 3); + buttonPlus = !((regRead[6] & 0x04) >> 2); + buttonRT = !((regRead[6] & 0x02) >> 1); + + buttonZL = !((regRead[7] & 0x80) >> 7); + buttonB = !((regRead[7] & 0x40) >> 6); + buttonY = !((regRead[7] & 0x20) >> 5); + buttonA = !((regRead[7] & 0x10) >> 4); + buttonX = !((regRead[7] & 0x08) >> 3); + buttonZR = !((regRead[7] & 0x04) >> 2); + directionLeft = !((regRead[7] & 0x02) >> 1); + directionUp = !((regRead[7] & 0x01) >> 0); + } else { + // unknown + } + +#if WII_EXTENSION_DEBUG==true + //if ((_lastRead[0] != regRead[0]) || (_lastRead[1] != regRead[1]) || (_lastRead[2] != regRead[2]) || (_lastRead[3] != regRead[3])) { + printf("Joy1 X=%4d Y=%4d Joy2 X=%4d Y=%4d\n", joy1X, joy1Y, joy2X, joy2Y); + //} + //printf("Joy1 X=%4d Y=%4d Joy2 X=%4d Y=%4d U=%1d D=%1d L=%1d R=%1d TL=%4d TR=%4d\n", joy1X, joy1Y, joy2X, joy2Y, directionUp, directionDown, directionLeft, directionRight, triggerLeft, triggerRight); + //printf("A=%1d B=%1d X=%1d Y=%1d ZL=%1d ZR=%1d LT=%1d RT=%1d -=%1d H=%1d +=%1d\n", buttonA, buttonB, buttonX, buttonY, buttonZL, buttonZR, buttonLT, buttonRT, buttonMinus, buttonHome, buttonPlus); +#endif + + break; + case WII_EXTENSION_GUITAR: + if (dataType == WII_DATA_TYPE_1) { + joy1X = (regRead[0] & 0x3F); + joy1Y = (regRead[1] & 0x3F); + + whammyBar = (regRead[3] & 0x1F); + joy2X = (regRead[3] & 0x1F); + + directionDown = !((regRead[4] & 0x40) >> 6); + buttonMinus = !((regRead[4] & 0x10) >> 4); + buttonPlus = !((regRead[4] & 0x04) >> 2); + + fretOrange = !((regRead[5] & 0x80) >> 7); + fretRed = !((regRead[5] & 0x40) >> 6); + fretBlue = !((regRead[5] & 0x20) >> 5); + fretGreen = !((regRead[5] & 0x10) >> 4); + fretYellow = !((regRead[5] & 0x08) >> 3); + pedalButton = !((regRead[5] & 0x04) >> 2); + directionUp = !((regRead[5] & 0x01) >> 0); + } else if (dataType == WII_DATA_TYPE_2) { + joy1X = ((regRead[0] << 2) | ((regRead[4] & 0x03) >> 0)); + joy1Y = ((regRead[2] << 2) | ((regRead[4] & 0x30) >> 4)); + + // analog 2 is not used but appears to not change? + //joy2X = ((regRead[1] << 2) | ((regRead[4] & 0x0C) >> 2)); + //joy2Y = ((regRead[3] << 2) | ((regRead[4] & 0xC0) >> 6)); + + whammyBar = (regRead[6] & 0xFF); + joy2X = (regRead[6] & 0xFF); + + directionDown = !((regRead[7] & 0x40) >> 6); + buttonMinus = !((regRead[7] & 0x10) >> 4); + buttonPlus = !((regRead[7] & 0x04) >> 2); + + fretOrange = !((regRead[8] & 0x80) >> 7); + fretRed = !((regRead[8] & 0x40) >> 6); + fretBlue = !((regRead[8] & 0x20) >> 5); + fretGreen = !((regRead[8] & 0x10) >> 4); + fretYellow = !((regRead[8] & 0x08) >> 3); + pedalButton = !((regRead[8] & 0x04) >> 2); + directionUp = !((regRead[8] & 0x01) >> 0); + } else if (dataType == WII_DATA_TYPE_3) { + joy1X = (regRead[0] & 0xFF); + joy1Y = (regRead[2] & 0xFF); + + // analog 2 is not used but appears to not change? + //joy2X = regRead[1]; + //joy2Y = regRead[3]; + + whammyBar = (regRead[5] & 0xFF); + joy2X = (regRead[5] & 0xFF); + + directionDown = !((regRead[6] & 0x40) >> 6); + buttonMinus = !((regRead[6] & 0x10) >> 4); + buttonPlus = !((regRead[6] & 0x04) >> 2); + + fretOrange = !((regRead[7] & 0x80) >> 7); + fretRed = !((regRead[7] & 0x40) >> 6); + fretBlue = !((regRead[7] & 0x20) >> 5); + fretGreen = !((regRead[7] & 0x10) >> 4); + fretYellow = !((regRead[7] & 0x08) >> 3); + pedalButton = !((regRead[7] & 0x04) >> 2); + directionUp = !((regRead[7] & 0x01) >> 0); + } + +#if WII_EXTENSION_DEBUG==true +// printf("Joy1 X=%4d Y=%4d Whammy=%4d U=%1d D=%1d -=%1d +=%1d\n", joy1X, joy1Y, whammyBar, directionUp, directionDown, buttonMinus, buttonPlus); +// printf("Joy1 X=%4d Y=%4d Whammy=%4d U=%1d D=%1d -=%1d +=%1d\n", joy1X, joy1Y, whammyBar, directionUp, directionDown, buttonMinus, buttonPlus); +// printf("O=%1d B=%1d Y=%1d R=%1d G=%1d\n", fretOrange, fretBlue, fretYellow, fretRed, fretGreen); +#endif + break; + case WII_EXTENSION_TAIKO: + if (dataType == WII_DATA_TYPE_1) { + drumLeft = !((regRead[5] & 0x40) >> 6); + rimLeft = !((regRead[5] & 0x20) >> 5); + drumRight = !((regRead[5] & 0x10) >> 4); + rimRight = !((regRead[5] & 0x08) >> 3); + } else if (dataType == WII_DATA_TYPE_2) { + drumLeft = !((regRead[8] & 0x40) >> 6); + rimLeft = !((regRead[8] & 0x20) >> 5); + drumRight = !((regRead[8] & 0x10) >> 4); + rimRight = !((regRead[8] & 0x08) >> 3); + } else if (dataType == WII_DATA_TYPE_3) { + drumLeft = !((regRead[7] & 0x40) >> 6); + rimLeft = !((regRead[7] & 0x20) >> 5); + drumRight = !((regRead[7] & 0x10) >> 4); + rimRight = !((regRead[7] & 0x08) >> 3); + } + +#if WII_EXTENSION_DEBUG==true + //if (_lastRead[0] != regRead[0]) printf("Byte0 " BYTE_TO_BINARY_PATTERN "\n", BYTE_TO_BINARY(regRead[0])); + //if (_lastRead[1] != regRead[1]) printf("Byte1 " BYTE_TO_BINARY_PATTERN "\n", BYTE_TO_BINARY(regRead[1])); + //if (_lastRead[2] != regRead[2]) printf("Byte2 " BYTE_TO_BINARY_PATTERN "\n", BYTE_TO_BINARY(regRead[2])); + //if (_lastRead[3] != regRead[3]) printf("Byte3 " BYTE_TO_BINARY_PATTERN "\n", BYTE_TO_BINARY(regRead[3])); + //if (_lastRead[4] != regRead[4]) printf("Byte4 " BYTE_TO_BINARY_PATTERN "\n", BYTE_TO_BINARY(regRead[4])); + //if (_lastRead[5] != regRead[5]) printf("Byte5 " BYTE_TO_BINARY_PATTERN "\n", BYTE_TO_BINARY(regRead[5])); + // + //if (_lastRead[7] != regRead[7]) { + // printf("DL=%1d RL=%1d DR=%1d RR=%1d\n", drumLeft, rimLeft, drumRight, rimRight); + //} +#endif + + break; + } + + // calibrate and remap + joy1X = map( + calibrate(joy1X, _minX1, _maxX1, _cenX1), + 0+_minX1, + (_analogPrecision1From-_maxX1), + 0, + (_analogPrecision1To-1) + ); + joy1Y = map( + calibrate(joy1Y, _minY1, _maxY1, _cenY1), + 0+_minY1, + (_analogPrecision1From-_maxY1), + 0, + (_analogPrecision1To-1) + ); + + joy2X = map( + calibrate(joy2X, _minX2, _maxX2, _cenX2), + 0+_minX2, + (_analogPrecision2From-_maxX2), + 0, + (_analogPrecision2To-1) + ); + joy2Y = map( + calibrate(joy2Y, _minY2, _maxY2, _cenY2), + 0+_minY2, + (_analogPrecision2From-_maxY2), + 0, + (_analogPrecision2To-1) + ); + + triggerLeft = map( + triggerLeft, + 0, + (_triggerPrecision1From-1), + 0, + (_triggerPrecision1To-1) + ); + triggerRight = map( + triggerRight, + 0, + (_triggerPrecision2From-1), + 0, + (_triggerPrecision2To-1) + ); + +#if WII_EXTENSION_DEBUG==true + //if ((_lastRead[0] != regRead[0]) || (_lastRead[1] != regRead[1]) || (_lastRead[2] != regRead[2]) || (_lastRead[3] != regRead[3])) { + // printf("Joy1 X=%4d Y=%4d Joy2 X=%4d Y=%4d\n", joy1X, joy1Y, joy2X, joy2Y); + //} + //printf("Joy1 X=%4d Y=%4d Joy2 X=%4d Y=%4d U=%1d D=%1d L=%1d R=%1d TL=%4d TR=%4d\n", joy1X, joy1Y, joy2X, joy2Y, directionUp, directionDown, directionLeft, directionRight, triggerLeft, triggerRight); + //printf("A=%1d B=%1d X=%1d Y=%1d ZL=%1d ZR=%1d LT=%1d RT=%1d -=%1d H=%1d +=%1d\n", buttonA, buttonB, buttonX, buttonY, buttonZL, buttonZR, buttonLT, buttonRT, buttonMinus, buttonHome, buttonPlus); + for (int i = 0; i < result; ++i) { + _lastRead[i] = regRead[i]; + } +#endif + // continue poll + regWrite[0] = 0x00; + result = doI2CWrite(regWrite, 1); + } else { + // device disconnected or invalid read + extensionType = WII_EXTENSION_NONE; + reset(); + start(); + } + } else { + reset(); + start(); + } +} + +uint16_t WiiExtension::map(uint16_t x, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max) { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +uint16_t WiiExtension::calibrate(uint16_t pos, uint16_t min, uint16_t max, uint16_t cen) { + uint16_t result; + +#if WII_EXTENSION_CALIBRATION==true + if (pos >= min && pos <= max) { + result = pos; + } else { + if (pos < min) { + result = min; + } else if (pos > max) { + result = max; + } else { + result = cen; + } + } +#else + result = pos; +#endif + + return result; +} + +int WiiExtension::doI2CWrite(uint8_t *pData, int iLen) { + int result = i2c_write_blocking(picoI2C, address, pData, iLen, false); + waitUntil_us(WII_EXTENSION_DELAY); + return result; +} + +int WiiExtension::doI2CRead(uint8_t *pData, int iLen) { + int result = i2c_read_blocking(picoI2C, address, pData, iLen, false); + waitUntil_us(WII_EXTENSION_DELAY); + return result; +} + +uint8_t WiiExtension::doI2CTest() { + int result; + uint8_t rxdata; + result = doI2CRead(&rxdata, 1); + return (result >= 0); +} + +void WiiExtension::doI2CInit() { + if ((iSDA + 2 * i2c_hw_index(picoI2C))%4 != 0) return; + if ((iSCL + 3 + 2 * i2c_hw_index(picoI2C))%4 != 0) return; + + i2c_init(picoI2C, iSpeed); + gpio_set_function(iSDA, GPIO_FUNC_I2C); + gpio_set_function(iSCL, GPIO_FUNC_I2C); + gpio_pull_up(iSDA); + gpio_pull_up(iSCL); + + return; +} + +void WiiExtension::waitUntil_us(uint64_t us) { + WiiExtension_alarmFired = false; + + // Enable the interrupt for our alarm (the timer outputs 4 alarm irqs) + hw_set_bits(&timer_hw->inte, 1u << WII_ALARM_NUM); + // Set irq handler for alarm irq + irq_set_exclusive_handler(WII_ALARM_IRQ, alarmIRQ); + // Enable the alarm irq + irq_set_enabled(WII_ALARM_IRQ, true); + // Enable interrupt in block and at processor + + // Alarm is only 32 bits so if trying to delay more + // than that need to be careful and keep track of the upper + // bits + uint64_t target = timer_hw->timerawl + us; + + // Write the lower 32 bits of the target time to the alarm which + // will arm it + timer_hw->alarm[WII_ALARM_NUM] = (uint32_t) target; + + while (!WiiExtension_alarmFired); +} + +void WiiExtension::alarmIRQ() { + // Clear the alarm irq + hw_clear_bits(&timer_hw->intr, 1u << WII_ALARM_NUM); + + // Assume alarm 0 has fired + WiiExtension_alarmFired = true; +} diff --git a/lib/WiiExtension/WiiExtension.h b/lib/WiiExtension/WiiExtension.h new file mode 100644 index 000000000..a869afd54 --- /dev/null +++ b/lib/WiiExtension/WiiExtension.h @@ -0,0 +1,187 @@ +// WiiNunchuk Library +// category=Signal Input/Output + +#ifndef _WIIEXTENSION_H_ +#define _WIIEXTENSION_H_ + +#include "pico/stdlib.h" +#include "hardware/i2c.h" + +#define WII_EXTENSION_NONE -1 +#define WII_EXTENSION_NUNCHUCK 0 +#define WII_EXTENSION_CLASSIC 1 +#define WII_EXTENSION_CLASSIC_PRO 2 +#define WII_EXTENSION_DRAWSOME 3 +#define WII_EXTENSION_GUITAR 4 +#define WII_EXTENSION_DRUMS 5 +#define WII_EXTENSION_TURNTABLE 6 +#define WII_EXTENSION_TAIKO 7 +#define WII_EXTENSION_UDRAW 8 +#define WII_EXTENSION_BALANCE_BOARD 9 +#define WII_EXTENSION_MOTION_PLUS 10 + +#define WII_DATA_TYPE_0 0 +#define WII_DATA_TYPE_1 1 +#define WII_DATA_TYPE_2 2 +#define WII_DATA_TYPE_3 3 + +#define WII_ANALOG_PRECISION_0 32 +#define WII_ANALOG_PRECISION_1 64 +#define WII_ANALOG_PRECISION_2 256 +#define WII_ANALOG_PRECISION_3 1024 + +#ifndef WII_EXTENSION_DEBUG +#define WII_EXTENSION_DEBUG false +#endif + +#ifndef WII_EXTENSION_DELAY +#define WII_EXTENSION_DELAY 300 +#endif + +#ifndef WII_EXTENSION_TIMEOUT +#define WII_EXTENSION_TIMEOUT 2 +#endif + +#ifndef WII_EXTENSION_CALIBRATION +#define WII_EXTENSION_CALIBRATION false +#endif + +#define WII_ALARM_NUM 0 +#define WII_ALARM_IRQ TIMER_IRQ_0 + +static volatile bool WiiExtension_alarmFired; + +#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" +#define BYTE_TO_BINARY(byte) \ + ((byte) & 0x80 ? '1' : '0'), \ + ((byte) & 0x40 ? '1' : '0'), \ + ((byte) & 0x20 ? '1' : '0'), \ + ((byte) & 0x10 ? '1' : '0'), \ + ((byte) & 0x08 ? '1' : '0'), \ + ((byte) & 0x04 ? '1' : '0'), \ + ((byte) & 0x02 ? '1' : '0'), \ + ((byte) & 0x01 ? '1' : '0') + +class WiiExtension { + protected: + uint8_t address; + public: + int8_t extensionType = WII_EXTENSION_NONE; + int8_t dataType = WII_DATA_TYPE_0; + + uint16_t joy1X = 0; + uint16_t joy1Y = 0; + uint16_t joy2X = 0; + uint16_t joy2Y = 0; + uint16_t accelX = 0; + uint16_t accelY = 0; + uint16_t accelZ = 0; + + bool buttonZ = false; + bool buttonC = false; + bool buttonZR = false; + bool buttonZL = false; + bool buttonA = false; + bool buttonB = false; + bool buttonX = false; + bool buttonY = false; + bool buttonPlus = false; + bool buttonHome = false; + bool buttonMinus = false; + bool buttonLT = false; + bool buttonRT = false; + + bool directionUp = false; + bool directionDown = false; + bool directionLeft = false; + bool directionRight = false; + + uint16_t triggerLeft = 0; + uint16_t triggerRight = 0; + + bool fretGreen = false; + bool fretRed = false; + bool fretYellow = false; + bool fretBlue = false; + bool fretOrange = false; + bool pedalButton = false; + + uint16_t whammyBar = 0; + + bool rimLeft = false; + bool rimRight = false; + bool drumLeft = false; + bool drumRight = false; + + bool isReady = false; + + // Constructor + WiiExtension(int sda, int scl, i2c_inst_t *i2cCtl, int32_t speed, uint8_t addr); + + // Methods + void begin(); + void reset(); + void start(); + void poll(); + private: + + uint8_t iSDA; + uint8_t iSCL; + uint8_t bWire; + i2c_inst_t *picoI2C; + + int32_t iSpeed; + + uint16_t _minX1 = 0; + uint16_t _maxX1 = 1; + uint16_t _cenX1 = 0; + + uint16_t _minY1 = 0; + uint16_t _maxY1 = 1; + uint16_t _cenY1 = 0; + + uint16_t _minX2 = 0; + uint16_t _maxX2 = 1; + uint16_t _cenX2 = 0; + + uint16_t _minY2 = 0; + uint16_t _maxY2 = 1; + uint16_t _cenY2 = 0; + + uint16_t _accelX0G = 0; + uint16_t _accelY0G = 0; + uint16_t _accelZ0G = 0; + uint16_t _accelX1G = 0; + uint16_t _accelY1G = 0; + uint16_t _accelZ1G = 0; + + //uint8_t _lastRead[16]; + + uint16_t _calibrationPrecision1From = WII_ANALOG_PRECISION_0; + uint16_t _calibrationPrecision1To = WII_ANALOG_PRECISION_0; + uint16_t _calibrationPrecision2From = WII_ANALOG_PRECISION_0; + uint16_t _calibrationPrecision2To = WII_ANALOG_PRECISION_0; + + uint16_t _analogPrecision1From = WII_ANALOG_PRECISION_0; + uint16_t _analogPrecision1To = WII_ANALOG_PRECISION_0; + uint16_t _analogPrecision2From = WII_ANALOG_PRECISION_0; + uint16_t _analogPrecision2To = WII_ANALOG_PRECISION_0; + + uint16_t _triggerPrecision1From = WII_ANALOG_PRECISION_0; + uint16_t _triggerPrecision1To = WII_ANALOG_PRECISION_0; + uint16_t _triggerPrecision2From = WII_ANALOG_PRECISION_0; + uint16_t _triggerPrecision2To = WII_ANALOG_PRECISION_0; + + uint16_t map(uint16_t x, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max); + uint16_t calibrate(uint16_t pos, uint16_t min, uint16_t max, uint16_t center); + + int doI2CWrite(uint8_t *pData, int iLen); + int doI2CRead(uint8_t *pData, int iLen); + uint8_t doI2CTest(); + void doI2CInit(); + + void waitUntil_us(uint64_t us); + static void alarmIRQ(); +}; + +#endif diff --git a/src/addons/wiiext.cpp b/src/addons/wiiext.cpp new file mode 100644 index 000000000..a40aa104a --- /dev/null +++ b/src/addons/wiiext.cpp @@ -0,0 +1,146 @@ +#include "addons/wiiext.h" +#include "storagemanager.h" +#include "hardware/gpio.h" + +bool WiiExtensionInput::available() { + const BoardOptions& boardOptions = Storage::getInstance().getBoardOptions(); + AddonOptions options = Storage::getInstance().getAddonOptions(); + + return (!boardOptions.hasI2CDisplay && (options.WiiExtensionAddonEnabled && + options.wiiExtensionSDAPin != (uint8_t)-1 && + options.wiiExtensionSCLPin != (uint8_t)-1)); +} + +void WiiExtensionInput::setup() { + AddonOptions options = Storage::getInstance().getAddonOptions(); + nextTimer = getMillis(); + +#if WII_EXTENSION_DEBUG==true + stdio_init_all(); +#endif + + uIntervalMS = 0; + + wii = new WiiExtension( + options.wiiExtensionSDAPin, + options.wiiExtensionSCLPin, + options.wiiExtensionBlock == 0 ? i2c0 : i2c1, + options.wiiExtensionSpeed, + WII_EXTENSION_I2C_ADDR); + wii->begin(); + wii->start(); +} + +void WiiExtensionInput::process() { + if (nextTimer < getMillis()) { + wii->poll(); + + if (wii->extensionType == WII_EXTENSION_NUNCHUCK) { + buttonZ = wii->buttonZ; + buttonC = wii->buttonC; + + leftX = map(wii->joy1X,0,1023,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + leftY = map(wii->joy1Y,1023,0,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + rightX = GAMEPAD_JOYSTICK_MID; + rightY = GAMEPAD_JOYSTICK_MID; + + triggerLeft = 0; + triggerRight = 0; + } else if ((wii->extensionType == WII_EXTENSION_CLASSIC) || (wii->extensionType == WII_EXTENSION_CLASSIC_PRO)) { + buttonA = wii->buttonA; + buttonB = wii->buttonB; + buttonX = wii->buttonX; + buttonY = wii->buttonY; + buttonL = wii->buttonZL; + buttonZL = wii->buttonLT; + buttonR = wii->buttonZR; + buttonZR = wii->buttonRT; + dpadUp = wii->directionUp; + dpadDown = wii->directionDown; + dpadLeft = wii->directionLeft; + dpadRight = wii->directionRight; + buttonSelect = wii->buttonMinus; + buttonStart = wii->buttonPlus; + buttonHome = wii->buttonHome; + + if (wii->extensionType == WII_EXTENSION_CLASSIC) { + triggerLeft = wii->triggerLeft; + triggerRight = wii->triggerRight; + } + + leftX = map(wii->joy1X,0,WII_ANALOG_PRECISION_3,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + leftY = map(wii->joy1Y,WII_ANALOG_PRECISION_3,0,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + rightX = map(wii->joy2X,0,WII_ANALOG_PRECISION_3,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + rightY = map(wii->joy2Y,WII_ANALOG_PRECISION_3,0,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + } else if (wii->extensionType == WII_EXTENSION_GUITAR) { + buttonSelect = wii->buttonMinus; + buttonStart = wii->buttonPlus; + + dpadUp = wii->directionUp; + dpadDown = wii->directionDown; + + buttonB = wii->fretGreen; + buttonA = wii->fretRed; + buttonX = wii->fretYellow; + buttonY = wii->fretBlue; + buttonL = wii->fretOrange; + + // whammy currently maps to Joy2X in addition to the raw whammy value + whammyBar = wii->whammyBar; + + leftX = map(wii->joy1X,0,WII_ANALOG_PRECISION_3,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + leftY = map(wii->joy1Y,WII_ANALOG_PRECISION_3,0,GAMEPAD_JOYSTICK_MIN,GAMEPAD_JOYSTICK_MAX); + rightX = map(wii->joy2X,0,WII_ANALOG_PRECISION_3,GAMEPAD_JOYSTICK_MID,GAMEPAD_JOYSTICK_MAX); + rightY = GAMEPAD_JOYSTICK_MID; + + triggerLeft = 0; + triggerRight = 0; + } else if (wii->extensionType == WII_EXTENSION_TAIKO) { + buttonL = wii->rimLeft; + buttonR = wii->rimRight; + + dpadRight = wii->drumLeft; + buttonA = wii->drumRight; + } + + nextTimer = getMillis() + uIntervalMS; + } + + Gamepad * gamepad = Storage::getInstance().GetGamepad(); + + gamepad->state.lx = leftX; + gamepad->state.ly = leftY; + gamepad->state.rx = rightX; + gamepad->state.ry = rightY; + + if (wii->extensionType == WII_EXTENSION_CLASSIC) { + gamepad->hasAnalogTriggers = true; + gamepad->state.lt = triggerLeft; + gamepad->state.rt = triggerRight; + } else { + gamepad->hasAnalogTriggers = false; + } + + if (buttonC) gamepad->state.buttons |= GAMEPAD_MASK_B1; + if (buttonZ) gamepad->state.buttons |= GAMEPAD_MASK_B2; + + if (buttonA) gamepad->state.buttons |= GAMEPAD_MASK_B2; + if (buttonB) gamepad->state.buttons |= GAMEPAD_MASK_B1; + if (buttonX) gamepad->state.buttons |= GAMEPAD_MASK_B4; + if (buttonY) gamepad->state.buttons |= GAMEPAD_MASK_B3; + if (buttonL) gamepad->state.buttons |= GAMEPAD_MASK_L1; + if (buttonZL) gamepad->state.buttons |= GAMEPAD_MASK_L2; + if (buttonR) gamepad->state.buttons |= GAMEPAD_MASK_R1; + if (buttonZR) gamepad->state.buttons |= GAMEPAD_MASK_R2; + if (buttonSelect) gamepad->state.buttons |= GAMEPAD_MASK_S1; + if (buttonStart) gamepad->state.buttons |= GAMEPAD_MASK_S2; + if (buttonHome) gamepad->state.buttons |= GAMEPAD_MASK_A1; + if (dpadUp) gamepad->state.dpad |= GAMEPAD_MASK_UP; + if (dpadDown) gamepad->state.dpad |= GAMEPAD_MASK_DOWN; + if (dpadLeft) gamepad->state.dpad |= GAMEPAD_MASK_LEFT; + if (dpadRight) gamepad->state.dpad |= GAMEPAD_MASK_RIGHT; +} + +uint16_t WiiExtensionInput::map(uint16_t x, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max) { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} diff --git a/src/configs/webconfig.cpp b/src/configs/webconfig.cpp index 1c8d0d391..5b2bdb260 100644 --- a/src/configs/webconfig.cpp +++ b/src/configs/webconfig.cpp @@ -802,6 +802,10 @@ std::string setAddonOptions() docToValue(addonOptions.sliderSOCDModeOne, doc, "sliderSOCDModeOne"); docToValue(addonOptions.sliderSOCDModeTwo, doc, "sliderSOCDModeTwo"); docToValue(addonOptions.sliderSOCDModeDefault, doc, "sliderSOCDModeDefault"); + docToPin(addonOptions.wiiExtensionSDAPin, doc, "wiiExtensionSDAPin"); + docToPin(addonOptions.wiiExtensionSCLPin, doc, "wiiExtensionSCLPin"); + docToValue(addonOptions.wiiExtensionBlock, doc, "wiiExtensionBlock"); + docToValue(addonOptions.wiiExtensionSpeed, doc, "wiiExtensionSpeed"); docToValue(addonOptions.AnalogInputEnabled, doc, "AnalogInputEnabled"); docToValue(addonOptions.BoardLedAddonEnabled, doc, "BoardLedAddonEnabled"); docToValue(addonOptions.BuzzerSpeakerAddonEnabled, doc, "BuzzerSpeakerAddonEnabled"); @@ -815,6 +819,7 @@ std::string setAddonOptions() docToValue(addonOptions.PS4ModeAddonEnabled, doc, "PS4ModeAddonEnabled"); docToValue(addonOptions.ReverseInputEnabled, doc, "ReverseInputEnabled"); docToValue(addonOptions.TurboInputEnabled, doc, "TurboInputEnabled"); + docToValue(addonOptions.WiiExtensionAddonEnabled, doc, "WiiExtensionAddonEnabled"); Storage::getInstance().setAddonOptions(addonOptions); @@ -972,6 +977,10 @@ std::string getAddonOptions() writeDoc(doc, "sliderSOCDModeOne", addonOptions.sliderSOCDModeOne); writeDoc(doc, "sliderSOCDModeTwo", addonOptions.sliderSOCDModeTwo); writeDoc(doc, "sliderSOCDModeDefault", addonOptions.sliderSOCDModeDefault); + writeDoc(doc, "wiiExtensionSDAPin", addonOptions.wiiExtensionSDAPin == 0xFF ? -1 : addonOptions.wiiExtensionSDAPin); + writeDoc(doc, "wiiExtensionSCLPin", addonOptions.wiiExtensionSCLPin == 0xFF ? -1 : addonOptions.wiiExtensionSCLPin); + writeDoc(doc, "wiiExtensionBlock", addonOptions.wiiExtensionBlock); + writeDoc(doc, "wiiExtensionSpeed", addonOptions.wiiExtensionSpeed); writeDoc(doc, "AnalogInputEnabled", addonOptions.AnalogInputEnabled); writeDoc(doc, "BoardLedAddonEnabled", addonOptions.BoardLedAddonEnabled); writeDoc(doc, "BuzzerSpeakerAddonEnabled", addonOptions.BuzzerSpeakerAddonEnabled); @@ -985,6 +994,7 @@ std::string getAddonOptions() writeDoc(doc, "PS4ModeAddonEnabled", addonOptions.PS4ModeAddonEnabled); writeDoc(doc, "ReverseInputEnabled", addonOptions.ReverseInputEnabled); writeDoc(doc, "TurboInputEnabled", addonOptions.TurboInputEnabled); + writeDoc(doc, "WiiExtensionAddonEnabled", addonOptions.WiiExtensionAddonEnabled); addUsedPinsArray(doc); diff --git a/src/gp2040.cpp b/src/gp2040.cpp index b38675c2a..2e1b8ce3f 100644 --- a/src/gp2040.cpp +++ b/src/gp2040.cpp @@ -17,6 +17,7 @@ #include "addons/reverse.h" #include "addons/turbo.h" #include "addons/slider_socd.h" +#include "addons/wiiext.h" // Pico includes #include "pico/bootrom.h" @@ -104,6 +105,7 @@ void GP2040::setup() { addons.LoadAddon(new JSliderInput(), CORE0_INPUT); addons.LoadAddon(new ReverseInput(), CORE0_INPUT); addons.LoadAddon(new TurboInput(), CORE0_INPUT); + addons.LoadAddon(new WiiExtensionInput(), CORE0_INPUT); addons.LoadAddon(new PlayerNumAddon(), CORE0_USBREPORT); addons.LoadAddon(new SliderSOCDInput(), CORE0_INPUT); } diff --git a/src/storagemanager.cpp b/src/storagemanager.cpp index 1f0aca91a..780939e87 100644 --- a/src/storagemanager.cpp +++ b/src/storagemanager.cpp @@ -30,6 +30,7 @@ #include "addons/reverse.h" #include "addons/turbo.h" #include "addons/slider_socd.h" +#include "addons/wiiext.h" #include "bitmaps.h" @@ -195,6 +196,10 @@ void Storage::setDefaultAddonOptions() addonOptions.sliderSOCDModeOne = SLIDER_SOCD_SLOT_ONE; addonOptions.sliderSOCDModeTwo = SLIDER_SOCD_SLOT_TWO; addonOptions.sliderSOCDModeDefault = SLIDER_SOCD_SLOT_DEFAULT; + addonOptions.wiiExtensionSDAPin = WII_EXTENSION_I2C_SDA_PIN; + addonOptions.wiiExtensionSCLPin = WII_EXTENSION_I2C_SCL_PIN; + addonOptions.wiiExtensionBlock = (WII_EXTENSION_I2C_BLOCK == i2c0) ? 0 : 1; + addonOptions.wiiExtensionSpeed = WII_EXTENSION_I2C_SPEED; addonOptions.AnalogInputEnabled = ANALOG_INPUT_ENABLED; addonOptions.BoardLedAddonEnabled = BOARD_LED_ENABLED; addonOptions.BootselButtonAddonEnabled = BOOTSEL_BUTTON_ENABLED; @@ -208,6 +213,7 @@ void Storage::setDefaultAddonOptions() addonOptions.PS4ModeAddonEnabled = PS4MODE_ADDON_ENABLED; addonOptions.ReverseInputEnabled = REVERSE_ENABLED; addonOptions.TurboInputEnabled = TURBO_ENABLED; + addonOptions.WiiExtensionAddonEnabled = WII_EXTENSION_ENABLED; setAddonOptions(addonOptions); }