• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
2 /*
3  * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
4  */
5 
6 #include <cstring>
7 #include <ctime>
8 #include <sstream>
9 
10 #include <fcntl.h>
11 #include <getopt.h>
12 #include <sys/ioctl.h>
13 
14 #include "cec-follower.h"
15 #include "compiler.h"
16 
17 /* Short option list
18 
19    Please keep in alphabetical order.
20    That makes it easier to see which short options are still free.
21 
22    In general the lower case is used to set something and the upper
23    case is used to retrieve a setting. */
24 enum Option {
25 	OptSetAdapter = 'a',
26 	OptSetDevice = 'd',
27 	OptSetDriver = 'D',
28 	OptExclusive = 'e',
29 	OptHelp = 'h',
30 	OptIgnore = 'i',
31 	OptNoWarnings = 'n',
32 	OptTrace = 'T',
33 	OptVerbose = 'v',
34 	OptShowMsgs = 'm',
35 	OptShowState = 's',
36 	OptWallClock = 'w',
37 	OptServiceByDigID = 128,
38 	OptStandby,
39 	OptTogglePowerStatus,
40 	OptIgnoreStandby,
41 	OptIgnoreViewOn,
42 	OptVersion,
43 	OptLast = 256
44 };
45 
46 static char options[OptLast];
47 
48 bool show_info;
49 bool show_msgs;
50 bool show_state;
51 bool show_warnings = true;
52 unsigned warnings;
53 std::set<struct Timer> programmed_timers;
54 
55 static struct option long_options[] = {
56 	{ "device", required_argument, nullptr, OptSetDevice },
57 	{ "adapter", required_argument, nullptr, OptSetAdapter },
58 	{ "driver", required_argument, nullptr, OptSetDriver },
59 	{ "exclusive", no_argument, 0, OptExclusive },
60 	{ "help", no_argument, nullptr, OptHelp },
61 	{ "no-warnings", no_argument, nullptr, OptNoWarnings },
62 	{ "trace", no_argument, nullptr, OptTrace },
63 	{ "verbose", no_argument, nullptr, OptVerbose },
64 	{ "show-msgs", no_argument, nullptr, OptShowMsgs },
65 	{ "show-state", no_argument, nullptr, OptShowState },
66 	{ "wall-clock", no_argument, nullptr, OptWallClock },
67 	{ "service-by-dig-id", no_argument, nullptr, OptServiceByDigID },
68 	{ "standby", no_argument, nullptr, OptStandby },
69 	{ "toggle-power-status", required_argument, nullptr, OptTogglePowerStatus },
70 	{ "ignore-standby", required_argument, nullptr, OptIgnoreStandby },
71 	{ "ignore-view-on", required_argument, nullptr, OptIgnoreViewOn },
72 	{ "ignore", required_argument, nullptr, OptIgnore },
73 	{ "version", no_argument, nullptr, OptVersion },
74 
75 	{ nullptr, 0, nullptr, 0 }
76 };
77 
78 #define STR(x) #x
79 #define STRING(x) STR(x)
80 
usage()81 static void usage()
82 {
83 	printf("Usage:\n"
84 	       "  -d, --device <dev>  Use device <dev> instead of /dev/cec0\n"
85 	       "                      If <dev> starts with a digit, then /dev/cec<dev> is used.\n"
86 	       "  -D, --driver <driver>    Use a cec device with this driver name\n"
87 	       "  -a, --adapter <adapter>  Use a cec device with this adapter name\n"
88 	       "  -h, --help          Display this help message\n"
89 	       "  -n, --no-warnings   Turn off warning messages\n"
90 	       "  -T, --trace         Trace all called ioctls\n"
91 	       "  -v, --verbose       Turn on verbose reporting\n"
92 	       "  -w, --wall-clock    Show timestamps as wall-clock time (implies -v)\n"
93 	       "  -e, --exclusive     If specified, then the follower uses exclusive mode (CEC_MODE_EXCL_FOLLOWER)\n"
94 	       "  -m, --show-msgs     Show received messages\n"
95 	       "  -s, --show-state    Show state changes from the emulated device\n"
96 	       "  --service-by-dig-id Report digital services by digital ID instead of by channel\n"
97 	       "  --standby           Start in Standby state\n"
98 	       "  --toggle-power-status <secs>\n"
99 	       "                      Toggle the power status every <secs> seconds\n"
100 	       "  --ignore-standby <n>\n"
101 	       "                      Ignore every <n>th received Standby message\n"
102 	       "  --ignore-view-on <n>\n"
103 	       "                      Ignore every <n>th received Image/Text View On message\n"
104 	       "  -i, --ignore <la>,<opcode>\n"
105 	       "                      Ignore messages from logical address <la> and opcode\n"
106 	       "                      <opcode>. 'all' can be used for <la> or <opcode> to match\n"
107 	       "                      all logical addresses or opcodes.\n"
108 	       "  --version           Show version information\n"
109 	       );
110 }
111 
sad_encode(const struct short_audio_desc * sad,__u32 * descriptor)112 void sad_encode(const struct short_audio_desc *sad, __u32 *descriptor)
113 {
114 	__u8 b1, b2, b3 = 0;
115 
116 	b1 = (sad->num_channels - 1) & 0x07;
117 	b1 |= (sad->format_code & 0x0f) << 3;
118 
119 	b2 = sad->sample_freq_mask;
120 
121 	switch (sad->format_code) {
122 	case SAD_FMT_CODE_LPCM:
123 		b3 = sad->bit_depth_mask & 0x07;
124 		break;
125 	case 2:
126 	case 3:
127 	case 4:
128 	case 5:
129 	case 6:
130 	case 7:
131 	case 8:
132 		b3 = sad->max_bitrate;
133 		break;
134 	case 9:
135 	case 10:
136 	case 11:
137 	case 12:
138 	case 13:
139 		b3 = sad->format_dependent;
140 		break;
141 	case SAD_FMT_CODE_WMA_PRO:
142 		b3 = sad->wma_profile & 0x03;
143 		break;
144 	case SAD_FMT_CODE_EXTENDED:
145 		b3 = (sad->extension_type_code & 0x1f) << 3;
146 
147 		switch (sad->extension_type_code) {
148 		case 4:
149 		case 5:
150 		case 6:
151 			b3 |= (sad->frame_length_mask & 0x03) << 1;
152 			break;
153 		case 8:
154 		case 10:
155 			b3 |= (sad->frame_length_mask & 0x03) << 1;
156 			b3 |= sad->mps & 1;
157 			break;
158 		case SAD_EXT_TYPE_MPEG_H_3D_AUDIO:
159 		case SAD_EXT_TYPE_AC_4:
160 			b3 |= sad->format_dependent & 0x07;
161 			fallthrough;
162 		case SAD_EXT_TYPE_LPCM_3D_AUDIO:
163 			b3 |= sad->bit_depth_mask & 0x07;
164 			break;
165 		}
166 		break;
167 	}
168 
169 	*descriptor = (b1 << 16) | (b2 << 8) | b3;
170 }
171 
audio_format_code2s(__u8 format_code)172 static std::string audio_format_code2s(__u8 format_code)
173 {
174 	switch (format_code) {
175 	case 0:
176 		return "Reserved";
177 	case SAD_FMT_CODE_LPCM:
178 		return "L-PCM";
179 	case SAD_FMT_CODE_AC3:
180 		return "AC-3";
181 	case SAD_FMT_CODE_MPEG1:
182 		return "MPEG-1";
183 	case SAD_FMT_CODE_MP3:
184 		return "MP3";
185 	case SAD_FMT_CODE_MPEG2:
186 		return "MPEG2";
187 	case SAD_FMT_CODE_AAC_LC:
188 		return "AAC LC";
189 	case SAD_FMT_CODE_DTS:
190 		return "DTS";
191 	case SAD_FMT_CODE_ATRAC:
192 		return "ATRAC";
193 	case SAD_FMT_CODE_ONE_BIT_AUDIO:
194 		return "One Bit Audio";
195 	case SAD_FMT_CODE_ENHANCED_AC3:
196 		return "Enhanced AC-3";
197 	case SAD_FMT_CODE_DTS_HD:
198 		return "DTS-HD";
199 	case SAD_FMT_CODE_MAT:
200 		return "MAT";
201 	case SAD_FMT_CODE_DST:
202 		return "DST";
203 	case SAD_FMT_CODE_WMA_PRO:
204 		return "WMA Pro";
205 	case SAD_FMT_CODE_EXTENDED:
206 		return "Extended";
207 	default:
208 		return "Illegal";
209 	}
210 }
211 
extension_type_code2s(__u8 type_code)212 static std::string extension_type_code2s(__u8 type_code)
213 {
214 	switch (type_code) {
215 	case 0:
216 	case 1:
217 	case 2:
218 	case 3:
219 		return "Not in use";
220 	case SAD_EXT_TYPE_MPEG4_HE_AAC:
221 		return "MPEG-4 HE AAC";
222 	case SAD_EXT_TYPE_MPEG4_HE_AACv2:
223 		return "MPEG-4 HE AAC v2";
224 	case SAD_EXT_TYPE_MPEG4_AAC_LC:
225 		return "MPEG-4 AAC LC";
226 	case SAD_EXT_TYPE_DRA:
227 		return "DRA";
228 	case SAD_EXT_TYPE_MPEG4_HE_AAC_SURROUND:
229 		return "MPEG-4 HE AAC + MPEG Surround";
230 	case SAD_EXT_TYPE_MPEG4_AAC_LC_SURROUND:
231 		return "MPEG-4 AAC LC + MPEG Surround";
232 	case SAD_EXT_TYPE_MPEG_H_3D_AUDIO:
233 		return "MPEG-H 3D Audio";
234 	case SAD_EXT_TYPE_AC_4:
235 		return "AC-4";
236 	case SAD_EXT_TYPE_LPCM_3D_AUDIO:
237 		return "L-PCM 3D Audio";
238 	default:
239 		return "Reserved";
240 	}
241 }
242 
audio_format_id_code2s(__u8 audio_format_id,__u8 audio_format_code)243 std::string audio_format_id_code2s(__u8 audio_format_id, __u8 audio_format_code)
244 {
245 	if (audio_format_id == 0)
246 		return audio_format_code2s(audio_format_code);
247 	if (audio_format_id == 1)
248 		return extension_type_code2s(audio_format_code);
249 	return "Invalid";
250 }
251 
opcode2s(const struct cec_msg * msg)252 std::string opcode2s(const struct cec_msg *msg)
253 {
254 	std::stringstream oss;
255 	__u8 opcode = msg->msg[1];
256 	const char *name;
257 
258 	if (opcode == CEC_MSG_CDC_MESSAGE) {
259 		__u8 cdc_opcode = msg->msg[4];
260 		name = cec_cdc_opcode2s(cdc_opcode);
261 
262 		if (name)
263 			return name;
264 		oss << "CDC: 0x" << std::hex << static_cast<unsigned>(cdc_opcode);
265 		return oss.str();
266 	}
267 
268 	name = cec_opcode2s(opcode);
269 
270 	if (name)
271 		return name;
272 	oss << "0x" << std::hex << static_cast<unsigned>(opcode);
273 	return oss.str();
274 }
275 
cec_named_ioctl(int fd,const char * name,unsigned long int request,void * parm)276 int cec_named_ioctl(int fd, const char *name,
277 		    unsigned long int request, void *parm)
278 {
279 	int retval;
280 	int e;
281 
282 	retval = ioctl(fd, request, parm);
283 
284 	e = retval == 0 ? 0 : errno;
285 	if (options[OptTrace])
286 		printf("\t\t%s returned %d (%s)\n",
287 			name, retval, strerror(e));
288 
289 	if (!retval) {
290 		const auto msg = static_cast<const struct cec_msg *>(parm);
291 
292 		/* Update the timestamp whenever we successfully transmit to an LA,
293 		   or whenever we receive something from the LA */
294 		if (request == CEC_TRANSMIT && (msg->tx_status & CEC_TX_STATUS_OK) &&
295 		    !cec_msg_is_broadcast(msg)) {
296 			if (msg->timeout) {
297 				if (msg->rx_status & (CEC_RX_STATUS_OK | CEC_RX_STATUS_FEATURE_ABORT))
298 					la_info[cec_msg_initiator(msg)].ts = msg->rx_ts;
299 			} else
300 				la_info[cec_msg_destination(msg)].ts = msg->tx_ts;
301 		}
302 		if (request == CEC_RECEIVE &&
303 		    cec_msg_initiator(msg) != CEC_LOG_ADDR_UNREGISTERED &&
304 		    (msg->rx_status & CEC_RX_STATUS_OK))
305 			la_info[cec_msg_initiator(msg)].ts = msg->rx_ts;
306 	}
307 
308 	return retval == -1 ? e : (retval ? -1 : 0);
309 }
310 
print_timers(struct node * node)311 void print_timers(struct node *node)
312 {
313 	if (show_info) {
314 		printf("Timers Set:\n");
315 		if (node->state.recording_controlled_by_timer)
316 			printf("Deck is currently recording from the first timer.\n");
317 		if (node->state.one_touch_record_on && !node->state.recording_controlled_by_timer)
318 			printf("Deck is currently recording independent of timers.\n");
319 		for (auto &t : programmed_timers) {
320 			std::string start = ctime(&t.start_time);
321 			time_t end_time = t.start_time + t.duration;
322 			std::string end = ctime(&end_time);
323 			/* Remove the seconds because timer is precise only to the minute. */
324 			start.erase(16, 3);
325 			end.erase(16, 3);
326 			/* Remove the new line characters. */
327 			start.erase(start.end() - 1);
328 			end.erase(end.end() - 1);
329 			/* Remove the start year if it is the same as the end year. */
330 			if ((start.compare(start.size() - 4, 5, end, end.size() - 4, 5) == 0))
331 				start.erase(start.size() - 5, 5);
332 			printf("\t%s - %s, ", start.c_str(), end.c_str());
333 			/* Find and print the source. */
334 			std::string source;
335 			switch (t.src.type) {
336 			case CEC_OP_RECORD_SRC_OWN:
337 				source = "own";
338 				break;
339 			case CEC_OP_RECORD_SRC_DIGITAL:
340 				source = "digital";
341 				break;
342 			case CEC_OP_RECORD_SRC_ANALOG:
343 				source = "analog";
344 				break;
345 			case CEC_OP_RECORD_SRC_EXT_PLUG:
346 				source = "ext plug";
347 				break;
348 			case CEC_OP_RECORD_SRC_EXT_PHYS_ADDR:
349 				source = "ext phy addr";
350 				break;
351 			default:
352 				break;
353 			}
354 			printf("source: %s, ", source.c_str());
355 			if (t.recording_seq)
356 				printf("rec-seq: 0x%x, ", t.recording_seq);
357 			printf("needs: %d %s\n", t.duration, "MB."); /* 1MB per second. */
358 		}
359 		printf("Total media space available for recording: ");
360 		if (node->state.media_space_available >= 0)
361 			printf("%d MB.\n\n", node->state.media_space_available);
362 		else
363 			printf("0 MB.\n\n");
364 	}
365 }
366 
state_init(struct node & node)367 void state_init(struct node &node)
368 {
369 	if (options[OptStandby])
370 		node.state.power_status = CEC_OP_POWER_STATUS_STANDBY;
371 	else
372 		node.state.power_status = CEC_OP_POWER_STATUS_ON;
373 	node.state.old_power_status = CEC_OP_POWER_STATUS_ON;
374 	node.state.power_status_changed_time = 0;
375 	strcpy(node.state.menu_language, "eng");
376 	node.state.video_latency = 10;
377 	node.state.low_latency_mode = 1;
378 	node.state.audio_out_compensated = 3;
379 	node.state.audio_out_delay = 20;
380 	node.state.arc_active = false;
381 	node.state.sac_active = false;
382 	node.state.volume = 50;
383 	node.state.mute = false;
384 	node.state.deck_report_changes = false;
385 	node.state.deck_report_changes_to = 0;
386 	node.state.deck_state = CEC_OP_DECK_INFO_STOP;
387 	node.state.deck_skip_start = 0;
388 	node.state.one_touch_record_on = false;
389 	node.state.record_received_standby = false;
390 	node.state.media_space_available = 36000; /* In MB; space for 10 hours @ 1MB/sec */
391 	node.state.recording_controlled_by_timer = false;
392 	tuner_dev_info_init(&node.state);
393 	node.state.last_aud_rate_rx_ts = 0;
394 }
395 
main(int argc,char ** argv)396 int main(int argc, char **argv)
397 {
398 	std::string device;
399 	struct node node = { };
400 	const char *driver = nullptr;
401 	const char *adapter = nullptr;
402 	unsigned toggle_power_status = 0;
403 	char short_options[26 * 2 * 2 + 1];
404 	int idx = 0;
405 	int fd = -1;
406 	int ch;
407 	int i;
408 
409 	for (i = 0; long_options[i].name; i++) {
410 		if (!isalpha(long_options[i].val))
411 			continue;
412 		short_options[idx++] = long_options[i].val;
413 		if (long_options[i].has_arg == required_argument) {
414 			short_options[idx++] = ':';
415 		} else if (long_options[i].has_arg == optional_argument) {
416 			short_options[idx++] = ':';
417 			short_options[idx++] = ':';
418 		}
419 	}
420 	while (true) {
421 		int option_index = 0;
422 
423 		short_options[idx] = 0;
424 		ch = getopt_long(argc, argv, short_options,
425 				 long_options, &option_index);
426 		if (ch == -1)
427 			break;
428 
429 		options[ch] = 1;
430 		if (!option_index) {
431 			for (i = 0; long_options[i].val; i++) {
432 				if (long_options[i].val == ch) {
433 					option_index = i;
434 					break;
435 				}
436 			}
437 		}
438 		if (long_options[option_index].has_arg == optional_argument &&
439 		    !optarg && argv[optind] && argv[optind][0] != '-')
440 			optarg = argv[optind++];
441 
442 		switch (ch) {
443 		case OptHelp:
444 			usage();
445 			return 0;
446 		case OptSetDevice:
447 			device = optarg;
448 			if (device[0] >= '0' && device[0] <= '9' && device.length() <= 3) {
449 				static char newdev[20];
450 
451 				sprintf(newdev, "/dev/cec%s", optarg);
452 				device = newdev;
453 			}
454 			break;
455 		case OptSetDriver:
456 			driver = optarg;
457 			break;
458 		case OptSetAdapter:
459 			adapter = optarg;
460 			break;
461 		case OptNoWarnings:
462 			show_warnings = false;
463 			break;
464 		case OptShowMsgs:
465 			show_msgs = true;
466 			break;
467 		case OptShowState:
468 			show_state = true;
469 			break;
470 		case OptTogglePowerStatus:
471 			toggle_power_status = strtoul(optarg, nullptr, 0);
472 			break;
473 		case OptIgnoreStandby:
474 			node.ignore_standby = strtoul(optarg, nullptr, 0);
475 			break;
476 		case OptIgnoreViewOn:
477 			node.ignore_view_on = strtoul(optarg, nullptr, 0);
478 			break;
479 		case OptIgnore: {
480 			bool all_la = !strncmp(optarg, "all", 3);
481 			bool all_opcodes = true;
482 			const char *sep = std::strchr(optarg, ',');
483 			unsigned la_mask = 0xffff, opcode, la = 0;
484 
485 			if (sep)
486 				all_opcodes = !strncmp(sep + 1, "all", 3);
487 			if (!all_la) {
488 				la = strtoul(optarg, nullptr, 0);
489 
490 				if (la > 15) {
491 					fprintf(stderr, "invalid logical address (> 15)\n");
492 					usage();
493 					return 1;
494 				}
495 				la_mask = 1 << la;
496 			}
497 			if (!all_opcodes) {
498 				opcode = strtoul(sep + 1, nullptr, 0);
499 				if (opcode > 255) {
500 					fprintf(stderr, "invalid opcode (> 255)\n");
501 					usage();
502 					return 1;
503 				}
504 				node.ignore_opcode[opcode] |= la_mask;
505 				break;
506 			}
507 			if (all_la && all_opcodes) {
508 				fprintf(stderr, "all,all is invalid\n");
509 				usage();
510 				return 1;
511 			}
512 			node.ignore_la[la] = true;
513 			break;
514 		}
515 		case OptWallClock:
516 		case OptVerbose:
517 			show_info = true;
518 			show_msgs = true;
519 			show_state = true;
520 			break;
521 		case OptVersion:
522 			printf("cec-follower %s%s\n", PACKAGE_VERSION, STRING(GIT_COMMIT_CNT));
523 			if (strlen(STRING(GIT_SHA)))
524 				printf("cec-follower SHA: %s %s\n",
525 				       STRING(GIT_SHA), STRING(GIT_COMMIT_DATE));
526 			std::exit(EXIT_SUCCESS);
527 		case ':':
528 			fprintf(stderr, "Option '%s' requires a value\n",
529 				argv[optind]);
530 			usage();
531 			return 1;
532 		case '?':
533 			if (argv[optind])
534 				fprintf(stderr, "Unknown argument '%s'\n", argv[optind]);
535 			usage();
536 			return 1;
537 		}
538 	}
539 	if (optind < argc) {
540 		printf("unknown arguments: ");
541 		while (optind < argc)
542 			printf("%s ", argv[optind++]);
543 		printf("\n");
544 		usage();
545 		return 1;
546 	}
547 
548 	if (device.empty() && (driver || adapter)) {
549 		device = cec_device_find(driver, adapter);
550 		if (device.empty()) {
551 			fprintf(stderr,
552 				"Could not find a CEC device for the given driver/adapter combination\n");
553 			std::exit(EXIT_FAILURE);
554 		}
555 	}
556 	if (device.empty())
557 		device = "/dev/cec0";
558 
559 	if ((fd = open(device.c_str(), O_RDWR)) < 0) {
560 		fprintf(stderr, "Failed to open %s: %s\n", device.c_str(),
561 			strerror(errno));
562 		std::exit(EXIT_FAILURE);
563 	}
564 
565 	struct cec_caps caps = { };
566 
567 	node.fd = fd;
568 	node.device = device.c_str();
569 	doioctl(&node, CEC_ADAP_G_CAPS, &caps);
570 	node.caps = caps.capabilities;
571 	node.available_log_addrs = caps.available_log_addrs;
572 	node.state.service_by_dig_id = options[OptServiceByDigID];
573 	node.state.toggle_power_status = toggle_power_status;
574 	state_init(node);
575 
576 	if (strlen(STRING(GIT_SHA)))
577 		printf("cec-follower SHA                   : %s %s\n",
578 		       STRING(GIT_SHA), STRING(GIT_COMMIT_DATE));
579 
580 	doioctl(&node, CEC_ADAP_G_PHYS_ADDR, &node.phys_addr);
581 
582 	struct cec_log_addrs laddrs = { };
583 	doioctl(&node, CEC_ADAP_G_LOG_ADDRS, &laddrs);
584 	node.adap_la_mask = laddrs.log_addr_mask;
585 	node.cec_version = laddrs.cec_version;
586 
587 	struct cec_connector_info conn_info = {};
588 
589 	doioctl(&node, CEC_ADAP_G_CONNECTOR_INFO, &conn_info);
590 
591 	cec_driver_info(caps, laddrs, node.phys_addr, conn_info);
592 
593 	/*
594 	 * For CEC 1.4, features of a logical address may still be
595 	 * filled in according to the CEC 2.0 guidelines even though
596 	 * the CEC framework won’t use the features in the CEC 2.0
597 	 * CEC_MSG_REPORT_FEATURES.
598 	 */
599 	bool is_dev_feat = false;
600 
601 	for (__u8 byte : laddrs.features[0]) {
602 		if (is_dev_feat) {
603 			node.source_has_arc_rx = (byte & CEC_OP_FEAT_DEV_SOURCE_HAS_ARC_RX) != 0;
604 			node.sink_has_arc_tx = (byte & CEC_OP_FEAT_DEV_SINK_HAS_ARC_TX) != 0;
605 			node.has_aud_rate = (byte & CEC_OP_FEAT_DEV_HAS_SET_AUDIO_RATE) != 0;
606 			node.has_deck_ctl = (byte & CEC_OP_FEAT_DEV_HAS_DECK_CONTROL) != 0;
607 			node.has_rec_tv = (byte & CEC_OP_FEAT_DEV_HAS_RECORD_TV_SCREEN) != 0;
608 			node.has_osd_string = (byte & CEC_OP_FEAT_DEV_HAS_SET_OSD_STRING) != 0;
609 			break;
610 		}
611 		if (byte & CEC_OP_FEAT_EXT)
612 			continue;
613 		if (!is_dev_feat)
614 			is_dev_feat = true;
615 		else
616 			break;
617 	}
618 	printf("\n");
619 
620 	if (laddrs.num_log_addrs == 0 && (node.caps & CEC_CAP_LOG_ADDRS)) {
621 		printf("\nFAIL: missing logical address(es), use cec-ctl to configure this\n");
622 		std::exit(EXIT_FAILURE);
623 	}
624 
625 	testProcessing(&node, options[OptExclusive], options[OptWallClock]);
626 }
627