A => LICENSE +19 -0
@@ 1,19 @@
+Copyright (c) 2023 Krystian Chachuła
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
A => README.md +7 -0
@@ 1,7 @@
+# Fast CEC
+
+Turn a TV on and off using a Pulse-Eight USB - CEC Adapter.
+
+See [fastcec](fastcec) or run `fastcec --help` for usage instructions.
+
+Also see [this blog post](https://krystianch.com/cec/).
A => fastcec +103 -0
@@ 1,103 @@
+#!/usr/bin/env python3
+"""
+Copyright (c) 2023 Krystian Chachuła
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+
+import serial
+import argparse
+import sys
+
+START = 0xff
+END = 0xfe
+
+PING = 0x01
+FRAME_START = 0x05
+FRAME_DATA = 0x05
+TRANSMIT_SUCCEEDED = 0x10
+SET_CONTROLLED = 0x18
+COMMAND_ACCEPTED = 0x08
+TRANSMIT = 0x0b
+TRANSMIT_EOM = 0x0c
+TRANSMIT_IDLETIME = 0x0d
+TRANSMIT_ACK_POLARITY = 0x0e
+
+FRAME_EOM = 0x80
+FRAME_ACK = 0x40
+
+parser = argparse.ArgumentParser(description="Turn a TV on and off using a "
+ "Pulse-Eight USB - CEC Adapter.")
+parser.add_argument("port", help="serial port to use (e.g. /dev/ttyACM0)")
+parser.add_argument("power", choices=("on", "standby"),
+ help="desired power state")
+parser.add_argument("-t", "--timeout", type=float, default=1.0)
+parser.add_argument('--version', action='version', version='%(prog)s 0.1')
+
+def command_write(ser, command, *args):
+ ser.write(bytes([START, command, *args, END]))
+
+def command_read(ser):
+ res = ser.read_until(bytes([END]))
+ if res[0] != START or res[-1] != END:
+ raise RuntimeError("Invalid response: %s" % res)
+
+ cmd, *data = res[1:-1]
+ cmd = cmd & ~FRAME_ACK
+
+ return cmd, *data
+
+def expect(ser, *expected):
+ res = command_read(ser)
+ if res != expected:
+ raise RuntimeError("Unexpected response. Expected %s, got %s"
+ % (expected, res))
+
+if __name__ == "__main__":
+ args = parser.parse_args()
+
+ try:
+ ser = serial.Serial(args.port, 38400, timeout=args.timeout)
+ except serial.SerialException as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+
+ try:
+ command_write(ser, PING)
+ expect(ser, COMMAND_ACCEPTED, PING)
+
+ command_write(ser, SET_CONTROLLED, 1)
+ expect(ser, COMMAND_ACCEPTED, SET_CONTROLLED)
+
+ command_write(ser, TRANSMIT_ACK_POLARITY, 0)
+ command_write(ser, TRANSMIT, 0x10)
+ command_write(ser, TRANSMIT_EOM, {"on": 0x04, "standby": 0x36}[args.power])
+ expect(ser, COMMAND_ACCEPTED, TRANSMIT_ACK_POLARITY)
+ expect(ser, COMMAND_ACCEPTED, TRANSMIT)
+ expect(ser, COMMAND_ACCEPTED, TRANSMIT_EOM)
+ expect(ser, TRANSMIT_SUCCEEDED)
+
+ command_write(ser, SET_CONTROLLED, 0)
+ expect(ser, COMMAND_ACCEPTED, SET_CONTROLLED)
+
+ except (serial.SerialException, RuntimeError) as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+ finally:
+ ser.close()
A => scripts/slsnif_decode.py +107 -0
@@ 1,107 @@
+import sys
+from itertools import groupby
+
+START = 0xff
+END = 0xfe
+
+# https://github.com/Pulse-Eight/libcec
+# GNU GPL 2.0 licensed
+# Copyright (C) 2011-2020 Pulse-Eight Limited
+ACK = 0x40
+EOM = 0x80
+MSG_CODES = [
+ "NOTHING",
+ "PING",
+ "TIMEOUT_ERROR",
+ "HIGH_ERROR",
+ "LOW_ERROR",
+ "FRAME_START",
+ "FRAME_DATA",
+ "RECEIVE_FAILED",
+ "COMMAND_ACCEPTED",
+ "COMMAND_REJECTED",
+ "SET_ACK_MASK",
+ "TRANSMIT",
+ "TRANSMIT_EOM",
+ "TRANSMIT_IDLETIME",
+ "TRANSMIT_ACK_POLARITY",
+ "TRANSMIT_LINE_TIMEOUT",
+ "TRANSMIT_SUCCEEDED",
+ "TRANSMIT_FAILED_LINE",
+ "TRANSMIT_FAILED_ACK",
+ "TRANSMIT_FAILED_TIMEOUT_DATA",
+ "TRANSMIT_FAILED_TIMEOUT_LINE",
+ "FIRMWARE_VERSION",
+ "START_BOOTLOADER",
+ "GET_BUILDDATE",
+ "SET_CONTROLLED",
+ "GET_AUTO_ENABLED",
+ "SET_AUTO_ENABLED",
+ "GET_DEFAULT_LOGICAL_ADDRESS",
+ "SET_DEFAULT_LOGICAL_ADDRESS",
+ "GET_LOGICAL_ADDRESS_MASK",
+ "SET_LOGICAL_ADDRESS_MASK",
+ "GET_PHYSICAL_ADDRESS",
+ "SET_PHYSICAL_ADDRESS",
+ "GET_DEVICE_TYPE",
+ "SET_DEVICE_TYPE",
+ "GET_HDMI_VERSION",
+ "SET_HDMI_VERSION",
+ "GET_OSD_NAME",
+ "SET_OSD_NAME",
+ "WRITE_EEPROM",
+ "GET_ADAPTER_TYPE",
+ "SET_ACTIVE_SOURCE",
+ "GET_AUTO_POWER_ON",
+ "SET_AUTO_POWER_ON",
+]
+
+def parse_bytes(stream):
+ for line in stream:
+ line = line.strip()
+ if not line:
+ continue
+
+ side, _, rest = line.partition(b"-->")
+ side = side.strip()
+
+ while True:
+ start = rest.find(b"(")
+ end = rest.find(b")", start + 1)
+ while (idx := rest.find(b"(", start + 1, end)) != -1:
+ start = idx
+ if end == -1:
+ break
+ byte = int(rest[(start + 1):end])
+ assert 0 <= byte <= 255
+ yield side.decode(), byte
+ rest = rest[(end + 1):]
+
+
+def parse_commands(raw_bytes):
+ for side, payload in groupby(raw_bytes, key=lambda x: x[0]):
+ payload = [x[1] for x in payload]
+ while True:
+ if not payload:
+ break
+ assert payload[0] == 0xff
+ end = payload.index(0xfe)
+ command = payload[1]
+ yield side, command, payload[2:end]
+ payload = payload[(end + 1):]
+
+
+
+if __name__ == "__main__":
+ raw_bytes = parse_bytes(sys.stdin.buffer)
+
+ for side, command, args in parse_commands(raw_bytes):
+ ack = command & ACK
+ command &= ~ACK
+
+ eom = command & EOM
+ command &= ~EOM
+
+ name = "%s%s%s (0x%02x)" % (MSG_CODES[command],
+ " ACK" if ack else "", " EOM" if eom else "", command)
+ print("OUT" if side == "Host" else " IN", name, "args", *("0x%02x" % x for x in args))