Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

302 changed files with 2756 additions and 38592 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "docs/doxygen_theme_flat_design"]
path = docs/doxygen_theme_flat_design
url = https://github.com/kcwongjoe/doxygen_theme_flat_design.git

View File

@ -1,41 +1,47 @@
cmake_minimum_required(VERSION 3.20)
set(DEFAULT_BUILD_TYPE "Release")
project(lcp VERSION 0.3.1)
cmake_minimum_required(VERSION 3.14)
project(ESTA VERSION 1.0.0 LANGUAGES CXX)
set(DEFAULT_BUILD_TYPE "Release")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG")
endif()
add_subdirectory(example)
add_subdirectory(lib)
add_subdirectory(platform)
add_subdirectory(protocol)
add_subdirectory(test)
set(SOURCE_FILES
acn/acn.h
acn/appliance.cpp
acn/appliance.h
acn/component.h
acn/dmp.cpp
acn/dmp.h
acn/pdu.cpp
acn/pdu.h
acn/rlp-tcp.cpp
acn/rlp-tcp.h
acn/rlp-udp.cpp
acn/rlp-udp.h
acn/rlp.cpp
acn/rlp.h
acn/sdt-udp.cpp
acn/sdt-udp.h
acn/sdt.cpp
acn/sdt.h
dmx/universe.cpp
dmx/universe.h
sacn/data.cpp
sacn/data.h
sacn/extended.h
sacn/receiver.cpp
sacn/receiver.h
sacn/sacn.h
sacn/universe.cpp
sacn/universe.h
uuid/uuid.cpp
uuid/uuid.h
)
#if (CMAKE_BUILD_TYPE MATCHES "^[Rr]elease")
option(BUILD_DOC "Build documentation" ON)
find_package(Doxygen)
if (DOXYGEN_FOUND)
# set input and output files
set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile.in)
set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
# request to configure the file
configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)
message("Doxygen build started")
# note the option ALL which allows to build the docs together with the application
add_custom_target(docs ALL
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/docs
COMMENT "Generating API documentation with Doxygen"
VERBATIM )
else (DOXYGEN_FOUND)
message("Doxygen need to be installed to generate the doxygen documentation")
endif (DOXYGEN_FOUND)
#endif()
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES})
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION})
set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION 1)
target_compile_definitions(${PROJECT_NAME} PRIVATE ESTA_LIBRARY)

View File

@ -1,4 +1,4 @@
Copyright (c) 2020-2022 Kevin Matz
Copyright (c) 2020 Kevin Matz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

110
README.md
View File

@ -1,50 +1,15 @@
# OpenLCP
# LibESTA
A collection of C++ libraries for Lighting Control Protocols, focusing on ESTA
sponsored ANSI standards.
<img alt="ESTA Protocols Block Diagram"
src="https://git.company235.com/official/OpenLCP/raw/branch/master/docs/establockdiagram.png"
width="80%">
## Object Architecture
The library encompases both the structured data being communicated between
controllable components, and the control objects that expose the data in
meaningful ways, without needing to know or understand the underlying protocols.
### Objects
Control objects are structured thusly;
<img alt="Architecture Flowchart"
src="https://git.company235.com/official/OpenLCP/raw/branch/master/docs/classdiagram.png"
width="100%">
### Data
Serveral of the ESTA protocols use the E1.17 Protocol Data Unit to transfer
data. The `PDU` are a series of nested hierarchical data structures. `PDU` may
encapsulate one or more `PDU` of an embedded protocol, or be a data endpoint
for that protocol. The following slide describes the relational hierocracy of
the `PDU` within `ACN`, `sACN`, `RDMnet`, and `OTP`. While similiar in
structure and concept, the `PDU` in `RDMnet` do not inherit directly from the
`PDU` in `ACN`, and the `PDU` in `OTP` are notably simpler, as data segments
may not be inherited from sibling `PDU` in the data.
<img alt="PDU Hierarch Flowchart"
src="https://git.company235.com/official/OpenLCP/raw/branch/master/docs/pduflowchart.png"
width="90%">
A collection of C++ libraries for ESTA sponsored ANSI standards.
## Supported Standards
| Protocol Name | Standard | Implementation Status |
| :- | :-: | :-: |
| 0 to 10 V Analog Control | E1.3 | |
| 0 to 10 V Analog Control | E1.3 | |
| USITT DMX512-A | E1.11 | Data Abstraction |
| ACN Root Layer Protocol (RLP) | E1.17 | |
| ACN Session Data Transport Protocol (SDT) | E1.17 | i/o |
| ACN Root Layer Protocol (RLP) | E1.17 | Rx |
| ACN Session Data Transport Protocol (SDT) | E1.17 | |
| ACN Device Management Protocol (DMP) | E1.17 | Limited |
| ACN Device Description Language (DDL) | E1.17 | |
| EPI 10 - Autogeneration of Multicast Address on IPv4 Networks | E1.17 | |
@ -53,68 +18,27 @@ may not be inherited from sibling `PDU` in the data.
| EPI 13 - Allocation of Internet Protocol Version 4 Addresses to ACN Hosts | E1.17 | |
| EPI 15 - ACN Allocation of Multicast Addresses on IPv4 Networks | E1.17 | |
| EPI 16 - ESTA Registered Names and Identifiers | E1.17 | |
| EPI 17 - ACN Root Layer Protocol Operation on UDP | E1.17 | |
| EPI 17 - ACN Root Layer Protocol Operation on UDP | E1.17 | Rx |
| EPI 18 - Operation of SDT on UDP Networks | E1.17 | |
| EPI 19 - ACN Discovery on IP Networks | E1.17 | Names |
| EPI 19 - ACN Discovery on IP Networks | E1.17 | |
| EPI 20 - MTU Size for ACN on IPv4 Networks | E1.17 | |
| EPI 22 - DDL Core Modules for ACN Devices | E1.17 | |
| Remote Device Management (RDM) | E1.20 | Responder |
| RDM Subdevice | E1.20 | ✓ |
| RDM Sensors | E1.20 | ✓ |
| RDM Discovery | E1.20 | ✓ |
| RDM Required PIDs | E1.20 | ✓ |
| RDM Status collection PIDs | E1.20 | ✓ |
| Remote Device Management (RDM) | E1.20 | |
| EPI 23 - Device Identification Subdevice | E1.30-1 | |
| EPI 25 - Time Reference in ACN Systems Using SNTP and NTP | E1.30-3 | |
| EPI 26 - DDL Extensions for DMX and RDM Devices | E1.30-4 | |
| EPI 32 - Identification of Draft DDL Modules | E1.30-10 | |
| EPI 33 - ACN RLP Operation on TCP | E1.30-11 | ✓ |
| Streaming ACN (sACN) | E1.31 | Data/Discovery |
| sACN Receiver | E1.31 | ✓ |
| sACN Source | E1.31 | ✓ |
| sACN Data | E1.31 | ✓ |
| sACN Sync | E1.31 | Rx |
| sACN Discovery | E1.31 | ✓ |
| sACN Preview | E1.31 | - |
| RDMNet | E1.33 | i/o |
| RDM Dimmer Message Sets | E1.37-1 | Normative |
| RDM IPv4 & DNS Configuration Messages | E1.37-2 | Normative |
| RDM Gateway & Splitter Configuration Messages | E1.37-7 | Normative |
| EPI 33 - ACN RLP Operation on TCP | E1.30-11 | |
| Streaming ACN (sACN) | E1.31 | Data |
| RDMNet | E1.33 | |
| RDM Dimmer Message Sets | E1.37-1 | |
| RDM IPv4 & DNS Configuration Messages | E1.37-2 | |
| RDM Gateway & Splitter Configuration Messages | E1.37-7 | |
### Non-Standard Protocols
| Protocol | Designer | Version | Implementation Status |
| :- | :- | :-: | :-: |
| Art-Net | Artistic Licence Holdings Ltd. | Protocol 4 v1.4 | |
| OSC | Open Sound Control | Spec 1.1 | ✓ |
### Dependent Protocols
| Protocol Name | Standard | Referenced By | Implementation Status |
| :- | :-: |:-: | :-: |
| Universally Unique Identifier (UUID) | RFC 4122 | RLP, DDL | Partial |
| Service Location Protocol (SLP) | RFC 2609 | EPI 19 | |
| Trivial File Transfer Protocol (TFTP) | RFC 1350 | EPI 25 | |
| Network Time Protocol (NTP) | RFC 5909 | OSC | |
| Serial Line Internet Protocol (SLIP) | RFC 1055 | OSC | |
## License
Copyright &copy; 2020-2023 Kevin Matz
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.
|UUID Universally Unique Identifier | RFC 4122 | RLP, DDL | Partial |
|Service Location Protocol (SLP) | RFC 2609 | EPI 19 | |
|Trivial File Transfer Protocol (TFTP) | RFC 1350 | EPI 25 | |

35
acn/README.md Normal file
View File

@ -0,0 +1,35 @@
# Library for E1.17 ACN
## `Architecture for Control Networks`
## Top level include
Include `acn.h` for full protocol support.
> ```#include "acn.h"```
## Logical components
### Protocol Handling
* ACN Architecture
* `pdu.h`: [`ACN::PDU`]
* `rlp.h`: [`ACN::RLP`]
* Device Management Protocol
* `dmp.h`: [`ACN::DMP`]
* Session Data Transport
* `sdt.h`: [`ACN::SDT`]
### EPI Support
* EPI 17. ACN Root Layer Protocol
Operation on UDP
* `rlp-udp.h`: [`ACN::RLP::UDP`]
* EPI 18. Operation of SDT on UDP Networks
* `sdt-udp.h`: [`ACN::SDT::UDP`]
* EPI 33. ACN Root Layer Protocol Operation on TCP
* `rlp-tcp.h`: [`ACN::RLP::TCP`]

View File

@ -1,5 +1,5 @@
/*
rlp.h
acn.h
Copyright (c) 2020 Kevin Matz (kevin.matz@gmail.com)
@ -23,58 +23,38 @@
*/
#pragma once
#include <cstdint>
#include <memory>
#include "appliance.h"
#include "component.h"
#include "dmp.h"
#include "pdu.h"
#include "uuid.h"
#include "rlp.h"
#include "rlp-tcp.h"
#include "rlp-udp.h"
#include "sdt.h"
#include "sdt-udp.h"
/**
* @brief @cite ACN 2.3.2 The Root Layer Protocol
*
* The lowest ACN layer above the underlying transport (e.g. the contents of a UDP packet) is
* handled by the ACN Root Layer Protocol. The Root Layer protocol may be tailored to fit higher
* ACN protocols onto each individual transport.
*/
namespace ACN::RLP {
// ANSI E1.17- 2015, Architecture for Control Networks
namespace ACN {
/**
* @brief 2.6.1.2.2. Header Field in Root Layer PDUs
*
* The Header field in Root Layer PDUs shall contain the CID of the component
* that generated the PDU (the Source CID).
*/
struct rlp_header : PDU::pdu_header
{
UUID::uuid cid; //!< Component's UUID
// EPI 16 Protocol Identifier Database
// from https://tsp.esta.org/tsp/working_groups/CP/epi16ids.php
// as of 1/14/21
size_t streamSize() const override;
void iStream(PDU::Stream) override;
void oStream(PDU::Stream) const override;
};
// Session Data Transport Protocol
static const uint32_t PLASA_SDT = 0x00000001;
// Device Management Protocol
static const uint32_t PLASA_DMP = 0x00000002;
// Lightweight streaming protocol for transport of DMX512
static const uint32_t PLASA_E1_31 = 0x00000004;
// RDMnet
static const uint32_t PLASA_E1_33 = 0x00000005;
// Empty data used for health checking connections in E1.33
static const uint32_t PLASA_NULL = 0x00000006;
// Open Lighting Architecture
static const uint32_t OpenLightingProject_OLA = 0x00000007;
// Extended functionality for sACN
static const uint32_t PLASA_E1_31_EXTENDED = 0x00000008;
// E1.59 Object Transform Protocol (OTP)
const static uint32_t ESTA_OTP = 0x00000009;
/**
* @brief The RLP::Pdu class
*/
class Pdu
: public PDU::Pdu
{
public:
Pdu();
void iStream(PDU::Stream) override;
};
/**
* @brief The message_transport struct
*/
struct message_transport
: streamable
{
PDU::Block<RLP::Pdu> root; //!< root PDU block
void iStream(PDU::Stream) override;
void oStream(PDU::Stream) const override;
};
} // ACN::RLP
} // ACN

152
acn/appliance.cpp Normal file
View File

@ -0,0 +1,152 @@
/*
appliance.h
Copyright (c) 2021 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 "appliance.h"
#include "dmp.h"
#include "sdt.h"
#include "rlp.h"
#include "rlp-udp.h"
#include "rlp-tcp.h"
namespace ACN {
Appliance::Appliance(UUID::uuid cid)
: Component(cid)
{
registerRlpVectorHandler(SDT::SDT_PROTOCOL_ID,
std::bind(&Appliance::rootSdtHandler, this,
std::placeholders::_1));
registerRlpVectorHandler(DMP::DMP_PROTOCOL_ID,
std::bind(&Appliance::rootDmpHandler, this,
std::placeholders::_1));
};
/**
EPI 17
*/
void Appliance::UdpStreamHandler(PDU::Stream stream) {
// verify the UDP preamble
RLP::UDP::preamble_t preamble(stream);
// Implementations shall check the ACN Packet Identifier and preamble size.
if (!preamble)
stream->setstate(stream->rdstate() | std::ios_base::failbit);
// implementations shall compute the size and position of the PDU block from
// the preamble size and postamble size provided. ... ignoring any extra
// octets in the preamble or postamble.
for(int i = RLP::UDP::PREAMBLE_MINIMUM_SIZE; i < preamble.length; i++)
stream->read8();
if (!stream->good())
return;
auto block = PDU::readBlock<RLP::Pdu>(stream);
if (stream->fail())
return;
for(auto const &root : *block) {
RlpReceiver(root);
}
}
/**
EPI 33
*/
void Appliance::TcpStreamHandler(PDU::Stream stream) {
// verify the TCP preamble
RLP::TCP::preamble_t preamble(stream);
// implementations shall check the ACN Packet Identifier. If the ACN Packet
// Identifier is not correct the receiver shall close the connection.
if (!preamble)
stream->setstate(stream->rdstate() | std::ios_base::failbit);
if (!stream->good())
return;
auto block = PDU::readBlock<RLP::Pdu>(stream);
if (stream->fail())
return;
for(auto const &root : *block) {
RlpReceiver(root);
}
}
/**
Dispatch a recieved RLP PDU to the appropriate vector handlers.
*/
void Appliance::RlpReceiver(std::shared_ptr<RLP::Pdu> root) {
if (!rlp_vectors_.count(root->vector()))
return;
for(auto const &handler : rlp_vectors_[root->vector()]) {
handler(root);
}
}
/**
Add callback handler for a given RLP vector.
*/
void Appliance::registerRlpVectorHandler(uint32_t vect,
PDU::Handler<RLP::Pdu> handle) {
rlp_vectors_[vect].push_back(handle);
}
/**
Deregister RLP protocol handlers for the given vector.
*/
void Appliance::deregisterRlpVector(uint32_t vect) {
rlp_vectors_.erase(vect);
}
/**
Deregister _ALL_ RLP protocol handlers.
*/
void Appliance::deregisterRlpVector() {
rlp_vectors_.clear();
}
/**
*/
void Appliance::rootSdtHandler(std::shared_ptr<RLP::Pdu> rlp) {
}
/**
*/
void Appliance::rootDmpHandler(std::shared_ptr<RLP::Pdu> rlp) {
}
}; // ACN

79
acn/appliance.h Normal file
View File

@ -0,0 +1,79 @@
/*
appliance.h
Copyright (c) 2021 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.
*/
#pragma once
#include "component.h"
#include "rlp.h"
#include "sdt.h"
#include "pdu.h"
#include "../uuid/uuid.h"
#include <map>
#include <memory>
#include <vector>
// appliance: In DDL an appliance is a piece of equipment described by a root device and all its children and descendents. In DMP systems an appliance corresponds to a component that exposes one or more devices (since the rules require that all devices are descendants of a single root device). See also root device.
// component: The process, program or application corresponding to a single ACN endpoint. All messages in ACN are sent and received by a component. See [Arch] for a more complete definition.
// root device: An instance of a device described in DDL that has no parent device. See also appliance.
namespace ACN {
class Appliance
: public Component
{
public:
Appliance(UUID::uuid = UUID::uuid());
protected:
// EPI 17
virtual void UdpStreamHandler(PDU::Stream);
// EPI 33
virtual void TcpStreamHandler(PDU::Stream);
void RlpReceiver(std::shared_ptr<RLP::Pdu>);
void registerRlpVectorHandler(uint32_t, PDU::Handler<RLP::Pdu>);
void deregisterRlpVector(uint32_t);
void deregisterRlpVector();
// process SDT frames
virtual void rootSdtHandler(std::shared_ptr<RLP::Pdu>);
// SDT 4.4 SDT Base Layer Messages
// Join, Get Sessions, and Sessions are ad-hoc messages, that is, they are
// not sent within channels. These messages identify their intended recipient
// by CID.
virtual void JoinSession() {};
virtual void GetSessions() {};
virtual void Sessions() {};
// process DMP frames
virtual void rootDmpHandler(std::shared_ptr<RLP::Pdu>);
private:
std::map<uint32_t, std::vector<PDU::Handler<RLP::Pdu>>> rlp_vectors_;
std::vector<std::shared_ptr<SDT::Session>> sessions_;
};
}; // ACN

View File

