Русский
Русский
English
Статистика
Реклама

Пост 6398342

Управляем шторой через Raspberry Pi

flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм

Несмотря на то, что программирование для автоматизации существенно упрощается при использовании, например, графического языка LabVIEW, отдельные уважаемые пидоры задались вопросом в предыдущей статье: зачем вообще в эпоху ИИ программировать самому хоть на Си, хоть на LabVIEW?

Хороший вопрос. Именно поэтому сегодня мы завайбкодим управление шторой на Python. А в качестве платформы используем хакерский девайс: одноплатный компьютер RaspberryPi на базе процессора CortexA53 с частотой 1,2ГГц.

flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм

Элиот Алдерсон из Мистера Робота хакает ЦОД SteelMountain при помощи RaspberryPi

Для свежей RaspberryPi вам потребуется чистая SDкарточка. Подключаете её к компьютеру, скачиваете RaspberryPiImager с официального сайта и устанавливаете его, находите своё устройство и далее, следуя указаниям устанавливаете на карточку операционную систему RaspberiPiOS.

flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм

После установки неплохо было бы проверить базовую работоспособность и настройку нашей платформы. Поэтому откроем Cloud и вбиваем такой промт:

Напиши код на Python для Paspberry Pi 3 для мигания светодиодом, подключённым к штыревому разъёму.

Копируем код, вставляем в свежесозданный файл led_blink.py и запускаем его на Raspberry.

flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм

Работает? Отлично, дальше пишем подробненький промт про штору:

Нужен код на Python для Raspberry Pi 3.
Код должен управлять шаговым двигателем шторы.
Разрешено использовать только те библиотеки, которые доступны в Raspberry Pi OS по умолчанию. Доустановка через pip install или как либо ещё недопустима, код должен работать из коробки.

На вход Raspberry Pi 3 принимает состояния двух выходов переключателя и двух концевиков-оптопар. Все необходимые подтяжки уже имеются на схемотехническом уровне. Можно считать, что на входы Raspberry Pi 3 поступают исключительно логические уровни.
Переключатель имеет три положения: "вверх", "выключен" и "вниз" и два выхода - вверх и вниз.
Положение "вверх" приводит к появлению лог.1 на выходе переключателя вверх и лог.0 на другом выходе.
Положение "вниз" приводит к появлению лог.1 на выходе переключателя вниз и лог.0 на другом выходе.
Положение "выключен" приводит к состоянию лог.0 на обоих выходах.

Концевики-оптопары расположены вверху и внизу рамы. Они работают в режиме open-drain с подтяжкой к питанию.
Нижняя оптопара вырабатывает сигнал "стоп" в виде перехода из лог.0 в лог.1 когда штора попадает в неё и луч прерывается.
Верхняя оптопара вырабатывает сигнал "стоп" в виде перехода из лог.1 в лог.0, когда штора выходит из нее и луч перестаёт прерываться.

Следует предусмотреть возможность перемещения шторы вниз по нажатию переключателя вниз при срабатывании верхнего концевика, и перемещение шторы вверх по переключателя вверх при срабатывании нижнего концевика. То есть чтобы наезд на концевик не вызывал полную блокировку управления шторы.

На выход Raspberry Pi 3 должна выдавать управляющие сигналы по четырём линиям на драйвер uln2003a, управляющий шаговым двигателем. Требуется реализовать полношаговый режим работы двигателя с двумя активными фазами и четырьмя состояниями. Обмотки двигателя должны находиться под напряжением и в состоянии покоя тоже, обеспечивая блокировку. Длительность каждого шага принять за 5мс.

Код на Python на Raspberry Pi 3 будет запускаться и останавливаться пользователем самостоятельно. Предусматривать функционал запуска и останова не требуется.

Необходимо вывести GUI с индикацией считанного состояния переключателя и концевиков, а также четырёх линий обмоток мотора. Предполагается, что GUI будет демонстрироваться в окне 800х400. Соответственно, элементы GUI должны иметь такой размер, чтобы окно было заполнен достаточно полно. Допускается увеличение размеров шрифта.

Выделяем код, который написал Cloud:

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

"""

Управление шаговым двигателем шторы на Raspberry Pi 3.

Только стандартные библиотеки Raspberry Pi OS: tkinter, RPi.GPIO, threading, time.

"""

