blob: 661cbf42ec81eb44c9ebbe5bbea05dd8592e7d3a [file] [log] [blame]
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
/*
* Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <cerrno>
#include <ctime>
#include <string>
#include <sys/ioctl.h>
#include <sys/time.h>
#include "cec-follower.h"
#include "compiler.h"
#define VOLUME_MAX 0x64
#define VOLUME_MIN 0
/* States for the RC handling */
#define NOPRESS 0
#define PRESS 1
#define PRESS_HOLD 2
/* The follower safety timeout as defined in the spec */
#define FOLLOWER_SAFETY_TIMEOUT 450
#define MIN_INITIATOR_REP_TIME 200
#define MAX_INITIATOR_REP_TIME 500
/* Time between each polling message sent to a device */
#define POLL_PERIOD 15000
/* The maximum interval in nanoseconds between audio rate messages as defined in the spec */
#define MAX_AUD_RATE_MSG_INTERVAL_NS (2 * 1000000000ULL)
/* The maximum interval in nanoseconds to allow a deck to skip forward/reverse */
#define MAX_DECK_SKIP_NS (2 * 1000000000ULL)
struct cec_enum_values {
const char *type_name;
__u8 value;
};
struct la_info la_info[15];
static struct timespec start_monotonic;
static struct timeval start_timeofday;
static constexpr time_t time_to_transient = 1;
static constexpr time_t time_to_stable = 8;
static const char *get_ui_cmd_string(__u8 ui_cmd)
{
return cec_log_ui_cmd_string(ui_cmd) ? : "Unknown";
}
static std::string ts2s(__u64 ts, bool wallclock)
{
std::string s;
struct timeval sub;
struct timeval res;
__u64 diff;
char buf[64];
time_t t;
if (!wallclock) {
sprintf(buf, "%llu.%03llus", ts / 1000000000, (ts % 1000000000) / 1000000);
return buf;
}
diff = ts - start_monotonic.tv_sec * 1000000000ULL - start_monotonic.tv_nsec;
sub.tv_sec = diff / 1000000000ULL;
sub.tv_usec = (diff % 1000000000ULL) / 1000;
timeradd(&start_timeofday, &sub, &res);
t = res.tv_sec;
s = ctime(&t);
s = s.substr(0, s.length() - 6);
sprintf(buf, "%03lu", res.tv_usec / 1000);
return s + "." + buf;
}
static void log_event(struct cec_event &ev, bool wallclock)
{
__u16 pa;
if (ev.flags & CEC_EVENT_FL_DROPPED_EVENTS)
printf("(Note: events were lost)\n");
if (ev.flags & CEC_EVENT_FL_INITIAL_STATE)
printf("Initial ");
switch (ev.event) {
case CEC_EVENT_STATE_CHANGE:
pa = ev.state_change.phys_addr;
printf("Event: State Change: PA: %x.%x.%x.%x, LA mask: 0x%04x\n",
cec_phys_addr_exp(pa),
ev.state_change.log_addr_mask);
break;
case CEC_EVENT_LOST_MSGS:
printf("Event: Lost Messages\n");
break;
case CEC_EVENT_PIN_HPD_LOW:
case CEC_EVENT_PIN_HPD_HIGH:
printf("Event: HPD Pin %s\n",
ev.event == CEC_EVENT_PIN_HPD_HIGH ? "High" : "Low");
warn("Unexpected HPD pin event!\n");
break;
case CEC_EVENT_PIN_CEC_LOW:
case CEC_EVENT_PIN_CEC_HIGH:
printf("Event: CEC Pin %s\n",
ev.event == CEC_EVENT_PIN_CEC_HIGH ? "High" : "Low");
warn("Unexpected CEC pin event!\n");
break;
default:
printf("Event: Unknown (0x%x)\n", ev.event);
break;
}
if (show_info)
printf("\tTimestamp: %s\n", ts2s(ev.ts, wallclock).c_str());
}
void reply_feature_abort(struct node *node, struct cec_msg *msg, __u8 reason)
{
unsigned la = cec_msg_initiator(msg);
__u8 opcode = cec_msg_opcode(msg);
__u64 ts_now = get_ts();
if (cec_msg_is_broadcast(msg) || cec_msg_initiator(msg) == CEC_LOG_ADDR_UNREGISTERED)
return;
if (reason == CEC_OP_ABORT_UNRECOGNIZED_OP) {
la_info[la].feature_aborted[opcode].count++;
if (la_info[la].feature_aborted[opcode].count == 2) {
/* If the Abort Reason was "Unrecognized opcode", the Initiator should not send
the same message to the same Follower again at that time to avoid saturating
the bus. */
warn("Received message %s from LA %d (%s) shortly after\n",
opcode2s(msg).c_str(), la, cec_la2s(la));
warn("replying Feature Abort [Unrecognized Opcode] to the same message.\n");
}
}
else if (la_info[la].feature_aborted[opcode].count) {
warn("Replying Feature Abort with abort reason different than [Unrecognized Opcode]\n");
warn("to message that has previously been replied Feature Abort to with [Unrecognized Opcode].\n");
}
else
la_info[la].feature_aborted[opcode].ts = ts_now;
cec_msg_reply_feature_abort(msg, reason);
transmit(node, msg);
}
static bool exit_standby(struct node *node)
{
/* Cancel any standby request that was pending. */
node->state.record_received_standby = false;
if (node->state.power_status == CEC_OP_POWER_STATUS_STANDBY ||
node->state.power_status == CEC_OP_POWER_STATUS_TO_STANDBY) {
node->state.old_power_status = node->state.power_status;
node->state.power_status = CEC_OP_POWER_STATUS_ON;
node->state.power_status_changed_time = time(nullptr);
dev_info("Changing state to on\n");
return true;
}
return false;
}
bool enter_standby(struct node *node)
{
if (node->state.power_status == CEC_OP_POWER_STATUS_ON ||
node->state.power_status == CEC_OP_POWER_STATUS_TO_ON) {
/*
* Standby should not interrupt a recording in progress, but
* remember to go to standby once the recording is finished.
*/
if (node->state.one_touch_record_on) {
node->state.record_received_standby = true;
return false;
}
node->state.old_power_status = node->state.power_status;
node->state.power_status = CEC_OP_POWER_STATUS_STANDBY;
node->state.power_status_changed_time = time(nullptr);
node->state.deck_skip_start = 0;
node->state.record_received_standby = false;
dev_info("Changing state to standby\n");
return true;
}
return false;
}
static unsigned get_duration_ms(__u64 ts_a, __u64 ts_b)
{
return (ts_a - ts_b) / 1000000;
}
static void rc_press_hold_stop(const struct state *state)
{
unsigned mean_duration = state->rc_duration_sum / state->rc_press_hold_count;
dev_info("Stop Press and Hold. Mean duration between User Control Pressed messages: %dms\n",
mean_duration);
if (mean_duration < MIN_INITIATOR_REP_TIME) {
warn("The mean duration between User Control Pressed messages is lower\n");
warn("than the Minimum Initiator Repetition Time (200ms).\n");
}
if (mean_duration > MAX_INITIATOR_REP_TIME) {
warn("The mean duration between User Control Pressed messages is higher\n");
warn("than the Maximum Initiator Repetition Time (500ms).\n");
}
}
static __u16 pa_common_mask(__u16 pa1, __u16 pa2)
{
__u16 mask = 0xf000;
for (int i = 0; i < 3; i++) {
if ((pa1 & mask) != (pa2 & mask))
break;
mask = (mask >> 4) | 0xf000;
}
return mask << 4;
}
static bool pa_are_adjacent(__u16 pa1, __u16 pa2)
{
const __u16 mask = pa_common_mask(pa1, pa2);
const __u16 trail_mask = ((~mask) & 0xffff) >> 4;
if (pa1 == CEC_PHYS_ADDR_INVALID || pa2 == CEC_PHYS_ADDR_INVALID || pa1 == pa2)
return false;
if ((pa1 & trail_mask) || (pa2 & trail_mask))
return false;
return !((pa1 & ~mask) && (pa2 & ~mask));
}
static bool pa_is_upstream_from(__u16 pa1, __u16 pa2)
{
const __u16 mask = pa_common_mask(pa1, pa2);
if (pa1 == CEC_PHYS_ADDR_INVALID || pa2 == CEC_PHYS_ADDR_INVALID)
return false;
return !(pa1 & ~mask) && (pa2 & ~mask);
}
static __u8 current_power_state(struct node *node)
{
time_t t = time(nullptr);
if (t - node->state.power_status_changed_time <= time_to_transient)
return node->state.old_power_status;
if (t - node->state.power_status_changed_time >= time_to_stable)
return node->state.power_status;
if (node->state.power_status == CEC_OP_POWER_STATUS_ON)
return CEC_OP_POWER_STATUS_TO_ON;
return CEC_OP_POWER_STATUS_TO_STANDBY;
}
static void aud_rate_msg_interval_check(struct node *node, __u64 ts_new)
{
/*
* The interval since the last audio rate message is only relevant
* if the Source is currently in audio rate controlled mode
* (i.e. state.last_aud_rate_rx_ts != 0).
*/
__u64 ts_old = node->state.last_aud_rate_rx_ts;
if (ts_old) {
__u64 interval = ts_new - ts_old;
if (interval > MAX_AUD_RATE_MSG_INTERVAL_NS) {
warn("The interval since the last Audio Rate Control message was > 2s.\n");
node->state.last_aud_rate_rx_ts = 0;
}
}
}
static void update_deck_state(struct node *node, unsigned me, __u8 deck_state_new)
{
if (node->state.deck_state != deck_state_new) {
node->state.deck_state = deck_state_new;
if (node->state.deck_report_changes) {
struct cec_msg msg;
cec_msg_init(&msg, me, node->state.deck_report_changes_to);
cec_msg_deck_status(&msg, node->state.deck_state);
transmit(node, &msg);
}
}
}
static void processMsg(struct node *node, struct cec_msg &msg, unsigned me, __u8 type)
{
__u8 to = cec_msg_destination(&msg);
__u8 from = cec_msg_initiator(&msg);
bool is_bcast = cec_msg_is_broadcast(&msg);
__u16 remote_pa = (from < 15) ? node->remote_phys_addr[from] : CEC_PHYS_ADDR_INVALID;
switch (msg.msg[1]) {
/* OSD Display */
case CEC_MSG_SET_OSD_STRING: {
__u8 disp_ctl;
char osd[14];
if (to != 0 && to != 14)
break;
if (!node->has_osd_string)
break;
cec_ops_set_osd_string(&msg, &disp_ctl, osd);
switch (disp_ctl) {
case CEC_OP_DISP_CTL_DEFAULT:
case CEC_OP_DISP_CTL_UNTIL_CLEARED:
case CEC_OP_DISP_CTL_CLEAR:
return;
}
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
transmit(node, &msg);
return;
}
/* Give Device Power Status */
case CEC_MSG_GIVE_DEVICE_POWER_STATUS:
cec_msg_set_reply_to(&msg, &msg);
cec_msg_report_power_status(&msg, current_power_state(node));
transmit(node, &msg);
return;
case CEC_MSG_REPORT_POWER_STATUS:
// Nothing to do here for now.
return;
/* Standby */
case CEC_MSG_STANDBY:
enter_standby(node);
return;
/* One Touch Play and Routing Control */
case CEC_MSG_ACTIVE_SOURCE: {
__u16 phys_addr;
cec_ops_active_source(&msg, &phys_addr);
node->state.active_source_pa = phys_addr;
dev_info("New active source: %x.%x.%x.%x\n", cec_phys_addr_exp(phys_addr));
return;
}
case CEC_MSG_INACTIVE_SOURCE: {
__u16 phys_addr;
if (node->phys_addr)
break;
cec_ops_active_source(&msg, &phys_addr);
if (node->state.active_source_pa != phys_addr)
break;
node->state.active_source_pa = 0;
cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
cec_msg_active_source(&msg, node->phys_addr);
transmit(node, &msg);
dev_info("New active source: 0.0.0.0\n");
return;
}
case CEC_MSG_IMAGE_VIEW_ON:
case CEC_MSG_TEXT_VIEW_ON:
if (!cec_has_tv(1 << me))
break;
exit_standby(node);
return;
case CEC_MSG_SET_STREAM_PATH: {
__u16 phys_addr;
cec_ops_set_stream_path(&msg, &phys_addr);
if (phys_addr != node->phys_addr)
return;
if (!cec_has_tv(1 << me))
exit_standby(node);
cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
cec_msg_active_source(&msg, node->phys_addr);
transmit(node, &msg);
dev_info("Stream Path is directed to this device\n");
return;
}
case CEC_MSG_ROUTING_INFORMATION: {
__u8 la = cec_msg_initiator(&msg);
if (cec_has_tv(1 << la) && la_info[la].phys_addr == 0)
warn("TV (0) at 0.0.0.0 sent Routing Information.");
return;
}
/* System Information */
case CEC_MSG_GET_MENU_LANGUAGE:
if (!cec_has_tv(1 << me))
break;
cec_msg_set_reply_to(&msg, &msg);
cec_msg_set_menu_language(&msg, node->state.menu_language);
transmit(node, &msg);
return;
case CEC_MSG_CEC_VERSION:
return;
case CEC_MSG_REPORT_PHYSICAL_ADDR: {
__u16 phys_addr;
__u8 prim_dev_type;
cec_ops_report_physical_addr(&msg, &phys_addr, &prim_dev_type);
if (from < 15) {
node->remote_phys_addr[from] = phys_addr;
node->remote_prim_devtype[from] = prim_dev_type;
}
return;
}
/* Remote Control Passthrough
(System Audio Control, Device Menu Control) */
case CEC_MSG_USER_CONTROL_PRESSED: {
struct cec_op_ui_command rc_press;
unsigned new_state;
unsigned duration;
cec_ops_user_control_pressed(&msg, &rc_press);
duration = get_duration_ms(msg.rx_ts, node->state.rc_press_rx_ts);
new_state = PRESS;
if (node->state.rc_state == NOPRESS)
dev_info("Button press: %s\n", get_ui_cmd_string(rc_press.ui_cmd));
else if (rc_press.ui_cmd != node->state.rc_ui_cmd) {
/* We have not yet received User Control Released, but have received
another User Control Pressed with a different UI Command. */
if (node->state.rc_state == PRESS_HOLD)
rc_press_hold_stop(&node->state);
dev_info("Button press (no User Control Released between): %s\n",
get_ui_cmd_string(rc_press.ui_cmd));
/* The device shall send User Control Released if the time between
two messages is longer than the maximum Initiator Repetition Time. */
if (duration > MAX_INITIATOR_REP_TIME)
warn("Device waited more than the maximum Initiatior Repetition Time and should have sent a User Control Released message.");
} else if (duration < FOLLOWER_SAFETY_TIMEOUT) {
/* We have not yet received a User Control Released, but received
another User Control Pressed, with the same UI Command as the
previous, which means that the Press and Hold behavior should
be invoked. */
new_state = PRESS_HOLD;
if (node->state.rc_state != PRESS_HOLD) {
dev_info("Start Press and Hold with button %s\n",
get_ui_cmd_string(rc_press.ui_cmd));
node->state.rc_duration_sum = 0;
node->state.rc_press_hold_count = 0;
}
node->state.rc_duration_sum += duration;
node->state.rc_press_hold_count++;
}
node->state.rc_state = new_state;
node->state.rc_ui_cmd = rc_press.ui_cmd;
node->state.rc_press_rx_ts = msg.rx_ts;
switch (rc_press.ui_cmd) {
case CEC_OP_UI_CMD_VOLUME_UP:
if (node->state.volume < VOLUME_MAX)
node->state.volume++;
break;
case CEC_OP_UI_CMD_VOLUME_DOWN:
if (node->state.volume > VOLUME_MIN)
node->state.volume--;
break;
case CEC_OP_UI_CMD_MUTE:
node->state.mute = !node->state.mute;
break;
case CEC_OP_UI_CMD_MUTE_FUNCTION:
node->state.mute = true;
break;
case CEC_OP_UI_CMD_RESTORE_VOLUME_FUNCTION:
node->state.mute = false;
break;
case CEC_OP_UI_CMD_POWER_TOGGLE_FUNCTION:
if (!enter_standby(node))
exit_standby(node);
break;
case CEC_OP_UI_CMD_POWER_OFF_FUNCTION:
enter_standby(node);
break;
case CEC_OP_UI_CMD_POWER_ON_FUNCTION:
exit_standby(node);
break;
}
if (rc_press.ui_cmd >= CEC_OP_UI_CMD_VOLUME_UP && rc_press.ui_cmd <= CEC_OP_UI_CMD_MUTE) {
dev_info("Volume: %d%s\n", node->state.volume, node->state.mute ? ", muted" : "");
cec_msg_set_reply_to(&msg, &msg);
cec_msg_report_audio_status(&msg, node->state.mute ? 1 : 0, node->state.volume);
transmit(node, &msg);
}
return;
}
case CEC_MSG_USER_CONTROL_RELEASED:
if (node->state.rc_state == PRESS_HOLD)
rc_press_hold_stop(&node->state);
if (node->state.rc_state == NOPRESS)
dev_info("Button release (unexpected, %ums after last press)\n",
ts_to_ms(msg.rx_ts - node->state.rc_press_rx_ts));
else if (get_duration_ms(msg.rx_ts, node->state.rc_press_rx_ts) > FOLLOWER_SAFETY_TIMEOUT)
dev_info("Button release: %s (after timeout, %ums after last press)\n",
get_ui_cmd_string(node->state.rc_ui_cmd),
ts_to_ms(msg.rx_ts - node->state.rc_press_rx_ts));
else
dev_info("Button release: %s (%ums after last press)\n",
get_ui_cmd_string(node->state.rc_ui_cmd),
ts_to_ms(msg.rx_ts - node->state.rc_press_rx_ts));
node->state.rc_state = NOPRESS;
return;
/* Device Menu Control */
case CEC_MSG_MENU_REQUEST: {
if (node->cec_version < CEC_OP_CEC_VERSION_2_0 &&
!cec_has_tv(1 << me)) {
cec_msg_set_reply_to(&msg, &msg);
cec_msg_menu_status(&msg, CEC_OP_MENU_STATE_ACTIVATED);
transmit(node, &msg);
return;
}
break;
}
case CEC_MSG_MENU_STATUS:
if (node->cec_version < CEC_OP_CEC_VERSION_2_0 &&
cec_has_tv(1 << me))
return;
break;
/* Deck Control */
case CEC_MSG_GIVE_DECK_STATUS:
if (!node->has_deck_ctl)
break;
__u8 status_req;
cec_ops_give_deck_status(&msg, &status_req);
switch (status_req) {
case CEC_OP_STATUS_REQ_ON:
node->state.deck_report_changes = true;
node->state.deck_report_changes_to = cec_msg_initiator(&msg);
fallthrough;
case CEC_OP_STATUS_REQ_ONCE:
cec_msg_set_reply_to(&msg, &msg);
cec_msg_deck_status(&msg, node->state.deck_state);
transmit(node, &msg);
return;
case CEC_OP_STATUS_REQ_OFF:
node->state.deck_report_changes = false;
return;
default:
reply_feature_abort(node, &msg, CEC_OP_ABORT_INVALID_OP);
return;
}
case CEC_MSG_PLAY:
if (!node->has_deck_ctl)
break;
__u8 deck_state;
__u8 play_mode;
cec_ops_play(&msg, &play_mode);
switch (play_mode) {
case CEC_OP_PLAY_MODE_PLAY_FWD:
exit_standby(node);
deck_state = CEC_OP_DECK_INFO_PLAY;
break;
case CEC_OP_PLAY_MODE_PLAY_REV:
deck_state = CEC_OP_DECK_INFO_PLAY_REV;
break;
case CEC_OP_PLAY_MODE_PLAY_STILL:
deck_state = CEC_OP_DECK_INFO_STILL;
break;
case CEC_OP_PLAY_MODE_PLAY_FAST_FWD_MIN:
case CEC_OP_PLAY_MODE_PLAY_FAST_FWD_MED:
case CEC_OP_PLAY_MODE_PLAY_FAST_FWD_MAX:
deck_state = CEC_OP_DECK_INFO_FAST_FWD;
break;
case CEC_OP_PLAY_MODE_PLAY_FAST_REV_MIN:
case CEC_OP_PLAY_MODE_PLAY_FAST_REV_MED:
case CEC_OP_PLAY_MODE_PLAY_FAST_REV_MAX:
deck_state = CEC_OP_DECK_INFO_FAST_REV;
break;
case CEC_OP_PLAY_MODE_PLAY_SLOW_FWD_MIN:
case CEC_OP_PLAY_MODE_PLAY_SLOW_FWD_MED:
case CEC_OP_PLAY_MODE_PLAY_SLOW_FWD_MAX:
deck_state = CEC_OP_DECK_INFO_SLOW;
break;
case CEC_OP_PLAY_MODE_PLAY_SLOW_REV_MIN:
case CEC_OP_PLAY_MODE_PLAY_SLOW_REV_MED:
case CEC_OP_PLAY_MODE_PLAY_SLOW_REV_MAX:
deck_state = CEC_OP_DECK_INFO_SLOW_REV;
break;
default:
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
transmit(node, &msg);
return;
}
/* Only Play Forward and Play Still will close tray if open. */
if (play_mode != CEC_OP_PLAY_MODE_PLAY_FWD &&
play_mode != CEC_OP_PLAY_MODE_PLAY_STILL &&
node->state.deck_state == CEC_OP_DECK_INFO_NO_MEDIA) {
reply_feature_abort(node, &msg, CEC_OP_ABORT_INCORRECT_MODE);
return;
}
node->state.deck_skip_start = 0;
update_deck_state(node, me, deck_state);
return;
case CEC_MSG_DECK_CONTROL:
if (!node->has_deck_ctl)
break;
__u8 deck_control_mode;
cec_ops_deck_control(&msg, &deck_control_mode);
switch (deck_control_mode) {
case CEC_OP_DECK_CTL_MODE_STOP:
deck_state = CEC_OP_DECK_INFO_STOP;
node->state.deck_skip_start = 0;
break;
case CEC_OP_DECK_CTL_MODE_EJECT:
exit_standby(node);
deck_state = CEC_OP_DECK_INFO_NO_MEDIA;
node->state.deck_skip_start = 0;
break;
case CEC_OP_DECK_CTL_MODE_SKIP_FWD:
/* Skip Forward will not retract the deck tray. */
if (node->state.deck_state == CEC_OP_DECK_INFO_NO_MEDIA) {
reply_feature_abort(node, &msg, CEC_OP_ABORT_INCORRECT_MODE);
return;
}
deck_state = CEC_OP_DECK_INFO_SKIP_FWD;
node->state.deck_skip_start = msg.rx_ts;
break;
case CEC_OP_DECK_CTL_MODE_SKIP_REV:
/* Skip Reverse will not retract the deck tray. */
if (node->state.deck_state == CEC_OP_DECK_INFO_NO_MEDIA) {
reply_feature_abort(node, &msg, CEC_OP_ABORT_INCORRECT_MODE);
return;
}
deck_state = CEC_OP_DECK_INFO_SKIP_REV;
node->state.deck_skip_start = msg.rx_ts;
break;
default:
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
transmit(node, &msg);
return;
}
update_deck_state(node, me, deck_state);
return;
case CEC_MSG_DECK_STATUS:
return;
/* Tuner/Record/Timer Messages */
case CEC_MSG_GIVE_TUNER_DEVICE_STATUS:
case CEC_MSG_TUNER_DEVICE_STATUS:
case CEC_MSG_SELECT_ANALOGUE_SERVICE:
case CEC_MSG_SELECT_DIGITAL_SERVICE:
case CEC_MSG_TUNER_STEP_DECREMENT:
case CEC_MSG_TUNER_STEP_INCREMENT:
process_tuner_msgs(node, msg, me, type);
return;
case CEC_MSG_RECORD_TV_SCREEN:
case CEC_MSG_RECORD_ON:
case CEC_MSG_RECORD_OFF:
case CEC_MSG_RECORD_STATUS:
process_record_msgs(node, msg, me, type);
return;
case CEC_MSG_SET_ANALOGUE_TIMER:
case CEC_MSG_SET_DIGITAL_TIMER:
case CEC_MSG_SET_EXT_TIMER:
case CEC_MSG_CLEAR_ANALOGUE_TIMER:
case CEC_MSG_CLEAR_DIGITAL_TIMER:
case CEC_MSG_CLEAR_EXT_TIMER:
case CEC_MSG_SET_TIMER_PROGRAM_TITLE:
case CEC_MSG_TIMER_CLEARED_STATUS:
case CEC_MSG_TIMER_STATUS:
process_timer_msgs(node, msg, me, type);
return;
/* Dynamic Auto Lipsync */
case CEC_MSG_REQUEST_CURRENT_LATENCY: {
__u16 phys_addr;
cec_ops_request_current_latency(&msg, &phys_addr);
if (phys_addr == node->phys_addr) {
cec_msg_init(&msg, me, from);
cec_msg_report_current_latency(&msg, phys_addr,
node->state.video_latency,
node->state.low_latency_mode,
node->state.audio_out_compensated,
node->state.audio_out_delay);
transmit(node, &msg);
}
return;
}
/* Audio Return Channel Control */
case CEC_MSG_INITIATE_ARC:
if (node->sink_has_arc_tx) {
if (!pa_is_upstream_from(node->phys_addr, remote_pa) ||
!pa_are_adjacent(node->phys_addr, remote_pa)) {
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
transmit(node, &msg);
return;
}
cec_msg_set_reply_to(&msg, &msg);
cec_msg_report_arc_initiated(&msg);
transmit(node, &msg);
node->state.arc_active = true;
dev_info("ARC is initiated");
return;
}
break;
case CEC_MSG_TERMINATE_ARC:
if (node->sink_has_arc_tx) {
if (!pa_is_upstream_from(node->phys_addr, remote_pa) ||
!pa_are_adjacent(node->phys_addr, remote_pa)) {
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
transmit(node, &msg);
return;
}
cec_msg_set_reply_to(&msg, &msg);
cec_msg_report_arc_terminated(&msg);
transmit(node, &msg);
node->state.arc_active = false;
dev_info("ARC is terminated\n");
return;
}
break;
case CEC_MSG_REQUEST_ARC_INITIATION:
if (node->source_has_arc_rx) {
if (pa_is_upstream_from(node->phys_addr, remote_pa) ||
!pa_are_adjacent(node->phys_addr, remote_pa)) {
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
transmit(node, &msg);
return;
}
cec_msg_set_reply_to(&msg, &msg);
cec_msg_initiate_arc(&msg, false);
transmit(node, &msg);
dev_info("ARC initiation has been requested.");
return;
}
break;
case CEC_MSG_REQUEST_ARC_TERMINATION:
if (node->source_has_arc_rx) {
if (pa_is_upstream_from(node->phys_addr, remote_pa) ||
!pa_are_adjacent(node->phys_addr, remote_pa)) {
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
transmit(node, &msg);
return;
}
cec_msg_set_reply_to(&msg, &msg);
cec_msg_terminate_arc(&msg, false);
transmit(node, &msg);
dev_info("ARC initiation has been requested.");
return;
}
break;
case CEC_MSG_REPORT_ARC_INITIATED:
node->state.arc_active = true;
dev_info("ARC is initiated\n");
return;
case CEC_MSG_REPORT_ARC_TERMINATED:
node->state.arc_active = false;
dev_info("ARC is terminated\n");
return;
/* System Audio Control */
case CEC_MSG_SYSTEM_AUDIO_MODE_REQUEST: {
if (!cec_has_audiosystem(1 << me))
break;
__u16 phys_addr;
cec_ops_system_audio_mode_request(&msg, &phys_addr);
cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
if (phys_addr != CEC_PHYS_ADDR_INVALID) {
cec_msg_set_system_audio_mode(&msg, CEC_OP_SYS_AUD_STATUS_ON);
transmit(node, &msg);
node->state.sac_active = true;
}
else {
cec_msg_set_system_audio_mode(&msg, CEC_OP_SYS_AUD_STATUS_OFF);
transmit(node, &msg);
node->state.sac_active = false;
}
dev_info("System Audio Mode: %s\n", node->state.sac_active ? "on" : "off");
return;
}
case CEC_MSG_SET_SYSTEM_AUDIO_MODE: {
__u8 system_audio_status;
if (!cec_msg_is_broadcast(&msg)) {
/* Directly addressed Set System Audio Mode is used to see
if the TV supports the feature. If we time out, we
signalize that we support SAC. */
if (cec_has_tv(1 << me))
return;
break;
}
cec_ops_set_system_audio_mode(&msg, &system_audio_status);
if (system_audio_status == CEC_OP_SYS_AUD_STATUS_ON)
node->state.sac_active = true;
else if (system_audio_status == CEC_OP_SYS_AUD_STATUS_OFF)
node->state.sac_active = false;
dev_info("System Audio Mode: %s\n", node->state.sac_active ? "on" : "off");
return;
}
case CEC_MSG_REPORT_AUDIO_STATUS:
return;
case CEC_MSG_GIVE_SYSTEM_AUDIO_MODE_STATUS:
if (!cec_has_audiosystem(1 << me))
break;
cec_msg_set_reply_to(&msg, &msg);
cec_msg_system_audio_mode_status(&msg, node->state.sac_active ? CEC_OP_SYS_AUD_STATUS_ON :
CEC_OP_SYS_AUD_STATUS_OFF);
transmit(node, &msg);
fallthrough;
case CEC_MSG_GIVE_AUDIO_STATUS:
if (!cec_has_audiosystem(1 << me))
break;
cec_msg_set_reply_to(&msg, &msg);
cec_msg_report_audio_status(&msg, node->state.mute ? 1 : 0, node->state.volume);
transmit(node, &msg);
return;
case CEC_MSG_SYSTEM_AUDIO_MODE_STATUS:
return;
case CEC_MSG_REQUEST_SHORT_AUDIO_DESCRIPTOR: {
if (!cec_has_audiosystem(1 << me))
break;
/* The list of formats that the follower 'supports' */
const struct short_audio_desc supported_formats[] = {
{ 2, SAD_FMT_CODE_AC3,
SAD_SAMPLE_FREQ_MASK_32 | SAD_SAMPLE_FREQ_MASK_44_1,
{ 64 }, 0, 0 },
{ 4, SAD_FMT_CODE_AC3,
SAD_SAMPLE_FREQ_MASK_32,
{ 32 }, 0, 0 },
{ 4, SAD_FMT_CODE_ONE_BIT_AUDIO,
SAD_SAMPLE_FREQ_MASK_48 | SAD_SAMPLE_FREQ_MASK_192,
{ 123 }, 0, 0 },
{ 8, SAD_FMT_CODE_EXTENDED,
SAD_SAMPLE_FREQ_MASK_96,
{ 0 }, 0, SAD_EXT_TYPE_DRA },
{ 2, SAD_FMT_CODE_EXTENDED,
SAD_SAMPLE_FREQ_MASK_176_4,
{ SAD_FRAME_LENGTH_MASK_960 | SAD_FRAME_LENGTH_MASK_1024 }, 1, SAD_EXT_TYPE_MPEG4_HE_AAC_SURROUND },
{ 2, SAD_FMT_CODE_EXTENDED,
SAD_SAMPLE_FREQ_MASK_44_1,
{ SAD_BIT_DEPTH_MASK_16 | SAD_BIT_DEPTH_MASK_24 }, 0, SAD_EXT_TYPE_LPCM_3D_AUDIO },
};
__u8 num_descriptors, audio_format_id[4], audio_format_code[4];
__u32 descriptors[4];
std::string format_list;
cec_ops_request_short_audio_descriptor(&msg, &num_descriptors, audio_format_id, audio_format_code);
if (num_descriptors == 0) {
warn("Got Request Short Audio Descriptor with no operands");
reply_feature_abort(node, &msg, CEC_OP_ABORT_INVALID_OP);
return;
}
unsigned found_descs = 0;
for (int i = 0; i < num_descriptors; i++)
format_list += audio_format_id_code2s(audio_format_id[i], audio_format_code[i]) + ",";
format_list.erase(format_list.end() - 1);
dev_info("Requested descriptors: %s\n", format_list.c_str());
for (unsigned i = 0; i < num_descriptors; i++) {
for (const auto &supported_format : supported_formats) {
if (found_descs >= 4)
break;
if ((audio_format_id[i] == 0 &&
audio_format_code[i] == supported_format.format_code) ||
(audio_format_id[i] == 1 &&
audio_format_code[i] == supported_format.extension_type_code))
sad_encode(&supported_format, &descriptors[found_descs++]);
}
}
if (found_descs > 0) {
cec_msg_set_reply_to(&msg, &msg);
cec_msg_report_short_audio_descriptor(&msg, found_descs, descriptors);
transmit(node, &msg);
}
else
reply_feature_abort(node, &msg, CEC_OP_ABORT_INVALID_OP);
return;
}
/* Device OSD Transfer */
case CEC_MSG_SET_OSD_NAME:
return;
/* Audio Rate Control */
case CEC_MSG_SET_AUDIO_RATE:
if (!node->has_aud_rate)
break;
switch (msg.msg[2]) {
case CEC_OP_AUD_RATE_OFF:
aud_rate_msg_interval_check(node, msg.rx_ts);
node->state.last_aud_rate_rx_ts = 0;
return;
case CEC_OP_AUD_RATE_WIDE_STD:
case CEC_OP_AUD_RATE_WIDE_FAST:
case CEC_OP_AUD_RATE_WIDE_SLOW:
case CEC_OP_AUD_RATE_NARROW_STD:
case CEC_OP_AUD_RATE_NARROW_FAST:
case CEC_OP_AUD_RATE_NARROW_SLOW:
aud_rate_msg_interval_check(node, msg.rx_ts);
node->state.last_aud_rate_rx_ts = msg.rx_ts;
return;
default:
cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
transmit(node, &msg);
break;
}
break;
/* CDC */
case CEC_MSG_CDC_MESSAGE: {
switch (msg.msg[4]) {
case CEC_MSG_CDC_HEC_DISCOVER:
__u16 phys_addr;
cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
cec_ops_cdc_hec_discover(&msg, &phys_addr);
cec_msg_cdc_hec_report_state(&msg, phys_addr,
CEC_OP_HEC_FUNC_STATE_NOT_SUPPORTED,
CEC_OP_HOST_FUNC_STATE_NOT_SUPPORTED,
CEC_OP_ENC_FUNC_STATE_EXT_CON_NOT_SUPPORTED,
CEC_OP_CDC_ERROR_CODE_NONE,
1, 0); // We do not support HEC on any HDMI connections
transmit(node, &msg);
return;
}
}
/* Core */
case CEC_MSG_FEATURE_ABORT:
return;
default:
break;
}
if (is_bcast)
return;
reply_feature_abort(node, &msg);
}
static void poll_remote_devs(struct node *node, unsigned me)
{
node->remote_la_mask = 0;
for (unsigned short & i : node->remote_phys_addr)
i = CEC_PHYS_ADDR_INVALID;
if (!(node->caps & CEC_CAP_TRANSMIT))
return;
for (unsigned i = 0; i < 15; i++) {
struct cec_msg msg;
cec_msg_init(&msg, me, i);
doioctl(node, CEC_TRANSMIT, &msg);
if (msg.tx_status & CEC_TX_STATUS_OK) {
node->remote_la_mask |= 1 << i;
cec_msg_init(&msg, me, i);
cec_msg_give_physical_addr(&msg, true);
doioctl(node, CEC_TRANSMIT, &msg);
if (cec_msg_status_is_ok(&msg))
node->remote_phys_addr[i] = (msg.msg[2] << 8) | msg.msg[3];
}
}
}
static void update_programmed_timers(struct node *node)
{
std::set<struct Timer>::iterator it = programmed_timers.begin();
/* Use the current minute because timers do not have second precision. */
time_t current_minute = time(nullptr) / 60;
time_t timer_start_minute = it->start_time / 60;
time_t timer_end_minute = (it->start_time + it->duration) / 60;
/* Start the timed recording only if the deck is not already recording. */
if (timer_start_minute == current_minute && !node->state.one_touch_record_on) {
node->state.one_touch_record_on = true;
node->state.recording_controlled_by_timer = true;
print_timers(node);
}
/* Delete an overlapped timer. Recording will be at best incomplete. */
if (timer_start_minute < current_minute &&
(!node->state.recording_controlled_by_timer || !node->state.one_touch_record_on)) {
programmed_timers.erase(*it);
if (show_info)
printf("Deleted overlapped timer.\n");
print_timers(node);
}
if (timer_end_minute != current_minute || !node->state.recording_controlled_by_timer)
return;
/* Delete finished timers. */
node->state.one_touch_record_on = false;
node->state.recording_controlled_by_timer = false;
node->state.media_space_available -= it->duration; /* 1 MB per second */
/*
* TODO: We are only ever decreasing the amount of space available,
* there is no heuristic that reclaims the space.
*/
if (it->recording_seq) {
struct tm *last_start_time = localtime(&(it->start_time));
int next_wday = (last_start_time->tm_wday + 1) % 7;
int days_to_move_ahead = 1;
while ((it->recording_seq & (1 << next_wday)) == 0) {
days_to_move_ahead++;
next_wday = (next_wday + 1) % 7;
}
struct Timer next_timer = {};
next_timer = *it;
last_start_time->tm_mday += days_to_move_ahead;
last_start_time->tm_isdst = -1;
next_timer.start_time = mktime(last_start_time);
programmed_timers.insert(next_timer);
}
programmed_timers.erase(*it);
if (show_info)
printf("Deleted finished timer.\n");
print_timers(node);
/*
* If the finished timer was recording, and standby was received during recording,
* enter standby when the recording stops unless recording device is the active source.
*/
if (node->state.record_received_standby) {
if (node->phys_addr != node->state.active_source_pa)
enter_standby(node);
node->state.record_received_standby = false;
}
}
void testProcessing(struct node *node, bool wallclock)
{
struct cec_log_addrs laddrs;
fd_set rd_fds;
fd_set ex_fds;
int fd = node->fd;
__u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER;
unsigned me;
unsigned last_poll_la = 15;
__u8 last_pwr_state = current_power_state(node);
time_t last_pwr_status_toggle = time(nullptr);
clock_gettime(CLOCK_MONOTONIC, &start_monotonic);
gettimeofday(&start_timeofday, nullptr);
doioctl(node, CEC_S_MODE, &mode);
doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs);
me = laddrs.log_addr[0];
__u8 type = laddrs.log_addr_type[0];
poll_remote_devs(node, me);
while (true) {
int res;
struct timeval timeval = {};
fflush(stdout);
timeval.tv_sec = 1;
FD_ZERO(&rd_fds);
FD_ZERO(&ex_fds);
FD_SET(fd, &rd_fds);
FD_SET(fd, &ex_fds);
res = select(fd + 1, &rd_fds, nullptr, &ex_fds, &timeval);
if (res < 0)
break;
if (FD_ISSET(fd, &ex_fds)) {
struct cec_event ev;
res = doioctl(node, CEC_DQEVENT, &ev);
if (res == ENODEV) {
printf("Device was disconnected.\n");
break;
}
if (res)
continue;
log_event(ev, wallclock);
if (ev.event == CEC_EVENT_STATE_CHANGE) {
dev_info("CEC adapter state change.\n");
node->phys_addr = ev.state_change.phys_addr;
node->adap_la_mask = ev.state_change.log_addr_mask;
if (node->adap_la_mask) {
doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs);
me = laddrs.log_addr[0];
} else {
node->state.active_source_pa = CEC_PHYS_ADDR_INVALID;
me = CEC_LOG_ADDR_INVALID;
}
memset(la_info, 0, sizeof(la_info));
}
}
if (FD_ISSET(fd, &rd_fds)) {
struct cec_msg msg = { };
res = doioctl(node, CEC_RECEIVE, &msg);
if (res == ENODEV) {
printf("Device was disconnected.\n");
break;
}
if (res)
continue;
__u8 from = cec_msg_initiator(&msg);
__u8 to = cec_msg_destination(&msg);
__u8 opcode = cec_msg_opcode(&msg);
if (node->ignore_la[from])
continue;
if (node->ignore_opcode[msg.msg[1]] & (1 << from))
continue;
if (from != CEC_LOG_ADDR_UNREGISTERED &&
la_info[from].feature_aborted[opcode].ts &&
ts_to_ms(get_ts() - la_info[from].feature_aborted[opcode].ts) < 200) {
warn("Received message %s from LA %d (%s) less than 200 ms after\n",
opcode2s(&msg).c_str(), from, cec_la2s(from));
warn("replying Feature Abort (not [Unrecognized Opcode]) to the same message.\n");
}
if (from != CEC_LOG_ADDR_UNREGISTERED && !la_info[from].ts)
dev_info("Logical address %d (%s) discovered.\n", from, cec_la2s(from));
if (show_msgs) {
printf(" %s to %s (%d to %d): ",
cec_la2s(from), to == 0xf ? "all" : cec_la2s(to), from, to);
cec_log_msg(&msg);
if (show_info)
printf("\tSequence: %u Rx Timestamp: %s\n",
msg.sequence, ts2s(msg.rx_ts, wallclock).c_str());
}
if (node->adap_la_mask)
processMsg(node, msg, me, type);
}
__u8 pwr_state = current_power_state(node);
if (node->cec_version >= CEC_OP_CEC_VERSION_2_0 &&
last_pwr_state != pwr_state &&
(time_to_stable > 2 || pwr_state < CEC_OP_POWER_STATUS_TO_ON)) {
struct cec_msg msg;
cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
cec_msg_report_power_status(&msg, pwr_state);
transmit(node, &msg);
last_pwr_state = pwr_state;
}
if (node->state.toggle_power_status && cec_has_tv(1 << me) &&
(time(nullptr) - last_pwr_status_toggle > node->state.toggle_power_status)) {
last_pwr_status_toggle = time(nullptr);
if (pwr_state & 1) // standby or to-standby
exit_standby(node);
else
enter_standby(node);
}
__u64 ts_now = get_ts();
unsigned poll_la = ts_to_s(ts_now) % 16;
if (poll_la != me &&
poll_la != last_poll_la && poll_la < 15 && la_info[poll_la].ts &&
ts_to_ms(ts_now - la_info[poll_la].ts) > POLL_PERIOD) {
struct cec_msg msg;
cec_msg_init(&msg, me, poll_la);
transmit(node, &msg);
if (msg.tx_status & CEC_TX_STATUS_NACK) {
dev_info("Logical address %d stopped responding to polling message.\n", poll_la);
memset(&la_info[poll_la], 0, sizeof(la_info[poll_la]));
node->remote_la_mask &= ~(1 << poll_la);
node->remote_phys_addr[poll_la] = CEC_PHYS_ADDR_INVALID;
}
}
last_poll_la = poll_la;
unsigned ms_since_press = ts_to_ms(ts_now - node->state.rc_press_rx_ts);
if (ms_since_press > FOLLOWER_SAFETY_TIMEOUT) {
if (node->state.rc_state == PRESS_HOLD)
rc_press_hold_stop(&node->state);
else if (node->state.rc_state == PRESS) {
dev_info("Button timeout: %s\n", get_ui_cmd_string(node->state.rc_ui_cmd));
node->state.rc_state = NOPRESS;
}
}
if (node->has_aud_rate)
aud_rate_msg_interval_check(node, ts_now);
if (node->state.deck_skip_start && ts_now - node->state.deck_skip_start > MAX_DECK_SKIP_NS) {
node->state.deck_skip_start = 0;
update_deck_state(node, me, CEC_OP_DECK_INFO_PLAY);
}
if (!programmed_timers.empty())
update_programmed_timers(node);
}
mode = CEC_MODE_INITIATOR;
doioctl(node, CEC_S_MODE, &mode);
}