diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f860252ae752f4255b8d17b6df94e61777816bfa --- /dev/null +++ b/.gitignore @@ -0,0 +1,122 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/qt,qtcreator,c++ +# Edit at https://www.toptal.com/developers/gitignore?templates=qt,qtcreator,c++ + +### C++ ### +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Linker files +*.ilk + +# Debugger Files +*.pdb + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +### Qt ### +# C++ objects and libs +*.so.* + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +### QtCreator ### +# gitignore for Qt Creator like IDE for pure C/C++ project without Qt +# +# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html + + + +# Qt Creator autogenerated files + + +# A listing of all the files included in the project +*.files + +# Include directories +*.includes + +# Project configuration settings like predefined Macros +*.config + +# Qt Creator settings +*.creator + +# User project settings +*.creator.user* + +# Qt Creator backups + +# Flags for Clang Code Model +*.cxxflags +*.cflags + + +# End of https://www.toptal.com/developers/gitignore/api/qt,qtcreator,c++ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..a92b66b036f8fb457ae5eb6acd4b682cec9bc0df --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.17) + +project( + libmueb + VERSION 4.0 + DESCRIPTION "SchĂśnherz MĂĄtrix network library written in C++ using Qt" + LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC OFF) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC OFF) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package( + Qt6 + COMPONENTS Core Gui Network Concurrent + REQUIRED) + +add_subdirectory(src) diff --git a/include/libmueb/libmueb_global.h b/include/libmueb/libmueb_global.h new file mode 100644 index 0000000000000000000000000000000000000000..51696cdc72594eb664ea963c8909c673b17e5f84 --- /dev/null +++ b/include/libmueb/libmueb_global.h @@ -0,0 +1,19 @@ +#ifndef LIBMUEB_LIBMUEB_GLOBAL_H_ +#define LIBMUEB_LIBMUEB_GLOBAL_H_ + +#include <QtCore/qglobal.h> + +#include <QImage> + +namespace libmueb { +// One frame to be transmitted/received. Just an alias for user convenience. +using Frame = QImage; +} // namespace libmueb + +#if defined(LIBMUEB_LIBRARY) +#define LIBMUEB_EXPORT Q_DECL_EXPORT +#else +#define LIBMUEB_EXPORT Q_DECL_IMPORT +#endif + +#endif // LIBMUEB_LIBMUEB_GLOBAL_H_ diff --git a/include/libmueb/muebreceiver.h b/include/libmueb/muebreceiver.h new file mode 100644 index 0000000000000000000000000000000000000000..4c5c0403b0fbf1d30e5c29cf3af995732f5bddce --- /dev/null +++ b/include/libmueb/muebreceiver.h @@ -0,0 +1,31 @@ +#ifndef LIBMUEB_MUEBRECEIVER_H_ +#define LIBMUEB_MUEBRECEIVER_H_ + +#include <QObject> + +#include "libmueb_global.h" + +class MuebReceiverPrivate; + +class LIBMUEB_EXPORT MuebReceiver final : public QObject { + Q_OBJECT + Q_DECLARE_PRIVATE_D(d_ptr_, MuebReceiver) + Q_DISABLE_COPY(MuebReceiver) + + public: + static MuebReceiver& Instance(); + libmueb::Frame frame(); + + signals: + void FrameChanged(libmueb::Frame frame); + + private: + MuebReceiverPrivate* d_ptr_; + + explicit MuebReceiver(QObject* parent = nullptr); + ~MuebReceiver(); + + void readPendingDatagrams(); +}; + +#endif // LIBMUEB_MUEBRECEIVER_H_ diff --git a/include/libmueb/muebtransmitter.h b/include/libmueb/muebtransmitter.h new file mode 100644 index 0000000000000000000000000000000000000000..0316a3bbfb4c70a80bb54b2ba7345edc539b716f --- /dev/null +++ b/include/libmueb/muebtransmitter.h @@ -0,0 +1,34 @@ +#ifndef LIBMUEB_MUEBTRANSMITTER_H_ +#define LIBMUEB_MUEBTRANSMITTER_H_ + +#include <QObject> +#include <cstdint> + +#include "libmueb_global.h" + +class MuebTransmitterPrivate; + +class LIBMUEB_EXPORT MuebTransmitter final : public QObject { + Q_OBJECT + Q_DECLARE_PRIVATE_D(d_ptr_, MuebTransmitter) + Q_DISABLE_COPY(MuebTransmitter) + + public: + void SendFrame(libmueb::Frame frame); + + static MuebTransmitter& Instance(); + + std::int32_t width() const; + + std::int32_t height() const; + + libmueb::Frame frame() const; + + private: + MuebTransmitterPrivate* d_ptr_; + + explicit MuebTransmitter(QObject* parent = nullptr); + ~MuebTransmitter(); +}; + +#endif // LIBMUEB_MUEBTRANSMITTER_H_ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..d698c4297cc83a1d87eff78d2f993baa09439969 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library( + muebtransmitter SHARED + ${CMAKE_SOURCE_DIR}/include/libmueb/libmueb_global.h + ${CMAKE_SOURCE_DIR}/include/libmueb/muebtransmitter.h configuration.h + muebtransmitter.cc configuration.cc) +target_include_directories(muebtransmitter PUBLIC ../include/${PROJECT_NAME}) +target_link_libraries( + muebtransmitter + PUBLIC Qt6::Core Qt6::Gui + PRIVATE Qt6::Network Qt6::Concurrent) +target_compile_definitions(muebtransmitter PRIVATE LIBMUEB_LIBRARY) + +add_library( + muebreceiver SHARED + ${CMAKE_SOURCE_DIR}/include/libmueb/libmueb_global.h + ${CMAKE_SOURCE_DIR}/include/libmueb/muebreceiver.h configuration.h + muebreceiver.cc configuration.cc) +target_include_directories(muebreceiver PUBLIC ../include/${PROJECT_NAME}) +target_link_libraries( + muebreceiver + PUBLIC Qt6::Core Qt6::Gui + PRIVATE Qt6::Network) +target_compile_definitions(muebreceiver PRIVATE LIBMUEB_LIBRARY) diff --git a/src/configuration.cc b/src/configuration.cc new file mode 100644 index 0000000000000000000000000000000000000000..710b17566115ae64ccf98374d8726c2c2eee6b53 --- /dev/null +++ b/src/configuration.cc @@ -0,0 +1,147 @@ +#include "configuration.h" + +#include <cmath> + +Configuration::Configuration(QObject *parent) + : QObject(parent), settings_("matrix-group", "libmueb") { + LoadSettings(); +} + +QImage Configuration::frame() const { return frame_; } + +QHostAddress Configuration::target_address() const { return target_address_; } + +std::uint32_t Configuration::floors() const { return floors_; } + +std::uint32_t Configuration::rooms_per_floor() const { + return rooms_per_floor_; +} + +std::uint32_t Configuration::windows_per_room() const { + return windows_per_room_; +} + +std::uint32_t Configuration::vertical_pixel_unit() const { + return vertical_pixel_unit_; +} + +std::uint32_t Configuration::horizontal_pixel_unit() const { + return horizontal_pixel_unit_; +} + +std::uint32_t Configuration::pixels_per_window() const { + return pixels_per_window_; +} + +std::uint32_t Configuration::window_per_floor() const { + return window_per_floor_; +} + +std::uint32_t Configuration::windows() const { return windows_; } + +std::uint32_t Configuration::pixels() const { return pixels_; } + +std::int32_t Configuration::width() const { return width_; } + +std::int32_t Configuration::height() const { return height_; } + +std::uint8_t Configuration::protocol_type() const { return protocol_type_; } + +std::uint32_t Configuration::window_byte_size() const { + return window_byte_size_; +} + +std::uint32_t Configuration::max_windows_per_datagram() const { + return max_windows_per_datagram_; +} + +std::uint32_t Configuration::packet_header_size() const { + return packet_header_size_; +} + +std::uint32_t Configuration::packet_size() const { return packet_size_; } + +std::uint32_t Configuration::packet_payload_size() const { + return packet_payload_size_; +} + +std::uint32_t Configuration::max_pixel_per_datagram() const { + return max_pixel_per_datagram_; +} + +std::uint32_t Configuration::remainder_packet_size() const { + return remainder_packet_size_; +} + +std::uint16_t Configuration::unicast_animation_port() const { + return unicast_animation_port_; +} + +std::uint16_t Configuration::broadcast_animation_port() const { + return broadcast_animation_port_; +} + +std::uint8_t Configuration::max_packet_number() const { + return max_packet_number_; +} + +std::uint8_t Configuration::color_depth() const { return color_depth_; } + +std::uint8_t Configuration::factor() const { return factor_; } + +bool Configuration::debug_mode() const { return debug_mode_; } + +void Configuration::LoadSettings() { + // Building specific constants + floors_ = settings_.value("floors", 13).toUInt(); + rooms_per_floor_ = settings_.value("rooms_per_floor", 8).toUInt(); + windows_per_room_ = settings_.value("windows_per_room", 2).toUInt(); + + // Hardware specific constants + vertical_pixel_unit_ = settings_.value("vertical_pixel_unit", 2).toUInt(); + horizontal_pixel_unit_ = settings_.value("horizontal_pixel_unit", 2).toUInt(); + color_depth_ = settings_.value("color_depth", 3).toUInt(); + + // Software specific constants + pixels_per_window_ = vertical_pixel_unit_ * horizontal_pixel_unit_; + window_per_floor_ = rooms_per_floor_ * windows_per_room_; + windows_ = floors_ * window_per_floor_; + pixels_ = windows_ * pixels_per_window_; + width_ = window_per_floor_ * horizontal_pixel_unit_; + height_ = floors_ * vertical_pixel_unit_; + factor_ = 8 - color_depth_; + + frame_ = QImage(width_, height_, QImage::Format_RGB888); + frame_.fill(Qt::black); + + // Network protocol specific constants + protocol_type_ = 2; + unicast_animation_port_ = + settings_.value("unicast_animation_port", 3000).toUInt(); + broadcast_animation_port_ = + settings_.value("broadcast_animation_port", 10000).toUInt(); + + // Debug specific constants + // Send packets to localhost + debug_mode_ = settings_.value("debugMode", false).toBool(); + + target_address_ = + (debug_mode_) + ? QHostAddress("127.0.0.1") + : QHostAddress( + settings_.value("target_address", "10.6.255.255").toString()); + window_byte_size_ = (color_depth_ >= 3 && color_depth_ < 5) + ? pixels_per_window_ * 3 / 2 + : pixels_per_window_ * 3; + max_windows_per_datagram_ = + settings_.value("max_windows_per_datagram", windows_).toUInt(); + packet_header_size_ = 2; + packet_size_ = + packet_header_size_ + max_windows_per_datagram_ * window_byte_size_; + packet_payload_size_ = max_windows_per_datagram_ * window_byte_size_; + max_pixel_per_datagram_ = max_windows_per_datagram_ * pixels_per_window_; + max_packet_number_ = static_cast<std::uint32_t>( + std::ceil(static_cast<float>(windows_) / max_windows_per_datagram_)); + + // TODO Configuration check +} diff --git a/src/configuration.h b/src/configuration.h new file mode 100644 index 0000000000000000000000000000000000000000..5156f2c19d2bd89eed15058b7136cf001895df5a --- /dev/null +++ b/src/configuration.h @@ -0,0 +1,107 @@ +#ifndef LIBMUEB_CONFIGURATION_H_ +#define LIBMUEB_CONFIGURATION_H_ + +#include <QHostAddress> +#include <QImage> +#include <QObject> +#include <QSettings> +#include <cstdint> + +// TODO check, remove unused parameters +// TODO check, variable types +class Configuration : public QObject { + Q_OBJECT + + public: + explicit Configuration(QObject *parent = nullptr); + + QImage frame() const; + + QHostAddress target_address() const; + + std::uint32_t floors() const; + + std::uint32_t rooms_per_floor() const; + + std::uint32_t windows_per_room() const; + + std::uint32_t vertical_pixel_unit() const; + + std::uint32_t horizontal_pixel_unit() const; + + std::uint32_t pixels_per_window() const; + + std::uint32_t window_per_floor() const; + + std::uint32_t windows() const; + + std::uint32_t pixels() const; + + std::int32_t width() const; + + std::int32_t height() const; + + std::uint8_t protocol_type() const; + + std::uint32_t window_byte_size() const; + + std::uint32_t max_windows_per_datagram() const; + + std::uint32_t packet_header_size() const; + + std::uint32_t packet_size() const; + + std::uint32_t packet_payload_size() const; + + std::uint32_t max_pixel_per_datagram() const; + + std::uint32_t remainder_packet_size() const; + + std::uint16_t unicast_animation_port() const; + + std::uint16_t broadcast_animation_port() const; + + std::uint8_t max_packet_number() const; + + std::uint8_t color_depth() const; + + std::uint8_t factor() const; + + bool debug_mode() const; + + private: + QImage frame_; + QSettings settings_; + QHostAddress target_address_; + std::uint32_t floors_; + std::uint32_t rooms_per_floor_; + std::uint32_t windows_per_room_; + std::uint32_t vertical_pixel_unit_; + std::uint32_t horizontal_pixel_unit_; + std::uint32_t pixels_per_window_; + std::uint32_t window_per_floor_; + std::uint32_t windows_; + std::uint32_t pixels_; + // Qt width, height is signed + std::int32_t width_; + std::int32_t height_; + // + std::uint8_t protocol_type_; + std::uint32_t window_byte_size_; + std::uint32_t max_windows_per_datagram_; + std::uint32_t packet_header_size_; + std::uint32_t packet_size_; + std::uint32_t packet_payload_size_; + std::uint32_t max_pixel_per_datagram_; + std::uint32_t remainder_packet_size_; + std::uint16_t unicast_animation_port_; + std::uint16_t broadcast_animation_port_; + std::uint8_t max_packet_number_; + std::uint8_t color_depth_; + std::uint8_t factor_; + bool debug_mode_; + + void LoadSettings(); +}; + +#endif // LIBMUEB_CONFIGURATION_H_ diff --git a/src/muebreceiver.cc b/src/muebreceiver.cc new file mode 100644 index 0000000000000000000000000000000000000000..dade9e84f88eaee8d1d97a7368a7ae3028978570 --- /dev/null +++ b/src/muebreceiver.cc @@ -0,0 +1,102 @@ +#include "muebreceiver.h" + +#include <QNetworkDatagram> +#include <QUdpSocket> + +#include "configuration.h" + +class MuebReceiverPrivate { + Q_DECLARE_PUBLIC(MuebReceiver) + Q_DISABLE_COPY(MuebReceiverPrivate) + + public: + explicit MuebReceiverPrivate(MuebReceiver *q) + : frame(configuration.frame()), q_ptr(q) { + socket.bind(configuration.broadcast_animation_port()); + + QObject::connect(&socket, &QUdpSocket::readyRead, q, + &MuebReceiver::readPendingDatagrams); + + qInfo() << "[MuebReceiver] UDP Socket will receive packets on port" + << configuration.broadcast_animation_port(); + } + + Configuration configuration; + libmueb::Frame frame; + QUdpSocket socket; + MuebReceiver *q_ptr; +}; + +MuebReceiver::MuebReceiver(QObject *parent) + : QObject(parent), d_ptr_(new MuebReceiverPrivate(this)) {} + +MuebReceiver::~MuebReceiver() { delete d_ptr_; } + +MuebReceiver &MuebReceiver::Instance() { + static MuebReceiver instance; + + return instance; +} + +libmueb::Frame MuebReceiver::frame() { return d_ptr_->frame; } + +inline void datagram_uncompress_error() { + qWarning() << "[MuebReceiver] Processed packet is invalid! Check the header " + "or packet contents(size)"; +} + +void MuebReceiver::readPendingDatagrams() { + while (d_ptr_->socket.hasPendingDatagrams()) { + if (d_ptr_->socket.pendingDatagramSize() == + d_ptr_->configuration.packet_size()) { + QNetworkDatagram datagram = d_ptr_->socket.receiveDatagram(); + QByteArray data = datagram.data(); + + // Process datagram + // Packet header check + // Check protocol + if (data[0] != d_ptr_->configuration.protocol_type()) { + datagram_uncompress_error(); + return; + } + + auto packet_number = data[1]; + if (packet_number >= d_ptr_->configuration.max_packet_number() || + packet_number < 0) { + datagram_uncompress_error(); + return; + } + + data.remove(0, d_ptr_->configuration.packet_header_size()); + auto frame_begin = + d_ptr_->frame.bits() + + packet_number * d_ptr_->configuration.max_pixel_per_datagram(); + + // Uncompress 1 byte into 2 color components + if (d_ptr_->configuration.color_depth() < 5) { + for (auto i : data) { + *frame_begin = i & 0xf0; + frame_begin++; + *frame_begin = (i & 0x0f) << d_ptr_->configuration.factor(); + frame_begin++; + } + // No compression + } else { + // FIXME use better copy method + for (auto i : data) { + *frame_begin = i; + frame_begin++; + } + } + + emit(FrameChanged(d_ptr_->frame)); + } + // Drop invalid packet + else { + qWarning() << "[MuebReceiver] Packet has invalid size!" + << d_ptr_->socket.pendingDatagramSize() << "bytes"; + + d_ptr_->socket.receiveDatagram(0); + } + } +} diff --git a/src/muebtransmitter.cc b/src/muebtransmitter.cc new file mode 100644 index 0000000000000000000000000000000000000000..7f36005108f3f06d90b5f0fbd8831282b8801b6d --- /dev/null +++ b/src/muebtransmitter.cc @@ -0,0 +1,128 @@ +#include "muebtransmitter.h" + +#include <QByteArray> +#include <QDebug> +#include <QNetworkDatagram> +#include <QUdpSocket> +#include <QtConcurrent> + +#include "configuration.h" + +class MuebTransmitterPrivate { + Q_DECLARE_PUBLIC(MuebTransmitter) + Q_DISABLE_COPY(MuebTransmitterPrivate) + + public: + explicit MuebTransmitterPrivate(MuebTransmitter* q) + : datagram_(QByteArray(), configuration_.target_address(), + configuration_.broadcast_animation_port()), + q_ptr(q) { + qInfo().noquote() << "[MuebTransmitter] UDP Socket will send frame to" + << QString("%1:%2") + .arg(configuration_.target_address().toString()) + .arg(configuration_.broadcast_animation_port()); + } + + void SendFrame(libmueb::Frame frame) { + std::uint8_t packet_number{0}; + + QByteArray reduced_compressed_frame; + // Frame color reduction and compression + if (configuration_.color_depth() < 5) { + reduced_compressed_frame = + QtConcurrent::blockingMappedReduced<QByteArray>( + frame.constBits(), frame.constBits() + frame.sizeInBytes(), + /* Reference: + * http://threadlocalmutex.com/?p=48 + * http://threadlocalmutex.com/?page_id=60 + */ + [this](const uchar& color) -> uchar { + if (configuration_.color_depth() == 3) { + return (color * 225 + 4096) >> 13; + } else if (configuration_.color_depth() == 4) { + return (color * 15 + 135) >> 8; + } + + return color; + }, + [this](QByteArray& compressed_colors, const uchar& color) { + static bool msb{true}; + + // Compress 2 color components into 1 byte + if (msb) { + compressed_colors.append(color << configuration_.factor()); + } else { + compressed_colors.back() = compressed_colors.back() | color; + } + + msb = !msb; + }, + QtConcurrent::OrderedReduce | QtConcurrent::SequentialReduce); + } + // No compression + else { + reduced_compressed_frame.setRawData( + reinterpret_cast<const char*>(frame.bits()), frame.sizeInBytes()); + } + + for (std::uint8_t i = 0; i < configuration_.max_packet_number(); ++i) { + if (configuration_.max_packet_number() == 1) { + reduced_compressed_frame.insert(0, configuration_.protocol_type()) + .insert(1, packet_number); + + datagram_.setData(reduced_compressed_frame); + } else { + QByteArray data; + data.append(configuration_.protocol_type()) + .append(packet_number++) + .append(reduced_compressed_frame.sliced( + i * configuration_.packet_payload_size(), + configuration_.packet_payload_size())); + + datagram_.setData(data); + } + + socket_.writeDatagram(datagram_); + } + } + + Configuration configuration_; + QUdpSocket socket_; + QNetworkDatagram datagram_; + MuebTransmitter* q_ptr; +}; + +MuebTransmitter::MuebTransmitter(QObject* parent) + : QObject(parent), d_ptr_(new MuebTransmitterPrivate(this)) {} + +MuebTransmitter::~MuebTransmitter() { delete d_ptr_; } + +void MuebTransmitter::SendFrame(libmueb::Frame frame) { + if (frame.isNull() || frame.format() == QImage::Format_Invalid || + frame.width() != d_ptr_->configuration_.width() || + frame.height() != d_ptr_->configuration_.height()) { + qWarning() << "[MuebTransmitter] Frame is invalid"; + return; + } + + frame.convertTo(QImage::Format_RGB888); + d_ptr_->SendFrame(frame); +} + +MuebTransmitter& MuebTransmitter::Instance() { + static MuebTransmitter instance; + + return instance; +} + +int32_t MuebTransmitter::width() const { + return d_ptr_->configuration_.width(); +} + +int32_t MuebTransmitter::height() const { + return d_ptr_->configuration_.height(); +} + +libmueb::Frame MuebTransmitter::frame() const { + return d_ptr_->configuration_.frame(); +}