Загрузка данных
# ==============================================================================
# ИМПОРТ НЕОБХОДИМЫХ БИБЛИОТЕК
# ==============================================================================
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):
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):
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 - 10
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):
# Инициализация SPI и дисплея
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)
# Инициализация датчика DHT11
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
# Переменные для защиты от падения
self.edge_detected = False
self.safe_mode_active = 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 check_edge(self):
"""
Проверяет, есть ли край стола впереди.
Возвращает True, если край есть.
"""
dist = self.measure_distance()
# Если расстояние резко увеличилось (было < 10, стало > 20)
if self.last_distance < 10 and dist > 20:
self.edge_detected = True
return True
self.last_distance = dist
return False
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 force_stop_and_turn(self):
"""
Принудительная остановка, разворот и отъезд.
Вызывается при обнаружении края.
"""
print("!!! ПРИНУДИТЕЛЬНАЯ ОСТАНОВКА: КРАЙ СТОЛА !!!")
self.stop_motors()
self.turn_around_180()
# Отъезжаем назад на безопасное расстояние
self.move_forward()
time.sleep(1.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.edge_detected = False
def safe_forward(self, duration=1.0):
"""
Безопасное движение вперед.
Проверяет край стола ПЕРЕД началом движения.
"""
if self.check_edge():
self.force_stop_and_turn()
return False
# Если края нет, едем
self.move_forward()
time.sleep(duration)
self.stop_motors()
return True
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):
"""Разворот на 180 градусов."""
self.turn_left()
time.sleep(0.7)
self.stop_motors()
def update_display_text(self, t, h):
"""Обновление нижней полосы с текстом."""
if self.display:
self.display.fill_rectangle(0, 220, 320, 20, color565(0, 0, 0))
self.display.draw_text8x8(10, 225, 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. Проверка края стола (ВЫСШИЙ ПРИОРИТЕТ) ---
if self.check_edge():
self.force_stop_and_turn()
continue # Пропускаем остальную логику на этом цикле
# --- 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)
if action == 'forward':
# Используем безопасное движение
if self.safe_forward(duration):
self.is_moving = True
self.move_end_time = time.ticks_add(now, int(duration * 1000))
else:
# Если был край, мы уже развернулись, просто сбрасываем таймер
pass
elif action == 'left':
self.turn_left()
self.is_moving = True
self.move_end_time = time.ticks_add(now, int(duration * 1000))
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):
"""
Обработка команд. ВАЖНО: Команда 'y' теперь вызывает safe_forward,
которая проверяет край стола.
"""
try:
cmd = data.decode('utf-8').strip().lower()
print(f"BT Команда: {cmd}")
# --- КОМАНДЫ ДВИЖЕНИЯ ---
if cmd == 'y':
# Вместо прямого move_forward используем безопасную функцию
if not robot.safe_forward(1.0):
print("Движение вперед заблокировано: обнаружен край стола!")
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, 'LOVE.bin', fps=30)
elif cmd == 'q':
play_animation(robot.tft, 'hello.bin', fps=30)
# --- ТЕКСТ ---
elif cmd.startswith('text:'):
text_to_show = cmd.split(':')[1]
robot.show_text_on_screen(text_to_show)
# --- МОРГАНИЕ ---
elif cmd.startswith('blink:'):
try:
count = int(cmd.split(':')[1])
print(f"Моргаем {count} раз по команде с телефона...")
for _ in range(count):
play_animation(robot.tft, 'blink.bin', fps=30)
time.sleep(0.5)
except ValueError:
print("Ошибка: неверный формат команды моргания")
elif cmd == 'stop':
robot.stop_motors()
except Exception as e:
print("Ошибка команды:", e)
sp.on_write(on_rx)
# ==============================================================================
# ЗАПУСК ПРОГРАММЫ
# ==============================================================================
if __name__ == "__main__":
robot.run()