├── .gitignore ├── .gitmodules ├── .vscode └── launch.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build.bat ├── config.yaml └── src ├── msgqueue.cpp ├── msgqueue.h └── screenmqtt.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | build 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/vendor/paho.mqtt.c"] 2 | path = src/vendor/paho.mqtt.c 3 | url = https://github.com/eclipse/paho.mqtt.c.git 4 | [submodule "src/vendor/yaml-cpp"] 5 | path = src/vendor/yaml-cpp 6 | url = https://github.com/jbeder/yaml-cpp.git 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "C++ Launch (Windows)", 6 | "type": "cppvsdbg", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/build.bat", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${workspaceRoot}", 12 | "environment": [], 13 | "externalConsole": false 14 | }, 15 | { 16 | "name": "C++ Attach (Windows)", 17 | "type": "cppvsdbg", 18 | "request": "attach", 19 | "processId": "${command.pickProcess}" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | include(ExternalProject) 3 | 4 | project(screenmqtt VERSION 0.2 LANGUAGES CXX) 5 | 6 | add_definitions( 7 | ) 8 | 9 | set(includes 10 | src/ 11 | src/vendor/paho.mqtt.c/src/ 12 | src/vendor/yaml-cpp/include/ 13 | ) 14 | 15 | file(GLOB sources 16 | src/screenmqtt.cpp 17 | src/msgqueue.cpp 18 | ) 19 | 20 | add_subdirectory(src/vendor/paho.mqtt.c) 21 | add_subdirectory(src/vendor/yaml-cpp) 22 | 23 | add_executable(screenmqtt ${sources}) 24 | target_link_libraries(screenmqtt dxva2 paho-mqtt3c yaml-cpp) 25 | target_include_directories(screenmqtt PRIVATE ${includes}) 26 | 27 | add_custom_command(TARGET screenmqtt POST_BUILD 28 | COMMAND ${CMAKE_COMMAND} -E copy_if_different 29 | "$" 30 | $/paho-mqtt3c.dll) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Miha Lunar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # screenmqtt 2 | 3 | See `config.yaml` on how to configure it. 4 | 5 | The executable looks for it in the current working directory e.g. best if it's in the same directory. -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | cmake -E make_directory build 4 | cmake -E chdir build cmake -E time cmake .. 5 | cmake -E time cmake --build build --config RelWithDebInfo -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # URI of your MQTT broker 2 | broker: tcp://: 3 | 4 | # Username for MQTT (default none) 5 | # username: "batman" 6 | 7 | # Password for MQTT (default none) 8 | # password: "jkrsucks420" 9 | 10 | # Keep alive interval (seconds, default 15) 11 | # keepalive: 20 12 | 13 | # Global prefix for all MQTT topics (default /monitor/) 14 | # prefix: batcave/screens/ 15 | 16 | # Reconnect on connection loss (default true) 17 | # Reconnection works with a random backoff algorithm, starting at 18 | # "backoff min" and exponentially increasing until reaching "backoff max" 19 | # If false, application quits on a failed connection or on connection loss. 20 | # reconnect: false 21 | 22 | # Reconnection minimum interval (milliseconds, default 50) 23 | # backoff min: 10 24 | 25 | # Reconnection maximum interval (milliseconds, default 2000) 26 | # backoff max: 10000 27 | 28 | # Connection timeout (seconds, default 1) 29 | # connect timeout: 3 30 | 31 | # Disconnection timeout (seconds, default 4) 32 | # disconnect timeout: 2 33 | -------------------------------------------------------------------------------- /src/msgqueue.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "msgqueue.h" 9 | 10 | using namespace std::chrono; 11 | 12 | static const MessageQueueOptions DEFAULT_OPTIONS; 13 | 14 | static int logError(int code, std::string message) { 15 | std::cerr << message << " (" << code << ")" << std::endl; 16 | return code; 17 | } 18 | 19 | int MessageQueue::connect() 20 | { 21 | // Cleanup 22 | if (client) { 23 | MQTTClient_destroy(&client); 24 | client = nullptr; 25 | } 26 | 27 | if (!backoff_cur) backoff_cur = config.backoff_min; 28 | 29 | // Setup config 30 | MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer; 31 | if (!config.username.empty()) opts.username = config.username.c_str(); 32 | if (!config.password.empty()) opts.password = config.password.c_str(); 33 | opts.keepAliveInterval = config.keep_alive_interval; 34 | opts.cleansession = true; 35 | opts.connectTimeout = config.connectTimeout; 36 | opts.retryInterval = -1; 37 | 38 | int error; 39 | 40 | // Create client 41 | error = MQTTClient_create( 42 | &client, 43 | config.uri.c_str(), 44 | config.client_id.c_str(), 45 | MQTTCLIENT_PERSISTENCE_NONE, 46 | nullptr 47 | ); 48 | if (error) { 49 | return logError(error, "Unable to create MQTT client"); 50 | } 51 | 52 | // Set callbacks 53 | error = MQTTClient_setCallbacks( 54 | client, 55 | this, 56 | mqtt_disconnected, 57 | mqtt_received, 58 | mqtt_delivered 59 | ); 60 | if (error) { 61 | return logError(error, "Unable to set MQTT callbacks"); 62 | } 63 | 64 | std::cout << "Connecting" << std::endl; 65 | 66 | // Connect to server 67 | error = MQTTClient_connect(client, &opts); 68 | if (!error) { 69 | // If this is a reconnection attempt, we might have previously subscribed to topics, 70 | // so we try to resubscribe to them here. 71 | for (std::pair sub: subscriptions) { 72 | error = MQTTClient_subscribe(client, sub.first.c_str(), sub.second.qos); 73 | if (error) { 74 | std::cout << "Unable to resubscribe" << std::endl; 75 | break; 76 | } 77 | } 78 | } 79 | if (error) { 80 | logError(error, "Unable to connect to server"); 81 | if (config.reconnect) { 82 | return reconnect(); 83 | } else { 84 | return error; 85 | } 86 | } 87 | 88 | backoff_cur = config.backoff_min; 89 | std::cout << std::endl << "Connected" << std::endl << std::endl; 90 | 91 | return 0; 92 | } 93 | 94 | int MessageQueue::reconnect() 95 | { 96 | std::cout << "Reconnecting in " << backoff_cur << " ms" << std::endl; 97 | std::this_thread::sleep_for(milliseconds(backoff_cur)); 98 | backoff_cur += std::rand() % backoff_cur; 99 | backoff_cur = std::min(backoff_cur, config.backoff_max); 100 | return connect(); 101 | } 102 | 103 | int MessageQueue::disconnect() 104 | { 105 | int error; 106 | if (client) { 107 | error = MQTTClient_disconnect(client, config.disconnectTimeout*1000); 108 | if (error) { 109 | return logError(error, "Unable to disconnect from server"); 110 | } 111 | } 112 | MQTTClient_destroy(&client); 113 | return 0; 114 | } 115 | 116 | int MessageQueue::subscribe(std::string topic, const MessageQueueOptions *options) 117 | { 118 | if (!options) options = &DEFAULT_OPTIONS; 119 | 120 | int error; 121 | error = MQTTClient_subscribe(client, topic.c_str(), options->qos); 122 | if (error) { 123 | return logError(error, "Unable to subscribe"); 124 | } 125 | 126 | std::cout << "Subscribed to " << topic << std::endl; 127 | subscriptions.push_back({ topic, *options }); 128 | return 0; 129 | } 130 | 131 | int MessageQueue::publish(std::string topic, std::string payload, const MessageQueueOptions *options, MessageQueueToken *token) 132 | { 133 | if (!options) options = &DEFAULT_OPTIONS; 134 | 135 | int error; 136 | error = MQTTClient_publish( 137 | client, 138 | topic.c_str(), 139 | payload.size(), 140 | &payload[0], 141 | options->qos, 142 | options->retained, 143 | token 144 | ); 145 | if (error) { 146 | return logError(error, "Unable to publish"); 147 | } 148 | return 0; 149 | } 150 | 151 | 152 | void MessageQueue::handleReceived(std::string topic, std::string payload) 153 | { 154 | std::cout << "> " << topic << ": " << payload << std::endl; 155 | if (onReceived) onReceived(topic, payload); 156 | } 157 | 158 | void MessageQueue::handleDelivered(MessageQueueToken token) 159 | { 160 | std::cout << "Delivered: " << token << std::endl; 161 | if (onDelivered) onDelivered(token); 162 | } 163 | 164 | void MessageQueue::handleDisconnected(std::string cause) 165 | { 166 | std::string postfix = cause.empty() ? "" : ": " + cause; 167 | std::cout << "Connection lost" << postfix << std::endl; 168 | if (onDisconnected) onDisconnected(cause); 169 | reconnect(); 170 | } 171 | 172 | 173 | int mqtt_received(void *context, char *topicName, int topicLen, MQTTClient_message *message) { 174 | auto queue = static_cast(context); 175 | 176 | std::string topic, payload; 177 | 178 | if (topicLen) { 179 | topic.resize(topicLen); 180 | memcpy(&topic[0], topicName, topicLen); 181 | } else { 182 | topic = topicName; 183 | } 184 | 185 | if (message->payloadlen) { 186 | payload.resize(message->payloadlen); 187 | memcpy(&payload[0], message->payload, message->payloadlen); 188 | } else { 189 | payload = ""; 190 | } 191 | 192 | queue->handleReceived(topic, payload); 193 | 194 | MQTTClient_freeMessage(&message); 195 | MQTTClient_free(topicName); 196 | return 1; 197 | } 198 | 199 | void mqtt_delivered(void *context, MQTTClient_deliveryToken dt) 200 | { 201 | auto queue = static_cast(context); 202 | queue->handleDelivered(dt); 203 | } 204 | 205 | void mqtt_disconnected(void *context, char *cause) 206 | { 207 | auto queue = static_cast(context); 208 | std::string message = ""; 209 | if (cause) message = cause; 210 | queue->handleDisconnected(message); 211 | } 212 | 213 | -------------------------------------------------------------------------------- /src/msgqueue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "MQTTClient.h" 6 | 7 | typedef int MessageQueueToken; 8 | 9 | struct MessageQueueConfig { 10 | std::string uri; 11 | std::string client_id; 12 | std::string username; 13 | std::string password; 14 | 15 | bool reconnect = true; 16 | 17 | int keep_alive_interval = 15; 18 | 19 | int backoff_min = 50; 20 | int backoff_max = 2000; 21 | 22 | int connectTimeout = 1; 23 | int disconnectTimeout = 5; 24 | }; 25 | 26 | struct MessageQueueOptions { 27 | int qos = 0; 28 | bool retained = false; 29 | }; 30 | 31 | class MessageQueue { 32 | protected: 33 | MQTTClient client = nullptr; 34 | std::vector> subscriptions; 35 | 36 | int backoff_cur = 0; 37 | 38 | void handleReceived(std::string topic, std::string payload); 39 | void handleDelivered(MessageQueueToken token); 40 | void handleDisconnected(std::string cause); 41 | friend int mqtt_received(void *context, char *topicName, int topicLen, MQTTClient_message *message); 42 | friend void mqtt_delivered(void *context, MQTTClient_deliveryToken dt); 43 | friend void mqtt_disconnected(void *context, char *cause); 44 | 45 | int reconnect(); 46 | 47 | public: 48 | MessageQueueConfig config; 49 | 50 | std::function onReceived; 51 | std::function onDelivered; 52 | std::function onDisconnected; 53 | 54 | int connect(); 55 | int disconnect(); 56 | 57 | int subscribe(std::string topic, const MessageQueueOptions *options = nullptr); 58 | int publish(std::string topic, std::string payload, const MessageQueueOptions *options = nullptr, MessageQueueToken *token = nullptr); 59 | }; 60 | -------------------------------------------------------------------------------- /src/screenmqtt.cpp: -------------------------------------------------------------------------------- 1 | #include "stdio.h" 2 | #include "stdlib.h" 3 | #include "string.h" 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #define WIN32_LEAN_AND_MEAN 19 | #include 20 | #include 21 | #include 22 | #include "PhysicalMonitorEnumerationAPI.h" 23 | #include "LowLevelMonitorConfigurationAPI.h" 24 | #include "HighLevelMonitorConfigurationAPI.h" 25 | 26 | #include "yaml-cpp/yaml.h" 27 | 28 | #include "msgqueue.h" 29 | 30 | const std::string name = "screenmqtt 0.4.0"; 31 | 32 | MessageQueueOptions global_options; 33 | 34 | MessageQueue queue; 35 | 36 | std::string global_topic_prefix; 37 | std::mutex mutex; 38 | std::condition_variable cv; 39 | bool done = false; 40 | std::string topic_all_power_state; 41 | std::string topic_all_power_command; 42 | HWND window; 43 | 44 | 45 | void receive_monitors_all(const std::string &payload) { 46 | if (payload == "ON") { 47 | // -1 doesn't seem to work, so send mouse move event instead 48 | INPUT input = { INPUT_MOUSE }; 49 | input.mi.dwFlags = MOUSEEVENTF_MOVE; 50 | SendInput(1, &input, sizeof(INPUT)); 51 | } 52 | else if (payload == "OFF") { 53 | SendMessage(window, WM_SYSCOMMAND, SC_MONITORPOWER, 2); // -1: on, 1: low power, 2: off 54 | } 55 | else { 56 | std::cerr << "Unrecognized power mode " << payload << std::endl; 57 | return; 58 | } 59 | } 60 | 61 | void publish_monitors_all(const std::string &payload) { 62 | queue.publish(topic_all_power_state, payload, &global_options); 63 | } 64 | 65 | 66 | 67 | enum VCPCode { 68 | None = 0, 69 | PowerMode = 0xD6 70 | }; 71 | 72 | struct ParamSource { 73 | DWORD flag; 74 | VCPCode vcp; 75 | std::vector vcp_domain; 76 | }; 77 | 78 | static ParamSource PFlag(DWORD flag) { 79 | return ParamSource{ flag, VCPCode::None }; 80 | } 81 | static ParamSource PVCP(VCPCode vcp) { 82 | return ParamSource{ 0, vcp }; 83 | } 84 | 85 | struct Param { 86 | std::string name; 87 | ParamSource source; 88 | std::function get_state; 89 | std::function set_state; 90 | 91 | HANDLE monitor; 92 | std::string topic_state; 93 | std::string topic_command; 94 | }; 95 | 96 | 97 | std::vector available_params{ 98 | { "power", PVCP(VCPCode::PowerMode), 99 | [](HANDLE monitor) { 100 | MC_VCP_CODE_TYPE vcp_type; 101 | DWORD cur; 102 | DWORD max; 103 | if (!GetVCPFeatureAndVCPFeatureReply( 104 | monitor, 105 | VCPCode::PowerMode, 106 | &vcp_type, 107 | &cur, 108 | &max 109 | )) return std::string("OFF"); 110 | return std::string(cur == 0x01 ? "ON" : "OFF"); 111 | }, 112 | [](HANDLE monitor, std::string payload) { 113 | uint8_t power; 114 | if (payload == "ON") power = 0x01; 115 | else if (payload == "OFF") power = 0x04; 116 | else { 117 | std::cerr << "Unrecognized power mode " << payload << std::endl; 118 | return; 119 | } 120 | 121 | if (!SetVCPFeature( 122 | monitor, 123 | VCPCode::PowerMode, 124 | power 125 | )) std::cerr << "Unable to set power mode to " << (int)power << std::endl; 126 | } 127 | }, 128 | { "brightness", PFlag(MC_CAPS_BRIGHTNESS), 129 | [](HANDLE monitor) { 130 | DWORD min; 131 | DWORD cur; 132 | DWORD max; 133 | if (!GetMonitorBrightness( 134 | monitor, &min, &cur, &max 135 | )) return std::string("-1"); 136 | std::ostringstream stream; 137 | stream << cur*0xFF/100; 138 | return stream.str(); 139 | }, 140 | [](HANDLE monitor, std::string payload) { 141 | int brightness = std::stoi(payload)*100/0xFF; 142 | if (!SetMonitorBrightness(monitor, brightness)) { 143 | std::cerr << "Unable to set brightness to " << brightness << std::endl; 144 | } 145 | }, 146 | }, 147 | { "temperature", PFlag(MC_CAPS_COLOR_TEMPERATURE), 148 | [](HANDLE monitor) { 149 | MC_COLOR_TEMPERATURE temp; 150 | if (!GetMonitorColorTemperature( 151 | monitor, &temp 152 | )) return std::string("-3"); 153 | 154 | int kelvin = -2; 155 | 156 | switch (temp) { 157 | case MC_COLOR_TEMPERATURE_UNKNOWN: kelvin = -1; break; 158 | case MC_COLOR_TEMPERATURE_4000K: kelvin = 4000; break; 159 | case MC_COLOR_TEMPERATURE_5000K: kelvin = 5000; break; 160 | case MC_COLOR_TEMPERATURE_6500K: kelvin = 6500; break; 161 | case MC_COLOR_TEMPERATURE_7500K: kelvin = 7500; break; 162 | case MC_COLOR_TEMPERATURE_8200K: kelvin = 8200; break; 163 | case MC_COLOR_TEMPERATURE_9300K: kelvin = 9300; break; 164 | case MC_COLOR_TEMPERATURE_10000K: kelvin = 10000; break; 165 | case MC_COLOR_TEMPERATURE_11500K: kelvin = 11500; break; 166 | } 167 | 168 | int mired = 1000000 / kelvin; 169 | 170 | std::ostringstream stream; 171 | stream << mired; 172 | return stream.str(); 173 | }, 174 | [](HANDLE monitor, std::string payload) { 175 | int mired = std::stoi(payload); 176 | int kelvin = 1000000 / mired; 177 | 178 | MC_COLOR_TEMPERATURE temp; 179 | 180 | if (kelvin < 0) temp = MC_COLOR_TEMPERATURE_UNKNOWN; 181 | else if (kelvin < 4500) temp = MC_COLOR_TEMPERATURE_4000K; 182 | else if (kelvin < 5750) temp = MC_COLOR_TEMPERATURE_5000K; 183 | else if (kelvin < 7000) temp = MC_COLOR_TEMPERATURE_6500K; 184 | else if (kelvin < 7850) temp = MC_COLOR_TEMPERATURE_7500K; 185 | else if (kelvin < 8750) temp = MC_COLOR_TEMPERATURE_8200K; 186 | else if (kelvin < 9300) temp = MC_COLOR_TEMPERATURE_9300K; 187 | else if (kelvin < 9650) temp = MC_COLOR_TEMPERATURE_10000K; 188 | else temp = MC_COLOR_TEMPERATURE_11500K; 189 | 190 | if (temp == MC_COLOR_TEMPERATURE_UNKNOWN) return; 191 | 192 | if (!SetMonitorColorTemperature(monitor, temp)) { 193 | std::cerr << "Unable to set color temperature to " << temp << std::endl; 194 | } 195 | } 196 | }, 197 | { "contrast", PFlag(MC_CAPS_CONTRAST), 198 | [](HANDLE monitor) { 199 | DWORD min; 200 | DWORD cur; 201 | DWORD max; 202 | if (!GetMonitorContrast( 203 | monitor, &min, &cur, &max 204 | )) return std::string("-1"); 205 | std::ostringstream stream; 206 | stream << cur * 0xFF / 100; 207 | return stream.str(); 208 | }, 209 | [](HANDLE monitor, std::string payload) { 210 | int contrast = std::stoi(payload) * 100 / 0xFF; 211 | if (!SetMonitorContrast(monitor, contrast)) { 212 | std::cerr << "Unable to set contrast to " << contrast << std::endl; 213 | } 214 | } 215 | }, 216 | { "degauss", PFlag(MC_CAPS_DEGAUSS) }, 217 | { "technology", PFlag(MC_CAPS_MONITOR_TECHNOLOGY_TYPE), 218 | [](HANDLE monitor) { 219 | MC_DISPLAY_TECHNOLOGY_TYPE type; 220 | 221 | if (!GetMonitorTechnologyType( 222 | monitor, &type 223 | )) return std::string("N/A"); 224 | 225 | switch (type) 226 | { 227 | 228 | case MC_SHADOW_MASK_CATHODE_RAY_TUBE: 229 | return std::string("Shadow Mask Cathode Ray Tube (CRT)"); 230 | 231 | case MC_APERTURE_GRILL_CATHODE_RAY_TUBE: 232 | return std::string("Aperture Grill Cathode Ray Tube (CRT)"); 233 | 234 | case MC_THIN_FILM_TRANSISTOR: 235 | return std::string("Thin Film Transistor (TFT)"); 236 | 237 | case MC_LIQUID_CRYSTAL_ON_SILICON: 238 | return std::string("Liquid Crystal on Silicon (LCoS)"); 239 | 240 | case MC_PLASMA: 241 | return std::string("Plasma"); 242 | 243 | case MC_ORGANIC_LIGHT_EMITTING_DIODE: 244 | return std::string("Organic Light Emitting Diode (OLED)"); 245 | 246 | case MC_ELECTROLUMINESCENT: 247 | return std::string("Electroluminescent (ELD)"); 248 | 249 | case MC_MICROELECTROMECHANICAL: 250 | return std::string("Microelectromechanical"); 251 | 252 | case MC_FIELD_EMISSION_DEVICE: 253 | return std::string("Field Emission Display (FED)"); 254 | 255 | } 256 | return std::string("N/A"); 257 | } 258 | }, 259 | { "rgb-drive", PFlag(MC_CAPS_RED_GREEN_BLUE_DRIVE) }, 260 | { "rgb-gain", PFlag(MC_CAPS_RED_GREEN_BLUE_GAIN) } 261 | }; 262 | 263 | struct Cap { 264 | std::string name; 265 | std::vector list; 266 | }; 267 | 268 | static bool is_whitespace(const std::string &str) 269 | { 270 | for (std::string::const_iterator it = str.begin(); it != str.end(); ++it) 271 | { 272 | if (*it != ' ' && 273 | *it != '\t' && 274 | *it != '\r' && 275 | *it != '\n') return false; 276 | } 277 | return true; 278 | } 279 | 280 | static Cap parse_capstring(const char **pos) { 281 | Cap cap; 282 | std::string str; 283 | while (**pos > 0) { 284 | switch (**pos) { 285 | case '(': 286 | { 287 | (*pos)++; 288 | Cap sub = parse_capstring(pos); 289 | if (**pos != ')') goto break_loop; 290 | sub.name = str; 291 | str.clear(); 292 | cap.list.push_back(sub); 293 | } 294 | break; 295 | case ')': 296 | cap.list.push_back(Cap{str}); 297 | goto break_loop; 298 | case ' ': 299 | case '\t': 300 | case '\r': 301 | case '\n': 302 | if (!is_whitespace(str)) { 303 | cap.list.push_back(Cap{str}); 304 | } 305 | str.clear(); 306 | break; 307 | default: 308 | str += **pos; 309 | } 310 | (*pos)++; 311 | } 312 | break_loop: 313 | return cap; 314 | } 315 | 316 | static void dump_cap(const Cap &cap, int level = 0) { 317 | for (size_t i = 0; i < cap.list.size(); i++) { 318 | std::cout << std::string(level*2, ' ') << i << ": " << cap.list[i].name << std::endl; 319 | dump_cap(cap.list[i], level + 1); 320 | } 321 | } 322 | 323 | static Cap parse_capabilities(const char *capabilities) { 324 | const char *pos = capabilities; 325 | if (*pos != '(') return Cap{}; 326 | pos++; 327 | Cap cap = parse_capstring(&pos); 328 | if (*pos != ')') return Cap{}; 329 | return cap; 330 | } 331 | 332 | struct Screen { 333 | int index; 334 | HANDLE monitor; 335 | std::wstring desc; 336 | 337 | DWORD cap_flags; 338 | DWORD cap_color_temp; 339 | Cap capabilities; 340 | std::string model; 341 | std::vector supported_params; 342 | 343 | MessageQueue *queue; 344 | std::string topic_prefix; 345 | int qos; 346 | 347 | Screen(int index, HANDLE monitor, std::wstring desc) : index(index), monitor(monitor), desc(desc) { 348 | updateInfo(); 349 | }; 350 | ~Screen() { 351 | DestroyPhysicalMonitor(monitor); 352 | monitor = nullptr; 353 | } 354 | 355 | void printError(const char *msg) { 356 | DWORD lastError = GetLastError(); 357 | wchar_t lastErrorMsg[1024]; 358 | bool lastErrorValid = FormatMessageW( 359 | FORMAT_MESSAGE_FROM_SYSTEM, 360 | NULL, 361 | lastError, 362 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 363 | lastErrorMsg, 364 | sizeof(lastErrorMsg), 365 | NULL 366 | ) > 0; 367 | std::wcerr << index << ": " << desc << ": " << msg; 368 | if (lastErrorValid) std::wcerr << ": " << lastErrorMsg; 369 | std::wcerr << " (" << GetLastError() << ")" << std::endl; 370 | } 371 | 372 | void updateInfo() { 373 | std::wcout << index << ": " << desc << " (" << monitor << ")" << std::endl; 374 | 375 | { 376 | printf(" flags\n"); 377 | if (!GetMonitorCapabilities( 378 | monitor, 379 | &cap_flags, 380 | &cap_color_temp 381 | )) { 382 | printError("Error updating flags"); 383 | return; 384 | } 385 | 386 | for (auto param : available_params) { 387 | if (cap_flags & param.source.flag) { 388 | std::cout << " " << param.name << std::endl; 389 | supported_params.push_back(param); 390 | } 391 | } 392 | } 393 | 394 | { 395 | printf(" low level\n"); 396 | printf(" length\n"); 397 | DWORD cap_len; 398 | if (!GetCapabilitiesStringLength(monitor, &cap_len)) { 399 | printError("Error getting length"); 400 | return; 401 | } 402 | std::string cap; 403 | cap.resize(cap_len); 404 | 405 | printf(" request\n"); 406 | if (!CapabilitiesRequestAndCapabilitiesReply( 407 | monitor, 408 | (LPSTR)&cap[0], 409 | cap_len 410 | )) { 411 | printError("Request failed"); 412 | return; 413 | } 414 | 415 | printf(" parse\n"); 416 | capabilities = parse_capabilities(&cap[0]); 417 | 418 | for (Cap cap_main : capabilities.list) { 419 | if (cap_main.name == "model") { 420 | if (cap_main.list.size() > 0) { 421 | model = cap_main.list[0].name; 422 | } 423 | } 424 | } 425 | printf(" model\n"); 426 | std::cout << " " << model << std::endl; 427 | 428 | for (Cap cap_main : capabilities.list) { 429 | if (cap_main.name == "vcp") { 430 | for (Cap vcp : cap_main.list) { 431 | if (vcp.name.empty()) continue; 432 | uint8_t vcp_code = std::stoi(vcp.name, 0, 16); 433 | for (auto param : available_params) { 434 | if (param.source.vcp == vcp_code) { 435 | std::cout << " " << param.name << std::endl; 436 | supported_params.push_back(param); 437 | Param &added = supported_params.back(); 438 | for (auto cap_domain : vcp.list) { 439 | uint8_t dom_code = std::stoi(cap_domain.name, 0, 16); 440 | std::cout << " " << (int)dom_code << std::endl; 441 | param.source.vcp_domain.push_back(dom_code); 442 | } 443 | } 444 | } 445 | } 446 | } 447 | } 448 | 449 | // Uncomment to print all the capabilities 450 | //dump_cap(capabilities); 451 | } 452 | 453 | printf(" done\n\n"); 454 | } 455 | 456 | void publish(const Param ¶m, std::string state_override = "") 457 | { 458 | std::string state = state_override.empty() ? param.get_state(monitor) : state_override; 459 | printf("< %s: %s\n", param.topic_state.c_str(), state.c_str()); 460 | queue->publish(param.topic_state, state, &global_options); 461 | } 462 | 463 | void publishAll() 464 | { 465 | for (auto ¶m : supported_params) { 466 | if (!param.topic_state.empty()) { 467 | publish(param); 468 | } 469 | } 470 | } 471 | 472 | bool receive(const std::string &topic, const std::string &payload) 473 | { 474 | for (auto ¶m : supported_params) { 475 | if (param.topic_command == topic) { 476 | param.set_state(monitor, payload); 477 | publish(param, payload); 478 | // Maybe update the real state in a bit? 479 | //std::this_thread::sleep_for(std::chrono::milliseconds(2000)); 480 | //publish(param); 481 | return true; 482 | } 483 | } 484 | return false; 485 | } 486 | 487 | void connect(MessageQueue *queue) 488 | { 489 | if (queue == nullptr) return; 490 | 491 | this->queue = queue; 492 | this->topic_prefix = global_topic_prefix + model + "/"; 493 | 494 | for (auto ¶m : supported_params) { 495 | if (param.get_state) { 496 | param.topic_state = topic_prefix + param.name + "/state"; 497 | std::cout << "Publishing to " << param.topic_state << std::endl; 498 | } 499 | if (param.set_state) { 500 | param.topic_command = topic_prefix + param.name + "/command"; 501 | queue->subscribe(param.topic_command); 502 | } 503 | } 504 | publishAll(); 505 | } 506 | 507 | }; 508 | 509 | std::vector> screens; 510 | 511 | // Might be useful, but not right now. 512 | /* 513 | bool get_monitor_device(int index, DISPLAY_DEVICE &device) { 514 | bool valid = true; 515 | 516 | ZeroMemory(&device, sizeof(DISPLAY_DEVICE)); 517 | device.cb = sizeof(DISPLAY_DEVICE); 518 | 519 | if (EnumDisplayDevices(NULL, index, &device, 0)) { 520 | 521 | CHAR deviceName[32]; 522 | // DeviceName is first the gpu name 523 | lstrcpy(deviceName, device.DeviceName); 524 | 525 | std::cout << "device " << deviceName << std::endl; 526 | 527 | // DeviceName is now the monitor name 528 | EnumDisplayDevices(deviceName, 0, &device, 0); 529 | } 530 | else { 531 | valid = false; 532 | } 533 | 534 | return valid; 535 | } 536 | //*/ 537 | 538 | BOOL CALLBACK EnumDisplayCallback( 539 | _In_ HMONITOR hMonitor, 540 | _In_ HDC hdcMonitor, 541 | _In_ LPRECT lprcMonitor, 542 | _In_ LPARAM dwData 543 | ) { 544 | DWORD physical_num; 545 | 546 | if (!GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, &physical_num)) { 547 | std::cerr << "Unable to get number of physical monitors" << std::endl; 548 | return true; 549 | } 550 | 551 | std::vector monitors; 552 | 553 | monitors.resize(physical_num); 554 | 555 | bool success = GetPhysicalMonitorsFromHMONITOR(hMonitor, physical_num, &monitors[0]) == TRUE; 556 | if (!success) { 557 | std::cerr << "Unable to get physical monitors" << std::endl; 558 | return true; 559 | } 560 | 561 | for (DWORD i = 0; i < physical_num; i++) { 562 | PHYSICAL_MONITOR *mon = &monitors[i]; 563 | 564 | std::shared_ptr ptr = std::make_shared( 565 | screens.size(), 566 | mon->hPhysicalMonitor, 567 | std::wstring(mon->szPhysicalMonitorDescription) 568 | ); 569 | Screen &screen = *ptr; 570 | screens.push_back(ptr); 571 | } 572 | 573 | return true; 574 | } 575 | 576 | 577 | 578 | 579 | void receive_monitors(const std::string &topic, const std::string &payload) { 580 | if (topic == topic_all_power_command) { 581 | receive_monitors_all(payload); 582 | return; 583 | } 584 | for (auto ptr : screens) { 585 | Screen &screen = *ptr; 586 | bool handled = screen.receive(topic, payload); 587 | if (handled) break; 588 | } 589 | } 590 | 591 | void connect_monitors() { 592 | for (auto ptr : screens) { 593 | Screen &screen = *ptr; 594 | screen.connect(&queue); 595 | } 596 | } 597 | 598 | void update_monitors() { 599 | 600 | /* Might be useful in a bit. 601 | DISPLAY_DEVICE monitor; 602 | for (int i = 0; get_monitor_device(i, monitor); i++) { 603 | std::cout << i << ":" << std::endl; 604 | std::cout << monitor.DeviceID << std::endl; 605 | std::cout << monitor.DeviceKey << std::endl; 606 | std::cout << monitor.DeviceName << std::endl; 607 | std::cout << monitor.DeviceString << std::endl; 608 | std::cout << monitor.StateFlags << std::endl; 609 | } 610 | */ 611 | 612 | screens.clear(); 613 | if (!EnumDisplayMonitors(NULL, NULL, EnumDisplayCallback, 0)) { 614 | std::cerr << "Unable to enumerate display monitors" << std::endl; 615 | } 616 | connect_monitors(); 617 | } 618 | 619 | LRESULT CALLBACK WindowProcedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) 620 | { 621 | switch (msg) 622 | { 623 | case WM_DISPLAYCHANGE: 624 | std::cout << "Detected display change " << wp << " " << lp << std::endl; 625 | update_monitors(); 626 | return 0L; 627 | case WM_POWERBROADCAST: 628 | switch (wp) { 629 | case PBT_POWERSETTINGCHANGE: 630 | auto setting = (POWERBROADCAST_SETTING*)lp; 631 | if (setting->PowerSetting == GUID_MONITOR_POWER_ON) { 632 | DWORD state = *(DWORD*)&setting->Data[0]; 633 | publish_monitors_all(state == 1 ? "ON" : "OFF"); 634 | } 635 | // This is the newer API to use, but that just seems to make it less compatible? 636 | /* else if (setting->PowerSetting == GUID_CONSOLE_DISPLAY_STATE) { 637 | DWORD state = *(DWORD*)&setting->Data[0]; 638 | std::cout << "GUID_CONSOLE_DISPLAY_STATE " << state << std::endl; 639 | }*/ 640 | break; 641 | } 642 | 643 | return 0L; 644 | case WM_DESTROY: 645 | std::cout << "\nDestroying window\n"; 646 | PostQuitMessage(0); 647 | return 0L; 648 | default: 649 | return DefWindowProc(window, msg, wp, lp); 650 | } 651 | } 652 | 653 | void windows_run() { 654 | update_monitors(); 655 | WNDCLASSEX wndclass = { 656 | sizeof(WNDCLASSEX), 0, WindowProcedure, 657 | 0, 0, GetModuleHandle(0), nullptr, 658 | nullptr, nullptr, 659 | 0, name.c_str(), nullptr }; 660 | if (RegisterClassEx(&wndclass)) 661 | { 662 | window = CreateWindowEx(0, name.c_str(), "Spooky hidden ghost title!", 663 | WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 664 | CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, GetModuleHandle(0), 0); 665 | if (window) 666 | { 667 | MSG msg; 668 | RegisterPowerSettingNotification(window, &GUID_MONITOR_POWER_ON, 0); 669 | // Alternative newer API 670 | //RegisterPowerSettingNotification(window, &GUID_CONSOLE_DISPLAY_STATE, 0); 671 | while (GetMessage(&msg, 0, 0, 0)) DispatchMessage(&msg); 672 | } 673 | } 674 | } 675 | 676 | std::string get_default_topic_prefix() { 677 | std::string topic_prefix = "pc/"; 678 | char comp_name[MAX_COMPUTERNAME_LENGTH]; 679 | DWORD comp_name_len = MAX_COMPUTERNAME_LENGTH; 680 | if (GetComputerName(comp_name, &comp_name_len) != 0) { 681 | topic_prefix.resize(comp_name_len); 682 | memcpy(&topic_prefix[0], comp_name, comp_name_len); 683 | std::transform(topic_prefix.begin(), topic_prefix.end(), topic_prefix.begin(), ::tolower); 684 | topic_prefix += "/"; 685 | } 686 | topic_prefix += "monitor/"; 687 | return topic_prefix; 688 | } 689 | 690 | template 691 | bool initOptionSilent(const YAML::Node &yaml, std::string prop, std::string name, T* config) { 692 | auto value = yaml[prop]; 693 | if (value) { 694 | *config = value.as(); 695 | return true; 696 | } 697 | return false; 698 | } 699 | 700 | template 701 | bool initOption(const YAML::Node &yaml, std::string prop, std::string name, T* config) { 702 | bool exists = initOptionSilent(yaml, prop, name, config); 703 | if (exists) std::cout << name << ": " << *config << std::boolalpha << std::endl; 704 | return exists; 705 | } 706 | 707 | template 708 | bool initOptionHidden(const YAML::Node &yaml, std::string prop, std::string name, T* config) { 709 | bool exists = initOptionSilent(yaml, prop, name, config); 710 | if (exists) std::cout << name << ": *****" << std::endl; 711 | return exists; 712 | } 713 | 714 | void queueDisconnected(std::string cause) { 715 | if (!queue.config.reconnect) exit(3); 716 | } 717 | 718 | void queueReceived(std::string topic, std::string payload) { 719 | receive_monitors(topic, payload); 720 | } 721 | 722 | int main(int argc, char *argv[]) { 723 | 724 | std::cout << name << "\n\n"; 725 | 726 | global_options.retained = true; 727 | 728 | queue.config.client_id = name; 729 | queue.onReceived = std::bind(&queueReceived, std::placeholders::_1, std::placeholders::_2); 730 | queue.onDisconnected = std::bind(&queueDisconnected, std::placeholders::_1); 731 | 732 | global_topic_prefix = get_default_topic_prefix(); 733 | 734 | try { 735 | YAML::Node yaml = YAML::LoadFile("config.yaml"); 736 | 737 | auto broker = yaml["broker"]; 738 | if (!broker) throw "'broker' not found"; 739 | queue.config.uri = broker.as(); 740 | std::cout << "Broker: " << queue.config.uri << std::endl; 741 | 742 | initOption(yaml, "username", "Username", &queue.config.username); 743 | initOptionHidden(yaml, "password", "Password", &queue.config.password); 744 | initOption(yaml, "keepalive", "Keep alive", &queue.config.keep_alive_interval); 745 | initOption(yaml, "prefix", "Topic prefix", &global_topic_prefix); 746 | initOption(yaml, "reconnect", "Reconnect", &queue.config.reconnect); 747 | initOption(yaml, "connect timeout", "Connect timeout", &queue.config.connectTimeout); 748 | initOption(yaml, "disconnect timeout", "Disconnect timeout", &queue.config.disconnectTimeout); 749 | initOption(yaml, "backoff min", "Backoff minimum", &queue.config.backoff_min); 750 | initOption(yaml, "backoff max", "Backoff maximum", &queue.config.backoff_max); 751 | 752 | int error = queue.connect(); 753 | if (error) { 754 | return 3; 755 | } 756 | 757 | } catch (YAML::BadFile badFile) { 758 | std::cerr << "Error: config.yaml not found (" << badFile.msg << ")" << std::endl; 759 | return 1; 760 | } catch (const char* msg) { 761 | std::cerr << "Error: " << msg << std::endl; 762 | return 2; 763 | } 764 | 765 | std::string topic_prefix_all = global_topic_prefix + "all/"; 766 | topic_all_power_state = topic_prefix_all + "power/state"; 767 | topic_all_power_command = topic_prefix_all + "power/command"; 768 | std::cout << "Publishing to " << topic_all_power_state << std::endl; 769 | queue.subscribe(topic_all_power_command); 770 | 771 | std::thread windows_thread(windows_run); 772 | 773 | int ch; 774 | do 775 | { 776 | ch = _getch(); 777 | } while (ch != 'Q' && ch != 'q' && ch != 3 /* Ctrl-C */); 778 | 779 | { 780 | std::lock_guard lock(mutex); 781 | done = true; 782 | } 783 | cv.notify_all(); 784 | 785 | windows_thread.detach(); 786 | 787 | queue.disconnect(); 788 | 789 | return 0; 790 | } --------------------------------------------------------------------------------