Загрузка данных


# ==============================================================================
# ИМПОРТ НЕОБХОДИМЫХ БИБЛИОТЕК
# ==============================================================================
import random
import time
from machine import Pin, SPI, PWM, time_pulse_us
import bluetooth
from ble_simple_peripheral import BLESimplePeripheral
import dht

# Попытка импорта библиотеки для работы с текстом на дисплее
try:
    from ili9341 import Display, color565
    TEXT_LIB_AVAILABLE = True
except ImportError:
    TEXT_LIB_AVAILABLE = False
    print("Внимание: Библиотека ili9341.py не найдена. Текст выводиться не будет.")


# ==============================================================================
# КЛАСС 1: БЫСТРЫЙ ДРАЙВЕР ДЛЯ АНИМАЦИЙ (ILI9341Direct)
# ==============================================================================
class ILI9341Direct:
    """
    Этот класс используется для сверхбыстрой отрисовки 1-битных картинок (.bin).
    Он напрямую работает с SPI-шиной и регистрами дисплея.
    """
    def __init__(self, spi, cs_pin, dc_pin, rst_pin):
        self.spi = spi
        self.cs = Pin(cs_pin, Pin.OUT)
        self.dc = Pin(dc_pin, Pin.OUT)
        self.rst = Pin(rst_pin, Pin.OUT)
        self.reset()
        self.init_display()

    def reset(self):
        """Аппаратный сброс дисплея"""
        self.rst.value(0)
        time.sleep_ms(50)
        self.rst.value(1)
        time.sleep_ms(100)

    def write_cmd(self, cmd):
        """Отправка команды в дисплей"""
        self.dc.value(0)
        self.cs.value(0)
        self.spi.write(bytearray([cmd]))
        self.cs.value(1)

    def write_data(self, data):
        """Отправка данных в дисплей"""
        self.dc.value(1)
        self.cs.value(0)
        self.spi.write(data)
        self.cs.value(1)

    def init_display(self):
        """Инициализация дисплея базовыми командами"""
        self.write_cmd(0x01)
        time.sleep_ms(100)
        self.write_cmd(0x11)
        time.sleep_ms(120)
        self.write_cmd(0x3A)
        self.write_data(b'\x55')
        self.write_cmd(0x36)
        self.write_data(b'\x28')
        self.write_cmd(0x29)
        time.sleep_ms(50)

    def set_window(self, x0, y0, x1, y1):
        """Установка области отрисовки (окна)"""
        self.write_cmd(0x2A)
        self.write_data(bytes([(x0 >> 8) & 0xFF, x0 & 0xFF, (x1 >> 8) & 0xFF, x1 & 0xFF]))
        self.write_cmd(0x2B)
        self.write_data(bytes([(y0 >> 8) & 0xFF, y0 & 0xFF, (y1 >> 8) & 0xFF, y1 & 0xFF]))
        self.write_cmd(0x2C)

    def clear(self, color=0x0000):
        """Полная очистка экрана"""
        self.set_window(0, 0, 319, 239)
        row_buf = color.to_bytes(2, 'big') * 320
        for _ in range(240):
            self.write_data(row_buf)


# ==============================================================================
# ДЕКОДЕР КАРТИНОК (VIPER)
# ==============================================================================
@micropython.viper
def decode_chunk_normal(raw_ptr: ptr8, rgb_ptr: ptr8, num_bytes: int, fg_h: int, fg_l: int, bg_h: int, bg_l: int):
    """
    Переводит 1-битный формат картинки в 16-битный RGB565.
    Работает на уровне указателей для максимальной скорости.
    """
    out_idx = 0
    for i in range(num_bytes):
        b = raw_ptr[i]
        for bit in range(7, -1, -1):
            if (b >> bit) & 1:
                rgb_ptr[out_idx] = fg_h
                rgb_ptr[out_idx + 1] = fg_l
            else:
                rgb_ptr[out_idx] = bg_h
                rgb_ptr[out_idx + 1] = bg_l
            out_idx += 2


