blob: c247bcbad0e27c4fb4769d469f5bdfec7e0a1590 [file]
#!/usr/bin/env python3
"""
I2C/SMBus Monitor for Beagle Bus Analyzer with MCTP/SPDM Support
Automatically connects to the first available device and listens for I2C transactions.
Supports MCTP packet assembly and SPDM message parsing.
"""
import sys
import os
import argparse
from typing import List, Optional, Dict
from collections import defaultdict
# Add the Beagle API to the path
# Assumes script runs at the same level as beagle-api-linux-x86_64-v6.00 directory
script_dir = os.path.dirname(os.path.abspath(__file__))
beagle_lib_path = os.path.join(script_dir, 'beagle-api-linux-x86_64-v6.00', 'python')
sys.path.insert(0, beagle_lib_path)
from beagle_py import *
#==========================================================================
# MCTP/SPDM PARSING CODE (from mctp-parse)
#==========================================================================
class SPDMParser:
"""Parser for SPDM messages (DSP0274)"""
# Request codes (DSP0274 Table 4)
REQUEST_CODES = {
0x81: "GET_DIGESTS",
0x82: "GET_CERTIFICATE",
0x83: "CHALLENGE",
0x84: "GET_VERSION",
0x85: "CHUNK_SEND",
0x86: "CHUNK_GET",
0x87: "GET_ENDPOINT_INFO",
0xE0: "GET_MEASUREMENTS",
0xE1: "GET_CAPABILITIES",
0xE2: "GET_SUPPORTED_EVENT_TYPES",
0xE3: "NEGOTIATE_ALGORITHMS",
0xE4: "KEY_EXCHANGE",
0xE5: "FINISH",
0xE6: "PSK_EXCHANGE",
0xE7: "PSK_FINISH",
0xE8: "HEARTBEAT",
0xE9: "KEY_UPDATE",
0xEA: "GET_ENCAPSULATED_REQUEST",
0xEB: "DELIVER_ENCAPSULATED_RESPONSE",
0xEC: "END_SESSION",
0xED: "GET_CSR",
0xEE: "SET_CERTIFICATE",
0xEF: "GET_MEASUREMENT_EXTENSION_LOG",
0xF0: "SUBSCRIBE_EVENT_TYPES",
0xF1: "SEND_EVENT",
0xFC: "GET_KEY_PAIR_INFO",
0xFD: "SET_KEY_PAIR_INFO",
0xFE: "VENDOR_DEFINED_REQUEST",
0xFF: "RESPOND_IF_READY",
}
# Response codes (DSP0274 Table 5)
RESPONSE_CODES = {
0x01: "DIGESTS",
0x02: "CERTIFICATE",
0x03: "CHALLENGE_AUTH",
0x04: "VERSION",
0x05: "CHUNK_SEND_ACK",
0x06: "CHUNK_RESPONSE",
0x07: "ENDPOINT_INFO",
0x60: "MEASUREMENTS",
0x61: "CAPABILITIES",
0x62: "SUPPORTED_EVENT_TYPES",
0x63: "ALGORITHMS",
0x64: "KEY_EXCHANGE_RSP",
0x65: "FINISH_RSP",
0x66: "PSK_EXCHANGE_RSP",
0x67: "PSK_FINISH_RSP",
0x68: "HEARTBEAT_ACK",
0x69: "KEY_UPDATE_ACK",
0x6A: "ENCAPSULATED_REQUEST",
0x6B: "ENCAPSULATED_RESPONSE_ACK",
0x6C: "END_SESSION_ACK",
0x6D: "CSR",
0x6E: "SET_CERTIFICATE_RSP",
0x6F: "MEASUREMENT_EXTENSION_LOG",
0x70: "SUBSCRIBE_EVENT_TYPES_ACK",
0x71: "EVENT_ACK",
0x7C: "KEY_PAIR_INFO",
0x7D: "SET_KEY_PAIR_INFO_ACK",
0x7E: "VENDOR_DEFINED_RESPONSE",
0x7F: "ERROR",
}
@staticmethod
def parse(payload: List[int]) -> Optional[Dict]:
"""Parse SPDM message from payload"""
if len(payload) < 2:
return None
result = {}
# Byte 0: SPDM version (major [7:4], minor [3:0])
version_byte = payload[0]
result['version_major'] = (version_byte >> 4) & 0x0F
result['version_minor'] = version_byte & 0x0F
result['version'] = f"{result['version_major']}.{result['version_minor']}"
# Byte 1: Request/Response code
code = payload[1]
result['code'] = code
# Determine if request or response
if code in SPDMParser.REQUEST_CODES:
result['msg_direction'] = "Request"
result['msg_name'] = SPDMParser.REQUEST_CODES[code]
elif code in SPDMParser.RESPONSE_CODES:
result['msg_direction'] = "Response"
result['msg_name'] = SPDMParser.RESPONSE_CODES[code]
else:
result['msg_direction'] = "Unknown"
result['msg_name'] = f"Unknown (0x{code:02X})"
# Byte 2: Param1 (if present)
if len(payload) > 2:
result['param1'] = payload[2]
# Byte 3: Param2 (if present)
if len(payload) > 3:
result['param2'] = payload[3]
# Remaining data
if len(payload) > 4:
result['data'] = payload[4:]
result['data_len'] = len(result['data'])
else:
result['data'] = []
result['data_len'] = 0
return result
class MCTPParser:
"""Parser for MCTP packets"""
# Message type definitions (DSP0236 Table 3)
MESSAGE_TYPES = {
0x00: "MCTP Control Message",
0x01: "PLDM",
0x02: "NC-SI over MCTP",
0x03: "Ethernet over MCTP",
0x04: "NVMe-MI over MCTP",
0x05: "SPDM over MCTP",
0x06: "SECDED over MCTP",
0x07: "CXL FM API over MCTP",
0x08: "CXL CCI over MCTP",
}
@staticmethod
def parse(data: List[int]) -> dict:
"""Parse MCTP packet and return structured data"""
if len(data) < 4:
raise ValueError("Packet too short - minimum 4 bytes required for MCTP header")
result = {}
# Byte 0: Destination EID
result['dest_eid'] = data[0]
# Byte 1: Header Version [7:4], Reserved [3:0]
byte1 = data[1]
result['header_version'] = (byte1 >> 4) & 0x0F
result['rsvd'] = byte1 & 0x0F
# Byte 2: Source EID
result['src_eid'] = data[2]
# Byte 3: SOM, EOM, Pkt_Seq, TO, Msg_Tag
byte3 = data[3]
result['som'] = bool(byte3 & 0x80) # Start of Message
result['eom'] = bool(byte3 & 0x40) # End of Message
result['pkt_seq'] = (byte3 >> 4) & 0x03 # Packet sequence number
result['to'] = bool(byte3 & 0x08) # Tag Owner
result['msg_tag'] = byte3 & 0x07 # Message Tag
# Message body starts at byte 4
if len(data) > 4:
# Byte 4: IC [7], Message Type [6:0]
byte4 = data[4]
result['ic'] = bool(byte4 & 0x80) # Integrity Check
result['msg_type'] = byte4 & 0x7F
result['msg_type_name'] = MCTPParser.MESSAGE_TYPES.get(
result['msg_type'],
f"Vendor Defined (0x{result['msg_type']:02X})" if result['msg_type'] >= 0x7E
else f"Reserved (0x{result['msg_type']:02X})"
)
# Payload starts at byte 5
if len(data) > 5:
result['payload'] = data[5:]
result['payload_len'] = len(result['payload'])
# Parse SPDM if message type is 0x05
if result['msg_type'] == 0x05 and result['payload_len'] > 0:
result['spdm'] = SPDMParser.parse(result['payload'])
else:
result['payload'] = []
result['payload_len'] = 0
return result
def calculate_pec(data: List[int]) -> int:
"""
Calculate SMBus PEC (Packet Error Code) using CRC-8.
SMBus uses CRC-8 with polynomial 0x07 (x^8 + x^2 + x + 1).
Initial value is 0x00.
"""
crc = 0x00
polynomial = 0x07
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x80:
crc = (crc << 1) ^ polynomial
else:
crc = crc << 1
crc &= 0xFF
return crc
def parse_smbus_header(data: List[int], expect_pec: bool = False) -> Optional[Dict]:
"""
Parse SMBus/I2C transport binding header (DSP0237).
Returns parsed transport info or None if not valid SMBus format.
"""
if len(data) < 3:
return None
# Check for MCTP over SMBus command code (0x0F)
if data[0] != 0x0F:
return None
result = {}
result['cmd_code'] = data[0]
result['byte_count'] = data[1]
expected_total = 2 + result['byte_count']
if expect_pec:
expected_total += 1
if len(data) < expected_total:
return None
if expect_pec and len(data) >= expected_total:
result['pec_received'] = data[expected_total - 1]
# Calculate expected PEC
pec_data = data[:expected_total - 1]
result['pec_calculated'] = calculate_pec(pec_data)
result['pec_valid'] = (result['pec_received'] == result['pec_calculated'])
data_end = expected_total - 1
else:
result['pec_received'] = None
result['pec_calculated'] = None
result['pec_valid'] = None
data_end = min(len(data), expected_total)
# SMBus-specific headers start at byte 2
if result['byte_count'] < 5:
return None
offset = 2
result['source_slave_addr'] = data[offset]
offset += 1
# MCTP reserved + header version
byte_hdr = data[offset]
result['mctp_reserved'] = (byte_hdr >> 4) & 0x0F
result['mctp_hdr_version'] = byte_hdr & 0x0F
offset += 1
# Destination and Source EIDs
result['dest_eid'] = data[offset]
offset += 1
result['src_eid'] = data[offset]
offset += 1
# MCTP packet starts here (SOM/EOM byte)
result['mctp_offset'] = offset
result['mctp_data'] = data[offset:data_end]
return result
#==========================================================================
# MCTP MESSAGE ASSEMBLER
#==========================================================================
class MCTPAssembler:
"""Assembles fragmented MCTP messages"""
def __init__(self):
# Key: (src_eid, dest_eid, msg_tag)
# Value: {'fragments': [], 'expected_seq': int, 'complete': bool}
self.sessions = {}
def add_fragment(self, mctp_data: Dict) -> Optional[Dict]:
"""
Add a fragment and return complete message if EOM is reached.
Returns None if message is incomplete.
"""
key = (mctp_data['src_eid'], mctp_data['dest_eid'], mctp_data['msg_tag'])
# SOM - start new session
if mctp_data['som']:
self.sessions[key] = {
'fragments': [mctp_data],
'expected_seq': (mctp_data['pkt_seq'] + 1) % 4,
'src_eid': mctp_data['src_eid'],
'dest_eid': mctp_data['dest_eid'],
'msg_tag': mctp_data['msg_tag'],
'msg_type': mctp_data.get('msg_type'),
'msg_type_name': mctp_data.get('msg_type_name'),
}
# Single packet message (SOM+EOM)
if mctp_data['eom']:
session = self.sessions.pop(key)
return self._assemble_session(session)
return None
# Middle or end fragment
if key not in self.sessions:
# Fragment without SOM - ignore or could be error
return None
session = self.sessions[key]
# Check sequence number
if mctp_data['pkt_seq'] != session['expected_seq']:
# Sequence error - drop session
del self.sessions[key]
return None
session['fragments'].append(mctp_data)
session['expected_seq'] = (mctp_data['pkt_seq'] + 1) % 4
# EOM - assemble complete message
if mctp_data['eom']:
complete_session = self.sessions.pop(key)
return self._assemble_session(complete_session)
return None
def _assemble_session(self, session: Dict) -> Dict:
"""Assemble fragments into complete message"""
# Combine all payloads
complete_payload = []
for frag in session['fragments']:
if 'payload' in frag and frag['payload']:
complete_payload.extend(frag['payload'])
result = {
'src_eid': session['src_eid'],
'dest_eid': session['dest_eid'],
'msg_tag': session['msg_tag'],
'msg_type': session['msg_type'],
'msg_type_name': session['msg_type_name'],
'payload': complete_payload,
'payload_len': len(complete_payload),
'fragment_count': len(session['fragments']),
}
# Parse SPDM if applicable
if session['msg_type'] == 0x05 and complete_payload:
result['spdm'] = SPDMParser.parse(complete_payload)
return result
#==========================================================================
# BEAGLE DEVICE FUNCTIONS
#==========================================================================
def find_and_connect():
"""Find and connect to the first available Beagle device."""
print("Searching for Beagle devices...")
# Find all attached devices
(num, ports, unique_ids) = bg_find_devices_ext(16, 16)
if num == 0:
print("Error: No Beagle devices found!")
sys.exit(1)
print(f"Found {num} device(s)")
# Find the first available (not in-use) device
device_port = None
for i in range(num):
port = ports[i]
unique_id = unique_ids[i]
if not (port & BG_PORT_NOT_FREE):
device_port = port
print(f"Connecting to device on port {port} (S/N: {unique_id:04d}-{unique_id % 1000000:06d})")
break
else:
print(f"Port {port & ~BG_PORT_NOT_FREE} is in use")
if device_port is None:
print("Error: All devices are in use!")
sys.exit(1)
# Open the device
beagle = bg_open(device_port)
if beagle <= 0:
print(f"Error: Unable to open Beagle device on port {device_port}")
print(f"Error code = {beagle}")
sys.exit(1)
print(f"Successfully opened Beagle device on port {device_port}")
return beagle
def configure_device(beagle, samplerate_khz=10000, timeout_ms=500, latency_ms=200):
"""Configure the Beagle device for I2C monitoring."""
# Set sampling rate
samplerate = bg_samplerate(beagle, samplerate_khz)
if samplerate < 0:
print(f"Error setting sample rate: {bg_status_string(samplerate)}")
sys.exit(1)
print(f"Sample rate set to {samplerate} kHz")
# Set idle timeout
bg_timeout(beagle, timeout_ms)
print(f"Idle timeout set to {timeout_ms} ms")
# Set latency
bg_latency(beagle, latency_ms)
print(f"Latency set to {latency_ms} ms")
# Disable pullups and target power (passive monitoring)
bg_i2c_pullup(beagle, BG_I2C_PULLUP_OFF)
bg_target_power(beagle, BG_TARGET_POWER_OFF)
# Get host interface speed
if bg_host_ifce_speed(beagle):
print("Host interface: high speed")
else:
print("Host interface: full speed")
#==========================================================================
# MONITORING FUNCTIONS
#==========================================================================
def monitor_i2c(beagle, args, max_packet_len=256):
"""Monitor and print I2C transactions with optional MCTP/SPDM parsing."""
# Calculate timing size
timing_size = bg_bit_timing_size(BG_PROTOCOL_I2C, max_packet_len)
# Get sample rate for timestamp conversion
samplerate_khz = bg_samplerate(beagle, 0)
# Enable I2C capture
if bg_enable(beagle, BG_PROTOCOL_I2C) != BG_OK:
print("Error: Could not enable I2C capture!")
sys.exit(1)
print("\n" + "="*80)
print("I2C Monitoring Started - Press Ctrl+C to stop")
print("="*80)
if args.spdm:
print("Mode: SPDM (with MCTP and SMBus)")
if args.mctp_hide:
print(" MCTP layer hidden")
elif args.mctp:
print("Mode: MCTP (with SMBus)")
if args.mctp_no_partial:
print(" Hiding partial MCTP fragments")
elif args.smbus:
print("Mode: SMBus")
if args.with_pec:
print(" PEC validation enabled")
else:
print("Mode: Raw I2C")
print("="*80 + "\n")
# Allocate buffers
data_in = array_u16(max_packet_len)
timing = array_u32(timing_size)
packet_count = 0
mctp_assembler = MCTPAssembler() if (args.mctp or args.spdm) else None
try:
while True:
# Read I2C transaction
(count, status, time_sop, time_duration,
time_dataoffset, data_in, timing) = \
bg_i2c_read_bit_timing(beagle, data_in, timing)
# Convert timestamp to nanoseconds
time_sop_ns = (time_sop * 1000) // (samplerate_khz // 1000)
# Skip if no data
if count <= 0:
if count < 0:
print(f"Error reading I2C data: {count}")
break
continue
packet_count += 1
# Extract raw data bytes (strip NACK bits)
i2c_addr = None
i2c_rw = None
offset = 0
# Get address if present
if not (status & BG_READ_ERR_MIDDLE_OF_PACKET) and count >= 1:
nack = data_in[0] & BG_I2C_MONITOR_NACK
if count == 1 or (data_in[0] & 0xf9) != 0xf0 or nack:
# 7-bit address
i2c_addr = (int(data_in[0] & 0xff) >> 1)
i2c_rw = "R" if (data_in[0] & 0x01) else "W"
offset = 1
else:
# 10-bit address
i2c_addr = ((data_in[0] << 7) & 0x300) | (data_in[1] & 0xff)
i2c_rw = "R" if (data_in[0] & 0x01) else "W"
offset = 2
# Extract payload bytes
raw_data = [int(data_in[i] & 0xff) for i in range(offset, count)]
# Process based on mode
if args.smbus and raw_data:
smbus_result = parse_smbus_header(raw_data, args.with_pec)
if smbus_result and (args.mctp or args.spdm):
# Parse MCTP
try:
# Create synthetic MCTP packet
synthetic_mctp = [
smbus_result['dest_eid'],
(smbus_result['mctp_hdr_version'] << 4) | smbus_result['mctp_reserved'],
smbus_result['src_eid']
] + smbus_result['mctp_data']
mctp_parsed = MCTPParser.parse(synthetic_mctp)
# Handle MCTP assembly
if args.mctp_no_partial or args.spdm:
complete_msg = mctp_assembler.add_fragment(mctp_parsed)
if complete_msg is None:
# Incomplete fragment - skip or show minimal info
if not args.mctp_no_partial:
print(f"[{time_sop_ns:12d} ns] MCTP Fragment: SOM={mctp_parsed['som']} EOM={mctp_parsed['eom']} Seq={mctp_parsed['pkt_seq']}")
continue
# Complete message assembled
mctp_parsed = complete_msg
# Display based on mode
if args.spdm and 'spdm' in mctp_parsed:
print_spdm_message(time_sop_ns, i2c_addr, i2c_rw, mctp_parsed, args.mctp_hide)
elif not args.spdm:
print_mctp_message(time_sop_ns, i2c_addr, i2c_rw, smbus_result, mctp_parsed)
except Exception as e:
print(f"[{time_sop_ns:12d} ns] MCTP Parse Error: {e}")
elif smbus_result:
# SMBus mode only
print_smbus_message(time_sop_ns, i2c_addr, i2c_rw, smbus_result, raw_data)
else:
# Not valid SMBus MCTP - print raw
print_raw_i2c(time_sop_ns, i2c_addr, i2c_rw, raw_data, status)
else:
# Raw I2C mode
print_raw_i2c(time_sop_ns, i2c_addr, i2c_rw, raw_data, status)
sys.stdout.flush()
except KeyboardInterrupt:
print("\n\nCapture stopped by user")
print(f"Total packets captured: {packet_count}")
finally:
# Disable capture
bg_disable(beagle)
def print_raw_i2c(timestamp, addr, rw, data, status):
"""Print raw I2C transaction"""
print(f"[{timestamp:12d} ns] ", end='')
if addr is not None:
print(f"[S] <0x{addr:02X}:{rw}> ", end='')
if data:
hex_str = ' '.join(f'0x{b:02X}' for b in data)
print(hex_str, end=' ')
if not (status & BG_READ_I2C_NO_STOP):
print("[P]", end=' ')
print()
def print_smbus_message(timestamp, addr, rw, smbus_info, raw_data):
"""Print SMBus message with PEC info"""
print(f"[{timestamp:12d} ns] SMBus: ", end='')
print(f"Cmd=0x{smbus_info['cmd_code']:02X} Len={smbus_info['byte_count']} ", end='')
if smbus_info['pec_valid'] is not None:
if smbus_info['pec_valid']:
print(f"PEC=✓", end=' ')
else:
print(f"PEC=✗", end=' ')
# Show data
hex_str = ' '.join(f'{b:02X}' for b in raw_data[:16])
if len(raw_data) > 16:
hex_str += "..."
print(f"[{hex_str}]")
def print_mctp_message(timestamp, addr, rw, smbus_info, mctp_parsed):
"""Print MCTP message with highlighting"""
som_marker = "🟢 SOM" if mctp_parsed.get('som') else ""
eom_marker = "🔴 EOM" if mctp_parsed.get('eom') else ""
markers = f"{som_marker} {eom_marker}".strip()
print(f"[{timestamp:12d} ns] MCTP: ", end='')
if markers:
print(f"{markers} ", end='')
print(f"EID {mctp_parsed['src_eid']:02X}→{mctp_parsed['dest_eid']:02X} ", end='')
print(f"Seq={mctp_parsed.get('pkt_seq', '?')} Tag={mctp_parsed.get('msg_tag', '?')} ", end='')
print(f"Type=0x{mctp_parsed.get('msg_type', 0):02X} ({mctp_parsed.get('msg_type_name', 'Unknown')})", end='')
# Show fragment count if assembled
if 'fragment_count' in mctp_parsed and mctp_parsed['fragment_count'] > 1:
print(f" [{mctp_parsed['fragment_count']} fragments]", end='')
print()
def print_spdm_message(timestamp, addr, rw, mctp_parsed, hide_mctp):
"""Print SPDM message with request/response highlighting"""
spdm = mctp_parsed.get('spdm')
if not spdm:
return
# Highlight request vs response
if spdm['msg_direction'] == 'Request':
direction_marker = "📤 REQ"
elif spdm['msg_direction'] == 'Response':
direction_marker = "📥 RSP"
else:
direction_marker = "❓"
print(f"[{timestamp:12d} ns] SPDM: {direction_marker} ", end='')
if not hide_mctp:
print(f"EID {mctp_parsed['src_eid']:02X}→{mctp_parsed['dest_eid']:02X} ", end='')
if 'fragment_count' in mctp_parsed and mctp_parsed['fragment_count'] > 1:
print(f"[{mctp_parsed['fragment_count']} frags] ", end='')
print(f"v{spdm['version']} {spdm['msg_name']} ", end='')
if 'param1' in spdm:
print(f"P1=0x{spdm['param1']:02X} ", end='')
if 'param2' in spdm:
print(f"P2=0x{spdm['param2']:02X} ", end='')
if spdm.get('data_len', 0) > 0:
print(f"+{spdm['data_len']}B", end='')
print()
#==========================================================================
# MAIN
#==========================================================================
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='I2C/SMBus Monitor with MCTP/SPDM Support',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s # Raw I2C monitoring
%(prog)s --smbus --with-pec # SMBus with PEC validation
%(prog)s --mctp # MCTP packet monitoring
%(prog)s --spdm # SPDM message monitoring
%(prog)s --spdm --mctp-hide # SPDM only (hide MCTP layer)
%(prog)s --samplerate 5000 --timeout 1000 # Custom sample rate and timeout
'''
)
# Protocol mode arguments
parser.add_argument('--smbus', action='store_true',
help='Treat all packets as having SMBus header')
parser.add_argument('--with-pec', action='store_true',
help='Assume PEC is present (requires --smbus)')
parser.add_argument('--mctp', action='store_true',
help='MCTP mode (implies --smbus --with-pec)')
parser.add_argument('--mctp-no-partial', action='store_true',
help='Hide MCTP fragments until EOM (requires --mctp or --spdm)')
parser.add_argument('--spdm', action='store_true',
help='SPDM mode (implies --mctp)')
parser.add_argument('--mctp-hide', action='store_true',
help='Hide MCTP layer details (requires --spdm)')
# Device configuration arguments
parser.add_argument('--samplerate', type=int, default=10000, metavar='KHZ',
help='Sample rate in kHz (default: 10000)')
parser.add_argument('--timeout', type=int, default=500, metavar='MS',
help='Idle timeout in milliseconds (default: 500)')
parser.add_argument('--latency', type=int, default=200, metavar='MS',
help='Latency in milliseconds (default: 200)')
args = parser.parse_args()
# Handle argument implications
if args.spdm:
args.mctp = True
args.mctp_no_partial = True # SPDM mode always waits for complete messages
if args.mctp:
args.smbus = True
args.with_pec = True
# Validate argument combinations
if args.with_pec and not args.smbus:
parser.error("--with-pec requires --smbus")
if args.mctp_no_partial and not (args.mctp or args.spdm):
parser.error("--mctp-no-partial requires --mctp or --spdm")
if args.mctp_hide and not args.spdm:
parser.error("--mctp-hide requires --spdm")
# Validate configuration arguments
if args.samplerate <= 0:
parser.error("Sample rate must be positive")
if args.timeout < 0:
parser.error("Timeout must be non-negative")
if args.latency < 0:
parser.error("Latency must be non-negative")
# Find and connect to device
beagle = find_and_connect()
try:
# Configure device
print()
configure_device(beagle, args.samplerate, args.timeout, args.latency)
# Start monitoring
monitor_i2c(beagle, args)
finally:
# Clean up
bg_close(beagle)
print("Beagle device closed")
if __name__ == "__main__":
main()