1
0
Fork 0
OpenLCP/protocol/enttec/dmx-usb-pro/widget.cpp

836 lines
20 KiB
C++

/*
widget.cpp
Copyright (c) 2023 Kevin Matz (kevin.matz@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "widget.h"
#include <rdm.h>
namespace ENTTEC {
Widget::Widget()
: serial_number(0) // SN# 0 for emulated devices
, firmware_version(2<<8) // emulated devices support RDM
, tx_break_intervals(17) // 181.4 us by default
, tx_mab_intervals(10) // 106.7 us by default
, tx_rate(40) // 40 packets/s, by default
, usb_mode(USBunknown)
, rx_update_mode_(Pro::RxNotifyAlways)
, token_data_changed_(nullptr)
, rdm_controller_(nullptr)
, rdm_responder_(nullptr)
{
token_rdm_receive_ = DMX::Universe::onRxData(RDM::SC_RDM, [this](const std::vector<uint8_t> &data)
{
if (rdm_responder_)
rdm_responder_->receive(data);
});
}
Widget::~Widget()
{
switch (usb_mode) {
case USBdevice:
Widget::halt();
break;
case USBhost:
Widget::close();
break;
default:
break;
}
if (rdm_controller_)
delete rdm_controller_;
if (rdm_responder_)
delete rdm_responder_;
}
/**
* @brief Begin operating as a USB Device
*
* The base class method should be called from all inheriting overrides.
*/
void Widget::init()
{
std::scoped_lock lock(mtx_metadata_);
usb_mode = USBdevice;
}
/**
* @brief Stop operating as a USB Device
*
* The base class method should be called from all inheriting overrides.
*/
void Widget::halt()
{
std::scoped_lock lock(mtx_metadata_);
usb_mode = USBunknown;
}
/**
* @brief Begin operating on the USB Host
*
* The base class method should be called from all inheriting overrides.
*/
void Widget::open()
{
std::scoped_lock lock(mtx_metadata_);
usb_mode = USBhost;
}
/**
* @brief Stop operating on the USB Host
*
* The base class method should be called from all inheriting overrides.
*/
void Widget::close()
{
std::scoped_lock lock(mtx_metadata_);
usb_mode = USBunknown;
}
/**
* @brief Widget::setModeController
*/
void Widget::setModeController()
{
sendDmx();
std::scoped_lock lock(mtx_metadata_);
token_data_changed_ = onDataChange([this](DMX::Universe*){sendDmx();});
device_class_ = DMX::CONTROLLER;
if (rdm_responder_)
delete rdm_responder_;
if (featureRDM())
rdm_controller_ = new RDM::Controller();
}
/**
* @brief Widget::setModeBridge
* @param mode
*/
void Widget::setModeBridge(Pro::DMX_RX_MODE mode)
{
{
std::scoped_lock lock(mtx_metadata_);
token_data_changed_ = nullptr;
rx_update_mode_ = mode;
if (rdm_controller_)
delete rdm_controller_;
if (featureRDM())
{
RDM::UID id(Pro::DecimalToBCD(serial_number), 0x454E); // Use the ENTTEC manufacturer ID?
rdm_responder_ = new RDM::Responder(id);
rdm_configure_responder_();
device_class_ = DMX::RESPONDER;
}
else
device_class_ = DMX::RECEIVER;
}
auto msg = std::make_shared<Pro::MsgRecieveDMXOnChange>();
msg->mode = mode;
sendMessage(msg);
}
/**
* @brief Widget::serialNumber
* @return
*/
uint32_t Widget::serialNumber() const
{
std::scoped_lock lock(mtx_metadata_);
return serial_number;
}
/**
* @brief Widget::firmwareVersion
* @return
*/
uint16_t Widget::firmwareVersion() const
{
std::scoped_lock lock(mtx_metadata_);
return firmware_version;
}
/**
* @brief Widget::txBreakTime
* @return
*/
double Widget::txBreakTime() const
{
std::scoped_lock lock(mtx_metadata_);
return tx_break_intervals * Pro::DMX_BREAK_INTERVAL;
}
/**
* @brief Widget::setTxBreakTime
* @param time
* @return
*/
double Widget::setTxBreakTime(double time)
{
setTxBreakIntervals(time / Pro::DMX_BREAK_INTERVAL);
return txBreakTime();
}
/**
* @brief Widget::setTxBreakIntervals
* @param count
*/
void Widget::setTxBreakIntervals(uint8_t count)
{
count = std::max(count, Pro::DMX_BREAK_MIN);
count = std::min(count, Pro::DMX_BREAK_MAX);
std::scoped_lock lock(mtx_metadata_);
tx_break_intervals = count;
}
/**
* @brief txMabTime
* @return
*/
double Widget::txMabTime() const
{
std::scoped_lock lock(mtx_metadata_);
return tx_mab_intervals * Pro::DMX_MAB_INTERVAL;
}
/**
* @brief Widget::setTxMabTime
* @param time
* @return
*/
double Widget::setTxMabTime(double time)
{
setTxMabIntervals(time / Pro::DMX_MAB_INTERVAL);
return txMabTime();
}
/**
* @brief Widget::setTxMabIntervals
* @param count
*/
void Widget::setTxMabIntervals(uint8_t count)
{
count = std::max(count, Pro::DMX_MAB_MIN);
count = std::min(count, Pro::DMX_MAB_MAX);
std::scoped_lock lock(mtx_metadata_);
tx_mab_intervals = count;
}
/**
* @brief txRate
* @return
*/
uint8_t Widget::txRate() const
{
std::scoped_lock lock(mtx_metadata_);
return tx_rate;
}
/**
* @brief Widget::setTxRate
* @param rate
*/
void Widget::setTxRate(uint8_t rate)
{
std::scoped_lock lock(mtx_metadata_);
tx_rate = std::min(rate, Pro::DMX_RATE_MAX);
}
/**
* @brief Widget::userData
* @return
*/
const std::vector<uint8_t> & Widget::userData() const
{
std::scoped_lock lock(mtx_metadata_);
return user_configuration;
}
/**
* @brief Widget::setUserData
* @param data
*/
void Widget::setUserData(std::vector<uint8_t> data)
{
std::scoped_lock lock(mtx_metadata_);
user_configuration = data;
}
/**
* @brief Widget::getParameters
* @param user_length
*/
void Widget::getParameters(size_t user_length)
{
auto msg = std::make_shared<Pro::MsgGetWidgetParametersRequest>();
msg->size = std::min(user_length, Pro::USER_CONFIGURATION_MAX);
sendMessage(msg);
}
/**
* @brief Widget::setParameters
* @param user_length
*/
void Widget::setParameters(size_t user_length) const
{
auto msg = std::make_shared<Pro::MsgSetWidgetParametersRequest>();
{
std::scoped_lock lock(mtx_metadata_);
msg->break_time = tx_break_intervals;
msg->mab_time = tx_mab_intervals;
msg->rate = tx_rate;
msg->user_data = user_configuration;
}
size_t length = user_length < 0 ? user_configuration.size() : user_length;
length = std::min(length, Pro::USER_CONFIGURATION_MAX);
msg->user_data.resize(length, 0xff); // resized data padded with 0xFF (matching OEM)
sendMessage(msg);
}
/**
* @brief Widget::writeFirmware
* @param data
* @param size
* @return
*/
bool Widget::writeFirmware(const uint8_t *data, const size_t size)
{
/**
* While the API documents the message formats, there is little discussion of the
* implimentation for updating firmware. As such, do nothing and return false.
*/
return false;
// reboot the device into the fw loader
rebootBootloader();
auto msg = std::make_shared<Pro::MsgProgramFlashPageRequest>();
bool success = false;
for (uint i = 0; i < size / sizeof(msg->page);)
{
std::copy(data + (i*sizeof(msg->page)), data + ((i+1)*sizeof(msg->page)), msg->page);
sendMessage(msg);
if (success)
i++; // page write was successful, do the next.
}
return success;
}
/**
* @brief Create ENTTEC API messages.
* @param label Opcode of the message.
* @param mode USB role.
* @return
*
* Some opcodes have dual meaning, depending on which side of USB this driver is operating.
*/
std::shared_ptr<Pro::MessageData> Widget::MessageDataFactory(Pro::MESSAGE_LABEL label,
OperatingMode mode)
{
switch (label) {
case Pro::OpReprogramFirmware:
return std::make_shared<Pro::MsgReprogramFirmware>();
case Pro::OpProgramFlashPage:
switch (mode) {
case USBdevice:
return std::make_shared<Pro::MsgProgramFlashPageReply>();
case USBhost:
return std::make_shared<Pro::MsgProgramFlashPageRequest>();
default:
return std::make_shared<Pro::MsgNoop>();
}
case Pro::OpGetWidgetParameters:
switch (mode) {
case USBdevice:
return std::make_shared<Pro::MsgGetWidgetParametersReply>();
case USBhost:
return std::make_shared<Pro::MsgGetWidgetParametersRequest>();
default:
return std::make_shared<Pro::MsgNoop>();
}
case Pro::OpSetWidgetParameters:
return std::make_shared<Pro::MsgSetWidgetParametersRequest>();
case Pro::OpRecievedDmxPacket:
return std::make_shared<Pro::MsgRecievedDmxPacket>();
case Pro::OpOutputOnlySendDMX:
return std::make_shared<Pro::MsgOutputOnlySendDMX>();
case Pro::OpSendRDMData:
return std::make_shared<Pro::MsgSendRDMData>();
case Pro::OpRecieveDMXOnChange:
return std::make_shared<Pro::MsgRecieveDMXOnChange>();
case Pro::OpRecievedDMXChanged:
return std::make_shared<Pro::MsgRecievedDMXChanged>();
case Pro::OpGetWidgetSerial:
switch (mode) {
case USBdevice:
return std::make_shared<Pro::MsgGetWidgetSerialReply>();
case USBhost:
return std::make_shared<Pro::MsgGetWidgetSerialRequest>();
default:
return std::make_shared<Pro::MsgNoop>();
}
case Pro::OpSendRDMDiscovery:
return std::make_shared<Pro::MsgSendRDMDiscovery>();
default:
return std::make_shared<Pro::MsgNoop>();
}
}
/**
* @brief Route recieved Enttec API messages.
* @param msg
*
* Some opcodes have dual meaning, depending on which side of USB this driver is operating.
*/
void Widget::routeRxMessage(std::shared_ptr<Pro::MessageData> msg)
{
switch (msg->label)
{
case Pro::OpNoop:
rxMsgHello();
break;
case Pro::OpReprogramFirmware:
rxMsgReprogramFirmware();
break;
case Pro::OpProgramFlashPage:
{
switch (usb_mode) {
case USBdevice:
{
auto data = std::static_pointer_cast<Pro::MsgProgramFlashPageRequest>(msg);
rxMsgProgramFlashPageRequest(data);
}
break;
case USBhost:
{
auto data = std::static_pointer_cast<Pro::MsgProgramFlashPageReply>(msg);
rxMsgProgramFlashPageReply(data);
}
break;
default:
break;
}
}
break;
case Pro::OpGetWidgetParameters:
{
switch (usb_mode) {
case USBdevice:
{
auto data = std::static_pointer_cast<Pro::MsgGetWidgetParametersRequest>(msg);
rxMsgGetWidgetParametersRequest(data);
}
break;
case USBhost:
{
auto data = std::static_pointer_cast<Pro::MsgGetWidgetParametersReply>(msg);
rxMsgGetWidgetParametersReply(data);
}
break;
default:
break;
}
}
break;
case Pro::OpSetWidgetParameters:
{
auto data = std::static_pointer_cast<Pro::MsgSetWidgetParametersRequest>(msg);
rxMsgSetWidgetParametersRequest(data);
}
break;
case Pro::OpRecievedDmxPacket:
{
auto data = std::static_pointer_cast<Pro::MsgRecievedDmxPacket>(msg);
rxMsgRecievedDmxPacket(data);
}
break;
case Pro::OpOutputOnlySendDMX:
{
auto data = std::static_pointer_cast<Pro::MsgOutputOnlySendDMX>(msg);
rxMsgOutputOnlySendDMX(data);
}
break;
case Pro::OpSendRDMData:
{
auto data = std::static_pointer_cast<Pro::MsgSendRDMData>(msg);
rxMsgSendRDMData(data);
}
break;
case Pro::OpRecieveDMXOnChange:
{
auto data = std::static_pointer_cast<Pro::MsgRecieveDMXOnChange>(msg);
rxMsgRecieveDMXOnChange(data);
}
break;
case Pro::OpRecievedDMXChanged:
{
auto data = std::static_pointer_cast<Pro::MsgRecievedDMXChanged>(msg);
rxMsgRecievedDMXChanged(data);
}
break;
case Pro::OpGetWidgetSerial:
{
switch (usb_mode) {
case USBdevice:
rxMsgGetWidgetSerialRequest();
break;
case USBhost:
{
auto data = std::static_pointer_cast<Pro::MsgGetWidgetSerialReply>(msg);
rxMsgGetWidgetSerialReply(data);
}
break;
default:
break;
}
}
break;
case Pro::OpSendRDMDiscovery:
{
auto data = std::static_pointer_cast<Pro::MsgSendRDMDiscovery>(msg);
rxMsgSendRDMDiscovery(data);
}
break;
default:
break;
}
}
/**
* @brief Send a message to the widget.
* @param msg
*
* \note The base class method is to discard the message.
*/
void Widget::sendMessage(std::shared_ptr<Pro::MessageData> msg) const
{
(void)msg;
}
/**
* @brief Widget::rebootBootloader
*
* USB devices should override this method to do something meaningful to their state.
*/
void Widget::rebootBootloader()
{
switch (usb_mode) {
case USBhost:
{
auto msg = std::make_shared<Pro::MsgReprogramFirmware>();
sendMessage(msg);
}
break;
default:
/// \note This method is remotely triggered by USB Hosts prior to sending a firemware upload.
break;
}
}
/**
* @brief Request that the USB Device reply with it's serial number.
*/
void Widget::getSerialNumber()
{
auto msg = std::make_shared<Pro::MsgGetWidgetSerialRequest>();
sendMessage(msg);
}
/**
* @brief Request that the USB Device output the current dimmer data.
* @param trimmed Remove trailing null slots.
*/
void Widget::sendDmx(bool trimmed) const
{
auto msg = std::make_shared<Pro::MsgOutputOnlySendDMX>();
uint16_t l = null_start_data.size() - 1;
if (trimmed)
for (l = null_start_data.size() - 1; l > 0 && null_start_data[l] == 0; --l) {};
msg->data = std::vector<uint8_t>(null_start_data.begin(), null_start_data.begin() + l + 1);
sendMessage(msg);
}
/**
* @brief Request that the USB Device send RDM data.
* @param data
*/
void Widget::sendRDMdata(const std::vector<uint8_t> &data) const
{
auto msg = std::make_shared<Pro::MsgSendRDMData>();
msg->data = data;
sendMessage(msg);
}
/**
* @brief Request that the USB Device send RDM discovery data.
* @param data
*/
void Widget::sendRDMdiscovery(const std::vector<uint8_t> &data) const
{
auto msg = std::make_shared<Pro::MsgSendRDMDiscovery>();
(void)data;
/// \todo copy data to discovery message
sendMessage(msg);
}
/**
* @brief Widget::rxMsgHello
*/
void Widget::rxMsgHello()
{
switch (usb_mode) {
case USBdevice:
sendMessage(std::make_shared<Pro::MsgNoop>()); // mirror the message back to the host
break;
case USBhost:
break;
default:
break;
}
}
/**
* @brief The USB Host intents to begin sending a firmware upload.
*/
void Widget::rxMsgReprogramFirmware()
{
rebootBootloader();
}
/**
* @brief The USB Host has sent a page of a new firmware.
* @param msg
*/
void Widget::rxMsgProgramFlashPageRequest(std::shared_ptr<Pro::MsgProgramFlashPageRequest> msg)
{
auto reply = std::make_shared<Pro::MsgProgramFlashPageReply>();
reply->success = writeFwPage(msg->page);
sendMessage(reply);
}
/**
* @brief The USB Device is reporting the status of the previous firmware page write.
* @param msg
*/
void Widget::rxMsgProgramFlashPageReply(std::shared_ptr<Pro::MsgProgramFlashPageReply> msg)
{
(void)msg;
/// \todo Send next firmware page.
}
/**
* @brief The USB Host is request this widgets tx parameters.
* @param msg
*/
void Widget::rxMsgGetWidgetParametersRequest(std::shared_ptr<Pro::MsgGetWidgetParametersRequest> msg)
{
auto reply = std::make_shared<Pro::MsgGetWidgetParametersReply>();
{
std::scoped_lock lock(mtx_metadata_);
reply->break_time = tx_break_intervals;
reply->mab_time = tx_mab_intervals;
reply->rate = tx_rate;
reply->user_data = std::vector<uint8_t>(user_configuration);
}
reply->user_data.resize(msg->size, 0);
sendMessage(reply);
}
/**
* @brief The USB Device has reported it's tx parameters.
* @param msg
*
* No sanity checking is performed on the values in the reply.
*/
void Widget::rxMsgGetWidgetParametersReply(std::shared_ptr<Pro::MsgGetWidgetParametersReply> msg)
{
std::scoped_lock lock(mtx_metadata_);
firmware_version = msg->version;
tx_break_intervals = msg->break_time;
tx_mab_intervals = msg->mab_time;
tx_rate = msg->rate;
user_configuration = std::vector<uint8_t>(msg->user_data);
}
/**
* @brief The USB Host has requested an update to the widget's tx parameters.
* @param msg
*/
void Widget::rxMsgSetWidgetParametersRequest(std::shared_ptr<Pro::MsgSetWidgetParametersRequest> msg)
{
std::scoped_lock lock(mtx_metadata_);
setTxBreakIntervals(msg->break_time);
setTxMabIntervals(msg->mab_time);
setTxRate(msg->rate);
user_configuration = std::vector<uint8_t>(msg->user_data);
}
/**
* @brief The USB Device has recieved a frame of DMX
* @param msg
*/
void Widget::rxMsgRecievedDmxPacket(std::shared_ptr<Pro::MsgRecievedDmxPacket> msg)
{
setData(msg->data);
}
/**
* @brief The USB Host has send a frame of DMX for the widget to transmit.
* @param msg
*/
void Widget::rxMsgOutputOnlySendDMX(std::shared_ptr<Pro::MsgOutputOnlySendDMX> msg)
{
setData(msg->data);
}
/**
* @brief The USB host is requesting a half-duplex data write.
* @param msg
*/
void Widget::rxMsgSendRDMData(std::shared_ptr<Pro::MsgSendRDMData> msg)
{
(void)msg;
/// \todo Impliment USB Device side of sending half-duplex RDM data.
}
/**
* @brief The USB Host has requested to only updated about recv'd data if it has changed.
* @param msg
*/
void Widget::rxMsgRecieveDMXOnChange(std::shared_ptr<Pro::MsgRecieveDMXOnChange> msg)
{
setData(std::vector<uint8_t>(DMX::E111_LAST_SLOT+1, 0)); // clear dimmer data
std::scoped_lock lock(mtx_metadata_);
rx_update_mode_ = msg->mode;
}
/**
* @brief The USB Device has send the portion of DMX data that has changed.
* @param msg
*/
void Widget::rxMsgRecievedDMXChanged(std::shared_ptr<Pro::MsgRecievedDMXChanged> msg)
{
(void)msg;
/// \todo Merge changed slots into the universe dimmer data.
}
/**
* @brief The USB Host has reqested this widget's serial number.
*/
void Widget::rxMsgGetWidgetSerialRequest()
{
auto reply = std::make_shared<Pro::MsgGetWidgetSerialReply>();
{
std::scoped_lock lock(mtx_metadata_);
reply->serial = serial_number;
}
sendMessage(reply);
}
/**
* @brief The USB Device has sent it's serial number.
* @param msg
*/
void Widget::rxMsgGetWidgetSerialReply(std::shared_ptr<Pro::MsgGetWidgetSerialReply> msg)
{
std::scoped_lock lock(mtx_metadata_);
serial_number = msg->serial;
reply_serial = true;
}
/**
* @brief The USB Host is requesting the sending of a RDM Discovery message.
* @param msg
*/
void Widget::rxMsgSendRDMDiscovery(std::shared_ptr<Pro::MsgSendRDMDiscovery> msg)
{
(void)msg;
/// \todo Impliment USB Device side of sending half-duplex RDM discovery data.
}
void Widget::rdm_configure_responder_()
{
if (!rdm_responder_)
return;
// send responders RDM messages
token_rdm_send_ = rdm_responder_->setSender(std::bind(&Widget::sendRDMdata,
this, std::placeholders::_1));
rdm_responder_->deviceManufacturerLabel = "ENTTEC";
rdm_responder_->deviceModelDescription = "DMX USB Pro";
rdm_responder_->deviceModelID = (uint16_t)70304; // manufacturer SKU
rdm_responder_->deviceProductCategory = RDM::PRODUCT_CATEGORY_DATA;
rdm_responder_->addProductDetailId(RDM::PRODUCT_DETAIL_PROTOCOL_CONVERTOR);
}
} // namespace ENTTEC