diff --git a/CLAUDE.md b/CLAUDE.md index dcd063e..7db9895 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden **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**: 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. +**Recent Major Implementation**: Fixed critical LedWiz event subscription chain - LedWiz outputs must use `OutputList::Add()` not `push_back()` to properly subscribe to output change events. This ensures the event flow: `Output::SetOutput()` → `OutputList::OutputValueChanged` → `OutputControllerBase` → `LedWiz::OnOutputValueChanged()` → `UpdateOutputs()` → USB commands. LedWiz inheritance corrected from `OutputControllerCompleteBase` to `OutputControllerBase` to match C# exactly. PAC controllers previously migrated to libusb with proper device permissions. ## Core Coding Principles @@ -39,6 +39,7 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden - **Matrix Targeting**: Matrix effects only for matrix toys, AnalogToyValueEffect for single outputs - **Change Detection**: Initialize `m_oldOutputValues` to 255 to match C# exactly - **Timing Values**: Must match C# (e.g. FadeEffect 30ms, MatrixFlicker 30ms interval) +- **OutputList Event Chain**: MUST use `OutputList::Add()` not `push_back()` - Add() automatically subscribes outputs to events for controller notifications ### Cross-Platform Requirements - **Manual Dependencies**: Build libusb, libftdi, libserialport, hidapi from source @@ -55,11 +56,12 @@ libdof is a C++ port of the C# DirectOutput Framework achieving 1:1 corresponden - **Arrays**: MSVC requires `{{0.0f, 0.0f}}` for std::array initialization` ### Test ROM Configurations -- **ij_l7**: Blink + Fade effects -- **gw**: Blink + Fade effects +- **ij_l7**: Blink + Fade effects +- **gw**: Blink + Fade effects - **tna**: Matrix effects - **bourne**: Bitmap effects - **goldcue**: Shape effects +- **afm**: RGB toys + LedWiz testing ## Implementation Status diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f9a026..35714a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -256,6 +256,7 @@ if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "macos" OR PLATFORM STREQUAL "li src/cab/out/ftdichip/FT245RBitbangControllerAutoConfigurator.cpp src/cab/out/ftdichip/FTDI.cpp src/cab/out/lw/LedWiz.cpp + src/cab/out/lw/LedWizOutput.cpp src/cab/out/lw/LedWizAutoConfigurator.cpp src/cab/out/pac/PacLed64.cpp src/cab/out/pac/PacLed64AutoConfigurator.cpp diff --git a/README.md b/README.md index 68fb404..3b51804 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ This library is currently used by [Visual Pinball Standalone](https://github.com - **[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 +- **[LedWiz](https://groovygamegear.com/webstore/index.php?main_page=product_info&products_id=239)** - LED-Wiz's 32-port USB compatible lighting and output controller ### **Implemented & Ready To Test** -- **LedWiz** - Classic 32-output controller - **DudesCab** - RP2040-based controller with 128 PWM outputs - **ArtNet/DMX** - Professional lighting control via Ethernet (all platforms) - **PinControl** - Arduino-based controller with 10 outputs diff --git a/src/cab/out/lw/LedWiz.cpp b/src/cab/out/lw/LedWiz.cpp index 2a98198..27d9e68 100644 --- a/src/cab/out/lw/LedWiz.cpp +++ b/src/cab/out/lw/LedWiz.cpp @@ -1,4 +1,5 @@ #include "LedWiz.h" +#include "LedWizOutput.h" #include "../../../Log.h" #include "../../../general/StringExtensions.h" #include "../../Cabinet.h" @@ -23,6 +24,8 @@ namespace DOF { std::vector LedWiz::s_deviceList = {}; +int LedWiz::s_startedUp = 0; +std::mutex LedWiz::s_startupLocker; LedWiz::LedWiz() { @@ -38,7 +41,14 @@ LedWiz::LedWiz(int number) SetNumber(number); } -LedWiz::~LedWiz() { Finish(); } +LedWiz::~LedWiz() +{ + if (m_fp) + { + AllOff(); + } + DisconnectFromController(); +} void LedWiz::SetNumber(int value) @@ -95,12 +105,47 @@ void LedWiz::Finish() AllOff(); } DisconnectFromController(); - OutputControllerCompleteBase::Finish(); Log::Write(StringExtensions::Build("LedWiz Nr. {0:00} finished and updater thread stopped.", std::to_string(m_number))); } void LedWiz::Update() { } +void LedWiz::OnOutputValueChanged(IOutput* output) +{ + if (!output || !m_fp) + return; + + LedWizOutput* ledWizOutput = dynamic_cast(output); + if (!ledWizOutput) + { + Log::Exception(StringExtensions::Build("The OutputValueChanged event handler for LedWiz {0:00} has been called by a sender which is not a LedWizOutput.", std::to_string(m_number))); + return; + } + + int ledWizOutputNumber = ledWizOutput->GetLedWizOutputNumber(); + if (ledWizOutputNumber < 1 || ledWizOutputNumber > 32) + { + Log::Exception(StringExtensions::Build("LedWiz output numbers must be in the range of 1-32. The supplied output number {0} is out of range.", std::to_string(ledWizOutputNumber))); + return; + } + + OutputList* outputs = GetOutputs(); + if (!outputs) + return; + + std::vector outputValues(32, 0); + for (auto* out : *outputs) + { + LedWizOutput* lwOut = dynamic_cast(out); + if (lwOut && lwOut->GetLedWizOutputNumber() >= 1 && lwOut->GetLedWizOutputNumber() <= 32) + { + outputValues[lwOut->GetLedWizOutputNumber() - 1] = lwOut->GetOutput(); + } + } + + UpdateOutputs(outputValues); +} + void LedWiz::AddOutputs() { OutputList* outputs = GetOutputs(); @@ -112,7 +157,8 @@ void LedWiz::AddOutputs() bool found = false; for (auto* output : *outputs) { - if (output->GetNumber() == i) + LedWizOutput* lwOut = dynamic_cast(output); + if (lwOut && lwOut->GetLedWizOutputNumber() == i) { found = true; break; @@ -121,10 +167,9 @@ void LedWiz::AddOutputs() if (!found) { - Output* newOutput = new Output(); + LedWizOutput* newOutput = new LedWizOutput(i); newOutput->SetName(StringExtensions::Build("{0}.{1:00}", GetName(), std::to_string(i))); - newOutput->SetNumber(i); - outputs->push_back(newOutput); + outputs->Add(newOutput); } } } @@ -135,8 +180,9 @@ void LedWiz::AllOff() { if (m_fp) { - std::vector buf = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + std::vector buf = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00 }; WriteUSB(buf); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } @@ -165,6 +211,16 @@ void LedWiz::ConnectToController() m_path = device.path; Log::Write(StringExtensions::Build("LedWiz {0} connected successfully", GetName())); m_oldOutputValues.resize(32, 0); + + std::vector initSba = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00 }; + WriteUSB(initSba); + + for (int ofs = 0; ofs < 32; ofs += 8) + { + std::vector initPba = { 0x00, 49, 49, 49, 49, 49, 49, 49, 49 }; + WriteUSB(initPba); + } + return; } } @@ -206,7 +262,7 @@ void LedWiz::UpdateOutputs(const std::vector& newOutputValues) if (hasChanges) { - std::vector sbaCmd = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + std::vector sbaCmd = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00 }; for (int i = 0; i < 32 && i < static_cast(newOutputValues.size()); ++i) { @@ -248,70 +304,77 @@ bool LedWiz::WriteUSB(const std::vector& data) int result = hid_write(m_fp, data.data(), data.size()); if (result < 0) - { - Log::Write(StringExtensions::Build("LedWiz {0} WriteUSB failed after retries", std::to_string(m_number))); return false; - } return true; } -void LedWiz::FindDevices() +void LedWiz::StartupLedWiz() { - s_deviceList.clear(); - - hid_device_info* devs = hid_enumerate(0x0000, 0x0000); - hid_device_info* curDev = devs; - - while (curDev) + std::lock_guard lock(s_startupLocker); + if (s_startedUp == 0) { - std::string productName = GetDeviceProductName(curDev); - std::string manufacturerName = GetDeviceManufacturerName(curDev); + s_deviceList.clear(); - char vidStr[8], pidStr[8]; - snprintf(vidStr, sizeof(vidStr), "%04X", curDev->vendor_id); - snprintf(pidStr, sizeof(pidStr), "%04X", curDev->product_id); + hid_device_info* devs = hid_enumerate(0x0000, 0x0000); + hid_device_info* curDev = devs; - Log::Instrumentation("LedWizDiscovery", - StringExtensions::Build("Scanning HID at VID/PID: {0}/{1}, product string: {2}, manufacturer: {3}", std::string(vidStr), std::string(pidStr), productName, manufacturerName)); + while (curDev) + { + std::string productName = GetDeviceProductName(curDev); + std::string manufacturerName = GetDeviceManufacturerName(curDev); - bool ok = false; - std::string okBecause; + bool ok = false; - if (curDev->vendor_id == 0xFAFA) - { - ok = true; - okBecause = "recognized by LedWiz vendor ID"; - } - else if (curDev->vendor_id == 0x20A0) - { - std::regex zebsPattern("zebsboards", std::regex::ECMAScript | std::regex::icase); - if (std::regex_search(manufacturerName, zebsPattern)) + if (curDev->vendor_id == 0xFAFA) { ok = true; - okBecause = "recognized by ZebsBoards manufacturer"; } - } + else if (curDev->vendor_id == 0x20A0) + { + std::regex zebsPattern("zebsboards", std::regex::ECMAScript | std::regex::icase); + if (std::regex_search(manufacturerName, zebsPattern)) + { + ok = true; + } + } - if (ok) - { - int unitNo = (curDev->product_id & 0x0F) + 1; - if (unitNo < 1 || unitNo > 16) - unitNo = 1; + if (ok) + { + int unitNo = (curDev->product_id & 0x0F) + 1; + if (unitNo < 1 || unitNo > 16) + unitNo = 1; - s_deviceList.emplace_back(unitNo, curDev->path, productName); + s_deviceList.emplace_back(unitNo, curDev->path, productName); - Log::Write(StringExtensions::Build("Found LedWiz device: {0} (unit {1}, {2})", productName, std::to_string(unitNo), okBecause)); + char pidHex[8]; + snprintf(pidHex, sizeof(pidHex), "%04X", curDev->product_id); + Log::Write(StringExtensions::Build("Found LedWiz device: {0} (unit {1}) PID:{2} Path:{3}", productName, std::to_string(unitNo), std::string(pidHex), std::string(curDev->path))); + } + + curDev = curDev->next; } - curDev = curDev->next; + hid_free_enumeration(devs); } + s_startedUp++; +} - hid_free_enumeration(devs); - - Log::Write(StringExtensions::Build("LedWiz device scan found {0} devices", std::to_string(s_deviceList.size()))); +void LedWiz::TerminateLedWiz() +{ + std::lock_guard lock(s_startupLocker); + if (s_startedUp > 0) + { + s_startedUp--; + if (s_startedUp == 0) + { + s_deviceList.clear(); + } + } } +void LedWiz::FindDevices() { StartupLedWiz(); } + std::string LedWiz::GetDeviceProductName(hid_device_info* dev) { if (dev->product_string) @@ -358,6 +421,8 @@ std::string LedWiz::GetDeviceManufacturerName(hid_device_info* dev) std::vector LedWiz::GetLedwizNumbers() { + StartupLedWiz(); + std::vector numbers; for (const auto& device : s_deviceList) { @@ -368,7 +433,14 @@ std::vector LedWiz::GetLedwizNumbers() tinyxml2::XMLElement* LedWiz::ToXml(tinyxml2::XMLDocument& doc) const { - tinyxml2::XMLElement* element = OutputControllerCompleteBase::ToXml(doc); + tinyxml2::XMLElement* element = doc.NewElement(GetXmlElementName().c_str()); + + if (!GetName().empty()) + { + tinyxml2::XMLElement* nameElement = doc.NewElement("Name"); + nameElement->SetText(GetName().c_str()); + element->InsertEndChild(nameElement); + } tinyxml2::XMLElement* numberElement = doc.NewElement("Number"); numberElement->SetText(m_number); @@ -383,8 +455,11 @@ tinyxml2::XMLElement* LedWiz::ToXml(tinyxml2::XMLDocument& doc) const bool LedWiz::FromXml(const tinyxml2::XMLElement* element) { - if (!OutputControllerCompleteBase::FromXml(element)) - return false; + const tinyxml2::XMLElement* nameElement = element->FirstChildElement("Name"); + if (nameElement && nameElement->GetText()) + { + SetName(nameElement->GetText()); + } const tinyxml2::XMLElement* numberElement = element->FirstChildElement("Number"); if (numberElement && numberElement->GetText()) diff --git a/src/cab/out/lw/LedWiz.h b/src/cab/out/lw/LedWiz.h index 0f071b9..838ea0c 100644 --- a/src/cab/out/lw/LedWiz.h +++ b/src/cab/out/lw/LedWiz.h @@ -1,14 +1,15 @@ #pragma once -#include "../OutputControllerCompleteBase.h" +#include "../OutputControllerBase.h" #include #include #include +#include namespace DOF { -class LedWiz : public OutputControllerCompleteBase +class LedWiz : public OutputControllerBase { public: LedWiz(); @@ -31,14 +32,20 @@ class LedWiz : public OutputControllerCompleteBase static void FindDevices(); static std::vector GetLedwizNumbers(); -protected: - virtual int GetNumberOfConfiguredOutputs() override { return 32; } +private: + static void StartupLedWiz(); + static void TerminateLedWiz(); + static int s_startedUp; + static std::mutex s_startupLocker; +protected: + virtual void OnOutputValueChanged(IOutput* output) override; - virtual bool VerifySettings() override; - virtual void ConnectToController() override; - virtual void DisconnectFromController() override; - virtual void UpdateOutputs(const std::vector& outputValues) override; +private: + bool VerifySettings(); + void ConnectToController(); + void DisconnectFromController(); + void UpdateOutputs(const std::vector& outputValues); private: struct LWDEVICE diff --git a/src/cab/out/lw/LedWizAutoConfigurator.cpp b/src/cab/out/lw/LedWizAutoConfigurator.cpp index 35c5350..8df36d3 100644 --- a/src/cab/out/lw/LedWizAutoConfigurator.cpp +++ b/src/cab/out/lw/LedWizAutoConfigurator.cpp @@ -17,8 +17,6 @@ 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/lw/LedWizOutput.cpp b/src/cab/out/lw/LedWizOutput.cpp new file mode 100644 index 0000000..c10e634 --- /dev/null +++ b/src/cab/out/lw/LedWizOutput.cpp @@ -0,0 +1,42 @@ +#include "LedWizOutput.h" +#include "../../../Log.h" +#include "../../../general/StringExtensions.h" + +namespace DOF +{ + +LedWizOutput::LedWizOutput() + : m_ledWizOutputNumber(1) +{ +} + +LedWizOutput::LedWizOutput(int ledWizOutputNumber) + : LedWizOutput() +{ + SetLedWizOutputNumber(ledWizOutputNumber); + SetNumber(ledWizOutputNumber); + SetName(StringExtensions::Build("LedWizOutput {0:00}", std::to_string(ledWizOutputNumber))); + SetOutput(0); +} + +LedWizOutput::~LedWizOutput() { } + +void LedWizOutput::SetLedWizOutputNumber(int value) +{ + if (value < 1 || value > 32) + { + Log::Exception(StringExtensions::Build("LedWiz output numbers must be in the range of 1-32. The supplied number {0} is out of range.", std::to_string(value))); + return; + } + + if (m_ledWizOutputNumber != value) + { + if (GetName().empty() || GetName() == StringExtensions::Build("LedWizOutput {0:00}", std::to_string(m_ledWizOutputNumber))) + { + SetName(StringExtensions::Build("LedWizOutput {0:00}", std::to_string(value))); + } + m_ledWizOutputNumber = value; + } +} + +} \ No newline at end of file diff --git a/src/cab/out/lw/LedWizOutput.h b/src/cab/out/lw/LedWizOutput.h new file mode 100644 index 0000000..3211bcb --- /dev/null +++ b/src/cab/out/lw/LedWizOutput.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../Output.h" + +namespace DOF +{ + +class LedWizOutput : public Output +{ +public: + LedWizOutput(); + LedWizOutput(int ledWizOutputNumber); + virtual ~LedWizOutput(); + + int GetLedWizOutputNumber() const { return m_ledWizOutputNumber; } + void SetLedWizOutputNumber(int value); + +private: + int m_ledWizOutputNumber; +}; + +} \ No newline at end of file diff --git a/src/cab/toys/layer/AnalogAlphaToy.cpp b/src/cab/toys/layer/AnalogAlphaToy.cpp index 8eef5a2..e3077c4 100644 --- a/src/cab/toys/layer/AnalogAlphaToy.cpp +++ b/src/cab/toys/layer/AnalogAlphaToy.cpp @@ -70,7 +70,6 @@ void AnalogAlphaToy::UpdateOutputs() if (m_output != nullptr) { int resultingValue = GetResultingValue(); - Log::Debug(StringExtensions::Build("AnalogAlphaToy: {0}: {1}", m_output->GetName(), std::to_string(resultingValue))); if (m_fadingCurve != nullptr) m_output->SetOutput(m_fadingCurve->MapValue(resultingValue)); diff --git a/src/tools/dof_test.cpp b/src/tools/dof_test.cpp index f004f13..7e93bea 100644 --- a/src/tools/dof_test.cpp +++ b/src/tools/dof_test.cpp @@ -31,7 +31,7 @@ struct TestRom }; 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" } }; + { "bourne", "Bourne Identity" }, { "twenty4", "24" }, { "afm", "Attack From Mars" } }; void LIBDOFCALLBACK LogCallback(DOF_LogLevel logLevel, const char* format, va_list args) { @@ -298,6 +298,42 @@ void RunTwenty4Tests(DOF::DOF* pDof) pDof->Finish(); } +void RunAFMTests(DOF::DOF* pDof) +{ + pDof->Init("", "afm"); + + Log("========================================"); + Log("Testing ROM: afm"); + Log("========================================"); + + std::this_thread::sleep_for(std::chrono::milliseconds(TIMEOUT_START_DELAY)); + + TriggerOutputOnOff(pDof, 'S', 27); + TriggerOutputOnOff(pDof, 'S', 11); + TriggerOutputOnOff(pDof, 'S', 28); + TriggerOutputOnOff(pDof, 'W', 74); + TriggerOutputOnOff(pDof, 'S', 9); + TriggerOutputOnOff(pDof, 'S', 25); + TriggerOutputOnOff(pDof, 'S', 12); + TriggerOutputOnOff(pDof, 'S', 21); + TriggerOutputOnOff(pDof, 'S', 23); + TriggerOutputOnOff(pDof, 'S', 26); + TriggerOutputOnOff(pDof, 'S', 10); + TriggerOutputOnOff(pDof, 'S', 17); + TriggerOutputOnOff(pDof, 'S', 18); + TriggerOutputOnOff(pDof, 'S', 22); + TriggerOutputOnOff(pDof, 'W', 38); + TriggerOutputOnOff(pDof, 'S', 19); + TriggerOutputOnOff(pDof, 'S', 13); + TriggerOutputOnOff(pDof, 'S', 20); + TriggerOutputOnOff(pDof, 'W', 48); + TriggerOutputOnOff(pDof, 'W', 72); + TriggerOutputOnOff(pDof, 'S', 39); + TriggerOutputOnOff(pDof, 'W', 65); + + pDof->Finish(); +} + std::string GetDefaultBasePath() { #ifdef _WIN32 @@ -401,6 +437,8 @@ int main(int argc, const char* argv[]) RunBourneTests(pDof); else if (testRom.name == "twenty4") RunTwenty4Tests(pDof); + else if (testRom.name == "afm") + RunAFMTests(pDof); break; } } @@ -428,6 +466,7 @@ int main(int argc, const char* argv[]) RunGoldcueTests(pDof); RunBourneTests(pDof); RunTwenty4Tests(pDof); + RunAFMTests(pDof); } Log("Shutting down...");