Загрузка данных
# ==============================================================================
# ФАЙЛ: main.py
# ОПИСАНИЕ: Главный скрипт управления роботом LOIK.
# ВЕРСИЯ: 3.0 (Ideal State Machine Edition)
# ==============================================================================
# ==============================================================================
# БЛОК 1: ИМПОРТ БИБЛИОТЕК И НАСТРОЙКА ОКРУЖЕНИЯ
# ==============================================================================
# Импортируем стандартные библиотеки
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 не найдена. Текст выводиться не будет.")
# ==============================================================================
# БЛОК 2: КЛАСС ДРАЙВЕРА ДИСПЛЕЯ (ILI9341Direct)
# Этот класс отвечает за быструю отрисовку анимаций и очистку экрана.
# ==============================================================================
class DisplayDriver:
"""
Драйвер для дисплея ILI9341.
Реализует прямую работу с регистрами через SPI для максимальной скорости.
"""
def __init__(self, spi, cs_pin, dc_pin, rst_pin):
"""
Конструктор класса.
Принимает шину SPI и номера пинов управления (CS, DC, RST).
"""
# Сохраняем шину SPI для дальнейшего использования
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):
"""
Аппаратный сброс дисплея.
Подаем низкий уровень на RST, ждем, затем высокий.
"""
self.rst.value(0)
time.sleep_ms(50)
self.rst.value(1)
time.sleep_ms(100)
def write_cmd(self, cmd):
"""
Отправка команды в дисплей.
DC = 0 (команда), CS = 0 (выбор чипа).
"""
self.dc.value(0)
self.cs.value(0)
self.spi.write(bytearray([cmd]))
self.cs.value(1)
def write_data(self, data):
"""
Отправка данных в дисплей.
DC = 1 (данные), CS = 0 (выбор чипа).
"""
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) # Установка формата цвета (16 бит)
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):
"""
Установка области отрисовки (окна).
Принимает координаты левого верхнего и правого нижнего углов.
"""
# Команда установки колонок (X)
self.write_cmd(0x2A)
self.write_data(bytes([(x0 >> 8) & 0xFF, x0 & 0xFF, (x1 >> 8) & 0xFF, x1 & 0xFF]))
# Команда установки строк (Y)
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)
# Создаем буфер одной строки (320 пикселей по 2 байта)
row_buf = color.to_bytes(2, 'big') * 320
# Отправляем буфер 240 раз (по количеству строк)
for _ in range(240):
self.write_data(row_buf)
# ==============================================================================
# БЛОК 3: ДЕКОДЕР АНИМАЦИЙ (VIPER)
# Переводит 1-битный формат в 16-битный RGB565 на лету.
# ==============================================================================
@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):
"""
Функция декодирования, ускоренная через Viper.
Принимает указатели на сырые данные и буфер вывода.
"""
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
# ==============================================================================
# БЛОК 4: ФУНКЦИЯ ПРОИГРЫВАНИЯ АНИМАЦИИ
# Читает файл .bin и отрисовывает его на экране.
# ==============================================================================
def play_animation(display, filename, width=240, height=240, fps=30, fg_color=0xFFFF, bg_color=0x0000):
"""
Воспроизводит бинарный файл как анимацию.
Вычисляет задержку между кадрами на основе FPS.
"""
# Вычисляем количество байт в одной строке (ширина / 8)
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()
# Устанавливаем окно для текущего кадра
display.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)
display.write_data(chunk_rgb)
# Выдерживаем задержку для соблюдения FPS
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} не найден!")
# ==============================================================================
# БЛОК 5: КЛАСС УПРАВЛЕНИЯ ДАТЧИКАМИ (SensorManager)
# Отвечает за опрос DHT11 и ультразвукового датчика с фильтрацией.
# ==============================================================================
class SensorManager:
"""
Менеджер датчиков. Инкапсулирует логику работы с DHT11 и HC-SR04.
"""
def __init__(self, trig_pin, echo_pin, dht_pin):
# Инициализация ультразвукового датчика
self.trig = Pin(trig_pin, Pin.OUT)
self.echo = Pin(echo_pin, Pin.IN)
# Инициализация датчика температуры и влажности
self.dht_sensor = dht.DHT11(Pin(dht_pin))
# Переменные для хранения последних показаний
self.last_distance = 999
self.last_temperature = 0
self.last_humidity = 0
def _single_measure(self):
"""
Одиночный замер расстояния ультразвуком.
Возвращает расстояние в сантиметрах или 999 в случае ошибки.
"""
self.trig.value(0)
time.sleep_us(2)
self.trig.value(1)
time.sleep_us(10)
self.trig.value(0)
try:
# Ждем эхо-сигнал (максимум 30 мс)
duration = time_pulse_us(self.echo, 1, 30000)
if duration > 0:
# Скорость звука 343 м/с = 0.0343 см/мкс. Делим на 2 (путь туда-обратно)
return (duration * 0.0343) / 2
return 999
except OSError:
return 999
def measure_distance_filtered(self):
"""
Фильтрованный замер. Делает 3 замера и отсеивает случайные выбросы.
Это защищает от ложных срабатываний датчика.
"""
d1 = self._single_measure()
time.sleep_ms(5)
d2 = self._single_measure()
time.sleep_ms(5)
d3 = self._single_measure()
# Если два значения близки, возвращаем их среднее
if abs(d1 - d2) < 15: return (d1 + d2) / 2
if abs(d2 - d3) < 15: return (d2 + d3) / 2
if abs(d1 - d3) < 15: return (d1 + d3) / 2
# Если все разные (шум), возвращаем медиану
return sorted([d1, d2, d3])[1]
def check_edge(self):
"""
Проверяет наличие края стола.
Возвращает True, если обнаружен резкий перепад (обрыв).
"""
dist = self.measure_distance_filtered()
# Если было меньше 10 см, а стало больше 30 см - это обрыв
if self.last_distance < 10 and dist > 30:
return True
self.last_distance = dist
return False
def read_dht(self):
"""
Читает температуру и влажность.
Возвращает кортеж (температура, влажность) или (None, None) при ошибке.
"""
try:
self.dht_sensor.measure()
self.last_temperature = self.dht_sensor.temperature()
self.last_humidity = self.dht_sensor.humidity()
return self.last_temperature, self.last_humidity
except OSError:
return None, None
# ==============================================================================
# БЛОК 6: КЛАСС УПРАВЛЕНИЯ МОТОРАМИ (MotorDriver)
# Инкапсулирует логику подачи сигналов на драйвер L9110S.
# ==============================================================================
class MotorDriver:
"""
Драйвер моторов. Управляет двумя каналами (A и B).
"""
def __init__(self, a1a_pin, a1b_pin, b1a_pin, b1b_pin):
# Инициализация пинов как выходов
self.M_A_1A = Pin(a1a_pin, Pin.OUT)
self.M_A_1B = Pin(a1b_pin, Pin.OUT)
self.M_B_1A = Pin(b1a_pin, Pin.OUT)
self.M_B_1B = Pin(b1b_pin, Pin.OUT)
def stop(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 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 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)
# ==============================================================================
# БЛОК 7: КЛАСС УПРАВЛЕНИЯ СЕРВОПРИВОДОМ (ServoManager)
# Отвечает за поворот шеи робота.
# ==============================================================================
class ServoManager:
"""
Управление сервоприводом (SG90).
"""
def __init__(self, pin):
self.servo = PWM(Pin(pin), freq=50)
def set_angle(self, angle):
"""
Устанавливает угол поворота (0-180 градусов).
Переводит угол в скважность (duty) для PWM.
"""
# 4000 - это 0 градусов, 8000 - это 180 градусов (для 16-битного PWM)
duty = int(4000 + (angle / 180) * 4000)
self.servo.duty_u16(duty)
# ==============================================================================
# БЛОК 8: КЛАСС УПРАВЛЕНИЯ РОБОТОМ (RobotController)
# Главный класс, связывающий всё воедино. Использует машину состояний.
# ==============================================================================
class RobotController:
"""
Ядро робота. Управляет состояниями, таймерами и взаимодействием модулей.
"""
# Определяем константы состояний
STATE_IDLE = 0 # Стоим, ждем
STATE_MOVING = 1 # Двигаемся
STATE_LOOKING = 2 # Осматриваемся
STATE_EMERGENCY = 3 # Экстренное торможение
def __init__(self):
# Инициализация аппаратных модулей
self.spi = SPI(2, baudrate=40000000, sck=Pin(18), mosi=Pin(23), miso=Pin(19))
self.display = DisplayDriver(self.spi, cs_pin=5, dc_pin=2, rst_pin=4)
self.display.clear(0x0000)
# Инициализация текстового дисплея
self.text_display = None
if TEXT_LIB_AVAILABLE:
try:
self.text_display = Display(self.spi, dc=Pin(2), cs=Pin(5), rst=Pin(4), width=320, height=240, rotation=90)
except:
pass
# Инициализация менеджеров
self.sensors = SensorManager(trig_pin=16, echo_pin=35, dht_pin=33)
self.motors = MotorDriver(a1a_pin=26, a1b_pin=27, b1a_pin=13, b1b_pin=14)
self.servo = ServoManager(pin=25)
# Переменные машины состояний
self.current_state = self.STATE_IDLE
self.state_timer = time.ticks_ms() # Таймер текущего состояния
self.action_timer = 0 # Таймер длительности движения
self.last_dht_time = time.ticks_ms()
# Случайные параметры
self.wait_time = random.randint(10, 17) * 1000 # Время ожидания (10-17 сек)
# Флаги
self.looked_around = False
self.has_blinked = False
self.action_type = ''
def update_display_text(self, t, h):
"""Обновляет текст в нижней полосе экрана."""
if self.text_display:
# Очищаем нижние 20 пикселей
self.text_display.fill_rectangle(0, 220, 320, 20, color565(0, 0, 0))
# Пишем текст
self.text_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.text_display:
self.text_display.fill_rectangle(0, 20, 320, 240, color565(0, 0, 0))
self.text_display.draw_text8x8(10, 100, text, color565(255, 255, 255), color565(0,0,0), 0)
def turn_around_180(self):
"""Разворот на 180 градусов."""
self.motors.turn_left()
time.sleep(0.7)
self.motors.stop()
def execute_emergency(self):
"""Экстренная остановка при обнаружении края."""
print("!!! КРАЙ СТОЛА !!!")
self.motors.stop()
self.turn_around_180()
self.motors.forward()
time.sleep(1.5) # Отъезжаем назад
self.motors.stop()
# Сбрасываем таймеры и генерируем новое время ожидания
self.current_state = self.STATE_IDLE
self.state_timer = time.ticks_ms()
self.wait_time = random.randint(10, 17) * 1000
self.looked_around = False
self.has_blinked = False
def start_new_cycle(self):
"""Начинает новый цикл ожидания."""
self.wait_time = random.randint(10, 17) * 1000
self.state_timer = time.ticks_ms()
self.current_state = self.STATE_IDLE
self.looked_around = False
self.has_blinked = False
self.action_type = ''
def run(self):
"""Главный цикл программы."""
print("Робот LOIK запущен! (v3.0)")
while True:
now = time.ticks_ms()
# --- 1. ОБНОВЛЕНИЕ ДАТЧИКА DHT11 ---
if time.ticks_diff(now, self.last_dht_time) > 3000:
t, h = self.sensors.read_dht()
if t is not None:
self.update_display_text(t, h)
self.last_dht_time = now
# --- 2. ГЛОБАЛЬНАЯ ПРОВЕРКА КРАЯ (ВЫСШИЙ ПРИОРИТЕТ) ---
if self.current_state != self.STATE_EMERGENCY:
if self.sensors.check_edge():
self.execute_emergency()
continue # Пропускаем остальную логику, начинаем заново
# --- 3. МАШИНА СОСТОЯНИЙ ---
# --- СОСТОЯНИЕ: ОЖИДАНИЕ ---
if self.current_state == self.STATE_IDLE:
elapsed = time.ticks_diff(now, self.state_timer)
# 3.1. Моргание раз в 10 секунд
if elapsed >= 10000 and not self.has_blinked:
print("[IDLE] Моргаем...")
play_animation(self.display, 'BLINK.bin', fps=30)
self.has_blinked = True
# 3.2. Осмотр ровно на 10-й секунде
if elapsed >= 10000 and not self.looked_around:
wait_seconds = self.wait_time // 1000
if wait_seconds > 14:
print(f"[IDLE] Осматриваемся (ждем {wait_seconds} сек)...")
self.servo.set_angle(180); time.sleep(2)
self.servo.set_angle(0); time.sleep(4)
self.servo.set_angle(90); time.sleep(1)
self.looked_around = True
# 3.3. Начало движения (если время вышло)
if elapsed >= self.wait_time:
action = random.choice(['forward', 'forward', 'forward', 'left', 'right'])
duration = random.uniform(1.0, 2.5)
self.action_type = action
self.action_timer = time.ticks_add(now, int(duration * 1000))
self.current_state = self.STATE_MOVING
print(f"[MOVE] Едем {action} ({duration:.1f} сек)")
# --- СОСТОЯНИЕ: ДВИЖЕНИЕ ---
elif self.current_state == self.STATE_MOVING:
# Проверяем, не пора ли остановиться
if time.ticks_diff(now, self.action_timer) >= 0:
self.motors.stop()
print("[MOVE] Остановка. Уходим в ожидание.")
self.start_new_cycle()
else:
# Выполняем движение с проверкой края (микро-шагами)
if self.action_type == 'forward':
self.motors.forward()
time.sleep_ms(50)
self.motors.stop()
elif self.action_type == 'left':
self.motors.turn_left()
time.sleep_ms(50)
self.motors.stop()
elif self.action_type == 'right':
self.motors.turn_right()
time.sleep_ms(50)
self.motors.stop()
# --- ФОНОВАЯ ЗАДЕРЖКА ---
time.sleep_ms(10)
# ==============================================================================
# БЛОК 9: ОБРАБОТКА КОМАНД BLUETOOTH
# ==============================================================================
# Инициализация робота
robot = RobotController()
# Инициализация Bluetooth
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':
# Проверка края перед принудительным движением
if robot.sensors.check_edge():
robot.execute_emergency()
else:
robot.current_state = robot.STATE_MOVING
robot.action_type = 'forward'
robot.action_timer = time.ticks_add(time.ticks_ms(), 1000)
elif cmd == 'o':
robot.motors.backward(); time.sleep(1); robot.motors.stop()
elif cmd == 'r':
robot.motors.turn_left(); time.sleep(0.5); robot.motors.stop()
elif cmd == 't':
robot.motors.turn_right(); time.sleep(0.5); robot.motors.stop()
# --- КОМАНДЫ ШЕИ ---
elif cmd == 'gleft':
robot.servo.set_angle(180); time.sleep(1.5); robot.servo.set_angle(90)
elif cmd == 'gright':
robot.servo.set_angle(0); time.sleep(1.5); robot.servo.set_angle(90)
# --- АНИМАЦИИ ---
elif cmd == 'w':
play_animation(robot.display, 'LOVE.bin', fps=30)
elif cmd == 'q':
play_animation(robot.display, '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])
for _ in range(count):
play_animation(robot.display, 'blink.bin', fps=30)
time.sleep(0.5)
except ValueError:
print("Ошибка: неверный формат команды моргания")
elif cmd == 'stop':
robot.motors.stop()
robot.start_new_cycle()
except Exception as e:
print("Ошибка обработки команды:", e)
# Привязываем обработчик к Bluetooth
sp.on_write(on_rx)
# ==============================================================================
# БЛОК 10: ЗАПУСК ПРОГРАММЫ
# ==============================================================================
if __name__ == "__main__":
# Запускаем главный цикл робота
robot.run()