Skip to content

Commit 0b28078

Browse files
committedAug 18, 2022
Add tmp-dev-utils (TEMPORARY)
1 parent 67bf8ba commit 0b28078

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed
 

Diff for: ‎tmp-dev-utils/log-reader.py

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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()

Diff for: ‎tmp-dev-utils/switcher-spy.py

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
# coding: utf-8
3+
"""switcher-spy.py - Simple UDP man-in-the-middle for switcher debugging.
4+
Part of the PyATEMMax library."""
5+
6+
import socket
7+
import json
8+
import argparse
9+
from datetime import datetime
10+
11+
import importlib
12+
log_reader = importlib.import_module("log-reader")
13+
14+
args = None
15+
16+
UDP_PORT = 9910 # Standard ATEM UDP port
17+
LOG_FILE_NAME = "switcher-spy-log-{y:04}{m:02}{d:02}-{h:02}{mm:02}{s:02}.jsonl"
18+
19+
20+
def tsstr(ts):
21+
return f"{ts.hour:02}:{ts.minute:02}:{ts.second:02}.{ts.microsecond:06}"
22+
23+
24+
def log(msg="", end=None):
25+
print(f"[{tsstr(datetime.now())}] {msg}", end=end)
26+
27+
28+
def start_server(args, server_addr, switcher_addr, log_name):
29+
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
30+
server.bind(server_addr)
31+
clients = []
32+
switcher_messages = 0
33+
switcher_bytes = 0
34+
client_messages = 0
35+
client_bytes = 0
36+
37+
log("Listening, press <CTRL+C> to stop.")
38+
while True:
39+
data, addr = server.recvfrom(10240)
40+
is_switcher_msg = (addr == switcher_addr)
41+
42+
if is_switcher_msg:
43+
switcher_messages += 1
44+
switcher_bytes += len(data)
45+
for client_addr in clients:
46+
server.sendto(data, client_addr)
47+
else:
48+
client_messages += 1
49+
client_bytes += len(data)
50+
if addr not in clients:
51+
print()
52+
log(f"New client connected from {addr[0]}:{addr[1]} - total: {len(clients)}")
53+
clients.append(addr)
54+
server.sendto(data, switcher_addr)
55+
56+
total_messages = client_messages + switcher_messages
57+
total_bytes = client_bytes + switcher_bytes
58+
59+
source = "SW " if is_switcher_msg else "CLI"
60+
log("" \
61+
+ f"[TOTAL: {total_messages:,} msgs, {total_bytes:,}b]" \
62+
+ f" [Switcher: {switcher_messages:,} msgs, {switcher_bytes:,}b]" \
63+
+ f" [Client: {client_messages:,} msgs, {client_bytes:,}b]" \
64+
+ f" [Last: {source}, {len(data):,}b]" \
65+
+ " "*10
66+
, end='\r')
67+
68+
69+
if args.show_messages:
70+
if len(data) > 12:
71+
msg = log_reader.LogMessage(
72+
"now",
73+
source,
74+
data,
75+
log_reader.parse_switcher_message(data),)
76+
77+
log_reader.show_message(args, msg)
78+
79+
80+
if log_name:
81+
if len(data) > 12 or args.save_keepalive:
82+
log_obj = {
83+
"ts": tsstr(datetime.now()),
84+
"source": source,
85+
"data": "".join([f"{b:02X} " for b in data]),
86+
}
87+
log_json = json.dumps(log_obj)
88+
with open(log_name, "a") as log_file:
89+
log_file.write(f"{log_json},\n")
90+
91+
92+
def main():
93+
global args
94+
parser = argparse.ArgumentParser()
95+
parser.add_argument("ip", help="switcher IP address")
96+
parser.add_argument('-s', '--show-messages', action='store_true', help='Show parsed messages')
97+
parser.add_argument('-d', '--dry-run', action='store_true', help='Dry run, do not save output')
98+
parser.add_argument('-k', '--save-keepalive', action='store_true', help='Save "keep-alive" messages')
99+
args = parser.parse_args()
100+
101+
start_time = datetime.now()
102+
log_name = LOG_FILE_NAME.format(
103+
y = start_time.year, m = start_time.month, d = start_time.day,
104+
h = start_time.hour, mm = start_time.minute, s = start_time.second)
105+
106+
log("ATEM debug switcher spy")
107+
log('-'*80)
108+
log(f"Server port.....: {UDP_PORT}")
109+
log(f"Switcher address: {args.ip}:{UDP_PORT}")
110+
if args.dry_run:
111+
log(f"Log file........: NONE (dry run)")
112+
else:
113+
log(f"Log file........: {log_name}")
114+
log('-'*80)
115+
116+
switcher_addr = (args.ip, UDP_PORT)
117+
server_addr = ("0.0.0.0", UDP_PORT)
118+
119+
try:
120+
if args.dry_run:
121+
start_server(args, server_addr, switcher_addr, None)
122+
else:
123+
start_server(args, server_addr, switcher_addr, log_name)
124+
except KeyboardInterrupt:
125+
print()
126+
log("Server STOPPED")
127+
128+
129+
if __name__ == "__main__":
130+
main()

0 commit comments

Comments
 (0)
Please sign in to comment.