diff --git a/.gitignore b/.gitignore index b41ca378ac..3633d3898c 100644 --- a/.gitignore +++ b/.gitignore @@ -246,3 +246,7 @@ javascript/new-src/node_modules .cproject .settings .vscode/ +*.config +*.creator* +*.files +*.includes diff --git a/.travis-ci.sh b/.travis-ci.sh index 466503673f..67e2e6139b 100755 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -11,6 +11,19 @@ COVERITY_SCAN_BUILD_URL="https://scan.coverity.com/scripts/travisci_build_coveri PYCHECKER_BLACKLIST="threading,unittest,cmd,optparse,google,google.protobuf,ssl,fftpack,lapack_lite,mtrand" +LINT_BLACKLIST=$(cat < #include +#include "ola/Logging.h" + namespace ola { using std::string; @@ -129,6 +131,11 @@ int64_t BaseTimeVal::InMilliSeconds() const { m_tv.tv_usec / ONE_THOUSAND); } +int64_t BaseTimeVal::InMicroSeconds() const { + return (m_tv.tv_sec * static_cast(USEC_IN_SECONDS) + + m_tv.tv_usec); +} + int64_t BaseTimeVal::AsInt() const { return (m_tv.tv_sec * static_cast(USEC_IN_SECONDS) + m_tv.tv_usec); } @@ -272,4 +279,89 @@ void MockClock::CurrentTime(TimeStamp *timestamp) const { *timestamp = tv; *timestamp += m_offset; } + +Sleep::Sleep(std::string caller) : + m_caller(caller) { +} + +/** + * @brief Set wanted granularity for usleep and check it. + * @note does not check at the nanosecond level, + * since internal sturtures use usecs. + * + * @param wanted wanted/needed granularity in usecs + * @param maxDeviation max deviation in usecs tolerated by calling thread. + * + * @attention the granularity of sleep is highly fluctuating depending on the + * load of the system, a prior GOOD state is no guarantee for future proper + * timing. + */ +bool Sleep::CheckTimeGranularity(uint64_t wanted, uint64_t maxDeviation) { + TimeStamp ts1, ts2; + Clock clock; + + m_wanted_granularity = wanted; + m_max_granularity_deviation = maxDeviation; + + timespec t; + t.tv_sec = wanted / USEC_IN_SECONDS; + t.tv_nsec = (wanted % USEC_IN_SECONDS) * ONE_THOUSAND; + + clock.CurrentTime(&ts1); + this->usleep(1); + clock.CurrentTime(&ts2); + TimeInterval interval = ts2 - ts1; + m_clock_overhead = interval.InMicroSeconds(); + + clock.CurrentTime(&ts1); + this->usleep(t); + clock.CurrentTime(&ts2); + + interval = ts2 - ts1; + m_granularity = (interval.InMicroSeconds() > + (wanted + maxDeviation + m_clock_overhead)) ? BAD : GOOD; + + OLA_INFO << "Granularity for OlaSleep in " << m_caller << " is " + << ((m_granularity == GOOD) ? "GOOD" : "BAD") + << " Requested: " << wanted << " Got: " << interval.InMicroSeconds() + << " Overhead: " << m_clock_overhead; + if (m_granularity == GOOD) { + return true; + } + return false; +} + +void Sleep::usleep(TimeInterval requested) { + timespec req; + req.tv_sec = requested.Seconds(); + req.tv_nsec = requested.MicroSeconds() * ONE_THOUSAND; + + this->usleep(req); +} + +void Sleep::usleep(uint32_t requested) { + timespec req; + req.tv_sec = requested / USEC_IN_SECONDS; + req.tv_nsec = (requested % USEC_IN_SECONDS) * ONE_THOUSAND; + req.tv_sec = 0; + + this->usleep(req); +} + +void Sleep::usleep(timespec requested) { + timespec rem; + + if (nanosleep(&requested, &rem) < 0) { + if (errno == EINTR) { + while (rem.tv_nsec > 0 || rem.tv_sec > 0) { + requested.tv_nsec = rem.tv_nsec; + requested.tv_sec = rem.tv_sec; + nanosleep(&requested, &rem); + } + } else { + OLA_WARN << "nanosleep failed with state: " << errno; + } + } +} + } // namespace ola diff --git a/common/utils/StringUtils.cpp b/common/utils/StringUtils.cpp index c777bd016f..57f305159e 100644 --- a/common/utils/StringUtils.cpp +++ b/common/utils/StringUtils.cpp @@ -152,13 +152,16 @@ bool StringToBoolTolerant(const string &value, bool *output) { return false; } -bool StringToInt(const string &value, unsigned int *output, bool strict) { +bool StringToInt(const string &value, + unsigned int *output, + bool strict, + uint8_t base) { if (value.empty()) { return false; } char *end_ptr; errno = 0; - long long l = strtoll(value.data(), &end_ptr, 10); // NOLINT(runtime/int) + long long l = strtoll(value.data(), &end_ptr, base); // NOLINT(runtime/int) if (l < 0 || (l == 0 && errno != 0)) { return false; } @@ -168,10 +171,12 @@ bool StringToInt(const string &value, unsigned int *output, bool strict) { if (strict && *end_ptr != 0) { return false; } + + *output = static_cast(l); + if (l > static_cast(UINT32_MAX)) { // NOLINT(runtime/int) return false; } - *output = static_cast(l); return true; } @@ -199,16 +204,19 @@ bool StringToInt(const string &value, uint8_t *output, bool strict) { return true; } -bool StringToInt(const string &value, int *output, bool strict) { +bool StringToInt(const string &value, int *output, bool strict, uint8_t base) { if (value.empty()) { return false; } char *end_ptr; errno = 0; - long long l = strtoll(value.data(), &end_ptr, 10); // NOLINT(runtime/int) + long long l = strtoll(value.data(), &end_ptr, base); // NOLINT(runtime/int) if (l == 0 && errno != 0) { return false; } + + *output = static_cast(l); + if (value == end_ptr) { return false; } @@ -218,7 +226,7 @@ bool StringToInt(const string &value, int *output, bool strict) { if (l < INT32_MIN || l > INT32_MAX) { return false; } - *output = static_cast(l); + return true; } diff --git a/include/ola/Clock.h b/include/ola/Clock.h index 657f75bbeb..4163a9caa8 100644 --- a/include/ola/Clock.h +++ b/include/ola/Clock.h @@ -41,7 +41,9 @@ namespace ola { static const int USEC_IN_SECONDS = 1000000; -static const int ONE_THOUSAND = 1000; +static const int MSEC_IN_SEC = 1000; +static const uint64_t NSEC_IN_SEC = 1000000000; +static const int ONE_THOUSAND = MSEC_IN_SEC; /** * Don't use this class directly. It's an implementation detail of TimeInterval @@ -98,6 +100,12 @@ class BaseTimeVal { */ int64_t InMilliSeconds() const; + /** + * @brief Returns the entire BaseTimeVal as microseconds + * @return The entire BaseTimeVal in microseconds + */ + int64_t InMicroSeconds() const; + /** * @brief Returns the entire BaseTimeVal as microseconds * @return The entire BaseTimeVal in microseconds @@ -160,6 +168,7 @@ class TimeInterval { int32_t MicroSeconds() const { return m_interval.MicroSeconds(); } int64_t InMilliSeconds() const { return m_interval.InMilliSeconds(); } + int64_t InMicroSeconds() const { return m_interval.InMicroSeconds(); } int64_t AsInt() const { return m_interval.AsInt(); } std::string ToString() const { return m_interval.ToString(); } @@ -257,5 +266,30 @@ class MockClock: public Clock { private: TimeInterval m_offset; }; + +enum TimerGranularity { UNKNOWN, GOOD, BAD }; + +class Sleep { + public: + explicit Sleep(std::string caller); + + void setCaller(std::string caller) { m_caller = caller; } + + void usleep(TimeInterval requested); + void usleep(uint32_t requested); + void usleep(timespec requested); + + TimerGranularity getGranularity() { return m_granularity; } + bool CheckTimeGranularity(uint64_t wanted, uint64_t maxDeviation); + private: + std::string m_caller; + uint64_t m_wanted_granularity; + uint64_t m_max_granularity_deviation; + uint64_t m_clock_overhead; + + static const uint32_t BAD_GRANULARITY_LIMIT = 10; + + TimerGranularity m_granularity = UNKNOWN; +}; } // namespace ola #endif // INCLUDE_OLA_CLOCK_H_ diff --git a/include/ola/StringUtils.h b/include/ola/StringUtils.h index ff88dbdca8..bd8298ac44 100644 --- a/include/ola/StringUtils.h +++ b/include/ola/StringUtils.h @@ -262,12 +262,14 @@ bool StringToBoolTolerant(const std::string &value, bool *output); * @param[in] value the string to convert * @param[out] output a pointer where the value will be stored. * @param[in] strict this controls if trailing characters produce an error. + * @param[in] base sets the base of the input string, default decimal (10) * @returns true if the value was converted, false if the string was not an int * or the value was too large / small for the type. */ bool StringToInt(const std::string &value, unsigned int *output, - bool strict = false); + bool strict = false, + uint8_t base = 10); /** * @brief Convert a string to a uint16_t. @@ -300,11 +302,15 @@ bool StringToInt(const std::string &value, * @param[in] value the string to convert * @param[out] output a pointer where the value will be stored. * @param[in] strict this controls if trailing characters produce an error. + * @param[in] base sets the base of the input string, default decimal (10) * @returns true if the value was converted, false if the string was not an int * or the value was too large / small for the type. * @sa StringToInt. */ -bool StringToInt(const std::string &value, int *output, bool strict = false); +bool StringToInt(const std::string &value, + int *output, + bool strict = false, + uint8_t base = 10); /** * @brief Convert a string to a int16_t. diff --git a/plugins/convert_README_to_header.sh b/plugins/convert_README_to_header.sh index 86b1b42f09..e64c215822 100755 --- a/plugins/convert_README_to_header.sh +++ b/plugins/convert_README_to_header.sh @@ -30,8 +30,8 @@ outfilename=`basename $outfile`; # See http://stackoverflow.com/a/16576291 # On Mac OS's sed, \n is not recognized as a newline character, but # \[actual newline] works -desc=`sed -e ':a' -e 'N' -e '$!ba' -e 's/\"/\\\"/g' -e 's/\n/\\\\n"\\ -"/g' "$path/README.md"`; +desc=`sed -e ':a' -e 'N' -e '$!ba' -e 's#\\\#\\\\\\\#g' -e 's#\"#\\\"#g' -e 's#\n#\\\\n"\\ +"#g' "$path/README.md"`; identifier=`echo "PLUGINS_${plugin}_${outfilename%.h}_H_" | tr '[:lower:]' '[:upper:]'` diff --git a/plugins/ftdidmx/FtdiDmxDevice.cpp b/plugins/ftdidmx/FtdiDmxDevice.cpp index ed77711a10..ccb17822d1 100644 --- a/plugins/ftdidmx/FtdiDmxDevice.cpp +++ b/plugins/ftdidmx/FtdiDmxDevice.cpp @@ -56,15 +56,20 @@ FtdiDmxDevice::~FtdiDmxDevice() { bool FtdiDmxDevice::StartHook() { unsigned int interface_count = m_widget->GetInterfaceCount(); unsigned int successfully_added = 0; + unsigned int serial = 0; OLA_INFO << "Widget " << m_widget->Name() << " has " << interface_count << " interfaces."; for (unsigned int i = 1; i <= interface_count; i++) { + if (!StringToInt(m_widget->Serial().substr(2, 6), &serial, false, 36)) { + OLA_WARN << "StringToInt returned false, serial used: " << serial + << " Generated from: " << m_widget->Serial().substr(2, 6); + } FtdiInterface *port = new FtdiInterface(m_widget, static_cast(i)); if (port->SetupOutput()) { - AddPort(new FtdiDmxOutputPort(this, port, i, m_frequency)); + AddPort(new FtdiDmxOutputPort(this, port, i, m_frequency, serial)); successfully_added += 1; } else { OLA_WARN << "Failed to add interface: " << i; diff --git a/plugins/ftdidmx/FtdiDmxPort.h b/plugins/ftdidmx/FtdiDmxPort.h index 5eb3208c92..4496c9c59b 100644 --- a/plugins/ftdidmx/FtdiDmxPort.h +++ b/plugins/ftdidmx/FtdiDmxPort.h @@ -29,8 +29,12 @@ #include #include "ola/DmxBuffer.h" +#include "ola/rdm/DiscoveryAgent.h" +#include "ola/rdm/RDMResponseCodes.h" + #include "olad/Port.h" #include "olad/Preferences.h" + #include "plugins/ftdidmx/FtdiDmxDevice.h" #include "plugins/ftdidmx/FtdiWidget.h" #include "plugins/ftdidmx/FtdiDmxThread.h" @@ -44,10 +48,11 @@ class FtdiDmxOutputPort : public ola::BasicOutputPort { FtdiDmxOutputPort(FtdiDmxDevice *parent, FtdiInterface *interface, unsigned int id, - unsigned int freq) - : BasicOutputPort(parent, id), + unsigned int freq, + unsigned int serial) + : BasicOutputPort(parent, id, false, true), m_interface(interface), - m_thread(interface, freq) { + m_thread(interface, freq, serial) { m_thread.Start(); } ~FtdiDmxOutputPort() { @@ -59,6 +64,18 @@ class FtdiDmxOutputPort : public ola::BasicOutputPort { return m_thread.WriteDMX(buffer); } + void SendRDMRequest(ola::rdm::RDMRequest *request, + ola::rdm::RDMCallback *callback) { + m_thread.SendRDMRequest(request, callback); + } + + void RunFullDiscovery(ola::rdm::RDMDiscoveryCallback *callback) { + m_thread.RunFullDiscovery(callback); + } + void RunIncrementalDiscovery(ola::rdm::RDMDiscoveryCallback *callback) { + m_thread.RunIncrementalDiscovery(callback); + } + std::string Description() const { return m_interface->Description(); } private: diff --git a/plugins/ftdidmx/FtdiDmxThread.cpp b/plugins/ftdidmx/FtdiDmxThread.cpp index c31be8b898..4f5ccbb3a0 100644 --- a/plugins/ftdidmx/FtdiDmxThread.cpp +++ b/plugins/ftdidmx/FtdiDmxThread.cpp @@ -25,12 +25,25 @@ #include #include +#include #include +#include +#include #include "ola/Clock.h" #include "ola/Logging.h" #include "ola/StringUtils.h" +#include "ola/Constants.h" + +#include "ola/rdm/RDMCommand.h" +#include "ola/rdm/RDMControllerInterface.h" +#include "ola/rdm/RDMCommandSerializer.h" +#include "ola/rdm/RDMResponseCodes.h" +#include "ola/rdm/DiscoveryAgent.h" + +#include "ola/math/Random.h" + #include "plugins/ftdidmx/FtdiWidget.h" #include "plugins/ftdidmx/FtdiDmxThread.h" @@ -38,18 +51,37 @@ namespace ola { namespace plugin { namespace ftdidmx { -FtdiDmxThread::FtdiDmxThread(FtdiInterface *interface, unsigned int frequency) - : m_granularity(UNKNOWN), +FtdiDmxThread::FtdiDmxThread(FtdiInterface *interface, + unsigned int frequency, + unsigned int serial) + : m_timer("FtdiDmxThread"), + m_granularity(TimerGranularity::UNKNOWN), m_interface(interface), m_term(false), - m_frequency(frequency) { + m_frequency(frequency), + m_transaction_number(0), + m_discovery_agent(this), + m_uid(OPEN_LIGHTING_ESTA_CODE, serial), + m_pending_request(nullptr), + m_rdm_callback(nullptr), + m_mute_complete(nullptr), + m_unmute_complete(nullptr), + m_branch_callback(nullptr) { + m_timer.setCaller("FtdiDmxThread " + m_interface->Description()); + + if (serial == 0) { + ola::math::InitRandom(); + unsigned int deviceId = ola::math::Random(0, INT_MAX); + OLA_WARN << "Setting Device ID to random value due to lack of serial: " + << deviceId; + m_uid = ola::rdm::UID(OPEN_LIGHTING_ESTA_CODE, deviceId); + } } FtdiDmxThread::~FtdiDmxThread() { Stop(); } - /** * @brief Stop this thread */ @@ -58,6 +90,12 @@ bool FtdiDmxThread::Stop() { ola::thread::MutexLocker locker(&m_term_mutex); m_term = true; } + + if (m_pending_request != nullptr) { + destroyPendingRequest(); + destroyPendindingCallback(ola::rdm::RDM_FAILED_TO_SEND); + } + m_discovery_agent.Abort(); return Join(); } @@ -73,15 +111,167 @@ bool FtdiDmxThread::WriteDMX(const DmxBuffer &buffer) { } } +void FtdiDmxThread::SendRDMRequest(ola::rdm::RDMRequest *request, + ola::rdm::RDMCallback *callback) { + ola::thread::MutexLocker locker(&m_rdm_mutex); + if (m_pending_request == nullptr) { + m_pending_request = request; + m_rdm_callback = callback; + } else { + OLA_WARN << "Unable to queue RDM request, RDM operation already pending"; + } +} + +void FtdiDmxThread::RunFullDiscovery(ola::rdm::RDMDiscoveryCallback *callback) { + m_discovery_agent.StartFullDiscovery( + ola::NewSingleCallback(this, + &FtdiDmxThread::DiscoveryComplete, + callback)); +} + +void FtdiDmxThread::RunIncrementalDiscovery(rdm::RDMDiscoveryCallback *cb) { + m_discovery_agent.StartIncrementalDiscovery( + ola::NewSingleCallback(this, + &FtdiDmxThread::DiscoveryComplete, + cb)); +} + +/** + * Called when the discovery process finally completes + * @param callback the callback passed to StartFullDiscovery or + * StartIncrementalDiscovery that we should execute. + * @param status true if discovery worked, false otherwise + * @param uids the UIDSet of UIDs that were found. + */ +void FtdiDmxThread::DiscoveryComplete(ola::rdm::RDMDiscoveryCallback *callback, + bool status, + const ola::rdm::UIDSet &uids) { + if (status) { + OLA_DEBUG << "FTDI discovery complete: " << uids; + } else { + OLA_WARN << "FTDI discovery failed"; + } + if (callback) { + callback->Run(uids); + } +} + +/** + * @brief Method called to cleanup any outstanding callbacks + * @param state to return to caller when possible. + * + * @note All callbacks except the RDMCallback lack a way of reporting an error + * state to the caller. + */ +void FtdiDmxThread::destroyPendindingCallback(ola::rdm::RDMStatusCode state) { + MuteDeviceCallback *thread_mute_callback = nullptr; + UnMuteDeviceCallback *thread_unmute_callback = nullptr; + BranchCallback *thread_branch_callback = nullptr; + ola::rdm::RDMCallback *thread_rdm_callback = nullptr; + + destroyPendingRequest(); + + if (m_mute_complete != nullptr) { + thread_mute_callback = m_mute_complete; + m_mute_complete = nullptr; + thread_mute_callback->Run(false); + } else if (m_unmute_complete != nullptr) { + thread_unmute_callback = m_unmute_complete; + m_unmute_complete = nullptr; + thread_unmute_callback->Run(); + } else if (m_branch_callback != nullptr) { + thread_branch_callback = m_branch_callback; + m_branch_callback = nullptr; + thread_branch_callback->Run(nullptr, 0); + } else if (m_rdm_callback != nullptr) { + thread_rdm_callback = m_rdm_callback; + m_rdm_callback = nullptr; + ola::rdm::RunRDMCallback(thread_rdm_callback, state); + } +} + +void FtdiDmxThread::destroyPendingRequest() { + ola::rdm::RDMRequest *tmp_rdm_request = m_pending_request; + + m_pending_request = nullptr; + delete tmp_rdm_request; +} + +void FtdiDmxThread::MuteDevice(const ola::rdm::UID &target, + MuteDeviceCallback *mute_complete) { + ola::thread::MutexLocker locker(&m_rdm_mutex); + if (m_pending_request == nullptr) { + OLA_INFO << "Muting device"; + m_mute_complete = mute_complete; + m_pending_request = ola::rdm::NewMuteRequest(m_uid, + target, + m_transaction_number); + m_transaction_number++; + } else { + // Already pending request + OLA_WARN << "Unable to queue Mute request, " + << "RDM operation already pending"; + } +} + +void FtdiDmxThread::UnMuteAll(UnMuteDeviceCallback *unmute_complete) { + ola::thread::MutexLocker locker(&m_rdm_mutex); + if (m_pending_request == nullptr) { + OLA_INFO << "Sending UnMuteAll"; + m_unmute_complete = unmute_complete; + m_pending_request = ola::rdm::NewUnMuteRequest(m_uid, + ola::rdm::UID::AllDevices(), + m_transaction_number); + m_transaction_number++; + } else { + // Already pending request + OLA_WARN << "Unable to queue UnMuteAll request, " + << "RDM operation already pending"; + } +} + +void FtdiDmxThread::Branch(const ola::rdm::UID &lower, + const ola::rdm::UID &upper, + BranchCallback *callback) { + ola::thread::MutexLocker locker(&m_rdm_mutex); + if (m_pending_request == nullptr) { + OLA_INFO << "Sending branch"; + m_branch_callback = callback; + m_pending_request = + ola::rdm::NewDiscoveryUniqueBranchRequest(m_uid, + lower, + upper, + m_transaction_number); + m_transaction_number++; + } else { + // Already pending request + OLA_WARN << "Unable to queue Branch request, " + << "RDM operation already pending"; + } +} /** * @brief The method called by the thread */ void *FtdiDmxThread::Run() { - TimeStamp ts1, ts2, ts3; + OLA_INFO << "Starting FtdiDmxThread"; + TimeStamp ts1, ts2, ts3, lastDMX; Clock clock; CheckTimeGranularity(); DmxBuffer buffer; + bool sendRDM = false; + TimeInterval elapsed, interval; + int readBytes; + unsigned int additionalWait = 0; + unsigned char readBuffer[258]; + ola::io::ByteString packetBuffer; + + MuteDeviceCallback *thread_mute_callback = nullptr; + UnMuteDeviceCallback *thread_unmute_callback = nullptr; + BranchCallback *thread_branch_callback = nullptr; + ola::rdm::RDMCallback *thread_rdm_callback = nullptr; + + ola::rdm::RDMReply *received_reply = nullptr; int frameTime = static_cast(floor( (static_cast(1000) / m_frequency) + static_cast(0.5))); @@ -105,13 +295,40 @@ void *FtdiDmxThread::Run() { } clock.CurrentTime(&ts1); + if (m_pending_request != nullptr) { + elapsed = ts1 - lastDMX; + + if (elapsed.InMilliSeconds() < HALF_SECOND_MS || + (buffer.Size() < 24 && + elapsed.InMilliSeconds() < ALMOST_SECOND_MS)) { + if (!packetBuffer.empty()) { + packetBuffer.clear(); + } + + if (!ola::rdm::RDMCommandSerializer::PackWithStartCode( + *m_pending_request, &packetBuffer)) { + OLA_WARN << "RDMCommandSerializer failed. Dropping packet."; + + destroyPendindingCallback(ola::rdm::RDM_FAILED_TO_SEND); + sendRDM = false; + } else { + OLA_INFO << "OK To send RDM"; + sendRDM = true; + } + } else { + OLA_INFO << "NOK to send RDM (DMX interval)"; + sendRDM = false; + } + } else { + sendRDM = false; + } if (!m_interface->SetBreak(true)) { goto framesleep; } if (m_granularity == GOOD) { - usleep(DMX_BREAK); + m_timer.usleep(DMX_BREAK); } if (!m_interface->SetBreak(false)) { @@ -119,29 +336,163 @@ void *FtdiDmxThread::Run() { } if (m_granularity == GOOD) { - usleep(DMX_MAB); + m_timer.usleep(DMX_MAB); } - if (!m_interface->Write(buffer)) { + if (!sendRDM) { + if (!m_interface->Write(buffer)) { + goto framesleep; + } else { + clock.CurrentTime(&lastDMX); + } + } else { + if (m_interface->Write(&packetBuffer)) { + OLA_INFO << "RDM packet written to line"; + if (m_pending_request->IsDUB()) { + m_timer.usleep(MIN_WAIT_DUB_US); + + readBytes = m_interface->Read(readBuffer, sizeof(readBuffer)); + OLA_INFO << "DUB Read: " << readBytes; + if (m_branch_callback != nullptr) { + thread_branch_callback = m_branch_callback; + m_branch_callback = nullptr; + destroyPendingRequest(); + thread_branch_callback->Run(readBuffer, + (readBytes >= 0 ? readBytes : 0)); + } + } else if (!m_pending_request->DestinationUID().IsBroadcast()) { + m_timer.usleep(MIN_WAIT_RDM_US); + readBytes = m_interface->Read(readBuffer, sizeof(readBuffer)); + + if (readBytes > 0) { + /* + * The following section of code tries to deal with replies that + * are being broadcast too slowly, I don't have equipment that + * malfunctions in this way so I have no way of testing this. + */ + if (readBytes < 4) { + OLA_WARN << "FTDI Didn't receive at least 4B during minWait"; + additionalWait = (MIN_WAIT_RDM_US / readBytes)*(4 - readBytes); + m_timer.usleep(additionalWait); + readBytes += m_interface->Read(readBuffer + readBytes, + sizeof(readBuffer) - readBytes); + } + /* + * This section of code does minimal verification of the received + * frame. + * This assumes that the 4th byte in the buffer is the 3rd byte of + * the RDM Frame which defines length and no checksum has been done + * yet. + */ + if (readBytes >= 4) { + if (readBuffer[0] == 0x00 && readBuffer[1] == 0xcc) { + while (((readBytes - 1) < readBuffer[3]) && + elapsed.InMilliSeconds() <= 1250) { + OLA_WARN << "FTDI Didn't receive full frame during minWait"; + additionalWait = (MIN_WAIT_RDM_US / readBytes) * + (readBuffer[3] - readBytes + 1); + m_timer.usleep(additionalWait); + readBytes += m_interface->Read( + readBuffer + readBytes, + sizeof(readBuffer) - readBytes); + + clock.CurrentTime(&ts2); + elapsed = ts2 - ts1; + } + if (readBytes < (readBuffer[3] + 1)) { + OLA_WARN << "Discarding due to timeout."; + destroyPendindingCallback(rdm::RDM_TIMEOUT); + } else { + received_reply = rdm::RDMReply::FromFrame( + rdm::RDMFrame(readBuffer+1, readBytes-1), + m_pending_request); + + if (received_reply != nullptr) { + if (m_mute_complete != nullptr) { + thread_mute_callback = m_mute_complete; + m_mute_complete = nullptr; + + if (received_reply->Response()->SourceUID() == + m_pending_request->DestinationUID()) { + destroyPendingRequest(); + thread_mute_callback->Run(true); + } else { + destroyPendingRequest(); + thread_mute_callback->Run(false); + } + } else if (m_rdm_callback != nullptr) { + thread_rdm_callback = m_rdm_callback; + m_rdm_callback = nullptr; + + if (readBytes > 0) { + destroyPendingRequest(); + thread_rdm_callback->Run(received_reply); + } else { + destroyPendingRequest(); + RunRDMCallback(thread_rdm_callback, rdm::RDM_TIMEOUT); + } + } + } else { + OLA_WARN << "received reply is nullptr"; + destroyPendindingCallback(rdm::RDM_INVALID_RESPONSE); + } + // Reset reply buffer. + if (received_reply != nullptr) { + rdm::RDMReply *tmp = received_reply; + received_reply = nullptr; + delete tmp; + } + } // End handling seemingly valid data + } else { + destroyPendindingCallback(rdm::RDM_INVALID_RESPONSE); + } + } else { + destroyPendindingCallback(rdm::RDM_TIMEOUT); + } + } else { + destroyPendindingCallback(rdm::RDM_TIMEOUT); + } + } else { + if (m_unmute_complete != nullptr) { + thread_unmute_callback = m_unmute_complete; + m_unmute_complete = nullptr; + destroyPendingRequest(); + thread_unmute_callback->Run(); + } else if (m_rdm_callback != nullptr) { + thread_rdm_callback = m_rdm_callback; + m_rdm_callback = nullptr; + destroyPendingRequest(); + ola::rdm::RunRDMCallback(thread_rdm_callback, + ola::rdm::RDM_WAS_BROADCAST); + } + } + } else { + /* Something went wrong, already reported at hw level + * but we'll need to handle the callbacks. + */ + destroyPendindingCallback(ola::rdm::RDM_FAILED_TO_SEND); + } // End of Write loop */ + goto framesleep; } framesleep: // Sleep for the remainder of the DMX frame time clock.CurrentTime(&ts2); - TimeInterval elapsed = ts2 - ts1; + elapsed = ts2 - ts1; if (m_granularity == GOOD) { while (elapsed.InMilliSeconds() < frameTime) { - usleep(1000); + m_timer.usleep(1000); clock.CurrentTime(&ts2); elapsed = ts2 - ts1; } } else { // See if we can drop out of bad mode. - usleep(1000); + CheckTimeGranularity(); + m_timer.usleep(1000); clock.CurrentTime(&ts3); - TimeInterval interval = ts3 - ts2; + interval = ts3 - ts2; if (interval.InMilliSeconds() < BAD_GRANULARITY_LIMIT) { m_granularity = GOOD; OLA_INFO << "Switching from BAD to GOOD granularity for ftdi thread"; @@ -162,19 +513,52 @@ void *FtdiDmxThread::Run() { * @brief Check the granularity of usleep. */ void FtdiDmxThread::CheckTimeGranularity() { - TimeStamp ts1, ts2; - Clock clock; + m_timer.CheckTimeGranularity(8, 4); + m_granularity = m_timer.getGranularity(); +} - clock.CurrentTime(&ts1); - usleep(1000); - clock.CurrentTime(&ts2); +/** + * @brief FtdiDmxThread::CheckEchoState + * @return true is echo is on + * + * echo is always assumed on unless proven otherwise. + * +bool FtdiDmxThread::CheckEchoState() { + ola::io::ByteString testPattern; + testPattern.push_back(0xff); + testPattern.push_back(0x55); + testPattern.push_back(0xff); + testPattern.push_back(0xaa); + testPattern.push_back(0xff); + testPattern.push_back(0x55); + testPattern.push_back(0xff); + testPattern.push_back(0xaa); + testPattern.push_back(0xff); + testPattern.push_back(0x55); + testPattern.push_back(0xff); + testPattern.push_back(0xaa); - TimeInterval interval = ts2 - ts1; - m_granularity = (interval.InMilliSeconds() > BAD_GRANULARITY_LIMIT) ? - BAD : GOOD; - OLA_INFO << "Granularity for FTDI thread is " - << ((m_granularity == GOOD) ? "GOOD" : "BAD"); -} + unsigned char readBuffer[13]; + int readBytes; + + if(!m_interface->PurgeBuffers()) { + OLA_WARN << "Failed to clear buffers so can't verify echo state."; + return true; + } + + if(m_interface->Write(&testPattern)) { + readBytes = m_interface->Read(readBuffer, 13); + if(readBytes == 0) { + return false; + } else { + // check other + } + } else { + //handle write error + } + // If we can't verify beyond a doubt that echo is off it is assumed on. + return true; +}*/ } // namespace ftdidmx } // namespace plugin } // namespace ola diff --git a/plugins/ftdidmx/FtdiDmxThread.h b/plugins/ftdidmx/FtdiDmxThread.h index 26f767e40f..f1ed438abc 100644 --- a/plugins/ftdidmx/FtdiDmxThread.h +++ b/plugins/ftdidmx/FtdiDmxThread.h @@ -26,32 +26,94 @@ #ifndef PLUGINS_FTDIDMX_FTDIDMXTHREAD_H_ #define PLUGINS_FTDIDMX_FTDIDMXTHREAD_H_ +#include +#include + #include "ola/DmxBuffer.h" #include "ola/thread/Thread.h" +#include "ola/rdm/RDMCommand.h" +#include "ola/rdm/DiscoveryAgent.h" +#include "ola/rdm/RDMResponseCodes.h" namespace ola { namespace plugin { namespace ftdidmx { -class FtdiDmxThread : public ola::thread::Thread { +enum { + HALF_SECOND_MS = 500, + ALMOST_SECOND_MS = 900, + MIN_WAIT_DUB_US = 58000, + MIN_WAIT_RDM_US = 30000, +}; + +class FtdiDmxThread + : public ola::thread::Thread, + public ola::rdm::DiscoverableRDMControllerInterface, + public ola::rdm::DiscoveryTargetInterface { public: - FtdiDmxThread(FtdiInterface *interface, unsigned int frequency); + FtdiDmxThread(FtdiInterface *interface, + unsigned int frequency, + unsigned int serial); ~FtdiDmxThread(); bool Stop(); void *Run(); bool WriteDMX(const DmxBuffer &buffer); + void SendRDMRequest(ola::rdm::RDMRequest *request, + ola::rdm::RDMCallback *callback); + + void RunFullDiscovery(ola::rdm::RDMDiscoveryCallback *callback); + void RunIncrementalDiscovery(ola::rdm::RDMDiscoveryCallback *cb); + + void MuteDevice(const ola::rdm::UID &target, + MuteDeviceCallback *mute_complete); + + void UnMuteAll(UnMuteDeviceCallback *unmute_complete); + + void Branch(const ola::rdm::UID &lower, + const ola::rdm::UID &upper, + BranchCallback *callback); + private: - enum TimerGranularity { UNKNOWN, GOOD, BAD }; + ola::Sleep m_timer; TimerGranularity m_granularity; FtdiInterface *m_interface; bool m_term; unsigned int m_frequency; + DmxBuffer m_buffer; ola::thread::Mutex m_term_mutex; ola::thread::Mutex m_buffer_mutex; + ola::thread::Mutex m_rdm_mutex; + + uint8_t m_transaction_number; + ola::rdm::DiscoveryAgent m_discovery_agent; + ola::rdm::UID m_uid; + + ola::rdm::RDMRequest *m_pending_request; + ola::rdm::RDMCallback *m_rdm_callback; + MuteDeviceCallback *m_mute_complete; + UnMuteDeviceCallback *m_unmute_complete; + BranchCallback *m_branch_callback; + + void DiscoveryComplete(ola::rdm::RDMDiscoveryCallback *callback, + bool status, + const ola::rdm::UIDSet &uids); + /** + * @brief Method called to cleanup any outstanding callbacks + * @param state to return to caller when possible. + * + * @note All callbacks except the RDMCallback lack a way of reporting an + * error state to the caller. + */ + void destroyPendindingCallback(ola::rdm::RDMStatusCode state); + /** + * @brief Method called to cleanup the pending request without leaking + * memory. + */ + void destroyPendingRequest(); void CheckTimeGranularity(); diff --git a/plugins/ftdidmx/FtdiWidget.cpp b/plugins/ftdidmx/FtdiWidget.cpp index 384ed20381..8287b0fbaf 100644 --- a/plugins/ftdidmx/FtdiWidget.cpp +++ b/plugins/ftdidmx/FtdiWidget.cpp @@ -44,6 +44,7 @@ #include #include +#include #include #include @@ -53,6 +54,9 @@ #include "ola/BaseTypes.h" #include "ola/StringUtils.h" #include "ola/Constants.h" +#include "ola/rdm/RDMCommand.h" + + #include "plugins/ftdidmx/FtdiWidget.h" namespace ola { @@ -209,10 +213,11 @@ void FtdiWidget::Widgets(vector *widgets) { ftdi_free(ftdi); } -FtdiInterface::FtdiInterface(const FtdiWidget * parent, +FtdiInterface::FtdiInterface(FtdiWidget *parent, const ftdi_interface interface) : m_parent(parent), - m_interface(interface) { + m_interface(interface), + m_echoState(UNKNOWN) { memset(&m_handle, '\0', sizeof(struct ftdi_context)); ftdi_init(&m_handle); } @@ -225,10 +230,28 @@ FtdiInterface::~FtdiInterface() { ftdi_deinit(&m_handle); } +std::string FtdiInterface::Description() const { + switch (m_interface) { + case INTERFACE_A: + return m_parent->Description() + " Port: 1"; + case INTERFACE_B: + return m_parent->Description() + " Port: 2"; + case INTERFACE_C: + return m_parent->Description() + " Port: 3"; + case INTERFACE_D: + return m_parent->Description() + " Port: 4"; + case INTERFACE_ANY: + return m_parent->Description(); + } + return m_parent->Description() + " Interface detection failure."; +} + + bool FtdiInterface::SetInterface() { OLA_INFO << "Setting interface to: " << m_interface; + m_parent->setId(static_cast(m_interface)); if (ftdi_set_interface(&m_handle, m_interface) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -241,7 +264,7 @@ bool FtdiInterface::Open() { OLA_WARN << m_parent->Name() << " has no serial number, which might cause " << "issues with multiple devices"; if (ftdi_usb_open(&m_handle, m_parent->Vid(), m_parent->Pid()) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -254,7 +277,7 @@ bool FtdiInterface::Open() { if (ftdi_usb_open_desc(&m_handle, m_parent->Vid(), m_parent->Pid(), m_parent->Name().c_str(), m_parent->Serial().c_str()) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -265,7 +288,7 @@ bool FtdiInterface::Open() { bool FtdiInterface::Close() { if (ftdi_usb_close(&m_handle) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -279,7 +302,7 @@ bool FtdiInterface::IsOpen() const { bool FtdiInterface::Reset() { if (ftdi_usb_reset(&m_handle) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -289,7 +312,7 @@ bool FtdiInterface::Reset() { bool FtdiInterface::SetLineProperties() { if ((ftdi_set_line_property(&m_handle, BITS_8, STOP_BIT_2, NONE) < 0)) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -299,7 +322,7 @@ bool FtdiInterface::SetLineProperties() { bool FtdiInterface::SetBaudRate(int speed) { if (ftdi_set_baudrate(&m_handle, speed) < 0) { - OLA_WARN << "Error setting " << m_parent->Description() << " to baud rate " + OLA_WARN << "Error setting " << Description() << " to baud rate " << "of " << speed << " - " << ftdi_get_error_string(&m_handle); return false; } else { @@ -309,7 +332,7 @@ bool FtdiInterface::SetBaudRate(int speed) { bool FtdiInterface::SetFlowControl() { if (ftdi_setflowctrl(&m_handle, SIO_DISABLE_FLOW_CTRL) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -319,7 +342,7 @@ bool FtdiInterface::SetFlowControl() { bool FtdiInterface::ClearRts() { if (ftdi_setrts(&m_handle, 0) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -329,7 +352,7 @@ bool FtdiInterface::ClearRts() { bool FtdiInterface::PurgeBuffers() { if (ftdi_usb_purge_buffers(&m_handle) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -340,7 +363,7 @@ bool FtdiInterface::PurgeBuffers() { bool FtdiInterface::SetBreak(bool on) { if (ftdi_set_line_property2(&m_handle, BITS_8, STOP_BIT_2, NONE, (on ? BREAK_ON : BREAK_OFF)) < 0) { - OLA_WARN << m_parent->Description() << " " + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; } else { @@ -349,30 +372,102 @@ bool FtdiInterface::SetBreak(bool on) { } bool FtdiInterface::Write(const ola::DmxBuffer& data) { - unsigned char buffer[DMX_UNIVERSE_SIZE + 1]; - unsigned int length = DMX_UNIVERSE_SIZE; - buffer[0] = DMX512_START_CODE; + ola::io::ByteString packetBuffer; + packetBuffer[0] = DMX512_START_CODE; + + if (data.Size() > 0) { + packetBuffer.append(data.GetRaw(), data.Size()); + if (data.Size() < 24) { + packetBuffer.append(24 - data.Size(), '\x00'); + } + } else { + packetBuffer.append(24, '\x00'); + } + + return Write(&packetBuffer); +} - data.Get(buffer + 1, &length); - if (ftdi_write_data(&m_handle, buffer, length + 1) < 0) { - OLA_WARN << m_parent->Description() << " " +bool FtdiInterface::Write(ola::io::ByteString *packet) { + int bytesWritten = ftdi_write_data( + &m_handle, + const_cast(packet->data()), + packet->size()); + + int size = packet->size(); + + /* In case echo may be on immediately read the amount of bytes that were + * put on the line so that read will start at point of reception. + */ + if (bytesWritten > 0 && m_echoState != OFF) { + unsigned char readBuffer[bytesWritten+1]; + ftdi_read_data(&m_handle, readBuffer, bytesWritten+1); + } + + if (bytesWritten < 0) { + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); return false; + } else if (bytesWritten != static_cast(packet->size())) { + OLA_WARN << "Bytes Written: " << bytesWritten + << " != Packet Size: " << size; + return false; } else { return true; } } -bool FtdiInterface::Read(unsigned char *buff, int size) { +int FtdiInterface::Read(unsigned char *buff, int size) { int read = ftdi_read_data(&m_handle, buff, size); - if (read <= 0) { - OLA_WARN << m_parent->Description() << " " + + OLA_DEBUG << Description() << "Read: " << read; + + if (read < 0) { + OLA_WARN << Description() << " " << ftdi_get_error_string(&m_handle); - return false; - } else { - return true; } + return read; +} + +void FtdiInterface::DetectEchoState() { + unsigned char testPattern[] = "\xff\x55\xff\xaa\xff\x0f\xf0"; + int size = sizeof(testPattern); + unsigned char readBuffer[(size + 1)]; + + int bytesWritten = ftdi_write_data(&m_handle, testPattern, size); + if (bytesWritten < 0) { + OLA_WARN << Description() << " " + << ftdi_get_error_string(&m_handle); + m_echoState = UNKNOWN; + return; + } else if (bytesWritten != size) { + OLA_WARN << "Bytes Written: " << bytesWritten + << " != Pattern Size: " << size + << " Attempting detection of what was written."; + } + int bytesRead = ftdi_read_data(&m_handle, readBuffer, bytesWritten); + if (bytesRead == 0) { + OLA_INFO << Description() << " No data read, echo state OFF."; + m_echoState = OFF; + } else if (bytesRead < 0) { + OLA_WARN << Description() << " " + << ftdi_get_error_string(&m_handle) << "\n" + << "Echo state UNKNOWN"; + m_echoState = UNKNOWN; + return; + } else if (bytesRead <= bytesWritten) { + for (int i = 0; i < bytesRead; i++) { + if (testPattern[i] != readBuffer[i]) { + m_echoState = UNKNOWN; + OLA_WARN << Description() + << " Mismatch in read data and test pattern, " + << "echo state remains UNKNOWN."; + return; + } + } + } + OLA_INFO << Description() << " Echo state ON."; + m_echoState = ON; } bool FtdiInterface::SetupOutput() { @@ -417,6 +512,8 @@ bool FtdiInterface::SetupOutput() { return false; } + DetectEchoState(); + return true; } diff --git a/plugins/ftdidmx/FtdiWidget.h b/plugins/ftdidmx/FtdiWidget.h index 2f856e5f2b..51be556a5b 100644 --- a/plugins/ftdidmx/FtdiWidget.h +++ b/plugins/ftdidmx/FtdiWidget.h @@ -128,7 +128,7 @@ class FtdiWidget { * @brief Construct a new FtdiWidget instance for one widget. * @param serial The widget's USB serial number * @param name The widget's USB name (description) - * @param id id based on order of adding it seems from the code + * @param id based on order of adding it seems from the code * @param vid The VendorID of the device, def = FtdiWidgetInfo::ftdi_vid * @param pid The ProductID of the device, def = FtdiWidgetInfo::ft232_pid */ @@ -153,8 +153,10 @@ class FtdiWidget { /** @brief Get the widget's FTD2XX ID number */ uint32_t Id() const { return m_id; } + void setId(uint32_t id) { m_id = id; } + std::string Description() const { - return m_name + " with serial number : " + m_serial +" "; + return m_name + " serial: " + m_serial; } /** @brief Get Widget available interface count **/ @@ -180,18 +182,22 @@ class FtdiWidget { const uint16_t m_pid; }; +enum EchoState { + UNKNOWN, + ON, + OFF +}; + class FtdiInterface { public: - FtdiInterface(const FtdiWidget * parent, + FtdiInterface(FtdiWidget * parent, const ftdi_interface interface); virtual ~FtdiInterface(); - std::string Description() const { - return m_parent->Description(); - } + std::string Description() const; - /** @brief Set interface on the widget */ + /** @brief Pick interface on multiport widgets */ bool SetInterface(); /** @brief Open the widget */ @@ -224,19 +230,32 @@ class FtdiInterface { /** @brief Toggle communications line BREAK condition on/off */ bool SetBreak(bool on); - /** @brief Write data to a previously-opened line */ + /** @brief Write data to a previously-opened line, DMX only */ bool Write(const ola::DmxBuffer &data); - /** @brief Read data from a previously-opened line */ - bool Read(unsigned char* buff, int size); + /** @brief Write prepared packets to previously opened line, + * agnostic to packet contents + * @pre The whole line setup and opening sequence. + * Should haven been performed by the plugin before ever reaching this. + */ + bool Write(ola::io::ByteString *packet); + + /** @brief Read data from a previously-opened line + * @pre The whole line setup and opening sequence. + * Should haven been performed by the plugin before ever reaching this. + */ + int Read(unsigned char* buff, int size); + + void DetectEchoState(); /** @brief Setup device for DMX Output **/ bool SetupOutput(); private: - const FtdiWidget * m_parent; + FtdiWidget * m_parent; struct ftdi_context m_handle; const ftdi_interface m_interface; + EchoState m_echoState; }; // FtdiInterface } // namespace ftdidmx } // namespace plugin diff --git a/plugins/ftdidmx/README.md b/plugins/ftdidmx/README.md index b0ce125a37..93ad42ed01 100644 --- a/plugins/ftdidmx/README.md +++ b/plugins/ftdidmx/README.md @@ -1,10 +1,89 @@ FTDI USB Chipset DMX Plugin =========================== -This plugin is compatible with Enttec OpenDmx and other FTDI chipset based -USB to DMX converters where the host needs to create the DMX stream itself -and not the interface (the interface has no microprocessor to do so). +This plugin is compatible with Enttec Open DMX USB and other FTDI chipset +based USB to DMX converters where the host needs to create the DMX stream +itself and not the interface (the interface has no microprocessor to do so). +## RDM Support + +FTDI based chips/outputs that have the correct line biasing setup should be +able to output and receive RDM packets. + +At this stage we can't guarantee that the plugin meets all timing specs since +I don't have any faulty RDM equipment to test with. It has however been tested +with multiple responders on the line and discovery works correctly. + +Ideally FTDI local echo should be disabled to prevent transimitted data from +being read. +The driver does have echo detection and attempts to correct for it, however +we can't commit to this working 100% off the time and it will add +unpredictability. + +The same applies for line biasing, things do seem to work without it but we +don't recommend it. + +### Proper Line Biasing +For simple DMX output (and input) all that is needed is a 130 Ohm resistor +between data+ and data-. + +For RDM 2 additional resistors of 680 Ohm are needed: +1. Pull-up connects between Data+ and VCC +2. Pull-down between Data- and the common/ground. + +*Please note:* these values are based on the book "Control Freak" by Simon +Howell and are what I used in my test setup, however the standard actually +proscribes 133 Ohm and 562 Ohm resistors. + +#### Diagram + +V + --- + | + +----------+ + | | + | [680 Ohm] + |\| | + | \----------+---------- DMX Pin 3 (Data+) + | \ | + | \ [130 Ohm] + | / | + | / | + | /o---------+---------- DMX Pin 2 (Data-) + |/| | + | [680 Ohm] + | | + +----------+---------- DMX Pin 1 (Common) + | + +-------[<=20 Ohm]---+ + | | + Common ----- + --- + - + +#### FTDI Board DB9 pinouts +Based on the FTDI spec this is the pinout to be used on their DB9 connectors +and the way to connect the lines and resistors. + +1. Data- (130 Ohm -> 3, 680 Ohm -> 5) +2. Data+ (680 Ohm -> 9) +3. 130 Ohm -> 1 +4. Not used +5. GND (680 Ohm -> 1) +6. Not used +7. Short with 8 to disable echo +8. Short with 7 to disable echo +9. +5VDC (680 Ohm -> 2) + +The FTDI spec and the DMX/RDM spec disagree where the 130 Ohm resistor should +terminate, it could be that really it should be between pins 1 and 2, both +ways have worked for me for pure output, for RDM I only tested this way. + +### RDM was tested with +- FT4232H (USB-COM485-PLUS4) +- USB-RS485-WE-1800-BT + +### RDM was tested as not working with +- Enttec Open DMX USB (we believe this may be due to incorrect line biasing) ## Config file: ola-ftdidmx.conf