# ==============================================================================
# ФУНКЦИЯ ПРОИГРЫВАНИЯ АНИМАЦИИ
# ==============================================================================
def play_animation(tft, filename, width=240, height=240, fps=30, fg_color=0xFFFF, bg_color=0x0000):
    """Читает файл .bin и отрисовывает его на экране кадр за кадром."""
    bytes_per_row = width // 8
    frame_bytes = bytes_per_row * height
    raw_buf = bytearray(frame_bytes)
    chunk_rows = 20
    chunk_raw_bytes = bytes_per_row * chunk_rows
    chunk_rgb = bytearray(width * chunk_rows * 2)

    fg_h = (fg_color >> 8) & 0xFF
    fg_l = fg_color & 0xFF
    bg_h = (bg_color >> 8) & 0xFF
    bg_l = bg_color & 0xFF

    # Центрируем анимацию по экрану
    x_offset = (320 - width) // 2
    y_offset = (240 - height) // 2
    
    delay_ms = int(1000 / fps) if fps > 0 else 0

    try:
        with open(filename, 'rb') as f:
            while True:
                read_bytes = f.readinto(raw_buf)
                if read_bytes < frame_bytes:
                    break
                
                t_start = time.ticks_ms()
                tft.set_window(x_offset, y_offset, x_offset + width - 1, y_offset + height - 1)
                
                for offset in range(0, frame_bytes, chunk_raw_bytes):
                    chunk_mem = memoryview(raw_buf)[offset : offset + chunk_raw_bytes]
                    decode_chunk_normal(chunk_mem, chunk_rgb, chunk_raw_bytes, fg_h, fg_l, bg_h, bg_l)
                    tft.write_data(chunk_rgb)
                
                if delay_ms > 0:
                    elapsed = time.ticks_diff(time.ticks_ms(), t_start)
                    sleep_time = delay_ms - elapsed
                    if sleep_time > 0:
                        time.sleep_ms(sleep_time)
    except OSError:
        print(f"Файл {filename} не найден")


# ==============================================================================
# КЛАСС 2: УПРАВЛЕНИЕ РОБОТОМ (ROBOT CONTROLLER)
# ==============================================================================
class RobotController:
    def __init__(self):
        # Инициализация пинов
        self.spi = SPI(2, baudrate=40000000, sck=Pin(18), mosi=Pin(23), miso=Pin(19))
        self.tft = ILI9341Direct(self.spi, cs_pin=5, dc_pin=2, rst_pin=4)
        self.tft.clear(0x0000)
        
        self.display = None
        if TEXT_LIB_AVAILABLE:
            try:
                self.display = Display(self.spi, dc=Pin(2), cs=Pin(5), rst=Pin(4), width=320, height=240, rotation=90)
            except:
                pass
                
        self.servo = PWM(Pin(25), freq=50)
        self.dht_sensor = dht.DHT11(Pin(33))
        self.trig = Pin(16, Pin.OUT)
        self.echo = Pin(35, Pin.IN)
        
        self.M_A_1A = Pin(26, Pin.OUT)
        self.M_A_1B = Pin(27, Pin.OUT)
        self.M_B_1A = Pin(13, Pin.OUT)
        self.M_B_1B = Pin(14, Pin.OUT)
        
        # Переменные состояния
        self.last_move_time = time.ticks_ms()
        self.wait_time = random.randint(10, 17) * 1000 
        self.is_moving = False
        self.move_end_time = 0
        self.last_distance = 999
        self.last_dht_time = time.ticks_ms()
        self.looked_around_this_wait = False 
        self.has_blinked_this_wait = False

    def set_servo_angle(self, angle):
        """Управление шеей (0-180)"""
        duty = int(4000 + (angle / 180) * 4000)
        self.servo.duty_u16(duty)

    def measure_distance(self):
        """Замер расстояния ультразвуком"""
        self.trig.value(0)
        time.sleep_us(2)
        self.trig.value(1)
        time.sleep_us(10)
        self.trig.value(0)
        try:
            duration = time_pulse_us(self.echo, 1, 30000)
            if duration > 0:
                return (duration * 0.0343) / 2
            return 999
        except OSError:
            return 999

    def stop_motors(self):
        self.M_A_1A.value(0)
        self.M_A_1B.value(0)
        self.M_B_1A.value(0)
        self.M_B_1B.value(0)

    def move_forward(self):
        self.M_A_1A.value(1)
        self.M_A_1B.value(0)
        self.M_B_1A.value(1)
        self.M_B_1B.value(0)

    def move_backward(self):
        self.M_A_1A.value(0)
        self.M_A_1B.value(1)
        self.M_B_1A.value(0)
        self.M_B_1B.value(1)

    def turn_left(self):
        self.M_A_1A.value(0)
        self.M_A_1B.value(1)
        self.M_B_1A.value(1)
        self.M_B_1B.value(0)

    def turn_right(self):
        self.M_A_1A.value(1)
        self.M_A_1B.value(0)
        self.M_B_1A.value(0)
        self.M_B_1B.value(1)

    def turn_around_180(self):
        """Разворот"""
        self.turn_left()
        time.sleep(0.7)
        self.stop_motors()
        
    def update_display_text(self, t, h):
        """Обновление верхней полосы с текстом"""
        if self.display:
            # Очищаем только верхние 20 пикселей
            self.display.fill_rectangle(0, 0, 320, 20, color565(0, 0, 0)) 
            self.display.draw_text8x8(10, 5, f"T: {t}C  H: {h}%", color565(255, 255, 255), color565(0,0,0), 0)

    def show_text_on_screen(self, text):
        """Вывод произвольного текста на экран"""
        if self.display:
            self.display.fill_rectangle(0, 20, 320, 240, color565(0, 0, 0))
            self.display.draw_text8x8(10, 100, text, color565(255, 255, 255), color565(0,0,0), 0)

    def run(self):
        """Главный цикл робота"""
        print("Робот LOIK запущен!")
        
        while True:
            now = time.ticks_ms()

            # 1. Обновление датчика DHT11
            if time.ticks_diff(now, self.last_dht_time) > 3000:
                try:
                    self.dht_sensor.measure()
                    t = self.dht_sensor.temperature()
                    h = self.dht_sensor.humidity()
                    self.update_display_text(t, h)
                except OSError:
                    pass
                self.last_dht_time = now

            # 2. Проверка края стола
            dist = self.measure_distance()
            if self.last_distance < 10 and dist > 20:
                print("КРАЙ СТОЛА! Разворот.")
                self.stop_motors()
                self.turn_around_180()
                self.move_forward()
                time.sleep(random.uniform(1.0, 2.5))
                self.stop_motors()
                
                self.last_move_time = time.ticks_ms()
                self.wait_time = random.randint(10, 17) * 1000
                self.is_moving = False
                self.looked_around_this_wait = False
                self.has_blinked_this_wait = False
            self.last_distance = dist

            # 3. Моргание
            if not self.is_moving and not self.has_blinked_this_wait:
                if time.ticks_diff(now, self.last_move_time) >= 10000:
                    play_animation(self.tft, 'blink.bin', fps=30)
                    self.has_blinked_this_wait = True

            # 4. Логика ожидания и осмотра
            if not self.is_moving:
                elapsed_wait = time.ticks_diff(now, self.last_move_time)
                
                # Осмотр
                if elapsed_wait >= 10000 and not self.looked_around_this_wait:
                    wait_seconds = self.wait_time // 1000 
                    if wait_seconds > 14:  
                        print(f"Осматриваемся (ждем {wait_seconds} сек)...")
                        self.set_servo_angle(180)
                        time.sleep(2)
                        self.set_servo_angle(0)
                        time.sleep(4)
                        self.set_servo_angle(90)
                        time.sleep(1)
                    self.looked_around_this_wait = True 

                # Движение
                if elapsed_wait >= self.wait_time:
                    action = random.choice(['forward', 'forward', 'forward', 'left', 'right'])
                    duration = random.uniform(1.0, 2.5) 
                    print(f"Едем: {action} ({duration:.1f} сек)")
                    
                    if action == 'forward': self.move_forward()
                    elif action == 'left': self.turn_left()
                    elif action == 'right': self.turn_right()
                        
                    self.is_moving = True
                    self.move_end_time = time.ticks_add(now, int(duration * 1000))

            # 5. Завершение движения
            else:
                if time.ticks_diff(now, self.move_end_time) >= 0:
                    self.stop_motors()
                    self.is_moving = False
                    self.last_move_time = time.ticks_ms()
                    self.wait_time = random.randint(10, 17) * 1000 
                    self.looked_around_this_wait = False 
                    self.has_blinked_this_wait = False

            time.sleep_ms(20)