@ -1,5 +1,5 @@
/*
rpt/device.h
component.h
Copyright (c) 2021 Kevin Matz (kevin.matz@gmail.com)
@ -23,18 +23,19 @@
*/
#pragma once
namespace RDMnet::RPT {
#include "../uuid/uuid.h"
/**
* @brief The Device class
*/
class Device
{
namespace ACN {
// 2.1. ACN Components and Component Identifiers (CIDs)
class Component {
public:
explicit Device();
Component(UUID::uuid cid = UUID::uuid()) { cid_ = cid; };
protected:
bool connectedToBroker; ///< True if the device is connected to a broker.
const UUID::uuid cid() const { return cid_; };
private:
UUID::uuid cid_;
};
} // namespace RDMnet::RPT
}; // ACN

119
acn/dmp.cpp Normal file
View File

@ -0,0 +1,119 @@
/*
dmp.cpp
Copyright (c) 2020 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 "dmp.h"
namespace ACN {
namespace DMP {
address_type::address_type(uint8_t val) {
z_reserved = (val >> 7) & 0b1;
relative = (val >> 6) & 0b1;
type = (data_type)((val >> 4) & 0b11);
x_reserved = (val >> 2) & 0b11;
width = (address_length)(val & 0b11);
}
range::range(PDU::Stream stream, data_type t, address_length l) :
incriment(0),
count(0)
{
address = read(stream, l);
if (t == SINGLE) return;
incriment = read(stream, l);
count = read(stream, l);
}
uint32_t range::read(PDU::Stream stream, address_length length) {
switch (length) {
case ONE:
return stream->read8();
case TWO:
return stream->read16();
case FOUR:
return stream->read32();
default:
return 0;
}
}
Pdu::Pdu(PDU::Stream stream)
: PDU::Pdu(stream, 1) // vectors are 1 octet
{
if (stream->fail()) return;
if (!buffer_->good()) return;
if (flags_.hasHeader)
setHeader(new address_type(buffer_->read8()));
if (flags_.hasData) {
switch(vector()) {
case SET_PROPERTY:
readSetData();
break;
default:
break;
}
}
}
void Pdu::readSetData() {
// header may be inherited. check that one exists
if (!header()) {
buffer_->setstate(buffer_->rdstate() | std::ios_base::failbit);
return;
}
auto header = (address_type*)this->header();
auto data = new dmp_set_data();
while(buffer_->good()) {
// Property Address
range pr(buffer_, header->type, header->width);
if (!buffer_->good() || pr.count * pr.incriment > buffer_->available()) {
buffer_->setstate(buffer_->rdstate() | std::ios_base::failbit);
break;
}
// Property Data
std::vector<uint8_t> pd;
pd.resize(pr.address, 0);
for (uint32_t i = 0; i < pr.count * pr.incriment; i ++)
pd.push_back(buffer_->read8());
// Property Fields
set_property fields(pr, pd);
data->properties.push_back(fields);
//set EOF if buffer insufficient for another property range
if (buffer_->available() < header->width * (header->type == SINGLE? 1 : 3))
buffer_->setstate(buffer_->rdstate() | std::ios_base::eofbit);
}
setData(data);
}
} // DMP
} // ACN

121
acn/dmp.h Normal file
View File

@ -0,0 +1,121 @@
/*
dmp.h
Copyright (c) 2020 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.
*/
#pragma once
#include <cstdint>
#include <memory>
#include <vector>
#include "pdu.h"
// Architecture for Control Networks Device Management Protocol
namespace ACN {
namespace DMP {
using std::uint8_t;
using std::uint32_t;
using std::vector;
using std::pair;
using PDU::Block;
// 5.1.4 Address and Data Types
enum data_type {
SINGLE = 0b00, // 0
RANGE = 0b01, // 1
ARRAY = 0b10, // 2
SERIES = 0b11 // 3
};
enum address_length {
ONE = 0b00, // 0
TWO = 0b01, // 1
FOUR = 0b10, // 2
ZERO = 0b11 // 3 (reserved)
};
struct address_type : PDU::pdu_header {
bool z_reserved : 1; // Z
bool relative : 1; // R
data_type type : 2; // D1, D0
uint8_t x_reserved : 2; // X1, X0
address_length width : 2; // A1, A0
address_type(uint8_t);
};
// 5.1.5
struct range {
uint32_t address;
uint32_t incriment;
uint32_t count;
range() {};
range(PDU::Stream, data_type, address_length);
private:
uint32_t read(PDU::Stream, address_length);
};
typedef pair<range, vector<uint8_t>> set_property;
struct dmp_set_data : PDU::pdu_data {
vector<set_property> properties;
};
// 7 Response Messages
enum failure_reason {
NONSPECIFIC = 1, // Non-specific or non-DMP reason.
NOT_PROPERTY = 2, // The address does not correspond to a property.
WRITE_ONLY = 3, // The propertys value may not be read.
NOT_WRITABLE = 4, // The propertys value may not be written.
DATA_ERROR = 5, // The data does not correspond to the property.
SUBSCIRPTION_NOT_SUPPORTED = 10, // Subscriptions on the specified property are not supported by the device.
NO_SUBSCRIPTIONS_SUPPORTED = 11, // Subscriptions not supported on any property.
INSUFFICIENT_RESOURCES = 12, // The component cannot support more subscriptions due to resource limitations
UNAVAILABLE = 13 // The propertys value is not available due to restrictions imposed by device specific functionality (e.g., access permission mechanisms).
};
// 13.1 Protocol Codes
static const uint32_t DMP_PROTOCOL_ID = 2; // PDU protocol value
// 13.2 Message Codes
static const uint8_t GET_PROPERTY = 1;
static const uint8_t SET_PROPERTY = 2;
static const uint8_t GET_PROPERTY_REPLY = 3;
static const uint8_t EVENT = 4;
static const uint8_t SUBSCRIBE = 7;
static const uint8_t UNSUBSCRIBE = 8;
static const uint8_t GET_PROPERTY_FAIL = 9;
static const uint8_t SET_PROPERTY_FAIL = 10;
static const uint8_t SUBSCRIBE_ACCEPT = 12;
static const uint8_t SUBSCRIBE_REJECT = 13;
static const uint8_t SYNC_EVENT = 17;
class Pdu
: public PDU::Pdu
{
public:
Pdu(PDU::Stream);
private:
void readSetData();
};
} // DMP
} // ACN

177
acn/pdu.cpp Normal file
View File

@ -0,0 +1,177 @@
/*
pdu.cpp
Copyright (c) 2020 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 "pdu.h"
#include <cmath>
namespace ACN {
namespace PDU {
Pdu::Pdu(Stream stream, size_t vector_size)
: flags_(stream->peek())
{
header_ = 0;
data_ = 0;
parent_ = 0;
inherit_ = 0; // pointer to previous PDU in block
// read length and vector off of the stream
// abort if the remaining PDU length isn't available
if (stream->available() < (flags_.hasLength ? 3 : 2)) {
stream->setstate(stream->rdstate() | std::ios_base::failbit);
return;
}
readLength(stream);
if (!length_ || length_ > std::pow(2, (8 * (flags_.hasLength ? 3 : 2)) - 4)) {
stream->setstate(stream->rdstate() | std::ios_base::failbit);
return;
}
// length includes the flags, length, and vector
// calculate the remaining length of the PDU
int len = length_ - (flags_.hasLength ? 3 : 2);
// abort if the remaining PDU length isn't available
if (!stream->good() || stream->available() < len) {
stream->setstate(stream->rdstate() | std::ios_base::failbit);
return;
}
// create a stream buffer for the header and data
buffer_ = Stream(new pdu_stream(stream->data(), len));
if (buffer_->available() != len) {
stream->setstate(stream->rdstate() | std::ios_base::failbit);
return;
}
// fast-forward the input stream
for (int i = 0; i < len; i++)
stream->get();
if (!stream->available())
stream->setstate(stream->rdstate() | std::ios_base::eofbit);
if (flags_.hasVector)
readVector(vector_size);
}
Pdu::~Pdu() {
if (header_) delete header_;
if (data_) delete data_;
}
const uint32_t Pdu::vector() {
if (flags_.hasVector)
return vector_;
if (inherit_ != nullptr)
return inherit_->vector();
return 0;
}
pdu_header * Pdu::header() {
if (flags_.hasHeader)
return header_;
if (inherit_ != nullptr)
return inherit_->header();
return 0;
}
pdu_data * Pdu::data() {
if (flags_.hasData)
return data_;
if (inherit_ != nullptr)
return inherit_->data();
return 0;
}
void Pdu::readLength(Stream stream) {
length_ = stream->read16() & 0x0fff; // high 4 bytes are flags
if (flags_.hasLength)
length_ = (length_ << 8 ) | stream->read8();
}
void Pdu::readVector(uint8_t vector_size) {
vector_ = 0;
for (int o = vector_size - 1; o >= 0; o--)
vector_ |= (buffer_->read8() << (8 * o));
}
pdu_flags::pdu_flags(uint8_t val) {
hasLength = (val >> 7) & 0b1;
hasVector = (val >> 6) & 0b1;
hasHeader = (val >> 5) & 0b1;
hasData = (val >> 4) & 0b1;
};
uint8_t pdu_stream::read8() {
uint8_t ret = 0;
if (available() < (int)sizeof(ret)) {
setstate(rdstate() | std::ios_base::failbit);
return 0;
}
ret = get();
if (!available())
setstate(rdstate() | std::ios_base::eofbit);
return ret;
}
uint16_t pdu_stream::read16() {
uint16_t ret = 0;
if (available() < (int)sizeof(ret)) {
setstate(rdstate() | std::ios_base::failbit);
return 0;
}
ret |= get() << 8;
ret |= get();
if (!available())
setstate(rdstate() | std::ios_base::eofbit);
return ret;
}
uint32_t pdu_stream::read32() {
uint32_t ret = 0;
if (available() < (int)sizeof(ret)) {
setstate(rdstate() | std::ios_base::failbit);
return 0;
}
ret |= get() << 24;
ret |= get() << 16;
ret |= get() << 6;
ret |= get();
if (!available())
setstate(rdstate() | std::ios_base::eofbit);
return ret;
}
} // PDU
} // ACN

171
acn/pdu.h Normal file
View File

@ -0,0 +1,171 @@
/*
pdu.h
Copyright (c) 2020 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.
*/
#pragma once
#include <cstdint>
#include <functional>
#include <istream>
#include <memory>
#include <vector>
namespace ACN {
namespace PDU {
using std::uint8_t;
using std::uint16_t;
using std::uint32_t;
using std::vector;
using std::shared_ptr;
// 2.4.1. Flags
// Flags is a 4-bit field containing flags L, V, H and D which declare how the PDU is packed.
struct pdu_flags {
bool hasLength : 1;
bool hasVector : 1;
bool hasHeader : 1;
bool hasData : 1;
pdu_flags(uint8_t);
};
// MAYBE: remove virtuals?
// Arduino doen't enable RTTI for run-time polymorphism.
struct pdu_header { virtual ~pdu_header() {} };
struct pdu_data { virtual ~pdu_data() {} };
/**
Memory buffer of uint8_t data.
*/
class pdu_buffer
: public std::basic_streambuf<uint8_t>
{
public:
pdu_buffer(uint8_t * p, size_t l) { setg(p, p, p + l); };
uint8_t * cur_ptr() { return gptr(); };
};
/**
Input stream of nested PDU
*/
class pdu_stream
: public std::basic_istream<uint8_t>
{
public:
pdu_stream(uint8_t * p, size_t l)
: std::basic_istream<uint8_t>(&_buffer)
, _buffer(p, l) { rdbuf(&_buffer); }
uint32_t available() { return _buffer.in_avail(); }
uint8_t * data() { return _buffer.cur_ptr(); };
uint8_t read8 ();
uint16_t read16();
uint32_t read32();
private:
pdu_buffer _buffer;
};
using Stream = shared_ptr<pdu_stream>;
/**
Base class PDU
All PDU share common structure of:
flags, length, vector,
and protocol specific header/data.
Flag values indicate if lenght, vector, header or data
are present in the PDU, or if they should be inherited from the
preceding PDU.
*/
class Pdu {
public:
Pdu(Stream, size_t vector_size);
~Pdu();
// getters
const pdu_flags flags() {return flags_;}
const uint32_t length() {return length_;}
const uint32_t vector(); // may inherit
pdu_header * header(); // may inherit
pdu_data * data(); // may inherit
shared_ptr<Pdu> parent() {return parent_;}
Stream buffer() {return buffer_;}
// setters
void setParent (shared_ptr<Pdu> pdu) {parent_ = pdu;}
void setInherit(shared_ptr<Pdu> pdu) {inherit_ = pdu;}
protected:
pdu_flags flags_;
uint32_t length_;
uint32_t vector_;
shared_ptr<Pdu> parent_;
shared_ptr<Pdu> inherit_;
Stream buffer_;
// private setters
void setHeader (pdu_header * h) {header_ = h;}
void setData (pdu_data * d) {data_ = d;}
private:
pdu_header * header_;
pdu_data * data_;
void readLength(Stream);
void readVector(uint8_t);
};
template <typename T>
using Handler = std::function<void(std::shared_ptr<T>)>;
template <typename T>
using Block = std::shared_ptr<std::vector<std::shared_ptr<T>>>;
/**
Template creator of a PDU Block.
@param std::shared_ptr<PDU::pdu_stream> The stream to read from.
@return std::shared_ptr<std::vector<std::shared_ptr<T>>> A block of PDU
*/
template<typename T>
Block<T> readBlock(Stream buffer, shared_ptr<PDU::Pdu> parent = 0) {
auto block = Block<T>(new vector<shared_ptr<T>>);
while(buffer->good()) {
shared_ptr<T> pdu(new T(buffer));
if (buffer->fail()) // stream failed during pdu constructor
break;
if (pdu->buffer()->fail()) // pdu buffer errors
continue;
if (parent != 0) // set parent
pdu->setParent(parent);
if (!block->empty()) // set inheritee
pdu->setInherit(block->back());
block->push_back(pdu); // add to block
}
return block;
}
} // PDU
} // ACN

View File

@ -1,7 +1,7 @@
/*
test_uuid.cpp
rlp-tcp.cpp
Copyright (c) 2021 Kevin Matz (kevin.matz@gmail.com)
Copyright (c) 2020 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
@ -22,29 +22,35 @@
SOFTWARE.
*/
#include <gtest/gtest.h>
#include <string>
#include "uuid.h"
#include "rlp-tcp.h"
#include <cstring>
TEST(UUID, ver4fromString)
{
// from online generator
auto str = std::string("c2ffda84-0743-4d3f-8299-e2ba3a09e16c");
auto uuid = UUID::uuid(str);
namespace ACN {
namespace RLP {
namespace TCP {
ASSERT_EQ(uuid.type(), UUID::RFC4122);
ASSERT_EQ(uuid.version(), 4);
ASSERT_EQ(uuid.string(), str);
preamble_t::preamble_t(PDU::Stream stream) {
stream->read(acn_id, sizeof(acn_id));
if (stream->gcount() != 12)
stream->setstate(stream->rdstate() | std::ios_base::failbit);
length = stream->read32();
}
TEST(UUID, createV4)
{
auto uuid = UUID::uuid();
uuid.uuid4();
/*
Validate compliance with ACN EPI 33. ACN Root Layer Protocol Operation on TCP
4.2. Reception
*/
preamble_t::operator bool () {
// 2. Preamble Format: The ACN Packet Identifier shall be the text string
// “ASC-E1.17\0\0\0” encoded in [ASCII].
if (memcmp(acn_id, ACN_PACKET_IDENTIFIER, 12))
return false;
ASSERT_EQ(uuid.type(), UUID::RFC4122);
ASSERT_EQ(uuid.version(), 4);
ASSERT_EQ(uuid, UUID::uuid(uuid.string()));
return true;
}
} // TCP
} // RLP
} // ACN

View File

@ -1,5 +1,5 @@
/*
controller.h
rlp-tcp.h
Copyright (c) 2021 Kevin Matz (kevin.matz@gmail.com)
@ -24,32 +24,32 @@
#pragma once
#include <cstdint>
#include "message.h"
#include "uid.h"
#include "rlp.h"
#include "pdu.h"
namespace RDM {
// EPI 33. ACN Root Layer Protocol Operation on TCP
/**
* @brief The Controller class
*/
class Controller
{
public:
explicit Controller();
virtual ~Controller();
namespace ACN {
namespace RLP {
namespace TCP {
bool findDevices(uint64_t lowerBound = 0,
uint64_t upperBound = 0xFFFFFFFFFFFE) const;
protected:
bool quick_find(const UID &uid, uint64_t lowerBound, uint64_t upperBound) const;
virtual void rxDiscoveryResponse(const MsgPtr message);
virtual void rxGetResponse(const MsgPtr message);
virtual void rxSetResponse(const MsgPtr message);
private:
uint8_t next_transaction_;
// 3. Frame Preamble Format
struct preamble_t {
uint8_t acn_id[12]; // 3.1 Packet Identifier
uint32_t length; // 3.2 PDU Block Size
preamble_t(PDU::Stream);
operator bool();
};
} // namespace RDM
// 3.1 Packet Identifier
// The ACN Packet Identifier shall be the text string
// “ASC-E1.17\0\0\0” encoded in [ASCII].
static constexpr uint8_t ACN_PACKET_IDENTIFIER[] = { 0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00 };
// 3.2 PDU Block Size
static const uint32_t INDEFINITE = 0xffffffff;
} // TCP
} // RLP
} // ACN

View File

@ -1,7 +1,7 @@
/*
personality.cpp
rlp-udp.cpp
Copyright (c) 2021 Kevin Matz (kevin.matz@gmail.com)
Copyright (c) 2020 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
@ -22,57 +22,41 @@
SOFTWARE.
*/
#include "personality.h"
#include "rlp-udp.h"
#include <cstring>
namespace DMX {
namespace ACN {
namespace RLP {
namespace UDP {
/**
* @brief Personality::Personality
*/
Personality::Personality()
: name_("Default Name")
, description_("Default Description")
, footprint_(0)
{
preamble_t::preamble_t(PDU::Stream stream) {
length = stream->read16();
postamble_size = stream->read16();
stream->read(acn_id, 12);
if (stream->gcount() != 12)
stream->setstate(stream->rdstate() | std::ios_base::failbit);
}
/**
* @brief Personality::~Personality
*/
Personality::~Personality()
{
/*
Validate compliance with ACN EPI 17: ACN Root Layer Protocol on UDP
4.2. Reception
*/
preamble_t::operator bool () {
// 2. Preamble Format: The preamble size includes both size fields so the
// minimum value for preamble size is 16 (octets).
if (length < PREAMBLE_MINIMUM_SIZE)
return false;
// 2. Preamble Format: The ACN Packet Identifier shall be the text string
// “ASC-E1.17\0\0\0” encoded in [ASCII].
if (memcmp(acn_id, ACN_PACKET_IDENTIFIER, 12))
return false;
return true;
}
/**
* @brief Personality::name
* @return
*/
const std::string Personality::name() const
{
return name_;
}
/**
* @brief Personality::description
* @return
*/
const std::string Personality::description() const
{
return description_;
}
/**
* @brief Personality::footprint
* @return
*/
uint16_t Personality::footprint() const
{
return footprint_;
}
} // namespace DMX
} // UDP
} // RLP
} // ACN

View File

@ -1,7 +1,7 @@
/*
dmx/patch.h
rlp-udp.h
Copyright (c) 2021 Kevin Matz (kevin.matz@gmail.com)
Copyright (c) 2020 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
@ -23,39 +23,37 @@
*/
#pragma once
#include "personality.h"
#include "universe.h"
#include <cstdint>
#include <memory>
#include <vector>
#include "rlp.h"
#include "pdu.h"
namespace DMX {
// ACN EPI 17. ACN Root Layer Protocol Operation on UDP
/**
* @brief The DMX::Patch class
*/
class Patch
{
public:
explicit Patch();
namespace ACN {
namespace RLP {
namespace UDP {
void setUniverse(std::shared_ptr<Universe>);
using std::uint8_t;
using std::uint16_t;
bool patch(std::shared_ptr<Personality> device, uint16_t address);
void unpatch(std::shared_ptr<Personality> device);
void unpatch(size_t index);
// The ACN Packet Identifier shall be the text string
// “ASC-E1.17\0\0\0” encoded in [ASCII].
static constexpr uint8_t ACN_PACKET_IDENTIFIER[] = { 0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00 };
const std::vector<std::pair<std::weak_ptr<Personality>,uint16_t>> * listing();
// 2. Preamble Format: The preamble size includes both size fields so the
// minimum value for preamble size is 16 (octets).
static const uint8_t PREAMBLE_MINIMUM_SIZE = 16;
protected:
void universeDataChanged();
private:
std::vector<std::pair<std::weak_ptr<Personality>,uint16_t>> members_;
std::weak_ptr<Universe> universe_;
std::shared_ptr<void> data_change_token;
// 2. Preamble Format
struct preamble_t {
uint16_t length;
uint16_t postamble_size;
uint8_t acn_id[12];
preamble_t(PDU::Stream);
operator bool();
};
} // namespace DMX
} // UDP
} // RLP
} // ACN

View File

@ -1,7 +1,7 @@
/*
rdm/subdevice.cpp
rlp.cpp
Copyright (c) 2023 Kevin Matz (kevin.matz@gmail.com)
Copyright (c) 2020 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
@ -22,18 +22,31 @@
SOFTWARE.
*/
#include "subdevice.h"
#include "rlp.h"
namespace RDM {
namespace ACN {
namespace RLP {
SubDevice::SubDevice()
: BasicDevice()
rlp_header::rlp_header(PDU::Stream stream)
: PDU::pdu_header()
, cid()
{
uint8_t buf[16];
stream->read(buf, 16);
if (stream->gcount() != 16)
stream->setstate(stream->rdstate() | std::ios_base::failbit);
cid = UUID::uuid(buf);
}
SubDevice::~SubDevice()
Pdu::Pdu(PDU::Stream stream)
: ACN::PDU::Pdu(stream, 4)
{
if (stream->fail()) return;
if (!buffer_->good()) return;
if (flags_.hasHeader)
setHeader(new rlp_header(buffer_));
}
} // namespace RDM
} // RLP
} // ACN

View File

@ -1,7 +1,7 @@
/*
fptc.h
rlp.h
Copyright (c) 2023 Kevin Matz (kevin.matz@gmail.com)
Copyright (c) 2020 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
@ -24,15 +24,27 @@
#pragma once
#include <cstdint>
#include <memory>
#include "pdu.h"
#include "../uuid/uuid.h"
/**
* \cite CITP 6 CITP/FPTC, Fixture patch layer
*
* The Fixture Patch layer is used to communicate fixture existence and patch information.
*/
namespace CITP::FPTC {
} // namespace FPTC
namespace ACN {
namespace RLP {
// 2.6.1.2.2. Header Field in Root Layer PDUs
// The Header field in Root Layer PDUs shall contain the CID of the component
// that generated the PDU (the Source CID).
struct rlp_header : PDU::pdu_header {
UUID::uuid cid;
rlp_header(PDU::Stream);
};
class Pdu
: public PDU::Pdu
{
public:
Pdu(PDU::Stream);
};
} // RLP
} // ACN

View File

@ -1,5 +1,5 @@
/*
pdu-stream.cpp
sdt-udp.cpp
Copyright (c) 2020 Kevin Matz (kevin.matz@gmail.com)
@ -22,35 +22,33 @@
SOFTWARE.
*/
#include "pdu-stream.h"
#include "sdt-udp.h"
namespace ACN {
namespace SDT {
namespace UDP {
/**
* @brief operator >>
* @param buffer
* @param uuid
* @return
*/
bufferstream &operator>> (bufferstream &buffer, UUID::uuid &uuid)
{
uint8_t data[UUID_LENGTH];
buffer.read(data, UUID_LENGTH);
if (buffer.gcount() == UUID_LENGTH)
uuid.setBytes(data);
else
buffer.setstate(std::ios_base::badbit);
return buffer;
};
address_t::address_t(PDU::Stream stream) {
type = stream->read8();
if (type != SDT_ADDR_NULL)
port = stream->read16();
/**
* @brief operator <<
* @param buffer
* @param uuid
* @return
*/
bufferstream &operator<< (bufferstream &buffer, const UUID::uuid &uuid)
{
buffer.write(uuid.bytes(), UUID_LENGTH);
return buffer;
switch (type) {
case SDT_ADDR_IPV4:
for (unsigned int i = 0; i < sizeof(ipv4); i++)
ipv4[i] = stream->read8();
break;
case SDT_ADDR_IPV6:
for (unsigned int i = 0; i < sizeof(ipv6); i++)
ipv6[i] = stream->read8();
break;
case SDT_ADDR_NULL:
default:
break;
}
}
}; // UDP
}; // SDT
}; // ACN

69
acn/sdt-udp.h Normal file
View File

@ -0,0 +1,69 @@
/*
sdt-udp.h
Copyright (c) 2020 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.
*/
#pragma once
#include <cstdint>
#include "pdu.h"
// ACN EPI 18 - Operation of SDT on UDP Networks
namespace ACN {
namespace SDT {
namespace UDP {
// Table 3. SDT symbolic parameters
static const float MAK_TIMEOUT_FACTOR = 0.1;
static const uint8_t MAK_MAX_RETRIES = 2; // (3 tries total)
static const uint8_t AD_HOC_TIMEOUT = 200; // ms
static const uint8_t AD_HOC_RETRIES = 2; // (3 tries total)
static const float RECIPROCAL_TIMEOUT_FACTOR = 0.2;
static const uint8_t MIN_EXPIRY_TIME = 2; // s
static const float NAK_TIMEOUT_FACTOR = 0.1;
static const uint8_t NAK_MAX_RETRIES = 2; // (3 tries total)
static const uint8_t NAK_HOLDOFF_INTERVAL = 2; // ms
static const uint8_t NAK_MAX_TIME = 10 * NAK_HOLDOFF_INTERVAL;
static const uint8_t NAK_BLANKTIME = 3 * NAK_HOLDOFF_INTERVAL;
static const uint16_t SDT_MULTICAST_PORT = 5568; // IANA registered “sdt”
// defined in SDT, but needed here without including that header
enum ip_addr_spec_t {
SDT_ADDR_NULL = 0, // Address is not present (0 octets).
SDT_ADDR_IPV4 = 1, // Address specified is in IP v4 format
SDT_ADDR_IPV6 = 2 // Address specified is in IP v6 format
};
// 3 Address Specification
struct address_t {
uint8_t type = SDT_ADDR_NULL;
uint16_t port = SDT_MULTICAST_PORT;
union {
uint8_t ipv4[4];
uint8_t ipv6[16] = {0};
};
address_t() {};
address_t(PDU::Stream);
};
} // UDP
} // SDT
} // ACN

169
acn/sdt.cpp Normal file
View File

@ -0,0 +1,169 @@
/*
sdt.cpp
Copyright (c) 2021 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 "sdt.h"
namespace ACN {
namespace SDT {
params_t::params_t(PDU::Stream stream) {
Expiry = stream->read8();
reserved = stream->peek() & 0xef;
NAK_Outbound = stream->read8() >> 7;
reserved = stream->read8() & 0xef;
NAKholdoff = stream->read16();
NAKmodulus = stream->read16();
NAKmaxwait = stream->read16();
}
join_data_t::join_data_t(PDU::Stream stream) {
mid = stream->read16();
number = stream->read16();
reciprocal = stream->read16();
sequence = stream->read32();
reliable = stream->read32();
destination = UDP::address_t(stream);
parameters = params_t(stream);
expiry = stream->read8();
}
join_accept_data_t::join_accept_data_t(PDU::Stream stream) {
uint8_t buf[16];
stream->read(buf, sizeof(buf));
leader = UUID::uuid(buf);
number = stream->read16();
mid = stream->read16();
reliable = stream->read32();
reciprocal = stream->read16();
}
join_refuse_data_t::join_refuse_data_t(PDU::Stream stream) {
uint8_t buf[16];
stream->read(buf, sizeof(buf));
leader = UUID::uuid(buf);
number = stream->read16();
mid = stream->read16();
reliable = stream->read32();
code = stream->read8();
}
nak_data_t::nak_data_t(PDU::Stream stream) {
uint8_t buf[16];
stream->read(buf, sizeof(buf));
leader = UUID::uuid(buf);
number = stream->read16();
mid = stream->read16();
reliable = stream->read32();
missed_first = stream->read32();
missed_last = stream->read32();
}
wrapper_data_t::wrapper_data_t(PDU::Stream stream) {
number = stream->read16();
}
/**
compare to another sessionID
*/
bool SessionId::operator== (const SessionId & other) const {
return ((other.cid == cid) &
(other.number == number) &
(other.protocol == protocol));
}
/**
Constuct a sequenced channel with an owner and a direction.
@param owner the owner of the channel
@param direction the direction of channel communication
*/
Channel::Channel(std::shared_ptr<Component> owner, Direction direction) {
owner_ = owner;
direction_ = direction;
}
/**
deconstructor that closes the channel cleanly.
*/
Channel::~Channel() {
// TODO: close the channel.
}
/**
Construct a new session.
*/
Session::Session() {
}
/**
deconstructor that leaves the session gracefully.
*/
Session::~Session() {
// TODO: Disconnect the session.
}
/**
get the ID of the session
*/
SessionId Session::id() {
SessionId ret;
ret.cid = leader->cid();
ret.number = number_;
ret.protocol = protocol_;
return ret;
}
Pdu::Pdu(PDU::Stream stream)
: PDU::Pdu(stream, 1) // vectors are 1 octet
{
// if (stream->fail()) return;
// if (!buffer_->good()) return;
}
client_pdu_header_t::client_pdu_header_t(PDU::Stream stream) {
protocol = stream->read32();
association = stream->read16();
}
ClientPdu::ClientPdu(PDU::Stream stream)
: PDU::Pdu(stream, 2) // vectors are 2 octets (MID)
{
if (stream->fail()) return;
if (!buffer_->good()) return;
if (flags_.hasHeader)
setHeader(new client_pdu_header_t(buffer_));
}
}; // SDT
}; // ACN

313
acn/sdt.h Normal file
View File

@ -0,0 +1,313 @@
/*
sdt.h
Copyright (c) 2020 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.
*/
#pragma once
#include <cstdint>
#include <list>
#include <memory>
#include <unordered_map>
#include "component.h"
#include "pdu.h"
#include "sdt-udp.h" // EPI 18
// ANSI E1.17- 2015, Architecture for Control Networks
// Session Data Transport Protocol
namespace ACN {
namespace SDT {
// 3.1 Session Identity
struct SessionId {
UUID::uuid cid; // the component ID (CID) of the session leader
uint16_t number; // the session number the leader has assigned
uint32_t protocol; // the ID of the protocol carried by the session
bool operator== (const SessionId &) const;
};
// 3.3 Sequenced Channels
// Sequenced channels transport three categories of traffic:
enum Direction {
internal, // SDT internal traffic
downstream, // Session downstream traffic
upstream // Session upstream traffic
};
// 3.5.1.1 Member Identifiers
using MID = uint16_t;
// 4.4.1.2 Channel Parameter Block
struct params_t {
uint8_t Expiry; // number of seconds without traffic before leaving
struct {
uint8_t NAK_Outbound : 1; // NAK to channel (1) or destination address (0)
uint8_t reserved : 7;
};
uint16_t NAKholdoff; // calculation of a standoff time for sending NAKs
uint16_t NAKmodulus; // calculation of a standoff time for sending NAKs
uint16_t NAKmaxwait; // maximum milliseconds to wait before sending a NAK.
params_t() {};
params_t(PDU::Stream);
};
// 4.4.1 Join
struct join_data_t : PDU::pdu_data {
MID mid;
uint16_t number;
uint16_t reciprocal;
uint32_t sequence;
uint32_t reliable;
UDP::address_t destination;
params_t parameters;
uint8_t expiry;
join_data_t() {};
join_data_t(PDU::Stream);
};
// 4.4.2 Join Accept
struct join_accept_data_t : PDU::pdu_data {
UUID::uuid leader;
uint16_t number;
MID mid;
uint32_t reliable;
uint16_t reciprocal;
join_accept_data_t() {};
join_accept_data_t(PDU::Stream);
};
// 4.4.3 Join Refuse
// 4.4.4 Leaving
struct join_refuse_data_t : PDU::pdu_data {
UUID::uuid leader;
uint16_t number;
MID mid;
uint32_t reliable;
uint8_t code;
join_refuse_data_t() {};
join_refuse_data_t(PDU::Stream);
};
// 4.4.5 NAK
struct nak_data_t : PDU::pdu_data {
UUID::uuid leader;
uint16_t number;
MID mid;
uint32_t reliable;
uint32_t missed_first;
uint32_t missed_last;
nak_data_t() {};
nak_data_t(PDU::Stream);
};
// 4.4.6 Reliable Wrapper and Unreliable Wrapper
struct wrapper_data_t : PDU::pdu_data {
uint16_t number;
uint32_t sequence;
uint32_t reliable;
uint32_t oldest;
MID ack_range_begin;
MID ack_range_end;
uint16_t MAK_threshold;
wrapper_data_t() {};
wrapper_data_t(PDU::Stream);
};
// 4.4.7 SDT Client Block
struct client_pdu_header_t : PDU::pdu_header {
uint32_t protocol;
uint16_t association;
client_pdu_header_t() {};
client_pdu_header_t(PDU::Stream);
};
// Client Block PDU
class ClientPdu
: public PDU::Pdu
{
public:
ClientPdu(PDU::Stream);
};
// 4.4.8 Get Sessions
struct get_sessions_data_t : PDU::pdu_data {
UUID::uuid cid;
};
// 4.4.9.1 Channel Owner Info Block
struct channel_info_block_t {
MID mid;
UUID::uuid owner;
uint16_t number;
UDP::address_t destination;
UDP::address_t source;
uint16_t reciprocal;
uint16_t count;
std::list<uint32_t> protocols;
};
// 4.4.9 Sessions
struct sessions_data_t : PDU::pdu_data {
std::list<channel_info_block_t> list;
};
// 4.5.1 ACK
struct ack_data_t : PDU::pdu_data {
uint32_t reliable;
};
// 4.5.2 Channel Params
struct channel_params_data_t : PDU::pdu_data {
params_t parameters;
UDP::address_t address;
uint8_t expiry;
};
// 4.5.3 Connect
// 4.5.4 Connect Accept
// 4.5.6 Disconnect
struct connect_data_t : PDU::pdu_data {
uint32_t protocol;
};
// 4.5.5 Connect Refuse
// 4.5.7 Disconnecting
struct connect_refuse_data_t : PDU::pdu_data {
uint32_t protocol;
uint8_t code;
};
// 7.1 Protocol Code
static const uint32_t SDT_PROTOCOL_ID = 1; // PDU protocol value
// 7.2 PDU Vector Codes
enum sdt_vector_t {
REL_WRAP = 1,
UNREL_WRAP = 2,
CHANNEL_PARAMS = 3,
JOIN = 4,
JOIN_REFUSE = 5,
JOIN_ACCEPT = 6,
LEAVE = 7,
LEAVING = 8,
CONNECT = 9,
CONNECT_ACCEPT = 10,
CONNECT_REFUSE = 11,
DISCONNECT = 12,
DISCONNECTING = 13,
ACK = 14,
NAK = 15,
GET_SESSIONS = 16,
SESSIONS = 17,
};
// 7.4 Other Symbolic Parameters
// Table 6: Reason Codes
enum reason_code_t {
NONSPECIFIC = 1, // Non-specific, non-SDT reason.
ILLEGAL_PARAMETERS = 2, // Illegal channel parameters.
LOW_RESOURCES = 3, // Insufficient resources.
ALREADY_MEMBER = 4, // Multiple MIDs for single component.
BAD_ADDRESS_TYPE = 5, // Unrecognized transport address type.
NO_RECIPROCAL_CHANNEL = 6, // No upstream channel and cant create one
CHANNEL_EXPIRED = 7, // Channel has expired.
LOST_SEQUENCE = 8, // Unrecoverable packets missed.
SATURATED = 9, // Cant keep up, processor saturation.
TRANSPORT_ADDRESS_CHANGING = 10, // (e.g., IP number lease expired).
ASKED_TO_LEAVE = 11, // Asked to leave the channel.
NO_RECIPIENT = 12, // Component does not support protocol ID.
ONLY_UNICAST_SUPPORTED = 13 // Only unicast channels are supported
};
// Table 7: Address Specification Types
enum ip_addr_spec_t {
SDT_ADDR_NULL = 0, // Address is not present (0 octets).
SDT_ADDR_IPV4 = 1, // Address specified is in IP v4 format
SDT_ADDR_IPV6 = 2 // Address specified is in IP v6 format
};
// PDU type for this protocol
class Pdu
: public PDU::Pdu
{
public:
Pdu(PDU::Stream);
};
// Sequenced channels are unidirectional communication channels (unicast or
// multicast) from an owner component to one or more member components.
class Channel {
public:
Channel(std::shared_ptr<Component>, Direction);
~Channel();
private:
std::shared_ptr<Component> owner_;
Direction direction_ = internal;
};
// A session has a single leader and zero or more session members. The leader
// communicates to members using the downstream address. Members respond to the
// leader on the upstream address. A unique session identifier identifies a
// session.
class Session {
public:
Session();
~Session();
SessionId id();
// 4.4 SDT Base Layer Messages (non ad-hoc)
void join_accept() {};
void join_refuse() {};
void leaving() {};
void nak() {};
void reliable_wrapper() {};
void unreliable_wrapper() {};
// 4.5 SDT Wrapped Messages
void ack();
void channel_params();
void leave();
void connect();
void connect_accept();
void connect_refuse();
void disconnect();
void disconnecting();
protected:
std::shared_ptr<ACN::Component> leader;
std::shared_ptr<Channel> downstream;
std::unordered_map<MID, std::shared_ptr<Channel>> upstream;
private:
uint16_t number_;
uint32_t protocol_;
MID next_id_ = 1;
};
} // SDT
} // ACN

View File

@ -1,7 +1,7 @@
/*
universe.cpp
Copyright (c) 2022 Kevin Matz (kevin.matz@gmail.com)
Copyright (c) 2020 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
@ -24,11 +24,41 @@
#include "universe.h"
namespace ARTNET {
universe::universe()
{
namespace DMX {
using std::copy;
/**
*/
Universe::Universe() {
null_start_data_.fill(0);
}
} // namespace ARTNET
/**
accept new data from receiver
@param vector<uint8_t> varaibly sized data, beginning with a start code.
*/
void Universe::set(vector<uint8_t> vect) {
switch (vect.front()) { // start code
case E111_NULL_START:
vect.resize(null_start_data_.size(), 0); // pad shorter, truncate larger
copy(vect.begin(), vect.end(), null_start_data_.begin());
for (const auto &cb : callbacks_)
cb(this);
break;
default:
break;
}
}
/**
register a data consumer callback function
@param const DataHandlerFunction something that knows what to do with DMX
*/
void Universe::onData(const DataHandler callback)
{
callbacks_.push_back(callback);
}
} // DMX

72
dmx/universe.h Normal file
View File

@ -0,0 +1,72 @@
/*
universe.h
Copyright (c) 2020 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.
*/
#pragma once
#include <cstdint>
#include <functional>
#include <vector>
namespace DMX {
using std::uint8_t;
using std::uint16_t;
using std::array;
using std::function;
using std::vector;
// Table D1 - Reserved START Codes
static const uint8_t E111_NULL_START = 0;
static const uint8_t E111_ASC_TEXT_ASCII = 23;
static const uint8_t E111_ASC_TEST = 85;
static const uint8_t E111_ASC_TEXT_UTF8 = 144;
static const uint8_t E111_ASC_MANUFACTURER = 145;
static const uint8_t E111_ASC_SIP = 207;
class Universe; // forward declare the Univserse class
using DataHandler = function<void(Universe *)>;
using DimmerData = array<uint8_t, 513>;
/*
The Universe class
ANSI E1.11 describes may things about moving data over
serial EIA-485-A links. This class encapselates that data,
regardless of how it was transmitted.
*/
class Universe {
public:
Universe ();
DimmerData * data() { return &null_start_data_; }
void onData (const DataHandler callback);
void set (vector<uint8_t>);
uint8_t slot (uint16_t address) { return null_start_data_[address]; }
private:
DimmerData null_start_data_;
vector<DataHandler> callbacks_;
};
} // DMX

View File

@ -1,304 +0,0 @@
# Doxyfile 1.9.1
#---------------------------------------------------------------------------
# Project related configuration options
#---------------------------------------------------------------------------
DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = OpenLCP
PROJECT_NUMBER = @PROJECT_VERSION@
#PROJECT_BRIEF = "A collection of C++ libraries for Lighting Control Protocols, focusing on ESTA sponsored ANSI standards."
PROJECT_LOGO =
OUTPUT_DIRECTORY = @CMAKE_CURRENT_BINARY_DIR@/docs
CREATE_SUBDIRS = YES
ALLOW_UNICODE_NAMES = YES
OUTPUT_LANGUAGE = English
BRIEF_MEMBER_DESC = YES
REPEAT_BRIEF = YES
ABBREVIATE_BRIEF = "The $name class" \
"The $name widget" \
"The $name file" \
is \
provides \
specifies \
contains \
represents \
a \
an \
the
ALWAYS_DETAILED_SEC = NO
INLINE_INHERITED_MEMB = NO
FULL_PATH_NAMES = YES
STRIP_FROM_PATH =
STRIP_FROM_INC_PATH =
SHORT_NAMES = NO
JAVADOC_AUTOBRIEF = YES
JAVADOC_BANNER = NO
QT_AUTOBRIEF = YES
MULTILINE_CPP_IS_BRIEF = NO
PYTHON_DOCSTRING = YES
INHERIT_DOCS = YES
SEPARATE_MEMBER_PAGES = NO
TAB_SIZE = 2
ALIASES =
OPTIMIZE_OUTPUT_FOR_C = NO
OPTIMIZE_OUTPUT_JAVA = NO
OPTIMIZE_FOR_FORTRAN = NO
OPTIMIZE_OUTPUT_VHDL = NO
OPTIMIZE_OUTPUT_SLICE = NO
EXTENSION_MAPPING =
MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 5
AUTOLINK_SUPPORT = YES
BUILTIN_STL_SUPPORT = YES
CPP_CLI_SUPPORT = NO
SIP_SUPPORT = NO
IDL_PROPERTY_SUPPORT = YES
DISTRIBUTE_GROUP_DOC = NO
GROUP_NESTED_COMPOUNDS = NO
SUBGROUPING = YES
INLINE_GROUPED_CLASSES = NO
INLINE_SIMPLE_STRUCTS = NO
TYPEDEF_HIDES_STRUCT = NO
LOOKUP_CACHE_SIZE = 0
NUM_PROC_THREADS = 4
#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------
EXTRACT_ALL = NO
EXTRACT_PRIVATE = NO
EXTRACT_PRIV_VIRTUAL = NO
EXTRACT_PACKAGE = NO
EXTRACT_STATIC = NO
EXTRACT_LOCAL_CLASSES = YES
EXTRACT_LOCAL_METHODS = NO
EXTRACT_ANON_NSPACES = NO
RESOLVE_UNNAMED_PARAMS = YES
HIDE_UNDOC_MEMBERS = NO
HIDE_UNDOC_CLASSES = NO
HIDE_FRIEND_COMPOUNDS = NO
HIDE_IN_BODY_DOCS = NO
INTERNAL_DOCS = NO
CASE_SENSE_NAMES = NO
HIDE_SCOPE_NAMES = NO
HIDE_COMPOUND_REFERENCE= NO
SHOW_INCLUDE_FILES = YES
SHOW_GROUPED_MEMB_INC = NO
FORCE_LOCAL_INCLUDES = NO
INLINE_INFO = YES
SORT_MEMBER_DOCS = YES
SORT_BRIEF_DOCS = NO
SORT_MEMBERS_CTORS_1ST = NO
SORT_GROUP_NAMES = NO
SORT_BY_SCOPE_NAME = NO
STRICT_PROTO_MATCHING = NO
GENERATE_TODOLIST = YES
GENERATE_TESTLIST = YES
GENERATE_BUGLIST = YES
GENERATE_DEPRECATEDLIST= YES
ENABLED_SECTIONS =
MAX_INITIALIZER_LINES = 30
SHOW_USED_FILES = YES
SHOW_FILES = NO
SHOW_NAMESPACES = YES
FILE_VERSION_FILTER =
LAYOUT_FILE =
CITE_BIB_FILES = @CMAKE_CURRENT_SOURCE_DIR@/protocol/artistic/ARTISTIC.bib \
@CMAKE_CURRENT_SOURCE_DIR@/protocol/citp/CITP.bib \
@CMAKE_CURRENT_SOURCE_DIR@/protocol/enttec/ENTTEC.bib \
@CMAKE_CURRENT_SOURCE_DIR@/protocol/esta/ESTA.bib \
@CMAKE_CURRENT_SOURCE_DIR@/protocol/ietf/IETF.bib \
@CMAKE_CURRENT_SOURCE_DIR@/protocol/osc/OSC.bib
#---------------------------------------------------------------------------
# Configuration options related to warning and progress messages
#---------------------------------------------------------------------------
QUIET = YES
WARNINGS = YES
WARN_IF_UNDOCUMENTED = YES
WARN_IF_DOC_ERROR = YES
WARN_NO_PARAMDOC = YES
WARN_AS_ERROR = NO
WARN_FORMAT = "$file:$line: $text"
WARN_LOGFILE =
#---------------------------------------------------------------------------
# Configuration options related to the input files
#---------------------------------------------------------------------------
INPUT = @doxy_main_page@ \
@CMAKE_CURRENT_SOURCE_DIR@
INPUT_ENCODING = UTF-8
FILE_PATTERNS = *.cpp \
*.h \
*.md \
*.py
RECURSIVE = YES
EXCLUDE = @CMAKE_CURRENT_SOURCE_DIR@/docs \
@CMAKE_CURRENT_SOURCE_DIR@/example \
@CMAKE_CURRENT_SOURCE_DIR@/platform \
@CMAKE_CURRENT_SOURCE_DIR@/test
EXCLUDE_SYMLINKS = NO
EXCLUDE_PATTERNS =
EXCLUDE_SYMBOLS =
EXAMPLE_PATH =
EXAMPLE_PATTERNS = *
EXAMPLE_RECURSIVE = NO
IMAGE_PATH =
INPUT_FILTER =
FILTER_PATTERNS =
FILTER_SOURCE_FILES = NO
FILTER_SOURCE_PATTERNS =
USE_MDFILE_AS_MAINPAGE = @CMAKE_CURRENT_SOURCE_DIR@/README.md
#---------------------------------------------------------------------------
# Configuration options related to source browsing
#---------------------------------------------------------------------------
SOURCE_BROWSER = NO
INLINE_SOURCES = NO
STRIP_CODE_COMMENTS = NO
REFERENCED_BY_RELATION = NO
REFERENCES_RELATION = NO
REFERENCES_LINK_SOURCE = YES
SOURCE_TOOLTIPS = YES
USE_HTAGS = NO
VERBATIM_HEADERS = NO
#---------------------------------------------------------------------------
# Configuration options related to the alphabetical class index
#---------------------------------------------------------------------------
ALPHABETICAL_INDEX = YES
IGNORE_PREFIX =
#---------------------------------------------------------------------------
# Configuration options related to the HTML output
#---------------------------------------------------------------------------
GENERATE_HTML = YES
HTML_OUTPUT = html
HTML_FILE_EXTENSION = .html
HTML_HEADER = @CMAKE_CURRENT_SOURCE_DIR@/docs/doxygen_files/header.html
HTML_FOOTER = @CMAKE_CURRENT_SOURCE_DIR@/docs/doxygen_files/footer.html
HTML_STYLESHEET = @CMAKE_CURRENT_SOURCE_DIR@/docs/doxygen_files/doxygen.css
HTML_EXTRA_STYLESHEET = @CMAKE_CURRENT_SOURCE_DIR@/docs/doxygen_theme_flat_design/src/doxygen-style.css
HTML_EXTRA_FILES =
HTML_COLORSTYLE_HUE = 220
HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80
HTML_TIMESTAMP = NO
HTML_DYNAMIC_MENUS = YES
HTML_DYNAMIC_SECTIONS = YES
HTML_INDEX_NUM_ENTRIES = 100
GENERATE_DOCSET = NO
GENERATE_HTMLHELP = NO
CHM_FILE =
HHC_LOCATION =
GENERATE_CHI = NO
CHM_INDEX_ENCODING =
BINARY_TOC = NO
TOC_EXPAND = NO
GENERATE_QHP = NO
QCH_FILE =
QHP_NAMESPACE = org.doxygen.Project
QHP_VIRTUAL_FOLDER = doc
QHP_CUST_FILTER_NAME =
QHP_CUST_FILTER_ATTRS =
QHP_SECT_FILTER_ATTRS =
QHG_LOCATION =
GENERATE_ECLIPSEHELP = NO
DISABLE_INDEX = NO
GENERATE_TREEVIEW = NO
ENUM_VALUES_PER_LINE = 4
TREEVIEW_WIDTH = 250
EXT_LINKS_IN_WINDOW = NO
HTML_FORMULA_FORMAT = png
FORMULA_FONTSIZE = 10
FORMULA_MACROFILE =
USE_MATHJAX = NO
MATHJAX_FORMAT = HTML-CSS
MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2
MATHJAX_EXTENSIONS =
MATHJAX_CODEFILE =
SEARCHENGINE = YES
SERVER_BASED_SEARCH = NO
EXTERNAL_SEARCH = NO
SEARCHENGINE_URL =
SEARCHDATA_FILE = searchdata.xml
EXTERNAL_SEARCH_ID =
EXTRA_SEARCH_MAPPINGS =
#---------------------------------------------------------------------------
# Configuration options related to the LaTeX output
#---------------------------------------------------------------------------
GENERATE_LATEX = NO
#---------------------------------------------------------------------------
# Configuration options related to the RTF output
#---------------------------------------------------------------------------
GENERATE_RTF = NO
#---------------------------------------------------------------------------
# Configuration options related to the man page output
#---------------------------------------------------------------------------
GENERATE_MAN = NO
#---------------------------------------------------------------------------
# Configuration options related to the XML output
#---------------------------------------------------------------------------
GENERATE_XML = NO
#---------------------------------------------------------------------------
# Configuration options related to the DOCBOOK output
#---------------------------------------------------------------------------
GENERATE_DOCBOOK = NO
#---------------------------------------------------------------------------
# Configuration options for the AutoGen Definitions output
#---------------------------------------------------------------------------
GENERATE_AUTOGEN_DEF = NO
#---------------------------------------------------------------------------
# Configuration options related to Sqlite3 output
#---------------------------------------------------------------------------
#---------------------------------------------------------------------------
# Configuration options related to the Perl module output
#---------------------------------------------------------------------------
GENERATE_PERLMOD = NO
#---------------------------------------------------------------------------
# Configuration options related to the preprocessor
#---------------------------------------------------------------------------
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_ONLY_PREDEF = YES
SEARCH_INCLUDES = YES
INCLUDE_PATH =
INCLUDE_FILE_PATTERNS =
PREDEFINED = __attribute__((x))=
EXPAND_AS_DEFINED =
SKIP_FUNCTION_MACROS = YES
#---------------------------------------------------------------------------
# Configuration options related to external references
#---------------------------------------------------------------------------
TAGFILES =
GENERATE_TAGFILE =
ALLEXTERNALS = NO
EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES
#---------------------------------------------------------------------------
# Configuration options related to the dot tool
#---------------------------------------------------------------------------
DIA_PATH =
HIDE_UNDOC_RELATIONS = YES
HAVE_DOT = YES
DOT_NUM_THREADS = 0
DOT_FONTPATH =
CLASS_GRAPH = YES
COLLABORATION_GRAPH = NO
GROUP_GRAPHS = YES
UML_LOOK = NO
UML_LIMIT_NUM_FIELDS = 10
DOT_UML_DETAILS = NO
DOT_WRAP_THRESHOLD = 17
TEMPLATE_RELATIONS = YES
INCLUDE_GRAPH = YES
INCLUDED_BY_GRAPH = YES
CALL_GRAPH = YES
CALLER_GRAPH = YES
GRAPHICAL_HIERARCHY = YES
DIRECTORY_GRAPH = YES
DOT_IMAGE_FORMAT = svg
INTERACTIVE_SVG = NO
DOT_PATH =
DOTFILE_DIRS =
MSCFILE_DIRS =
DIAFILE_DIRS =
DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
DOT_MULTI_TARGETS = NO
GENERATE_LEGEND = YES
DOT_CLEANUP = YES

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
<!--BEGIN GENERATE_TREEVIEW-->
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
<ul>
$navpath
<li class="footer">Copyright &copy; 2020-2023 Kevin Matz -- Generated by doxygen $doxygenversion</li>
</ul>
</div>
<!--END GENERATE_TREEVIEW-->
<!--BEGIN !GENERATE_TREEVIEW-->
<address class="footer">
<small>
Copyright &copy; 2020-2023 Kevin Matz -- Generated by doxygen $doxygenversion
</small>
</address>
<!--END !GENERATE_TREEVIEW-->
</body>
</html>

View File

@ -1,56 +0,0 @@
<!-- HTML header for doxygen 1.9.1-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
<meta name="generator" content="Doxygen $doxygenversion"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="$relpath^jquery.js"></script>
<script type="text/javascript" src="$relpath^dynsections.js"></script>
$treeview
$search
$mathjax
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
$extrastylesheet
</head>
<body>
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<!--BEGIN TITLEAREA-->
<div id="titlearea">
<table cellspacing="0" cellpadding="0">
<tbody>
<tr style="height: 56px;">
<!--BEGIN PROJECT_LOGO-->
<td id="projectlogo"><img alt="Logo" src="$relpath^$projectlogo"/></td>
<!--END PROJECT_LOGO-->
<!--BEGIN PROJECT_NAME-->
<td id="projectalign" style="padding-left: 0.5em;">
<div id="projectname">$projectname
<!--BEGIN PROJECT_NUMBER-->&#160;<span id="projectnumber">$projectnumber</span><!--END PROJECT_NUMBER-->
</div>
<!--BEGIN PROJECT_BRIEF--><div id="projectbrief">$projectbrief</div><!--END PROJECT_BRIEF-->
</td>
<!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME-->
<!--BEGIN PROJECT_BRIEF-->
<td style="padding-left: 0.5em;">
<div id="projectbrief">$projectbrief</div>
</td>
<!--END PROJECT_BRIEF-->
<!--END !PROJECT_NAME-->
<!--BEGIN DISABLE_INDEX-->
<!--BEGIN SEARCHENGINE-->
<td>$searchbox</td>
<!--END SEARCHENGINE-->
<!--END DISABLE_INDEX-->
</tr>
</tbody>
</table>
</div>
<!--END TITLEAREA-->
<!-- end header part -->

@ -1 +0,0 @@
Subproject commit 7be08cbe5531f60607dffb2f8f76f6c070a9c81b

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

View File

@ -1,3 +0,0 @@
add_subdirectory("sACN Explorer")
add_subdirectory("widgetExplorer")

View File

@ -1,38 +0,0 @@
project(sacnExplorer VERSION ${PROJECT_VERSION})
find_package(Qt6 COMPONENTS Network Widgets REQUIRED)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
add_executable(${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PRIVATE
adduniversedialog.cpp
adduniversedialog.h
adduniversedialog.ui
lineeditdialog.cpp
lineeditdialog.h
lineeditdialog.ui
main.cpp
multiversewindow.cpp
multiversewindow.h
multiversewindow.ui
sacnexplorer.h
sacnexplorer.cpp
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
LCP::Qt::sACN
Qt::Network
Qt::Widgets
)
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER ${PROJECT_NAME}.company235.com
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
)

View File

@ -1,17 +0,0 @@
#include "adduniversedialog.h"
#include "ui_adduniversedialog.h"
AddUniverseDialog::AddUniverseDialog(QWidget *parent, uint16_t universe)
: QDialog(parent)
, ui(new Ui::AddUniverseDialog)
{
ui->setupUi(this);
ui->spinBox->setValue(universe);
connect(this, &QDialog::accepted,
this, [this](){emit submit(ui->spinBox->value());});
}
AddUniverseDialog::~AddUniverseDialog()
{
delete ui;
}

View File

@ -1,23 +0,0 @@
#pragma once
#include <QDialog>
QT_BEGIN_NAMESPACE
namespace Ui {
class AddUniverseDialog;
}
QT_END_NAMESPACE
class AddUniverseDialog : public QDialog
{
Q_OBJECT
public:
explicit AddUniverseDialog(QWidget *parent = nullptr, uint16_t universe = 1);
virtual ~AddUniverseDialog();
signals:
void submit(const uint16_t universe);
private:
Ui::AddUniverseDialog *ui;
};

View File

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddUniverseDialog</class>
<widget class="QDialog" name="AddUniverseDialog">
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="_3">
<item>
<layout class="QFormLayout" name="_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Universe</string>
</property>
<property name="buddy">
<cstring>spinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>63999</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="dialogButtonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>dialogButtonBox</sender>
<signal>accepted()</signal>
<receiver>AddUniverseDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>dialogButtonBox</sender>
<signal>rejected()</signal>
<receiver>AddUniverseDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,23 +0,0 @@
#include "lineeditdialog.h"
#include "ui_lineeditdialog.h"
LineEditDialog::LineEditDialog(QWidget *parent, QString label, int length,
QString value, QString placeholder) :
QDialog(parent),
ui(new Ui::LineEditDialog)
{
ui->setupUi(this);
ui->label->setText(label);
if (length > 0)
ui->lineEdit->setMaxLength(length);
ui->lineEdit->setText(value);
ui->lineEdit->setPlaceholderText(placeholder);
connect(this, &LineEditDialog::accepted,
this, [this]() {emit submit(ui->lineEdit->text());});
}
LineEditDialog::~LineEditDialog()
{
delete ui;
}

View File

@ -1,26 +0,0 @@
#ifndef LINEEDITDIALOG_H
#define LINEEDITDIALOG_H
#include <QDialog>
namespace Ui {
class LineEditDialog;
}
class LineEditDialog : public QDialog
{
Q_OBJECT
public:
explicit LineEditDialog(QWidget *parent = nullptr,
QString label = "line", int length = 0,
QString value = "", QString placeholder = "");
~LineEditDialog();
signals:
void submit(const QString);
private:
Ui::LineEditDialog *ui;
};
#endif // LINEEDITDIALOG_H

View File

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LineEditDialog</class>
<widget class="QDialog" name="LineEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>82</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>LineEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>LineEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,7 +0,0 @@
#include "sacnexplorer.h"
int main(int argc, char *argv[])
{
SacnExplorer a(argc, argv);
return a.exec();
}

View File

@ -1,327 +0,0 @@
#include "adduniversedialog.h"
#include "lineeditdialog.h"
#include "multiversewindow.h"
#include "multiversemodel.h"
#include "ui_multiversewindow.h"
#include "universewindow.h"
#include <QActionGroup>
#include <QMessageBox>
#include <QStringBuilder>
/**
* @brief MultiverseWindow::MultiverseWindow
* @param parent
*/
MultiverseWindow::MultiverseWindow(QWidget *parent, QSacnNode *node)
: QMainWindow(parent)
, ui(new Ui::MultiverseWindow)
, node(node)
, model(new MultiverseModel(this, node))
, sortProxy(new QSortFilterProxyModel(this))
{
// build the UI
ui->setupUi(this);
ui->menubar->setNativeMenuBar(false);
auto mergeMode = new QActionGroup(this);
mergeMode->addAction(ui->actionMergeModeHTP);
mergeMode->addAction(ui->actionMergeModeLTP);
// initial states
ui->actionTerminate->setEnabled(false);
ui->actionUnsubscribe->setEnabled(false);
ui->actionInspect->setEnabled(false);
ui->actionIPv4->setChecked(node->isEnabledIPv4());
ui->actionIPv6->setChecked(node->isEnabledIPv6());
ui->actionDiscovery->setChecked(node->discoveryEnabled());
// setup the model
sortProxy->setSourceModel(model);
ui->multiverseView->setModel(sortProxy);
ui->multiverseView->setSortingEnabled(true);
ui->multiverseView->sortByColumn(MultiverseModel::Column::Universe,
Qt::SortOrder::AscendingOrder);
ui->multiverseView->setContextMenuPolicy(Qt::CustomContextMenu);
ui->multiverseView->expandAll();
for (int i = 0; i < ui->multiverseView->model()->rowCount(); i++)
ui->multiverseView->setFirstColumnSpanned(i, QModelIndex(), true);
// model connections
connect(ui->multiverseView->selectionModel(), &QItemSelectionModel::currentChanged,
this, &MultiverseWindow::selectionChanged);
connect(ui->multiverseView, &QTreeView::customContextMenuRequested,
this, &MultiverseWindow::openContextMenu);
connect(ui->multiverseView, &QTreeView::doubleClicked,
this, &MultiverseWindow::openUniverseEditor);
// action connections
connect(ui->actionUACN, &QAction::triggered,
this, &MultiverseWindow::openUacnEditor);
connect(ui->actionIPv4, &QAction::toggled,
node, &QSacnNode::setIPv4);
connect(ui->actionIPv6, &QAction::toggled,
node, &QSacnNode::setIPv6);
connect(ui->actionDiscovery, &QAction::toggled,
node, [&, node](bool checked) {
checked ? node->discoveryStart() : node->discoveryStop();
});
connect(ui->actionCreate, &QAction::triggered,
this, [this, node]() {
auto dialog = new AddUniverseDialog(this, 1);
connect(dialog, &AddUniverseDialog::submit,
node, &QSacnNode::create);
dialog->setWindowTitle(tr("Create"));
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->open();
});
connect(ui->actionTerminate, &QAction::triggered,
this, [&, node]() {
auto index = ui->multiverseView->currentIndex();
auto data = index.data(Qt::EditRole);
if (data.metaType().id() == qMetaTypeId<QSacnUniverse*>())
{
auto univ = data.value<QSacnUniverse*>();
if (univ->isEditable())
node->terminate(univ->number());
}
});
connect(ui->actionSubscribe, &QAction::triggered,
this, [this, node]() {
auto val = QVariant(1);
auto index = ui->multiverseView->currentIndex();
if (index.isValid())
{
auto actual = sortProxy->mapToSource(index);
auto item = static_cast<MultiverseItem*>(actual.internalPointer());
val = item->data(MultiverseModel::Column::Universe, Qt::DisplayRole);
}
auto dialog = new AddUniverseDialog(this, val.toInt());
connect(dialog, &AddUniverseDialog::submit,
node, &QSacnNode::subscribe);
dialog->setWindowTitle(tr("Subscribe"));
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->open();
});
connect(ui->actionUnsubscribe, &QAction::triggered,
this, [&, node]() {
auto index = ui->multiverseView->currentIndex();
auto data = index.data(Qt::EditRole);
if (data.metaType().id() == qMetaTypeId<QSacnUniverse*>())
{
auto univ = data.value<QSacnUniverse*>();
if (!univ->isEditable())
node->unsubscribe(univ->number());
}
});
connect(ui->actionInspect, &QAction::triggered,
this, [this]() {
auto selected = ui->multiverseView->currentIndex();
openUniverseEditor(selected);
});
connect(ui->actionUniverseHoldLastLook, &QAction::toggled,
this, [this](bool state) {
auto selected = ui->multiverseView->currentIndex();
auto data = selected.data(Qt::EditRole);
auto univ = data.value<QSacnUniverse*>();
univ->setHoldLastLook(state);
});
connect(ui->actionMergeModeHTP, &QAction::toggled,
this, [this](bool state) {
auto selected = ui->multiverseView->currentIndex();
auto data = selected.data(Qt::EditRole);
auto univ = data.value<QSacnUniverse*>();
if (state)
univ->setMergeMode(sACN::UniverseArbitrator::MERGE_HTP);
else
univ->setMergeMode(sACN::UniverseArbitrator::MERGE_LTP);
});
connect(ui->actionAbout, &QAction::triggered,
this, [this](){
QString title = tr("About") % " " % qAppName();
QString text = QApplication::organizationName() % "\n"
% "https://" % QApplication::organizationDomain() % "\n\n"
% QApplication::applicationName() % " is a demonstration of the capabilities "
% "of the sACN implimentation in the OpenLCP protocol library using the "
% "QsACN platform driver." % "\n\n"
% "© 2020-2022 Kevin Matz" % "\n\n"
% "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:" % "\n\n"
% "The above copyright notice and this permission notice shall be included in "
% "all copies or substantial portions of the Software." % "\n\n"
% "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.";
QMessageBox::about(this, title, text);
});
connect(ui->actionAbout_Qt, &QAction::triggered,
this, [this](){QMessageBox::aboutQt(this);});
}
/**
* @brief MultiverseWindow::~MultiverseWindow
*/
MultiverseWindow::~MultiverseWindow()
{
qDeleteAll(mInspectors);
delete ui;
}
void MultiverseWindow::closeEvent(QCloseEvent *)
{
// close all universe windows
foreach (const auto & inspector, mInspectors)
inspector->close();
}
/**
* @brief MultiverseWindow::selectionChanged
* @param selected
* @param deselected
*/
void MultiverseWindow::selectionChanged(const QModelIndex &current,
const QModelIndex &previous)
{
Q_UNUSED(previous)
ui->actionTerminate->setEnabled(false);
ui->actionUnsubscribe->setEnabled(false);
ui->actionInspect->setEnabled(false);
ui->actionUniverseHoldLastLook->setEnabled(false);
ui->menuMerge_Mode->setEnabled(false);
auto data = current.data(Qt::EditRole);
// Data universes:
if (data.metaType().id() == qMetaTypeId<QSacnUniverse*>())
{
auto univ = data.value<QSacnUniverse*>();
ui->actionInspect->setEnabled(true);
ui->actionUniverseHoldLastLook->setChecked(univ->getHoldLastLook());
switch (univ->getMergeMode()){
case sACN::UniverseArbitrator::MERGE_HTP:
ui->actionMergeModeHTP->setChecked(true);
break;
case sACN::UniverseArbitrator::MERGE_LTP:
ui->actionMergeModeLTP->setChecked(true);
break;
default:
break;
}
if (univ->isEditable())
ui->actionTerminate->setEnabled(true);
else
{
ui->actionUnsubscribe->setEnabled(true);
if (univ->hasSources())
{
ui->actionUniverseHoldLastLook->setEnabled(true);
ui->menuMerge_Mode->setEnabled(true);
}
}
return;
}
}
/**
* @brief MultiverseWindow::openContextMenu
* @param pos
*/
void MultiverseWindow::openContextMenu(const QPoint &pos)
{
auto menu = new QMenu(ui->multiverseView);
menu->setAttribute(Qt::WA_DeleteOnClose, true);
auto globalpos = ui->multiverseView->mapToGlobal(pos);
auto data = ui->multiverseView->indexAt(pos).data(Qt::EditRole);
if (data.metaType().id() == qMetaTypeId<QSacnUniverse*>())
{ // Data universes:
menu->addAction(ui->actionInspect);
menu->addSeparator();
auto univ = data.value<QSacnUniverse*>();
if (univ->isEditable())
menu->addAction(ui->actionTerminate);
else
menu->addAction(ui->actionUnsubscribe);
menu->popup(globalpos);
}
else if (data.metaType().id() ==
qMetaTypeId<sACN::EXTENDED::DISCOVERY::discoveredUniverse*>())
{ // Discovery universes:
menu->addAction(ui->actionSubscribe);
menu->popup(globalpos);
}
else
// unknow metaType
menu->deleteLater();
}
/**
* @brief MultiverseWindow::openUniverseEditor
* @param index
*/
void MultiverseWindow::openUniverseEditor(const QModelIndex &index)
{
auto data = index.data(Qt::EditRole);
// Data universes:
if (data.metaType().id() == qMetaTypeId<QSacnUniverse*>())
{
auto univ = data.value<QSacnUniverse*>();
foreach (const auto & inspector, mInspectors)
if (inspector->universe() == univ->get())
return inspector->show();
auto view = new UniverseWindow(nullptr, univ->get());
mInspectors.insert(view);
connect(view, &QObject::destroyed, this, [this, view](){mInspectors.remove(view);});
connect(univ, &QObject::destroyed, view, &QMainWindow::close);
view->setWindowTitle(QString("%1 '%2' : "+tr("Universe")+" %3").arg(
univ->isEditable() ? tr("Editing") : tr("Viewing"),
univ->sourceName(),
QString::number(univ->number())));
view->setAttribute(Qt::WA_DeleteOnClose, true);
view->show();
return;
}
// Discovery universes:
if (data.metaType().id() ==
qMetaTypeId<sACN::EXTENDED::DISCOVERY::discoveredUniverse*>())
{
auto disc = data.value<sACN::EXTENDED::DISCOVERY::discoveredUniverse*>();
auto dialog = new AddUniverseDialog(this, disc->universe);
connect(dialog, &AddUniverseDialog::submit,
node, &QSacnNode::subscribe);
dialog->setWindowTitle(tr("Create"));
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->open();
return;
}
}
/**
* @brief MultiverseWindow::openUacnEditor
*/
void MultiverseWindow::openUacnEditor()
{
auto editor = new LineEditDialog(this, "Name", 63,
node->userName().c_str(), node->fixedName().c_str());
connect(editor, &LineEditDialog::submit,
this, [&](const QString name) {node->assignUserName(name.toStdString());});
editor->setWindowTitle("User Assigned Component Name");
editor->setAttribute(Qt::WA_DeleteOnClose);
editor->open();
}

View File

@ -1,68 +0,0 @@
/*
multiverseview.h
Copyright (c) 2021 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.
*/
#pragma once
#include "multiversemodel.h"
#include <qsacnnode.h>
#include <universewindow.h>
#include <QMainWindow>
#include <QSortFilterProxyModel>
QT_BEGIN_NAMESPACE
namespace Ui
{
class MultiverseWindow;
}
QT_END_NAMESPACE
/**
* @brief The MultiverseView class
*/
class MultiverseWindow
: public QMainWindow
{
Q_OBJECT
public:
explicit MultiverseWindow(QWidget *parent = nullptr,
QSacnNode* node = nullptr);
virtual ~MultiverseWindow();
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void selectionChanged(const QModelIndex &current, const QModelIndex &previous);
void openContextMenu(const QPoint &pos);
void openUniverseEditor(const QModelIndex &index);
void openUacnEditor();
private:
Ui::MultiverseWindow *ui;
QSacnNode * node;
MultiverseModel * model;
QSortFilterProxyModel *sortProxy;
QSet<UniverseWindow*> mInspectors;
};

View File

@ -1,304 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MultiverseWindow</class>
<widget class="QMainWindow" name="MultiverseWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="multiverseView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<widget class="QMenu" name="menuNode">
<property name="title">
<string>Node</string>
</property>
<addaction name="actionIPv4"/>
<addaction name="actionIPv6"/>
<addaction name="actionUACN"/>
</widget>
<widget class="QMenu" name="menusUniverse">
<property name="title">
<string>Universe</string>
</property>
<widget class="QMenu" name="menuMerge_Mode">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Merge Mode</string>
</property>
<addaction name="actionMergeModeHTP"/>
<addaction name="actionMergeModeLTP"/>
</widget>
<addaction name="actionInspect"/>
<addaction name="actionUniverseHoldLastLook"/>
<addaction name="menuMerge_Mode"/>
</widget>
<widget class="QMenu" name="menuReceiver">
<property name="title">
<string>Receiver</string>
</property>
<addaction name="actionSubscribe"/>
<addaction name="actionUnsubscribe"/>
<addaction name="separator"/>
<addaction name="actionDiscovery"/>
</widget>
<widget class="QMenu" name="menuSource">
<property name="title">
<string>Source</string>
</property>
<addaction name="actionCreate"/>
<addaction name="actionTerminate"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionAbout_Qt"/>
</widget>
<addaction name="menuNode"/>
<addaction name="menuReceiver"/>
<addaction name="menuSource"/>
<addaction name="menusUniverse"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>toolBar</string>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonFollowStyle</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionSubscribe"/>
<addaction name="actionUnsubscribe"/>
<addaction name="separator"/>
<addaction name="actionCreate"/>
<addaction name="actionTerminate"/>
<addaction name="separator"/>
<addaction name="actionInspect"/>
</widget>
<action name="actionSubscribe">
<property name="icon">
<iconset theme="call-start">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Subscribe</string>
</property>
<property name="toolTip">
<string>Subscribe to an sACN Universe.</string>
</property>
<property name="shortcut">
<string>+</string>
</property>
</action>
<action name="actionUnsubscribe">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="call-stop">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Unsubscribe</string>
</property>
<property name="toolTip">
<string>Unsubscribe from the sACN Universe.</string>
</property>
<property name="shortcut">
<string>-</string>
</property>
</action>
<action name="actionCreate">
<property name="icon">
<iconset theme="document-new">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Create</string>
</property>
<property name="toolTip">
<string>Create an sACN Universe to transmit.</string>
</property>
<property name="shortcut">
<string>N</string>
</property>
</action>
<action name="actionTerminate">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="edit-delete">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Terminate</string>
</property>
<property name="toolTip">
<string>Terminate transmission of the sACN Universe.</string>
</property>
<property name="shortcut">
<string>X</string>
</property>
</action>
<action name="actionUACN">
<property name="icon">
<iconset theme="network-server">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>User Assigned Component Name</string>
</property>
<property name="toolTip">
<string>Set this Component's UACN</string>
</property>
</action>
<action name="actionInspect">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="edit-find">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Inspect</string>
</property>
<property name="toolTip">
<string>View the data of the Universe.</string>
</property>
<property name="shortcut">
<string>Space</string>
</property>
</action>
<action name="actionIPv4">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>IPv4</string>
</property>
<property name="toolTip">
<string>Enable IPv4</string>
</property>
<property name="shortcut">
<string>Ctrl+4</string>
</property>
</action>
<action name="actionIPv6">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>IPv6</string>
</property>
<property name="toolTip">
<string>Enable IPv6</string>
</property>
<property name="shortcut">
<string>Ctrl+6</string>
</property>
</action>
<action name="actionDiscovery">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Discovery</string>
</property>
<property name="toolTip">
<string>Enable Discovery in the Receiver. Discovery messages are always transmitted.</string>
</property>
<property name="shortcut">
<string>Ctrl+D</string>
</property>
</action>
<action name="actionUniverseHoldLastLook">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Hold Last Look</string>
</property>
<property name="toolTip">
<string>Retain at least 1 inacitve source on this universe.</string>
</property>
</action>
<action name="actionMergeModeLTP">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>LTP</string>
</property>
<property name="toolTip">
<string>Latest Takes Precedence</string>
</property>
</action>
<action name="actionMergeModeHTP">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>HTP</string>
</property>
<property name="toolTip">
<string>Highest Takes Precedence</string>
</property>
</action>
<action name="actionAbout">
<property name="icon">
<iconset theme="help-about"/>
</property>
<property name="text">
<string>About</string>
</property>
</action>
<action name="actionAbout_Qt">
<property name="text">
<string>About Qt</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,67 +0,0 @@
#include <QDebug>
#include "sacnexplorer.h"
SacnExplorer::SacnExplorer(int argc, char *argv[])
: QApplication(argc, argv)
, node_(nullptr)
, window_(nullptr)
{
setOrganizationName("Company235");
setOrganizationDomain("company235.com");
setApplicationName(tr("sACN Explorer"));
settings_ = new QSettings(this);
// Persistant configuration
qDebug() << "Loaded application settings from" << settings_->fileName();
settings_->beginGroup("ACN");
auto cid = settings_->value("CID", QUuid::createUuid()).toUuid();
auto uacn = settings_->value("UACN", "").toString();
auto ipv4 = settings_->value("IPv4", true).toBool();
auto ipv6 = settings_->value("IPv6", true).toBool();
settings_->endGroup();
settings_->beginGroup("sACN");
settings_->beginGroup("receiver");
auto discovery = settings_->value("discovery", true).toBool();
settings_->endGroup();
settings_->endGroup();
// sACN Node
node_ = new QSacnNode(this, cid, applicationName(), ipv4, ipv6);
node_->assignUserName(uacn.toStdString());
qDebug() << "sACN node" << QString(node_->name().c_str()) << "started with CID" << cid.toString();
if (discovery)
node_->discoveryStart();
// Multiverse Explorer
window_ = new MultiverseWindow(nullptr, node_);
window_->setWindowTitle(QApplication::applicationName());
window_->show();
}
SacnExplorer::~SacnExplorer()
{
saveSettings();
delete window_;
}
/**
* @brief SacnExplorer::saveSettings
*/
void SacnExplorer::saveSettings()
{
qDebug() << "Saving application settings to" << settings_->fileName();
settings_->beginGroup("ACN");
settings_->setValue("CID", node_->cid().string().c_str());
settings_->setValue("UACN", node_->userName().c_str());
settings_->setValue("IPv4", node_->isEnabledIPv4());
settings_->setValue("IPv6", node_->isEnabledIPv6());
settings_->endGroup();
settings_->beginGroup("sACN");
settings_->beginGroup("receiver");
settings_->setValue("discovery", node_->discoveryEnabled());
settings_->endGroup();
settings_->endGroup();
}

View File

@ -1,24 +0,0 @@
#pragma once
#include "multiversewindow.h"
#include "qsacnnode.h"
#include <QApplication>
#include <QSettings>
class SacnExplorer
: public QApplication
{
Q_OBJECT
public:
explicit SacnExplorer(int argc, char *argv[]);
virtual ~SacnExplorer();
private:
void saveSettings();
QSettings *settings_;
QSacnNode *node_;
MultiverseWindow *window_;
};

View File

@ -1,38 +0,0 @@
project(widgetExplorer VERSION ${PROJECT_VERSION})
find_package(Qt6 COMPONENTS Gui Network SerialPort Widgets REQUIRED)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
add_executable(${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PRIVATE
devicewindow.cpp
devicewindow.h
devicewindow.ui
main.cpp
metadatadialog.h
metadatadialog.cpp
metadatadialog.ui
widgetexplorer.cpp
widgetexplorer.h
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
LCP::Qt::sACN
LCP::Qt::DmxWidget
Qt::Gui
Qt::Network
Qt::SerialPort
Qt::Widgets
)
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER ${PROJECT_NAME}.company235.com
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
)

View File

@ -1,178 +0,0 @@
#include "devicewindow.h"
#include "metadatadialog.h"
#include "ui_devicewindow.h"
#include <QMessageBox>
#include <QStringBuilder>
DeviceWindow::DeviceWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::DeviceWindow)
, model(new WidgetModel(this))
, sortProxy(new QSortFilterProxyModel(this))
{
// build the UI
ui->setupUi(this);
ui->menubar->setNativeMenuBar(false); // workaround for poor Qt6 support in KDE 5
// setup the model
sortProxy->setSourceModel(model);
ui->widgetView->setModel(sortProxy);
ui->widgetView->setSortingEnabled(true);
ui->widgetView->sortByColumn(WidgetModel::PortName, Qt::SortOrder::AscendingOrder);
ui->widgetView->setColumnWidth(WidgetModel::Manufacturer, 128);
ui->widgetView->setColumnWidth(WidgetModel::Description, 196);
ui->widgetView->setColumnWidth(WidgetModel::SerialNumber, 96);
// model connections
connect(ui->widgetView->selectionModel(), &QItemSelectionModel::currentChanged,
this, &DeviceWindow::selectionChanged);
connect(model, &WidgetModel::dataChanged, this, [this](){
auto selected = ui->widgetView->currentIndex();
if (selected.isValid() && model->rowCount())
ui->widgetView->selectRow(selected.row());
});
connect(ui->actionOpen, &QAction::triggered,
this, [this]() {
auto selected = ui->widgetView->currentIndex();
if (selected.isValid())
selected.data(Qt::EditRole).value<DmxWidget*>()->open();
});
connect(ui->actionClose, &QAction::triggered,
this, [this]() {
auto selected = ui->widgetView->currentIndex();
if (selected.isValid())
selected.data(Qt::EditRole).value<DmxWidget*>()->close();
});
connect(ui->actionDMX, &QAction::triggered,
this, [this]() {
auto selected = ui->widgetView->currentIndex();
if (!selected.isValid())
return;
openDmxWindow(selected);
});
connect(ui->actionRDM, &QAction::triggered,
this, [this]() {
auto selected = ui->widgetView->currentIndex();
if (!selected.isValid())
return;
/// \todo Add an action to monitor RDM.
});
connect(ui->actionParameter, &QAction::triggered,
this, [this]() {
auto selected = ui->widgetView->currentIndex();
if (!selected.isValid())
return;
MetadataDialog dlg(selected.data(Qt::EditRole).value<std::shared_ptr<DmxWidget>>().get(), this);
dlg.exec();
});
connect(ui->actionUserData, &QAction::triggered,
this, [this]() {
auto selected = ui->widgetView->currentIndex();
if (!selected.isValid())
return;
/// \todo Add an action to manage user configuraion data.
});
connect(ui->actionFirmware, &QAction::triggered,
this, [this]() {
auto selected = ui->widgetView->currentIndex();
if (!selected.isValid())
return;
/// \todo Add a notice about firmware support.
});
// action connections
connect(ui->actionRefreshList, &QAction::triggered, model, &WidgetModel::rescanPorts);
connect(ui->actionRefreshList, &QAction::triggered, this, [this](){
ui->widgetView->clearSelection();
selectionChanged(QModelIndex(), QModelIndex());
});
connect(ui->actionAbout, &QAction::triggered,
this, [this](){
QString title = tr("About") % " " % qAppName();
QString text = QApplication::organizationName() % "\n"
% "https://" % QApplication::organizationDomain() % "\n\n"
% QApplication::applicationName() % " is a demonstration of the capabilities "
% "of the DMX-USB-Pro driver implimentation in the OpenLCP protocol library."
% "\n\n" % "© 2023 Kevin Matz" % "\n\n"
% "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:" % "\n\n"
% "The above copyright notice and this permission notice shall be included in "
% "all copies or substantial portions of the Software." % "\n\n"
% "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.";
QMessageBox::about(this, title, text);
});
connect(ui->actionAbout_Qt, &QAction::triggered,
this, [this](){QMessageBox::aboutQt(this);});
}
DeviceWindow::~DeviceWindow()
{
qDeleteAll(inspectors_);
delete ui;
}
void DeviceWindow::selectionChanged(const QModelIndex &current, const QModelIndex &previous)
{
Q_UNUSED(previous)
bool valid = false;
bool connected = false;
bool hasdmx = false;
bool hasrdm = false;
if (current.isValid())
{
auto index = model->index(current.row(), 0, current.parent());
auto wdgt = index.data(Qt::EditRole).value<std::shared_ptr<DmxWidget>>();
valid = true;
connected = wdgt->isConnected();
hasdmx = connected & wdgt->featureDMX();
hasrdm = connected & wdgt->featureRDM();
}
ui->menuWidget->setEnabled(valid);
ui->actionOpen->setEnabled(!connected);
ui->actionClose->setEnabled(connected);
ui->actionDMX->setEnabled(hasdmx);
ui->actionRDM->setEnabled(hasrdm);
ui->actionParameter->setEnabled(connected);
ui->actionUserData->setEnabled(connected);
ui->actionFirmware->setEnabled(false);
}
/**
* @brief DeviceWindow::openDmxWindow
* @param index
*/
void DeviceWindow::openDmxWindow(const QModelIndex &index)
{
auto wdgt = index.data(Qt::EditRole).value<std::shared_ptr<DmxWidget>>();
auto univ = std::static_pointer_cast<DMX::Universe>(wdgt);
foreach (const auto & inspector, inspectors_)
if (inspector->universe() == univ)
return inspector->show();
auto view = new UniverseWindow(nullptr, univ);
inspectors_.insert(view);
connect(view, &QObject::destroyed, this, [this, view]() {
inspectors_.remove(view);
});
view->setWindowTitle(QString("%1 '%2'").arg(
wdgt->isEditable() ? tr("Editing") : tr("Viewing"),
wdgt->portName()));
view->setAttribute(Qt::WA_DeleteOnClose, true);
view->show();
}

View File

@ -1,30 +0,0 @@
#pragma once
#include <QMainWindow>
#include <QSortFilterProxyModel>
#include <universewindow.h>
#include <widgetmodel.h>
namespace Ui {
class DeviceWindow;
}
class DeviceWindow : public QMainWindow
{
Q_OBJECT
public:
explicit DeviceWindow(QWidget *parent = nullptr);
~DeviceWindow();
private slots:
void selectionChanged(const QModelIndex &current, const QModelIndex &previous);
void openDmxWindow(const QModelIndex &index);
private:
Ui::DeviceWindow *ui;
WidgetModel *model;
QSortFilterProxyModel *sortProxy;
QSet<UniverseWindow*> inspectors_;
};

View File

@ -1,323 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DeviceWindow</class>
<widget class="QMainWindow" name="DeviceWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableView" name="widgetView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>25</height>
</rect>
</property>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionAbout_Qt"/>
</widget>
<widget class="QMenu" name="menuPort">
<property name="title">
<string>Port</string>
</property>
<addaction name="actionOpen"/>
<addaction name="actionClose"/>
<addaction name="separator"/>
<addaction name="actionRefreshList"/>
</widget>
<widget class="QMenu" name="menuWidget">
<property name="title">
<string>Widget</string>
</property>
<addaction name="actionDMX"/>
<addaction name="actionRDM"/>
<addaction name="separator"/>
<addaction name="actionParameter"/>
<addaction name="actionUserData"/>
<addaction name="separator"/>
<addaction name="actionFirmware"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>View</string>
</property>
<widget class="QMenu" name="menuToolbars">
<property name="title">
<string>Toolbars</string>
</property>
<addaction name="actionPortToolbar"/>
<addaction name="actionWidgetToolbar"/>
</widget>
<addaction name="menuToolbars"/>
</widget>
<addaction name="menuPort"/>
<addaction name="menuWidget"/>
<addaction name="menuView"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBarPort">
<property name="windowTitle">
<string>Port</string>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonFollowStyle</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionRefreshList"/>
<addaction name="actionOpen"/>
<addaction name="actionClose"/>
<addaction name="separator"/>
<addaction name="separator"/>
<addaction name="separator"/>
</widget>
<widget class="QToolBar" name="toolBarWidget">
<property name="windowTitle">
<string>Widget</string>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonFollowStyle</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionDMX"/>
<addaction name="actionRDM"/>
<addaction name="actionParameter"/>
</widget>
<action name="actionAbout">
<property name="icon">
<iconset theme="help-about">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>About</string>
</property>
</action>
<action name="actionAbout_Qt">
<property name="text">
<string>About Qt</string>
</property>
</action>
<action name="actionRefreshList">
<property name="icon">
<iconset theme="view-refresh">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Refresh List</string>
</property>
</action>
<action name="actionOpen">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="call-start">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Open</string>
</property>
</action>
<action name="actionClose">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="call-stop">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Close</string>
</property>
</action>
<action name="actionParameter">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="preferences-other">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Parameters</string>
</property>
<property name="toolTip">
<string>Tx Parameters</string>
</property>
</action>
<action name="actionUserData">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="document-properties">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>User Data</string>
</property>
</action>
<action name="actionFirmware">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="system-run">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Upload Firmware</string>
</property>
</action>
<action name="actionDMX">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="edit-find">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>DMX Universe</string>
</property>
</action>
<action name="actionRDM">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="computer">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>RDM Devices</string>
</property>
</action>
<action name="actionPortToolbar">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="drive-removable-media">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Port</string>
</property>
<property name="toolTip">
<string>Show Port Toolbar</string>
</property>
</action>
<action name="actionWidgetToolbar">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="media-flash">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Widget</string>
</property>
<property name="toolTip">
<string>Show Widget Toolbar</string>
</property>
</action>
</widget>
<resources/>
<connections>
<connection>
<sender>actionWidgetToolbar</sender>
<signal>triggered(bool)</signal>
<receiver>toolBarWidget</receiver>
<slot>setVisible(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>498</x>
<y>53</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionPortToolbar</sender>
<signal>triggered(bool)</signal>
<receiver>toolBarPort</receiver>
<slot>setVisible(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>98</x>
<y>53</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,7 +0,0 @@
#include "widgetexplorer.h"
int main(int argc, char *argv[])
{
WidgetExplorer a(argc, argv);
return a.exec();
}

View File

@ -1,47 +0,0 @@
#include "metadatadialog.h"
#include "ui_metadatadialog.h"
#include <pro.h>
#include <QPushButton>
MetadataDialog::MetadataDialog(DmxWidget *device, QWidget *parent)
: QDialog(parent)
, ui(new Ui::MetadataDialog)
, dev(device)
{
ui->setupUi(this);
ui->breakSpinBox->setSingleStep(ENTTEC::Pro::DMX_BREAK_INTERVAL);
ui->breakSpinBox->setMinimum(ENTTEC::Pro::DMX_BREAK_INTERVAL * ENTTEC::Pro::DMX_BREAK_MIN);
ui->breakSpinBox->setMaximum(ENTTEC::Pro::DMX_BREAK_INTERVAL * ENTTEC::Pro::DMX_BREAK_MAX);
ui->breakSpinBox->setValue(device->txBreakTime());
ui->mabSpinBox->setSingleStep(ENTTEC::Pro::DMX_MAB_INTERVAL);
ui->mabSpinBox->setMinimum(ENTTEC::Pro::DMX_MAB_INTERVAL * ENTTEC::Pro::DMX_MAB_MIN);
ui->mabSpinBox->setMaximum(ENTTEC::Pro::DMX_MAB_INTERVAL * ENTTEC::Pro::DMX_MAB_MAX);
ui->mabSpinBox->setValue(device->txMabTime());
ui->rateSpinBox->setSingleStep(1);
ui->rateSpinBox->setMinimum(0); // zero rate for fastest tx possible
ui->rateSpinBox->setMaximum(ENTTEC::Pro::DMX_RATE_MAX);
ui->rateSpinBox->setValue(device->txRate());
connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked,
this, [this](){
ui->breakSpinBox->setValue(17 * ENTTEC::Pro::DMX_BREAK_INTERVAL); // match OEM
ui->mabSpinBox->setValue(10 * ENTTEC::Pro::DMX_MAB_INTERVAL); // match OEM
ui->rateSpinBox->setValue(40); // match OEM
});
connect(this, &QDialog::accepted, this, [this](){
dev->setTxBreakTime(ui->breakSpinBox->value());
dev->setTxMabTime(ui->mabSpinBox->value());
dev->setTxRate(ui->rateSpinBox->value());
dev->setParameters();
});
}
MetadataDialog::~MetadataDialog()
{
delete ui;
}

View File

@ -1,22 +0,0 @@
#pragma once
#include <QDialog>
#include <dmxwidget.h>
namespace Ui {
class MetadataDialog;
}
class MetadataDialog : public QDialog
{
Q_OBJECT
public:
explicit MetadataDialog(DmxWidget *device, QWidget *parent = nullptr);
~MetadataDialog();
private:
Ui::MetadataDialog *ui;
DmxWidget * dev;
};

View File

@ -1,130 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MetadataDialog</class>
<widget class="QDialog" name="MetadataDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>467</width>
<height>151</height>
</rect>
</property>
<property name="windowTitle">
<string>Tx Parameters</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="breakSpinBox">
<property name="toolTip">
<string>The BREAK generated by a transmitter is defined as a mark-to-space transition followed by a low of this duration, followed by a low to high transition.</string>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="suffix">
<string> μs</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="singleStep">
<double>10.670000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelMAB">
<property name="toolTip">
<string>The MARK separating the BREAK and the START Code</string>
</property>
<property name="text">
<string>MAB Time</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="mabSpinBox">
<property name="toolTip">
<string>The MARK separating the BREAK and the START Code</string>
</property>
<property name="suffix">
<string> μs</string>
</property>
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelRate">
<property name="text">
<string>Rate</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="rateSpinBox">
<property name="suffix">
<string> fps</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::RestoreDefaults|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelBreak">
<property name="toolTip">
<string>The BREAK indicates the start of a new packet.</string>
</property>
<property name="text">
<string>Break Time</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>MetadataDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>MetadataDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,37 +0,0 @@
#include <QDebug>
#include "widgetexplorer.h"
WidgetExplorer::WidgetExplorer(int argc, char *argv[])
: QApplication(argc, argv)
, window_(nullptr)
{
setOrganizationName("Company235");
setOrganizationDomain("company235.com");
setApplicationName(tr("widgetExplorer"));
settings_ = new QSettings(this);
// Persistant configuration
qDebug() << "Loaded application settings from" << settings_->fileName();
// Device list
window_ = new DeviceWindow();
window_->setWindowTitle(QApplication::applicationName());
window_->show();
}
WidgetExplorer::~WidgetExplorer()
{
saveSettings();
delete window_;
}
/**
* @brief SacnExplorer::saveSettings
*/
void WidgetExplorer::saveSettings()
{
qDebug() << "Saving application settings to" << settings_->fileName();
}

View File

@ -1,21 +0,0 @@
#pragma once
#include "devicewindow.h"
#include <QApplication>
#include <QSettings>
class WidgetExplorer
: public QApplication
{
Q_OBJECT
public:
explicit WidgetExplorer(int argc, char *argv[]);
virtual ~WidgetExplorer();
private:
void saveSettings();
QSettings *settings_;
DeviceWindow *window_;
};

View File

@ -1 +0,0 @@
add_subdirectory(bufferstream)

View File

@ -1,14 +0,0 @@
project(bufferstream VERSION ${PROJECT_VERSION})
add_library(${PROJECT_NAME} SHARED)
add_library(LCP::BufferStream ALIAS ${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PUBLIC
bufferstream.h
PRIVATE
bufferstream.cpp
)
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION})
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,443 +0,0 @@
/*
bufferstream.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 "bufferstream.h"
/**
* @brief bufferstream::bufferstream
* @param p
* @param l
* @param o
*/
bufferstream::bufferstream(uint8_t * p, std::streamsize l, endian o)
: std::basic_streambuf<uint8_t>()
, std::basic_iostream<uint8_t>(this)
, order_(o)
{
setg(p, p, p + l);
setp(p, p + l);
}
/**
* @brief bufferstream::available
* @return
*/
size_t bufferstream::available()
{
return in_avail();
}
/**
* @brief bufferstream::data
* @return
*/
uint8_t *bufferstream::data()
{
return gptr();
}
/**
* @brief bufferstream::size
* @return
*/
size_t bufferstream::size()
{
return pptr() - pbase();
}
/**
* @brief bufferstream::base
* @return
*/
uint8_t *bufferstream::base()
{
return pbase();
}
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (uint8_t &val)
{
val = readType<uint8_t>();
return *this;
};
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (uint16_t &val)
{
val = readType<uint16_t>();
return *this;
};
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (uint32_t &val)
{
val = readType<uint32_t>();
return *this;
};
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (uint64_t &val)
{
val = readType<uint64_t>();
return *this;
};
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (int8_t &val)
{
val = readType<int8_t>();
return *this;
}
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (int16_t &val)
{
val = readType<int16_t>();
return *this;
}
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (int32_t &val)
{
val = readType<int32_t>();
return *this;
}
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (int64_t &val)
{
val = readType<int64_t>();
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const int8_t &val)
{
put(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const int16_t &val)
{
writeType<int16_t>(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const int32_t &val)
{
writeType<int32_t>(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const int64_t &val)
{
writeType<int64_t>(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const uint8_t &val)
{
writeType<uint8_t>(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const uint16_t &val)
{
writeType<uint16_t>(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const uint32_t &val)
{
writeType<uint32_t>(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const uint64_t &val)
{
writeType<uint64_t>(val);
return *this;
}
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (float &val)
{
val = readType<float>();
return *this;
};
/**
* @brief bufferstream::operator >>
* @param val
* @return
*/
bufferstream &bufferstream::operator>> (double &val)
{
val = readType<double>();
return *this;
};
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const float &val)
{
writeType<float>(val);
return *this;
}
/**
* @brief bufferstream::operator <<
* @param val
* @return
*/
bufferstream &bufferstream::operator<< (const double &val)
{
writeType<double>(val);
return *this;
}
/**
* @brief bufferstream::operator >>
* @param obj
* @return
*/
bufferstream &bufferstream::operator>> (streamable &obj)
{
obj.iStream(shared_from_this());
return *this;
}
/**
* @brief bufferstream::operator <<
* @param obj
* @return
*/
bufferstream &bufferstream::operator<< (const streamable &obj)
{
obj.oStream(shared_from_this());
return *this;
}
/**
* @brief bufferstream::operator >>
* @param str
* @return
*/
bufferstream &bufferstream::operator>> (std::string &str)
{
readString(str, 0, true); // variable length, null-terminated
return *this;
}
/**
* @brief bufferstream::operator <<
* @param str
* @return
*/
bufferstream &bufferstream::operator<< (const std::string &str)
{
writeString(str, 0, true); // variable length, null-terminated
return *this;
}
/**
* @brief bufferstream::readString
* @param str std::string to which the read string will be appended.
* @param fixed_length this many bytes will be read from the stream. If 0, all
* available bytes on the stream will be considered in appending the string.
* @param terminated If set, read the stream into the string until a 0 is read, otherwise
* the entire non-fixed length will be read. Has no effect on fixed-length strings.
*/
void bufferstream::readString(std::string &str, const int fixed_length, const bool terminated)
{
if (fixed_length) // FIXED LENGTH
{
uint8_t buffer[fixed_length];
read(buffer, fixed_length);
if (gcount() != fixed_length)
return setstate(std::ios_base::failbit);
str += std::string(reinterpret_cast<char*>(buffer));
} // end FIXED LENGTH
else // VARIABLE LENGTH
{
if (terminated) // null terminated
{
char c;
while (good())
{
c = readType<char>();
if (c == 0)
break;
str += c;
}
}
else // all remaining bytes
{
str += std::string(reinterpret_cast<char*>(data()), available());
setstate(std::ios_base::eofbit);
}
} // end VARIABLE LENGTH
}
/**
* @brief bufferstream::writeString
* @param str
* @param fixed_length write this length to the stream, padding with null
* if str is shorter than fixed_length. If 0, will write the unrestricted
* contents of str, including it's null termination.
* @param terminated If true, the last byte of fixed_length is guaranteed to
* be a 0 (null) byte.
*/
void bufferstream::writeString(const std::string &str, const size_t fixed_length,
const bool terminated)
{
// fixed_length == 0 means dynamic length.
bool fixed = fixed_length > 0 ? true : false;
// fixed length strings restricted to fixed_lengh characters
size_t maxlen = fixed ? fixed_length : str.size() + 1; // null terminated
// write no more of the string than is available
size_t wrtlen = str.size() < maxlen ? str.size() : maxlen;
// terminated fixed-length strings get a guaranteed padding byte
if (fixed && terminated && wrtlen == maxlen)
--wrtlen;
// output the correct ammount of data from the string
write(reinterpret_cast<const uint8_t*>(str.data()), wrtlen);
// output any required padding bytes
for (size_t i = wrtlen; i < maxlen; i++)
put(0);
}

View File

@ -1,184 +0,0 @@
/*
bufferstream.h
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.
*/
#pragma once
#include <iostream>
#include <memory>
#include <string>
struct streamable;
/**
* @brief Input/Output stream for unsigned 8-bit buffers
*
* By default, wide types will be handled as big-endian, the network byte order.
*/
class bufferstream
: public std::enable_shared_from_this<bufferstream>
, private std::basic_streambuf<uint8_t>
, public std::basic_iostream<uint8_t>
{
public:
/**
* @brief The endian enum is not available until C++20.
*
* \note This implimentation may be compiler dependant.
*
* \note After upgrading the project to C++20 or better, drop this if favor of
* the standard implimentation in <bit.h>
*/
enum class endian
{
little = __ORDER_LITTLE_ENDIAN__,
big = __ORDER_BIG_ENDIAN__,
native = __BYTE_ORDER__,
};
bufferstream(uint8_t *p, std::streamsize l, endian o = endian::big); // default network byte-order
// input sequence
size_t available();
uint8_t *data();
// output sequence
size_t size();
uint8_t *base();
// unsigned integer
bufferstream &operator>> (uint8_t &val);
bufferstream &operator>> (uint16_t &val);
bufferstream &operator>> (uint32_t &val);
bufferstream &operator>> (uint64_t &val);
bufferstream &operator<< (const uint8_t &val);
bufferstream &operator<< (const uint16_t &val);
bufferstream &operator<< (const uint32_t &val);
bufferstream &operator<< (const uint64_t &val);
// signed integer
bufferstream &operator>> (int8_t &val);
bufferstream &operator>> (int16_t &val);
bufferstream &operator>> (int32_t &val);
bufferstream &operator>> (int64_t &val);
bufferstream &operator<< (const int8_t &val);
bufferstream &operator<< (const int16_t &val);
bufferstream &operator<< (const int32_t &val);
bufferstream &operator<< (const int64_t &val);
// floating point
bufferstream &operator>> (float &val);
bufferstream &operator>> (double &val);
bufferstream &operator<< (const float &val);
bufferstream &operator<< (const double &val);
// stream objects
bufferstream &operator>> (streamable &obj);
bufferstream &operator<< (const streamable &obj);
// null-terminated strings
bufferstream &operator>> (std::string &str);
bufferstream &operator<< (const std::string &str);
// strings
void readString(std::string& str, const int fixed_length = 0, const bool terminated = true);
void writeString(const std::string& str, const size_t fixed_length = 0,
const bool terminated = true);
// reinterpreted i/o
/**
* @brief readType
* @return T
*/
template<typename T>
T readType()
{
T ret = 0;
uint_fast8_t width = sizeof(T);
if (in_avail() < width)
setstate(std::ios_base::failbit);
if (!good())
return ret;
if (width == 1)
{
ret = static_cast<T>(get());
}
else
{
auto bytes = reinterpret_cast<uint8_t*>(&ret);
if (order_ == endian::native)
for (int_fast8_t i = 0; i < width; i++)
bytes[i] = get();
else
for (int_fast8_t i = width; --i >= 0;)
bytes[i] = get();
}
if (!in_avail())
setstate(std::ios_base::eofbit);
return ret;
}
/**
* @brief writeType
* @param val
*/
template<typename T>
void writeType (const T& val)
{
uint_fast8_t width = sizeof(T);
if (width == 1) {
put(static_cast<uint8_t>(val));
return;
}
auto bytes = reinterpret_cast<const uint8_t*>(&val);
if (order_ == endian::native)
for (int_fast8_t i = 0; i < width; i++)
put(bytes[i]);
else
for (int_fast8_t i = width; --i >= 0;)
put(bytes[i]);
}
private:
endian order_;
};
/**
* @brief The base for data objects that can be read from and written to a bufferstream.
*/
struct streamable
{
virtual ~streamable() {};
/**
* @brief streamSize
* @return length (count of octets) on the wire
*/
virtual size_t streamSize() const = 0;
/**
* @brief fill structure data from input stream
*/
virtual void iStream(std::shared_ptr<bufferstream>) = 0;
/**
* @brief write structure data to output stream
*/
virtual void oStream(std::shared_ptr<bufferstream>) const = 0;
};

View File

@ -1 +0,0 @@
add_subdirectory(qt)

View File

@ -1,11 +0,0 @@
project(${PROJECT_NAME}-qt VERSION ${PROJECT_VERSION})
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
# network and gui interface for an sACN node
add_subdirectory(sacn)
# Enttec DMX-USB-Pro serial driver
add_subdirectory(dmxwidget)

View File

@ -1,25 +0,0 @@
project(${PROJECT_NAME}-dmxwidget VERSION ${PROJECT_VERSION})
find_package(Qt6 COMPONENTS SerialPort REQUIRED)
add_library(${PROJECT_NAME} SHARED)
add_library(LCP::Qt::DmxWidget ALIAS ${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PUBLIC
dmxwidget.h
widgetmodel.h
PRIVATE
dmxwidget.cpp
widgetmodel.cpp
)
target_link_libraries(${PROJECT_NAME}
PUBLIC
ENTTEC::Pro
PRIVATE
Qt::SerialPort
)
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION})
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,197 +0,0 @@
/*
dmxwidget.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 "dmxwidget.h"
#include <QDebug>
DmxWidget::DmxWidget(QObject *parent)
: QObject{parent}
, port_(new QSerialPort(this))
{
connect(this, &DmxWidget::serialDataRead, this, &DmxWidget::parseMessageBuffer);
connect(port_, &QSerialPort::errorOccurred, this, &DmxWidget::serialErrorOccured);
connect(port_, &QSerialPort::readyRead, this, [this]() {
message_rx_buffer_.append(port_->readAll());
emit serialDataRead();
});
}
DmxWidget::~DmxWidget()
{
DmxWidget::close();
}
void DmxWidget::open()
{
auto wait_for = [this](bool &reply) {
reply = false; // reset the reply state variale
for (int i = 0; i < 10; i++) { // serial reply may not be first in the read buffer
port_->waitForReadyRead(100); // wait for the port read buffer to have content
if (reply)
break; // break the loop if a reply was received
}
return reply;
};
if (!port_->open(QIODeviceBase::ReadWrite)) // open the port
return;
ENTTEC::Widget::open(); // open the base class widget
getSerialNumber(); // request a serial number
getParameters(0); // get the metadata while we're here
if (wait_for(reply_serial)) // widget is found if a serial number reply is received
{
setModeBridge(); // widget defaults to bridge, so sync our state
emit connectedChanged(isConnected());
}
else
close();
}
void DmxWidget::close()
{
port_->close();
ENTTEC::Widget::close();
emit connectedChanged(isConnected());
}
/**
* @brief DmxWidget::parseMessageBuffer
*/
void DmxWidget::parseMessageBuffer()
{
OperatingMode other_side = USBunknown;
switch (usb_mode) {
case ENTTEC::Widget::USBdevice:
other_side = USBhost;
break;
case ENTTEC::Widget::USBhost:
other_side = USBdevice;
break;
default:
break;
}
uint16_t length;
const int fixed_length = 5; // start + label + 2xlength + end
auto fast_forward = [this](int offset = 0) {
if (message_rx_buffer_.isEmpty())
return false;
if (offset == 0)
offset = message_rx_buffer_.indexOf(ENTTEC::Pro::START_DELIMITER);
if (offset < 0)
message_rx_buffer_.clear(); // start delimiter not found
else if (offset > 0)
message_rx_buffer_.remove(0, offset); // delimiter now at start of buffer
return (message_rx_buffer_.size() >= fixed_length
&& (uint8_t)message_rx_buffer_[0] == ENTTEC::Pro::START_DELIMITER);
};
while(fast_forward())
{
// read and check the data length
length = message_rx_buffer_[2] | message_rx_buffer_[3] << 8;
if (message_rx_buffer_.size() < length + fixed_length)
return; // message in buffer is incomplete
// look for the end delimeter
if ((uint8_t)message_rx_buffer_[length + fixed_length - 1] != ENTTEC::Pro::END_DELIMITER) {
fast_forward(length + fixed_length - 1); // discard corrupted bytes
continue;
}
// make a new message data
auto msg = MessageDataFactory((MESSAGE_LABEL)message_rx_buffer_[1], other_side);
// fill it with data
std::shared_ptr<bufferstream> stream(new bufferstream(
reinterpret_cast<uint8_t*>(message_rx_buffer_.data()+fixed_length-1),
length, bufferstream::endian::little));
msg->iStream(stream);
// ship it
routeRxMessage(msg);
// discard processed bytes
fast_forward(length + fixed_length);
}
}
void DmxWidget::sendMessage(std::shared_ptr<ENTTEC::Pro::MessageData> msg) const
{
uint16_t length = msg->streamSize();
const int fixed_length = 5;
char buffer[length + fixed_length];
std::shared_ptr<bufferstream> stream(new bufferstream(reinterpret_cast<uint8_t*>(buffer),
sizeof(buffer),
bufferstream::endian::little));
*stream << ENTTEC::Pro::START_DELIMITER;
*stream << msg->label;
*stream << length;
msg->oStream(stream);
*stream << ENTTEC::Pro::END_DELIMITER;
port_->write(buffer, sizeof(buffer));
}
/**
* @brief DmxWidget::serialErrorOccured
* @param error
*/
void DmxWidget::serialErrorOccured(QSerialPort::SerialPortError error)
{
switch (error) {
case QSerialPort::WriteError:
case QSerialPort::ReadError:
case QSerialPort::ResourceError:
case QSerialPort::TimeoutError:
close();
break;
default:
break;
}
}
/**
* @brief DmxWidget::availableWidgets
* @return
*/
QList<std::shared_ptr<DmxWidget>> DmxWidget::availableWidgets()
{
QList<std::shared_ptr<DmxWidget>> wdgts;
// evaluate all system serial ports
const auto ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &port : ports) {
// try to open a widget on each port
auto widget = std::make_shared<DmxWidget>();
widget->setPort(port);
widget->open();
if (widget->isConnected())
wdgts.push_back(widget); // add valid widgets to the list
else
widget->close();
}
return wdgts;
}

View File

@ -1,86 +0,0 @@
/*
qwidget.h
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.
*/
#pragma once
#include <QFlags>
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <widget.h>
using DMX::DeviceClass;
using ENTTEC::Pro::FIRMWARE_TYPE;
using ENTTEC::Pro::MESSAGE_LABEL;
using ENTTEC::Pro::DMX_RX_MODE;
/**
* @brief The DmxWidget class
*/
class DmxWidget
: public QObject
, public ENTTEC::Widget
{
Q_OBJECT
public:
explicit DmxWidget(QObject *parent = nullptr);
virtual ~DmxWidget();
virtual void open() override;
virtual void close() override;
void setPort(const QSerialPortInfo &port) { port_->setPort(port); emit portNameChanged(); }
void setPortName(const QString &name) { port_->setPortName(name); emit portNameChanged(); }
QString portName() const { return port_->portName(); }
QSerialPortInfo portInfo() const { return QSerialPortInfo(*port_); }
DeviceClass deviceClass() const { return device_class_; }
Q_ENUM(DeviceClass)
Q_ENUM(FIRMWARE_TYPE)
Q_ENUM(MESSAGE_LABEL)
Q_ENUM(DMX_RX_MODE)
Q_DECLARE_FLAGS(RxErrors, ENTTEC::Pro::RX_ERROR)
Q_PROPERTY(bool isConnected READ isConnected NOTIFY connectedChanged)
Q_PROPERTY(QString portName READ portName NOTIFY portNameChanged)
static QList<std::shared_ptr<DmxWidget>> availableWidgets();
signals:
void connectedChanged(bool);
void portNameChanged();
void serialDataRead();
protected:
virtual void sendMessage(std::shared_ptr<ENTTEC::Pro::MessageData>) const override;
private:
QSerialPort *port_;
QByteArray message_rx_buffer_;
private slots:
void parseMessageBuffer();
void serialErrorOccured(QSerialPort::SerialPortError);
};
Q_DECLARE_OPERATORS_FOR_FLAGS(DmxWidget::RxErrors)

View File

@ -1,162 +0,0 @@
#include <QMetaEnum>
#include "widgetmodel.h"
WidgetModel::WidgetModel(QObject *parent)
: QAbstractItemModel(parent)
{
rescanPorts();
}
WidgetModel::~WidgetModel() {
for(auto &wdgt: widgets_)
disconnect(wdgt.get(), nullptr, this, nullptr);
}
QVariant WidgetModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
switch (static_cast<Column>(section)) {
case PortName:
return tr("Port");
case Manufacturer:
return tr("Manufacturer");
case Description:
return tr("Description");
case SerialNumber:
return tr("Serial");
case FirmwareVersion:
return tr("Firmware");
case OperatingMode:
return tr("Mode");
default:
return QVariant();
}
}
return QVariant();
}
QModelIndex WidgetModel::index(int row, int column, const QModelIndex &parent) const
{
if (parent.isValid() || row >= widgets_.size())
return QModelIndex();
return createIndex(row, column, widgets_.at(row).get());
}
QModelIndex WidgetModel::parent(const QModelIndex &index) const
{
Q_UNUSED(index)
return QModelIndex();
}
int WidgetModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return widgets_.size();
}
int WidgetModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return QMetaEnum::fromType<Column>().keyCount();
}
QVariant WidgetModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return QVariant();
switch (role) {
case Qt::DisplayRole:
{
auto wdgt = widgets_.at(index.row());
auto info = wdgt->portInfo();
switch (static_cast<Column>(index.column()))
{
case PortName:
return info.portName();
case Manufacturer:
return info.manufacturer();
case Description:
return info.description();
case SerialNumber:
return wdgt->serialNumber();
case FirmwareVersion:
return QString::number(wdgt->firmwareVersion() >> 8)
+ "."
+ QString::number(wdgt->firmwareVersion() & 0xff);
case OperatingMode:
switch (wdgt->deviceClass()) {
case DMX::CONTROLLER:
return tr("Controller");
case DMX::RECEIVER:
return tr("Receiver");
case DMX::RESPONDER:
return tr("Responder");
default:
return QVariant();
}
default:
return QVariant();
}
}
case Qt::TextAlignmentRole:
switch (static_cast<Column>(index.column())) {
case SerialNumber:
case FirmwareVersion:
return Qt::AlignCenter;
default:
return QVariant();
}
case Qt::EditRole:
switch (static_cast<Column>(index.column())) {
case OperatingMode:
/// \todo Create a combobox delegate to change the DMX Widget operating mode.
default:
return QVariant::fromValue(widgets_.at(index.row()));
}
default:
return QVariant();
}
}
/**
* @brief WidgetModel::rescanPorts
*/
void WidgetModel::rescanPorts()
{
// remove disconnected widgets
for(qsizetype i = 0; i < widgets_.size();)
if (!widgets_.at(i)->isConnected())
{
beginRemoveRows(QModelIndex(), i, i+1);
disconnect(widgets_.at(i).get(), nullptr, this, nullptr);
widgets_.remove(i);
endRemoveRows();
}
else
i++;
// look for new widgets
auto newWidgets = DmxWidget::availableWidgets();
if (!newWidgets.isEmpty())
{
beginInsertRows(QModelIndex(), widgets_.size(), widgets_.size()+newWidgets.size()-1);
widgets_.append(newWidgets);
endInsertRows();
for(auto &wdgt: newWidgets)
connect(wdgt.get(), &DmxWidget::connectedChanged, this, [this](){
// refresh the whole view. Could be optimized by looking for this widget in the rows.
emit dataChanged(createIndex(0,0), createIndex(rowCount(),columnCount()));
});
}
}

View File

@ -1,44 +0,0 @@
#pragma once
#include "dmxwidget.h"
#include <QAbstractItemModel>
class WidgetModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit WidgetModel(QObject *parent = nullptr);
virtual ~WidgetModel();
// Header:
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
// Basic functionality:
QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/// @brief The Column enum
enum Column {
PortName,
Manufacturer,
Description,
SerialNumber,
FirmwareVersion,
OperatingMode,
};
Q_ENUM(Column)
public slots:
void rescanPorts();
private:
QList<std::shared_ptr<DmxWidget>> widgets_;
};

