2
This commit is contained in:
+110
-17
@@ -7,6 +7,7 @@ from dotenv import load_env
|
||||
|
||||
# ==== НАСТРОЙКИ ====
|
||||
TRANSMIT_PIN = 19
|
||||
CODES_FILE = "codes.json"
|
||||
# ====================
|
||||
|
||||
|
||||
@@ -27,7 +28,7 @@ def connect_wifi(ssid, password, timeout_s=15):
|
||||
return ip
|
||||
|
||||
|
||||
def load_codes(filename="codes.json"):
|
||||
def load_codes(filename=CODES_FILE):
|
||||
try:
|
||||
with open(filename, "r") as f:
|
||||
return json.load(f)
|
||||
@@ -35,6 +36,32 @@ def load_codes(filename="codes.json"):
|
||||
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)
|
||||
@@ -71,9 +98,28 @@ def parse_query(path):
|
||||
return route, params
|
||||
|
||||
|
||||
def handle_request(path):
|
||||
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:
|
||||
@@ -87,25 +133,65 @@ def handle_request(path):
|
||||
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()
|
||||
return 200, {"codes": list(codes.keys())}
|
||||
|
||||
elif route == "/":
|
||||
return 200, {"status": "ESP32 IR сервер запущен", "endpoints": ["/list", "/send?code=ИМЯ"]}
|
||||
details = {name: len(timings) for name, timings in codes.items()}
|
||||
return 200, {"codes": details}
|
||||
|
||||
else:
|
||||
return 404, {"error": "неизвестный маршрут"}
|
||||
|
||||
|
||||
def run_server(ip):
|
||||
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}/")
|
||||
print(f"Примеры: http://{ip}/list | http://{ip}/send?code=power")
|
||||
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:
|
||||
@@ -116,23 +202,25 @@ def run_server(ip):
|
||||
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)
|
||||
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", 404: "Not Found",
|
||||
500: "Internal Server Error"}.get(status, "OK")
|
||||
status_text = {200: "OK", 400: "Bad Request", 401: "Unauthorized", 404: "Not Found",
|
||||
408: "Request Timeout", 500: "Internal Server Error"}.get(status, "OK")
|
||||
|
||||
response = (
|
||||
response_headers = (
|
||||
f"HTTP/1.1 {status} {status_text}\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Content-Type: application/json; charset=utf-8\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
f"Content-Length: {len(body_json)}\r\n"
|
||||
f"Content-Length: {len(body_bytes)}\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
f"{body_json}"
|
||||
)
|
||||
conn.send(response.encode())
|
||||
conn.sendall(response_headers.encode("utf-8"))
|
||||
conn.sendall(body_bytes)
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Ошибка обработки запроса: {e}")
|
||||
@@ -146,14 +234,19 @@ 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)
|
||||
run_server(ip, api_key)
|
||||
else:
|
||||
print("Сервер не запущен: нет WiFi")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user