/* * pjctl - network projector control utility * * Copyright (C) 2011 Benjamin Franzke * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define _POSIX_C_SOURCE 200112L #define _BSD_SOURCE #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include "ext/gnulib/md5.h" enum pjlink_packet_offsets { PJLINK_HEADER = 0, PJLINK_CLASS = 1, PJLINK_COMMAND = 2, PJLINK_SEPERATOR = 6, PJLINK_PARAMETER = 7, PJLINK_TERMINATOR = 135 /* max or less */ }; enum pjctl_state { PJCTL_AWAIT_INITIAL, PJCTL_AWAIT_RESPONSE, PJCTL_AWAIT_RESPONSE_OR_AUTH_ERR, PJCTL_FINISH }; struct pjctl; union pjctl_param_parse { uint8_t p[8]; struct { uint8_t val; } power; struct { uint8_t type; uint8_t num; } source; struct { uint8_t type; uint8_t val; } mute; }; struct queue_command { char *command; void (*response_func)(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param); char *prefix; int (*toggle)(struct pjctl *pjctl, union pjctl_param_parse *parse); struct queue_command *prev, *next; }; struct pjctl { enum pjctl_state state; struct queue_command queue; int fd; char *password; int need_hash; char hash[32+1]; /* 0-terminated hex as ascii encoded 16 byte hash */ }; #define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) #define MIN(a,b) ((a)<(b) ? (a) : (b)) #define remove_from_list(elem) \ do { \ (elem)->next->prev = (elem)->prev; \ (elem)->prev->next = (elem)->next; \ } while (0) #define insert_at_head(list, elem) \ do { \ (elem)->prev = list; \ (elem)->next = (list)->next; \ (list)->next->prev = elem; \ (list)->next = elem; \ } while(0) /* return value: -1 = error, 1 = ok, 0 = unknown */ static int handle_pjlink_error(char *param) { if (strcmp(param, "OK") == 0) return 1; if (strncmp(param, "ERR", 3) == 0) { if (strlen(param) < 4) /* not a valid error code, ignore */ return 0; switch (param[3]) { case '1': printf("error: Undefined command.\n"); break; case '2': printf("error: Out-of-parameter.\n"); break; case '3': printf("error: Unavailable time.\n"); break; case '4': printf("error: Projector failure.\n"); break; default: return 0; } return -1; } return 0; } static void init_hash(struct pjctl *pjctl, const char *salt) { unsigned char md[MD5_DIGEST_SIZE]; struct md5_ctx ctx; md5_init_ctx(&ctx); md5_process_bytes(salt, strlen(salt), &ctx); md5_process_bytes(pjctl->password, strlen(pjctl->password), &ctx); md5_finish_ctx(&ctx, md); snprintf(pjctl->hash, sizeof(pjctl->hash), "%02x%02x%02x%02x%02x%02x%02x%02x" "%02x%02x%02x%02x%02x%02x%02x%02x", md[ 0], md[ 1], md[ 2], md[ 3], md[ 4], md[ 5], md[ 6], md[ 7], md[ 8], md[ 9], md[10], md[11], md[12], md[13], md[14], md[15]); pjctl->need_hash = 1; } static int send_next_cmd(struct pjctl *pjctl) { struct queue_command *cmd; struct msghdr msg; struct iovec iov[2]; /* Have we sent all commands (is the queue empty)? */ if (pjctl->queue.next == &pjctl->queue) { pjctl->state = PJCTL_FINISH; return 0; } pjctl->state = PJCTL_AWAIT_RESPONSE; memset(&msg, 0, sizeof msg); msg.msg_iov = iov; if (pjctl->need_hash) { pjctl->state = PJCTL_AWAIT_RESPONSE_OR_AUTH_ERR; iov[msg.msg_iovlen].iov_base = pjctl->hash; iov[msg.msg_iovlen].iov_len = 32; msg.msg_iovlen++; } cmd = pjctl->queue.prev; iov[msg.msg_iovlen].iov_base = cmd->command; iov[msg.msg_iovlen].iov_len = strlen(cmd->command); msg.msg_iovlen++; if (sendmsg(pjctl->fd, &msg, 0) < 0) { fprintf(stderr, "sendmsg failed: %s\n", strerror(errno)); return -1; } return 0; } static int handle_setup(struct pjctl *pjctl, char *data, int len) { switch (data[PJLINK_PARAMETER]) { case '1': if (pjctl->password == NULL) { fprintf(stderr, "Authentication required, password needed\n"); return -1; } if (strlen(&data[PJLINK_PARAMETER]) < 3) goto err; init_hash(pjctl, &data[PJLINK_PARAMETER+2]); break; case '0': /* No authentication */ break; case 'E': if (strcmp(&data[PJLINK_PARAMETER], "ERRA") == 0) { fprintf(stderr, "Authentication failed.\n"); return -1; } /* FALLTHROUGH */ default: goto err; } send_next_cmd(pjctl); return 0; err: fprintf(stderr, "error: invalid setup message received.\n"); return -1; } static int handle_data(struct pjctl *pjctl, char *data, int len) { struct queue_command *cmd; if (len < 7 || len > PJLINK_TERMINATOR) { fprintf(stderr, "error: invalid packet length: %d: data: %*s\n", len, len, data); return -1; } if (strncmp(data, "PJLINK ", 7) == 0) { switch (pjctl->state) { case PJCTL_AWAIT_INITIAL: case PJCTL_AWAIT_RESPONSE_OR_AUTH_ERR: break; default: fprintf(stderr, "error: got unexpected initial\n"); return -1; } return handle_setup(pjctl, data, len); } switch (pjctl->state) { case PJCTL_AWAIT_RESPONSE: case PJCTL_AWAIT_RESPONSE_OR_AUTH_ERR: break; default: fprintf(stderr, "error: got unexpected response.\n"); return -1; } if (data[PJLINK_HEADER] != '%') { fprintf(stderr, "invalid pjlink command received.\n"); return -1; } if (data[PJLINK_CLASS] != '1') { fprintf(stderr, "unhandled pjlink class: %c\n", data[1]); return -1; } if (data[PJLINK_SEPERATOR] != '=') { fprintf(stderr, "incorrect seperator in pjlink command\n"); return -1; } data[PJLINK_SEPERATOR] = '\0'; cmd = pjctl->queue.prev; remove_from_list(cmd); cmd->response_func(pjctl, cmd, &data[PJLINK_COMMAND], &data[PJLINK_PARAMETER]); free(cmd->command); free(cmd); send_next_cmd(pjctl); return 0; } static int read_cb(struct pjctl *pjctl) { char data[136]; ssize_t ret; char *end; ret = recv(pjctl->fd, data, sizeof data, 0); if (ret <= 0) { exit(1); } end = memchr(data, 0x0d, ret); if (end == NULL) { fprintf(stderr, "invalid pjlink msg received\n"); exit(1); return -1; } *end = '\0'; if (handle_data(pjctl, data, (ptrdiff_t) (end - data)) < 0) return -1; return 0; } static void power_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { int ret; union pjctl_param_parse p; fputs(cmd->prefix, stdout); free(cmd->prefix); ret = handle_pjlink_error(param); if (ret == 1) { printf("OK\n"); } else if (ret == 0) { printf("%s\n", param[0] == '1' ? "on" : "off" ); if (cmd->toggle) { p.power.val = param[0] == '1' ? '0' : '1'; cmd->toggle(pjctl, &p); } } } static int power(struct pjctl *pjctl, union pjctl_param_parse *parse) { struct queue_command *cmd; cmd = calloc(1, sizeof *cmd); if (!cmd) return -1; if (parse->power.val == '?') cmd->toggle = power; if (asprintf(&cmd->command, "%%1POWR %c\r", parse->power.val) < 0) return -1; cmd->response_func = power_response; if (asprintf(&cmd->prefix, "power %s: ", cmd->toggle ? "status" : "TODO on/off") < 0) return -1; insert_at_head(&pjctl->queue, cmd); return 0; } static void source_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { if (handle_pjlink_error(param) == 1) printf("OK\n"); } static int source(struct pjctl *pjctl, union pjctl_param_parse *p) { struct queue_command *cmd; uint8_t type = p->source.type, num = p->source.num; const char *switches[] = { "rgb", "video", "digital", "storage", "net" }; cmd = calloc(1, sizeof *cmd); if (!cmd) return -1; if (asprintf(&cmd->command, "%%1INPT %d%d\r", type, num) < 0) return -1; cmd->response_func = source_response; insert_at_head(&pjctl->queue, cmd); printf("source select %s%d: ", switches[type-1], num); return 0; } static void avmute_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { int ret; union pjctl_param_parse p; fputs(cmd->prefix, stdout); free(cmd->prefix); ret = handle_pjlink_error(param); if (ret == 1) { printf("OK\n"); } else if (ret == 0) { if (strlen(param) != 2) return; switch (param[0]) { case '1': printf("video"); break; case '2': printf("audio"); break; case '3': printf("video & audio"); break; default: // error? break; } printf(" mute "); printf("%s\n", param[1] == '1' ? "on" : "off"); if (cmd->toggle) { p.mute.type = param[0]; p.mute.val = param[1] == '1' ? '0' : '1'; cmd->toggle(pjctl, &p); } } } static int avmute(struct pjctl *pjctl, union pjctl_param_parse *p) { struct queue_command *cmd; uint8_t type = p->mute.type; char code = p->mute.val; const char *targets[] = { "video", "audio", "av" }; cmd = calloc(1, sizeof *cmd); if (!cmd) return -1; if (code == '?') cmd->toggle = power; if (asprintf(&cmd->command, "%%1AVMT %d%c\r", type, code) < 0) return -1; cmd->response_func = avmute_response; if (asprintf(&cmd->prefix, "%s mute %s: ", targets[type-1], cmd->toggle ? "status" : "TODO on/off") < 0) return -1; insert_at_head(&pjctl->queue, cmd); return 0; } static void name_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { if (!strlen(param)) return; printf("name: "); if (handle_pjlink_error(param) < 0) return; printf("%s\n", param); } static void manufactor_name_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { if (strlen(param)) printf("manufactor name: %s\n", param); } static void product_name_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { if (strlen(param)) printf("product name: %s\n", param); } static void info_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { if (strlen(param)) printf("model info: %s\n", param); } static const char * map_input_name(char sw) { switch (sw) { case '1': return "rgb"; case '2': return "video"; case '3': return "digital"; case '4': return "storage"; case '5': return "net"; default: return "unknown"; } } static void input_switch_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { if (!strlen(param)) return; printf("current input: "); if (handle_pjlink_error(param) < 0) return; if (strlen(param) == 2) printf("%s%c\n", map_input_name(param[0]), param[1]); else printf("error: invalid response\n"); } static void input_list_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { int i; int len = strlen(param); if (len % 3 != 2) return; printf("available input sources:"); for (i = 0; i < len; i+=3) printf(" %s%c", map_input_name(param[i]), param[i+1]); printf("\n"); } static void lamp_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { char *lamp_end, *lamp_on, *lamp_time = param; int i, len = strlen(param); printf("lamp: "); if (handle_pjlink_error(param) < 0) return; for (i = 0; len; ++i) { lamp_end = memchr(lamp_time, ' ', MIN(len, 5)); if (lamp_end == NULL) goto invalid; *lamp_end = '\0'; len -= lamp_end - lamp_time; if (len < 2) goto invalid; switch (*(lamp_end + 1)) { case '1': lamp_on = "on"; break; case '0': lamp_on = "off"; break; default: goto invalid; } printf("lamp%d:%s cumulative lighting time: %s; ", i, lamp_on, lamp_time); lamp_time = lamp_end + 2; len -= 2; if (strlen(lamp_time)) { if (lamp_time[0] != ' ') goto invalid; lamp_time++; len--; } } printf("\n"); return; invalid: printf("invalid message body: %s\n", param); } static void error_status_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { int i; int none = 1; const char *flags[] = { "fan", "lamp", "temperature", "cover", "filter", "other" }; printf("errors: "); if (handle_pjlink_error(param) < 0) return; if (strlen(param) != ARRAY_SIZE(flags)) { fprintf(stderr, "invalid message received\n"); return; } for (i = 0; i < ARRAY_SIZE(flags); ++i) { switch (param[i]) { case '2': printf("%s:error ", flags[i]); none = 0; break; case '1': printf("%s:warning ", flags[i]); none = 0; break; case '0': break; default: fprintf(stderr, "invalid message received\n"); return; } } if (none) fputs("none", stdout); fputs("\n", stdout); } static void class_response(struct pjctl *pjctl, struct queue_command *cmd, char *op, char *param) { printf("available classes: %s\n", param); } static int status(struct pjctl *pjctl, union pjctl_param_parse *parse) { /* Note: incomplete commands stored here */ static const struct queue_command cmds[] = { { "NAME", name_response }, { "INF1", manufactor_name_response }, { "INF2", product_name_response }, { "INFO", info_response }, { "POWR", power_response, "power status: " }, { "INPT", input_switch_response }, { "INST", input_list_response }, { "AVMT", avmute_response, "avmute: " }, { "LAMP", lamp_response }, { "ERST", error_status_response }, { "CLSS", class_response } }; struct queue_command *cmd; int i; for (i = 0; i < ARRAY_SIZE(cmds); ++i) { cmd = calloc(1, sizeof *cmd); if (!cmd) return -1; memcpy(cmd, &cmds[i], sizeof *cmd); if (asprintf(&cmd->command, "%%1%s ?\r", cmds[i].command) < 0) return -1; if (cmds[i].prefix) cmd->prefix = strdup(cmds[i].prefix); insert_at_head(&pjctl->queue, cmd); } return 0; } enum pjctl_param_type { PJCTL_PARAM_ATOM, PJCTL_PARAM_RANGE, PJCTL_PARAM_SWITCH, PJCTL_PARAM_END }; struct pjctl_param { enum pjctl_param_type type; union { const char **atoms; struct { int start, end, fallback; } range; } p; }; #define P (struct pjctl_param[]) #define P_ATOM(...) { \ .type = PJCTL_PARAM_ATOM, \ .p.atoms = (const char *[]) { __VA_ARGS__, NULL } \ } #define P_RANGE(a,b,c) { .type = PJCTL_PARAM_RANGE, .p.range = { (a),(b),(c) } } #define P_SWITCH { .type = PJCTL_PARAM_SWITCH } #define P_END { .type = PJCTL_PARAM_END } static struct pjctl_command { char *name; int (*func)(struct pjctl *pjctl, union pjctl_param_parse *p); struct pjctl_param *params; } commands[] = { { "power", power, P{ P_SWITCH, P_END } }, { "source", source, P{ P_ATOM("rgb", "video", "digital", "storage", "net"), P_RANGE(1,9,1), P_END } }, { "mute", avmute, P{ P_ATOM("video", "audio", "av"), P_SWITCH, P_END }}, { "status", status, NULL }, }; enum pjctl_command_type { PJCTL_COMMAND_POWER, PJCTL_COMMAND_SOURCE, PJCTL_COMMAND_MUTE, PJCTL_COMMAND_STATUS }; static void cmd_help(struct pjctl_param *params) { int i, j; struct pjctl_param *p; char prefix; if (params == NULL) return; for (i = 0; params[i].type != PJCTL_PARAM_END; ++i) { p = ¶ms[i]; switch (p->type) { case PJCTL_PARAM_SWITCH: printf(" "); break; case PJCTL_PARAM_RANGE: printf(" [%d-%d]", p->p.range.start, p->p.range.end); break; case PJCTL_PARAM_ATOM: printf(" "); prefix = '<'; for (j = 0; p->p.atoms[j]; ++j) { printf("%c%s", prefix, p->p.atoms[j]); prefix = '|'; } printf(">"); break; default: abort(); break; } } } static void usage(struct pjctl *pjctl) { int i; printf("usage: pjctl [-p password] command [args..]\n\n"); printf("commands:\n"); for (i = 0; i < ARRAY_SIZE(commands); ++i) { printf(" %s", commands[i].name); cmd_help(commands[i].params); printf("\n"); } } static int parse_params(struct pjctl_command *cmd, char **argv, int argc, union pjctl_param_parse *parse) { struct pjctl_param *param; uint8_t *p = parse->p; int idx = 1; int i; int type = 0; long int num; if (cmd->params == NULL) return 0; for (param = &cmd->params[0], idx = 0; param->type != PJCTL_PARAM_END; param++, idx++) { switch (param->type) { case PJCTL_PARAM_SWITCH: if (argc <= idx) return -1; if (strcmp(argv[idx], "on") == 0) p[idx] = '1'; else if (strcmp(argv[idx], "off") == 0) p[idx] = '0'; else if (strcmp(argv[idx], "toggle") == 0) { p[idx] = '?'; } else { fprintf(stderr, "invalid %s parameter\n", cmd->name); return -1; } break; case PJCTL_PARAM_ATOM: if (argc <= idx) return -1; for (i = 0; param->p.atoms[i]; ++i) { if (strcmp(argv[idx], param->p.atoms[i]) == 0) { type = i+1; break; } } if (type == 0) { fprintf(stderr, "incorrect %s type given\n", cmd->name); return -1; } p[idx] = type; break; case PJCTL_PARAM_RANGE: if (argc <= idx) { p[idx] = param->p.range.fallback; break; } num = strtol(argv[idx], NULL, 10); if (num < param->p.range.start || num > param->p.range.end) { fprintf(stderr, "invalid %s range parameter\n", cmd->name); return -1; } p[idx] = num; break; default: abort(); } } return 0; } int main(int argc, char **argv) { struct pjctl pjctl; union pjctl_param_parse parse; char *host; char *sport = "4352"; struct addrinfo hints, *result, *rp; int s, i, c; memset(&pjctl, 0, sizeof pjctl); pjctl.queue.next = pjctl.queue.prev = &pjctl.queue; while ((c = getopt(argc, argv, "p:")) != -1) { switch (c) { case 'p': pjctl.password = optarg; break; default: return 1; } } if (argc < optind+2) { usage(&pjctl); return 1; } for (i = 0; i < ARRAY_SIZE(commands); ++i) { if (strcmp(argv[optind+1], commands[i].name) == 0) { if (parse_params(&commands[i], &argv[optind+2], argc-optind-2, &parse) < 0) { usage(&pjctl); return 1; } if (commands[i].func(&pjctl, &parse) < 0) return 1; } } /* Nothing got into queue? User gave invalid command. */ if (pjctl.queue.next == &pjctl.queue) { fprintf(stderr, "error: invalid command\n"); usage(&pjctl); return 1; } memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; host = argv[optind]; s = getaddrinfo(host, sport, &hints, &result); if (s != 0) { fprintf(stderr, "getaddrinfo :%s\n", gai_strerror(s)); return 1; } for (rp = result; rp != NULL; rp = rp->ai_next) { pjctl.fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (pjctl.fd == -1) continue; if (connect(pjctl.fd, rp->ai_addr, rp->ai_addrlen) == 0) break; close(pjctl.fd); } freeaddrinfo(result); if (rp == NULL) { fprintf(stderr, "Failed to connect: %s\n", strerror(errno)); return 1; } pjctl.state = PJCTL_AWAIT_INITIAL; while (pjctl.state != PJCTL_FINISH) { if (read_cb(&pjctl) < 0) return 1; } close(pjctl.fd); return 0; }