View File

@ -1,39 +0,0 @@
project(${PROJECT_NAME}-sacn VERSION ${PROJECT_VERSION})
find_package(Qt6 COMPONENTS Gui Network Widgets REQUIRED)
add_library(${PROJECT_NAME} SHARED)
add_library(LCP::Qt::sACN ALIAS ${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PUBLIC
multiversemodel.h
qsacnnode.h
qsacnuniverse.h
universemodel.h
universestatuswidget.h
universewindow.h
PRIVATE
multiverseitem.h
multiverseitem.cpp
multiversemodel.cpp
qsacn_global.h
qsacnnode.cpp
qsacnuniverse.cpp
universemodel.cpp
universestatuswidget.cpp
universewindow.cpp
universewindow.ui
)
target_link_libraries(${PROJECT_NAME}
PUBLIC
LCP::sACN
PRIVATE
Qt::Gui
Qt::Network
Qt::Widgets
)
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION})
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,335 +0,0 @@
#include "multiverseitem.h"
#include "multiversemodel.h"
#include "universestatuswidget.h"
#include <QFont>
#include <QMetaEnum>
#include <QMetaType>
#include <QObject>
/**
* @brief MultiverseItem::MultiverseItem
* @param universe
* @param parent
*/
MultiverseItem::MultiverseItem(MultiverseItem* parent,
QSacnUniverse* universe,
QSacnDiscoveredUniverse *discovery)
: universe_(universe)
, discovery_(discovery)
, parentItem_(parent)
, override_data_(QVariant())
, mIndicator(nullptr)
{
createChildren();
if(parent)
parent->appendChild(this);
if (universe)
mIndicator = new UniverseStatusWidget(nullptr, universe->get());
}
/**
* @brief MultiverseItem::~MultiverseItem
*/
MultiverseItem::~MultiverseItem()
{
delete mIndicator;
qDeleteAll(childItems_);
}
/**
* @brief MultiverseItem::appendChild
* @param item
*/
void MultiverseItem::appendChild(MultiverseItem* item)
{
childItems_.append(item);
}
/**
* @brief MultiverseItem::removeChild
* @param child
*/
void MultiverseItem::removeChild(MultiverseItem* child)
{
for (int i = 0; i < childItems_.size(); ++i)
if (childItems_.at(i) == child)
childItems_.remove(i);
}
/**
* @brief MultiverseItem::removeChildren
*/
void MultiverseItem::removeChildren()
{
qDeleteAll(childItems_);
childItems_.clear();
}
/**
* @brief MultiverseItem::setOverrideData
* @param data
*/
void MultiverseItem::setOverrideData(QVariant data)
{
override_data_ = data;
}
/**
* @brief MultiverseItem::createChildren
*/
void MultiverseItem::createChildren()
{
if (!universe_)
return;
foreach (const auto & metadata, universe_->sources())
{
auto universe = universe_->sourceUniverse(metadata);
if (universe)
new MultiverseItem(this, universe);
}
}
/**
* @brief MultiverseItem::child
* @param row
* @return
*/
MultiverseItem* MultiverseItem::child(int row) const
{
return (row < 0 || row >= childItems_.size()) ? nullptr : childItems_.at(row);
}
/**
* @brief MultiverseItem::childCount
* @return
*/
int MultiverseItem::childCount() const
{
return childItems_.count();
}
/**
* @brief MultiverseItem::childRow
* @param universe
* @return
*/
int MultiverseItem::childRow(int column, const QVariant &data) const
{
for (int row = 0; row < childItems_.size(); row++)
{
auto childData = childItems_.at(row)->data(column, Qt::EditRole);
if (data == childData)
return row;
}
return -1;
}
/**
* @brief MultiverseItem::row
* @return
*/
int MultiverseItem::row() const
{
if (parentItem_)
return parentItem_->childItems_.indexOf(const_cast<MultiverseItem*>(this));
return 0;
}
/**
* @brief MultiverseItem::data
* @param column
* @return
*/
QVariant MultiverseItem::data(int column, int role) const
{
switch (role) {
case Qt::DisplayRole:
{
if (!universe_ && !discovery_)
{
if (column)
return QVariant();
return QString("%1 (%2)").arg(
override_data_.toString(),
QString::number(childCount()));
}
if (universe_)
{
switch (static_cast<MultiverseModel::Column>(column))
{
case MultiverseModel::Universe:
return universe_->number();
case MultiverseModel::Status:
return mIndicator? mIndicator->toolTip() : QVariant();
case MultiverseModel::Priority:
{
auto priority = universe_->priority();
return priority <= 200 ? priority : QVariant();
}
case MultiverseModel::Channels:
{
auto active = universe_->activeSlots();
return active ? active - 1 : QVariant();
}
case MultiverseModel::SourceName:
return universe_->sourceName();
default:
return QVariant();
}
}
if (discovery_)
{
switch (static_cast<MultiverseModel::Column>(column))
{
case MultiverseModel::Universe:
return discovery_->universe;
case MultiverseModel::SourceName:
return QString::fromUtf8(discovery_->description.c_str());
default:
return QVariant();
}
}
}
case Qt::FontRole:
{
QFont font;
if (!universe_ && !discovery_)
{
font.setBold(true);
return font;
}
if (universe_ && !universe_->isEditable())
{
switch (universe_->status())
{
case sACN::Universe::DMX_NULL:
font.setItalic(true);
return font;
// case sACN::Universe::DMX_ACTIVE:
// font.setWeight(QFont::Medium);
// return font;
case sACN::Universe::DMX_LOST:
font.setWeight(QFont::Light);
return font;
case sACN::Universe::sACN_TERMINATED:
font.setWeight(QFont::Light);
font.setItalic(true);
return font;
default:
return QVariant();
}
}
return QVariant();
}
case Qt::EditRole:
if (universe_)
{
if (!universe_->isEditable())
return QVariant::fromValue(universe_);
switch (static_cast<MultiverseModel::Column>(column))
{
case MultiverseModel::Priority:
return universe_->priority();
default:
return QVariant::fromValue(universe_);
}
}
if (discovery_)
return QVariant::fromValue(discovery_);
return data(column, Qt::DisplayRole);
default:
return QVariant();
}
}
/**
* @brief MultiverseItem::setData
* @param column
* @param value
* @param role
* @return
*/
bool MultiverseItem::setData(int column, const QVariant &value, int role)
{
if (!universe_->isEditable())
return false;
if (role != Qt::EditRole)
return false;
switch (column)
{
case MultiverseModel::Priority:
{
if (!value.canConvert<uint>())
return false;
auto p = value.toUInt();
if (p < 0 || p > 200)
return false;
universe_->setPriority(p);
return true;
}
default:
return false;
}
}
/**
* @brief MultiverseItem::flags
* @param column
* @return
*/
Qt::ItemFlags MultiverseItem::flags(int column, Qt::ItemFlags base) const
{
if (!universe_ && !discovery_)
{
if (column)
return Qt::NoItemFlags;
return base & ~Qt::ItemIsSelectable;
}
switch (static_cast<MultiverseModel::Column>(column))
{
case MultiverseModel::Universe:
case MultiverseModel::Status:
case MultiverseModel::Channels:
case MultiverseModel::SourceName:
return base;
case MultiverseModel::Priority:
if (universe_ && universe_->isEditable())
return base | Qt::ItemIsEditable;
else
return base;
default:
return Qt::NoItemFlags;
}
}
/**
* @brief MultiverseItem::parentItem
* @return
*/
MultiverseItem * MultiverseItem::parentItem() const
{
return parentItem_;
}

