top of page

SPACE LAB

© Space Communications Research Group, Chappell University for Protocol Analysis Institute, Inc. 

Default Lab Files (rar)

Welcome to Space Lab

OLED Configurations

The next two labs will take you through the steps to configure a 128x64 pixel OLED display module. Complete the first lab to set up IIC (Inter-Integrated Circuit serial communication protocol) and a Python virtual environment on your RPI. 

​

In the second lab you will run a Python script to display packets in/out. The instructions include suggestions for applying a filter for traffic to/from specific port numbers - the ports that your ION implementation is using.

Lab 1: RPi-5 OLED 128x64 Setup

# Raspberry Pi OLED Setup Instructions
# 128x64 (This can be changed by editing the scripts.) 
# We used Weewooday Blue and Yellow 0.96 OLED Module 12864 displays.

# Instructions and Scripts by Michael Klements.
# https://www.youtube.com/watch?v=pdaDvPCdAlY
# https://www.the-diy-life.com/add-an-oled-stats-display-to-raspberry-pi-os-bookworm/


# 4-cable (female)
# GND to Pin 9 (Ground)
# VCC (3.3V) to Pin 1 (3.3V)
# SCL to Pin 5 (GPIO 3)
# SDA to Pin 3 (GPIO 2)

# Install python3

sudo apt-get install python3-pip
sudo apt install --upgrade python3-setuptools


# Setup python3 in a virtual environment
sudo apt install python3-venv

# Create a virtual environment called "stats_env"
python3 -m venv stats_env --system-site-packages

# Activate the virtual environment
source stats_env/bin/activate

# Install adafruit Blinka library
cd ~
pip3 install --upgrade adafruit-python-shell
wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py
sudo -E env PATH=$PATH python3 raspi-blinka.py


# Reboot the Pi.

# Check to see if the Pi can see the module.

sudo i2cdetect -y 1

# It should look something like this: 
#        0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
#    00:                         -- -- -- -- -- -- -- --
#    10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
#    20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
#    30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
#    40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
#    50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
#    60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
#    70: -- -- -- -- -- -- -- --

# To check that the I2C interface is enabled, use the below command 
# to open up configuration options, then select “3 Interfacing Options”, 
# then select “I5 I2C”, “Yes” to enable the interface, “Ok” and then “Finish”

# To check the configuration, use
# sudo raspi-config

# Activate your virtual environment again

source stats_env/bin/activate
pip3 install --upgrade adafruit_blinka
pip3 install adafruit-circuitpython-ssd1306
sudo apt-get install python3-pil


# Deactivate virtual environment and get the Python script for the display
deactivate
sudo apt-get install git
git clone https://github.com/mklements/OLED_Stats.git


# Activate the python virtual environment
source stats_env/bin/activate
cd OLED_Stats


# Run one of the scripts:
python3 stats.py
python3 monitor.py


# If all works well, consider checking out the 
# tcpdump_OLED-wlan0.py and tcpdump_OLED-eth0.py scripts.

​

Lab 2: Display tcpdump Info on OLED

To run, type python3 pkts_OLED-wlan0-v2icon.py --bpf "tcp port 4556"

The example above will only display the counts for traffic to or from TCP port 4556.

This script defaults to wlan0 capture. To capture on the Ethernet adapter, use --iface eth0 parameter.

# pkts_OLED-wlan0-v2icon.py

# TCPDUMP statistics on OLED display
# Python Scripts Created for SSD1306
# 128x64 (This can be changed by editing the scripts.) 
# We used Weewooday Blue and Yellow 0.96 OLED Module 12864 displays.

# Perform the previous OLED lab first.

# If inside your python virtual environment...

deactivate
cd ~


# install tcpdump
sudo apt-get install tcpdump

# run script with sudo

sudo -E env PATH=$PATH ./pkts_oled.py --iface wlan0

# Now grant tcpdump capabilities (so no sudo required to run)
which tcpdump         

