diff --git a/README.md b/README.md index efb4226..e91846a 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Topics include the hostname to support multiple PCs: | Topic | Type | Description | |-------|------|-------------| | `pc-control//sleep` | Command | Put PC to sleep (send `1`, `true`, `on`, or `yes`) | -| `pc-control//monitor-off` | Command | Turn off monitor (send `1`, `true`, `on`, or `yes`) | +| `pc-control//monitor-off` | Command | Turn off monitor (empty payload is accepted; `1`, `true`, `on`, or `yes` are also accepted) | | `pc-control//status` | Status | `online` / `offline` (retained) | | `pc-control//version` | Info | Version string, e.g. `1.0.0` (retained) | @@ -155,6 +155,9 @@ mosquitto_pub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/desktop-pc # Turn off monitor mosquitto_pub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/desktop-pc/monitor-off" -m "1" +# Turn off monitor with an empty trigger payload +mosquitto_pub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/desktop-pc/monitor-off" -n + # Check status (subscribe) mosquitto_sub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/+/status" -v ``` @@ -194,6 +197,7 @@ The project uses GitHub Actions to build and release automatically. | Version | Date | Notes | |---------|------------|---------------------------------------------------------------------------------------------------------------| +| 1.1.1 | 09.06.2026 | Accept empty MQTT payload as a trigger for `monitor-off` and improve diagnostic logging. | | 1.1.0 | 02.03.2026 | Add new, separate "invisible" executable and statically link paho-mqtt, so only 1 file is required for distribution | | 1.0.1 | 02.02.2026 | Add logging to file, improved Home Assistant support, status topic (`online`/`offline`) via LWT, monitor off command. | | 1.0.0 | 01.02.2026 | Initial release | diff --git a/src/main.c b/src/main.c index 96b10f1..8ad0810 100644 --- a/src/main.c +++ b/src/main.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +72,17 @@ static void log_action(const char *action) { } } +static void log_actionf(const char *fmt, ...) { + char msg[1024]; + va_list args; + + va_start(args, fmt); + vsnprintf(msg, sizeof(msg), fmt, args); + va_end(args); + + log_action(msg); +} + static void log_windows_error(const char *action, DWORD error_code) { char msg[256]; @@ -90,6 +102,40 @@ static void log_startup(void) { log_action(msg); } +static void get_user_object_name(HANDLE handle, char *buf, size_t buf_size) { + DWORD needed = 0; + + if (buf_size == 0) { + return; + } + + if (handle == NULL || + !GetUserObjectInformationA(handle, UOI_NAME, buf, (DWORD)buf_size, &needed)) { + snprintf(buf, buf_size, ""); + } +} + +static void log_runtime_config(const char *address, const char *hostname) { + DWORD pid = GetCurrentProcessId(); + DWORD session_id = 0; + char window_station[128]; + char desktop[128]; + + if (!ProcessIdToSessionId(pid, &session_id)) { + session_id = (DWORD)-1; + } + + get_user_object_name(GetProcessWindowStation(), window_station, sizeof(window_station)); + get_user_object_name(GetThreadDesktop(GetCurrentThreadId()), desktop, sizeof(desktop)); + + log_actionf("Process context: pid=%lu, session_id=%lu, window_station=%s, desktop=%s", + (unsigned long)pid, (unsigned long)session_id, window_station, desktop); + log_actionf("Runtime config: address=%s, hostname=%s, client_id=%s", + address, hostname, client_id); + log_actionf("Topics: sleep=%s, monitor_off=%s, status=%s, version=%s", + topic_sleep, topic_monitor_off, topic_status, topic_version); +} + static void sanitize_hostname(char *dest, const char *src, size_t dest_size) { size_t j = 0; for (size_t i = 0; src[i] && j < dest_size - 1; i++) { @@ -116,6 +162,8 @@ static void do_sleep(void) { log_action("SLEEP command received - entering sleep mode"); if (!SetSuspendState(FALSE, FALSE, FALSE)) { log_windows_error("SetSuspendState", GetLastError()); + } else { + log_action("SetSuspendState returned success"); } } @@ -127,6 +175,9 @@ static void do_monitor_off(void) { if (SendMessageTimeout(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)2, SMTO_ABORTIFHUNG, MONITOR_POWER_TIMEOUT_MS, &result) == 0) { log_windows_error("SendMessageTimeout(SC_MONITORPOWER)", GetLastError()); + } else { + log_actionf("SendMessageTimeout(SC_MONITORPOWER) returned success, result=%lu", + (unsigned long)result); } } @@ -184,19 +235,96 @@ static int is_valid_command_payload(const MQTTClient_message *msg) { payload_token_equals(payload + start, end - start, "yes"); } -static int should_accept_command_message(const MQTTClient_message *msg) { - return msg != NULL && !msg->retained && is_valid_command_payload(msg); +static int is_empty_payload(const MQTTClient_message *msg) { + return msg == NULL || msg->payload == NULL || msg->payloadlen <= 0; +} + +static void format_payload_preview(const MQTTClient_message *msg, char *buf, size_t buf_size) { + const unsigned char *payload; + size_t len; + size_t out = 0; + + if (buf_size == 0) { + return; + } + + if (msg == NULL || msg->payload == NULL || msg->payloadlen <= 0) { + snprintf(buf, buf_size, ""); + return; + } + + payload = (const unsigned char *)msg->payload; + len = (size_t)msg->payloadlen; + + for (size_t i = 0; i < len && out + 1 < buf_size; i++) { + unsigned char c = payload[i]; + + if (c == '\r' || c == '\n' || c == '\t') { + if (out + 2 >= buf_size) { + break; + } + buf[out++] = ' '; + } else if (isprint(c)) { + buf[out++] = (char)c; + } else { + buf[out++] = '.'; + } + } + + if (len > out && out + 4 < buf_size) { + buf[out++] = '.'; + buf[out++] = '.'; + buf[out++] = '.'; + } + + buf[out] = '\0'; } static int message_arrived(void *context, char *topic, int topic_len, MQTTClient_message *msg) { (void)context; (void)topic_len; - if (should_accept_command_message(msg)) { + char payload_preview[128]; + int retained = msg != NULL ? msg->retained : 0; + int qos = msg != NULL ? msg->qos : 0; + int dup = msg != NULL ? msg->dup : 0; + int payloadlen = msg != NULL ? msg->payloadlen : 0; + + format_payload_preview(msg, payload_preview, sizeof(payload_preview)); + log_actionf("MQTT message arrived: topic=%s, payload_len=%d, qos=%d, retained=%d, dup=%d, payload=\"%s\"", + topic ? topic : "", payloadlen, qos, retained, dup, payload_preview); + + if (topic == NULL) { + log_action("MQTT message ignored: topic is null"); + } else if (msg == NULL) { + log_action("MQTT message ignored: message is null"); + } else if (msg->retained) { + log_action("MQTT command ignored: retained message"); + } else { if (strcmp(topic, topic_sleep) == 0) { + if (!is_valid_command_payload(msg)) { + log_action("MQTT sleep command ignored: invalid payload"); + MQTTClient_freeMessage(&msg); + MQTTClient_free(topic); + return 1; + } + log_action("MQTT command accepted: sleep queued"); queue_command(COMMAND_SLEEP); } else if (strcmp(topic, topic_monitor_off) == 0) { + if (is_empty_payload(msg)) { + log_action("MQTT monitor-off command accepted: empty payload trigger"); + } else if (is_valid_command_payload(msg)) { + log_action("MQTT monitor-off command accepted: valid payload trigger"); + } else { + log_action("MQTT monitor-off command ignored: invalid payload"); + MQTTClient_freeMessage(&msg); + MQTTClient_free(topic); + return 1; + } + log_action("MQTT command accepted: monitor-off queued"); queue_command(COMMAND_MONITOR_OFF); + } else { + log_actionf("MQTT command ignored: topic does not match subscribed commands (topic=%s)", topic); } } @@ -211,7 +339,7 @@ static void connection_lost(void *context, char *cause) { #ifndef HIDDEN_BUILD fprintf(stderr, "Connection lost: %s\n", cause ? cause : "unknown"); #endif - log_action("MQTT connection lost"); + log_actionf("MQTT connection lost: cause=%s", cause ? cause : "unknown"); } static BOOL WINAPI console_handler(DWORD signal) { @@ -252,32 +380,47 @@ static int publish_retained(MQTTClient client, const char *topic, const char *pa static int subscribe_topics(MQTTClient client) { int rc; if ((rc = MQTTClient_subscribe(client, topic_sleep, QOS)) != MQTTCLIENT_SUCCESS) { + log_actionf("MQTT subscribe failed: topic=%s, rc=%d", topic_sleep, rc); fprintf(stderr, "Failed to subscribe to %s: %d\n", topic_sleep, rc); return rc; } + log_actionf("MQTT subscribe ok: topic=%s, qos=%d", topic_sleep, QOS); if ((rc = MQTTClient_subscribe(client, topic_monitor_off, QOS)) != MQTTCLIENT_SUCCESS) { + log_actionf("MQTT subscribe failed: topic=%s, rc=%d", topic_monitor_off, rc); fprintf(stderr, "Failed to subscribe to %s: %d\n", topic_monitor_off, rc); return rc; } + log_actionf("MQTT subscribe ok: topic=%s, qos=%d", topic_monitor_off, QOS); return MQTTCLIENT_SUCCESS; } static int publish_birth_messages(MQTTClient client) { int rc; if ((rc = publish_retained(client, topic_status, STATUS_ONLINE)) != MQTTCLIENT_SUCCESS) { + log_actionf("MQTT publish failed: topic=%s, payload=%s, retained=1, rc=%d", + topic_status, STATUS_ONLINE, rc); fprintf(stderr, "Failed to publish status: %d\n", rc); return rc; } + log_actionf("MQTT publish ok: topic=%s, payload=%s, retained=1", + topic_status, STATUS_ONLINE); if ((rc = publish_retained(client, topic_version, VERSION)) != MQTTCLIENT_SUCCESS) { + log_actionf("MQTT publish failed: topic=%s, payload=%s, retained=1, rc=%d", + topic_version, VERSION, rc); fprintf(stderr, "Failed to publish version: %d\n", rc); return rc; } + log_actionf("MQTT publish ok: topic=%s, payload=%s, retained=1", + topic_version, VERSION); return MQTTCLIENT_SUCCESS; } static int try_connect(MQTTClient client, MQTTClient_connectOptions *conn_opts, const char *address) { + log_actionf("MQTT connect attempt: address=%s, client_id=%s, clean_session=%d, keep_alive=%d", + address, client_id, conn_opts->cleansession, conn_opts->keepAliveInterval); int rc = MQTTClient_connect(client, conn_opts); if (rc != MQTTCLIENT_SUCCESS) { + log_actionf("MQTT connect failed: rc=%d", rc); return rc; } @@ -294,7 +437,7 @@ static int try_connect(MQTTClient client, MQTTClient_connectOptions *conn_opts, } set_connected(1); - log_action("Connected to MQTT broker"); + log_actionf("Connected to MQTT broker: address=%s, client_id=%s", address, client_id); printf("Connected to %s\n", address); printf("Status: %s -> %s\n", topic_status, STATUS_ONLINE); printf("Subscribed to:\n %s\n %s\n", topic_sleep, topic_monitor_off); @@ -394,6 +537,7 @@ int main(int argc, char *argv[]) { printf("pc-control v%s\n", VERSION); printf("Device: %s\n", hostname); log_startup(); + log_runtime_config(address, hostname); MQTTClient client; MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; diff --git a/src/version.h b/src/version.h index 2cfbc70..922d523 100644 --- a/src/version.h +++ b/src/version.h @@ -3,9 +3,9 @@ #define PC_CONTROL_VERSION_MAJOR 1 #define PC_CONTROL_VERSION_MINOR 1 -#define PC_CONTROL_VERSION_PATCH 0 +#define PC_CONTROL_VERSION_PATCH 1 -#define VERSION "1.1.0" +#define VERSION "1.1.1" #define PC_CONTROL_VERSION_COMMA PC_CONTROL_VERSION_MAJOR,PC_CONTROL_VERSION_MINOR,PC_CONTROL_VERSION_PATCH,0 #endif