View File

@ -1,46 +0,0 @@
#pragma once
#include "extended.h"
#include <QVariant>
#include <QVector>
class QSacnUniverse; // forward declare
using QSacnDiscoveredUniverse = sACN::EXTENDED::DISCOVERY::discoveredUniverse;
Q_DECLARE_METATYPE(QSacnDiscoveredUniverse*)
/**
* @brief The MultiverseItem class
*/
class MultiverseItem
{
public:
explicit MultiverseItem(MultiverseItem* parentItem = nullptr,
QSacnUniverse* universe = nullptr,
QSacnDiscoveredUniverse* discovery = nullptr);
virtual ~MultiverseItem();
void appendChild(MultiverseItem* child);
void removeChild(MultiverseItem* child);
void removeChildren();
void setOverrideData(QVariant data);
void createChildren();
MultiverseItem * child(int row) const;
int childCount() const;
int childRow(int column, const QVariant &data) const;
QVariant data(int column, int role = Qt::DisplayRole) const;
bool setData(int column, const QVariant &value, int role = Qt::EditRole);
Qt::ItemFlags flags(int column, Qt::ItemFlags base) const;
int row() const;
MultiverseItem * parentItem() const;
private:
QVector<MultiverseItem*> childItems_;
QSacnUniverse* universe_;
QSacnDiscoveredUniverse* discovery_;
MultiverseItem* parentItem_;
QVariant override_data_;
QWidget *mIndicator;
};

