
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
import asyncio from bleak import BleakScanner import curses import time import math # RSSI de referencia y constante de propagación RSSI_REFERENCE = -59 PROPAGATION_CONSTANT = 2 # Distancia máxima (en metros) que queremos mapear al borde del radar MAX_DISTANCE_METERS = 10.0 def calculate_distance(rssi, reference=RSSI_REFERENCE, n=PROPAGATION_CONSTANT): return 10 ** ((reference - rssi) / (10 * n)) async def scan_for_devices_with_rssi(scan_time: float = 3.0): """ Escanea durante scan_time segundos y devuelve una lista de (device, rssi) usando un callback de detección, que en macOS suele ser más fiable. """ seen = {} # address -> (device, rssi) def detection_callback(device, advertisement_data): rssi = advertisement_data.rssi if rssi is None: return # Nos quedamos con el último RSSI visto para cada dirección seen[device.address] = (device, rssi) scanner = BleakScanner(detection_callback) await scanner.start() await asyncio.sleep(scan_time) await scanner.stop() return list(seen.values()) def draw_radar(stdscr, devices_with_rssi): """ Dibuja un panel de información a la izquierda y un radar tipo reloj a la derecha. Cada elemento de devices_with_rssi es una tupla (device, rssi). """ stdscr.clear() height, width = stdscr.getmaxyx() # Panel de información a la izquierda info_width = max(30, width // 3) radar_width = width - info_width - 2 radar_height = height - 4 if radar_width < 10 or radar_height < 10: stdscr.addstr(0, 0, "Ventana demasiado pequeña para dibujar el radar.") stdscr.refresh() return # Centro del radar center_x = info_width + radar_width // 2 center_y = 2 + radar_height // 2 max_radius = min(radar_width, radar_height) // 2 - 1 # Título stdscr.addstr(0, 1, "Dispositivos BLE (panel izq.)", curses.color_pair(3) | curses.A_BOLD) stdscr.addstr(0, info_width + 1, "Radar BLE (q para salir)", curses.color_pair(3) | curses.A_BOLD) # Ejes del radar (cruz) for x in range(center_x - max_radius, center_x + max_radius + 1): if 0 < x < width - 1 and 0 < center_y < height - 1: stdscr.addch(center_y, x, '-', curses.color_pair(2)) for y in range(center_y - max_radius, center_y + max_radius + 1): if 0 < y < height - 1 and 0 < center_x < width - 1: stdscr.addch(y, center_x, '|', curses.color_pair(2)) # Centro (tú) stdscr.addch(center_y, center_x, 'O', curses.color_pair(3) | curses.A_BOLD) if not devices_with_rssi: stdscr.addstr(2, 1, "No se encontraron dispositivos BLE con RSSI.", curses.color_pair(3)) stdscr.refresh() return num_devices = len(devices_with_rssi) for idx, (device, rssi) in enumerate(devices_with_rssi): distance = calculate_distance(rssi) # Normalizar distancia al radio del radar normalized = min(distance / MAX_DISTANCE_METERS, 1.0) radius = max(1, int(normalized * max_radius)) # Ángulo tipo reloj angle = 2 * math.pi * idx / num_devices y = int(center_y + radius * math.sin(angle)) x = int(center_x + radius * math.cos(angle)) label = str((idx + 1) % 10) if 1 <= y < height - 1 and info_width + 1 <= x < width - 1: stdscr.addch(y, x, label, curses.color_pair(1) | curses.A_BOLD) # Panel de información info_line = 2 + idx if info_line < height - 1: name = device.name or "Desconocido" info = ( f"{label} - {name[:15]:15} " f"RSSI: {rssi:4d} dBm " f"Dist: {distance:4.1f} m" ) stdscr.addnstr(info_line, 1, info, info_width - 2, curses.color_pair(3)) stdscr.refresh() def main(stdscr): curses.curs_set(0) stdscr.nodelay(1) curses.start_color() curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # Dispositivos curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Ejes radar curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLACK) # Texto while True: key = stdscr.getch() if key == ord('q'): break try: # Escaneo de unos segundos devices_with_rssi = asyncio.run(scan_for_devices_with_rssi(3.0)) except Exception as e: stdscr.clear() stdscr.addstr(0, 0, f"Error escaneando BLE: {e}", curses.color_pair(3)) stdscr.addstr(2, 0, "Comprueba permisos de Bluetooth y que bleak está instalado.", curses.color_pair(3)) stdscr.refresh() time.sleep(2) continue draw_radar(stdscr, devices_with_rssi) time.sleep(0.5) if __name__ == "__main__": print("Iniciando radar BLE... pulsa 'q' para salir.") time.sleep(0.5) curses.wrapper(main) |