# ==============================================================================
# НАСТРОЙКА BLUETOOTH И ОБРАБОТЧИК КОМАНД
# ==============================================================================
robot = RobotController()
ble = bluetooth.BLE()
sp = BLESimplePeripheral(ble, name="LOIK")

def on_rx(data):
    """Обработка всех команд, приходящих по Bluetooth"""
    try:
        cmd = data.decode('utf-8').strip().lower()
        print(f"BT Команда: {cmd}")
        
        # Команды движения
        if cmd == 'y': 
            robot.move_forward(); time.sleep(1); robot.stop_motors()
        elif cmd == 'o': 
            robot.move_backward(); time.sleep(1); robot.stop_motors()
        elif cmd == 'r': 
            robot.turn_left(); time.sleep(0.5); robot.stop_motors()
        elif cmd == 't': 
            robot.turn_right(); time.sleep(0.5); robot.stop_motors()
            
        # Команды шеи
        elif cmd == 'gleft': 
            robot.set_servo_angle(180); time.sleep(1.5); robot.set_servo_angle(90)
        elif cmd == 'gright': 
            robot.set_servo_angle(0); time.sleep(1.5); robot.set_servo_angle(90)
            
        # Анимации
        elif cmd == 'w': 
            play_animation(robot.tft, 'anim_1bit6.bin', fps=30) 
        elif cmd == 'q': 
            play_animation(robot.tft, 'hi.bin', fps=30) 
            
        # Вывод текста
        elif cmd.startswith('text:'):
            text_to_show = cmd.split(':')[1]
            robot.show_text_on_screen(text_to_show)
            
        elif cmd == 'stop':
            robot.stop_motors()
            
    except Exception as e:
        print("Ошибка команды:", e)

sp.on_write(on_rx)


# ==============================================================================
# ЗАПУСК ПРОГРАММЫ
# ==============================================================================
if __name__ == "__main__":
    robot.run()