# note the path (often /usr/sbin/tcpdump)
sudo setcap cap_net_raw,cap_net_admin=eip $(which tcpdump)

# optional start: keep it group-usable instead of world root
sudo groupadd -f pcap
sudo chgrp pcap $(which tcpdump)
sudo usermod -a -G pcap $USER
# log out/in (or reboot) so your group membership takes effect
# optional end


# capture on wlan0 interface
python3 pkts_OLED-wlan0-v2icon.py 

# capture on eth0 interface
python3 pkts_OLED-wlan0-v2icon.py --iface eth0 

# capture only packets to/from tcp port 4556 on wlan0 interface
python3 pkts_OLED-wlan0-v2icon.py --bpf "tcp port 4556"

# capture only packets to/from 10.0.2.2 on wlan0 interface
python3 pkts_OLED-wlan0-v2icon.py --bpf "host 10.0.2.2"

# Uncompress the script file

tar -xvf pkts_OLED-wlan0-v2icon.tar

​

Read through the Python script - there are many settings that can be changed,
as desired.

Multi-screen Display
pkts-MACIP-128x64-flip.py

#!/usr/bin/env python3
"""
SSD1306 128x64 OLED — Five-page Network Monitor with custom flip times

Page 0 (Start): 
  tcpdump (centered)        [8s]
  BPF: <expr|none> (centered)
  [blank]
  Chappell University (centered)

Page 1 (Identity): host/IP/MAC/iface
Page 2 (Status): totals, uptime, date, time
Page 3 (Protocols): totals + TCP/UDP/ICMP
Page 4 (PPS): packets/sec IN/OUT (ints)
"""

import argparse, subprocess, re, time, sys, socket, datetime, select
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont

import board, busio
from adafruit_ssd1306 import SSD1306_I2C

MAC_RE = re.compile(
    r'([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})\s*>\s*([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})'
)
PROTO_PORT_LINE = re.compile(r'\bIP6?\b .*?\.\d+\s*>\s*.*?\.\d+:')
def classify_protocol(line: str) -> str | None:
    L = line
    if "ICMP6" in L or "icmp6" in L or "ICMPv6" in L: return "icmp"
    if re.search(r'\bICMP\b', L, re.I): return "icmp"
    if re.search(r'\bUDP\b', L, re.I): return "udp"
    if "Flags [" in L: return "tcp"
    if re.search(r'\bTCP\b', L, re.I): return "tcp"
    if PROTO_PORT_LINE.search(L):
        if re.search(r'\bUDP\b', L, re.I): return "udp"
        return "tcp"
    return None

def read_local_mac(iface: str) -> str:
    return Path(f"/sys/class/net/{iface}/address").read_text().strip().lower()
def read_local_ip(iface: str) -> str:
    try:
        out = subprocess.check_output(["ip","-4","-o","addr","show","dev",iface], text=True)
        m = re.search(r"\binet\s+(\d+\.\d+\.\d+\.\d+)/\d+\b", out)
        return m.group(1) if m else "No IP"
    except: return "No IP"
def hostname() -> str: return socket.gethostname()

def init_display(width, height, addr):
    i2c = busio.I2C(board.SCL, board.SDA)
    disp = SSD1306_I2C(width, height, i2c, addr=addr)
    disp.fill(0); disp.show()
    return disp
def try_load_font(path, size):
    if path:
        try: return ImageFont.truetype(path, size)
        except: pass
    try: return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", size)
    except: return ImageFont.load_default()
def line_height(font):
    try: a,d = font.getmetrics(); return max(8,a+d)
    except: b = font.getbbox("Ag"); return max(8,b[3]-b[1])
def fit_font(fontpath, start_size, lines_needed, max_height):
    size = start_size
    while size>=7:
        f=try_load_font(fontpath,size); lh=line_height(f)
        if lh*lines_needed<=max_height: return f,lh
        size-=1
    f=try_load_font(fontpath,7); return f,line_height(f)
