Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
179 changes: 127 additions & 52 deletions src/cab/out/lw/LedWiz.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "LedWiz.h"
#include "LedWizOutput.h"
#include "../../../Log.h"
#include "../../../general/StringExtensions.h"
#include "../../Cabinet.h"
Expand All @@ -23,6 +24,8 @@ namespace DOF
{

std::vector<LedWiz::LWDEVICE> LedWiz::s_deviceList = {};
int LedWiz::s_startedUp = 0;
std::mutex LedWiz::s_startupLocker;

LedWiz::LedWiz()
{
Expand All @@ -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)
Expand Down Expand Up @@ -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<LedWizOutput*>(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<uint8_t> outputValues(32, 0);
for (auto* out : *outputs)
{
LedWizOutput* lwOut = dynamic_cast<LedWizOutput*>(out);
if (lwOut && lwOut->GetLedWizOutputNumber() >= 1 && lwOut->GetLedWizOutputNumber() <= 32)
{
outputValues[lwOut->GetLedWizOutputNumber() - 1] = lwOut->GetOutput();
}
}

UpdateOutputs(outputValues);
}

void LedWiz::AddOutputs()
{
OutputList* outputs = GetOutputs();
Expand All @@ -112,7 +157,8 @@ void LedWiz::AddOutputs()
bool found = false;
for (auto* output : *outputs)
{
if (output->GetNumber() == i)
LedWizOutput* lwOut = dynamic_cast<LedWizOutput*>(output);
if (lwOut && lwOut->GetLedWizOutputNumber() == i)
{
found = true;
break;
Expand All @@ -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);
}
}
}
Expand All @@ -135,8 +180,9 @@ void LedWiz::AllOff()
{
if (m_fp)
{
std::vector<uint8_t> buf = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
std::vector<uint8_t> buf = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00 };
WriteUSB(buf);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}

Expand Down Expand Up @@ -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<uint8_t> initSba = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00 };
WriteUSB(initSba);

for (int ofs = 0; ofs < 32; ofs += 8)
{
std::vector<uint8_t> initPba = { 0x00, 49, 49, 49, 49, 49, 49, 49, 49 };
WriteUSB(initPba);
}

return;
}
}
Expand Down Expand Up @@ -206,7 +262,7 @@ void LedWiz::UpdateOutputs(const std::vector<uint8_t>& newOutputValues)

if (hasChanges)
{
std::vector<uint8_t> sbaCmd = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
std::vector<uint8_t> sbaCmd = { 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00 };

for (int i = 0; i < 32 && i < static_cast<int>(newOutputValues.size()); ++i)
{
Expand Down Expand Up @@ -248,70 +304,77 @@ bool LedWiz::WriteUSB(const std::vector<uint8_t>& 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<std::mutex> 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<std::mutex> 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)
Expand Down Expand Up @@ -358,6 +421,8 @@ std::string LedWiz::GetDeviceManufacturerName(hid_device_info* dev)

std::vector<int> LedWiz::GetLedwizNumbers()
{
StartupLedWiz();

std::vector<int> numbers;
for (const auto& device : s_deviceList)
{
Expand All @@ -368,7 +433,14 @@ std::vector<int> 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);
Expand All @@ -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())
Expand Down
23 changes: 15 additions & 8 deletions src/cab/out/lw/LedWiz.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#pragma once

#include "../OutputControllerCompleteBase.h"
#include "../OutputControllerBase.h"
#include <hidapi/hidapi.h>
#include <string>
#include <chrono>
#include <mutex>

namespace DOF
{

class LedWiz : public OutputControllerCompleteBase
class LedWiz : public OutputControllerBase
{
public:
LedWiz();
Expand All @@ -31,14 +32,20 @@ class LedWiz : public OutputControllerCompleteBase
static void FindDevices();
static std::vector<int> 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<uint8_t>& outputValues) override;
private:
bool VerifySettings();
void ConnectToController();
void DisconnectFromController();
void UpdateOutputs(const std::vector<uint8_t>& outputValues);

private:
struct LWDEVICE
Expand Down
Loading