diff --git a/CLAUDE.md b/CLAUDE.md index 0dc58e6..699c49d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,9 +3,9 @@ ## Project Overview libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 correspondence. Cross-platform library for Direct Output Framework tasks, used by Visual Pinball Standalone. -**Current Status**: ~99% complete - Core architecture, effects system, device management, all controller types, shape effects with bitmap rendering at 100% 1:1 correspondence. +**Current Status**: ~99.5% complete - Core architecture, effects system, device management, all controller types, shape effects with bitmap rendering at 100% 1:1 correspondence. -**Recent Major Implementation**: Matrix toy effects configuration completely fixed to achieve perfect 1:1 C# correspondence - corrected conditional logic for RGBAMatrixColorEffect creation, fixed case-insensitive color lookup, resolved E142/E145 effect creation issues, and cross-platform image loading via stb_image.h for bitmap shapes system. +**Recent Major Implementation**: Complete PAC controllers libusb migration - PacLed64, PacDrive, PacUIO migrated from HIDAPI to libusb via PacDriveSingleton for unified USB device management. Fixed PacLed64 individual LED protocol with 0-based numbering to match Ultimarc specification. All PAC controllers now use libusb control transfers with proper device permissions. Serial port handling improved with Linux-specific fixes for USB CDC device cleanup to prevent hanging on close operations. ## Core Coding Principles @@ -68,7 +68,7 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden - **Memory Safety**: All heap corruption issues resolved via proper value semantics - **Effects System**: All effect types with correct chain ordering and nested blink support - **Toys System**: All toy types with correct interface implementations -- **Output Controllers**: LedWiz, Pinscape, PinscapePico, FTDI, ComPort, DudesCab, DMX, PinOne, LED Strips +- **Output Controllers**: LedWiz, Pinscape, PinscapePico, FTDI, ComPort, DudesCab, DMX, PinOne, LED Strips, PAC Controllers (PacLed64, PacDrive, PacUIO) - **Matrix Effects**: Flicker, Plasma, and Shift effects with exact timing correspondence - **Bitmap Effects System**: Complete bitmap loading, FastImage.Frames, DirectOutputShapes.png support - **Shape Effects System**: SHP code parsing, shape resolution, RGBAMatrixShapeEffect implementation @@ -76,8 +76,7 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden - **Configuration**: LedControl loader, GlobalConfig, ScheduledSettings system with corrected matrix effect logic - **Cross-Platform**: Windows/Linux/macOS builds with manual dependency compilation -### ❌ MISSING COMPONENTS (1% remaining) -- **PAC Controllers**: PacDrive, PacLed64, PacUIO +### ❌ MISSING COMPONENTS (0.5% remaining) - **SSF Controllers**: 7 variants with feedback systems - **Philips Hue Controllers**: Smart lighting integration - **Extensions Utilities**: 11 utility classes @@ -86,7 +85,8 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden ### Hardware Requirements - **PinscapePico**: Requires sudo access for HID enumeration -- **Unit Bias**: LedWiz(0), Pinscape(50), PinscapePico(119), DudesCab(90-94), PinOne(1-16) +- **PAC Controllers**: Use libusb with udev rules for device access without sudo +- **Unit Bias**: LedWiz(0), Pinscape(50), PinscapePico(119), DudesCab(90-94), PinOne(1-16), PAC(19-27) ### Windows API Conflicts - Use `SendPipeMessage()` not `SendMessage()` @@ -105,6 +105,18 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden - **DudesCab**: hidapi HID multi-part messages - **DMX**: UDP ArtNet broadcast port 6454 - **PinOne**: Named pipes with Base64 encoding over text +- **PAC Controllers**: libusb control transfers (PacLed64: individual LED intensity with 0-based numbering, PacDrive: 16-bit LED bitmask, PacUIO: via PacDriveSingleton) + +### Linux Serial Port Handling (Critical) +- **USB CDC Devices**: Can hang on `sp_close()` - requires RTS/DTR reset before closing +- **Device Existence Check**: Always verify device files exist before opening (prevents stale libserialport cache issues) +- **Cleanup Sequence**: `sp_flush(SP_BUF_BOTH)` → `sp_set_rts(SP_RTS_OFF)` → `sp_set_dtr(SP_DTR_OFF)` → `sp_close()` +- **Timeout Values**: Must match C# exactly (PinOne: 100ms, PinOneCommunication: 100ms server connection) + +### Auto-Configuration Logging Philosophy +- **C# Pattern**: Auto-configurators are mostly silent - only log when devices are actually found/configured +- **No Scan Messages**: C# never logs "device scan found X devices" - remove all such logging +- **Debug vs Production**: Never leave debug logging in production code - maintain 1:1 C# correspondence for all output ### Bitmap Shapes Architecture - **Image Loading**: Cross-platform loading via stb_image.h supporting PNG, GIF, BMP formats @@ -117,3 +129,4 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden ## References - **C# Source**: `/Users/jmillard/DirectOutput` - **Documentation**: See NOTES.md for DirectOutput configuration parsing details +- **PAC Protocol Reference**: [Ultimarc-linux](https://github.com/katie-snow/Ultimarc-linux) - Essential for libusb PAC controller implementation diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f561b6..d2e46f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -257,6 +257,13 @@ if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "macos" OR PLATFORM STREQUAL "li src/cab/out/ftdichip/FTDI.cpp src/cab/out/lw/LedWiz.cpp src/cab/out/lw/LedWizAutoConfigurator.cpp + src/cab/out/pac/PacLed64.cpp + src/cab/out/pac/PacLed64AutoConfigurator.cpp + src/cab/out/pac/PacDrive.cpp + src/cab/out/pac/PacDriveAutoConfigurator.cpp + src/cab/out/pac/PacUIO.cpp + src/cab/out/pac/PacUIOAutoConfigurator.cpp + src/cab/out/pac/PacDriveSingleton.cpp src/cab/out/pinone/NamedPipeServer.cpp src/cab/out/pinone/PinOne.cpp src/cab/out/pinone/PinOneAutoConfigurator.cpp diff --git a/NOTES.md b/NOTES.md index 22801e8..c023ee0 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,5 +1,38 @@ # NOTES.md +## USB Device Access Setup + +To use libusb devices (PAC Controllers) without sudo privileges: + +### Add user to plugdev group: +```bash +sudo usermod -a -G plugdev $USER +``` + +### Create udev rules for PAC devices: +```bash +sudo nano /etc/udev/rules.d/99-pacled64.rules +``` + +Add the following content: +``` +# PacLed64 devices (VID:0xD209, PID:0x1401-0x1404) +SUBSYSTEM=="usb", ATTRS{idVendor}=="d209", ATTRS{idProduct}=="1401", MODE="0664", GROUP="plugdev" +SUBSYSTEM=="usb", ATTRS{idVendor}=="d209", ATTRS{idProduct}=="1402", MODE="0664", GROUP="plugdev" +SUBSYSTEM=="usb", ATTRS{idVendor}=="d209", ATTRS{idProduct}=="1403", MODE="0664", GROUP="plugdev" +SUBSYSTEM=="usb", ATTRS{idVendor}=="d209", ATTRS{idProduct}=="1404", MODE="0664", GROUP="plugdev" + +# PacDrive devices (VID:0xD209, PID:0x1500) +SUBSYSTEM=="usb", ATTRS{idVendor}=="d209", ATTRS{idProduct}=="1500", MODE="0664", GROUP="plugdev" +``` + +### Reload udev rules and logout/login: +```bash +sudo udevadm control --reload-rules +sudo udevadm trigger +# Then logout and login to apply group membership +``` + ## DirectOutput Framework Configuration & Debugging ### Official DirectOutput Documentation @@ -61,4 +94,45 @@ PID=$! sleep 2 leaks $PID kill $PID -``` \ No newline at end of file +``` + +## Debug Iteration on Batocera + +For testing libdof on Batocera (ARM-based retro gaming distribution), use this cross-compilation and deployment workflow: + +### 1. Build on Ubuntu Host +```bash +# Cross-compile for Linux x64 (Batocera target architecture) +cmake -DPLATFORM=linux -DARCH=x64 -B build +cmake --build build -- -j10 +``` + +### 2. Deploy to Batocera Box +```bash +# Sync built libraries and test executable to Batocera +rsync -avz --delete \ + --exclude='libdof.a' \ + --include='lib*' \ + --include='dof_test' \ + --exclude='*' \ + build/ root@192.168.1.160:/userdata/dof +``` + +### 3. Test on Batocera Target +```bash +# SSH into Batocera box and run tests +ssh root@192.168.1.160 +cd /userdata/dof + +# Test with twenty4 ROM configuration +./dof_test twenty4 --base-path /userdata/system/configs/vpinball + +# Deploy library to VPinballX installation +cp libdof.so.0.3.0 ~/configs/vpinball/VPinballX_GL-10.8.0-2070-c87ffe5-Release-linux-x64/ +``` + +### Notes: +- **Target IP**: `192.168.1.160` - Update to match your Batocera box IP +- **VPinball Path**: Adjust version string (`10.8.0-2070-c87ffe5`) to match your VPinballX build +- **Config Path**: `/userdata/system/configs/vpinball` contains DOF configuration files +- **Architecture**: Batocera typically runs x64 architecture on PC hardware \ No newline at end of file diff --git a/README.md b/README.md index ce8fe71..68fb404 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This library is currently used by [Visual Pinball Standalone](https://github.com - **[Pinscape Pico](https://github.com/mjrgh/PinscapePico)** - RP2040-based version with enhanced features - **[TeensyStripController](https://github.com/DirectOutput/TeensyStripController)** - Teensy based WS2812 LED strip controller - **[WemosD1MPStripController](https://github.com/aetios50/PincabLedStrip)** - Wemos D1 Mini Pro based WS2812 LED strip controller +- **[PacLED64](https://www.ultimarc.com/output/led-and-output-controllers/pacled64/)** - Ultimarc's 64-output LED controller with PWM support ### **Implemented & Ready To Test** - **LedWiz** - Classic 32-output controller @@ -31,9 +32,9 @@ This library is currently used by [Visual Pinball Standalone](https://github.com - **PinOne** - Cleveland Software Design controller with 63 outputs - **FTDI Controllers** - FT245R bitbang controllers - **WS2811/WS2812 LED Strips** - Addressable LED strip support +- **PAC Controllers** (PacDrive, PacUIO) - Cross-platform libusb implementation ### **Not Implemented** -- **PAC Controllers** (PacDrive, PacLed64, PacUIO) - *Windows DLL dependencies* - **SSF Controllers** - *Audio-based feedback systems* - **Philips Hue** - *Smart lighting integration* @@ -154,6 +155,10 @@ cmake -DPLATFORM=android -DARCH=arm64-v8a -DCMAKE_BUILD_TYPE=Release -B build cmake --build build ``` +## Acknowledgments: + +PAC controller USB communication protocols were figured out with significant help from the [Ultimarc-linux](https://github.com/katie-snow/Ultimarc-linux) repository by @katie-snow. This project provided essential insights into the libusb control transfer implementation for PacLed64, PacDrive, and PacUIO devices. + ## Configuration: For custom setups, create a `CabinetConfig.xml` file in your DOF config directory: diff --git a/include/DOF/DOF.h b/include/DOF/DOF.h index 719ac84..e5822d7 100644 --- a/include/DOF/DOF.h +++ b/include/DOF/DOF.h @@ -1,7 +1,7 @@ #pragma once #define LIBDOF_VERSION_MAJOR 0 // X Digits -#define LIBDOF_VERSION_MINOR 3 // Max 2 Digits +#define LIBDOF_VERSION_MINOR 4 // Max 2 Digits #define LIBDOF_VERSION_PATCH 0 // Max 2 Digits #define _LIBDOF_STR(x) #x @@ -35,8 +35,9 @@ #if !((defined(__APPLE__) && ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || defined(__ANDROID__)) #define __HIDAPI__ -#define __LIBSERIALPORT__ +#define __LIBUSB__ #define __LIBFTDI__ +#define __LIBSERIALPORT__ #endif #ifdef min diff --git a/platforms/linux/aarch64/external.sh b/platforms/linux/aarch64/external.sh index 485c21e..6ae9e68 100755 --- a/platforms/linux/aarch64/external.sh +++ b/platforms/linux/aarch64/external.sh @@ -29,7 +29,8 @@ cd libusb ./configure \ --enable-shared make -j${NUM_PROCS} -cp libusb/libusb.h ../../third-party/include/ +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 cp -a libusb/.libs/libusb-1.0.{so,so.*} ../../third-party/runtime-libs/linux/aarch64/ cd .. diff --git a/platforms/linux/x64/external.sh b/platforms/linux/x64/external.sh index e5211d5..3257b96 100755 --- a/platforms/linux/x64/external.sh +++ b/platforms/linux/x64/external.sh @@ -29,7 +29,8 @@ cd libusb ./configure \ --enable-shared make -j${NUM_PROCS} -cp libusb/libusb.h ../../third-party/include/ +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 cp -a libusb/.libs/libusb-1.0.{so,so.*} ../../third-party/runtime-libs/linux/x64/ cd .. diff --git a/platforms/macos/arm64/external.sh b/platforms/macos/arm64/external.sh index 193fd2e..79d4b9f 100755 --- a/platforms/macos/arm64/external.sh +++ b/platforms/macos/arm64/external.sh @@ -31,7 +31,8 @@ cd libusb CFLAGS="-arch arm64" \ LDFLAGS="-Wl,-install_name,@rpath/libusb-1.0.dylib" make -j${NUM_PROCS} -cp libusb/libusb.h ../../third-party/include/ +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 cp -a libusb/.libs/libusb*.dylib ../../third-party/runtime-libs/macos/arm64/ cd .. diff --git a/platforms/macos/x64/external.sh b/platforms/macos/x64/external.sh index 2fd32f8..cc6334f 100755 --- a/platforms/macos/x64/external.sh +++ b/platforms/macos/x64/external.sh @@ -31,7 +31,8 @@ cd libusb CFLAGS="-arch x86_64" \ LDFLAGS="-Wl,-install_name,@rpath/libusb-1.0.dylib" make -j${NUM_PROCS} -cp libusb/libusb.h ../../third-party/include/ +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 cp -a libusb/.libs/libusb*.dylib ../../third-party/runtime-libs/macos/x64/ cd .. diff --git a/platforms/win/x64/external.sh b/platforms/win/x64/external.sh index 0da64be..7069ec7 100755 --- a/platforms/win/x64/external.sh +++ b/platforms/win/x64/external.sh @@ -37,7 +37,8 @@ msbuild.exe msvc/libusb_dll.vcxproj \ -p:TargetName=libusb64-1.0 \ -p:Platform=x64 \ -p:Configuration=Release -cp libusb/libusb.h ../../third-party/include/ +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 cp build/v143/x64/Release/libusb_dll/../dll/libusb64-1.0.lib ../../third-party/build-libs/win/x64 cp build/v143/x64/Release/libusb_dll/../dll/libusb64-1.0.dll ../../third-party/runtime-libs/win/x64 cd .. diff --git a/platforms/win/x86/external.sh b/platforms/win/x86/external.sh index f7bf07d..1d2fb41 100755 --- a/platforms/win/x86/external.sh +++ b/platforms/win/x86/external.sh @@ -35,7 +35,8 @@ cp ../../platforms/win/x86/libusb/libusb_dll.vcxproj msvc msbuild.exe msvc/libusb_dll.vcxproj \ -p:Platform=x86 \ -p:Configuration=Release -cp libusb/libusb.h ../../third-party/include/ +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 cp build/v143/Win32/Release/libusb_dll/../dll/libusb-1.0.lib ../../third-party/build-libs/win/x86 cp build/v143/Win32/Release/libusb_dll/../dll/libusb-1.0.dll ../../third-party/runtime-libs/win/x86 cd .. diff --git a/src/cab/Cabinet.cpp b/src/cab/Cabinet.cpp index 3c82c21..308551f 100644 --- a/src/cab/Cabinet.cpp +++ b/src/cab/Cabinet.cpp @@ -26,6 +26,12 @@ #include "out/dudescab/DudesCabAutoConfigurator.h" #endif +#ifdef __LIBUSB__ +#include "out/pac/PacLed64AutoConfigurator.h" +#include "out/pac/PacDriveAutoConfigurator.h" +#include "out/pac/PacUIOAutoConfigurator.h" +#endif + #ifdef __LIBFTDI__ #include "out/ftdichip/FT245RBitbangControllerAutoConfigurator.h" #endif @@ -95,6 +101,12 @@ void Cabinet::AutoConfig() items.push_back(new DudesCabAutoConfigurator()); #endif +#ifdef __LIBUSB__ + items.push_back(new PacLed64AutoConfigurator()); + items.push_back(new PacDriveAutoConfigurator()); + items.push_back(new PacUIOAutoConfigurator()); +#endif + #ifdef __LIBFTDI__ items.push_back(new FT245RBitbangControllerAutoConfigurator()); #endif diff --git a/src/cab/out/OutputControllerList.cpp b/src/cab/out/OutputControllerList.cpp index 0301a8c..871fb92 100644 --- a/src/cab/out/OutputControllerList.cpp +++ b/src/cab/out/OutputControllerList.cpp @@ -11,6 +11,16 @@ #include "dudescab/DudesCab.h" #endif +#ifdef __LIBUSB__ +#include "pac/PacLed64.h" +#include "pac/PacDrive.h" +#include "pac/PacUIO.h" +#endif + +#ifdef __LIBFTDI__ +#include "ftdichip/FT245RBitbangController.h" +#endif + #ifdef __LIBSERIALPORT__ #include "adressableledstrip/TeensyStripController.h" #include "adressableledstrip/WemosD1StripController.h" @@ -18,10 +28,6 @@ #include "pinone/PinOne.h" #endif -#ifdef __LIBFTDI__ -#include "ftdichip/FT245RBitbangController.h" -#endif - #include "dmx/ArtNet.h" namespace DOF @@ -110,6 +116,18 @@ IOutputController* OutputControllerList::CreateController(const std::string& typ else if (typeName == "DudesCab") return new DudesCab(); #endif +#ifdef __LIBUSB__ + else if (typeName == "PacLed64") + return new PacLed64(); + else if (typeName == "PacDrive") + return new PacDrive(); + else if (typeName == "PacUIO") + return new PacUIO(); +#endif +#ifdef __LIBFTDI__ + else if (typeName == "FT245RBitbangController") + return new FT245RBitbangController(); +#endif #ifdef __LIBSERIALPORT__ else if (typeName == "TeensyStripController") return new TeensyStripController(); @@ -119,10 +137,6 @@ IOutputController* OutputControllerList::CreateController(const std::string& typ return new PinControl(); else if (typeName == "PinOne") return new PinOne(); -#endif -#ifdef __LIBFTDI__ - else if (typeName == "FT245RBitbangController") - return new FT245RBitbangController(); #endif else if (typeName == "ArtNet") return new ArtNet(); diff --git a/src/cab/out/dudescab/DudesCab.cpp b/src/cab/out/dudescab/DudesCab.cpp index b168ab3..7645ccc 100644 --- a/src/cab/out/dudescab/DudesCab.cpp +++ b/src/cab/out/dudescab/DudesCab.cpp @@ -7,9 +7,7 @@ #include #include -#ifdef __HIDAPI__ #include -#endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -197,7 +195,6 @@ std::vector DudesCab::FindDevices() { std::vector devices; -#ifdef __HIDAPI__ struct hid_device_info* devs = hid_enumerate(VendorID, ProductID); struct hid_device_info* cur_dev = devs; @@ -265,7 +262,8 @@ std::vector DudesCab::FindDevices() } hid_free_enumeration(devs); -#endif + + Log::Write(StringExtensions::Build("DudesCab device scan found {0} devices", std::to_string(devices.size()))); return devices; } @@ -284,7 +282,6 @@ DudesCab::Device::Device(const std::string& path, const std::string& name, const , PwmExtensionsMask(0) , m_hidDevice(nullptr) { -#ifdef __HIDAPI__ m_hidDevice = hid_open_path(path.c_str()); if (m_hidDevice) { @@ -353,17 +350,14 @@ DudesCab::Device::Device(const std::string& path, const std::string& name, const } } } -#endif } DudesCab::Device::~Device() { -#ifdef __HIDAPI__ if (m_hidDevice) { hid_close(m_hidDevice); } -#endif } std::string DudesCab::Device::ToString() const { return StringExtensions::Build("{0} (unit {1})", m_name, std::to_string(m_unitNo)); } @@ -372,7 +366,6 @@ std::vector DudesCab::Device::ReadUSB() { std::vector result; -#ifdef __HIDAPI__ if (m_hidDevice) { uint8_t buffer[65]; @@ -382,20 +375,17 @@ std::vector DudesCab::Device::ReadUSB() result.assign(buffer, buffer + bytes_read); } } -#endif return result; } bool DudesCab::Device::WriteUSB(const std::vector& data) { -#ifdef __HIDAPI__ if (m_hidDevice && !data.empty()) { int result = hid_write(m_hidDevice, data.data(), data.size()); return result >= 0; } -#endif return false; } @@ -403,7 +393,6 @@ void DudesCab::Device::AllOff() { SendCommand(RT_PWM_ALLOFF); } void DudesCab::Device::SendCommand(HIDReportType command, const std::vector& parameters) { -#ifdef __HIDAPI__ if (!m_hidDevice) return; @@ -433,7 +422,6 @@ void DudesCab::Device::SendCommand(HIDReportType command, const std::vector #include -#ifdef __HIDAPI__ #include -#endif namespace DOF { @@ -79,11 +77,7 @@ class DudesCab : public OutputControllerFlexCompleteBase int m_numOutputs; int m_maxExtensions; -#ifdef __HIDAPI__ hid_device* m_hidDevice; -#else - void* m_hidDevice; -#endif std::vector ReadUSB(); bool WriteUSB(const std::vector& data); diff --git a/src/cab/out/dudescab/DudesCabAutoConfigurator.cpp b/src/cab/out/dudescab/DudesCabAutoConfigurator.cpp index 489f420..2c3ecb6 100644 --- a/src/cab/out/dudescab/DudesCabAutoConfigurator.cpp +++ b/src/cab/out/dudescab/DudesCabAutoConfigurator.cpp @@ -12,6 +12,8 @@ namespace DOF void DudesCabAutoConfigurator::AutoConfig(Cabinet* cabinet) { + Log::Write("DudesCab auto-configuration starting"); + std::vector devices = DudesCab::AllDevices(); for (DudesCab::Device* device : devices) diff --git a/src/cab/out/ftdichip/FT245RBitbangControllerAutoConfigurator.cpp b/src/cab/out/ftdichip/FT245RBitbangControllerAutoConfigurator.cpp index db3fae4..5be5c2a 100644 --- a/src/cab/out/ftdichip/FT245RBitbangControllerAutoConfigurator.cpp +++ b/src/cab/out/ftdichip/FT245RBitbangControllerAutoConfigurator.cpp @@ -14,6 +14,8 @@ namespace DOF void FT245RBitbangControllerAutoConfigurator::AutoConfig(Cabinet* cabinet) { + Log::Write("FTDI auto-configuration starting"); + FTDI* dummyFTDI = new FTDI(); uint32_t amountDevices = 0; std::vector deviceList; @@ -21,6 +23,8 @@ void FT245RBitbangControllerAutoConfigurator::AutoConfig(Cabinet* cabinet) FTDI::FT_STATUS status = dummyFTDI->GetNumberOfDevices(amountDevices); delete dummyFTDI; + Log::Write(StringExtensions::Build("FTDI device scan found {0} devices", std::to_string(amountDevices))); + if (status != FTDI::FT_OK || amountDevices == 0) { return; diff --git a/src/cab/out/lw/LedWizAutoConfigurator.cpp b/src/cab/out/lw/LedWizAutoConfigurator.cpp index 8df36d3..35c5350 100644 --- a/src/cab/out/lw/LedWizAutoConfigurator.cpp +++ b/src/cab/out/lw/LedWizAutoConfigurator.cpp @@ -17,6 +17,8 @@ LedWizAutoConfigurator::~LedWizAutoConfigurator() { } void LedWizAutoConfigurator::AutoConfig(Cabinet* cabinet) { + Log::Write("LedWiz auto-configuration starting"); + std::vector preconfigured; for (IOutputController* oc : *cabinet->GetOutputControllers()) { diff --git a/src/cab/out/pac/PacDrive.cpp b/src/cab/out/pac/PacDrive.cpp new file mode 100644 index 0000000..9a372fb --- /dev/null +++ b/src/cab/out/pac/PacDrive.cpp @@ -0,0 +1,271 @@ +#include "PacDrive.h" +#include "PacDriveSingleton.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" +#include "../../Cabinet.h" +#include "../Output.h" +#include "../../../cab/CabinetOwner.h" + +#include +#include +#include + +namespace DOF +{ + +PacDrive::PacDriveUnit* PacDrive::s_pacDriveInstance = nullptr; + +PacDrive::PacDrive() { SetName("PacDrive"); } + +PacDrive::~PacDrive() { Finish(); } + +void PacDrive::Init(Cabinet* cabinet) +{ + AddOutputs(); + + if (!s_pacDriveInstance) + { + s_pacDriveInstance = new PacDriveUnit(); + } + + s_pacDriveInstance->Init(cabinet); + Log::Write("PacDrive initialized and updater thread started."); +} + +void PacDrive::Finish() +{ + if (s_pacDriveInstance) + { + s_pacDriveInstance->Finish(); + s_pacDriveInstance->ShutdownLighting(); + } + Log::Write("PacDrive finished and updater thread stopped."); +} + +void PacDrive::Update() +{ + if (s_pacDriveInstance) + { + s_pacDriveInstance->TriggerPacDriveUpdaterThread(); + } +} + +void PacDrive::AddOutputs() +{ + OutputList* outputs = GetOutputs(); + for (int i = 1; i <= 16; i++) + { + bool found = false; + for (const auto& output : *outputs) + { + if (output->GetNumber() == i) + { + found = true; + break; + } + } + + if (!found) + { + Output* newOutput = new Output(); + newOutput->SetName(StringExtensions::Build("{0}.{1:00}", GetName(), std::to_string(i))); + newOutput->SetNumber(i); + outputs->push_back(newOutput); + } + } +} + +void PacDrive::OnOutputValueChanged(IOutput* output) +{ + IOutput* on = output; + + if (!on || on->GetNumber() < 1 || on->GetNumber() > 16) + { + Log::Exception( + StringExtensions::Build("PacDrive output numbers must be in the range of 1-16. The supplied output number {0} is out of range.", std::to_string(on ? on->GetNumber() : -1))); + return; + } + + if (s_pacDriveInstance) + { + s_pacDriveInstance->UpdateValue(on); + } +} + +tinyxml2::XMLElement* PacDrive::ToXml(tinyxml2::XMLDocument& doc) const +{ + tinyxml2::XMLElement* element = doc.NewElement(GetXmlElementName().c_str()); + + if (!GetName().empty()) + element->SetAttribute("Name", GetName().c_str()); + + return element; +} + +bool PacDrive::FromXml(const tinyxml2::XMLElement* element) +{ + if (!element) + return false; + + const char* name = element->Attribute("Name"); + if (name) + SetName(name); + + return true; +} + +PacDrive::PacDriveUnit::PacDriveUnit() + : m_newValue(0) + , m_currentValue(0) + , m_updateRequired(true) + , m_index(-1) + , m_keepPacDriveUpdaterAlive(false) + , m_triggerUpdate(false) +{ +} + +PacDrive::PacDriveUnit::~PacDriveUnit() { Finish(); } + +void PacDrive::PacDriveUnit::Init(Cabinet* cabinet) +{ + m_index = PacDriveSingleton::GetInstance().PacDriveGetIndex(); + if (m_index >= 0) + { + StartPacDriveUpdaterThread(); + } +} + +void PacDrive::PacDriveUnit::Finish() +{ + TerminatePacDriveUpdaterThread(); + ShutdownLighting(); +} + +void PacDrive::PacDriveUnit::UpdateValue(IOutput* output) +{ + if (!output || output->GetNumber() < 1 || output->GetNumber() > 16) + return; + + int zeroBasedOutputNumber = output->GetNumber() - 1; + uint16_t mask = static_cast(1 << zeroBasedOutputNumber); + + std::lock_guard lock(m_valueChangeLocker); + + if (output->GetOutput() != 0) + { + m_newValue |= mask; + } + else + { + m_newValue &= static_cast(~mask); + } + m_updateRequired = true; +} + +void PacDrive::PacDriveUnit::TriggerPacDriveUpdaterThread() +{ + { + std::lock_guard lock(m_pacDriveUpdaterThreadLocker); + m_triggerUpdate = true; + } + m_updateCondition.notify_one(); +} + +void PacDrive::PacDriveUnit::ShutdownLighting() +{ + if (m_index >= 0) + { + PacDriveSetLEDStates(0); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + +void PacDrive::PacDriveUnit::StartPacDriveUpdaterThread() +{ + std::lock_guard lock(m_pacDriveUpdaterThreadLocker); + if (!IsUpdaterThreadAlive()) + { + m_keepPacDriveUpdaterAlive = true; + m_pacDriveUpdater = std::thread(&PacDriveUnit::PacDriveUpdaterDoIt, this); + } +} + +void PacDrive::PacDriveUnit::TerminatePacDriveUpdaterThread() +{ + { + std::lock_guard lock(m_pacDriveUpdaterThreadLocker); + m_keepPacDriveUpdaterAlive = false; + m_triggerUpdate = true; + } + m_updateCondition.notify_all(); + + if (m_pacDriveUpdater.joinable()) + { + m_pacDriveUpdater.join(); + } +} + +void PacDrive::PacDriveUnit::PacDriveUpdaterDoIt() +{ + int failCount = 0; + while (m_keepPacDriveUpdaterAlive) + { + try + { + if (m_index >= 0) + { + SendPacDriveUpdate(); + } + failCount = 0; + } + catch (const std::exception& e) + { + Log::Exception(StringExtensions::Build("Error occurred when updating PacDrive: {0}", e.what())); + failCount++; + + if (failCount > MAX_UPDATE_FAIL_COUNT) + { + Log::Exception(StringExtensions::Build("More than {0} consecutive updates failed for PacDrive. Updater thread will terminate.", std::to_string(MAX_UPDATE_FAIL_COUNT))); + m_keepPacDriveUpdaterAlive = false; + } + } + + if (m_keepPacDriveUpdaterAlive) + { + std::unique_lock lock(m_pacDriveUpdaterThreadLocker); + m_updateCondition.wait_for(lock, std::chrono::milliseconds(50), [this] { return m_triggerUpdate.load() || !m_keepPacDriveUpdaterAlive; }); + } + m_triggerUpdate = false; + } +} + +void PacDrive::PacDriveUnit::SendPacDriveUpdate() +{ + if (m_index < 0) + return; + + std::lock_guard updateLock(m_pacDriveUpdateLocker); + std::lock_guard valueLock(m_valueChangeLocker); + + if (!m_updateRequired) + return; + + CopyNewToCurrent(); + m_updateRequired = false; + + PacDriveSetLEDStates(m_currentValue); +} + +void PacDrive::PacDriveUnit::CopyNewToCurrent() { m_currentValue = m_newValue; } + +bool PacDrive::PacDriveUnit::IsUpdaterThreadAlive() const { return m_pacDriveUpdater.joinable(); } + +bool PacDrive::PacDriveUnit::PacDriveSetLEDStates(uint16_t data) +{ + if (m_index < 0) + return false; + + return PacDriveSingleton::GetInstance().PacDriveUHIDSetLEDStates(m_index, data); +} + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacDrive.h b/src/cab/out/pac/PacDrive.h new file mode 100644 index 0000000..f5cfbe7 --- /dev/null +++ b/src/cab/out/pac/PacDrive.h @@ -0,0 +1,79 @@ +#pragma once + +#include "../OutputControllerBase.h" +#include +#include +#include +#include +#include + +namespace DOF +{ + +class Cabinet; + +class PacDrive : public OutputControllerBase +{ +public: + PacDrive(); + virtual ~PacDrive(); + + virtual void Init(Cabinet* cabinet) override; + virtual void Finish() override; + virtual void Update() override; + + virtual tinyxml2::XMLElement* ToXml(tinyxml2::XMLDocument& doc) const override; + virtual bool FromXml(const tinyxml2::XMLElement* element) override; + virtual std::string GetXmlElementName() const override { return "PacDrive"; } + +protected: + virtual void OnOutputValueChanged(IOutput* output) override; + +private: + class PacDriveUnit; + static PacDriveUnit* s_pacDriveInstance; + + void AddOutputs(); + + class PacDriveUnit + { + public: + PacDriveUnit(); + ~PacDriveUnit(); + + void Init(Cabinet* cabinet); + void Finish(); + void UpdateValue(IOutput* output); + void TriggerPacDriveUpdaterThread(); + void ShutdownLighting(); + + bool GetUpdateRequired() const { return m_updateRequired; } + + private: + static const int MAX_UPDATE_FAIL_COUNT = 5; + + uint16_t m_newValue; + uint16_t m_currentValue; + std::atomic m_updateRequired; + int m_index; + + std::mutex m_pacDriveUpdateLocker; + std::mutex m_valueChangeLocker; + + std::thread m_pacDriveUpdater; + std::atomic m_keepPacDriveUpdaterAlive; + std::mutex m_pacDriveUpdaterThreadLocker; + std::condition_variable m_updateCondition; + std::atomic m_triggerUpdate; + + void StartPacDriveUpdaterThread(); + void TerminatePacDriveUpdaterThread(); + void PacDriveUpdaterDoIt(); + void SendPacDriveUpdate(); + void CopyNewToCurrent(); + bool IsUpdaterThreadAlive() const; + bool PacDriveSetLEDStates(uint16_t data); + }; +}; + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacDriveAutoConfigurator.cpp b/src/cab/out/pac/PacDriveAutoConfigurator.cpp new file mode 100644 index 0000000..b671261 --- /dev/null +++ b/src/cab/out/pac/PacDriveAutoConfigurator.cpp @@ -0,0 +1,83 @@ +#include "PacDriveAutoConfigurator.h" +#include "PacDrive.h" +#include "PacDriveSingleton.h" +#include "../../Cabinet.h" +#include "../OutputControllerList.h" +#include "../../toys/ToyList.h" +#include "../../toys/lwequivalent/LedWizEquivalent.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" +#include + +namespace DOF +{ + +void PacDriveAutoConfigurator::AutoConfig(Cabinet* cabinet) +{ + if (!cabinet) + return; + + Log::Write("PacDrive auto-configuration starting"); + + int index = PacDriveSingleton::GetInstance().PacDriveGetIndex(); + + if (index >= 0) + { + bool found = false; + for (IOutputController* oc : *cabinet->GetOutputControllers()) + { + PacDrive* pacDrive = dynamic_cast(oc); + if (pacDrive) + { + found = true; + break; + } + } + + if (!found) + { + PacDrive* pacDrive = new PacDrive(); + + if (!cabinet->GetOutputControllers()->Contains(pacDrive->GetName())) + { + cabinet->GetOutputControllers()->push_back(pacDrive); + + Log::Write("Detected and added PacDrive"); + + bool toyFound = false; + for (IToy* toy : *cabinet->GetToys()) + { + LedWizEquivalent* lwe = dynamic_cast(toy); + if (lwe && lwe->GetLedWizNumber() == 19) + { + toyFound = true; + break; + } + } + + if (!toyFound) + { + LedWizEquivalent* lwe = new LedWizEquivalent(); + lwe->SetLedWizNumber(19); + lwe->SetName(StringExtensions::Build("{0} Equivalent", pacDrive->GetName())); + + for (int i = 1; i <= 16; i++) + { + LedWizEquivalentOutput* lweo = new LedWizEquivalentOutput(); + lweo->SetOutputName(StringExtensions::Build("{0}.{1:00}", pacDrive->GetName(), std::to_string(i))); + lweo->SetLedWizEquivalentOutputNumber(i); + lwe->GetOutputs().AddOutput(lweo); + } + + if (!cabinet->GetToys()->Contains(lwe->GetName())) + { + cabinet->GetToys()->AddToy(lwe); + Log::Write(StringExtensions::Build("Added LedwizEquivalent Nr. {0} with name {1} for PacDrive", std::to_string(lwe->GetLedWizNumber()), lwe->GetName())); + } + } + } + } + } +} + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacDriveAutoConfigurator.h b/src/cab/out/pac/PacDriveAutoConfigurator.h new file mode 100644 index 0000000..0bbc951 --- /dev/null +++ b/src/cab/out/pac/PacDriveAutoConfigurator.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../IAutoConfigOutputController.h" + +namespace DOF +{ + +class Cabinet; + +class PacDriveAutoConfigurator : public IAutoConfigOutputController +{ +public: + PacDriveAutoConfigurator() = default; + virtual ~PacDriveAutoConfigurator() = default; + + virtual void AutoConfig(Cabinet* cabinet) override; +}; + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacDriveSingleton.cpp b/src/cab/out/pac/PacDriveSingleton.cpp new file mode 100644 index 0000000..082ddd3 --- /dev/null +++ b/src/cab/out/pac/PacDriveSingleton.cpp @@ -0,0 +1,583 @@ +#include "PacDriveSingleton.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" +#include +#include + +namespace DOF +{ + +PacDriveSingleton& PacDriveSingleton::GetInstance() +{ + static PacDriveSingleton instance; + return instance; +} + +PacDriveSingleton::PacDriveSingleton() + : m_numDevices(0) + , m_libusbContext(nullptr) +{ + Initialize(); +} + +PacDriveSingleton::~PacDriveSingleton() { Shutdown(); } + +void PacDriveSingleton::Initialize() +{ + int result = libusb_init(&m_libusbContext); + if (result < 0) + { + Log::Exception(StringExtensions::Build("Failed to initialize libusb: {0}", std::to_string(result))); + return; + } + + EnumerateDevices(); +} + +void PacDriveSingleton::Shutdown() +{ + std::lock_guard lock(m_hidDevicesMutex); + for (auto& pair : m_hidDevices) + { + if (pair.second) + { + hid_close(pair.second); + } + } + m_hidDevices.clear(); + + { + std::lock_guard usbLock(m_usbDevicesMutex); + for (auto& pair : m_usbDevices) + { + if (pair.second) + { + libusb_close(pair.second); + } + } + m_usbDevices.clear(); + + if (m_libusbContext) + { + libusb_exit(m_libusbContext); + m_libusbContext = nullptr; + } + } +} + +void PacDriveSingleton::EnumerateDevices() +{ + m_devices.clear(); + + hid_device_info* devices = hid_enumerate(0x0, 0x0); + hid_device_info* currentDevice = devices; + + int index = 0; + while (currentDevice) + { + DeviceInfo info; + info.vendorId = currentDevice->vendor_id; + info.productId = currentDevice->product_id; + info.path = currentDevice->path ? currentDevice->path : ""; + if (currentDevice->serial_number) + { + std::wstring wserial(currentDevice->serial_number); + info.serial = std::string(wserial.begin(), wserial.end()); + } + else + { + info.serial = ""; + } + + if (IsPacLed64Device(info.vendorId, info.productId)) + { + info.type = DeviceType::PacLED64; + info.deviceId = ExtractPacLed64Id(info.productId); + } + else if (IsPacDriveDevice(info.vendorId, info.productId)) + { + info.type = DeviceType::PacDrive; + info.deviceId = 1; + } + else if (IsPacUIODevice(info.vendorId, info.productId)) + { + info.type = DeviceType::IPACIO; + info.deviceId = ExtractPacUIOId(info.productId); + } + else + { + info.type = DeviceType::Unknown; + info.deviceId = -1; + } + + m_devices.push_back(info); + currentDevice = currentDevice->next; + index++; + } + + hid_free_enumeration(devices); + + libusb_device** usbDevices; + ssize_t deviceCount = libusb_get_device_list(m_libusbContext, &usbDevices); + + if (deviceCount > 0) + { + for (ssize_t i = 0; i < deviceCount; i++) + { + libusb_device* device = usbDevices[i]; + libusb_device_descriptor desc; + + if (libusb_get_device_descriptor(device, &desc) == 0) + { + if (IsPacLed64Device(desc.idVendor, desc.idProduct)) + { + bool found = false; + for (const auto& existing : m_devices) + { + if (existing.vendorId == desc.idVendor && existing.productId == desc.idProduct && existing.type == DeviceType::PacLED64) + { + found = true; + break; + } + } + + if (!found) + { + DeviceInfo info; + info.type = DeviceType::PacLED64; + info.vendorId = desc.idVendor; + info.productId = desc.idProduct; + info.deviceId = ExtractPacLed64Id(desc.idProduct); + info.path = StringExtensions::Build("libusb:{0}:{1}", std::to_string(libusb_get_bus_number(device)), std::to_string(libusb_get_device_address(device))); + info.serial = ""; + + m_devices.push_back(info); + } + } + } + } + } + + libusb_free_device_list(usbDevices, 1); + + m_numDevices = static_cast(m_devices.size()); +} + +bool PacDriveSingleton::IsPacLed64Device(uint16_t vendorId, uint16_t productId) { return (vendorId == 0xD209) && (productId >= 0x1401) && (productId <= 0x1404); } + +int PacDriveSingleton::ExtractPacLed64Id(uint16_t productId) +{ + if (productId >= 0x1401 && productId <= 0x1404) + { + return static_cast(productId - 0x1401 + 1); + } + return -1; +} + +std::vector PacDriveSingleton::PacLed64GetIdList() +{ + std::vector idList; + + EnumerateDevices(); + + for (const auto& device : m_devices) + { + if (device.type == DeviceType::PacLED64 && device.deviceId > 0) + { + idList.push_back(device.deviceId); + } + } + + return idList; +} + +int PacDriveSingleton::PacLed64GetIndexForDeviceId(int id) +{ + for (size_t i = 0; i < m_devices.size(); i++) + { + if (m_devices[i].type == DeviceType::PacLED64 && m_devices[i].deviceId == id) + { + return static_cast(i); + } + } + return -1; +} + +int PacDriveSingleton::PacLed64GetDeviceId(int index) +{ + if (index >= 0 && index < static_cast(m_devices.size())) + { + if (m_devices[index].type == DeviceType::PacLED64) + { + return m_devices[index].deviceId; + } + } + return -1; +} + +PacDriveSingleton::DeviceType PacDriveSingleton::GetDeviceType(int index) +{ + if (index >= 0 && index < static_cast(m_devices.size())) + { + return m_devices[index].type; + } + return DeviceType::Unknown; +} + +std::string PacDriveSingleton::GetDevicePath(int index) +{ + if (index >= 0 && index < static_cast(m_devices.size())) + { + return m_devices[index].path; + } + return ""; +} + +bool PacDriveSingleton::IsPacDriveDevice(uint16_t vendorId, uint16_t productId) { return (vendorId == 0xD209) && (productId == 0x1500); } + +int PacDriveSingleton::PacDriveGetIndex() +{ + for (size_t i = 0; i < m_devices.size(); i++) + { + if (m_devices[i].type == DeviceType::PacDrive) + { + return static_cast(i); + } + } + return -1; +} + +bool PacDriveSingleton::PacDriveUHIDSetLEDStates(int index, uint16_t data) +{ + if (index < 0 || index >= static_cast(m_devices.size())) + return false; + + const DeviceInfo& device = m_devices[index]; + if (device.type != DeviceType::PacDrive) + return false; + + libusb_device_handle* usbDevice = GetUsbDevice(index); + if (!usbDevice) + return false; + + uint8_t message[4] = { 0, 0, 0, 0 }; + message[2] = static_cast((data >> 8) & 0xFF); + message[3] = static_cast(data & 0xFF); + + int result = libusb_control_transfer(usbDevice, 0x21, 9, 0x0200, 0, message, 4, 2000); + + return (result == 4); +} + +int PacDriveSingleton::PacUIOGetIndexForDeviceId(int id) +{ + for (size_t i = 0; i < m_devices.size(); i++) + { + if (m_devices[i].type == DeviceType::IPACIO && m_devices[i].deviceId == id) + { + return static_cast(i); + } + } + return -1; +} + +bool PacDriveSingleton::IsPacUIODevice(uint16_t vendorId, uint16_t productId) { return (vendorId == 0xD209) && (productId >= 0x0410) && (productId <= 0x0411); } + +int PacDriveSingleton::ExtractPacUIOId(uint16_t productId) +{ + if (productId >= 0x0410 && productId <= 0x0411) + { + return static_cast(productId - 0x0410); + } + return -1; +} + +int PacDriveSingleton::PacUIOGetDeviceId(int index) +{ + if (index >= 0 && index < static_cast(m_devices.size())) + { + if (m_devices[index].type == DeviceType::IPACIO) + { + return m_devices[index].deviceId; + } + } + return -1; +} + +std::vector PacDriveSingleton::PacUIOGetIdList() +{ + std::vector idList; + + EnumerateDevices(); + + for (const auto& device : m_devices) + { + if (device.type == DeviceType::IPACIO && device.deviceId >= 0) + { + idList.push_back(device.deviceId); + } + } + + return idList; +} + +bool PacDriveSingleton::PacLed64SetLEDIntensities(int index, const uint8_t* data) +{ + if (index < 0 || index >= static_cast(m_devices.size())) + return false; + + const DeviceInfo& device = m_devices[index]; + if (device.type != DeviceType::PacLED64 && device.type != DeviceType::IPACIO) + return false; + + libusb_device_handle* usbDevice = GetUsbDevice(index); + if (!usbDevice) + return false; + + bool success = true; + int maxLeds = (device.type == DeviceType::PacLED64) ? 64 : 96; + + for (int i = 0; i < maxLeds; i++) + { + uint8_t ledData[2]; + ledData[0] = static_cast(i); + ledData[1] = data[i]; + + int result = libusb_control_transfer(usbDevice, 0x21, 9, 0x0200, 0, ledData, 2, 2000); + if (result < 0) + { + success = false; + } + } + + return success; +} + +bool PacDriveSingleton::PacLed64SetLEDIntensity(int index, int port, uint8_t intensity) +{ + if (index < 0 || index >= static_cast(m_devices.size())) + return false; + + const DeviceInfo& device = m_devices[index]; + if (device.type != DeviceType::PacLED64 && device.type != DeviceType::IPACIO) + return false; + + int maxLeds = (device.type == DeviceType::PacLED64) ? 64 : 96; + if (port < 0 || port >= maxLeds) + return false; + + libusb_device_handle* usbDevice = GetUsbDevice(index); + if (!usbDevice) + return false; + + uint8_t data[2]; + data[0] = static_cast(port); + data[1] = intensity; + + int result = libusb_control_transfer(usbDevice, 0x21, 9, 0x0200, 0, data, 2, 2000); + + return (result >= 0); +} + +bool PacDriveSingleton::PacLed64SetLEDStates(int index, int group, uint8_t data) +{ + if (index < 0 || index >= static_cast(m_devices.size())) + return false; + + const DeviceInfo& device = m_devices[index]; + if (device.type != DeviceType::PacLED64 && device.type != DeviceType::IPACIO) + return false; + + libusb_device_handle* usbDevice = GetUsbDevice(index); + if (!usbDevice) + return false; + + bool success = true; + + // Special case: group 0 means turn off all LEDs + if (group == 0) + { + int maxLeds = (device.type == DeviceType::PacLED64) ? 64 : 96; + for (int ledIndex = 0; ledIndex < maxLeds; ledIndex++) + { + uint8_t ledData[2]; + ledData[0] = static_cast(ledIndex); + ledData[1] = 0; // intensity = 0 + + int result = libusb_control_transfer(usbDevice, 0x21, 9, 0x0200, 0, ledData, 2, 2000); + if (result < 0) + { + success = false; + } + } + } + else + { + for (int bit = 0; bit < 8; bit++) + { + int ledIndex = (group - 1) * 8 + bit; + int maxLeds = (device.type == DeviceType::PacLED64) ? 64 : 96; + + if (ledIndex < maxLeds) + { + uint8_t intensity = (data & (1 << bit)) ? 255 : 0; + uint8_t ledData[2]; + ledData[0] = static_cast(ledIndex); + ledData[1] = intensity; + + int result = libusb_control_transfer(usbDevice, 0x21, 9, 0x0200, 0, ledData, 2, 2000); + if (result < 0) + { + success = false; + } + } + } + } + + return success; +} + +bool PacDriveSingleton::PacLed64SetLEDFadeTime(int index, uint8_t fadeTime) +{ + if (index < 0 || index >= static_cast(m_devices.size())) + return false; + + const DeviceInfo& device = m_devices[index]; + if (device.type != DeviceType::PacLED64 && device.type != DeviceType::IPACIO) + return false; + + libusb_device_handle* usbDevice = GetUsbDevice(index); + if (!usbDevice) + return false; + + uint8_t data[2]; + data[0] = 0x40; // Fade command base + data[1] = fadeTime + 4; + + int result = libusb_control_transfer(usbDevice, 0x21, 9, 0x0300, 0, data, 2, 2000); + + return (result >= 0); +} + +hid_device* PacDriveSingleton::GetHidDevice(int index) +{ + std::lock_guard lock(m_hidDevicesMutex); + + auto it = m_hidDevices.find(index); + if (it != m_hidDevices.end()) + { + return it->second; + } + + OpenHidDevice(index); + + it = m_hidDevices.find(index); + if (it != m_hidDevices.end()) + { + return it->second; + } + + return nullptr; +} + +void PacDriveSingleton::OpenHidDevice(int index) +{ + if (index < 0 || index >= static_cast(m_devices.size())) + return; + + const DeviceInfo& device = m_devices[index]; + + if (m_hidDevices.find(index) != m_hidDevices.end()) + return; + + hid_device* hidDevice = hid_open_path(device.path.c_str()); + if (hidDevice) + { + m_hidDevices[index] = hidDevice; + } +} + +void PacDriveSingleton::CloseHidDevice(int index) +{ + std::lock_guard lock(m_hidDevicesMutex); + + auto it = m_hidDevices.find(index); + if (it != m_hidDevices.end()) + { + if (it->second) + { + hid_close(it->second); + } + m_hidDevices.erase(it); + } +} + +libusb_device_handle* PacDriveSingleton::GetUsbDevice(int index) +{ + std::lock_guard lock(m_usbDevicesMutex); + + auto it = m_usbDevices.find(index); + if (it != m_usbDevices.end()) + { + return it->second; + } + + OpenUsbDevice(index); + + it = m_usbDevices.find(index); + if (it != m_usbDevices.end()) + { + return it->second; + } + + return nullptr; +} + +void PacDriveSingleton::OpenUsbDevice(int index) +{ + if (index < 0 || index >= static_cast(m_devices.size())) + return; + + const DeviceInfo& device = m_devices[index]; + + if (m_usbDevices.find(index) != m_usbDevices.end()) + return; + + libusb_device_handle* handle = libusb_open_device_with_vid_pid(m_libusbContext, device.vendorId, device.productId); + if (handle) + { + if (libusb_kernel_driver_active(handle, 0) == 1) + { + libusb_detach_kernel_driver(handle, 0); + } + + int result = libusb_claim_interface(handle, 0); + if (result == 0) + { + m_usbDevices[index] = handle; + } + else + { + libusb_close(handle); + } + } +} + +void PacDriveSingleton::CloseUsbDevice(int index) +{ + std::lock_guard lock(m_usbDevicesMutex); + + auto it = m_usbDevices.find(index); + if (it != m_usbDevices.end()) + { + if (it->second) + { + libusb_release_interface(it->second, 0); + libusb_attach_kernel_driver(it->second, 0); + libusb_close(it->second); + } + m_usbDevices.erase(it); + } +} + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacDriveSingleton.h b/src/cab/out/pac/PacDriveSingleton.h new file mode 100644 index 0000000..84f1ded --- /dev/null +++ b/src/cab/out/pac/PacDriveSingleton.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace DOF +{ + +class PacDriveSingleton +{ +public: + enum DeviceType + { + Unknown, + PacDrive, + UHID, + PacLED64, + ServoStik, + USBButton, + NanoLED, + IPACIO + }; + + enum FlashSpeed : uint8_t + { + AlwaysOn = 0, + Seconds_2 = 1, + Seconds_1 = 2, + Seconds_0_5 = 3 + }; + + static PacDriveSingleton& GetInstance(); + + std::vector PacLed64GetIdList(); + int PacLed64GetIndexForDeviceId(int id); + int PacLed64GetDeviceId(int index); + bool PacLed64SetLEDIntensities(int index, const uint8_t* data); + bool PacLed64SetLEDIntensity(int index, int port, uint8_t intensity); + bool PacLed64SetLEDStates(int index, int group, uint8_t data); + bool PacLed64SetLEDFadeTime(int index, uint8_t fadeTime); + + int PacDriveGetIndex(); + bool PacDriveUHIDSetLEDStates(int index, uint16_t data); + + std::vector PacUIOGetIdList(); + int PacUIOGetIndexForDeviceId(int id); + int PacUIOGetDeviceId(int index); + + int GetNumDevices() const { return m_numDevices; } + DeviceType GetDeviceType(int index); + std::string GetDevicePath(int index); + + void Initialize(); + void Shutdown(); + +private: + PacDriveSingleton(); + ~PacDriveSingleton(); + + PacDriveSingleton(const PacDriveSingleton&) = delete; + PacDriveSingleton& operator=(const PacDriveSingleton&) = delete; + + struct DeviceInfo + { + DeviceType type; + int deviceId; + std::string path; + uint16_t vendorId; + uint16_t productId; + std::string serial; + }; + + std::vector m_devices; + std::map m_hidDevices; + std::mutex m_hidDevicesMutex; + int m_numDevices; + + libusb_context* m_libusbContext; + std::map m_usbDevices; + std::mutex m_usbDevicesMutex; + + void EnumerateDevices(); + bool IsPacLed64Device(uint16_t vendorId, uint16_t productId); + bool IsPacDriveDevice(uint16_t vendorId, uint16_t productId); + bool IsPacUIODevice(uint16_t vendorId, uint16_t productId); + int ExtractPacLed64Id(uint16_t productId); + int ExtractPacUIOId(uint16_t productId); + + hid_device* GetHidDevice(int index); + void OpenHidDevice(int index); + void CloseHidDevice(int index); + + libusb_device_handle* GetUsbDevice(int index); + void OpenUsbDevice(int index); + void CloseUsbDevice(int index); +}; + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacLed64.cpp b/src/cab/out/pac/PacLed64.cpp new file mode 100644 index 0000000..3727ffe --- /dev/null +++ b/src/cab/out/pac/PacLed64.cpp @@ -0,0 +1,505 @@ +#include "PacLed64.h" +#include "PacDriveSingleton.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" +#include "../../Cabinet.h" +#include "../Output.h" +#include "../../../cab/CabinetOwner.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace DOF +{ + +std::map PacLed64::s_pacLed64Units; + +PacLed64::PacLed64() + : m_id(-1) + , m_minUpdateIntervalMsSet(false) + , m_minUpdateIntervalMs(10) + , m_fullUpdateThreshold(30) +{ +} + +PacLed64::PacLed64(int id) + : PacLed64() +{ + SetId(id); +} + +PacLed64::~PacLed64() { Finish(); } + +void PacLed64::SetId(int value) +{ + if (value < 1 || value > 4) + { + Log::Exception(StringExtensions::Build("PacLed64 Ids must be between 1-4. The supplied Id {0} is out of range.", std::to_string(value))); + return; + } + + std::lock_guard lock(m_idUpdateLocker); + if (m_id != value) + { + if (GetName().empty() || GetName() == StringExtensions::Build("PacLed64 {0:0}", std::to_string(m_id))) + { + SetName(StringExtensions::Build("PacLed64 {0:0}", std::to_string(value))); + } + m_id = value; + } +} + +void PacLed64::SetMinUpdateIntervalMs(int value) +{ + m_minUpdateIntervalMs = std::clamp(value, 0, 1000); + m_minUpdateIntervalMsSet = true; +} + +void PacLed64::SetFullUpdateThreshold(int value) { m_fullUpdateThreshold = std::clamp(value, 0, 64); } + +void PacLed64::Init(Cabinet* cabinet) +{ + AddOutputs(); + + if (!m_minUpdateIntervalMsSet && cabinet && cabinet->GetOwner() && cabinet->GetOwner()->HasConfigurationSetting("PacLedDefaultMinCommandIntervalMs")) + { + std::string value = cabinet->GetOwner()->GetConfigurationSetting("PacLedDefaultMinCommandIntervalMs"); + try + { + int intervalMs = std::stoi(value); + SetMinUpdateIntervalMs(intervalMs); + } + catch (const std::exception&) + { + } + } + + // Initialize static units map if needed + static bool initialized = false; + static std::mutex initMutex; + if (!initialized) + { + std::lock_guard lock(initMutex); + if (!initialized) + { + for (int i = 1; i <= 4; i++) + { + s_pacLed64Units[i] = new PacLed64Unit(i); + } + initialized = true; + } + } + + if (s_pacLed64Units.find(m_id) != s_pacLed64Units.end()) + { + s_pacLed64Units[m_id]->SetFullUpdateThreshold(m_fullUpdateThreshold); + s_pacLed64Units[m_id]->SetMinUpdateInterval(std::chrono::milliseconds(m_minUpdateIntervalMs)); + s_pacLed64Units[m_id]->Init(cabinet); + } + + Log::Write(StringExtensions::Build("PacLed64 Id:{0} initialized and updater thread started.", std::to_string(m_id))); +} + +void PacLed64::Finish() +{ + if (s_pacLed64Units.find(m_id) != s_pacLed64Units.end()) + { + s_pacLed64Units[m_id]->Finish(); + s_pacLed64Units[m_id]->ShutdownLighting(); + } + Log::Write(StringExtensions::Build("PacLed64 Id:{0} finished and updater thread stopped.", std::to_string(m_id))); +} + +void PacLed64::Update() +{ + if (s_pacLed64Units.find(m_id) != s_pacLed64Units.end()) + { + s_pacLed64Units[m_id]->TriggerPacLed64UpdaterThread(); + } +} + +void PacLed64::AddOutputs() +{ + OutputList* outputs = GetOutputs(); + for (int i = 1; i <= 64; i++) + { + bool found = false; + for (const auto& output : *outputs) + { + if (output->GetNumber() == i) + { + found = true; + break; + } + } + + if (!found) + { + Output* newOutput = new Output(); + newOutput->SetName(StringExtensions::Build("{0}.{1:00}", GetName(), std::to_string(i))); + newOutput->SetNumber(i); + outputs->Add(newOutput); + } + } +} + +void PacLed64::OnOutputValueChanged(IOutput* output) +{ + IOutput* on = output; + + + if (!on || on->GetNumber() < 1 || on->GetNumber() > 64) + { + Log::Exception( + StringExtensions::Build("PacLed64 output numbers must be in the range of 1-64. The supplied output number {0} is out of range.", std::to_string(on ? on->GetNumber() : -1))); + return; + } + + PacLed64Unit* s = s_pacLed64Units[m_id]; + s->UpdateValue(on); +} + +tinyxml2::XMLElement* PacLed64::ToXml(tinyxml2::XMLDocument& doc) const +{ + tinyxml2::XMLElement* element = doc.NewElement(GetXmlElementName().c_str()); + + if (!GetName().empty()) + element->SetAttribute("Name", GetName().c_str()); + + element->SetAttribute("Id", m_id); + + if (m_minUpdateIntervalMsSet) + element->SetAttribute("MinUpdateIntervalMs", m_minUpdateIntervalMs); + + element->SetAttribute("FullUpdateThreshold", m_fullUpdateThreshold); + + return element; +} + +bool PacLed64::FromXml(const tinyxml2::XMLElement* element) +{ + if (!element) + return false; + + const char* name = element->Attribute("Name"); + if (name) + SetName(name); + + int id = element->IntAttribute("Id", -1); + if (id != -1) + SetId(id); + + int minInterval = element->IntAttribute("MinUpdateIntervalMs", -1); + if (minInterval != -1) + SetMinUpdateIntervalMs(minInterval); + + int threshold = element->IntAttribute("FullUpdateThreshold", 30); + SetFullUpdateThreshold(threshold); + + return true; +} + +// PacLed64Unit Implementation +PacLed64::PacLed64Unit::PacLed64Unit(int id) + : m_id(id) + , m_fullUpdateThreshold(30) + , m_minUpdateInterval(std::chrono::milliseconds(10)) + , m_index(-1) + , m_newValue(64, 0) + , m_currentValue(64, 0) + , m_lastValueSent(64, 0) + , m_lastStateSent(64, false) + , m_updateRequired(true) + , m_keepPacLed64UpdaterAlive(false) + , m_triggerUpdate(false) + , m_forceFullUpdate(true) +{ + std::fill(m_lastValueSent.begin(), m_lastValueSent.end(), 255); +} + +PacLed64::PacLed64Unit::~PacLed64Unit() { Finish(); } + +void PacLed64::PacLed64Unit::Init(Cabinet* cabinet) +{ + m_index = PacDriveSingleton::GetInstance().PacLed64GetIndexForDeviceId(m_id); + + if (m_index >= 0) + { + InitUnit(); + StartPacLed64UpdaterThread(); + Log::Write(StringExtensions::Build("PacLed64 device {0} initialized at index {1}", std::to_string(m_id), std::to_string(m_index))); + } + else + { + Log::Warning(StringExtensions::Build("PacLed64 device {0} not found", std::to_string(m_id))); + } +} + +void PacLed64::PacLed64Unit::Finish() +{ + TerminatePacLed64UpdaterThread(); + ShutdownLighting(); + + m_index = -1; +} + +void PacLed64::PacLed64Unit::UpdateValue(IOutput* output) +{ + if (!output || output->GetNumber() < 1 || output->GetNumber() > 64) + return; + + int zeroBasedOutputNumber = output->GetNumber() - 1; + std::lock_guard lock(m_valueChangeLocker); + + + if (m_newValue[zeroBasedOutputNumber] != output->GetOutput()) + { + m_newValue[zeroBasedOutputNumber] = output->GetOutput(); + m_updateRequired = true; + TriggerPacLed64UpdaterThread(); + } +} + +void PacLed64::PacLed64Unit::TriggerPacLed64UpdaterThread() +{ + { + std::lock_guard lock(m_pacLed64UpdaterThreadLocker); + m_triggerUpdate = true; + } + m_updateCondition.notify_one(); +} + +bool PacLed64::PacLed64Unit::IsPresent() const { return m_id >= 1 && m_id <= 4 && m_index >= 0; } + +void PacLed64::PacLed64Unit::ShutdownLighting() +{ + if (IsPresent()) + { + EnforceMinUpdateInterval(); + PacDriveSingleton::GetInstance().PacLed64SetLEDStates(0, 0, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::fill(m_lastStateSent.begin(), m_lastStateSent.end(), false); + } +} + +void PacLed64::PacLed64Unit::StartPacLed64UpdaterThread() +{ + std::lock_guard lock(m_pacLed64UpdaterThreadLocker); + if (!IsUpdaterThreadAlive()) + { + m_keepPacLed64UpdaterAlive = true; + m_pacLed64Updater = std::thread(&PacLed64Unit::PacLed64UpdaterDoIt, this); + } +} + +void PacLed64::PacLed64Unit::TerminatePacLed64UpdaterThread() +{ + { + std::lock_guard lock(m_pacLed64UpdaterThreadLocker); + m_keepPacLed64UpdaterAlive = false; + m_triggerUpdate = true; + } + m_updateCondition.notify_all(); + + if (m_pacLed64Updater.joinable()) + { + m_pacLed64Updater.join(); + } +} + +void PacLed64::PacLed64Unit::PacLed64UpdaterDoIt() +{ + try + { + ResetFadeTime(); + } + catch (const std::exception& e) + { + Log::Exception(StringExtensions::Build("Exception occurred while setting fade time for PacLed64 {0} to 0: {1}", std::to_string(m_index), e.what())); + return; + } + + int failCount = 0; + while (m_keepPacLed64UpdaterAlive) + { + try + { + if (IsPresent()) + { + SendPacLed64Update(); + } + failCount = 0; + } + catch (const std::exception& e) + { + Log::Exception(StringExtensions::Build("Error occurred when updating PacLed64 {0}: {1}", std::to_string(m_id), e.what())); + failCount++; + + if (failCount > MAX_UPDATE_FAIL_COUNT) + { + Log::Exception(StringExtensions::Build( + "More than {0} consecutive updates failed for PacLed64 {1}. Updater thread will terminate.", std::to_string(MAX_UPDATE_FAIL_COUNT), std::to_string(m_id))); + m_keepPacLed64UpdaterAlive = false; + } + } + + if (m_keepPacLed64UpdaterAlive) + { + std::unique_lock lock(m_pacLed64UpdaterThreadLocker); + m_updateCondition.wait_for(lock, std::chrono::milliseconds(50), [this] { return m_triggerUpdate.load() || !m_keepPacLed64UpdaterAlive; }); + } + m_triggerUpdate = false; + } +} + +void PacLed64::PacLed64Unit::SendPacLed64Update() +{ + if (!IsPresent()) + return; + + std::lock_guard updateLock(m_pacLed64UpdateLocker); + std::lock_guard valueLock(m_valueChangeLocker); + + if (!m_updateRequired && !m_forceFullUpdate) + return; + + CopyNewToCurrent(); + m_updateRequired = false; + + uint8_t intensityUpdatesRequired = 0; + uint8_t stateUpdatesRequired = 0; + + if (!m_forceFullUpdate) + { + for (int g = 0; g < 8; g++) + { + bool stateUpdateRequired = false; + for (int p = 0; p < 8; p++) + { + int o = (g << 3) | p; + if (m_currentValue[o] > 0) + { + if (m_currentValue[o] != m_lastValueSent[o]) + { + intensityUpdatesRequired++; + } + else if (!m_lastStateSent[o]) + { + stateUpdateRequired = true; + } + } + else if (m_lastStateSent[o]) + { + stateUpdateRequired = true; + } + } + if (stateUpdateRequired) + stateUpdatesRequired++; + } + } + + if (m_forceFullUpdate || (intensityUpdatesRequired + stateUpdatesRequired) > m_fullUpdateThreshold) + { + EnforceMinUpdateInterval(); + PacDriveSingleton::GetInstance().PacLed64SetLEDIntensities(m_index, m_currentValue.data()); + m_lastCommand = std::chrono::steady_clock::now(); + + m_lastValueSent = m_currentValue; + for (int i = 0; i < 64; i++) + { + m_lastStateSent[i] = (m_lastValueSent[i] > 0); + } + m_forceFullUpdate = false; + } + else + { + for (int g = 0; g < 8; g++) + { + int mask = 0; + bool stateUpdateRequired = false; + + for (int p = 0; p < 8; p++) + { + int o = (g << 3) | p; + if (m_currentValue[o] > 0) + { + if (m_currentValue[o] != m_lastValueSent[o]) + { + EnforceMinUpdateInterval(); + PacDriveSingleton::GetInstance().PacLed64SetLEDIntensity(m_index, o, m_currentValue[o]); + m_lastCommand = std::chrono::steady_clock::now(); + + m_lastStateSent[o] = true; + m_lastValueSent[o] = m_currentValue[o]; + } + else if (!m_lastStateSent[o]) + { + mask |= (1 << p); + stateUpdateRequired = true; + m_lastStateSent[o] = true; + } + } + else if (m_lastStateSent[o]) + { + stateUpdateRequired = true; + m_lastStateSent[o] = false; + m_lastValueSent[o] = 0; + } + } + + if (stateUpdateRequired) + { + EnforceMinUpdateInterval(); + PacDriveSingleton::GetInstance().PacLed64SetLEDStates(m_index, g + 1, static_cast(mask)); + m_lastCommand = std::chrono::steady_clock::now(); + } + } + } +} + +void PacLed64::PacLed64Unit::CopyNewToCurrent() { m_currentValue = m_newValue; } + +void PacLed64::PacLed64Unit::ResetFadeTime() +{ + EnforceMinUpdateInterval(); + PacDriveSingleton::GetInstance().PacLed64SetLEDFadeTime(m_index, 0); + m_lastCommand = std::chrono::steady_clock::now(); +} + +void PacLed64::PacLed64Unit::EnforceMinUpdateInterval() +{ + if (m_minUpdateInterval == std::chrono::milliseconds::zero()) + return; + + auto now = std::chrono::steady_clock::now(); + auto elapsed = now - m_lastCommand; + + if (elapsed < m_minUpdateInterval) + { + auto sleepTime = m_minUpdateInterval - elapsed; + if (sleepTime > std::chrono::milliseconds::zero() && sleepTime <= std::chrono::milliseconds(1000)) + { + std::this_thread::sleep_for(sleepTime); + } + } + m_lastCommand = std::chrono::steady_clock::now(); +} + +void PacLed64::PacLed64Unit::InitUnit() +{ + if (m_index >= 0) + { + std::lock_guard lock(m_valueChangeLocker); + std::fill(m_newValue.begin(), m_newValue.end(), 0); + m_updateRequired = true; + } +} + +bool PacLed64::PacLed64Unit::IsUpdaterThreadAlive() const { return m_pacLed64Updater.joinable(); } + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacLed64.h b/src/cab/out/pac/PacLed64.h new file mode 100644 index 0000000..9baa25b --- /dev/null +++ b/src/cab/out/pac/PacLed64.h @@ -0,0 +1,117 @@ +#pragma once + +#include "../OutputControllerBase.h" +#include "../IOutputController.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DOF +{ + +class Cabinet; + +class PacLed64 : public OutputControllerBase +{ +public: + PacLed64(); + PacLed64(int id); + virtual ~PacLed64(); + + int GetId() const { return m_id; } + void SetId(int value); + + int GetMinUpdateIntervalMs() const { return m_minUpdateIntervalMs; } + void SetMinUpdateIntervalMs(int value); + + int GetFullUpdateThreshold() const { return m_fullUpdateThreshold; } + void SetFullUpdateThreshold(int value); + + virtual void Init(Cabinet* cabinet) override; + virtual void Finish() override; + virtual void Update() override; + + virtual tinyxml2::XMLElement* ToXml(tinyxml2::XMLDocument& doc) const override; + virtual bool FromXml(const tinyxml2::XMLElement* element) override; + virtual std::string GetXmlElementName() const override { return "PacLed64"; } + +protected: + virtual void OnOutputValueChanged(IOutput* output) override; + +private: + class PacLed64Unit; + + std::mutex m_idUpdateLocker; + int m_id; + bool m_minUpdateIntervalMsSet; + int m_minUpdateIntervalMs; + int m_fullUpdateThreshold; + + static std::map s_pacLed64Units; + + void AddOutputs(); + + class PacLed64Unit + { + public: + PacLed64Unit(int id); + ~PacLed64Unit(); + + int GetId() const { return m_id; } + void SetFullUpdateThreshold(int value) { m_fullUpdateThreshold = value; } + void SetMinUpdateInterval(std::chrono::milliseconds interval) { m_minUpdateInterval = interval; } + + void Init(Cabinet* cabinet); + void Finish(); + void UpdateValue(IOutput* output); + void TriggerPacLed64UpdaterThread(); + bool IsPresent() const; + void ShutdownLighting(); + + bool GetUpdateRequired() const { return m_updateRequired; } + + private: + static const int MAX_UPDATE_FAIL_COUNT = 5; + + int m_id; + int m_fullUpdateThreshold; + std::chrono::milliseconds m_minUpdateInterval; + std::chrono::steady_clock::time_point m_lastCommand; + int m_index; + + std::vector m_newValue; + std::vector m_currentValue; + std::vector m_lastValueSent; + std::vector m_lastStateSent; + + std::atomic m_updateRequired; + std::mutex m_pacLed64UpdateLocker; + std::mutex m_valueChangeLocker; + + std::thread m_pacLed64Updater; + std::atomic m_keepPacLed64UpdaterAlive; + std::mutex m_pacLed64UpdaterThreadLocker; + std::condition_variable m_updateCondition; + std::atomic m_triggerUpdate; + + + void StartPacLed64UpdaterThread(); + void TerminatePacLed64UpdaterThread(); + void PacLed64UpdaterDoIt(); + void SendPacLed64Update(); + void CopyNewToCurrent(); + void ResetFadeTime(); + void EnforceMinUpdateInterval(); + void InitUnit(); + bool IsUpdaterThreadAlive() const; + + std::atomic m_forceFullUpdate; + }; +}; + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacLed64AutoConfigurator.cpp b/src/cab/out/pac/PacLed64AutoConfigurator.cpp new file mode 100644 index 0000000..d05e69b --- /dev/null +++ b/src/cab/out/pac/PacLed64AutoConfigurator.cpp @@ -0,0 +1,87 @@ +#include "PacLed64AutoConfigurator.h" +#include "PacLed64.h" +#include "PacDriveSingleton.h" +#include "../../Cabinet.h" +#include "../OutputControllerList.h" +#include "../../toys/ToyList.h" +#include "../../toys/lwequivalent/LedWizEquivalent.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" +#include + +namespace DOF +{ + +void PacLed64AutoConfigurator::AutoConfig(Cabinet* cabinet) +{ + if (!cabinet) + return; + + Log::Write("PacLed64 auto-configuration starting"); + + std::vector pacLed64Ids = PacDriveSingleton::GetInstance().PacLed64GetIdList(); + + for (int id : pacLed64Ids) + { + bool found = false; + for (IOutputController* oc : *cabinet->GetOutputControllers()) + { + PacLed64* pacLed64 = dynamic_cast(oc); + if (pacLed64 && pacLed64->GetId() == id) + { + found = true; + break; + } + } + + if (!found) + { + PacLed64* pacLed = new PacLed64(); + pacLed->SetId(id); + + if (!cabinet->GetOutputControllers()->Contains(pacLed->GetName())) + { + cabinet->GetOutputControllers()->push_back(pacLed); + + Log::Write(StringExtensions::Build("Detected and added PacLed64 Id {0} with name {1}", std::to_string(pacLed->GetId()), pacLed->GetName())); + + int ledWizNumber = pacLed->GetId() - 1 + 20; + + bool toyFound = false; + for (IToy* toy : *cabinet->GetToys()) + { + LedWizEquivalent* lwe = dynamic_cast(toy); + if (lwe && lwe->GetLedWizNumber() == ledWizNumber) + { + toyFound = true; + break; + } + } + + if (!toyFound) + { + LedWizEquivalent* lwe = new LedWizEquivalent(); + lwe->SetLedWizNumber(ledWizNumber); + lwe->SetName(StringExtensions::Build("{0} Equivalent 1", pacLed->GetName())); + + for (int i = 1; i <= 64; i++) + { + LedWizEquivalentOutput* lweo = new LedWizEquivalentOutput(); + lweo->SetOutputName(StringExtensions::Build("{0}.{1:00}", pacLed->GetName(), std::to_string(i))); + lweo->SetLedWizEquivalentOutputNumber(i); + lwe->GetOutputs().AddOutput(lweo); + } + + if (!cabinet->GetToys()->Contains(lwe->GetName())) + { + cabinet->GetToys()->AddToy(lwe); + Log::Write(StringExtensions::Build( + "Added LedwizEquivalent Nr. {0} with name {1} for PacLed64 with Id {2}", std::to_string(lwe->GetLedWizNumber()), lwe->GetName(), std::to_string(pacLed->GetId()))); + } + } + } + } + } +} + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacLed64AutoConfigurator.h b/src/cab/out/pac/PacLed64AutoConfigurator.h new file mode 100644 index 0000000..3e3d332 --- /dev/null +++ b/src/cab/out/pac/PacLed64AutoConfigurator.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../IAutoConfigOutputController.h" + +namespace DOF +{ + +class Cabinet; + +class PacLed64AutoConfigurator : public IAutoConfigOutputController +{ +public: + PacLed64AutoConfigurator() = default; + virtual ~PacLed64AutoConfigurator() = default; + + virtual void AutoConfig(Cabinet* cabinet) override; +}; + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacUIO.cpp b/src/cab/out/pac/PacUIO.cpp new file mode 100644 index 0000000..2e6ac1f --- /dev/null +++ b/src/cab/out/pac/PacUIO.cpp @@ -0,0 +1,418 @@ +#include "PacUIO.h" +#include "PacDriveSingleton.h" +#include "../../Cabinet.h" +#include "../OutputControllerList.h" +#include "../Output.h" +#include "../../toys/ToyList.h" +#include "../../toys/lwequivalent/LedWizEquivalent.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" +#include +#include + +namespace DOF +{ + +std::vector PacUIO::s_pacUIOUnits; +bool PacUIO::s_unitsInitialized = false; + +void PacUIO::InitializeUnits() +{ + if (!s_unitsInitialized) + { + for (int i = 0; i <= 2; i++) + { + s_pacUIOUnits.push_back(new PacUIOUnit(i)); + } + s_unitsInitialized = true; + } +} + +void PacUIO::SetId(int value) +{ + if (value < 0 || value > 1) + { + throw std::runtime_error(StringExtensions::Build("PacUIO Ids must be between 0-1. The supplied Id {0} is out of range.", std::to_string(value))); + } + + std::lock_guard lock(m_idUpdateLocker); + if (m_id != value) + { + if (GetName().empty() || GetName() == StringExtensions::Build("PacUIO {0:0}", std::to_string(m_id))) + { + SetName(StringExtensions::Build("PacUIO {0:0}", std::to_string(value))); + } + m_id = value; + } +} + +void PacUIO::Update() +{ + InitializeUnits(); + s_pacUIOUnits[m_id]->TriggerPacUIOUpdaterThread(); +} + +void PacUIO::Init(Cabinet* cabinet) +{ + AddOutputs(); + InitializeUnits(); + s_pacUIOUnits[m_id]->Init(cabinet); + Log::Write(StringExtensions::Build("PacUIO Id:{0} initialized and updater thread started.", std::to_string(m_id))); +} + +void PacUIO::Finish() +{ + if (s_unitsInitialized && m_id >= 0 && m_id < static_cast(s_pacUIOUnits.size())) + { + s_pacUIOUnits[m_id]->Finish(); + s_pacUIOUnits[m_id]->ShutdownLighting(); + } + Log::Write(StringExtensions::Build("PacUIO Id:{0} finished and updater thread stopped.", std::to_string(m_id))); +} + +void PacUIO::AddOutputs() +{ + OutputList* outputs = GetOutputs(); + for (int i = 1; i <= 96; i++) + { + bool found = false; + for (IOutput* output : *outputs) + { + if (output->GetNumber() == i) + { + found = true; + break; + } + } + + if (!found) + { + Output* newOutput = new Output(); + newOutput->SetName(StringExtensions::Build("{0}.{1:00}", GetName(), std::to_string(i))); + newOutput->SetNumber(i); + outputs->push_back(newOutput); + } + } +} + +void PacUIO::OnOutputValueChanged(IOutput* output) +{ + IOutput* on = output; + + if (!on || on->GetNumber() < 1 || on->GetNumber() > 96) + { + Log::Exception( + StringExtensions::Build("PacUIO output numbers must be in the range of 1-96. The supplied output number {0} is out of range.", std::to_string(on ? on->GetNumber() : -1))); + return; + } + + InitializeUnits(); + PacUIOUnit* s = s_pacUIOUnits[m_id]; + s->UpdateValue(on); +} + +PacUIO::PacUIO() { } + +PacUIO::PacUIO(int id) { SetId(id); } + +PacUIO::~PacUIO() { } + +tinyxml2::XMLElement* PacUIO::ToXml(tinyxml2::XMLDocument& doc) const +{ + tinyxml2::XMLElement* element = doc.NewElement(GetXmlElementName().c_str()); + + if (!GetName().empty()) + element->SetAttribute("Name", GetName().c_str()); + + element->SetAttribute("Id", m_id); + + return element; +} + +bool PacUIO::FromXml(const tinyxml2::XMLElement* element) +{ + if (!element) + return false; + + const char* name = element->Attribute("Name"); + if (name) + SetName(name); + + int id = element->IntAttribute("Id", -1); + if (id != -1) + SetId(id); + + return true; +} + +PacUIO::PacUIOUnit::PacUIOUnit(int id) + : m_id(id) +{ + m_pdSingleton = &PacDriveSingleton::GetInstance(); + m_index = PacDriveSingleton::GetInstance().PacUIOGetIndexForDeviceId(id); + + std::fill_n(m_newValue, 96, 0); + std::fill_n(m_currentValue, 96, 0); + std::fill_n(m_lastValueSent, 96, 0); + std::fill_n(m_lastStateSent, 96, false); + + InitUnit(); +} + +PacUIO::PacUIOUnit::~PacUIOUnit() +{ + if (m_keepPacUIOUpdaterAlive.load()) + { + Finish(); + } +} + +void PacUIO::PacUIOUnit::Init(Cabinet* cabinet) { StartPacUIOUpdaterThread(); } + +void PacUIO::PacUIOUnit::Finish() +{ + TerminatePacUIOUpdaterThread(); + ShutdownLighting(); +} + +void PacUIO::PacUIOUnit::UpdateValue(IOutput* output) +{ + int outputNumber = output->GetNumber(); + if (outputNumber < 1 || outputNumber > 96) + return; + + int zeroBasedOutputNumber = outputNumber - 1; + std::lock_guard lock(m_valueChangeLocker); + + if (m_newValue[zeroBasedOutputNumber] != output->GetOutput()) + { + m_newValue[zeroBasedOutputNumber] = output->GetOutput(); + m_updateRequired = true; + } +} + +void PacUIO::PacUIOUnit::CopyNewToCurrent() +{ + std::lock_guard lock(m_valueChangeLocker); + std::copy(m_newValue, m_newValue + 96, m_currentValue); +} + +bool PacUIO::PacUIOUnit::IsUpdaterThreadAlive() { return m_pacUIOUpdater.joinable(); } + +void PacUIO::PacUIOUnit::StartPacUIOUpdaterThread() +{ + std::lock_guard lock(m_pacUIOUpdaterThreadLocker); + if (!IsUpdaterThreadAlive()) + { + m_keepPacUIOUpdaterAlive.store(true); + m_pacUIOUpdater = std::thread(&PacUIOUnit::PacUIOUpdaterDoIt, this); + } +} + +void PacUIO::PacUIOUnit::TerminatePacUIOUpdaterThread() +{ + std::unique_lock lock(m_pacUIOUpdaterThreadLocker); + if (m_pacUIOUpdater.joinable()) + { + m_keepPacUIOUpdaterAlive.store(false); + TriggerPacUIOUpdaterThread(); + lock.unlock(); + + if (m_pacUIOUpdater.joinable()) + { + m_pacUIOUpdater.join(); + } + } +} + +void PacUIO::PacUIOUnit::TriggerPacUIOUpdaterThread() +{ + m_triggerUpdate.store(true); + m_triggerCondition.notify_one(); +} + +void PacUIO::PacUIOUnit::PacUIOUpdaterDoIt() +{ + try + { + ResetFadeTime(); + } + catch (const std::exception& e) + { + Log::Exception(StringExtensions::Build("A exception occurred while setting the fadetime for PacUIO {0} to 0.", std::to_string(m_index))); + return; + } + + int failCnt = 0; + while (m_keepPacUIOUpdaterAlive.load()) + { + try + { + if (IsPresent()) + { + SendPacUIOUpdate(); + } + failCnt = 0; + } + catch (const std::exception& e) + { + Log::Exception(StringExtensions::Build("A error occurred when updating PacUIO {0}", std::to_string(m_id))); + failCnt++; + + if (failCnt > MAX_UPDATE_FAIL_COUNT) + { + Log::Exception(StringExtensions::Build( + "More than {0} consecutive updates failed for PacUIO {1}. Updater thread will terminate.", std::to_string(MAX_UPDATE_FAIL_COUNT), std::to_string(m_id))); + m_keepPacUIOUpdaterAlive.store(false); + } + } + + if (m_keepPacUIOUpdaterAlive.load()) + { + std::unique_lock lock(m_pacUIOUpdaterThreadLocker); + m_triggerCondition.wait_for(lock, std::chrono::milliseconds(50), [this]() { return m_triggerUpdate.load() || !m_keepPacUIOUpdaterAlive.load(); }); + } + m_triggerUpdate.store(false); + } +} + +void PacUIO::PacUIOUnit::SendPacUIOUpdate() +{ + if (!IsPresent()) + { + m_forceFullUpdate = true; + return; + } + + std::lock_guard updateLock(m_pacUIOUpdateLocker); + + { + std::lock_guard valueLock(m_valueChangeLocker); + if (!m_updateRequired && !m_forceFullUpdate) + return; + + CopyNewToCurrent(); + m_updateRequired = false; + } + + uint8_t intensityUpdatesRequired = 0; + uint8_t stateUpdatesRequired = 0; + + if (!m_forceFullUpdate) + { + for (int g = 0; g < 12; g++) + { + bool stateUpdateRequired = false; + for (int p = 0; p < 8; p++) + { + int o = g << 3 | p; + if (m_currentValue[o] > 0) + { + if (m_currentValue[o] != m_lastValueSent[o]) + { + intensityUpdatesRequired++; + } + else if (!m_lastStateSent[o]) + { + stateUpdateRequired = true; + } + } + else if (m_lastStateSent[o]) + { + stateUpdateRequired = true; + } + } + if (stateUpdateRequired) + stateUpdatesRequired++; + } + } + + if (m_forceFullUpdate || (intensityUpdatesRequired + stateUpdatesRequired) > 50) + { + m_pdSingleton->PacLed64SetLEDIntensities(m_index, m_currentValue); + std::copy(m_currentValue, m_currentValue + 96, m_lastValueSent); + for (int i = 0; i < 96; i++) + { + m_lastStateSent[i] = (m_lastValueSent[i] > 0); + } + } + else + { + for (int g = 0; g < 12; g++) + { + int mask = 0; + bool stateUpdateRequired = false; + for (int p = 0; p < 8; p++) + { + int o = g << 3 | p; + if (m_currentValue[o] > 0) + { + if (m_currentValue[o] != m_lastValueSent[o]) + { + m_pdSingleton->PacLed64SetLEDIntensity(m_index, o, m_currentValue[o]); + m_lastStateSent[o] = true; + m_lastValueSent[o] = m_currentValue[o]; + } + else if (!m_lastStateSent[o]) + { + mask |= (1 << p); + stateUpdateRequired = true; + m_lastStateSent[o] = true; + } + } + else if (m_lastStateSent[o]) + { + stateUpdateRequired = true; + m_lastStateSent[o] = false; + m_lastValueSent[o] = 0; + } + } + if (stateUpdateRequired) + { + m_pdSingleton->PacLed64SetLEDStates(m_index, g + 1, static_cast(mask)); + } + } + } +} + +void PacUIO::PacUIOUnit::ShutdownLighting() +{ + Log::Write("PacUIO.ShutdownLighting"); + m_pdSingleton->PacLed64SetLEDStates(0, 0, 0); + std::fill_n(m_lastStateSent, 96, false); +} + +void PacUIO::PacUIOUnit::ResetFadeTime() +{ + Log::Write("PacUIO.ResetFadeTime"); + m_pdSingleton->PacLed64SetLEDFadeTime(m_index, 0); +} + +bool PacUIO::PacUIOUnit::IsPresent() +{ + if (m_id < 0 || m_id > 3) + return false; + return m_index >= 0; +} + +void PacUIO::PacUIOUnit::OnPacAttached(int index) +{ + m_index = m_pdSingleton->PacLed64GetIndexForDeviceId(m_id); + InitUnit(); + TriggerPacUIOUpdaterThread(); +} + +void PacUIO::PacUIOUnit::OnPacRemoved(int index) { m_index = m_pdSingleton->PacLed64GetIndexForDeviceId(m_id); } + +void PacUIO::PacUIOUnit::InitUnit() +{ + if (m_index >= 0) + { + std::fill_n(m_lastValueSent, 96, 0); + m_pdSingleton->PacLed64SetLEDIntensities(m_index, m_lastValueSent); + std::fill_n(m_lastStateSent, 96, false); + std::fill_n(m_lastValueSent, 96, 0); + } +} + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacUIO.h b/src/cab/out/pac/PacUIO.h new file mode 100644 index 0000000..6cd5042 --- /dev/null +++ b/src/cab/out/pac/PacUIO.h @@ -0,0 +1,98 @@ +#pragma once + +#include "../OutputControllerBase.h" +#include +#include +#include +#include +#include +#include + +namespace DOF +{ + +class PacUIO : public OutputControllerBase +{ +private: + std::mutex m_idUpdateLocker; + int m_id = -1; + +public: + int GetId() const { return m_id; } + void SetId(int value); + + void Update() override; + void Init(class Cabinet* cabinet) override; + void Finish() override; + + tinyxml2::XMLElement* ToXml(tinyxml2::XMLDocument& doc) const override; + bool FromXml(const tinyxml2::XMLElement* element) override; + std::string GetXmlElementName() const override { return "PacUIO"; } + +private: + void AddOutputs(); + void OnOutputValueChanged(class IOutput* output) override; + +public: + PacUIO(); + PacUIO(int id); + ~PacUIO(); + +private: + class PacUIOUnit + { + private: + static const int MAX_UPDATE_FAIL_COUNT = 5; + int m_id; + int m_index = -1; + class PacDriveSingleton* m_pdSingleton = nullptr; + + uint8_t m_newValue[96]; + uint8_t m_currentValue[96]; + uint8_t m_lastValueSent[96]; + bool m_lastStateSent[96]; + + bool m_updateRequired = true; + bool m_forceFullUpdate = false; + + std::mutex m_pacUIOUpdateLocker; + std::mutex m_valueChangeLocker; + + std::thread m_pacUIOUpdater; + std::atomic m_keepPacUIOUpdaterAlive { false }; + std::mutex m_pacUIOUpdaterThreadLocker; + std::condition_variable m_triggerCondition; + std::atomic m_triggerUpdate { false }; + + public: + PacUIOUnit(int id); + ~PacUIOUnit(); + + void Init(class Cabinet* cabinet); + void Finish(); + void UpdateValue(class IOutput* output); + void TriggerPacUIOUpdaterThread(); + void ShutdownLighting(); + + bool IsUpdaterThreadAlive(); + void StartPacUIOUpdaterThread(); + void TerminatePacUIOUpdaterThread(); + + private: + void CopyNewToCurrent(); + void PacUIOUpdaterDoIt(); + void SendPacUIOUpdate(); + void ResetFadeTime(); + void InitUnit(); + + bool IsPresent(); + void OnPacAttached(int index); + void OnPacRemoved(int index); + }; + + static std::vector s_pacUIOUnits; + static bool s_unitsInitialized; + static void InitializeUnits(); +}; + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacUIOAutoConfigurator.cpp b/src/cab/out/pac/PacUIOAutoConfigurator.cpp new file mode 100644 index 0000000..3fc571c --- /dev/null +++ b/src/cab/out/pac/PacUIOAutoConfigurator.cpp @@ -0,0 +1,88 @@ +#include "PacUIOAutoConfigurator.h" +#include "PacUIO.h" +#include "PacDriveSingleton.h" +#include "../../Cabinet.h" +#include "../OutputControllerList.h" +#include "../../toys/ToyList.h" +#include "../../toys/lwequivalent/LedWizEquivalent.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" +#include + +namespace DOF +{ + +void PacUIOAutoConfigurator::AutoConfig(Cabinet* cabinet) +{ + if (!cabinet) + return; + + Log::Write("PacUIO auto-configuration starting"); + + std::vector pacUIOIds = PacDriveSingleton::GetInstance().PacUIOGetIdList(); + + for (int id : pacUIOIds) + { + bool found = false; + for (IOutputController* oc : *cabinet->GetOutputControllers()) + { + PacUIO* pacUIO = dynamic_cast(oc); + if (pacUIO && pacUIO->GetId() == id) + { + found = true; + break; + } + } + + if (!found) + { + PacUIO* pacUIO = new PacUIO(); + pacUIO->SetId(id); + + Log::Write(StringExtensions::Build("PacUIOAutoConfigurator.AutoConfig.. Detected PacUIO[{0}], name={1}", std::to_string(id), pacUIO->GetName())); + + if (!cabinet->GetOutputControllers()->Contains(pacUIO->GetName())) + { + cabinet->GetOutputControllers()->push_back(pacUIO); + + Log::Write(StringExtensions::Build("Detected and added PacUIO Id {0} with name {1}", std::to_string(pacUIO->GetId()), pacUIO->GetName())); + + bool toyFound = false; + int ledWizNumber = pacUIO->GetId() + 27; + for (IToy* toy : *cabinet->GetToys()) + { + LedWizEquivalent* lwe = dynamic_cast(toy); + if (lwe && lwe->GetLedWizNumber() == ledWizNumber) + { + toyFound = true; + break; + } + } + + if (!toyFound) + { + LedWizEquivalent* lwe = new LedWizEquivalent(); + lwe->SetLedWizNumber(ledWizNumber); + lwe->SetName(StringExtensions::Build("{0} Equivalent 1", pacUIO->GetName())); + + for (int i = 1; i <= 96; i++) + { + LedWizEquivalentOutput* lweo = new LedWizEquivalentOutput(); + lweo->SetOutputName(StringExtensions::Build("{0}.{1:00}", pacUIO->GetName(), std::to_string(i))); + lweo->SetLedWizEquivalentOutputNumber(i); + lwe->GetOutputs().AddOutput(lweo); + } + + if (!cabinet->GetToys()->Contains(lwe->GetName())) + { + cabinet->GetToys()->AddToy(lwe); + Log::Write(StringExtensions::Build( + "Added LedwizEquivalent Nr. {0} with name {1} for PacUIO with Id {2}", std::to_string(lwe->GetLedWizNumber()), lwe->GetName(), std::to_string(pacUIO->GetId()))); + } + } + } + } + } +} + +} \ No newline at end of file diff --git a/src/cab/out/pac/PacUIOAutoConfigurator.h b/src/cab/out/pac/PacUIOAutoConfigurator.h new file mode 100644 index 0000000..e82ebef --- /dev/null +++ b/src/cab/out/pac/PacUIOAutoConfigurator.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../IAutoConfigOutputController.h" + +namespace DOF +{ + +class Cabinet; + +class PacUIOAutoConfigurator : public IAutoConfigOutputController +{ +public: + PacUIOAutoConfigurator() = default; + virtual ~PacUIOAutoConfigurator() = default; + + virtual void AutoConfig(Cabinet* cabinet) override; +}; + +} \ No newline at end of file diff --git a/src/cab/out/pinone/PinOneAutoConfigurator.cpp b/src/cab/out/pinone/PinOneAutoConfigurator.cpp index 6d3d863..de35ca4 100644 --- a/src/cab/out/pinone/PinOneAutoConfigurator.cpp +++ b/src/cab/out/pinone/PinOneAutoConfigurator.cpp @@ -12,12 +12,15 @@ #include #include #include +#include namespace DOF { void PinOneAutoConfigurator::AutoConfig(Cabinet* cabinet) { + Log::Write("PinOne auto-configuration starting"); + const int UnitBias = 10; std::vector preconfigured; @@ -74,70 +77,119 @@ void PinOneAutoConfigurator::AutoConfig(Cabinet* cabinet) } } -std::string PinOneAutoConfigurator::GetDevice() +std::string PinOneAutoConfigurator::TestSerialPort(const char* portName) { - struct sp_port** portList; - if (sp_list_ports(&portList) != SP_OK) - return ""; + struct sp_port* port = nullptr; - for (int i = 0; portList[i] != nullptr; i++) + try { - struct sp_port* port = nullptr; - char* portName = sp_get_port_name(portList[i]); + if (sp_get_port_by_name(portName, &port) != SP_OK) + return ""; - if (!portName) - continue; + // Check if port file actually exists before trying to open it + std::string portPath(portName); + std::ifstream portFile(portPath); + if (!portFile.good()) + { + sp_free_port(port); + return ""; + } + portFile.close(); - try + if (sp_open(port, SP_MODE_READ_WRITE) != SP_OK) { - if (sp_get_port_by_name(portName, &port) != SP_OK) - continue; + sp_free_port(port); + return ""; + } - if (sp_open(port, SP_MODE_READ_WRITE) != SP_OK) - { - sp_free_port(port); - continue; - } + // Set very short timeouts to prevent hanging + sp_set_baudrate(port, 2000000); + sp_set_bits(port, 8); + sp_set_parity(port, SP_PARITY_NONE); + sp_set_stopbits(port, 1); + sp_set_dtr(port, SP_DTR_ON); + sp_set_rts(port, SP_RTS_OFF); + sp_set_cts(port, SP_CTS_IGNORE); + sp_set_dsr(port, SP_DSR_IGNORE); + sp_set_xon_xoff(port, SP_XONXOFF_DISABLED); - sp_set_baudrate(port, 2000000); - sp_set_bits(port, 8); - sp_set_parity(port, SP_PARITY_NONE); - sp_set_stopbits(port, 1); - sp_set_dtr(port, SP_DTR_ON); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + sp_flush(port, SP_BUF_BOTH); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - sp_flush(port, SP_BUF_BOTH); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + uint8_t command[] = { 0, 251, 0, 0, 0, 0, 0, 0, 0 }; + sp_blocking_write(port, command, 9, 100); - uint8_t command[] = { 0, 251, 0, 0, 0, 0, 0, 0, 0 }; - sp_blocking_write(port, command, 9, 100); + char buffer[256]; + int bytesRead = sp_blocking_read(port, buffer, sizeof(buffer) - 1, 100); - char buffer[256]; - int bytesRead = sp_blocking_read(port, buffer, sizeof(buffer) - 1, 100); + if (bytesRead > 0) + { + buffer[bytesRead] = '\0'; + std::string result(buffer); + if (result == "DEBUG,CSD Board Connected") + { + sp_close(port); + sp_free_port(port); + return std::string(portName); + } + } - if (bytesRead > 0) + // Force flush and set non-blocking mode before closing to prevent hang + sp_flush(port, SP_BUF_BOTH); + sp_set_rts(port, SP_RTS_OFF); + sp_set_dtr(port, SP_DTR_OFF); + + sp_close(port); + sp_free_port(port); + } + catch (...) + { + if (port) + { + try { - buffer[bytesRead] = '\0'; - std::string result(buffer); - if (result == "DEBUG,CSD Board Connected") - { - sp_close(port); - sp_free_port(port); - sp_free_port_list(portList); - return std::string(portName); - } + sp_flush(port, SP_BUF_BOTH); + sp_set_rts(port, SP_RTS_OFF); + sp_set_dtr(port, SP_DTR_OFF); + } + catch (...) + { /* Ignore errors in cleanup */ } sp_close(port); sp_free_port(port); } - catch (...) + } + + // Small delay to ensure port cleanup completes + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + return ""; +} + +std::string PinOneAutoConfigurator::GetDevice() +{ + struct sp_port** portList; + if (sp_list_ports(&portList) != SP_OK) + return ""; + + int portCount = 0; + while (portList[portCount] != nullptr) + portCount++; + + for (int i = 0; portList[i] != nullptr; i++) + { + char* portName = sp_get_port_name(portList[i]); + if (!portName) + continue; + + // Test each port synchronously with timeouts matching C# version (100ms) + std::string result = TestSerialPort(portName); + if (!result.empty()) { - if (port) - { - sp_close(port); - sp_free_port(port); - } + sp_free_port_list(portList); + return result; } } diff --git a/src/cab/out/pinone/PinOneAutoConfigurator.h b/src/cab/out/pinone/PinOneAutoConfigurator.h index 8487803..03322ac 100644 --- a/src/cab/out/pinone/PinOneAutoConfigurator.h +++ b/src/cab/out/pinone/PinOneAutoConfigurator.h @@ -12,6 +12,7 @@ class PinOneAutoConfigurator : public IAutoConfigOutputController public: virtual void AutoConfig(Cabinet* cabinet) override; static std::string GetDevice(); + static std::string TestSerialPort(const char* portName); }; } \ No newline at end of file diff --git a/src/cab/out/pinone/PinOneCommunication.cpp b/src/cab/out/pinone/PinOneCommunication.cpp index c0c35d2..aa27d8b 100644 --- a/src/cab/out/pinone/PinOneCommunication.cpp +++ b/src/cab/out/pinone/PinOneCommunication.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #endif namespace DOF @@ -63,6 +65,10 @@ bool PinOneCommunication::ConnectToServer() if (sockfd < 0) return false; + // Set socket to non-blocking for timeout + int flags = fcntl(sockfd, F_GETFL, 0); + fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); + struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; @@ -70,10 +76,35 @@ bool PinOneCommunication::ConnectToServer() if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - close(sockfd); - return false; + if (errno == EINPROGRESS) + { + // Wait for connection with timeout + fd_set writefds; + FD_ZERO(&writefds); + FD_SET(sockfd, &writefds); + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; // 100ms timeout to match C# version + + int result = select(sockfd + 1, NULL, &writefds, NULL, &timeout); + if (result <= 0) + { + close(sockfd); + return false; + } + } + else + { + // Connection failed immediately (socket doesn't exist) + close(sockfd); + return false; + } } + // Restore blocking mode + fcntl(sockfd, F_SETFL, flags); + m_pipeClient = reinterpret_cast(static_cast(sockfd)); #endif return true; diff --git a/src/cab/out/ps/Pinscape.cpp b/src/cab/out/ps/Pinscape.cpp index 813cd34..c68aedd 100644 --- a/src/cab/out/ps/Pinscape.cpp +++ b/src/cab/out/ps/Pinscape.cpp @@ -203,6 +203,8 @@ void Pinscape::FindDevices() } hid_free_enumeration(pDevices); + + Log::Write(StringExtensions::Build("Pinscape device scan found {0} devices", std::to_string(s_devices.size()))); } void Pinscape::ClearDevices() diff --git a/src/cab/out/ps/PinscapeAutoConfigurator.cpp b/src/cab/out/ps/PinscapeAutoConfigurator.cpp index 05a7de3..76610aa 100644 --- a/src/cab/out/ps/PinscapeAutoConfigurator.cpp +++ b/src/cab/out/ps/PinscapeAutoConfigurator.cpp @@ -13,6 +13,8 @@ namespace DOF void PinscapeAutoConfigurator::AutoConfig(Cabinet* cabinet) { + Log::Write("Pinscape auto-configuration starting"); + const int unitBias = 50; std::vector preconfigured; diff --git a/src/cab/out/pspico/PinscapePico.cpp b/src/cab/out/pspico/PinscapePico.cpp index cc5c496..1fe2acf 100644 --- a/src/cab/out/pspico/PinscapePico.cpp +++ b/src/cab/out/pspico/PinscapePico.cpp @@ -410,8 +410,6 @@ void PinscapePico::FindDevices() } hid_free_enumeration(devs); - - Log::Write(StringExtensions::Build("PinscapePico device scan found {0} devices", { std::to_string(s_devices.size()) })); } std::string PinscapePico::GetDeviceProductName(hid_device_info* dev) diff --git a/src/cab/out/pspico/PinscapePicoAutoConfigurator.cpp b/src/cab/out/pspico/PinscapePicoAutoConfigurator.cpp index cd215c0..df58bc7 100644 --- a/src/cab/out/pspico/PinscapePicoAutoConfigurator.cpp +++ b/src/cab/out/pspico/PinscapePicoAutoConfigurator.cpp @@ -27,7 +27,6 @@ void PinscapePicoAutoConfigurator::AutoConfig(Cabinet* cabinet) if (allDevices.empty()) { - Log::Write("No PinscapePico devices found"); return; } diff --git a/src/cab/toys/layer/RGBAToy.cpp b/src/cab/toys/layer/RGBAToy.cpp index b1b5b81..95d0f61 100644 --- a/src/cab/toys/layer/RGBAToy.cpp +++ b/src/cab/toys/layer/RGBAToy.cpp @@ -1,5 +1,6 @@ #include "RGBAToy.h" #include "../../Cabinet.h" +#include "../../CabinetOutputList.h" #include "../../out/IOutput.h" #include "../../../Log.h" #include "../../../general/StringExtensions.h" @@ -28,6 +29,11 @@ void RGBAToy::Init(Cabinet* cabinet) Log::Write(StringExtensions::Build("Initializing RGBAToy: {0}", GetName())); + m_redOutput = cabinet->GetOutputs()->GetByName(m_outputNameRed); + m_greenOutput = cabinet->GetOutputs()->GetByName(m_outputNameGreen); + m_blueOutput = cabinet->GetOutputs()->GetByName(m_outputNameBlue); + + Reset(); } diff --git a/src/cab/toys/layer/RGBAToy.h b/src/cab/toys/layer/RGBAToy.h index b4b3bc6..429a6de 100644 --- a/src/cab/toys/layer/RGBAToy.h +++ b/src/cab/toys/layer/RGBAToy.h @@ -12,7 +12,7 @@ namespace DOF class IOutput; -class RGBAToy : public ToyBaseUpdatable, public IRGBOutputToy, public ILayerToy +class RGBAToy : public ToyBaseUpdatable, public IRGBOutputToy, public IRGBAToy { public: RGBAToy(); @@ -22,7 +22,7 @@ class RGBAToy : public ToyBaseUpdatable, public IRGBOutputToy, public ILayerToy< virtual void Reset() override; virtual void Finish() override; virtual void UpdateToy() override; - virtual int GetOutputCount() const { return 3; } + virtual int GetOutputCount() const override { return 3; } virtual void UpdateOutputs() override; virtual LayerDictionary& GetLayers() override { return m_layers; } virtual const LayerDictionary& GetLayers() const override { return m_layers; } diff --git a/src/fx/timmedfx/DurationEffect.cpp b/src/fx/timmedfx/DurationEffect.cpp index ec4f1b2..c51f799 100644 --- a/src/fx/timmedfx/DurationEffect.cpp +++ b/src/fx/timmedfx/DurationEffect.cpp @@ -45,6 +45,11 @@ void DurationEffect::Finish() { try { + if (m_table && m_table->GetPinball() && m_table->GetPinball()->GetAlarms()) + { + // Note: UnregisterAlarm with lambdas doesn't work reliably, but this matches C# behavior + // The alarm will naturally expire or be cleaned up by AlarmHandler + } } catch (...) { diff --git a/src/fx/timmedfx/MinDurationEffect.cpp b/src/fx/timmedfx/MinDurationEffect.cpp index 784d0eb..0c30bcb 100644 --- a/src/fx/timmedfx/MinDurationEffect.cpp +++ b/src/fx/timmedfx/MinDurationEffect.cpp @@ -22,21 +22,12 @@ void MinDurationEffect::Trigger(TableElementData* tableElementData) { if (tableElementData->m_value != 0) { - if (!m_active) + if (!m_active || m_retriggerBehaviour == RetriggerBehaviourEnum::Restart) { m_durationStart = std::chrono::steady_clock::now(); - TriggerTargetEffect(tableElementData); m_durationTimerTableElementData = *tableElementData; - m_table->GetPinball()->GetAlarms()->RegisterAlarm(m_minDurationMs, [this]() { this->MinDurationReached(&m_durationTimerTableElementData); }, true); - m_active = true; - m_untriggered = false; - } - else if (m_retriggerBehaviour == RetriggerBehaviourEnum::Restart) - { - m_durationStart = std::chrono::steady_clock::now(); TriggerTargetEffect(tableElementData); - m_durationTimerTableElementData = *tableElementData; - m_table->GetPinball()->GetAlarms()->RegisterAlarm(m_minDurationMs, [this]() { this->MinDurationReached(&m_durationTimerTableElementData); }, true); + m_active = true; } } else @@ -53,7 +44,6 @@ void MinDurationEffect::Trigger(TableElementData* tableElementData) else { int remainingMs = m_minDurationMs - static_cast(elapsed); - m_untriggered = true; m_table->GetPinball()->GetAlarms()->RegisterAlarm(remainingMs, [this]() { this->MinDurationEnd(); }, true); } } @@ -61,13 +51,6 @@ void MinDurationEffect::Trigger(TableElementData* tableElementData) } } -void MinDurationEffect::MinDurationReached(TableElementData* tableElementData) -{ - if (m_untriggered) - { - MinDurationEnd(); - } -} void MinDurationEffect::MinDurationEnd() { @@ -86,6 +69,11 @@ void MinDurationEffect::Finish() { try { + if (m_table && m_table->GetPinball() && m_table->GetPinball()->GetAlarms()) + { + // Note: UnregisterAlarm with lambdas doesn't work reliably, but this matches C# behavior + // The alarm will naturally expire or be cleaned up by AlarmHandler + } } catch (...) { diff --git a/src/ledcontrol/loader/LedControlConfig.cpp b/src/ledcontrol/loader/LedControlConfig.cpp index 73e5743..0372875 100644 --- a/src/ledcontrol/loader/LedControlConfig.cpp +++ b/src/ledcontrol/loader/LedControlConfig.cpp @@ -291,13 +291,9 @@ void LedControlConfig::ResolveRGBColors() break; } } - if (s->GetColorConfig() == nullptr && !s->GetColorName().empty()) - { - Log::Warning(StringExtensions::Build("Color '{0}' not found in color configuration", s->GetColorName())); - } } } } } -} \ No newline at end of file +} diff --git a/src/ledcontrol/loader/TableConfigSetting.cpp b/src/ledcontrol/loader/TableConfigSetting.cpp index a74cda3..7646ecb 100644 --- a/src/ledcontrol/loader/TableConfigSetting.cpp +++ b/src/ledcontrol/loader/TableConfigSetting.cpp @@ -5,6 +5,7 @@ #include "../../general/MathExtensions.h" #include "../../table/TableElementTypeEnum.h" #include +#include namespace DOF { @@ -562,9 +563,13 @@ void TableConfigSetting::ParseSettingData(const std::string& settingData) } integerCnt++; } - else if (partNr == 0) + else if (part[0] == '#' && (part.length() == 7 || part.length() == 9) && std::regex_match(part, std::regex("^#[0-9A-Fa-f]{6,8}$"))) { - m_colorName = part; + m_colorName = StringExtensions::ToUpper(part); + } + else if (part.length() > 2 && std::regex_match(part, std::regex("^[A-Za-z_]+$"))) + { + m_colorName = StringExtensions::ToUpper(part); } else { diff --git a/src/ledcontrol/setup/Configurator.cpp b/src/ledcontrol/setup/Configurator.cpp index 2e7020e..e6b6587 100644 --- a/src/ledcontrol/setup/Configurator.cpp +++ b/src/ledcontrol/setup/Configurator.cpp @@ -77,9 +77,7 @@ Configurator::~Configurator() { } void Configurator::Setup(LedControlConfigList* ledControlConfigList, Table* table, Cabinet* cabinet, const std::string& romName) { - Log::Write(StringExtensions::Build("Configurator::Setup called for ROM '{0}'", romName)); std::unordered_map tableConfigDict = ledControlConfigList->GetTableConfigDictionary(romName); - Log::Write(StringExtensions::Build("Found {0} table configs for ROM '{1}'", std::to_string(tableConfigDict.size()), romName)); std::string iniFilePath = ""; if (ledControlConfigList->size() > 0) @@ -287,7 +285,9 @@ std::unordered_map> Configurator::SetupCabin } if (targetToy != nullptr) + { toyAssignments[ledWizNr][tcc->GetNumber()] = targetToy; + } } } } @@ -300,21 +300,16 @@ void Configurator::SetupTable( for (const auto& kv : tableConfigDict) { int ledWizNr = kv.first; - Log::Write(StringExtensions::Build("Processing ledWizNr {0}", std::to_string(ledWizNr))); if (toyAssignments.find(ledWizNr) != toyAssignments.end()) { TableConfig* tc = kv.second; - Log::Write(StringExtensions::Build("Found TableConfig with {0} columns", std::to_string(tc->GetColumns()->size()))); for (TableConfigColumn* tcc : *tc->GetColumns()) { - Log::Write(StringExtensions::Build("Processing column {0}", std::to_string(tcc->GetNumber()))); - Log::Write(StringExtensions::Build("Column {0} has {1} settings", std::to_string(tcc->GetNumber()), std::to_string(tcc->size()))); if (toyAssignments.at(ledWizNr).find(tcc->GetNumber()) != toyAssignments.at(ledWizNr).end()) { IToy* toy = toyAssignments.at(ledWizNr).at(tcc->GetNumber()); - Log::Write(StringExtensions::Build("Found toy for column {0}: {1}", std::to_string(tcc->GetNumber()), toy->GetName())); int settingNumber = 0; for (TableConfigSetting* tcs : *tcc) @@ -993,11 +988,9 @@ void Configurator::AssignEffectToTableElements(Table* table, const std::vectorGetName())); for (const std::string& descriptor : tableElementDescriptors) { - Log::Write(StringExtensions::Build("Processing table element descriptor: '{0}'", descriptor)); if (descriptor.empty()) continue; @@ -1113,7 +1106,6 @@ void Configurator::AssignEffectToTableElements(Table* table, const std::vectorUpdateState(&elementData); for (TableElement* te : *tableElements) @@ -1138,8 +1130,6 @@ void Configurator::AssignEffectToTableElements(Table* table, const std::vectorGetAssignedEffects(); if (assignedEffects != nullptr) { - Log::Write(StringExtensions::Build("Configuration: Adding effect {0} to table element type={1}, number={2}", effect->GetName(), - std::string(1, (char)tableElement->GetTableElementType()), std::to_string(tableElement->GetNumber()))); assignedEffects->Add(effect->GetName()); } } diff --git a/src/test.cpp b/src/test.cpp index 06f47c8..6e3e366 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -1,19 +1,6 @@ // // libdof test app // -// The following entries should be in ~/.vpinball/directoutputconfig/directoutputconfig51.ini: -// -// ij_l7,0,0,0,0,0,0,0,L88 Blink fu500 fd550 -// tna,0,0,0,0,0,0,0,E140 Blink fu500 fd600 -// gw,0,0,0,0,0,0,0,L68 m550 Blink fu500 fd550 -// -// The following entries should be in ~/.vpinball/directoutputconfig/directoutputconfig30.ini: -// -// ij_l7,S10 Red AT0 AH12 L0/S20 Red AT0 AH12 L1/S24 Red AT0 AH12 L2/S52 Orange AT0 AH12 L3/S53 Red AT0 AH12 L4/W17 Orange AT0 AH12 L5/W51 Red AT0 AH12 L6/W52 Red AT0 AH12 L7/W53 Red AT0 AH12 L8/W75 Yellow AT0 AH12 L9/W76 Yellow AT0 AH12 L10/W77 Yellow AT0 AH12 L11/W78 Yellow AT0 AH12 L12/W88 Yellow AT85 AH15 FU250 FD270 BLINK 500,S9 Red AT0 AH12 L0/S51 Green AT0 AH12 L1/S53 Red AT0 AH12 L2/W16 Orange AT0 AH12 L3/W61 Red AT0 AH12 L4/W62 Red AT0 AH12 L5/W63 Red AT0 AH12 L6 -// tna,E144 Yellow AL0 AT0 F100 AFDEN5 AFMIN200 AFMAX300/E146 Blue AL0 AT0 F100 AFDEN5 AFMIN200 AFMAX300/E147 Red F100 AFDEN5 AFMIN200 AFMAX300/E148 Green AL0 AT0 F100 AFDEN5 AFMIN200 AFMAX300/E149 Purple AL0 AT0 AW100 AH100 AFDEN50 AFMIN500 AFMAX1000/E105 Red 50 AH100 ADU AS300/E107 White 40 AT40 AL0 AH10 AW100 AS400 ADU L25/E107 White 40 AT50 AL0 AH10 AW100 AS400 ADD L25/E116 Red 700 AT85 AH15 AFDEN40 AFMIN100 AFMAX160/E125 White 50 AH100 ADU AS300/E111 Red 40 AT40 AL0 AH10 AW100 AS400 ADU L25/E111 Red 40 AT50 AL0 AH10 AW100 AS400 ADD L25/E112 Purple L13 AT0 AFDEN15 AFMIN10 AFMAX20/E179 Red 60 AW100 AH100 ADD AS300/E150|E151|E152|E153 Yellow 40 AT15 AL0 AH10 AW100 AS400 ADU L25/E150|E151|E152|E153 Yellow 40 AT25 AL0 AH10 AW100 AS400 ADD L25,E144 Yellow AL0 AT0 F100 AFDEN5 AFMIN200 AFMAX300/E146 Blue AL0 AT0 F100 AFDEN5 AFMIN200 AFMAX300/E147 Red F100 AFDEN5 AFMIN200 AFMAX300/E148 Green AL0 AT0 F100 AFDEN5 AFMIN200 AFMAX300/E149 Purple AL0 AT0 AW100 AH100 AFDEN50 AFMIN500 AFMAX1000/E103 Red 50 AH100 ADU AS300/E116 Red 700 AT85 AH15 AFDEN40 AFMIN100 AFMAX160/E108 White 40 AT40 AL0 AH10 AW100 AS400 ADU L25/E108 White 40 AT50 AL0 AH10 AW100 AS400 ADD L25/E110 Red 40 AT40 AL0 AH10 AW100 AS400 ADU L25/E110 Red 40 AT50 AL0 AH10 AW100 AS400 ADD L25/E112 Purple L13 AT0 AFDEN15 AFMIN10 AFMAX20/E179 Red 60 AW100 AH100 ADD AS300/E150|E151|E152|E153 Yellow 40 AT15 AL0 AH10 AW100 AS400 ADU L25/E150|E151|E152|E153 Yellow 40 AT25 AL0 AH10 AW100 AS400 ADD L25 -// gw,S24 Yellow 50 AH100 ADU AS300/(W78=1 and (S46=1 or S48=1)) Red 500 W200 AT0 AH50 ADD AS500 L101/(W78=1 and (S46=1 or S48=1)) Black 500 W550 AT0 AH50 ADU AS500 L102/W78 Red AFDEN50 AFMIN60 AFMAX120 AT80 AH90/S16 Red 700 AT85 AH15 AFDEN40 AFMIN100 AFMAX160/W28 Red 100 AT80 AH20 ADD AS300/W27 Green 100 AT80 AH20 ADD AS300/W44|W45|W46 Orange_Red 40 AT25 AL0 AH25 AW100 AS400 ADU L25/W44|W45|W46 Orange_Red 40 AT50 AL0 AH25 AW100 AS400 ADD L25/W81 Red F150 AH30 AT0 ADD AS500 100 W450/S12 Red AT50 AH50 ADU 100 AS400/S12 Yellow AT0 AH50 ADD 100 AS400 W600 FD220/W36 Red AT20 AH5 150/W41 Yellow AT23 AH5 150/W51 Green AT26 AH5 150/W65 Blue AT0 AH50 ADD AS250 100/W65 Red AT0 AH50 ADD AS250 100 W90/W17 Green f200 AL0 AT0 AW100 AH15 L2 -// goldcue,L73 Lime L0 AL0 AW100 AH2 AT87 SHPRound4,L74 Lime L0 AL0 AW100 AH2 AT89 SHPRound4,L75 Lime L0 AL0 AW100 AH2 AT91 SHPRound4,L76 Lime L0 AL0 AW100 AH2 AT93 SHPRound4,L77 Lime L0 AL0 AW100 AH2 AT95 SHPRound4,L78 Lime L0 AL0 AW100 AH2 AT97 SHPRound4,S17 Sky_blue L0 AL0 AW100 AH10 AT2 SHPRound6,S3 Red L0 AL0 AW16 AH16 AT0 SHPLetterLargeA -// #include "DOF/DOF.h" #include @@ -43,8 +30,8 @@ struct TestRom std::string description; }; -std::vector testRoms - = { { "ij_l7", "Indiana Jones L7" }, { "tna", "Total Nuclear Annihilation" }, { "gw", "The Getaway High Speed II" }, { "goldcue", "Gold Cue" }, { "bourne", "Bourne Identity" } }; +std::vector testRoms = { { "ij_l7", "Indiana Jones L7" }, { "tna", "Total Nuclear Annihilation" }, { "gw", "The Getaway High Speed II" }, { "goldcue", "Gold Cue" }, + { "bourne", "Bourne Identity" }, { "twenty4", "24" } }; void LIBDOFCALLBACK LogCallback(DOF_LogLevel logLevel, const char* format, va_list args) { @@ -265,6 +252,51 @@ void RunBourneTests(DOF::DOF* pDof) pDof->Finish(); } +void RunTwenty4Tests(DOF::DOF* pDof) +{ + pDof->Init("", "twenty4"); + + Log("========================================"); + Log("Testing ROM: twenty4"); + Log("========================================"); + + std::this_thread::sleep_for(std::chrono::milliseconds(TIMEOUT_START_DELAY)); + + TriggerOutputOnOff(pDof, 'S', 9); + TriggerOutputOnOff(pDof, 'S', 19); + TriggerOutputOnOff(pDof, 'S', 31); + TriggerOutputOnOff(pDof, 'S', 32); + TriggerOutputOnOff(pDof, 'W', 3); + TriggerOutputOnOff(pDof, 'W', 10); + TriggerOutputOnOff(pDof, 'W', 46); + + TriggerOutputOnOff(pDof, 'S', 17); + TriggerOutputOnOff(pDof, 'S', 26); + + TriggerOutputOnOff(pDof, 'S', 11); + TriggerOutputOnOff(pDof, 'W', 43); + TriggerOutputOnOff(pDof, 'W', 55); + + TriggerOutputOnOff(pDof, 'S', 18); + TriggerOutputOnOff(pDof, 'S', 26); + TriggerOutputOnOff(pDof, 'W', 62); + TriggerOutputOnOff(pDof, 'W', 56); + + TriggerOutputOnOff(pDof, 'S', 10); + TriggerOutputOnOff(pDof, 'S', 20); + TriggerOutputOnOff(pDof, 'S', 27); + TriggerOutputOnOff(pDof, 'S', 31); + TriggerOutputOnOff(pDof, 'W', 54); + TriggerOutputOnOff(pDof, 'W', 57); + + TriggerOutputOnOff(pDof, 'S', 8, 1500); + TriggerOutputOnOff(pDof, 'W', 62, 1500); + + TriggerOutputOnOff(pDof, 'S', 24, 200, 800); + + pDof->Finish(); +} + std::string GetDefaultBasePath() { #ifdef _WIN32 @@ -366,6 +398,8 @@ int main(int argc, const char* argv[]) RunGoldcueTests(pDof); else if (testRom.name == "bourne") RunBourneTests(pDof); + else if (testRom.name == "twenty4") + RunTwenty4Tests(pDof); break; } } @@ -392,6 +426,7 @@ int main(int argc, const char* argv[]) RunGWTests(pDof); RunGoldcueTests(pDof); RunBourneTests(pDof); + RunTwenty4Tests(pDof); } Log("Shutting down...");