def truncate(draw,text,font,max_w):
    if draw.textlength(text,font=font)<=max_w: return text
    ell="…"; target=max_w-draw.textlength(ell,font=font); out=""
    for ch in text:
        if draw.textlength(out+ch,font=font)>target: break
        out+=ch
    return out+ell
def center_text(draw,text,font,w,y):
    tw=draw.textlength(text,font=font); x=max(0,(w-tw)//2)
    draw.text((x,y),text,font=font,fill=255)

def build_tcpdump_cmd(iface,bpf):
    return ["tcpdump","-i",iface,"-n","-e","-l"]+[bpf if bpf else "(ip or ip6)"]

# --- pages ---
def draw_page0_start(disp,font,lh,bpf):
    img=Image.new("1",(disp.width,disp.height)); d=ImageDraw.Draw(img); w,y=disp.width,0
    center_text(d,"tcpdump",font,w,y); y+=lh
    bfps=f"BPF: {bpf if bpf else 'none'}"
    center_text(d,bfps,font,w,y); y+=lh
    y+=lh  # blank line
    center_text(d,"Chappell University",font,w,y)
    disp.image(img); disp.show()
def draw_page1_identity(disp,font,lh,iface,bpf,host,ip,mac):
    img=Image.new("1",(disp.width,disp.height)); d=ImageDraw.Draw(img); max_w,y=disp.width,0
    lines=(f"Host: {host}",f"IP:   {ip}",f"MAC:  {mac}",f"IF: {iface}  BPF: {bpf if bpf else 'none'}")
    for txt in lines: d.text((0,y),truncate(d,txt,font,max_w),font=font,fill=255); y+=lh
    disp.image(img); disp.show()
def format_hms(seconds):
    seconds=int(seconds); h=seconds//3600; m=(seconds%3600)//60; s=seconds%60
    return f"{h:02d}:{m:02d}:{s:02d}"
def draw_page2_status(disp,font,lh,in_cnt,out_cnt,start_ts):
    img=Image.new("1",(disp.width,disp.height)); d=ImageDraw.Draw(img); max_w,y=disp.width,0
    nowdt=datetime.datetime.now()
    lines=(f"Totals: IN {in_cnt}  OUT {out_cnt}",
           f"Uptime: {format_hms(time.time()-start_ts)}",
           f"Date:   {nowdt.strftime('%Y-%m-%d')}",
           f"Time:   {nowdt.strftime('%H:%M:%S')}")
    for txt in lines: d.text((0,y),truncate(d,txt,font,max_w),font=font,fill=255); y+=lh
    disp.image(img); disp.show()
def draw_page3_protocols(disp,font,lh,tcp_cnt,udp_cnt,icmp_cnt):
    img=Image.new("1",(disp.width,disp.height)); d=ImageDraw.Draw(img); max_w,y=disp.width,0
    total=tcp_cnt+udp_cnt+icmp_cnt
    lines=(f"Total pkts: {total}",f"TCP: {tcp_cnt}",f"UDP: {udp_cnt}",f"ICMP: {icmp_cnt}")
    for txt in lines: d.text((0,y),truncate(d,txt,font,max_w),font=font,fill=255); y+=lh
    disp.image(img); disp.show()
def draw_page4_pps(disp,font,lh,in_pps,out_pps):
    img=Image.new("1",(disp.width,disp.height)); d=ImageDraw.Draw(img); max_w,y=disp.width,0
    in_i=int(round(in_pps)); out_i=int(round(out_pps))
   
# If you want OUT on the last line with a blank third line, use: ("Packets per Second", f"IN: {in_i}", "", f"OUT: {out_i}")
    lines=("Packets per Second",f"IN: {in_i}",f"OUT: {out_i}","")
    for txt in lines:
        if txt: d.text((0,y),truncate(d,txt,font,max_w),font=font,fill=255)
        y+=lh
    disp.image(img); disp.show()

def main():
    ap=argparse.ArgumentParser()
    ap.add_argument("--iface",default="wlan0"); ap.add_argument("--bpf",default=None)
    ap.add_argument("--addr",type=lambda x:int(x,0),default=0x3C)
    ap.add_argument("--width",type=int,default=128); ap.add_argument("--height",type=int,default=64)
    ap.add_argument("--fontsize",type=int,default=12); ap.add_argument("--fontpath",default=None)
    ap.add_argument("--update",type=float,default=0.25)
    ap.add_argument("--page",type=int,choices=[0,1,2,3,4],default=None)
    args=ap.parse_args()

    local_mac=read_local_mac(args.iface); local_ip=read_local_ip(args.iface); host=hostname()
    disp=init_display(args.width,args.height,args.addr)
    font,lh=fit_font(args.fontpath,args.fontsize,4,args.height)

    in_cnt=out_cnt=tcp_cnt=udp_cnt=icmp_cnt=0
    last_ts=time.time(); last_in=last_out=0; in_pps=out_pps=0.0; start_ts=time.time()

    draw_page0_start(disp,font,lh,args.bpf)
    cmd=build_tcpdump_cmd(args.iface,args.bpf)
    try:
        proc=subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,text=True,bufsize=1)
    except Exception as e:
        sys.exit(str(e))

    cur_page=0
   
