import socket
import struct
import threading
import time
import sys

# --- Константы ---
FPS = 20
FRAME_TIME = 1.0 / FPS
ARTNET_PORT = 6454

class ArtNetController:
    def __init__(self):
        # Инициализация адресов 12 шлюзов
        self.gateways = [f"192.168.2.{120 + i}" for i in range(1, 13)]
        
        # dmx_state[ip][universe] = bytearray(512)
        self.dmx_state = {ip: {u: bytearray(512) for u in range(4)} for ip in self.gateways}
        
        self.lock = threading.Lock()
        self.running = True
        
        # Настраиваем UDP сокет
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        
        # Запускаем поток отправки
        self.sender_thread = threading.Thread(target=self._artnet_loop, daemon=True)
        self.sender_thread.start()

    def _build_packet(self, universe, dmx_data):
        """Ручная сборка пакета Art-Net (OpDmx)."""
        packet = bytearray()
        packet.extend(b'Art-Net\x00')          # ID
        packet.extend(struct.pack('<H', 0x5000)) # OpCode (Little Endian)
        packet.extend(struct.pack('>H', 14))     # Протокол (Big Endian)
        packet.append(0)                         # Sequence
        packet.append(0)                         # Physical
        packet.extend(struct.pack('<H', universe))# Universe (Little Endian)
        packet.extend(struct.pack('>H', 512))    # Length (Big Endian)
        packet.extend(dmx_data)                  # DMX Data (512 байт)
        return packet

    def _artnet_loop(self):
        """Фоновый поток для безостановочной отправки 20 FPS."""
        while self.running:
            start_time = time.perf_counter()
            
            with self.lock:
                for ip, universes in self.dmx_state.items():
                    for u, data in universes.items():
                        packet = self._build_packet(u, data)
                        try:
                            self.sock.sendto(packet, (ip, ARTNET_PORT))
                        except Exception:
                            # Игнорируем сетевые ошибки, чтобы не уронить поток
                            pass
            
            # Высчитываем сколько нужно доспать до следующего кадра (20 FPS)
            elapsed = time.perf_counter() - start_time
            sleep_time = max(0, FRAME_TIME - elapsed)
            time.sleep(sleep_time)
            
    def set_device(self, gw, grp, dev, state):
        """Логика маппинга адресов и записи в буфер."""
        if not (1 <= gw <= 12 and 1 <= grp <= 8 and 1 <= dev <= 5):
            print("Ошибка: Проверьте границы (Шлюз: 1-12, Группа: 1-8, Устройство: 1-5)")
            return

        ip = f"192.168.2.{120 + gw}"
        
        # Расчет адресации (индексация с нуля)
        u = (grp - 1) // 2                # Юниверсы 0, 0, 1, 1, 2, 2, 3, 3
        base_ch = ((grp - 1) % 2) * 256   # 0 для нечетных групп, 256 для четных
        start_ch = base_ch + ((dev - 1) * 45)
        
        val = 255 if state else 0
        
        with self.lock:
            for i in range(45):
                self.dmx_state[ip][u][start_ch + i] = val

    def stop(self):
        self.running = False
        self.sender_thread.join(timeout=1.0)
        self.sock.close()

def run_batch_test(controller, batch_size):
    """Пакетное тестирование по N устройств за раз."""
    all_devices = [(gw, grp, dev) 
                   for gw in range(1, 13) 
                   for grp in range(1, 9) 
                   for dev in range(1, 6)]
    
    total = len(all_devices)
    print(f"\nЗапущено тестирование. Размер пачки: {batch_size}. Всего устройств: {total}")
    
    for i in range(0, total, batch_size):
        batch = all_devices[i:i + batch_size]
        print(f"--- Проверка: устройства с {i+1} по {i + len(batch)} ---")
        
        # Включаем пачку
        for gw, grp, dev in batch:
            controller.set_device(gw, grp, dev, 1)
            
        try:
            input("Устройства ВКЛЮЧЕНЫ. Нажми Enter чтобы выключить и перейти к следующей (Ctrl+C для отмены)...")
        except KeyboardInterrupt:
            # Гасим текущую пачку перед вылетом
            for gw, grp, dev in batch:
                controller.set_device(gw, grp, dev, 0)
            raise
            
        # Выключаем пачку
        for gw, grp, dev in batch:
            controller.set_device(gw, grp, dev, 0)
            
    print("Тестирование завершено!\n")

def main():
    print("Запуск Art-Net контроллера (20 FPS)...")
    controller = ArtNetController()
    
    print("\nКоманды:")
    print("  set <шлюз> <группа> <устройство> <1/0>  (Пример: set 1 3 5 1)")
    print("  test <размер_пачки>                     (Пример: test 44)")
    print("  exit                                    (Выход)")
    
    try:
        while True:
            cmd_input = input("\n> ").strip().lower()
            if not cmd_input:
                continue
                
            parts = cmd_input.split()
            cmd = parts[0]
            
            if cmd == "exit":
                break
                
            elif cmd == "set" and len(parts) == 5:
                try:
                    gw, grp, dev, state = map(int, parts[1:])
                    controller.set_device(gw, grp, dev, state)
                    state_str = "ВКЛ" if state else "ВЫКЛ"
                    print(f"Шлюз {gw}, Группа {grp}, Устройство {dev} -> {state_str}")
                except ValueError:
                    print("Ошибка: Параметры должны быть числами.")
                    
            elif cmd == "test" and len(parts) == 2:
                try:
                    batch_size = int(parts[1])
                    if batch_size <= 0:
                        print("Размер пачки должен быть > 0")
                        continue
                    run_batch_test(controller, batch_size)
                except ValueError:
                    print("Ошибка: Неверный формат числа.")
            else:
                print("Неизвестная команда или неверное число аргументов.")
                
    except KeyboardInterrupt:
        print("\nПолучен сигнал прерывания (Ctrl+C).")
    finally:
        print("Завершение работы и остановка потоков...")
        controller.stop()
        print("Готово.")

if __name__ == "__main__":
    main()
    