|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# coding: utf-8 |
| 3 | +"""log-reader.py - Log reader for switcher debugging. |
| 4 | + Part of the PyATEMMax library.""" |
| 5 | + |
| 6 | +import json |
| 7 | +import argparse |
| 8 | +from datetime import datetime |
| 9 | +from dataclasses import dataclass |
| 10 | + |
| 11 | +ATEM_HEADER_LEN = 12 |
| 12 | +ATEM_CMD_HEADER_LEN = 8 |
| 13 | + |
| 14 | +@dataclass |
| 15 | +class SwitcherCommand: |
| 16 | + len: int |
| 17 | + name: str |
| 18 | + header: bytes |
| 19 | + payload: bytes |
| 20 | + |
| 21 | + |
| 22 | +@dataclass |
| 23 | +class SwitcherMessage: |
| 24 | + header_bitmask: int |
| 25 | + packet_len: int |
| 26 | + session_id: int |
| 27 | + ack_id: int |
| 28 | + resend_packet_id: int |
| 29 | + unknown: int |
| 30 | + packet_id: int |
| 31 | + commands: list |
| 32 | + |
| 33 | + |
| 34 | +@dataclass |
| 35 | +class LogMessage: |
| 36 | + ts: str |
| 37 | + source: str |
| 38 | + data: bytes |
| 39 | + message: SwitcherMessage |
| 40 | + |
| 41 | + |
| 42 | +def tsstr(ts): |
| 43 | + return f"{ts.hour:02}:{ts.minute:02}:{ts.second:02}.{ts.microsecond:06}" |
| 44 | + |
| 45 | + |
| 46 | +def hexstr(buf): |
| 47 | + return ' '.join([f'{b:02X}' for b in buf]) |
| 48 | + |
| 49 | + |
| 50 | +def log(msg="", end=None): |
| 51 | + print(f"[{tsstr(datetime.now())}] {msg}", end=end) |
| 52 | + |
| 53 | + |
| 54 | +def parse_int16(buf, pos): |
| 55 | + return (buf[pos] << 8) + buf[pos+1] |
| 56 | + |
| 57 | + |
| 58 | +def parse_int8(buf, pos): |
| 59 | + return buf[pos] |
| 60 | + |
| 61 | + |
| 62 | +def bitmask_str(bm): |
| 63 | + bmstr = "" |
| 64 | + if bm & 0x01: bmstr += "[01 ackReq] " |
| 65 | + if bm & 0x02: bmstr += "[02 hello] " |
| 66 | + if bm & 0x04: bmstr += "[04 resend] " |
| 67 | + if bm & 0x08: bmstr += "[08 reqNxtAft] " |
| 68 | + if bm & 0x10: bmstr += "[10 ack] " |
| 69 | + return bmstr |
| 70 | + |
| 71 | + |
| 72 | +def parse_command(buf): |
| 73 | + cmd_len = parse_int16(buf, 0) |
| 74 | + cmd_str = ''.join([chr(x) for x in [ buf[4], buf[5], buf[6], buf[7]]]) |
| 75 | + |
| 76 | + cmd = SwitcherCommand( |
| 77 | + cmd_len, |
| 78 | + cmd_str, |
| 79 | + buf[:ATEM_CMD_HEADER_LEN], |
| 80 | + buf[ATEM_CMD_HEADER_LEN:cmd_len], |
| 81 | + ) |
| 82 | + return(cmd, buf[cmd_len:]) |
| 83 | + |
| 84 | + |
| 85 | +def parse_switcher_message(buf): |
| 86 | + # bytes 0-1: header_bitmask / packet_len |
| 87 | + header_bitmask = buf[0] >> 3 |
| 88 | + packet_len = (buf[0] << 8 & 0x07) + (buf[1]) |
| 89 | + |
| 90 | + # bytes 2-3: session_id |
| 91 | + session_id = parse_int16(buf, 2) |
| 92 | + |
| 93 | + # bytes 4-5: ack_id |
| 94 | + ack_id = parse_int16(buf, 4) |
| 95 | + |
| 96 | + # bytes 6-7: resend_packet_id |
| 97 | + resend_packet_id = parse_int16(buf, 6) |
| 98 | + |
| 99 | + # bytes 8-9: unknown |
| 100 | + unknown = parse_int16(buf, 8) |
| 101 | + |
| 102 | + # bytes 10-11: packet_id |
| 103 | + packet_id = parse_int16(buf, 10) |
| 104 | + |
| 105 | + commands = [] |
| 106 | + |
| 107 | + if packet_len > ATEM_HEADER_LEN: |
| 108 | + cmdbuf = buf[ATEM_HEADER_LEN:] |
| 109 | + while cmdbuf: |
| 110 | + try: |
| 111 | + (cmd, cmdbuf) = parse_command(cmdbuf) |
| 112 | + commands.append(cmd) |
| 113 | + except Exception: |
| 114 | + raise |
| 115 | + # print("PASSING !!") |
| 116 | + # cmdbuf = None |
| 117 | + # pass |
| 118 | + |
| 119 | + switcher_msg = SwitcherMessage( |
| 120 | + header_bitmask, |
| 121 | + packet_len, |
| 122 | + session_id, |
| 123 | + ack_id, |
| 124 | + resend_packet_id, |
| 125 | + unknown, |
| 126 | + packet_id, |
| 127 | + commands, |
| 128 | + ) |
| 129 | + return switcher_msg |
| 130 | + |
| 131 | + |
| 132 | +def read_messages(args): |
| 133 | + messages = [] |
| 134 | + with open(args.log) as f: |
| 135 | + for line in f.readlines(): |
| 136 | + line = line[:-2] # Remove trailing comma |
| 137 | + jsondata = json.loads(line) |
| 138 | + buf = bytes.fromhex(jsondata['data']) |
| 139 | + |
| 140 | + msg = LogMessage( |
| 141 | + jsondata['ts'], |
| 142 | + jsondata['source'], |
| 143 | + buf, |
| 144 | + parse_switcher_message(buf),) |
| 145 | + messages.append(msg) |
| 146 | + |
| 147 | + show_message(args, msg) |
| 148 | + |
| 149 | + log(f"{len(messages)} messages read") |
| 150 | + return messages |
| 151 | + |
| 152 | + |
| 153 | +def show_message(args, logmsg): |
| 154 | + log("*"*80) |
| 155 | + log(f"--- {logmsg.ts} {logmsg.source} " + '-'*30) |
| 156 | + swmsg = logmsg.message |
| 157 | + print(f"[DBG] msg len {len(logmsg.data):,}") |
| 158 | + print(f"[DBG] msg {hexstr(logmsg.data)}") |
| 159 | + print(f"[DBG] header_bitmask 0x{swmsg.header_bitmask:02X} {bitmask_str(swmsg.header_bitmask)}") |
| 160 | + print(f"[DBG] packet_len 0x{swmsg.packet_len:04X} {swmsg.packet_len}") |
| 161 | + print(f"[DBG] session_id 0x{swmsg.session_id:04X}") |
| 162 | + print(f"[DBG] ack_id 0x{swmsg.ack_id:04X}") |
| 163 | + print(f"[DBG] resend_packet_id 0x{swmsg.resend_packet_id:04X}") |
| 164 | + print(f"[DBG] unknown 0x{swmsg.unknown:04X}") |
| 165 | + print(f"[DBG] packet_id 0x{swmsg.packet_id:04X}") |
| 166 | + |
| 167 | + for cmd in swmsg.commands: |
| 168 | + print(f"[DBG] - {cmd.name} ({cmd.len}b) - {hexstr(cmd.header)} - {hexstr(cmd.payload)}") |
| 169 | + |
| 170 | + xstr = "" |
| 171 | + for b in logmsg.data: |
| 172 | + char = chr(b) |
| 173 | + if char.isalpha() and char.isascii(): |
| 174 | + xstr += chr(b) |
| 175 | + else: |
| 176 | + xstr += "." |
| 177 | + if xstr: |
| 178 | + print(f"[DBG] {xstr = }") |
| 179 | + |
| 180 | + |
| 181 | +def show_messages(args, log_messages): |
| 182 | + for logmsg in log_messages: |
| 183 | + |
| 184 | + showmsg = False |
| 185 | + |
| 186 | + if True: |
| 187 | + showmsg = True |
| 188 | + |
| 189 | + if showmsg: |
| 190 | + show_message(args, logmsg) |
| 191 | + |
| 192 | + |
| 193 | +def main(): |
| 194 | + parser = argparse.ArgumentParser() |
| 195 | + parser.add_argument("log", help="log file") |
| 196 | + args = parser.parse_args() |
| 197 | + |
| 198 | + start_time = datetime.now() |
| 199 | + |
| 200 | + log("ATEM debug switcher spy - log reader") |
| 201 | + log('-'*80) |
| 202 | + |
| 203 | + log_messages = read_messages(args) |
| 204 | + # show_messages(args, log_messages) |
| 205 | + |
| 206 | + |
| 207 | +if __name__ == "__main__": |
| 208 | + main() |
0 commit comments