View File

@ -1,321 +0,0 @@
#include "multiverseitem.h"
#include "multiversemodel.h"
#include "qsacnnode.h"
#include <QMetaEnum>
/**
* @brief MultiverseModel::MultiverseModel
* @param parent
* @param node
*/
MultiverseModel::MultiverseModel(QObject *parent, QSacnNode *node)
: QAbstractItemModel(parent)
, node_(node)
, rootItem_(new MultiverseItem())
{
// create items for the category headers
QMetaEnum categories = QMetaEnum::fromType<Catagory>();
for (int itr = 0; itr < categories.keyCount(); itr++)
{
auto header = new MultiverseItem(rootItem_);
header->setOverrideData(QString(categories.key(itr)));
auto idx = MultiverseModel::index(itr, 0, QModelIndex());
categoryIndexes.insert(static_cast<Catagory>(categories.value(itr)), idx);
}
// connect node changes
connect(node, &QSacnNode::discoveryUpdates,
this, &MultiverseModel::doDiscovery);
connect(node, &QSacnNode::subscribing,
this, [this](QSacnUniverse *universe){
insert_(categoryIndexes.value(Receiver), universe);
});
connect(node, &QSacnNode::unsubscribing,
this, [this](QSacnUniverse *universe){
remove_(categoryIndexes.value(Receiver), universe);
});
connect(node, &QSacnNode::creating,
this, [this](QSacnUniverse *universe){
insert_(categoryIndexes.value(Source), universe);
});
connect(node, &QSacnNode::terminating,
this, [this](QSacnUniverse *universe){
remove_(categoryIndexes.value(Source), universe);
});
}
/**
* @brief MultiverseModel::~MultiverseModel
*/
MultiverseModel::~MultiverseModel()
{
delete rootItem_;
}
/**
* @brief MultiverseModel::getItem
* @param index
* @return
*/
MultiverseItem * MultiverseModel::getItem(const QModelIndex &index) const
{
if (index.isValid())
{
auto item = static_cast<MultiverseItem*>(index.internalPointer());
if (item)
return item;
}
return rootItem_;
}
/**
* @brief MultiverseModel::headerData
* @param section
* @param orientation
* @param role
* @return
*/
QVariant MultiverseModel::headerData(int section, Qt::Orientation orientation,
int role) const
{
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
switch (static_cast<Column>(section)) {
case Universe:
return tr("Universe");
case Status:
return tr("Status");
case Priority:
return tr("Priority");
case Channels:
return tr("Channels");
case SourceName:
return tr("Source Name");
}
}
return QVariant();
}
/**
* @brief MultiverseModel::index
* @param row
* @param column
* @param parent
* @return
*/
QModelIndex MultiverseModel::index(int row, int column,
const QModelIndex &parent) const
{
if (parent.isValid() && parent.column() != 0)
return QModelIndex();
MultiverseItem *parentItem = getItem(parent);
if (!parentItem)
return QModelIndex();
auto childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem);
return QModelIndex();
}
/**
* @brief MultiverseModel::parent
* @param index
* @return
*/
QModelIndex MultiverseModel::parent(const QModelIndex &index) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid |
CheckIndexOption::DoNotUseParent))
return QModelIndex();
auto childItem = getItem(index);
auto parentItem = childItem ? childItem->parentItem() : nullptr;
if (parentItem == rootItem_ || !parentItem)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem);
}
/**
* @brief MultiverseModel::rowCount
* @param parent
* @return
*/
int MultiverseModel::rowCount(const QModelIndex &parent) const
{
auto parentItem = getItem(parent);
return parentItem ? parentItem->childCount() : 0;
}
/**
* @brief MultiverseModel::columnCount
* @param parent
* @return
*/
int MultiverseModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return QMetaEnum::fromType<Column>().keyCount();
}
/**
* @brief MultiverseModel::data
* @param index
* @param role
* @return
*/
QVariant MultiverseModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return QVariant();
return getItem(index)->data(index.column(), role);
}
/**
* @brief MultiverseModel::setData
* @param index
* @param value
* @param role
* @return
*/
bool MultiverseModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return false;
if (getItem(index)->setData(index.column(), value, role))
{
emit dataChanged(index, index, QVector<int>() << role);
return true;
}
return false;
}
/**
* @brief MultiverseModel::flags
* @param index
* @return
*/
Qt::ItemFlags MultiverseModel::flags(const QModelIndex &index) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return Qt::NoItemFlags;
auto item = getItem(index);
if (!item)
return Qt::NoItemFlags;
return item->flags(index.column(), QAbstractItemModel::flags(index));
}
/**
* @brief MultiverseModel::insert
* @param universe
* @param parent
* @return
*/
void MultiverseModel::insert_(const QModelIndex &parent,
QSacnUniverse* universe,
QSacnDiscoveredUniverse *discovery)
{
auto item = getItem(parent);
beginInsertRows(parent, item->childCount(), item->childCount());
auto child = new MultiverseItem(item, universe, discovery);
endInsertRows();
auto index = QPersistentModelIndex(createIndex(item->childCount() - 1, 0,
child));
auto refreshRow = [this] (QPersistentModelIndex index) {
auto begin = index.sibling(index.row(), 0);
auto end = begin.siblingAtColumn(columnCount() - 1);
emit dataChanged(begin, end);
};
connect(universe, &QSacnUniverse::dataChanged,
this, [refreshRow, index](){ refreshRow(index); });
connect(universe, &QSacnUniverse::statusChanged,
this, [refreshRow, index](){ refreshRow(index); });
connect(universe, &QSacnUniverse::sourceListChanged,
this, [this, universe, child, index, refreshRow]() {
// remove the old children
beginRemoveRows(index, 0, child->childCount() - 1);
child->removeChildren();
endRemoveRows();
// add the new children
beginInsertRows(index, 0, universe->sources().size() - 1);
child->createChildren();
endInsertRows();
// refresh row of new child when data arrives
for (int i = 0; i < child->childCount(); i++)
{
auto grandchild = child->child(i);
auto index = QPersistentModelIndex(createIndex(i, 0, grandchild));
auto data = grandchild->data(Column::Universe, Qt::EditRole);
if (data.metaType().id() != qMetaTypeId<QSacnUniverse*>())
return;
auto universe = data.value<QSacnUniverse*>();
connect(universe, &QSacnUniverse::dataChanged,
this, [refreshRow, index](){ refreshRow(index); });
connect(universe, &QSacnUniverse::statusChanged,
this, [refreshRow, index](){ refreshRow(index); });
}
});
}
/**
* @brief MultiverseModel::remove
* @param parent
* @param data
*/
void MultiverseModel::remove_(const QModelIndex &parent,
const QSacnUniverse *universe)
{
auto item = static_cast<MultiverseItem*>(parent.internalPointer());
auto row = item->childRow(Column::Universe, QVariant::fromValue(universe));
if (row < 0)
return;
auto child = item->child(row);
if (!child)
return;
beginRemoveRows(parent, row, row);
item->removeChild(child);
endRemoveRows();
}
void MultiverseModel::doDiscovery()
{
auto parentIndex = categoryIndexes.value(MultiverseModel::Discovery);
auto parent = static_cast<MultiverseItem*>(parentIndex.internalPointer());
beginRemoveRows(parentIndex, 0, parent->childCount() - 1);
parent->removeChildren();
endRemoveRows();
beginInsertRows(parentIndex, 0, node_->discovered.size() - 1);
for (auto& discovery : node_->discovered)
new MultiverseItem(parent, nullptr, discovery.get());
endInsertRows();
}