# First flip after 8s on start page; then 5s elsewhere
    next_flip=time.time()+ (8 if args.page is None else 10**9)

    try:
       
# Non-blocking loop: flip/draw even when no packets match BPF
        while True:
            # Wait up to args.update seconds for a tcpdump line
            rlist, _, _ = select.select([proc.stdout], [], [], args.update)
            if rlist:
                line = proc.stdout.readline()
                if not line:
                    break  
# tcpdump ended

                # IN/OUT (MAC-based)
                m=MAC_RE.search(line)
                if m:
                    src,dst=m.group(1).lower(),m.group(2).lower()
                    if src==local_mac: out_cnt+=1
                    elif dst==local_mac: in_cnt+=1

                # Protocol counts
                proto=classify_protocol(line)
                if proto=="icmp": icmp_cnt+=1
                elif proto=="udp": udp_cnt+=1
                elif proto=="tcp": tcp_cnt+=1

            # Tick/update even if no new line arrived
            now=time.time()
            if now-last_ts>=args.update:
                dt=now-last_ts; din=in_cnt-last_in; dout=out_cnt-last_out
                in_pps=din/dt if dt>0 else 0.0
                out_pps=dout/dt if dt>0 else 0.0
                last_ts,last_in,last_out=now,in_cnt,out_cnt

                # Page selection / flip
                if args.page is not None:
                    cur_page=args.page
                else:
                    if now>=next_flip:
                        cur_page=0 if cur_page==4 else cur_page+1
                        next_flip=now+(8 if cur_page==0 else 5)

                # Draw current page
                if cur_page==0: draw_page0_start(disp,font,lh,args.bpf)
                elif cur_page==1: draw_page1_identity(disp,font,lh,args.iface,args.bpf,host,local_ip,local_mac)
                elif cur_page==2: draw_page2_status(disp,font,lh,in_cnt,out_cnt,start_ts)
                elif cur_page==3: draw_page3_protocols(disp,font,lh,tcp_cnt,udp_cnt,icmp_cnt)
                else: draw_page4_pps(disp,font,lh,in_pps,out_pps)

    except KeyboardInterrupt:
        pass
    finally:
        try: proc.terminate()
        except: pass
        disp.fill(0); disp.show()

if __name__=="__main__": main()
 

# Uncompress the script file

tar -xvf pkts-MACIP-128x64-flip.tar

Reach Out

For more information about the Chappell University Space Lab, reach out to us.

Thanks for reaching out to us!

bottom of page