import tkinter as tk

import threading

import time

import RPi.GPIO as GPIO

# ---------------------------------------------------------------------------

# Конфигурация GPIO (BCM)

# ---------------------------------------------------------------------------

PIN_SW_UP = 17 # переключатель "вверх"

PIN_SW_DOWN = 27 # переключатель "вниз"

PIN_LIM_TOP = 22 # верхний концевик: стоп по переходу 1 -> 0

PIN_LIM_BOT = 23 # нижний концевик: стоп по переходу 0 -> 1

PIN_M1 = 5 # ULN2003 IN1

PIN_M2 = 6 # ULN2003 IN2

PIN_M3 = 13 # ULN2003 IN3

PIN_M4 = 19 # ULN2003 IN4

MOTOR_PINS = [PIN_M1, PIN_M2, PIN_M3, PIN_M4]

STEP_DELAY = 0.005 # 5 мс на шаг

# Полношаговый режим, 2 активные фазы, 4 состояния

FULL_STEP_SEQ = [

[1, 1, 0, 0],

[0, 1, 1, 0],

[0, 0, 1, 1],

[1, 0, 0, 1],

]

# ---------------------------------------------------------------------------

# Инициализация GPIO

# ---------------------------------------------------------------------------

GPIO.setmode(GPIO.BCM)

# Все входы: подтяжки уже есть на схемотехническом уровне

GPIO.setup(PIN_SW_UP, GPIO.IN)

GPIO.setup(PIN_SW_DOWN, GPIO.IN)

GPIO.setup(PIN_LIM_TOP, GPIO.IN)

GPIO.setup(PIN_LIM_BOT, GPIO.IN)

for p in MOTOR_PINS:

GPIO.setup(p, GPIO.OUT, initial=0)

def apply_phase(phase):

"""Выставить состояние обмоток согласно фазе."""

for pin, val in zip(MOTOR_PINS, phase):

GPIO.output(pin, val)

# ---------------------------------------------------------------------------

# Логика управления

# ---------------------------------------------------------------------------

class CurtainController:

"""

Поток-контроллер. Опрашивает переключатель и концевики, вращает мотор.

Логика концевиков:

* верхний концевик сигнал "стоп" по переходу 1 -> 0;

* нижний концевик сигнал "стоп" по переходу 0 -> 1.

Движение вверх останавливается при срабатывании верхнего концевика.

Движение вниз останавливается при срабатывании нижнего концевика.

При этом, если после остановки пользователь удерживает переключатель

в направлении "от концевика" движение выполняется (наезд на концевик

не блокирует управление).

"""

def __init__(self):

self.lock = threading.Lock()

# Наблюдаемое состояние (для GUI)

self.sw_up = 0

self.sw_down = 0

self.lim_top = 0

self.lim_bot = 0

self.phase = [0, 0, 0, 0]

# Внутренние переменные

self.running = True

self.phase_idx = 0

# Предыдущие состояния концевиков для детектирования фронтов

self.prev_lim_top = GPIO.input(PIN_LIM_TOP)

self.prev_lim_bot = GPIO.input(PIN_LIM_BOT)

# Флаги "защёлкнутого" срабатывания концевика.

# True = соответствующий концевик сработал (стоп зафиксирован).

self.top_triggered = False

self.bot_triggered = False

# Текущее направление вращения: +1 вверх, -1 вниз, 0 стоп

self.direction = 0

self.thread = threading.Thread(target=self._loop, daemon=True)

# ------------------------------------------------------------------

def start(self):

# В покое удерживаем фазу (обмотки под напряжением)

apply_phase(FULL_STEP_SEQ[self.phase_idx])

with self.lock:

self.phase = list(FULL_STEP_SEQ[self.phase_idx])

self.thread.start()

def stop(self):

self.running = False

# ------------------------------------------------------------------

def _read_inputs(self):

"""Считать входы и обновить фронты концевиков."""

self.sw_up = GPIO.input(PIN_SW_UP)

self.sw_down = GPIO.input(PIN_SW_DOWN)

lim_top = GPIO.input(PIN_LIM_TOP)

lim_bot = GPIO.input(PIN_LIM_BOT)

# Верхний концевик: стоп по переходу 1 -> 0

if self.prev_lim_top == 1 and lim_top == 0:

self.top_triggered = True