View File

@ -1,68 +0,0 @@
#pragma once
#include <QAbstractItemModel>
#include "multiverseitem.h"
#include "qsacn_global.h"
#include "qsacnuniverse.h"
class QSacnNode; // forward declare node class
/**
* @brief The MultiverseModel class
*/
class QT_EXPORT MultiverseModel
: public QAbstractItemModel
{
Q_OBJECT
public:
explicit MultiverseModel(QObject *parent = nullptr,
QSacnNode *node = nullptr);
virtual ~MultiverseModel();
// QAbstractItemModel overrides:
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index,
int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
/// @brief The Catagory enum
enum Catagory {
Discovery,
Receiver,
Source
};
Q_ENUM(Catagory)
/// @brief The Column enum
enum Column {
Universe,
Status,
Priority,
Channels,
SourceName
};
Q_ENUM(Column)
private slots:
void doDiscovery();
private:
MultiverseItem * getItem(const QModelIndex &index) const;
QMap<MultiverseModel::Catagory, QModelIndex> categoryIndexes;
QSacnNode *node_;
MultiverseItem *rootItem_;
void insert_(const QModelIndex &parent,
QSacnUniverse *universe = nullptr,
QSacnDiscoveredUniverse *discovery = nullptr);
void remove_(const QModelIndex &parent,
const QSacnUniverse *universe);
};

View File

@ -1,9 +0,0 @@
#pragma once
#include <QtCore/qglobal.h>
#if defined(QsACN_LIBRARY)
# define QT_EXPORT Q_DECL_EXPORT
#else
# define QT_EXPORT Q_DECL_IMPORT
#endif

View File

@ -1,302 +0,0 @@
#include "qsacnnode.h"
#include <QDebug>
#include <QMetaEnum>
#include <QNetworkDatagram>
#include <QNetworkInterface>
/**
* @brief QSacnNode::QSacnNode
* @param parent
* @param cid
* @param fctn
*/
QSacnNode::QSacnNode(QObject *parent, QUuid cid, QString fctn, bool ipv4, bool ipv6)
: Component(UUID::uuid(cid.toString().toStdString()), fctn.toStdString(), ipv4, ipv6)
, QObject(parent)
, rx_socket_(new QUdpSocket(this))
, tx_socket_(new QUdpSocket(this))
{
rx_socket_->bind(QHostAddress::AnyIPv4, sACN::ACN_SDT_MULTICAST_PORT);
tx_socket_->bind(QHostAddress::AnyIPv4);
connect(rx_socket_, &QUdpSocket::readyRead,
this, &QSacnNode::udpReceive);
sACN::Receiver::onDiscovered([this](){ emit discoveryUpdates(); });
}
/**
* @brief QSacnNode::~QSacnNode
*/
QSacnNode::~QSacnNode()
{
if (Receiver::discoveryEnabled())
QSacnNode::unsubscribe(sACN::E131_DISCOVERY_UNIVERSE);
foreach (const auto num, rx_universes.keys())
QSacnNode::unsubscribe(num);
foreach (const auto num, tx_universes.keys())
QSacnNode::terminate(num);
}
void QSacnNode::setIPv4(const bool enable)
{
Node::setIPv4(enable);
QList<QHostAddress> groups;
foreach (const auto & universe, rx_universes)
groups.append(IPv4MulticastAddress(universe->number()));
if (discoveryEnabled())
groups.append(IPv4MulticastAddress(sACN::E131_DISCOVERY_UNIVERSE));
if (enable)
{
qDebug() << "Enabling IPv4";
foreach (const auto & ip, groups)
{
qDebug() << "Joining IGMP Group" << ip.toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->joinMulticastGroup(ip, iface);
}
}
else
{
qDebug() << "Disabling IPv4";
foreach (const auto & ip, groups)
{
qDebug() << "Leaving IGMP Group" << ip.toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->leaveMulticastGroup(ip, iface);
}
}
}
void QSacnNode::setIPv6(const bool enable)
{
Node::setIPv6(enable);
QList<QHostAddress> groups;
foreach (const auto & universe, rx_universes)
groups.append(IPv6MulticastAddress(universe->number()));
if (discoveryEnabled())
groups.append(IPv6MulticastAddress(sACN::E131_DISCOVERY_UNIVERSE));
if (enable)
{
qDebug() << "Enabling IPv6";
foreach (const auto & ip, groups)
{
qDebug() << "Joining MLD Group" << ip.toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->joinMulticastGroup(ip, iface);
}
}
else
{
qDebug() << "Disabling IPv6";
foreach (const auto & ip, groups)
{
qDebug() << "Leaving MLD Group" << ip.toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->leaveMulticastGroup(ip, iface);
}
}
}
/**
* @brief QSacnNode::subscribe
* @param num
*/
void QSacnNode::subscribe(const uint16_t num)
{
if (Receiver::universe(num)) // already subscribed
return;
switch (num) {
case sACN::E131_DISCOVERY_UNIVERSE:
{
if (discoveryEnabled())
return;
qDebug() << "Enabling sACN Discovery";
}
break;
default:
{
qDebug() << "Subscribing to universe" << QString::number(num);
Receiver::subscribe(num);
rx_universes.emplace(num, new QSacnUniverse(this, Receiver::universe(num)));
emit subscribing(rx_universes.value(num));
}
break;
}
if (enable_IPv4)
{
qDebug() << "Joining IGMP Group" << IPv4MulticastAddress(num).toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->joinMulticastGroup(IPv4MulticastAddress(num), iface);
}
if (enable_IPv6)
{
qDebug() << "Joining MLD Group" << IPv6MulticastAddress(num).toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->joinMulticastGroup(IPv6MulticastAddress(num), iface);
}
}
/**
* @brief QSacnNode::unsubscribe
* @param num
*/
void QSacnNode::unsubscribe(const uint16_t num)
{
switch (num) {
case sACN::E131_DISCOVERY_UNIVERSE:
{
if (!discoveryEnabled())
return;
qDebug() << "Disabling sACN Discovery";
}
break;
default:
{
if (Receiver::universe(num)) {
qDebug() << "Unsubscribing from universe " << QString::number(num);
emit unsubscribing(rx_universes.value(num));
rx_universes.take(num)->deleteLater();
Receiver::unsubscribe(num);
}
}
break;
}
if (enable_IPv4)
{
qDebug() << "Leaving IGMP Group" << IPv4MulticastAddress(num).toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->leaveMulticastGroup(IPv4MulticastAddress(num), iface);
}
if (enable_IPv6)
{
qDebug() << "Leaving MLD Group" << IPv6MulticastAddress(num).toString();
for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
rx_socket_->leaveMulticastGroup(IPv6MulticastAddress(num), iface);
}
}
/**
* @brief QSacnNode::create
* @param num
*/
void QSacnNode::create(const uint16_t num)
{
if (Source::universe(num)) // already created
return;
qDebug() << "Creating universe " << QString::number(num);
Source::create(num);
tx_universes.emplace(num, new QSacnUniverse(this, Source::universe(num)));
emit creating(tx_universes.value(num));
}
/**
* @brief QSacnNode::terminate
* @param num
*/
void QSacnNode::terminate(const uint16_t num)
{
if (Source::universe(num)) {
qDebug() << "Terminating universe " << QString::number(num);
Source::terminate(num);
emit terminating(tx_universes.value(num));
delete tx_universes.take(num);
}
}
/**
* @brief QSacnNode::UdpPayloadReceiver
*/
void QSacnNode::udpReceive()
{
while (rx_socket_->hasPendingDatagrams())
{
auto datagram = rx_socket_->receiveDatagram();
// expecting IANA registered Session Data Transport traffic
if (datagram.destinationPort() != sACN::ACN_SDT_MULTICAST_PORT)
return;
// only receive over configured protocols
switch (datagram.senderAddress().protocol()) {
case QUdpSocket::IPv4Protocol:
if (!enable_IPv4)
return;
break;
case QUdpSocket::IPv6Protocol:
if (!enable_IPv6)
return;
break;
default:
return;
}
// wrap a PDU i/o stream around the QNetworkDatagram data buffer
auto data = datagram.data();
auto stream = std::make_shared<bufferstream>(
reinterpret_cast<uint8_t*>(data.data()), data.length());
UdpPayloadReceiver(stream);
}
}
/**
* @brief QSacnNode::sendUDP
* @param stream
* @param ip
*/
void QSacnNode::sendUDP(const ACN::PDU::Stream stream,
const ACN::SDT::UDP::ipAddress& ip) const
{
QHostAddress addr;
switch (ip.type) {
case ACN::SDT::SDT_ADDR_IPV4:
if (!enable_IPv4)
return;
addr = QHostAddress(ip.address.ipv4.value);
break;
case ACN::SDT::SDT_ADDR_IPV6:
if (!enable_IPv6)
return;
addr = QHostAddress(ip.address.ipv6.bytes);
break;
default:
return;
}
tx_socket_->writeDatagram(reinterpret_cast<char*>(stream->base()), stream->size(), addr, ip.port);
}
/**
* @brief QSacnNode::sendTCP
* @param stream
* @param ip
*/
void QSacnNode::sendTCP(const ACN::PDU::Stream stream,
const ACN::SDT::UDP::ipAddress& ip) const
{
Q_UNUSED(stream)
Q_UNUSED(ip)
qDebug() << "sACN uses UDP only. Stop trying to send TCP!";
}

View File

@ -1,69 +0,0 @@
#pragma once
#include "qsacn_global.h"
#include "qsacnuniverse.h"
#include "node.h"
#include <QHash>
#include <QHostAddress>
#include <QUdpSocket>
#include <QUuid>
/**
* @brief The QSacnNode class
*/
class QT_EXPORT QSacnNode
: public QObject
, public sACN::Node
{
Q_OBJECT
public:
explicit QSacnNode(QObject *parent = nullptr,
QUuid = QUuid::createUuid(), QString fctn = "OpenLCP QSacnNode",
bool ipv4 = true, bool ipv6 = true);
virtual ~QSacnNode();
// sACN::RLP::Component
void sendUDP(const ACN::PDU::Stream, const ACN::SDT::UDP::ipAddress&) const override;
void sendTCP(const ACN::PDU::Stream, const ACN::SDT::UDP::ipAddress&) const override;
public slots:
void setIPv4(const bool) override;
void setIPv6(const bool) override;
void subscribe(const uint16_t) override;
void unsubscribe(const uint16_t) override;
void create(const uint16_t) override;
void terminate(const uint16_t) override;
signals:
void discoveryUpdates();
void subscribing(QSacnUniverse*);
void unsubscribing(QSacnUniverse*);
void creating(QSacnUniverse*);
void terminating(QSacnUniverse*);
private:
void udpReceive();
QUdpSocket * rx_socket_;
QUdpSocket * tx_socket_;
QHash<uint16_t, QSacnUniverse*> rx_universes;
QHash<uint16_t, QSacnUniverse*> tx_universes;
public:
/// \cite sACN 9.3.1 Allocation of IPv4 Multicast Addresses
///
/// Multicast addresses are from the IPv4 Local Scope.
static QHostAddress IPv4MulticastAddress(const uint16_t universe) {
sACN::ipAddress ip = sACN::IPv4MulticastAddress(universe);
return QHostAddress(ip.address.ipv4.value);
};
/// \cite sACN 9.3.2 Allocation of IPv6 Multicast Addresses
static QHostAddress IPv6MulticastAddress(const uint16_t universe) {
sACN::ipAddress ip = sACN::IPv6MulticastAddress(universe);
return QHostAddress(ip.address.ipv6.bytes);
};
};

View File

@ -1,312 +0,0 @@
#include "qsacnuniverse.h"
/**
* @brief QSacnUniverse::QSacnUniverse
* @param parent
* @param universe
*/
QSacnUniverse::QSacnUniverse(QObject *parent, std::shared_ptr<sACN::Universe> universe)
: QObject(parent)
, universe_(universe)
, status_watchdog_(new QTimer(this))
{
if (!universe)
return;
data_change_token = universe_->onDataChange([this](DMX::Universe*){emit dataChanged();});
status_change_token = universe_->onStatusChange([this](DMX::Universe*){emit statusChanged();});
if (universe->arbitrator())
list_change_token = universe_->arbitrator()->onSourceListChange([this](DMX::Universe*){syncSources();});
// set the status watchdog to update the status if the universe
// isn't showing frequent activity
status_watchdog_->callOnTimeout([this](){universe_->status();});
status_watchdog_->setInterval(DMX::E111_DATA_LOSS_TIMEOUT);
status_watchdog_->start();
connect(this, &QSacnUniverse::dataChanged,
this, [this](){status_watchdog_->start();});
connect(this, &QSacnUniverse::statusChanged,
this, [this](){status_watchdog_->start();});
};
/**
* @brief QSacnUniverse::~QSacnUniverse
*/
QSacnUniverse::~QSacnUniverse()
{
// delete QSacnUniverse sources
qDeleteAll(sources_);
}
/**
* @brief QSacnUniverse::description
* @return
*/
const QString QSacnUniverse::sourceName() const
{
auto metadata = universe_->metadata();
return metadata ? QString::fromUtf8(metadata->source_name.c_str()) : QString();
}
/**
* @brief QSacnUniverse::number
* @return
*/
uint16_t QSacnUniverse::number() const
{
auto metadata = universe_->metadata();
return metadata ? metadata->universe : 0;
}
/**
* @brief QSacnUniverse::priority
* @return
*/
uint8_t QSacnUniverse::priority() const
{
auto metadata = universe_->metadata();
return metadata ? metadata->priority : 0;
}
/**
* @brief QSacnUniverse::rxRate
* @return
*/
double QSacnUniverse::rxRate() const
{
return universe_ ? universe_->rxRate() : -1;
}
/**
* @brief QSacnUniverse::status
* @return
*/
uint8_t QSacnUniverse::status() const
{
return universe_ ? universe_->status() : sACN::Universe::DMX_NULL;
}
/**
* @brief QSacnUniverse::slot
* @param slot
* @return
*/
uint8_t QSacnUniverse::slot(const uint16_t slot) const
{
return universe_ ? universe_->slot(slot) : 0;
}
/**
* @brief QSacnUniverse::isEditable
* @return
*/
bool QSacnUniverse::isEditable() const
{
return universe_ ? universe_->isEditable() : false;
}
/**
* @brief QSacnUniverse::hasSources
* @return
*/
bool QSacnUniverse::hasSources() const
{
return universe_ ? universe_->hasSources() : false;
}
/**
* @brief QSacnUniverse::sources
* @return
*/
const QList<sACN::DATA::data_header> QSacnUniverse::sources() const
{
QList<sACN::DATA::data_header> ret;
if (!universe_)
return ret;
for (const auto & src : universe_->sources())
ret.append(src);
return ret;
}
/**
* @brief QSacnUniverse::sourceUniverse
* @param metadata
* @return
*/
QSacnUniverse* QSacnUniverse::sourceUniverse(const sACN::DATA::data_header& metadata) const
{
if (!universe_)
return nullptr;
if (!sources_.count(metadata))
return nullptr;
return sources_.value(metadata);
}
/**
* @brief QSacnUniverse::activeSlots
* @return
*/
uint16_t QSacnUniverse::activeSlots() const
{
return universe_ ? universe_->activeSlots() : 0;
}
/**
* @brief QSacnUniverse::getHoldLastLook
* @return
*/
bool QSacnUniverse::getHoldLastLook() const
{
return universe_ && universe_->arbitrator()
? universe_->arbitrator()->getHoldLastLook()
: false;
}
/**
* @brief QSacnUniverse::getMergeMode
* @return
*/
sACN::UniverseArbitrator::MergeMode QSacnUniverse::getMergeMode() const
{
return universe_ && universe_->arbitrator()
? universe_->arbitrator()->getMergeMode()
: sACN::UniverseArbitrator::MERGE_OTHER;
}
/**
* @brief QSacnUniverse::get
* @return
*/
std::shared_ptr<sACN::Universe> QSacnUniverse::get() const
{
return universe_;
}
/**
* @brief QSacnUniverse::setHoldLastLook
* @param state
*/
void QSacnUniverse::setHoldLastLook(bool state)
{
if (isEditable() && universe_ && universe_->arbitrator())
universe_->arbitrator()->setHoldLastLook(state);
}
/**
* @brief QSacnUniverse::setMergeMode
* @param mode
*/
void QSacnUniverse::setMergeMode(const sACN::UniverseArbitrator::MergeMode mode)
{
if (isEditable() && universe_ && universe_->arbitrator())
universe_->arbitrator()->setMergeMode(mode);
}
/**
* @brief QSacnUniverse::setPriority
* @param p
*/
void QSacnUniverse::setPriority(uint8_t p)
{
if (!isEditable())
return;
universe_->metadata()->priority = p;
emit dataChanged();
}
/**
* @brief QSacnUniverse::setSyncAddress
* @param a
*/
void QSacnUniverse::setSyncAddress(uint16_t a)
{
if (!isEditable())
return;
universe_->metadata()->sync_address = a;
emit dataChanged();
}
/**
* @brief QSacnUniverse::setValue
* @param addr
* @param level
*/
void QSacnUniverse::setValue (const uint16_t addr, const uint8_t level)
{
setValue(addr, 1, &level);
}
/**
* @brief QSacnUniverse::setValue
* @param addr
* @param size
* @param profile
*/
void QSacnUniverse::setValue (const uint16_t addr, const uint16_t size,
const uint8_t* profile)
{
if (!isEditable())
return;
universe_->sACN::Universe::setValue(addr, size, profile);
emit dataChanged();
}
/**
* @brief QSacnUniverse::syncSources
*/
void QSacnUniverse::syncSources()
{
// create a new list of sources
QMap<sACN::DATA::data_header, QSacnUniverse*> newSources;
auto headers = universe_->sources();
for (const auto & metadata : headers)
{
if (sources_.contains(metadata))
// move the QSacnUniverse to the new list
newSources.insert(metadata, sources_.take(metadata));
else
{ // make a new QSacnUniverse to add to the new list
auto universe = universe_->arbitrator()->sourceUniverse(metadata);
newSources.insert(metadata, new QSacnUniverse(this, universe));
}
}
// delete any remaining sources, ie. not moved to the new list
qDeleteAll(sources_);
// accept the new list
sources_ = newSources;
emit sourceListChanged();
}

View File

@ -1,66 +0,0 @@
#pragma once
#include <QObject>
#include <QList>
#include <QMap>
#include <QTimer>
#include "qsacn_global.h"
#include "universe.h"
/**
* @brief The QSacnUniverse class
*
* Rather than QSacnNode reimplemnting the sACN::Node api for a QSacnUniverse that uses
* sACN::Universe as a base class, the QSacnUniverse class wraps a sACN::Universe.
*/
class QT_EXPORT QSacnUniverse
: public QObject
{
Q_OBJECT
public:
explicit QSacnUniverse(QObject *parent = nullptr,
std::shared_ptr<sACN::Universe> universe = nullptr);
virtual ~QSacnUniverse();
const QString sourceName() const;
uint16_t number() const;
uint8_t priority() const;
double rxRate() const;
uint8_t status() const;
uint8_t slot(const uint16_t) const;
uint16_t activeSlots() const;
bool isEditable() const;
bool hasSources() const;
const QList<sACN::DATA::data_header> sources() const;
QSacnUniverse* sourceUniverse(const sACN::DATA::data_header&) const;
bool getHoldLastLook() const;
sACN::UniverseArbitrator::MergeMode getMergeMode() const;
std::shared_ptr<sACN::Universe> get() const;
public slots:
void setHoldLastLook(bool state);
void setMergeMode(const sACN::UniverseArbitrator::MergeMode);
void setPriority(uint8_t p);
void setSyncAddress(uint16_t a);
void setValue (const uint16_t addr, const uint8_t level);
void setValue (const uint16_t addr, const uint16_t size,
const uint8_t* profile);
void syncSources();
signals:
void dataChanged();
void sourceListChanged();
void statusChanged();
private:
std::shared_ptr<sACN::Universe> universe_;
QMap<sACN::DATA::data_header, QSacnUniverse*> sources_;
std::shared_ptr<void> data_change_token;
std::shared_ptr<void> list_change_token;
std::shared_ptr<void> status_change_token;
QTimer *status_watchdog_;
};
Q_DECLARE_METATYPE(QSacnUniverse*)

View File

@ -1,213 +0,0 @@
#include "universemodel.h"
#include <QBrush>
#include <QFont>
#include <QChar>
#include <QMetaEnum>
/**
* @brief UniverseModel::UniverseModel
* @param parent
* @param universe
*/
UniverseModel::UniverseModel(QObject *parent, std::shared_ptr<DMX::Universe> universe)
: QAbstractTableModel(parent)
, universe_(universe)
, data_mode_(Decimal)
{
if (!universe_)
return;
data_change_token = universe_->onDataChange([this] (DMX::Universe*) {
emit dataChanged(index(0,0), index(rowCount(), columnCount()));
});
}
/**
* @brief UniverseModel::headerData
* @param section
* @param orientation
* @param role
* @return
*/
QVariant UniverseModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
{
case Qt::DisplayRole:
switch (orientation)
{
case Qt::Horizontal:
return section + 1;
case Qt::Vertical:
return section * 10;
}
default:
return QVariant();
}
}
/**
* @brief UniverseModel::rowCount
* @param parent
* @return
*/
int UniverseModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return 52;
}
/**
* @brief UniverseModel::columnCount
* @param parent
* @return
*/
int UniverseModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return 10;
}
/**
* @brief UniverseModel::data
* @param index
* @param role
* @return
*/
QVariant UniverseModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return QVariant();
if (!universe_)
return QVariant();
uint16_t slot = (index.row() * 10) + (index.column() + 1);
switch (role) {
case Qt::DisplayRole:
{
if (slot == 0 || slot > 512)
return QVariant();
switch (data_mode_)
{
case Decimal:
return universe_->slot(slot);
case Hex:
return QString("%1")
.arg(universe_->slot(slot), 2, 16, QChar('0'))
.toUpper();
case Percent:
return QString("%1%").arg((universe_->slot(slot) / 255.0F) * 100,
0, 'f', 0, '0');
}
return QVariant();
}
case Qt::FontRole:
return QFont("monospace");
case Qt::TextAlignmentRole:
return int(Qt::AlignCenter | Qt::AlignVCenter);
case Qt::ForegroundRole:
{
if (slot > universe_->activeSlots() - 1)
return QBrush(Qt::gray);
return QVariant();
}
default:
return QVariant();
}
}
/**
* @brief UniverseModel::setData
* @param index
* @param value
* @param role
* @return
*/
bool UniverseModel::setData(const QModelIndex &index, const QVariant &value,
int role)
{
if (!universe_->isEditable())
return false;
if (data(index, role) == value)
return false;
uint16_t slot = (index.row() * 10) + (index.column() + 1);
uint data;
bool ok;
switch (data_mode_)
{
case Decimal:
data = value.toUInt(&ok);
break;
case Hex:
data = value.toString().toUInt(&ok, 16);
break;
case Percent:
data = 255 * (value.toDouble(&ok) / 100);
break;
}
if (!ok || data > 255)
return false;
universe_->setValue(slot, data);
emit dataChanged(index, index, QVector<int>() << role);
return true;
}
/**
* @brief UniverseModel::flags
* @param index
* @return
*/
Qt::ItemFlags UniverseModel::flags(const QModelIndex &index) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return Qt::NoItemFlags;
if (!universe_)
return Qt::NoItemFlags;
uint16_t slot = (index.row() * 10) + (index.column() + 1);
if (slot == 0 || slot > 512)
return Qt::NoItemFlags;
auto f = QAbstractItemModel::flags(index);
return universe_->isEditable() ? f | Qt::ItemIsEditable : f;
}
/**
* @brief UniverseModel::setDataMode
* @param mode
*/
void UniverseModel::setDataMode(const UniverseModel::data_modes mode)
{
data_mode_ = mode;
emit dataChanged(index(0,0), index(rowCount(), columnCount()));
}
/**
* @brief UniverseModel::dataMode
* @return
*/
UniverseModel::data_modes UniverseModel::dataMode()
{
return data_mode_;
}

View File

@ -1,41 +0,0 @@
#pragma once
#include <QAbstractTableModel>
#include <universe.h>
class UniverseModel
: public QAbstractTableModel
{
Q_OBJECT
public:
explicit UniverseModel(QObject *parent = nullptr,
std::shared_ptr<DMX::Universe> universe = nullptr);
// Model overrides:
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index,
int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
/// @brief The data_modes enum
enum data_modes {
Decimal,
Hex,
Percent
};
Q_ENUM(data_modes)
data_modes dataMode();
public slots:
void setDataMode(const UniverseModel::data_modes mode);
private:
std::shared_ptr<DMX::Universe> universe_;
data_modes data_mode_;
std::shared_ptr<void> data_change_token;
};

View File

@ -1,155 +0,0 @@
/*
universestatuswidget.cpp
Copyright (c) 2022 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 "universestatuswidget.h"
#include <QPainter>
#include <QTimer>
UniverseStatusWidget::UniverseStatusWidget(QWidget *parent,
std::shared_ptr<DMX::Universe> universe)
: QWidget{parent}
, mUniverse(universe)
, mBlinkState(true)
{
setMinimumWidth(16);
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding);
// initial state
setToolTip(statusText());
statusChanged();
// status feedback
data_change_token = mUniverse->onDataChange([this] (DMX::Universe*) {
dataActivity();
});
status_change_token = mUniverse->onStatusChange([this] (DMX::Universe*) {
statusChanged();
setToolTip(statusText());
});
}
/**
* @brief StatusIndicatorWidget::statusChanged
*/
void UniverseStatusWidget::statusChanged()
{
switch (mUniverse->status()) {
case DMX::Universe::DMX_LOST:
{
mBlinkState = !mBlinkState;
QTimer::singleShot(250, this, [this]() {
statusChanged();
});
break;
}
default:
mBlinkState = true;;
}
update();
}
/**
* @brief StatusIndicatorWidget::dataActivity
*/
void UniverseStatusWidget::dataActivity()
{
if (!mBlinkState)
return; // already in a blink state
QTimer::singleShot(100, this, [this]() {
mBlinkState = true;
update();
});
mBlinkState = false;
update();
}
/**
* @brief StatusIndicatorWidget::statusText
* @return
*/
const QString UniverseStatusWidget::statusText() const
{
switch (mUniverse->status())
{
case DMX::Universe::DMX_NULL:
return tr("Pending");
case DMX::Universe::DMX_ACTIVE:
return tr("Active");
case DMX::Universe::DMX_LOST:
return tr("Lost");
case DMX::Universe::RX_TIMEOUT:
return tr("Missing");
case sACN::Universe::sACN_TERMINATED:
return tr("Terminated");
default:
return tr("Undefined");
}
}
void UniverseStatusWidget::paintEvent(QPaintEvent *)
{
QColor stroke(127,127,127); // neutral
QColor fill(64,64,64); // grey
switch (mUniverse->status()) {
case DMX::Universe::DMX_NULL:
fill.setRgb(250,250,250); // white
break;
case DMX::Universe::DMX_ACTIVE:
fill.setRgb(32,255,64); // green
break;
case DMX::Universe::DMX_LOST:
fill.setRgb(255,255,64); // yellow
break;
case DMX::Universe::RX_TIMEOUT:
fill.setRgb(255,128,32); // amber
break;
case sACN::Universe::sACN_TERMINATED:
fill.setRgb(255,32,32); // red
break;
default:
break;
}
fill.setAlpha(mBlinkState ? 255 : 42);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(stroke);
painter.setBrush(fill);
const int size = qMin(width(), height());
switch (mUniverse->status()) {
case DMX::Universe::DMX_NULL:
case DMX::Universe::DMX_ACTIVE:
case DMX::Universe::DMX_LOST:
case DMX::Universe::RX_TIMEOUT:
case sACN::Universe::sACN_TERMINATED:
default:
painter.drawEllipse((width()-size)/2, (height()-size)/2, size, size); // circle
}
}

