#!/usr/bin/env python3 """ Map ESP32 USB ports to IP addresses Creates and manages mapping between /dev/ttyUSB* and assigned IPs """ import serial.tools.list_ports import argparse import json import glob import re from pathlib import Path class USBIPMapper: def __init__(self, start_ip="192.168.1.51", config_file="usb_ip_map.json"): self.start_ip = start_ip self.config_file = config_file self.mapping = {} def get_ip_for_index(self, index): """Calculate IP address for a given index""" ip_parts = self.start_ip.split('.') base_ip = int(ip_parts[3]) ip_parts[3] = str(base_ip + index) return '.'.join(ip_parts) def extract_usb_number(self, port): """Extract number from /dev/ttyUSBX""" match = re.search(r'ttyUSB(\d+)', port) if match: return int(match.group(1)) return None def detect_devices(self): """Detect all ESP32 USB devices and create mapping""" devices = sorted(glob.glob('/dev/ttyUSB*')) print(f"\n{'='*70}") print(f"ESP32 USB to IP Address Mapping") print(f"{'='*70}") print(f"Start IP: {self.start_ip}") print(f"Detected {len(devices)} USB device(s)\n") self.mapping = {} for idx, port in enumerate(devices): usb_num = self.extract_usb_number(port) ip = self.get_ip_for_index(idx) # Get device info try: ports = serial.tools.list_ports.comports() device_info = next((p for p in ports if p.device == port), None) if device_info: serial_num = device_info.serial_number or "Unknown" description = device_info.description or "Unknown" else: serial_num = "Unknown" description = "Unknown" except: serial_num = "Unknown" description = "Unknown" self.mapping[port] = { 'index': idx, 'usb_number': usb_num, 'ip': ip, 'serial': serial_num, 'description': description } print(f"[{idx:2d}] {port:14s} → {ip:15s} (USB{usb_num}, SN: {serial_num})") print(f"\n{'='*70}") print(f"Total: {len(devices)} devices mapped") print(f"IP Range: {self.mapping[devices[0]]['ip']} - {self.mapping[devices[-1]]['ip']}" if devices else "") print(f"{'='*70}\n") return self.mapping def save_mapping(self): """Save mapping to JSON file""" with open(self.config_file, 'w') as f: json.dump(self.mapping, f, indent=2) print(f"✓ Mapping saved to {self.config_file}") def load_mapping(self): """Load mapping from JSON file""" try: with open(self.config_file, 'r') as f: self.mapping = json.load(f) print(f"✓ Mapping loaded from {self.config_file}") return self.mapping except FileNotFoundError: print(f"✗ No saved mapping found at {self.config_file}") return {} def get_ip(self, port): """Get IP address for a specific USB port""" if port in self.mapping: return self.mapping[port]['ip'] return None def get_port(self, ip): """Get USB port for a specific IP address""" for port, info in self.mapping.items(): if info['ip'] == ip: return port return None def print_mapping(self): """Print current mapping""" if not self.mapping: print("No mapping loaded. Run with --detect first.") return print(f"\n{'='*70}") print(f"Current USB to IP Mapping") print(f"{'='*70}") for port, info in sorted(self.mapping.items(), key=lambda x: x[1]['index']): print(f"[{info['index']:2d}] {port:14s} → {info['ip']:15s} (USB{info['usb_number']})") print(f"{'='*70}\n") def export_bash_script(self, filename="usb_ip_vars.sh"): """Export mapping as bash variables""" with open(filename, 'w') as f: f.write("#!/bin/bash\n") f.write("# USB to IP mapping - Auto-generated\n\n") # Create associative array f.write("declare -A USB_TO_IP\n") for port, info in self.mapping.items(): f.write(f"USB_TO_IP[{port}]=\"{info['ip']}\"\n") f.write("\n# Create reverse mapping\n") f.write("declare -A IP_TO_USB\n") for port, info in self.mapping.items(): f.write(f"IP_TO_USB[{info['ip']}]=\"{port}\"\n") f.write("\n# Helper functions\n") f.write("get_ip_for_usb() { echo \"${USB_TO_IP[$1]}\"; }\n") f.write("get_usb_for_ip() { echo \"${IP_TO_USB[$1]}\"; }\n") print(f"✓ Bash script exported to {filename}") print(f" Usage: source {filename} && get_ip_for_usb /dev/ttyUSB0") def main(): parser = argparse.ArgumentParser( description='Map ESP32 USB ports to IP addresses', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Detect devices and create mapping %(prog)s --detect # Detect and save to file %(prog)s --detect --save # Load saved mapping and display %(prog)s --load --print # Get IP for specific USB port %(prog)s --load --port /dev/ttyUSB5 # Get USB port for specific IP %(prog)s --load --ip 192.168.1.55 # Export as bash script %(prog)s --load --export # Use custom IP range %(prog)s --detect --start-ip 10.0.0.100 """ ) parser.add_argument('--detect', action='store_true', help='Detect USB devices and create mapping') parser.add_argument('--save', action='store_true', help='Save mapping to file') parser.add_argument('--load', action='store_true', help='Load mapping from file') parser.add_argument('--print', action='store_true', help='Print current mapping') parser.add_argument('--start-ip', default='192.168.1.51', help='Starting IP address (default: 192.168.1.51)') parser.add_argument('--config', default='usb_ip_map.json', help='Config file path (default: usb_ip_map.json)') parser.add_argument('--port', metavar='PORT', help='Get IP for specific USB port (e.g., /dev/ttyUSB5)') parser.add_argument('--ip', metavar='IP', help='Get USB port for specific IP address') parser.add_argument('--export', action='store_true', help='Export mapping as bash script') args = parser.parse_args() mapper = USBIPMapper(start_ip=args.start_ip, config_file=args.config) # Detect devices if args.detect: mapper.detect_devices() if args.save: mapper.save_mapping() # Load mapping if args.load: mapper.load_mapping() # Print mapping if args.print: mapper.print_mapping() # Query specific port if args.port: if not mapper.mapping: mapper.load_mapping() ip = mapper.get_ip(args.port) if ip: print(f"{args.port} → {ip}") else: print(f"Port {args.port} not found in mapping") # Query specific IP if args.ip: if not mapper.mapping: mapper.load_mapping() port = mapper.get_port(args.ip) if port: print(f"{args.ip} → {port}") else: print(f"IP {args.ip} not found in mapping") # Export bash script if args.export: if not mapper.mapping: mapper.load_mapping() mapper.export_bash_script() # Default: detect and print if not any([args.detect, args.load, args.print, args.port, args.ip, args.export]): mapper.detect_devices() if __name__ == '__main__': main()