# Если шторка ушла с верхнего концевика (0 -> 1) снимаем защёлку,

# чтобы концевик мог снова сработать при следующем заезде.

if self.prev_lim_top == 0 and lim_top == 1:

self.top_triggered = False

self.prev_lim_top = lim_top

# Нижний концевик: стоп по переходу 0 -> 1

if self.prev_lim_bot == 0 and lim_bot == 1:

self.bot_triggered = True

if self.prev_lim_bot == 1 and lim_bot == 0:

self.bot_triggered = False

self.prev_lim_bot = lim_bot

with self.lock:

self.lim_top = lim_top

self.lim_bot = lim_bot

# ------------------------------------------------------------------

def _decide_direction(self):

"""

Определить направление движения по переключателю и концевикам.

Возвращает +1 (вверх), -1 (вниз) или 0 (стоп).

"""

up = self.sw_up

down = self.sw_down

# Приоритет движение вверх

if up == 1 and down == 0:

# Стоп по верхнему концевику, если он сработал.

# Разрешаем движение, если концевик не сработал

# (в т.ч. если шторка уже вышла из него переход 0->1 снял защёлку).

if self.top_triggered:

return 0

return +1

if down == 1 and up == 0:

if self.bot_triggered:

return 0

return -1

# "выключено" или нештатная комбинация стоп

return 0

# ------------------------------------------------------------------

def _do_step(self, direction):

"""Сделать один шаг в заданном направлении."""

self.phase_idx = (self.phase_idx + direction) % 4

phase = FULL_STEP_SEQ[self.phase_idx]

apply_phase(phase)

with self.lock:

self.phase = list(phase)

time.sleep(STEP_DELAY)

# ------------------------------------------------------------------

def _loop(self):

while self.running:

self._read_inputs()

direction = self._decide_direction()

if direction == 0:

# Стоп: обмотки остаются под напряжением (блокировка),

# фаза не меняется двигатель удерживает положение.

self.direction = 0

time.sleep(0.002)

else:

self.direction = direction

self._do_step(direction)

# ---------------------------------------------------------------------------

# GUI

# ---------------------------------------------------------------------------

class App:

BG = "#101418"

FG = "#e6e6e6"

ON = "#28c76f"

OFF = "#3a3f45"

WARN = "#ff9f43"

ACCENT = "#1e88e5"

def __init__(self, root, controller):

self.root = root

self.ctrl = controller

root.title("Curtain Stepper Control")

root.geometry("800x400")

root.configure(bg=self.BG)

root.resizable(False, False)

big = ("DejaVu Sans", 22, "bold")

mid = ("DejaVu Sans", 18, "bold")

small = ("DejaVu Sans", 14)

# --- Заголовок ---

tk.Label(root, text="УПРАВЛЕНИЕ ШТОРОЙ",

font=("DejaVu Sans", 26, "bold"),

bg=self.BG, fg=self.FG).pack(pady=(10, 0))

# --- Верхний блок: переключатель + концевики ---

top = tk.Frame(root, bg=self.BG)

top.pack(fill="x", padx=20, pady=10)

# Переключатель

sw_frame = tk.LabelFrame(top, text=" Переключатель ",

font=mid, bg=self.BG, fg=self.FG,

labelanchor="n", bd=2, relief="groove")

sw_frame.pack(side="left", expand=True, fill="both", padx=6)

self.lbl_sw_up = self._make_indicator(sw_frame, "ВВЕРХ", big)

self.lbl_sw_down = self._make_indicator(sw_frame, "ВНИЗ", big)

# Концевики

lim_frame = tk.LabelFrame(top, text=" Концевики ",

font=mid, bg=self.BG, fg=self.FG,

labelanchor="n", bd=2, relief="groove")

lim_frame.pack(side="left", expand=True, fill="both", padx=6)

self.lbl_lim_top = self._make_indicator(lim_frame, "ВЕРХНИЙ", big)

self.lbl_lim_bot = self._make_indicator(lim_frame, "НИЖНИЙ", big)

# --- Блок обмоток ---

mot_frame = tk.LabelFrame(root, text=" Обмотки мотора (ULN2003) ",

font=mid, bg=self.BG, fg=self.FG,

labelanchor="n", bd=2, relief="groove")

mot_frame.pack(fill="x", padx=26, pady=10)