View File

@ -1,51 +0,0 @@
/*
universestatuswidget.h
Copyright (c) 2022 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.
*/
#pragma once
#include <QWidget>
#include <universe.h>
class UniverseStatusWidget
: public QWidget
{
Q_OBJECT
public:
explicit UniverseStatusWidget(QWidget *parent = nullptr,
std::shared_ptr<DMX::Universe> universe = nullptr);
const QString statusText() const;
protected:
void paintEvent(QPaintEvent *) override;
private slots:
void statusChanged();
void dataActivity();
private:
std::shared_ptr<DMX::Universe> mUniverse;
bool mBlinkState;
std::shared_ptr<void> data_change_token;
std::shared_ptr<void> status_change_token;
};

View File

@ -1,140 +0,0 @@
/*
universewindow.h
Copyright (c) 2022 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 "universewindow.h"
#include "ui_universewindow.h"
#include "universemodel.h"
#include "universestatuswidget.h"
#include <QActionGroup>
#include <QComboBox>
#include <QLabel>
#include <QMetaEnum>
/**
* @brief UniverseWindow::UniverseWindow
* @param parent
*/
UniverseWindow::UniverseWindow(QWidget *parent, std::shared_ptr<DMX::Universe> universe)
: QMainWindow(parent)
, ui(new Ui::UniverseWindow())
, universe_(universe)
, status_watchdog_(new QTimer(this))
{
ui->setupUi(this);
// set the status watchdog to update the status if the universe
// isn't showing frequent activity
status_watchdog_->callOnTimeout([this](){update_status_();});
status_watchdog_->setInterval(DMX::E111_DATA_LOSS_TIMEOUT / 10);
status_watchdog_->start();
auto model = new UniverseModel(this, universe);
ui->tableView->setModel(model);
// update the status bar whenever the universe refreshes
connect(model, &UniverseModel::dataChanged, this, [this]() {
update_status_();
status_watchdog_->start();
});
// add data format combobox to toolbar
auto formatLabel = new QLabel(ui->toolBarDMX);
ui->toolBarDMX->addWidget(formatLabel);
formatLabel->setText(tr("Data Format") + ": ");
auto dataModes = new QComboBox(ui->toolBarDMX);
ui->toolBarDMX->addWidget(dataModes);
const QMetaEnum types = QMetaEnum::fromType<UniverseModel::data_modes>();
for (int i=0; i < types.keyCount(); ++i)
dataModes->addItem(types.key(i));
auto formatGroup = new QActionGroup(ui->centralwidget);
formatGroup->addAction(ui->actionViewDecimal);
formatGroup->addAction(ui->actionViewHex);
formatGroup->addAction(ui->actionViewPercent);
connect(dataModes, &QComboBox::currentTextChanged,
this, [this, model, types, formatGroup](QString mode) {
auto data_mode = static_cast<UniverseModel::data_modes>(types.keyToValue(mode.toLocal8Bit()));
model->setDataMode(data_mode);
formatGroup->blockSignals(true);
switch (data_mode) {
case UniverseModel::Decimal:
ui->actionViewDecimal->setChecked(true);
break;
case UniverseModel::Hex:
ui->actionViewHex->setChecked(true);
break;
case UniverseModel::Percent:
ui->actionViewPercent->setChecked(true);
break;
default:
break;
}
formatGroup->blockSignals(false);
});
connect(ui->actionViewDecimal, &QAction::toggled, this, [dataModes](bool state) {
if (state)
dataModes->setCurrentIndex((int)UniverseModel::Decimal);
});
connect(ui->actionViewHex, &QAction::toggled, this, [dataModes](bool state) {
if (state)
dataModes->setCurrentIndex((int)UniverseModel::Hex);
});
connect(ui->actionViewPercent, &QAction::toggled, this, [dataModes](bool state) {
if (state)
dataModes->setCurrentIndex((int)UniverseModel::Percent);
});
// status indicator
auto indicator = new UniverseStatusWidget(this, universe_);
ui->statusbar->addPermanentWidget(indicator);
update_status_();
}
/**
* @brief UniverseWindow::~UniverseWindow
*/
UniverseWindow::~UniverseWindow()
{
delete ui;
}
/**
* @brief UniverseWindow::updateStatusBar
*/
void UniverseWindow::update_status_()
{
if (!universe_->isEditable())
{
// status bar
ui->statusbar->clearMessage();
QString message = QString("%1 " + tr("Hz")).arg(
QString::number(universe_->rxRate()));
ui->statusbar->showMessage(message, 2500);
}
}

View File

@ -1,57 +0,0 @@
/*
universewindow.h
Copyright (c) 2022 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.
*/
#pragma once
#include <universe.h>
#include <QMainWindow>
#include <QTimer>
QT_BEGIN_NAMESPACE
namespace Ui {
class UniverseWindow;
}
QT_END_NAMESPACE
class UniverseWindow
: public QMainWindow
{
Q_OBJECT
public:
explicit UniverseWindow(QWidget *parent = nullptr,
std::shared_ptr<DMX::Universe> universe = nullptr);
virtual ~UniverseWindow();
/**
* @brief universe
* @return
*/
std::shared_ptr<DMX::Universe> universe() { return universe_; }
private:
Ui::UniverseWindow *ui;
std::shared_ptr<DMX::Universe> universe_;
void update_status_();
QTimer *status_watchdog_;
};

View File

@ -1,169 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UniverseWindow</class>
<widget class="QMainWindow" name="UniverseWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>800</height>
</rect>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableView" name="tableView">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>60</number>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>25</height>
</rect>
</property>
<property name="nativeMenuBar">
<bool>false</bool>
</property>
<widget class="QMenu" name="menuView">
<property name="title">
<string>View</string>
</property>
<widget class="QMenu" name="menuToolbars">
<property name="title">
<string>Toolbars</string>
</property>
<addaction name="actionToolbarDMX"/>
</widget>
<addaction name="menuToolbars"/>
</widget>
<widget class="QMenu" name="menuDMX">
<property name="title">
<string>DMX</string>
</property>
<widget class="QMenu" name="menuData_Format">
<property name="title">
<string>Data Format</string>
</property>
<addaction name="actionViewDecimal"/>
<addaction name="actionViewHex"/>
<addaction name="actionViewPercent"/>
</widget>
<addaction name="menuData_Format"/>
</widget>
<addaction name="menuDMX"/>
<addaction name="menuView"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBarDMX">
<property name="windowTitle">
<string>DMX</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
</widget>
<action name="actionViewHex">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Hex</string>
</property>
</action>
<action name="actionViewDecimal">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Decimal</string>
</property>
</action>
<action name="actionViewPercent">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Percent</string>
</property>
</action>
<action name="actionToolbarDMX">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>DMX</string>
</property>
</action>
</widget>
<resources/>
<connections>
<connection>
<sender>actionToolbarDMX</sender>
<signal>toggled(bool)</signal>
<receiver>toolBarDMX</receiver>
<slot>setVisible(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>33</y>
</hint>
</hints>
</connection>
<connection>
<sender>toolBarDMX</sender>
<signal>visibilityChanged(bool)</signal>
<receiver>actionToolbarDMX</receiver>
<slot>setChecked(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>199</x>
<y>33</y>
</hint>
<hint type="destinationlabel">
<x>-1</x>
<y>-1</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,32 +0,0 @@
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC OFF)
set(CMAKE_AUTOMOC OFF)
set(CMAKE_AUTORCC OFF)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG")
endif()
add_compile_definitions(BUILT_WITH_CMAKE)
configure_file(config.h.in config.h)
# Entertainment Services and Technology Association
add_subdirectory(esta)
# Internet Engineering Task Force
add_subdirectory(ietf)
# Artistic License
add_subdirectory(artistic)
# Enttec
add_subdirectory(enttec)
# OpenSoundControl
add_subdirectory(osc)
# Controller Interface Transport Protocol
add_subdirectory(citp)

View File

@ -1,52 +0,0 @@
# OpenLCP Protocols
Copies of the specifications and standards are copyright their authors, and may be
downloaded from the publishers website.
## ESTA
[Entertainment Services and Technology Association ](esta.org)
[ESTA Technical Standards Program](tsp.esta.org)
[TSP Published Documents](tsp.esta.org/tsp/documents/published_docs.php)
## Artistic License
[Artistic License](artisticlicence.com)
[Art-Net](art-net.org.uk)
[Art-Net 4 Specification](artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf)
## Open Sound Control
[OSC](opensoundcontrol.stanford.edu)
[Specification 1.0](opensoundcontrol.stanford.edu/spec-1_0.html)
[Specification 1.1](opensoundcontrol.stanford.edu/spec-1_1.html)
## ENTTEC
[ENTTEC](www.enttec.com)
[DMX USB Pro](www.enttec.com/product/lighting-communication-protocols/dmx512/dmx-usb-pro/)
[API V1.44](cdn.enttec.com/pdf/assets/70304/70304_DMX_USB_PRO_API.pdf)
## CITP
The [CITP Protocol](www.lewlight.com/citp-protocol) website appears to
have gone dormant, but the text is still available in the
[author's BitBucket](bitbucket.org/lars_wernlund/citp/downloads/)
## IETF
[The Internet Engineering Task Force](www.ietf.org)
[Various RFC documents](www.ietf.org/standards/rfcs/)

View File

@ -1,9 +0,0 @@
@String{ARTISTIC = "Artistic Licence Holdings Ltd."}
@manual{ARTNET,
key = "ARTNET",
title = "Specification for the Art-Net 4 Ethernet Communication Protocol",
organization = ARTISTIC,
year = 2021,
address = "Bovey Tracey, England"
}

View File

@ -1,2 +0,0 @@
# Art-Net 4
add_subdirectory(artnet)

View File

@ -1,36 +0,0 @@
project(${PROJECT_NAME}-artnet VERSION ${PROJECT_VERSION})
add_library(${PROJECT_NAME} SHARED)
add_library(LCP::ArtNet ALIAS ${PROJECT_NAME})
configure_file(../../config.h.in config.h)
target_sources(${PROJECT_NAME}
PUBLIC
controller.h
device.h
gateway.h
node.h
universe.h
PRIVATE
artnet.h
controller.cpp
device.cpp
gateway.cpp
node.cpp
packet.h
packet.cpp
port.h
port.cpp
universe.cpp
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
LCP::ACN::SDT # for IP Address types
LCP::BufferStream
LCP::RDM
)
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION})
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,214 +0,0 @@
# OpenLCP support for Art-Net
The Art-Net 4 Release 1.4dd2 (1/2/2021) describes three members of an Art-Net network:
- **Controller** : A lighting console.
- **Node** : A DMX to / from Art-Net device.
- **Media Server** : A Media Server.
The ArtPollReply Styles (Table 4) enumerates those, plus an additional 4 device types:
- **Route** : A network routing device.
- **Backup** : A backup device.
- **Config** : A configuration or diagnostic tool
- **Visual** : A visualizer
The behaviors of Route, Backup, Config, and Visual devices is not specified. The behaviors of Controller, Node, and Media Server are defined, and summarized in a table below.
In OpenLCP, the base class for all Art-Net devices is the ARTNET::Device class. ARTNET::Controller and ARTNET::Node inherit from ARTNET::Device virtually, allowing custom implementations that may need to inherit from ARTNET::Controller _and_ ARTNET::Node to do so with a common base ARTNET::Device.
- ARTNET::Device carries the implementation for:
- Rx
- ArtPoll
- ArtTimeCode
- ArtCommand
- ArtTrigger
- ArtDmx
- ArtNzs
- ArtVlc
- ArtRdm
- ArtRdmSub
- Tx
- ArtPollReply
- ArtDiagData
- ArtTimeCode
- ArtCommand
- ArtTrigger
- ArtDmx (Peer to Peer)
- ArtNzs
- ArtVlc
- ArtRdm
- ArtRdmSub
- ARTNET::Controller extends ARTNET::Device with:
- Rx
- ArtPollReply
- ArtIpProgReply
- ArtDiagData
- ArtFirmwareReply
- ArtTodData
- Tx
- ArtPoll
- ArtIpProg
- ArtAddress
- ArtDmx (Controller to Peer)
- ArtSync
- ArtInput
- ArtFirmwareMaster
- ArtTodRequest
- ArtTodControl
- ARTNET::Node extends ARTNET::Device with:
- Rx
- ArtIpProg
- ArtAddress
- ArtSync
- ArtInput
- ArtFirmwareMaster
- ArtTodRequest
- ArtTodControl
- Tx
- ArtIpProgReply
- ArtFirmwareReply
- ArtTodData
## Art-Net Table of Behaviors
<table>
<tr>
<th rowspan=3>OpCode</th>
<th colspan=3>Controller</th>
<th colspan=3>Node</th>
<th colspan=3>Media Server</th>
</tr>
<tr>
<th rowspan=2>RX Behavior</th><th colspan=2>TX</th>
<th rowspan=2>RX Behavior</th><th colspan=2>TX</th>
<th rowspan=2>RX Behavior</th><th colspan=2>TX</th>
</tr>
<tr>
<th>uni</th><th>brd</th>
<th>uni</th><th>brd</th>
<th>uni</th><th>brd</th>
</tr>
<tr>
<td>ArtPoll</td>
<td>ArtPollReply</td><td></td><td>TX</td>
<td>ArtPollReply</td><td></td><td></td>
<td>ArtPollReply</td><td></td><td></td>
</tr>
<tr>
<td>ArtPollReply</td>
<td></td><td></td><td>TX</td>
<td></td><td></td><td>TX</td>
<td></td><td></td><td>TX</td>
</tr>
<tr>
<td>ArtIpProg</td>
<td></td><td>TX</td><td></td>
<td>ArtIpProgReply</td><td></td><td></td>
<td>ArtIpProgReply</td><td></td><td></td>
</tr>
<tr>
<td>ArtIpProgReply</td>
<td></td><td></td><td></td>
<td></td><td>TX</td><td></td>
<td></td><td>TX</td><td></td>
</tr>
<tr>
<td>ArtAddress</td>
<td></td><td>TX</td><td></td>
<td>ArtPollReply</td><td></td><td></td>
<td>ArtPollReply</td><td></td><td></td>
</tr>
<tr>
<td>ArtDiagData</td>
<td></td><td colspan=2>per ArtPoll</td>
<td></td><td colspan=2>per ArtPoll</td>
<td></td><td colspan=2>per ArtPoll</td>
</tr>
<tr>
<td>ArtTimeCode</td>
<td colspan=9>Application Specific</td>
</tr>
<tr>
<td>ArtCommand</td>
<td colspan=9>Application Specific</td>
</tr>
<tr>
<td>ArtTrigger</td>
<td colspan=9>Application Specific</td>
</tr>
<tr>
<td>ArtDmx</td>
<td>Application Specific</td><td>TX</td><td></td>
<td>Application Specific</td><td>TX</td><td></td>
<td>Application Specific</td><td>TX</td><td></td>
</tr>
<tr>
<td>ArtSync</td>
<td></td><td></td><td>TX</td>
<td>Sync</td><td></td><td></td>
<td>Sync</td><td></td><td></td>
</tr>
<tr>
<td>ArtNzs</td>
<td>Application Specific</td><td>TX</td><td></td>
<td>Application Specific</td><td>TX</td><td></td>
<td>Application Specific</td><td>TX</td><td></td>
</tr>
<tr>
<td>ArtVlc</td>
<td colspan=9>As ArtNzs</td>
</tr>
<tr>
<td>ArtInput</td>
<td></td><td>TX</td><td></td>
<td>ArtPollReply</td><td></td><td></td>
<td>ArtPollReply</td><td></td><td></td>
</tr>
<tr>
<td>ArtFirmwareMaster</td>
<td></td><td>TX</td><td></td>
<td>OpFirmwareReply</td><td></td><td></td>
<td>OpFirmwareReply</td><td></td><td></td>
</tr>
<tr>
<td>ArtFirmwareReply</td>
<td>Next OpFirmwareMaster</td><td></td><td></td>
<td></td><td>TX</td><td></td>
<td></td><td>TX</td><td></td>
</tr>
<tr>
<td>ArtTodRequest</td>
<td></td><td></td><td>TX</td>
<td>ArtTodData</td><td></td><td></td>
<td></td><td></td><td></td>
</tr>
<tr>
<td>ArtTodData</td>
<td></td><td></td><td></td>
<td></td><td></td><td></td>
<td></td><td></td><td></td>
</tr>
<tr>
<td>ArtTodControl</td>
<td></td><td></td><td>TX</td>
<td>ArtTodData</td><td></td><td></td>
<td></td><td></td><td></td>
</tr>
<tr>
<td>ArtRdm</td>
<td></td><td>TX</td><td>tx</td>
<td></td><td>TX</td><td>tx</td>
<td></td><td>TX</td><td>tx</td>
</tr>
<tr>
<td>ArtRdmSub</td>
<td></td><td>TX</td><td></td>
<td></td><td>TX</td><td></td>
<td></td><td></td><td></td>
</tr>
</table>
Art-Net™ is a trade mark of Artistic Licence Holdings Ltd. The Art-Net protocol and associated documentation is copyright Artistic Licence Holdings Ltd.

View File

@ -1,541 +0,0 @@
/*
artnet.h
Copyright (c) 2022 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.
*/
#pragma once
#include <cstdint>
/**
* @brief \cite ARTNET Art-Net is an Ethernet protocol based on the TCP/IP protocol
* suite.
*
* Its purpose is to allow transfer of large amounts of DMX512 data
* over a wide area using standard networking technology.
*/
namespace ARTNET {
/// @brief \cite ARTNET Universe Addressing
///
/// > The Port-Address of each DMX512 Universe is encoded as a 15-bit number
struct PortAddress {
union {
uint16_t value;
struct {
union {
uint8_t subuni;
struct {
uint8_t universe : 4; //!< A single DMX512 frame of 512 channels is referred to as a Universe.
uint8_t subnet : 4; //!< A group of 16 consecutive universes is referred to as a sub-net.
};
};
struct {
uint8_t net : 7; //!< A group of 16 consecutive Sub-Nets or 256 consecutive Universes is referred to as a net. There are 128 Nets in total.
bool reserved : 1;
};
};
};
};
/// @brief \cite ARTNET Protocol Operation
///
/// > The UDP port used as both source and destination is 0x1936.
static const uint16_t UDP_PORT = 6454;
/// @brief Packet ID
///
/// > Array of 8 characters, the final character is a null termination.
/// > Value = A r t - N e t 0x00
static const uint8_t PACKET_IDENTIFIER[] = { 0x41, 0x72, 0x74, 0x2D,
0x4E, 0x65, 0x74, 0x00};
/// @brief \cite ARTNET Art-Net protocol revision number.
///
/// Current value 14. Controllers should ignore communication with nodes
/// using a protocol version lower than 14.
static const uint16_t VERSION = 14;
/// @brief \cite ARTNET Table 1 - OpCodes
///
/// The OpCode defines the class of data following within the UDP packet.
/// The following table details the legal OpCode values used in Art-Net packets
enum OpCode : uint16_t {
OpNull = 0x0000, //!< no-op. Ignore this packet.
OpPoll = 0x2000, //!< an ArtPoll packet, no other data is contained in this UDP packet.
OpPollReply = 0x2100, //!< an ArtPollReply Packet. It contains device status information.
OpDiagData = 0x2300, //!< Diagnostics and data logging packet.
OpCommand = 0x2400, //!< Used to send text based parameter commands.
// OpOutput = 0x5000, //!< an ArtDmx data packet. It contains zero start code DMX512 information for a single Universe.
OpDmx = 0x5000, //!< an ArtDmx data packet. It contains zero start code DMX512 information for a single Universe.
OpNzs = 0x5100, //!< an ArtNzs data packet. It contains non-zero start code (except RDM) DMX512 information for a single Universe.
OpSync = 0x5200, //!< an ArtSync data packet. It is used to force synchronous transfer of ArtDmx packets to a nodes output.
OpAddress = 0x6000, //!< an ArtAddress packet. It contains remote programming information for a Node.
OpInput = 0x7000, //!< an ArtInput packet. It contains enable disable data for DMX inputs.
OpTodRequest = 0x8000, //!< an ArtTodRequest packet. It is used to request a Table of Devices (ToD) for RDM discovery.
OpTodData = 0x8100, //!< an ArtTodData packet. It is used to send a Table of Devices (ToD) for RDM discovery.
OpTodControl = 0x8200, //!< an ArtTodControl packet. It is used to send RDM discovery control messages.
OpRdm = 0x8300, //!< an ArtRdm packet. It is used to send all non discovery RDM messages.
OpRdmSub = 0x8400, //!< an ArtRdmSub packet. It is used to send compressed, RDM Sub-Device data.
OpVideoSetup = 0xA010, //!< an ArtVideoSetup packet. It contains video screen setup information for nodes that implement the extended video features.
OpVideoPalette = 0xA020, //!< an ArtVideoPalette packet. It contains colour palette setup information for nodes that implement the extended video features.
OpVideoData = 0xA040, //!< an ArtVideoData packet. It contains display data for nodes that implement the extended video features.
OpMacMaster = 0xF000, //!< depreciated
OpMacSlave = 0xF100, //!< depreciated
OpFirmwareMaster = 0xF200, //!< an ArtFirmwareMaster packet. It is used to upload new firmware or firmware extensions to the Node.
OpFirmwareReply = 0xF300, //!< an ArtFirmwareReply packet. It is returned by the node to acknowledge receipt of an ArtFirmwareMaster packet or ArtFileTnMaster packet.
OpFileTnMaster = 0xF400, //!< Uploads user file to node.
OpFileFnMaster = 0xF500, //!< Downloads user file from node.
OpFileFnReply = 0xF600, //!< Server to Node acknowledge for download packets.
OpIpProg = 0xF800, //!< an ArtIpProg packet. It is used to re- programme the IP, Mask and Port address of the Node.
OpIpProgReply = 0xF900, //!< an ArtIpProgReply packet. It is returned by the node to acknowledge receipt of an ArtIpProg packet.
OpMedia = 0x9000, //!< an ArtMedia packet. It is Unicast by a Media Server and acted upon by a Controller.
OpMediaPatch = 0x9100, //!< an ArtMediaPatch packet. It is Unicast by a Controller and acted upon by a Media Server.
OpMediaControl = 0x9200, //!< an ArtMediaControl packet. It is Unicast by a Controller and acted upon by a Media Server.
OpMediaContrlReply = 0x9300, //!< an ArtMediaControlReply packet. It is Unicast by a Media Server and acted upon by a Controller.
OpTimeCode = 0x9700, //!< an ArtTimeCode packet. It is used to transport time code over the network.
OpTimeSync = 0x9800, //!< Used to synchronise real time date and clock
OpTrigger = 0x9900, //!< Used to send trigger macros
OpDirectory = 0x9A00, //!< Requests a node's file list
OpDirectoryReply = 0x9B00 //!< Replies to OpDirectory with file list
};
/// OEM is unknown or non-registered type
static const uint16_t oem_unknown = 0x00ff;
/// Used by ArtTrigger for general purpose codes
static const uint16_t oem_gobal = 0xffff;
/// @brief \cite ARTNET Table 3 - NodeReport Codes
enum NodeReport : uint16_t {
RcDebug = 0x0000, //!< Booted in debug mode (Only used in development)
RcPowerOk = 0x0001, //!< Power On Tests successful
RcPowerFail = 0x0002, //!< Hardware tests failed at Power On
RcSocketWr1 = 0x0003, //!< Last UDP from Node failed due to truncated length, Most likely caused by a collision.
RcParseFail = 0x0004, //!< Unable to identify last UDP transmission. Check OpCode and packet length.
RcUdpFail = 0x0005, //!< Unable to open Udp Socket in last transmission attempt
RcShNameOk = 0x0006, //!< Confirms that Short Name programming via ArtAddress, was successful.
RcLoNameOk = 0x0007, //!< Confirms that Long Name programming via ArtAddress, was successful.
RcDmxError = 0x0008, //!< DMX512 receive errors detected.
RcDmxUdpFull = 0x0009, //!< Ran out of internal DMX transmit buffers.
RcDmxRxFull = 0x000A, //!< Ran out of internal DMX Rx buffers.
RcSwitchErr = 0x000B, //!< Rx Universe switches conflict.
RcConfigErr = 0x000C, //!< Product configuration does not match firmware.
RcDmxShort = 0x000D, //!< DMX output short detected. See GoodOutput field.
RcFirmwareFail = 0x000E, //!< Last attempt to upload new firmware failed.
RcUserFail = 0x000F, //!< User changed switch settings when address locked by remote programming. User changes ignored.
RcFactoryRes = 0x0010 //!< Factory reset has occurred.
};
/// \cite ARTNET Table 4 - Style Codes
enum Style : uint8_t {
StNode = 0x00, //!< A DMX to / from Art-Net device
StController = 0x01, //!< A lighting console.
StMedia = 0x02, //!< A Media Server.
StRoute = 0x03, //!< A network routing device.
StBackup = 0x04, //!< A backup device.
StConfig = 0x05, //!< A configuration or diagnostic tool
StVisual = 0x06 //!< A visualiser
};
/// \cite ARTNET Table 5 - Priority Codes
enum Priority : uint8_t {
DpLow = 0x10, //!< Low priority message.
DpMed = 0x40, //!< Medium priority message.
DpHigh = 0x80, //!< High priority message.
DpCritial = 0xE0, //!< Critical priority message.
DpVolatile = 0xF0 //!< Volatile message.
};
/// Set the communication behavior during ArtPoll
struct TalkToMe {
union {
uint8_t value = 0;
struct {
bool depreciated : 1;
bool reply_on_change : 1; //!< Send ArtPollReply whenever Node conditions change.
bool diag_enable : 1; //!< Send me diagnostics messages.
bool diag_unicast : 1; //!< Diagnostics messages are unicast. (if bit 2).
bool VLC_disable : 1; //!< Disable VLC transmission.
uint8_t reserved : 3;
};
};
};
/**
* @brief The OEM word
*
* Describes the equipment vendor and the feature set available.
* Bit 15 high indicates extended features available.
*/
struct OEM {
union {
uint16_t value = oem_unknown;
struct {
uint16_t manufacturer : 15;
bool extended_features : 1;
};
};
};
/**
* @brief The Authority enum
*/
enum Authority : uint8_t {
AuthorityUnkown = 0b00, //!< Port-Address Programming Authority unknown.
AuthorityLocal = 0b01, //!< Port-Address set by front panel controls.
AuthorityRemote = 0b10, //!< All or part of Port-Address programmed by network
AuthorityUnused = 0b11, //!< not used
};
/**
* @brief The Indicator enum
*/
enum Indicator : uint8_t {
IndicatorUnknown = 0b00, //!< Indicator state unknown.
IndicatorIdentify = 0b01, //!< Indicators in Locate / Identify Mode.
IndicatorMute = 0b10, //!< Indicators in Mute Mode.
IndicatorNormal = 0b11, //!< Indicators in Normal Mode.
};
/**
* @brief The FailoverMode enum
*/
enum FailoverMode : uint8_t {
FailoverHoldLast = 0b00, //!< Hold last state.
FailoverAllZeros = 0b01, //!< All output to zero.
FailoverAllFull = 0b10, //!< All output to full.
FailoverScenePlayback = 0b11, //!< Playback failover scene.
};
/**
* @brief The General_Status register
*/
struct GeneralStatus {
union {
uint8_t value1 = 0;
struct {
bool ubea_valid : 1; //!< UBEA present and not corrupt.
bool rdm_capable : 1; //!< Capable of Remote Device Management (RDM).
bool boot_failsafe : 1; //!< Booted from ROM
bool reserved1 : 1; //!< not implimented
Authority authority : 2; //!< Port-Address Programming Authority
Indicator indicator : 2; //!< Indicator State
};
};
union {
uint8_t value2 = 0b00010000; // 15-bit port-addresses (Art-Net 3 or 4)
struct {
bool web_config_available : 1; //!< supports web browser configuration.
bool DHCP_active : 1; //!< Nodes IP is DHCP configured.
bool DHCP_capable : 1; //!< Node is DHCP capable.
bool port_address_15b : 1; //!< Node supports 15 bit Port-Address
bool sACN_capable : 1; //!< Node is able to switch between Art-Net and sACN.
bool squawking : 1; //!< is sqawking
bool io_remote_set : 1; //!> Node supports switching of output style using ArtCommand.
bool remote_RDM : 1; //!> Node supports control of RDM using ArtCommand.
};
};
union {
uint8_t value3 = 0;
struct {
uint8_t reserved3 : 5; //!< Not used, set to zero
bool failover_capable : 1; //!< Node supports fail-over.
FailoverMode failover_mode : 2; //!< network data loss behavior.
};
};
};
/**
* @brief The KnownProtocols enum
*
* Protocols elegible to be reported in the PortTypes array
*/
enum KnownProtocols : uint8_t {
Protocol_DMX = 0b000000,
Protocol_MIDI = 0b000001,
Protocol_Avab = 0b000010,
Protocol_CMX = 0b000011,
Protocol_ADB = 0b000100,
Protocol_ArtNet = 0b000101,
};
/**
* @brief The PortTypes struct
*
* An array of 4 PortTypes are reported in the ArtPollReply packet.
*/
struct PortTypes {
union {
uint8_t value = 0;
struct {
KnownProtocols protocol : 6;
bool can_input : 1;
bool can_output : 1;
};
};
};
/**
* @brief The GoodInput struct
*
* An array of 4 GoodInput are reported in the ArtPollReply packet.
*/
struct GoodInput {
union {
uint8_t value = 0;
struct {
uint8_t reserved : 2; //!< Unused and transmitted as zero.
bool rx_errors : 1; //!< Receive errors detected.
bool disabled : 1; //!< Input is disabled.
bool DMX_text : 1; //!< includes DMX512 text packets.
bool DMX_SIP : 1; //!< includes DMX512 SIPs.
bool DMX_test : 1; //!< includes DMX512 test packets.
bool active : 1; //!< Data received.
};
};
};
/**
* @brief The GoodOutput struct
*
* An array of 4 GoodOutput are reported in the ArtPollReply packet.
*/
struct GoodOutput {
union {
uint8_t valueA = 0;
struct {
bool sACN_enabled : 1; //!< Output is selected to transmit sACN.
bool merge_LTP : 1; //!< Merge Mode is LTP.
bool output_short : 1; //!< DMX output short detected on power up.
bool merge_ArtNet : 1; //!< Output is merging ArtNet data.
bool DMX_text : 1; //!< includes DMX512 text packets.
bool DMX_SIP : 1; //!< includes DMX512 SIPs.
bool DMX_test : 1; //!< includes DMX512 test packets.
bool active : 1; //!< Data received.
};
};
union {
uint8_t valueB = 0;
struct {
uint8_t reserved : 6; //!< Not used, set to zero
bool output_continuous : 1; //!< oposite of changes-only
bool rdm_disabled : 1; //!< RDM disabled
};
};
};
/**
* @brief The ActivityReport struct
*
* reported in the ArtPollReply packet as SwMacro and SwRemote
*/
struct ActivityReport {
union {
uint8_t value = 0;
struct {
bool active_1 : 1;
bool active_2 : 1;
bool active_3 : 1;
bool active_4 : 1;
bool active_5 : 1;
bool active_6 : 1;
bool active_7 : 1;
bool active_8 : 1;
};
};
};
/**
* @brief The AddressCommand enum
*/
enum AddressCommand : uint8_t {
// Node configuration commands:
AcNone = 0x00, //!< No action
AcCancelMerge = 0x01, //!< if in merge mode, cancel
AcLedNormal = 0x02, //!< set front panel indicators to normal
AcLedMute = 0x03, //!< switch off front panel indicators
AcLedLocate = 0x04, //!< flash front panel indicators
AcResetRxFlags = 0x05, //!< reset SIP, Text, Test and Error flags
// Fail-over configuration commands:
AcFailHold = 0x08, //!< hold last look
AcFailZero = 0x09, //!< output zeros on data loss
AcFailFull = 0x0a, //!< output full on data loss
AcFailScene = 0x0b, //!< output scene on data loss
AcFailRecord = 0x0c, //!< record current output state as failover scene
// Port configuration commands:
AcMergeLtp0 = 0x10, //!< set port 0 to merge LTP
AcMergeLtp1 = 0x11, //!< set port 1 to merge LTP
AcMergeLtp2 = 0x12, //!< set port 2 to merge LTP
AcMergeLtp3 = 0x13, //!< set port 3 to merge LTP
AcMergeHtp0 = 0x50, //!< set port 0 to merge HTP
AcMergeHtp1 = 0x51, //!< set port 1 to merge HTP
AcMergeHtp2 = 0x52, //!< set port 2 to merge HTP
AcMergeHtp3 = 0x53, //!< set port 3 to merge HTP
AcArtNetSel0 = 0x60, //!< set port 0 protocol to Art-Net
AcArtNetSel1 = 0x61, //!< set port 1 protocol to Art-Net
AcArtNetSel2 = 0x62, //!< set port 2 protocol to Art-Net
AcArtNetSel3 = 0x63, //!< set port 3 protocol to Art-Net
AcAcnSel0 = 0x70, //!< set port 0 protocol to sACN
AcAcnSel1 = 0x71, //!< set port 1 protocol to sACN
AcAcnSel2 = 0x72, //!< set port 2 protocol to sACN
AcAcnSel3 = 0x73, //!< set port 3 protocol to sACN
AcClearOp0 = 0x90, //!< clear output buffer for port 0
AcClearOp1 = 0x91, //!< clear output buffer for port 1
AcClearOp2 = 0x92, //!< clear output buffer for port 2
AcClearOp3 = 0x93, //!< clear output buffer for port 3
AcStyleDelta0 = 0xa0, //!< set port 0 to changes-only
AcStyleDelta1 = 0xa1, //!< set port 1 to changes-only
AcStyleDelta2 = 0xa2, //!< set port 2 to changes-only
AcStyleDelta3 = 0xa3, //!< set port 3 to changes-only
AcStyleConst0 = 0xb0, //!< set port 0 to constant output
AcStyleConst1 = 0xb1, //!< set port 1 to constant output
AcStyleConst2 = 0xb2, //!< set port 2 to constant output
AcStyleConst3 = 0xb3, //!< set port 3 to constant output
AcRdEnable0 = 0xc0, //!< enable RDM for port 0
AcRdEnable1 = 0xc1, //!< enable RDM for port 1
AcRdEnable2 = 0xc2, //!< enable RDM for port 2
AcRdEnable3 = 0xc3, //!< enable RDM for port 3
AcRdmDisable0 = 0xd0, //!< disable RDM for port 0
AcRdmDisable1 = 0xd1, //!< disable RDM for port 1
AcRdmDisable2 = 0xd2, //!< disable RDM for port 2
AcRdmDisable3 = 0xd3, //!< disable RDM for port 3
};
/**
* @brief The TimecodeType enum
*/
enum TimecodeType : uint8_t {
Film = 0, //!< 24 fps
EBU = 1, //!< 25 fps
DF = 3, //!< 29.97 fps
SMPTE = 4, //!< 30 fps
};
/**
* @brief The Timecode struct
*/
struct Timecode {
uint8_t frame; //!< 24-30, depending on type
uint8_t seconds; //!< 0-59
uint8_t minutes; //!< 0-59
uint8_t hours; //!< 0-24
TimecodeType type; //!< framerate
};
// Table 7 ArtTrigger Key Values.
/// The SubKey field contains an ASCII character which the receiving device
/// should process as if it were a keyboard press.
static const uint8_t KeyAscii = 0;
/// The SubKey field contains the number of a Macro which the receiving
/// device should execute.
static const uint8_t KeyMacro = 1;
/// The SubKey field contains a soft-key number which the receiving device
/// should process as if it were a soft-key keyboard press.
static const uint8_t KeySoft = 2;
/// The SubKey field contains the number of a Show which the receiving
/// device should run.
static const uint8_t KeyShow = 3;
static const std::size_t Short_Name_Length = 18; //!< string length
static const std::size_t Long_Name_Length = 64; //!< string length
static const std::size_t Node_Report_Length = 64; //!< string length
/// \cite ARTNET
/// ...to allow transition between synchronous and non-synchronous modes, a
/// node shall time out to non-synchronous operation if an ArtSync is not
/// received for 4 seconds or more.
static const uint16_t SYNC_LOSS_TIMEOUT = 4000;
namespace VLC {
/// The DMX512 start code of this (ArtVlc) packet is set to 0x91.
static const uint8_t START_CODE = 0x91;
/// Magic number used to identify this packet.
static const uint8_t MagicNumber[] = {0x41, 0x4c, 0x45};
/// Payload contains a simple text string representing a URL.
static const uint16_t BeaconURL = 0x0000;
/// Payload contains a simple ASCII text message
static const uint16_t BeaconText = 0x0001;
} // namespace ARTNET::VLC
namespace FIRMWARE {
/// Type of firmware master packet
enum MasterType : uint8_t {
FirmFirst = 0x00, //!< The first packet of a firmware upload.
FirmCont = 0x01, //!< A consecutive continuation packet of a firmware upload.
FirmLast = 0x02, //!< The last packet of a firmware upload.
UbeaFirst = 0x03, //!< The first packet of a UBEA upload.
UbeaCont = 0x04, //!< A consecutive continuation packet of a UBEA upload.
UbeaLast = 0x05, //!< The last packet of a UBEA upload.
};
/// Type of firmware reply packet
enum ResponseType : uint8_t {
FirmBlockGood = 0x00, //!< Last packet recieved successfully.
FirmAllGood = 0x01, //!< All firmware receieved successfully.
FirmFail = 0xff, //!< Firmware upload failed.
};
} // namespace ARTNET::FIRMWARE
namespace RDM {
enum Version : uint8_t {
Draft = 0x00, //!< RDM DRAFT V1.0
Standard = 0x01, //!< RDM V1.0
};
enum TodRequestCommand : uint8_t {
TodFull = 0x00, //!< Send the Entire TOD
};
enum TodCommandResponse : uint8_t {
ResponseTodFull = 0x00, //!< entire or packet in sequence of the entire TOD.
ResponseTodNak = 0xff, //!< TOD is not available or discovery is incomplete.
};
enum TodControlCommand : uint8_t {
AtcNone = 0x00, //!< No action.
AtcFlush = 0x01, //!< The node flushes its TOD and instigates full discovery.
};
enum RdmCommand : uint8_t {
ArProcess = 0x00, //!< Process RDM Packet.
};
} // namespace ARTNET::RDM
/// @brief Communication Activity
///
/// On if any Art-Net packets detected on network, timeout after 6 seconds.
static const uint16_t DATA_LOSS_TIMEOUT = 6000;
} // namespace ARTNET

