diff --git a/README.md b/README.md index 4b18711..3e3d27e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,29 @@ https://telavivmakers.org/tamiwiki/projects/thermalcam +### Starting the stream + +#### Enable jumbo frames + +``` +sudo ip link set eth0 mtu 9000 +``` + +#### Send start packet +You need to send a special packet. + +Sending it via sudo because of raw sockets: +```bash +sudo ./venv/bin/python ./replay.py +``` + +To send it you need the capability to open sockets in raw mode, but that does not work well with scripts (see [1] + +[1] setcap for executables, not helpful for python scripts: +``` +setcap cap_net_raw,cap_net_admin=eip ./replay.py +``` + ### Rust lib usage # if you don't already have a virtualenv. Linux specific, adjust to your OS. diff --git a/decode.py b/decode.py old mode 100755 new mode 100644 index aa6152c..f2b6a2f --- a/decode.py +++ b/decode.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 import argparse +from pathlib import Path import os import subprocess +from io import BytesIO import numpy as np from tqdm import tqdm +from datetime import datetime import pandas as pd import pcapng from struct import unpack @@ -12,6 +15,7 @@ from PIL import Image # Create the parser parser = argparse.ArgumentParser(description="Process a pcap file.") +parser.add_argument("--live", action="store_true", help="Process images live") # Add an argument for the pcap file, with a default value parser.add_argument('input_file', nargs='?', default='in.pcap', help='The pcap file to process') @@ -19,31 +23,57 @@ parser.add_argument('input_file', nargs='?', default='in.pcap', help='The pcap f # Parse the arguments args = parser.parse_args() -# Now use args.input_file as the file to process -input_file = args.input_file -basename = os.path.splitext(os.path.basename(input_file))[0] + +# TODO - probably a better way to do this +def live_capture(): + while True: + # TODO: This is wrong: + # during scapy.all.sniff construction we lose about 27 packets. + # we work around it by capturing 2* packets(frame), so we just lose + # about half the frames. + cap = scapy.all.sniff(iface="enp1s0f0", count=32 * 2) + for pkt in cap: + l = len(bytes(pkt)) + if l != 6972: + continue + data = bytes(pkt) + with open('udp.bytes', 'ab+') as fd: + fd.write(bytes(pkt.payload)) + yield data -# Read packets from a pcap file -scanner = pcapng.scanner.FileScanner(open(input_file, "rb")) -blocks = tqdm(scanner) - -# Helper function to safely get an attribute from an object -def tryget(obj, att): - if hasattr(obj, att): - return getattr(obj, att) - return None - +if args.live: + print('live stream, import scapy') + import scapy.all + print('open stream') + stream = live_capture() + start = datetime.now() + timestamp = start.strftime('%Y%m%d_%H%M%S') + basename = f'live_{timestamp}' + blocks = tqdm(stream) +else: + # Now use args.input_file as the file to process + input_file = args.input_file + basename = os.path.splitext(os.path.basename(input_file))[0] + stream = open(input_file, 'rb') + # Read packets from a pcap file + scanner = pcapng.scanner.FileScanner(stream) + blocks = tqdm(scanner) def rightsize(it): for i, obj in enumerate(it): - if not hasattr(obj, 'packet_len'): + if isinstance(obj, bytes): + l = len(obj) + data = obj + else: + if not hasattr(obj, 'packet_len'): + continue + l = obj.packet_len + data = obj.packet_data + if l != 6972: continue - len = obj.packet_len - if len != 6972: - continue - yield obj.packet_data + yield data def removestart(it): @@ -52,7 +82,6 @@ def removestart(it): yield x[0x2A:] - # Function to parse packet data def parse(data): hdr = 4 + 2 * 7 # Header length @@ -73,26 +102,38 @@ def parsed(it): # Function to group data into frames def frames(it): current = [] + #otherdata = [] for obj in it: if obj['part'] == 0: if len(current) > 0: yield b"".join(current) current = [] + #otherdata = [] current.append(obj["data"]) + #otherdata.append(obj) if len(current) > 0: yield b"".join(current) +WIDTH = 384 +HEIGHT = 288 -def iterimages(it, width, height, pixelformat=">H"): + +def skip_bad_frames(it, width=WIDTH, height=HEIGHT): for frame in it: if len(frame) != width * height * 2: # 16 bpp + # Will be fixed when we stopped doing restarts + #print(f'{len(frame)} != {width} * {height} * 2') continue + yield frame + + +def iterimages(it, width=WIDTH, height=HEIGHT, pixelformat=">H"): + for frame in it: yield Image.fromarray(np.frombuffer(frame, dtype=pixelformat).reshape(width, height)) # Get frames and convert them to images -frames = frames(parsed(removestart(rightsize(blocks)))) -images = iterimages(it=frames, width=384, height=288) +frames = skip_bad_frames(frames(parsed(removestart(rightsize(blocks))))) # Create the directory for frames if not exists frame_dir = f"frames/{basename}" @@ -100,26 +141,37 @@ if not os.path.exists(frame_dir): os.makedirs(frame_dir) # Save each image as a PNG file -for i, img in enumerate(images): - img.save(f'frames/{basename}/{basename}_{i:04}.png') +if not args.live: + images = iterimages(it=frames) + for i, img in enumerate(images): + img.save(f'frames/{basename}/{basename}_{i:04}.png') + ffmpeg_input = f"frames/{basename}/{basename}_%04d.png" + command = [ + "ffmpeg", + "-y", # Overwrite output file without asking + "-hide_banner", # Hide banner + "-loglevel", "info", # Log level + "-f", "image2", # Input format + "-framerate", "25", # Framerate + "-i", ffmpeg_input, # Input file pattern + "-vf", "transpose=1", # Video filter for transposing + "-s", "384x288", # Size of one frame + "-vcodec", "libopenh264", # Video codec + "-pix_fmt", "yuv420p", # Pixel format: YUV 4:2:0 + "thermal.mp4", # Output file in MP4 container + ] -# Produce a video from the saved images -ffmpeg_input = f"frames/{basename}/{basename}_%04d.png" -command = [ - "ffmpeg", - "-y", # Overwrite output file without asking - "-hide_banner", # Hide banner - "-loglevel", "info", # Log level - "-f", "image2", # Input format - "-framerate", "25", # Framerate - "-i", ffmpeg_input, # Input file pattern - "-vf", "transpose=1", # Video filter for transposing - "-s", "384x288", # Size of one frame - "-vcodec", "libx264", # Video codec - "-pix_fmt", "yuv420p", # Pixel format: YUV 4:2:0 - "thermal.mp4", # Output file in MP4 container -] + subprocess.run(command) + print("to play: ffplay thermal.mp4") -subprocess.run(command) - -print("to play: ffplay thermal.mp4") +else: + output = 'to_ffmpeg' + # live: write to named pipe + if not Path(output).exists(): + print(f'making fifo at {output}') + os.mkfifo(output) + fd = open(output, 'wb') + for i, frame in enumerate(frames): + print(f'frame {i:3}') + Image.fromarray(np.frombuffer(frame, dtype='>H').reshape(WIDTH, HEIGHT)).save(f'test_{i:04}.png') + fd.write(frame) diff --git a/listen.py b/listen.py new file mode 100644 index 0000000..b735034 --- /dev/null +++ b/listen.py @@ -0,0 +1,9 @@ +from socket import socket, AF_INET, SOCK_DGRAM + +s = socket(AF_INET, SOCK_DGRAM) +s.bind(('', 8090)) + +while True: + d = s.recvfrom(1024) + print(d) + diff --git a/replay.py b/replay.py old mode 100644 new mode 100755 index f664c23..a45f57e --- a/replay.py +++ b/replay.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 #replay the "trigger" packet. #this packets will start the source broadcasting its packets. diff --git a/test_rust.py b/test_rust.py index 5d3a3d7..bf55562 100755 --- a/test_rust.py +++ b/test_rust.py @@ -2,11 +2,31 @@ from pathlib import Path from thermaldecoder import decode import numpy as np +import subprocess import matplotlib.pyplot as plt + + +# Create a directory to store the frames if it doesn't exist root = Path('frames') root.mkdir(exist_ok=True) -frames = list(decode('in.pcap')) -f = np.array(frames[0]) -f.shape = (384, 288) -plt.imshow(f) -plt.show() + +# Decode the frames from the pcap file +frames = list(decode('indesk.pcapng')) + +# Iterate over the frames +for i, frame in enumerate(frames): + try: + # Convert the frame to an image file + img_path = root / f"frame_{i}.png" + f = np.array(frame) + f.shape = (384, 288) + plt.imshow(f) + plt.axis('off') + plt.savefig(img_path, bbox_inches='tight', pad_inches=0) + plt.close() + + # Use ffmpeg to display the image + subprocess.run(['ffmpeg', '-i', str(img_path), '-vf', 'scale=800:600', '-framerate', '25', '-f', 'image2pipe', '-'], check=True) + + except ValueError as e: + print(f"Error processing frame {i}: {e}") diff --git a/thermal.mp4 b/thermal.mp4 index cd76de9..235cd66 100644 Binary files a/thermal.mp4 and b/thermal.mp4 differ