import network import socket import time import json from dotenv import load_env # ==== НАСТРОЙКИ ==== TRANSMIT_PIN = 19 CODES_FILE = "codes.json" # ==================== def connect_wifi(ssid, password, timeout_s=15): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print(f"Подключение к WiFi '{ssid}'...") wlan.connect(ssid, password) start = time.time() while not wlan.isconnected(): if time.time() - start > timeout_s: print("Не удалось подключиться к WiFi (таймаут)") return None time.sleep(0.5) ip = wlan.ifconfig()[0] print(f"WiFi подключен. IP адрес: {ip}") return ip def load_codes(filename=CODES_FILE): try: with open(filename, "r") as f: return json.load(f) except OSError: return {} def save_codes(codes, filename=CODES_FILE): with open(filename, "w") as f: json.dump(codes, f) def delete_code(name, filename=CODES_FILE): codes = load_codes(filename) if name not in codes: return False del codes[name] save_codes(codes, filename) return True def capture_and_save(name, timeout_ms=5000, idle_us=8000, recv_pin=None): from capture import capture_raw, RECV_PIN pin = recv_pin if recv_pin is not None else RECV_PIN timings = capture_raw(pin_num=pin, timeout_ms=timeout_ms, idle_us=idle_us) if not timings: return False, 0 codes = load_codes() codes[name] = timings save_codes(codes) return True, len(timings) def send_ir_code(name, pin_num=TRANSMIT_PIN): from transmit import send_code send_code(name, pin_num=pin_num) def url_decode(s): """Простейший url-decode для параметров запроса (без внешних зависимостей).""" s = s.replace("+", " ") result = "" i = 0 while i < len(s): if s[i] == "%" and i + 2 < len(s): try: result += chr(int(s[i + 1:i + 3], 16)) i += 3 continue except ValueError: pass result += s[i] i += 1 return result def parse_query(path): """Возвращает (route, {param: value}) из строки вида /send?code=power""" if "?" not in path: return path, {} route, qs = path.split("?", 1) params = {} for pair in qs.split("&"): if "=" in pair: k, v = pair.split("=", 1) params[url_decode(k)] = url_decode(v) return route, params def handle_request(path, headers, api_key): route, params = parse_query(path) # Публичный маршрут - не требует ключа, чтобы можно было проверить что сервер жив if route == "/": return 200, { "status": "ESP32 IR сервер запущен", "endpoints": { "/list": "список сохранённых кодов", "/send?code=ИМЯ": "воспроизвести код", "/record?code=ИМЯ&timeout_ms=5000": "записать новый код (наведите пульт и нажмите кнопку в течение timeout_ms)", "/delete?code=ИМЯ": "удалить код" }, "auth": "передайте ключ через заголовок X-API-Key или параметр ?key=" } # Проверка ключа для всех остальных маршрутов if api_key: provided_key = headers.get("x-api-key") or params.get("key") if provided_key != api_key: return 401, {"error": "неверный или отсутствующий API-ключ"} if route == "/send": name = params.get("code") if not name: return 400, {"error": "укажите ?code=имя_кода"} codes = load_codes() if name not in codes: return 404, {"error": f"код '{name}' не найден", "available": list(codes.keys())} try: send_ir_code(name) return 200, {"status": "ok", "sent": name} except Exception as e: return 500, {"error": str(e)} elif route == "/record": name = params.get("code") if not name: return 400, {"error": "укажите ?code=имя_кода"} timeout_ms = int(params.get("timeout_ms", "5000")) try: ok, count = capture_and_save(name, timeout_ms=timeout_ms) if ok: return 200, {"status": "ok", "saved": name, "pulses": count} else: return 408, {"error": "сигнал не обнаружен (таймаут)"} except Exception as e: return 500, {"error": str(e)} elif route == "/delete": name = params.get("code") if not name: return 400, {"error": "укажите ?code=имя_кода"} if delete_code(name): return 200, {"status": "ok", "deleted": name} else: return 404, {"error": f"код '{name}' не найден"} elif route == "/list": codes = load_codes() details = {name: len(timings) for name, timings in codes.items()} return 200, {"codes": details} else: return 404, {"error": "неизвестный маршрут"} def parse_headers(request): """Парсит заголовки HTTP-запроса в словарь с ключами в нижнем регистре.""" headers = {} lines = request.split("\r\n") for line in lines[1:]: if not line: break if ":" in line: key, value = line.split(":", 1) headers[key.strip().lower()] = value.strip() return headers def run_server(ip, api_key=None): addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1] s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(addr) s.listen(1) print(f"Сервер запущен: http://{ip}/") if api_key: print( f"Примеры: http://{ip}/list?key=... | http://{ip}/send?code=power&key=...") print("Защита включена: передавайте key через ?key= или заголовок X-API-Key") else: print(f"Примеры: http://{ip}/list | http://{ip}/send?code=power") print("ВНИМАНИЕ: сервер работает без защиты (API_KEY не задан в .env)") while True: try: conn, client_addr = s.accept() request = conn.recv(1024).decode("utf-8", "ignore") # Парсим первую строку запроса: "GET /send?code=power HTTP/1.1" first_line = request.split("\r\n", 1)[0] parts = first_line.split(" ") path = parts[1] if len(parts) > 1 else "/" headers = parse_headers(request) status, body = handle_request(path, headers, api_key) body_json = json.dumps(body) body_bytes = body_json.encode("utf-8") status_text = {200: "OK", 400: "Bad Request", 401: "Unauthorized", 404: "Not Found", 408: "Request Timeout", 500: "Internal Server Error"}.get(status, "OK") response_headers = ( f"HTTP/1.1 {status} {status_text}\r\n" "Content-Type: application/json; charset=utf-8\r\n" "Access-Control-Allow-Origin: *\r\n" f"Content-Length: {len(body_bytes)}\r\n" "Connection: close\r\n" "\r\n" ) conn.sendall(response_headers.encode("utf-8")) conn.sendall(body_bytes) conn.close() except Exception as e: print(f"Ошибка обработки запроса: {e}") try: conn.close() except Exception: pass def main(): env = load_env() ssid = env.get("WIFI_SSID") password = env.get("WIFI_PASSWORD") api_key = env.get("API_KEY") if not ssid or not password: print("Ошибка: заполните WIFI_SSID и WIFI_PASSWORD в .env") return if not api_key: print( "Предупреждение: API_KEY не задан в .env - эндпоинты будут доступны без защиты") ip = connect_wifi(ssid, password) if ip: run_server(ip, api_key) else: print("Сервер не запущен: нет WiFi") if __name__ == "__main__": main()