View File

@ -1,123 +0,0 @@
/*
controller.cpp
Copyright (c) 2022 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 "controller.h"
namespace ARTNET {
Controller::Controller()
: Device(StController)
, minimum_polling_interval(2500)
, _periodic_polling_enabled(true)
, _pollster(std::thread(&Controller::_periodic_polling, this))
, last_poll(std::chrono::system_clock::from_time_t(0))
, _talktome_request({.VLC_disable=true})
, _priority_request(DpCritial)
{
setShortName("OpenLCP Controller");
setLongName("Generic OpenLCP Art-Net Controller");
}
Controller::~Controller()
{
if (_pollster.joinable())
{
_periodic_polling_enabled = false; // disable
{
std::shared_lock lk_ctl(_mtx_poll_control);
_poll_request.notify_all(); // wake-up
}
_pollster.join();
}
}
/**
* @brief Controller::setTalkToMeRequest
* @param talktome
*/
void Controller::setTalkToMeRequest(TalkToMe talktome)
{
std::unique_lock lk_data(_mtx_control_data);
_talktome_request = talktome;
}
/**
* @brief Controller::setPriorityRequest
* @param priority
*/
void Controller::setPriorityRequest(Priority priority)
{
std::unique_lock lk_data(_mtx_control_data);
_priority_request = priority;
}
/**
* @brief Controller::txArtPoll
*/
void Controller::txArtPoll()
{
auto data = std::make_shared<poll_data>();
{
std::shared_lock lk_data(_mtx_control_data);
data->talk_to_me = _talktome_request;
data->diagnostic_level = _priority_request;
}
auto packet = std::make_shared<ArtPoll>(data);
send(packet, broadcastIp());
// every poll tx gets considered in the minimum_poll_interval
{
std::unique_lock lk_ctl(_mtx_poll_control);
last_poll = std::chrono::system_clock::now();
}
}
void Controller::_periodic_polling()
{
std::chrono::nanoseconds elapsed;
std::unique_lock lk_thread(_mtx_poll_thread);
while (_periodic_polling_enabled) {
// enforce strict minimum update interval
{
std::shared_lock lk_ctl(_mtx_poll_control);
elapsed = std::chrono::system_clock::now() - last_poll;
}
if (elapsed < minimum_polling_interval)
std::this_thread::sleep_for(minimum_polling_interval - elapsed);
if(!_periodic_polling_enabled) // may have been disabled while enforcing minimum interval
break;
txArtPoll();
// sleep before the next cycle
_poll_request.wait_for(lk_thread, minimum_polling_interval);
}
}
} // namespace ARTNET

View File

@ -1,70 +0,0 @@
/*
controller.h
Copyright (c) 2022 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.
*/
#pragma once
#include "device.h"
#include "gateway.h"
#include <atomic>
#include <condition_variable>
#include <shared_mutex>
#include <thread>
namespace ARTNET {
/**
* @brief \cite ARTNET A generic term describing an Art-Net device with the
* primary task of generating control data.
*
* \cite ARTNET Controllers emulate the operation of Input Gateways.
*/
class Controller
: public Device
, public InputGateway
{
public:
explicit Controller();
virtual ~Controller();
void setTalkToMeRequest(TalkToMe);
void setPriorityRequest(Priority);
protected:
virtual void txArtPoll();
private:
const std::chrono::milliseconds minimum_polling_interval;
mutable std::shared_mutex _mtx_poll_control;
mutable std::mutex _mtx_poll_thread;
std::atomic<bool> _periodic_polling_enabled;
std::condition_variable_any _poll_request;
std::thread _pollster;
void _periodic_polling();
mutable std::shared_mutex _mtx_control_data;
std::chrono::system_clock::time_point last_poll;
TalkToMe _talktome_request; //!< TalkToMe to be uses in the ArtPoll
Priority _priority_request; //!< Priority to be used in the ArtPoll
};
} // namespace ARTNET

View File

@ -1,589 +0,0 @@
/*
device.cpp
Copyright (c) 2022 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 "device.h"
namespace ARTNET {
/**
* @brief Device::Device
* @param style
*
* \note A ArtPollReply packet is required to be broadcast to the Directed Broadcast address by
* all Art-Net devices on power up. Since information in that packet requres a fully configured
* device, it must be specifically sent later.
*/
Device::Device(Style style)
: styleCode(style)
, diagnostic_reporting_threshold(DpCritial)
, device_status {.rdm_capable=true, .authority=AuthorityUnused}
, _shortName("OpenLCP Device")
, _longName("Generic OpenLCP Art-Net Device")
, _report_code(RcPowerOk)
, _poll_reply_count(0)
, _report_text("initialized")
{
}
/**
* @brief Device::receive
* @param buffer
* @param origin
*/
void Device::receive(ACN::PDU::Stream buffer, ipAddress &origin)
{
if (origin.port != UDP_PORT || origin.type != 1) // ACN::SDT::SDT_ADDR_IPV4
return buffer->setstate(std::ios_base::badbit);
auto packet = std::make_shared<Packet>();
packet->iStream(buffer);
if (buffer->fail() || buffer->bad())
{
_report_code = RcParseFail;
_report_text = "unable to read packet";
return;
}
packet->originAddress = origin.address.ipv4.value;
receive(packet);
}
/**
* @brief Device::receive
* @param packet
*/
void Device::receive(std::shared_ptr<Packet> packet)
{
switch (packet->opcode()) {
case OpPoll:
rxArtPoll(std::static_pointer_cast<ArtPoll>(packet));
break;
case OpPollReply:
rxArtPollReply(std::static_pointer_cast<ArtPollReply>(packet));
break;
case OpDiagData:
rxArtDiagData(std::static_pointer_cast<ArtDiagData>(packet));
break;
case OpCommand:
rxArtCommand(std::static_pointer_cast<ArtCommand>(packet));
break;
case OpDmx:
rxArtDmx(std::static_pointer_cast<ArtDmx>(packet));
break;
case OpNzs:
rxArtNzs(std::static_pointer_cast<ArtNzs>(packet));
break;
case OpSync:
rxArtSync(std::static_pointer_cast<ArtSync>(packet));
break;
case OpAddress:
rxArtAddress(std::static_pointer_cast<ArtAddress>(packet));
break;
case OpInput:
rxArtInput(std::static_pointer_cast<ArtInput>(packet));
break;
case OpTodRequest:
rxArtTodRequest(std::static_pointer_cast<ArtTodRequest>(packet));
break;
case OpTodData:
rxArtTodData(std::static_pointer_cast<ArtTodData>(packet));
break;
case OpTodControl:
rxArtTodControl(std::static_pointer_cast<ArtTodControl>(packet));
break;
case OpRdm:
rxArtRdm(std::static_pointer_cast<ArtRdm>(packet));
break;
case OpRdmSub:
rxArtRdmSub(std::static_pointer_cast<ArtRdmSub>(packet));
break;
case OpFirmwareMaster:
rxArtFirmwareMaster(std::static_pointer_cast<ArtFirmwareMaster>(packet));
break;
case OpFirmwareReply:
rxArtFirmwareReply(std::static_pointer_cast<ArtFirmwareReply>(packet));
break;
case OpIpProg:
rxArtIpProg(std::static_pointer_cast<ArtIpProg>(packet));
break;
case OpIpProgReply:
rxArtIpProgReply(std::static_pointer_cast<ArtIpProgReply>(packet));
break;
case OpTimeCode:
rxArtTimeCode(std::static_pointer_cast<ArtTimeCode>(packet));
break;
case OpTrigger:
rxArtTrigger(std::static_pointer_cast<ArtTrigger>(packet));
break;
default:
break;
}
}
/**
* @brief Device::setSender
* @param sender
* @return
*/
std::shared_ptr<void> Device::setSender(const std::function<void(std::shared_ptr<bufferstream>,
ipAddress)> sender)
{
// wrap the callback with a shared pointer
auto sp = std::make_shared<std::function<void(std::shared_ptr<bufferstream>,
ipAddress)>>(std::move(sender));
// store sender function (as a weak pointer)
_sender = sp;
// return token that caller must keep throughout it's scope
return sp;
}
/**
* @brief Device::send
* @param packet
* @param address
*/
void Device::send(const std::shared_ptr<Packet> packet, const ipAddress &address) const
{
auto buffer = std::vector<uint8_t>(packet->streamSize());
auto stream = std::make_shared<bufferstream>(buffer.data(), buffer.size());
packet->oStream(stream);
if (auto sp = _sender.lock()) // the owner is still holding the token
(*sp)(stream, address);
}
/**
* @brief Device::setStatus
* @param status
* @param text
*/
void Device::setReport(const NodeReport status, const std::string &text)
{
_report_code = status;
_report_text = text.substr(Node_Report_Length-8-1); // less required chars and null terminator
}
/**
* @brief Device::rxArtPoll
* @param packet
*/
void Device::rxArtPoll(std::shared_ptr<ArtPoll> packet)
{
auto data_opt = packet->data<poll_data>();
if (!data_opt.has_value())
return;
auto data = data_opt.value();
/// \bug The specification details resolution of diagostice behavior from multiple simultanious
/// controllers. However, the spec does not provide a mechanism for differentiating between
/// controllers. Until OSI layer 3 data is available, LTP the reporting settings.
diagnostic_reporting_behavior = data->talk_to_me;
diagnostic_reporting_threshold = data->diagnostic_level;
/// All device types send an ArtPollReply in response to receiving an ArtPoll.
txArtPollReply();
}
/**
* @brief Device::rxArtPollReply
* @param packet
*/
void Device::rxArtPollReply(std::shared_ptr<ArtPollReply> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtDiagData
* @param packet
*/
void Device::rxArtDiagData(std::shared_ptr<ArtDiagData> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtCommand
* @param packet
*/
void Device::rxArtCommand(std::shared_ptr<ArtCommand> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtDmx
* @param packet
*/
void Device::rxArtDmx(std::shared_ptr<ArtDmx> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtNzs
* @param packet
*/
void Device::rxArtNzs(std::shared_ptr<ArtNzs> packet)
{
if (packet->isVLC())
rxArtVlc(packet);
}
/**
* @brief Device::rxArtVlc
* @param packet
*/
void Device::rxArtVlc(std::shared_ptr<ArtNzs> packet)
{
auto data_opt = packet->data<nzs_data>();
if (!data_opt.has_value())
return;
auto data = std::make_shared<vlc_data>(data_opt.value().get());
}
/**
* @brief Device::rxArtSync
* @param packet
*/
void Device::rxArtSync(std::shared_ptr<ArtSync> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtAddress
* @param packet
*/
void Device::rxArtAddress(std::shared_ptr<ArtAddress> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtInput
* @param packet
*/
void Device::rxArtInput(std::shared_ptr<ArtInput> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtTodRequest
* @param packet
*/
void Device::rxArtTodRequest(std::shared_ptr<ArtTodRequest> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtTodData
* @param packet
*/
void Device::rxArtTodData(std::shared_ptr<ArtTodData> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtTodControl
* @param packet
*/
void Device::rxArtTodControl(std::shared_ptr<ArtTodControl> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtRdm
* @param packet
*/
void Device::rxArtRdm(std::shared_ptr<ArtRdm> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtRdmSub
* @param packet
*/
void Device::rxArtRdmSub(std::shared_ptr<ArtRdmSub> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtFirmwareMaster
* @param packet
*/
void Device::rxArtFirmwareMaster(std::shared_ptr<ArtFirmwareMaster> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtFirmwareReply
* @param packet
*/
void Device::rxArtFirmwareReply(std::shared_ptr<ArtFirmwareReply> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtIpProg
* @param packet
*/
void Device::rxArtIpProg(std::shared_ptr<ArtIpProg> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtIpProgReply
* @param packet
*/
void Device::rxArtIpProgReply(std::shared_ptr<ArtIpProgReply> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtTimeCode
* @param packet
*/
void Device::rxArtTimeCode(std::shared_ptr<ArtTimeCode> packet)
{
(void)packet;
}
/**
* @brief Device::rxArtTrigger
* @param packet
*/
void Device::rxArtTrigger(std::shared_ptr<ArtTrigger> packet)
{
(void)packet;
}
/**
* @brief Device::txArtPollReply
* @param reply
*/
void Device::txArtPollReply(std::shared_ptr<pollreply_data> reply)
{
if (!reply)
reply = std::make_shared<pollreply_data>();
/// \todo complete data field population of ArtPollReply
reply->my_ip = deviceIp().address.ipv4.value;
reply->fw_version = LIB_VERSION >> 16; // only able to send 16 bits, use the highest
// reply->net_sub_switch = ;
reply->status = device_status;
reply->short_name = _shortName;
reply->long_name = _longName;
reply->set_report(_report_code, _poll_reply_count, _report_text);
// reply->num_ports = ;
// reply->port_types = ;
// reply->good_input = ;
// reply->good_output = ;
// reply->SwIn = ;
// reply->SwOut = ;
// reply->SwMacro = ;
// reply->SwRemote = ;
reply->style = styleCode;
const auto mac = deviceMac();
std::copy(mac.cbegin(), mac.cend(), reply->mac_address);
// reply->bind_ip = reply->my_ip;
// reply->bind_index = ;
auto packet = std::make_shared<ArtPollReply>(reply);
send(packet, broadcastIp());
// reset report components
_report_code = RcPowerOk;
_poll_reply_count++;
_report_text = "nominal";
}
/**
* @brief Device::txArtDiagData
*/
void Device::txArtDiagData()
{
/// \todo impliment txArtDiagData
}
/**
* @brief Device::deviceMac
* @return
*
* The default implimentation is to return an invalid MAC
* \note Platform aware device types should return a valid MAC address.
*/
std::array<uint8_t, 6> Device::deviceMac() const
{
return std::array<uint8_t, 6>({0});
}
/**
* @brief Device::deviceIp
* @return
*
* The default implimentation is to return an invalid IP.
* \note Platform aware device types must return a valid IP address.
*/
ipAddress Device::deviceIp() const
{
ipAddress addr;
addr.type = 1; // ACN::SDT::SDT_ADDR_IPV4
addr.port = UDP_PORT;
addr.address.ipv4.value = 0;
return addr;
}
/**
* @brief Device::broadcastIp
* @return
*
* The default implimentation is to return the limited broadcast address.
* \note Platform aware device types must return their directed broadcast address.
*/
ipAddress Device::broadcastIp() const
{
ipAddress addr;
addr.type = 1; // ACN::SDT::SDT_ADDR_IPV4
addr.port = UDP_PORT;
addr.address.ipv4.value = 0xFFFFFFFF;
return addr;
}
/**
* @brief Device::deviceSubnetMask
* @return
*
* The default implimenation is to return an invalid mask.
* \note Platform aware device types must return their valid subnet mask.
*/
uint32_t Device::deviceSubnetMask() const
{
return -1;
}
/**
* @brief Device::deviceHasDHCP
* @return
*
* The default implimentation is to return false, as the default invalid IP is static.
* \note Platform aware device types must return their DHCP status.
*/
bool Device::deviceHasDHCP() const
{
return false;
}
/**
* @brief Device::shortName
* @return
*/
std::string Device::shortName() const
{
return _shortName;
}
/**
* @brief Device::setShortName
* @param newShortName
*/
void Device::setShortName(const std::string &newShortName)
{
_shortName = newShortName.substr(0, Short_Name_Length-1); // leave room for null terminator
_report_code = RcShNameOk;
if (newShortName.length() == _shortName.length())
_report_text = "short name accepted";
else
_report_text = "short name truncated";
}
/**
* @brief Device::longName
* @return
*/
std::string Device::longName() const
{
return _longName;
}
/**
* @brief Device::setLongName
* @param newLongName
*/
void Device::setLongName(const std::string &newLongName)
{
_longName = newLongName.substr(0, Long_Name_Length-1); // leave room for null terminator
_report_code = RcLoNameOk;
if (newLongName.length() == _longName.length())
_report_text = "long name accepted";
else
_report_text = "long name truncated";
}
} // namespace ARTNET

View File

@ -1,108 +0,0 @@
/*
device.h
Copyright (c) 2022 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.
*/
#pragma once
#include "packet.h"
#include <functional>
#include <udp.h>
namespace ARTNET {
using ACN::SDT::UDP::ipAddress;
/**
* @brief The Device class is the base class for ARTNET::Controller
* and ARTNET::Node
*/
class Device
{
public:
explicit Device(Style);
const Style styleCode; //!< \cite ARTNET The Style code defines the equipment style of the device.
void receive(ACN::PDU::Stream, ipAddress&);
void receive(std::shared_ptr<Packet>);
std::shared_ptr<void> setSender(const std::function<void(std::shared_ptr<bufferstream>,
ipAddress)>);
std::string shortName() const;
void setShortName(const std::string &newShortName);
std::string longName() const;
void setLongName(const std::string &newLongName);
protected:
TalkToMe diagnostic_reporting_behavior; //!< behavior flags
Priority diagnostic_reporting_threshold; //!< lowest priority dignostic message to send
GeneralStatus device_status; //!< general status register
void send(const std::shared_ptr<Packet> packet, const ipAddress &) const;
void setReport(const NodeReport, const std::string &);
void rxArtPoll(std::shared_ptr<ArtPoll>);
virtual void rxArtPollReply(std::shared_ptr<ArtPollReply>);
virtual void rxArtDiagData(std::shared_ptr<ArtDiagData>);
virtual void rxArtCommand(std::shared_ptr<ArtCommand>);
virtual void rxArtDmx(std::shared_ptr<ArtDmx>);
virtual void rxArtNzs(std::shared_ptr<ArtNzs>);
virtual void rxArtVlc(std::shared_ptr<ArtNzs>);
virtual void rxArtSync(std::shared_ptr<ArtSync>);
virtual void rxArtAddress(std::shared_ptr<ArtAddress>);
virtual void rxArtInput(std::shared_ptr<ArtInput>);
virtual void rxArtTodRequest(std::shared_ptr<ArtTodRequest>);
virtual void rxArtTodData(std::shared_ptr<ArtTodData>);
virtual void rxArtTodControl(std::shared_ptr<ArtTodControl>);
virtual void rxArtRdm(std::shared_ptr<ArtRdm>);
virtual void rxArtRdmSub(std::shared_ptr<ArtRdmSub>);
virtual void rxArtFirmwareMaster(std::shared_ptr<ArtFirmwareMaster>);
virtual void rxArtFirmwareReply(std::shared_ptr<ArtFirmwareReply>);
virtual void rxArtIpProg(std::shared_ptr<ArtIpProg>);
virtual void rxArtIpProgReply(std::shared_ptr<ArtIpProgReply>);
virtual void rxArtTimeCode(std::shared_ptr<ArtTimeCode>);
virtual void rxArtTrigger(std::shared_ptr<ArtTrigger>);
virtual void txArtPollReply(std::shared_ptr<pollreply_data> = nullptr);
virtual void txArtDiagData();
// OSI layer 2
virtual std::array<uint8_t, 6> deviceMac() const;
// OSI layer 3
virtual ipAddress deviceIp() const;
virtual ipAddress broadcastIp() const;
virtual uint32_t deviceSubnetMask() const;
virtual bool deviceHasDHCP() const;
private:
std::weak_ptr<const std::function<void(std::shared_ptr<bufferstream>, ipAddress)>> _sender;
std::string _shortName;
std::string _longName;
NodeReport _report_code;
uint _poll_reply_count;
std::string _report_text;
};
} // namespace ARTNET

Some files were not shown because too many files have changed in this diff Show More