coils = tk.Frame(mot_frame, bg=self.BG)

coils.pack(pady=8)

self.coil_labels = []

for i in range(4):

f = tk.Frame(coils, bg=self.BG)

f.pack(side="left", padx=8)

tk.Label(f, text=f"IN{i+1}", font=small, bg=self.BG,

fg=self.FG).pack()

lbl = tk.Label(f, text="0", font=("DejaVu Sans", 34, "bold"),

width=3, height=1, bg=self.OFF, fg=self.FG)

lbl.pack()

self.coil_labels.append(lbl)

# --- Статус ---

self.lbl_status = tk.Label(root, text="СТОП",

font=("DejaVu Sans", 24, "bold"),

bg=self.BG, fg=self.ACCENT)

self.lbl_status.pack(pady=(0, 12))

self._update()

# ------------------------------------------------------------------

def _make_indicator(self, parent, text, font):

f = tk.Frame(parent, bg=self.BG)

f.pack(side="left", expand=True, padx=10, pady=8)

tk.Label(f, text=text, font=font, bg=self.BG,

fg=self.FG).pack()

lbl = tk.Label(f, text="0", font=("DejaVu Sans", 46, "bold"),

width=3, height=1, bg=self.OFF, fg=self.FG)

lbl.pack(pady=4)

return lbl

# ------------------------------------------------------------------

def _set_ind(self, label, value, on_color):

label.config(text=str(value),

bg=on_color if value else self.OFF)

# ------------------------------------------------------------------

def _update(self):

with self.ctrl.lock:

sw_up = self.ctrl.sw_up

sw_down = self.ctrl.sw_down

lim_top = self.ctrl.lim_top

lim_bot = self.ctrl.lim_bot

phase = list(self.ctrl.phase)

direction = self.ctrl.direction

top_trg = self.ctrl.top_triggered

bot_trg = self.ctrl.bot_triggered

self._set_ind(self.lbl_sw_up, sw_up, self.ON)

self._set_ind(self.lbl_sw_down, sw_down, self.ON)

# Концевики: подсвечиваем как "активные", когда сработали (стоп)

self._set_ind(self.lbl_lim_top, lim_top,

self.WARN if top_trg else self.ON)

self._set_ind(self.lbl_lim_bot, lim_bot,

self.WARN if bot_trg else self.ON)

for lbl, val in zip(self.coil_labels, phase):

lbl.config(text=str(val), bg=self.ON if val else self.OFF)

if direction > 0:

self.lbl_status.config(text="ДВИЖЕНИЕ ВВЕРХ", fg=self.ON)

elif direction < 0:

self.lbl_status.config(text="ДВИЖЕНИЕ ВНИЗ", fg=self.ON)

else:

self.lbl_status.config(text="СТОП (обмотки под напряжением)",

fg=self.ACCENT)

self.root.after(50, self._update)

# ---------------------------------------------------------------------------

# main

# ---------------------------------------------------------------------------

def main():

controller = CurtainController()

controller.start()

root = tk.Tk()

app = App(root, controller)

def on_close():

controller.stop()

time.sleep(0.05)

# Обесточиваем обмотки и освобождаем GPIO

for p in MOTOR_PINS:

GPIO.output(p, 0)

GPIO.cleanup()

root.destroy()

root.protocol("WM_DELETE_WINDOW", on_close)

root.mainloop()

if __name__ == "__main__":

main()

Копируем, вставляем.

Готово!

Есть, правда, один момент, который меня беспокоит.

Зачастую, слова, которые говорят люди обозначают не совсем то, чем кажутся. Более того, при буквальном прочтении оказывается, что слова эти заметно противоречат друг другу.

Например, когда, условно, говорится, что:
Raspberry лучше, чем Arduino потому, что
- Raspberry работает на частоте 1200МГц, против 16МГц у Arduino;
- У Raspberry есть встроенный Python;
- И потому на Raspberry можно вайбкодить
.

Проблема этого высказывания в том, что его первый пункт уничтожается вторым, а второй третьим.

Смотрите, вот так выглядит на Raspberry выполнение следующего кода:

flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм
flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм

Raspberry Pi, работающий на 1200МГц изо всех сил мигает пином с частотой 7,1кГц

А вот так выглядит выполнение аналогичного кода на Arduino:

flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм
flammmable,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,электроника,Raspberry PI,программирование,прикольные гаджеты,инкапсуляция и полиморфизм

Arduino, работающий на 16МГц не напрягаясь мигает пином с частотой 147,2кГц

Почему частота работы Raspberry на два порядка превосходит частоту работы Arduino,
а скорость выполнения команд на Raspberry примерно на два порядка уступает тому же Arduino?

Ну, потому, что Python интерпретируемый язык программирования. В отличие от компилируемого Си, на котором программируют Arduino. Из 1200МГц у Raspberry 1199,8МГц уходит на безостановочный парсинг питоновской текстовки.

Не-не, но, нет! Но подождите! Но позвольте! Есть же библиотеки! Всё самое тяжеловесное оптимизировано в них! Есть же периферия! Аппаратные таймеры, аппаратные SPI, UART и прочая! Есть же в концеконцов JITкомпиляция, заметно ускоряющая выполнение кода на Python можно услышать в ответ.

Я же отвечу, что оптимизированные библиотеки писаны далеко не на Python, аппаратные таймеры есть и у ATmega328P в Arduino, а JITкомпиляция даёт по яйцам переносимости кода, которой кичится сообщество питонщиков.
То есть возникает вопрос: какой смысл, какая выгода в интерпретируемом языке, когда вы делаете его компилируемым, а всё тяжёлое перекладываете на иноязычные библиотеки и аппаратную часть?

Тут произносится волшебное слово Интеграция. Типа, сложный и запутанный Си сложен и запутан. А вот человекочитаемый Python, а также его обширное, тёплое комьюнити и экосистема позволяют легко программировать даже новичку. В результате пускай и медленный, но очень дружелюбный Python как бы склеивает между собой все те куски нечеловеческой мощи, что могут крутиться на 1,2ГГц у Raspberry.

Данный довод можно было бы принять, если бы не ИИ.

Существует слабый и сильный сценарий использования ИИ для программирования.
В слабом сценарии ИИ по промту пишет код, а программист проводит ревью этого кода: читает получившееся глазками.
В сильном сценарии один ИИ по промту пишет код, другой ИИ по промту пишет тесты. Тесты должны проходить ревью, а код тесты. При этом код получается чёрным ящиком: Функционал выполняется? Ну и славно! Глазами мы всё равно ничего не сможем/не успеем к релизу разобрать в коде.

Но если в эпоху ИИ код становится чёрным ящиком какая разница на чём он написан? Если сама логика кода не предполагает человекочитаемость, какой плюс того, что сам код будет написан на дружелюбном к непрофессионалу языке? Если же разницы нет, возникает крамольный вопрос: насколько распространены ситуации, когда даже без учёта цены Arduino оказывается лучше, чем RaspberryPi?

------

Предыдущие статьи:
Управляем шторой через ПЛИС
Управляем шторой через LabVIEW
Управляем шторой через Arduino


Источник: joyreactor.cc
К списку статей
Опубликовано: 12.10.2026 20:00:22
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Flammmable

Geek

Электроника

Raspberry pi

Программирование

Прикольные гаджеты

Инкапсуляция и полиморфизм

Последние комментарии

  • Имя: Тимофей
    29.05.2026 | 13:50
    Оригинальное замещение весла! Плюс фигуристая дама выглядит весьма притягательно. Подробнее..
  • Имя: sa<sa>as"<sa
    23.04.2026 | 02:16
  • Имя: Vorgal
    17.12.2025 | 12:06
    Современная версия классической скульптуры "Девушка с веслом". «Девушка с глушителем». - это новый, концептуальный арт-объект в городе Москве. Он появился в начале 2000-х. Композиция предст Подробнее..
  • Имя: Vorgal
    17.12.2025 | 12:06
    Современная версия классической скульптуры "Девушка с веслом". «Девушка с глушителем». - это новый, концептуальный арт-объект в городе Москве. Он появился в начале 2000-х. Композиция предст Подробнее..
  • Имя: Rino
    23.11.2023 | 19:26
  • Имя: Домовой
    12.10.2023 | 12:32
  • Имя: Домовой
    12.10.2023 | 12:26
  • Имя: Стас
    03.07.2023 | 21:34
  • Имя: Васян
    02.08.2022 | 18:40
    Кончил))) Подробнее..
© 2006-2026, ebanoeit.ru