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


import random
import time
from machine import Pin, SPI, UART, PWM
import bluetooth
from ble_simple_peripheral import BLESimplePeripheral


# ==============================================================================
# 1. ДРАЙВЕР ДИСПЛЕЯ ILI9341 (320x240)
# ==============================================================================
class ILI9341Direct:

  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)  # Soft reset
    time.sleep_ms(100)
    self.write_cmd(0x11)  # Sleep out
    time.sleep_ms(120)
    self.write_cmd(0x3A)  # RGB565 (16 бит)
    self.write_data(b'\x55')
    self.write_cmd(0x36)  # Альбомная ориентация (320x240)
    self.write_data(b'\x28')
    self.write_cmd(0x29)  # Display on
    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):
    """Очистка всего экрана 320x240 черным цветом"""
    self.set_window(0, 0, 319, 239)
    row_buf = color.to_bytes(2, 'big') * 320
    for _ in range(240):
      self.write_data(row_buf)


# ==============================================================================
# 2. СВЕРХБЫСТРЫЕ ДЕКОДЕРЫ 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,
):
  """Обычный вывод кадров слева направо"""
  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


@micropython.viper
def decode_chunk_mirrored(
    raw_ptr: ptr8,
    rgb_ptr: ptr8,
    num_bytes: int,
    bytes_per_row: int,
    fg_h: int,
    fg_l: int,
    bg_h: int,
    bg_l: int,
):
  """Отзеркаливание кадра по горизонтали на лету"""
  num_rows = num_bytes // bytes_per_row
  out_idx = 0
  for r in range(num_rows):
    row_start = r * bytes_per_row
    # Читаем байты строки в обратном порядке (справа налево)
    for c in range(bytes_per_row - 1, -1, -1):
      b = raw_ptr[row_start + c]
      # Читаем биты внутри байта в обратном порядке
      for bit in range(8):
        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,
    mirror=False,
):
  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]

          if mirror:
            decode_chunk_mirrored(
                chunk_mem,
                chunk_rgb,
                chunk_raw_bytes,
                bytes_per_row,
                fg_h,
                fg_l,
                bg_h,
                bg_l,
            )
          else:
            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} не найден!')


# ==============================================================================
# 3. НАСТРОЙКА И ОСНОВНОЙ ЦИКЛ РОБОТА
# ==============================================================================
# Инициализация SPI и Дисплея
servo = PWM(Pin(25), freq=50)
ble = bluetooth.BLE()
sp = BLESimplePeripheral(ble, name="LOIK 2 PRO")
spi = SPI(2, baudrate=40000000, sck=Pin(18), mosi=Pin(23), miso=Pin(19))
tft = ILI9341Direct(spi, cs_pin=5, dc_pin=2, rst_pin=4)
tft.clear(0x0000)

# Инициализация Bluetooth (UART2: TX=17, RX=16)
bt_uart = UART(2, baudrate=9600, tx=17, rx=16)


def read_bluetooth_command():
  """Считывание команд по Bluetooth без задержки системы"""
  if bt_uart.any():
    raw_data = bt_uart.read()
    if raw_data:
      return raw_data.decode('utf-8').strip().lower()
  return None

def set_servo_angle(angle):
    # Переводим угол (0-180) в ширину импульса для duty_u16
    # 4000 (~0.5 мс) — это 0 градусов, 8000 (~2.4 мс) — 180 градусов
    duty = int(4000 + (angle / 180) * 4000)
    servo.duty_u16(duty)

def OG():
    # Генерируем случайное число от 1 до 20
    rand_val = random.randint(5, 20)
    print(f"Проверка бездействия. Выпало число: {rand_val}")
    
    time.sleep(1)
    
    # Если число 14 или больше — крутим головой
    if rand_val >= 14:
        print("Скучно! Осматриваемся...")
        
        # Поворот налево (например, 30 градусов)
        set_servo_angle(180)
        time.sleep(2)
        
        # Поворот направо (например, 150 градусов)
        set_servo_angle(0)
        time.sleep(4)
        
        # Возврат в исходное положение (центр — 90 градусов)
        set_servo_angle(90)
        time.sleep(2)

# Переменные таймеров
last_blink_time = time.ticks_ms()
blink_count = 0

# Список анимаций для случайного воспроизведения в режиме простоя
IDLE_ANIMATIONS = ['idle1.bin', 'idle2.bin']

# Запуск стартовой анимации при включении
play_animation(tft, 'blink.bin', width=240, height=240, fps=30)

while True:
  # 1. Проверяем команды с телефона по Bluetooth
  cmd = read_bluetooth_command()

  if cmd:
    print(f'Команда по BT: {cmd}')
    blink_count = 0  # Сбрасываем таймер простоя
    last_blink_time = time.ticks_ms()

    if cmd == 'love':
      play_animation(tft, 'anim_1bit6.bin', fps=30)

    elif cmd == 'transform':
      # Случайно выбираем: проиграть обычно или отзеркалить!
      use_mirror = random.choice([True, False])
      play_animation(tft, 'transform.bin', fps=30, mirror=use_mirror)

  # 2. Моргание каждые 10 секунд (10 000 мс)
  elif time.ticks_diff(time.ticks_ms(), last_blink_time) >= 10000:
    play_animation(tft, 'blink.bin', fps=30)
    last_blink_time = time.ticks_ms()
    blink_count += 1
    
    OG()
    
  time.sleep_ms(50)