електронний конструктор для малинки

| щоденник, комп'ютери, linux, raspberry pi, поновлюється

[поновлюється] колись для розваги я запускав на «малинці» (raspberry pi першого покоління) сервер minecraft… досліди показали, що воно працює, але практичного сенсу не має: гальмує, ще й споживає картки sd майже як витратний матеріял. тож minecraft «переїхав» в контейнер на потужнішій машині, а «малинка» довго лежала без застосування. аж днями за підказки товариша я придбав електронний «конструктор» freenove ultimate starter kit for raspberry pi. тут буде журнал розваг із цим набором.

зображення: електронний набір freenove ultimate starter kit for raspberry pi

зміст

«бонуси» — відступи від теми, коли я щось випробовував, не пов’язане безпосередньо з поточним проєктом з набору; вони розкидані по тексту, але в змісті я зібрав їх окремим розділом.

tl;dr

це вже дуууже довгий допис, а я додаю цей розділ, щойно пройшовши четвертий експеримент, тож попереду ще більше тексту, але вже зараз можу наголосити: набір вартий своїх грошей! обов’язково придбайте, якщо вдома вже лежить без діла raspberry pi, а ви хочете навчитися програмувати на python’і чи c/c++ і отримати базові знання з електроніки.

якщо зайвої «малинки» нема, я би поглянув на інші набори ultimate starter kit від freenove з контролерами в комплекті, які підтримують micropython; втім, сам ще не пробував.

підготовка

raspberry pi

виявилося, що дві старі картки sd (по 8 гб) з системою для «малинки» чомусь відмовлялися завантажуватися, але на третій знайшовся noobs, і з нього розгорнулася raspbian. потім я чимало часу витратив, щоби змусити працювати wifi: знайшов у себе й випробував два usb-модуля 802.11n, але жоден не захотів працювати: один «відвалювався» при кожній спробі активувати wifi, другий наче активувався, «чіплявся» до точки доступу й навіть отримував ip, але далі — глухо, нічого не пінгується. можливо, колись згодом ще спробую, але наразі — просто підключив мережевим кабелем до комутатора в стійці.

базові налаштування за допомогою raspi-config: поміняти пароль профіля pi, включити сервер ssh (опційно ще й vnc), автозавантаження в текстову консоль і автологін pi.

наступна проблема: в raspbian напаковано забагато як малинки першого покоління — аж так, що на картці 8 гб бракує місця, щоби поновити (!) щойно встановлену систему. видалення абсолютно непотрібних на такій малопотужній «іграшці» wolfram-engine та libreoffice допомогло:

> sudo apt udpate
> sudo apt autoremove wolfram-engine libreoffice
> sudo apt upgrade

втім, apt не може завершити завантаження через помилки з сертифікатом дзеркала mirrors.switch.ca:

E: Failed to fetch https://mirrors.switch.ca/raspbian/raspbian/pool/main/d/dpkg/dpkg_1.18.26_armhf.deb  server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none

потрібна поправка до /etc/apt/sources.list (тут stretch, а не buster, бо старий noobs на флешці мав попередню версію raspbian); після цього sudo apt upgrade працює:

> sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup_$(date +'%Y-%m-%d_%H%M%S')
> sudo vim /etc/apt/sources.list
# ----->8-----
deb https://raspbian.mirror.globo.tech/raspbian/ stretch main contrib non-free rpi
# -----8<-----
> sudo apt upgrade

далі — поновлення прошивки (в моєму випадку — до версії 5.15):

> sudo rpi-update
…
> sudo systemctl reboot

якщо все пройшло добре, час поновити систему до buster‘а:

> sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup_$(date +'%Y-%m-%d_%H%M%S')
> sudo sed -i 's/ stretch / buster /g' /etc/apt/sources.list
> sudo apt dist-upgrade

передостаннє — доставити (і, опційно, налаштувати автостарт сесії) gnu screen, тому що працювати з віддаленою linux-машиною по ssh без screen’а — це неправильно:

> sudo apt install screen

наостанок — не забути занотувати mac-адресу мережевого інтерфейсу:

> ip link

навіщо? бо я не фіксую ip для портативних пристроїв, а малинка — портативна; ну, принаймні я планую використовувати її не лише в домашній мережі (без монітора, з живленням від павербанка, підключену кабелем до лептопа), і тоді вона швидко знаходиться «обзвоном» всіх адрес (замість справіжнього mac’а тут умовне 12:23:45…):

# для локальної мережі 192.168.0.x/24 з сервером dhcp: 
> for i in {1..254}; do ping -c1 -W0.2 -q 192.168.0.$i 2>&1 >/dev/null && echo -n ":" || echo -n "."; done; echo; ip neigh | grep 12:34:56:78:90:ab
#
# для link local 169.254.x.x/16 без сервера dhcp (займає багато часу!):
> for i in {0..255}; do for j in {1..254}; do ping -c1 -W0.2 -q 169.254.$i.$j 2>&1 >/dev/null && echo -n ":" || echo -n "."; done; done; echo; ip neigh | grep 12:34:56:78:90:ab

на щастя, більшість серверів dhcp і алгоритм вибору адреси link local (використовується, коли немає dhcp) працюють так, щоби призначати ту саму адресу ip при повторних перепідключеннях пристрою з тією самою mac-адресою; тож пошук малинки доведеться, ймовірно, робити лише раз (для кожної локальної мережі).

starter kit

далі інструкція до набору freenove каже затягти й компілювати бібліотеку wiringpi для роботи з конектором gpio на малинці:

> cd ~
> git clone https://github.com/WiringPi/WiringPi
> cd WiringPi
> ./build
…
> gpio -v

потрібна також копія репозиторію freenove для набору ultimate starter kit for raspberry pi, буде в теці ~/Projects/Starter:

mkdir -p ~/Projects/Freenove
cd ~/Projects/Freenove
git clone --depth 1 https://github.com/freenove/Freenove_Ultimate_Starter_Kit_for_Raspberry_Pi
mv ~/Projects/Freenove_Ultimate_Starter_Kit_for_Raspberry_Pi ~/Projects/Starter

для початку мене цікавитиме python (третій!), тож аби не плутатися у версіях…

# python -> python3
> sudo rm /usr/bin/python 
> sudo ln -s /usr/bin/python3 /usr/bin/python
> python --version
Python 3.5.3
# pip -> pip3
> sudo rm /usr/bin/pip
> sudo ln -s /usr/bin/pip3 /usr/bin/pip
> pip --version
pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.5)

все готово для розваг з конструктором?

зображення: «малинку» налаштовано для розваг з набором freenove

розводка gpio

насправді, не зовсім: перше покоління raspberry pi має коротший конектор gpio, аніж наступні, лише 26 роз’ємів (замість 40 в наступних моделях). це згадано лише побігом в інструкції (схема на стор. 44). не проблема для проєктів, в котрих не потрібно більше 7 виводів gpio, але подовжувач і шлейф з набору freenove мені не підходять — доведеться обережненько підключати кабелями напряму контакти gpio до макетної плати, орієнтуючись на схему розпіновки raspberry pi 1 або на вивід gpio:

> gpio readall
 +-----+-----+---------+------+---+-Model B1-+---+------+---------+-----+-----+
 | BCM | wPi |   Name  | Mode | V | Physical | V | Mode | Name    | wPi | BCM |
 +-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
 |     |     |    3.3v |      |   |  1 || 2  |   |      | 5v      |     |     |
 |   2 |   8 |   SDA.1 |   IN | 1 |  3 || 4  |   |      | 5v      |     |     |
 |   3 |   9 |   SCL.1 |   IN | 1 |  5 || 6  |   |      | 0v      |     |     |
 |   4 |   7 | GPIO. 7 |   IN | 1 |  7 || 8  | 1 | ALT0 | TxD     | 15  | 14  |
 |     |     |      0v |      |   |  9 || 10 | 1 | ALT0 | RxD     | 16  | 15  |
 |  17 |   0 | GPIO. 0 |   IN | 0 | 11 || 12 | 0 | IN   | GPIO. 1 | 1   | 18  |
 |  27 |   2 | GPIO. 2 |   IN | 0 | 13 || 14 |   |      | 0v      |     |     |
 |  22 |   3 | GPIO. 3 |   IN | 0 | 15 || 16 | 0 | IN   | GPIO. 4 | 4   | 23  |
 |     |     |    3.3v |      |   | 17 || 18 | 0 | IN   | GPIO. 5 | 5   | 24  |
 |  10 |  12 |    MOSI |   IN | 0 | 19 || 20 |   |      | 0v      |     |     |
 |   9 |  13 |    MISO |   IN | 0 | 21 || 22 | 0 | IN   | GPIO. 6 | 6   | 25  |
 |  11 |  14 |    SCLK |   IN | 0 | 23 || 24 | 1 | IN   | CE0     | 10  | 8   |
 |     |     |      0v |      |   | 25 || 26 | 1 | IN   | CE1     | 11  | 7   |
 +-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
 | BCM | wPi |   Name  | Mode | V | Physical | V | Mode | Name    | wPi | BCM |
 +-----+-----+---------+------+---+-Model B1-+---+------+---------+-----+-----+

цей вивід зручний тим, що показує чотири різних версії неймінгу виводів, їхні режими й стан. якщо потрібна більш графічна інформація зі схемою розташування виводів на платі малинки (просто в терміналі):

> pinout

зображення: короткий (20 контактів) роз'єм на малинці, довгий (40 контактів) на подовжувачі й шлейфі

(повернутися до змісту)

проєкти з набору

1.1 миготливий світлодіод

нічого простіше не буває: червоний світлодіод і резистор 220 ом послідовно між gpio.0 (контакт 11) та землею (контакт 25) — про суті, просто перевірка, чи все працює (і вступ до використання конструктора для таких новачків, як я):

python ~/Projects/Freenove/Starter/Code/Python_Code/01.1.1_Blink/Blink.py

і… нічого. світлодіод не світиться. починається найцікавіше! візуально схема в порядку; для початку — відключу її від малинки, відкрию код і збільшу затримку між зміною станів gpio.0 до 5 секунд, і запущу ще раз:

> vi ~/Projects/Freenove/Starter/Code/Python_Code/01.1.1_Blink/Blink.py
# ----->8-----
def loop():
    while True:
        …
        time.sleep(5)   # чекати 5 сек
        …
        time.sleep(5)
# -----8<----- 
python ~/Projects/Freenove/Starter/Code/Python_Code/01.1.1_Blink/Blink.py

в сусідньому терміналі — ще одна сесія ssh до малинки, і поглянемо, що робиться з gpio.0 на контакті 11: стан має змінитися на OUT, а рівень що п’ять секунд перемикатися між 0 та 1:

> watch gpio readall

перемикається, як годинник. мультиметром перевіряю, чи справді є напруга на контакті (ніколи не лізти щупами просто до роз’єма! лише через кабелі) — також все гаразд. отже, лишається схема… або монтажна плата? знову підключаю схему й міряю напругу на краях пари світлодіод-резистор — напруга на місці. можливо, пробитий світлодіод? бракований резистор? коротше кажучи, ніт: всього лишень переплутав полярність підключення діода, рукалице!

гаразд, діод світиться, урок засвоєно, можна рухатися далі.

зображення: перевірка проблемної схеми: на вольтметрі 3 вольти між gpio.0 та землею

перший бонус: живлення від usb

поки заряджається павербанк, від якого працює малинка, я вирішив скористатися вже зібраною схемою (додав кнопочку), щоби випробувати перетворювач живлення, що оце буквально вчора прийшов за замовленням на amazon’і (12$ за два, плюс податки й доставка): бере 5 в від usb чи mini usb (або 3,5..12 в через контакні майдачики), видає 1,2..24 в на клемках — може стати в пригоді, щоби живити якісь маленькі проєкти з набору, де потрібні 9 в, без необхідності шукати «крону» чи знімати з роботанка коробку на 6 батарейок (якщо, звісно, струму вистачить).

зображення: перетворювач живлення usb на 1,2..24 в

(повернутися до змісту)

2.1 кнопка й світлодіод

наступна проста схема: світлодіод на gpio.0 (контакт 11), кнопка на gpio.1 (к.12); тиснеш кнопку — світиться. з другого разу схема зібралася й запрацювала. але програма в тому вигляді, як її пропонує репозиторій freenove, надто проста і спамить повідомленнями в консоль (без затримки!), потрібна модифікація:

  • зробити перемикання між режимами: вкл > миготіння > викл;
  • виводити повідомлення лише на зміну режиму.
#!/usr/bin/env python3
########################################################################
# Filename    : ButtonLED_v2.py (based on ButtonLED.py)
# Description : Control LED modes (ON > blinking OFF) with a button
# Author      : tivasyk (base on code from www.freenove.com)
# Modified    : 2022-12-13
########################################################################
import RPi.GPIO as GPIO
import time

ledPin = 11       # pin controlling the LED
buttonPin = 12    # pin controlled by the button
ledMode = 1       # initial mode = ON
ledState = False  # initial LED state = OFF 
tick = 0.1        # tick duration = 0.1 sec
buttonWait = 5    # number of ticks to wait after a button press
blinkWait = 2     # number of ticks between blinking state change

def setup():
    GPIO.setmode(GPIO.BOARD)       # use PHYSICAL GPIO Numbering
    GPIO.setup(ledPin, GPIO.OUT)   # set ledPin to OUTPUT mode
    GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)    # set buttonPin to PULL UP INPUT mode

def loop():
    global ledMode, ledState
    buttonWaiting = 0              # initially do not ignore button presses
    blinkWaiting = 0               # initially set blink wait to zero

    while True:
        if GPIO.input(buttonPin)==GPIO.LOW:     # if button is pressed
            if buttonWaiting==0:
                if ledMode<2:
                    ledMode+=1
                else:
                    ledMode=0
                buttonWaiting = buttonWait
                print ("LED mode: %s" % ledMode)

        if ledMode==0:               # turn LED off
            GPIO.output(ledPin,GPIO.LOW)
        elif ledMode==1:             # turn LED on
            GPIO.output(ledPin,GPIO.HIGH)
        else:                     # blinking mode!
            if blinkWaiting > 0:
                blinkWaiting-=1
            else:
                if ledState:
                    GPIO.output(ledPin,GPIO.LOW)
                else:
                    GPIO.output(ledPin,GPIO.HIGH)
                ledState = not ledState
                blinkWaiting = blinkWait
        if buttonWaiting > 0:
            buttonWaiting-=1
        time.sleep(tick)

def destroy():
    GPIO.output(ledPin, GPIO.LOW)     # turn off led
    GPIO.cleanup()                    # Release GPIO resource

if __name__ == '__main__':     # Program entrance
    print ('Program is starting...')
    setup()
    try:
        loop()
    except KeyboardInterrupt:  # Press ctrl-c to end the program.
        destroy()

цікаво було приблизно заміряти (ps) використання процесора малинки оригінальним кодом freenove (ButtonLED.py ~30%) та модифікованим (ButtonLED_v2.py ~1%), запустивши щось таке в сусідній сесії ssh:

watch "ps -C python -o 'pid,etime,%cpu,%mem,rss,args'"

чи справді така простенька тестова програмка потребувала стільки ресурсу, чи можна було одразу додати 0,1 сек затримки в код? питання риторичне. реальне використання процесора нижче, але ps досить грубо усереднює показник за ввесь час роботи процесу, включно з досить інтенсивним запуском.

зображення: кнопка і світлодіод

(повернутися до змісту)

2.2 настільна лампа

звісно ж, нема ніякої ложки лампи, а є та сама схема (див. 2.1), але програма робить з кнопки stateful перемикач вкл/викл. щось подібне на мою подифікацію попереднього коду, але з використанням фукнції GPIO.add_event_detect бібліотеки gpio. оригінальний код Tablelamp.py теж не має затримки в циклі (cpu ~90%), але її легко додати (cpu ~3%). мені ж цікавіше переписати свою версію ButtonLED_v2.py з використанням add_event_detect:

#!/usr/bin/env python3
########################################################################
# Filename    : ButtonLED_v3.py (based on ButtonLED.py and Tablelamp.py)
# Description : Control LED modes (ON > blinking OFF) with a button
# Author      : tivasyk (base on code from www.freenove.com)
# Modified    : 2022-12-14
########################################################################
import RPi.GPIO as GPIO
import time

ledPin = 11       # pin controlling the LED
buttonPin = 12    # pin controlled by the button
ledMode = 1       # початковий режим (вкл)
ledState = False  # початковий стан світлодіода
bounceTime = 300  # інтервал очікування на стан кнопки, мс
tick = 0.1        # тривалість такту для циклу = 0.1 с 
blinkWait = 2     # тривалість стану для миготіння (в тактах)

def setup():
    GPIO.setmode(GPIO.BOARD)       # use PHYSICAL GPIO Numbering
    GPIO.setup(ledPin, GPIO.OUT)   # set ledPin to OUTPUT mode
    GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)    # set buttonPin to PULL UP INPUT mode

def buttonEvent(channel):          # оброблювач натисків кнопки
    global ledMode
    if ledMode < 2:                # зміна режимів: 0 > 1 > 2 > 0
        ledMode += 1
    else:
        ledMode = 0
    print ("LED mode: %s" % ledMode)

def loop():
    global ledState
    blinkWaiting = 0               # initially set blink wait to zero

    # реєструємо обробник натискань кнопки
    GPIO.add_event_detect(buttonPin, GPIO.FALLING, callback = buttonEvent, bouncetime = bounceTime)

    while True:
        if ledMode==0:               # режим 0 = викл
            GPIO.output(ledPin,GPIO.LOW)
        elif ledMode==1:             # режим 1 = вкл
            GPIO.output(ledPin,GPIO.HIGH)
        else:                        # режим 2 = мигтіння
            if blinkWaiting > 0:
                blinkWaiting-=1
            else:
                if ledState:
                    GPIO.output(ledPin,GPIO.LOW)
                else:
                    GPIO.output(ledPin,GPIO.HIGH)
                ledState = not ledState
                blinkWaiting = blinkWait
        time.sleep(tick)

def destroy():
    GPIO.output(ledPin, GPIO.LOW)     # turn off led
    GPIO.cleanup()                    # Release GPIO resource

if __name__ == '__main__':     # Program entrance
    print ('Program is starting...')
    setup()
    try:
        loop()
    except KeyboardInterrupt:  # Press ctrl-c to end the program.
        destroy()

в попередній версії (ButtonLED_v2.py) я використовував buttonWaiting, аби ігнорувати надто швидкі повторні натискання кнопки (можливо, було зайве, з досить довгим тактом 0,1 с), — тут можна позбутися цієї «обв’язки», бо bouncetime виконує схожу функцію, але краще.

(повернутися до змісту)

3.1 світлодіодна планка

по світлодіодній планці (десять діодів) туди-сюди бігає світлова смужка; використано десять виводів gpio (на моїй планці один зі світлодіодів, схоже, бракований: не запалюється). особливість схеми — низький рівень на gpio запалює світлодіод, а не навпаки. все це не дуже цікаво, тож використаймо вже зібрану схему, щоби написати щось цікавіше: простенький фізичний аналог gauge, мого невеличкого проєктика з баштану, для відображення навантаження процесора:

  • етап 1: функція gauge(level) для рівнів 0..10;
  • етап 2: навантаження процесора;
  • етап 3: різні покращення (нелінійність, плавність).

перший етап — GaugeTest.py:

#!/usr/bin/env python3
########################################################################
# Filename    : GaugeTest.py
# Description : Use LEDBar (10 LED) to show some level 0-10 
# Author      : tivasyk (based on LightWave.py code from www.freenove.com)
# modification: 2022-12-15
########################################################################
import RPi.GPIO as GPIO
import time

ledPins = [11, 12, 13, 15, 16, 18, 22, 3, 5, 24]
tick = 0.1

def setup():
    GPIO.setmode(GPIO.BOARD)        # use PHYSICAL GPIO Numbering
    GPIO.setup(ledPins, GPIO.OUT)   # set all ledPins to OUTPUT mode
    GPIO.output(ledPins, GPIO.HIGH) # make all ledPins output HIGH level, turn off all led

def gauge(level):          # запалює світлодіодну планку на вказану довжину (0-10)
    if level < 0:          # перевірка потрапляння в діапазон (0-10)
        level = 0
    elif level > 10:
        level = 10
    for led in range(1, len(ledPins) + 1):          # перебір світлодіодів (1-10)
        if level >= led:                            # діоди до рівня — запалити
            GPIO.output(ledPins[led-1], GPIO.LOW)   # LOW = діод вкл (див. схему!)
        else:                                       # діоди вище рівня — загасити
            GPIO.output(ledPins[led-1], GPIO.HIGH)  # HIGH = діод викл

def loop():
    while True:
        print("Going up >>>")
        for level in range(0,11):         # вгору 0-10 (не питайте)
            gauge(level)
            time.sleep(tick)
        print("Going down <<<")
        for level in range(10,-1,-1):     # вниз 10-0 (тим більше не питайте)
            gauge(level)
            time.sleep(tick)
        time.sleep(tick * 3)

def destroy():
    GPIO.cleanup()                     # Release all GPIO

if __name__ == '__main__':     # Program entrance
    print ('Program is starting...')
    setup()
    try:
        loop()
    except KeyboardInterrupt:  # Press ctrl-c to end the program.
        destroy()

коли це працює, додати вивід навантаження процесора за допомогою бібліотеки psutil — простіше простого; по-перше, знадобиться сама бібліотека (встановлення на малинці першого покоління займає доооовго):

> pip install psutil

для другого етапу достатньо імпортувати бібліотеку, спростити loop() і трохи збільшити tick:

import psutil
…
tick = 0.3
… 
def loop():
    while True:
        gauge(int(psutil.cpu_percent() / 10))
        time.sleep(tick)

для перевірки можна навантажити чимось малинку в сусідній сесії ssh:

> find / | xargs file

але… проблема з лінійним відображенням навантаження процесора в тому (особливо на малинці), що воно або дуже низьке (одиниці відсотків), або впирається у 100%, і спостерігати за світлодіодною панелькою, котра то повністю загоряється, то повністю гасне, нецікаво. але можна пожертвувати точністю на користь видовищності й зробити відображення трішки нелінійним, замінивши ділення квадратним коренем — тоді діапазон 1..10% розтягнеться на 3 світлодіода, наступні 11..50% — відображатимуться 4-ма світлодіодами посередині, а 51..100% стиснуться до 3-х верхніх діодів:

import psutil
import math
…
def loop():
    while True:
        gauge(int(math.sqrt(psutil.cpu_percent())))
        time.sleep(tick)

тепер навіть невеликі навантаження процесора відображаються кількома «паличками». мені подобається, але… можна покращити плавність відображення, ще трішки пожертвувавши точністю, льоль! додавання короткого стеку fifo для отриманих від psutil.cpu_percent() значень, і усереднення по стеку для відображення на планці змусять індикатор зростати-спадати замість стрибати; плавність (і похибка) залежить від довжини стеку:

cpuStack = [0, 0, 0]
…
def cpuAverage():           # віддає усереднений % cpu зі стеку
    global cpuStack
    cpuStack.insert(0, psutil.cpu_percent())    # додає новий % cpu
    cpuStack.pop()                              # викидає найстарший елемент
    return sum(cpuStack)/len(cpuStack)
…
def loop():
    while True:
        gauge(int(math.sqrt(cpuAverage())))

тепер мені все подобається, окрім того, що для такої примітивної іграшки довелося використати аж 10 виводів gpio; я певен, що далі в наборі будуть цікавіші варіанти індикаторів.

зображення: світлодіодна планка

(повернутися до змісту)

4.1 імпульсна модуляція для світлодіода

шкода, але розбираю індикатор навантаження процесора — повертаємося до найпростішого: один діод, один резистор… і програмна реалізація (бо python не вміє ініціалізувати апаратну) цифрової широтно-імпульсної модуляції для керування яскравістю світлодіода. нуууудно! пропоную таке:

  • додати на схему кнопку (див. 2.2) для перемикання режимів;
  • початок кожного режиму позначається короткими проблисками:
    • один проблиск: навантаження процесора (див. 3.1);
    • два проблиски: i/o накопичувача;
    • три проблиски: мережевий rx/tx;
  • якщо можливо, яскравістю світлодіода показати % навантаження чи інтенсивність.

найдовше довелося мудрувати, чи можна якось просто перетворити статистику psutil для накопичувача та мережевого інтерфейсу на відсоток… так і не вимудрував, — тому в цих двох режимах блимання просто показує активність:

#!/usr/bin/env python3
########################################################################
# Filename    : BreathingLED_v2.py
# Description : Breathing LED for system stats (% CPU, disk I/O, net rx/tx)
# Author      : tivasyk (based on the code from www.freenove.com)
# modification: 2022-12-15
########################################################################
import RPi.GPIO as GPIO
import time
import psutil
import math

ledPin = 11             # світлодіод на 11 виводі
buttonPin = 12          # кнопка на 12 виводі
ledMode = 1             # початковий режим (1 %cpu, 2 disk i/0, 3 net rx/tx)
ledBlock = False        # блокує вивід статистики для індикації режиму
bounceTime = 300        # інтервал очікування на стан кнопки, мс
blinkDuration = 0.2     # тривалість проблиску
rapidBlinks = 1         # кількість швидких блимів для активності на такт
tick = 0.3              # такт головного циклу
dataStack = [0, 0, 0]   # стек fifo для усереднення статистики

def setup():
    global led                     # об'єкт для програмної шім
    GPIO.setmode(GPIO.BOARD)       # use PHYSICAL GPIO Numbering
    GPIO.setup(ledPin, GPIO.OUT)   # set ledPin to OUTPUT mode
    GPIO.output(ledPin, GPIO.LOW)  # make ledPin output LOW level to turn off LED
    led = GPIO.PWM(ledPin, 100)    # set PWM Frequence to 500Hz
    led.start(0)                   # set initial Duty Cycle to 0
    GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

def buttonEvent(channel):               # оброблювач натисків кнопки
    global ledMode, ledBlock, dataStack
    ledBlock = True                     # забороняємо вивід статистики
    if ledMode < 3:                     # зміна режимів: 1 > 2 > 3 > 1
        ledMode += 1
    else:
        ledMode = 1
    dataStack = [0]*len(dataStack)      # обнулити стек fifo
    led.ChangeDutyCycle(0)              # вимкнути світлодіод
    time.sleep(blinkDuration)           # …щоби показати початок зміни режиму
    for i in range(0, ledMode):         # кількість проблисків
        led.ChangeDutyCycle(100)        # повна яскравість
        time.sleep(blinkDuration)       # …на тривалість проблиску
        led.ChangeDutyCycle(0)          # і така сама пауза
        time.sleep(blinkDuration)
    time.sleep(2 * blinkDuration)       # довша пауза для «відбиття»
    ledBlock = False                    # дозволяємо вивід статистики на діоді

def loop():
    global led
    # реєструємо обробник натискань
    GPIO.add_event_detect(buttonPin, GPIO.FALLING, callback = buttonEvent, bouncetime = bounceTime)
    while True:
       if not ledBlock:                 # якщо не зайнято індикацією режиму
           if ledMode == 1:             # режим 1: % cpu
               dataStack.insert(0, psutil.cpu_percent())
               dataStack.pop()
               led.ChangeDutyCycle(sum(dataStack)/len(dataStack))
               time.sleep(tick)
           else:
               if ledMode == 2:         # режим 2: disk i/o
                   dataStack.insert(0, psutil.disk_io_counters(nowrap = True).busy_time)
               else:                    # режим 3: net rx/tx
                   dataStack.insert(0, psutil.net_io_counters(nowrap = True).bytes_sent + psutil.net_io_counters(nowrap = True).bytes_recv)
               if dataStack[0] > dataStack[1]:          # є активність
                   for i in range (0, rapidBlinks):     # швидке блимання впродовж такту
                       led.ChangeDutyCycle(100.0)
                       time.sleep(tick / (rapidBlinks * 2))
                       led.ChangeDutyCycle(0.0)
                       time.sleep(tick / (rapidBlinks * 2))
               dataStack.pop()

def destroy():
    led.stop() # stop PWM
    GPIO.cleanup() # Release all GPIO

if __name__ == '__main__':     # Program entrance
    print ('Program is starting ... ')
    setup()
    try:
        loop()
    except KeyboardInterrupt:  # Press ctrl-c to end the program.
        destroy()

одна явна проблема цього коду, — в режимах 2 (i/o накопичувача) та 3 (rx/tx мережі) він споживає 60-90% процесорного часу на малинці, навіть якщо нема активності; тобто проблема десь в оцім фрагменті, але не можу побачити причини (запити до psutil перевірив — не воно):

…
else:
   if ledMode == 2:         # режим 2: disk i/o
       dataStack.insert(0, psutil.disk_io_counters(nowrap = True).busy_time)
   else:                    # режим 3: net rx/tx
       dataStack.insert(0, psutil.net_io_counters(nowrap = True).bytes_sent + psutil.net_io_counters(nowrap = True).bytes_recv)
   if dataStack[0] > dataStack[1]:
…

інша, можливо значно важливіша проблема — недостатня «ізоляція» асинхронних викликів оброблювача кнопки: адже ці виклики припадають на довільний момент часу, коли виконується будь-яка інша команда в loop(). я від початку це передбачав і додав ledBlock для блокування виводу даних на діод, поки він відображає проблисками вибраний режим… але цього мало — обидві функції напряму змінюють чергу dataStack з даними попередніх прогонів, і це призводить, здається, до періодичних глюків. тут треба було би покопирсатися, але наразі досить для одного експерименту.

зображення: схема для трирежимного індикатора навантаження з одним діодом

(повернутися до змісту)

другий бонус: швидкий тест на bash

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

> gpio -1 mode 12 out
> gpio -1 toggle 12

перша команда (mode) переключає вивід 12 в режим виводу (початково всі gpio працюють на читання), друга (toggle) перекидає напругу (3,3 > 0 > 3,3 в). якщо це «загорнути» в цикл, буде код першої задачки з набору, одним рядком… що наводить на думку: можна буде згодом для розваги переписати найцікавіші проєкти на bash’ику:

> PIN=12; gpio -1 mode $PIN out; while :; do gpio -1 toggle $PIN; sleep 1s; done

(повернутися до змісту)

5.1 багатоколірний світлодіод

проста схема: один світлодіод rgb через три резистори «підвішений» між трьома керуючими виводами gpio і живленням. програма в комплекті щосекунди переключає колір на випадковий, генеруючи для кожного каналу rgb коефіцієнт заповнення 0..100%.

найперше кинулося у вічі, що діод почина мерехтіти, коли хоча би на одному з каналів rgb яскравість надто низька. я спробував виключити діапазон 0..30% з генерації:

r=random.randint(3,10)*10       # випадковий % в діапазоні (30,100) з кроком 10

…і стало гірше. чому? бо я забув, що це схема зі спільним анодом на лінії живлення 3,3 в (а не на землі) — тобто якщо на контрольний вивід подати високий рівень (ті самі 3,3 в), струм через цей канал не тече, відповідний колір гасне; те саме з модуляцією: що вищий коефіцієнт подаємо, то сильніше гасне канал, а не навпаки, і тому мерехтіти починає на на 0..30%, а на 70..100%. тому треба так:

r=random.randint(0,7)*10        # випадковий % в діапазоні (0,70) з кроком 10

я прихильник написання логічного, самодокументованого коду, і спрощення логіки ціною складнішої математики: на мою думку, в основному коді краще оперувати % яскравості (0-100%), а в коефіцієнт заповнення конвертувати його лише аж там, де треба подати на діок, там же врахувати мінімальне заповнення без мерехтіння («стиснувши» 100% в доступний діапазон 31..100%), і можливість використовувати схеми зі спільним анодом чи катодом. якось так:

commonAnode = True                          # схема зі спільним анодом?
minDutyCycle = 30                           # мінімальна прогальність без мерехтіння
…
def setColor(r_val,g_val,b_val):            # прогальність для rgb
    if commonAnode:                         # спільний анод! 0% - повна яскравість, 100% - викл.
        pwmRed.ChangeDutyCycle(100-minDutyCycle-((100-minDutyCycle)/100)*r_val)     
        pwmGreen.ChangeDutyCycle(100-minDutyCycle-((100-minDutyCycle)/100)*g_val) 
        pwmBlue.ChangeDutyCycle(100-minDutyCycle-((100-minDutyCycle)/100)*b_val) 
    else:                                   # спільний катод! 0% - викл, 100% - повна яскравість
        pwmRed.ChangeDutyCycle(minDutyCycle + ((100-minDutyCycle)/100)*r_val)     
        pwmGreen.ChangeDutyCycle(gminDutyCycle + ((100-minDutyCycle)/100)*_val) 
        pwmBlue.ChangeDutyCycle(bminDutyCycle + ((100-minDutyCycle)/100)*_val) 

фукнція ускладнилась, — але зате основна логіка програми, що використовує таку функцію, спрощується. втім, для простого експерименту на монтажній платі то все аж занадто мудро.

друга цікава проблема: програма генерує випадкові комбінації так, що діод то палає, то жевріє, а часом зовсім гасне, — хотілося би її модифікувати так, щоби колір діода мінявся, але не яскравість. як? генерувати тріади коефіцієнтів заповнення так, щоби їх сума завжди була однакова. виявилося, що це не така вже й тривіальна задача… особливо якщо накласти на неї додаткову умову, що коефіцієнт для кожного каналу окремо не може бути вищий за 100%.

найпростіше, що я швидко вигадав: довільно генерувати три числа (r′, g′, b′), знаходити суму (∑′(r′, g′, b′)), розраховувати коефіцієнт масштабування (k = ∑/∑′) і масштабувати (r = kr′…). на жаль, я не подужав поєднати це з обмеженням мінімального заповнення для уникнення мерехтіння, — але якщо генерувати вихідну тріаду в вузькому діапазоні (0,2) — проблема відпадає за рахунок масштабування!

while True:
    rt = random.randint(0,2)
    gt = random.randint(0,2)
    bt = random.randint(0,2)
    sumrgbt = rt + gt + bt
    if sumrgbt > 0:
        break
k = int(100/sumrgbt)
r = rt * k
g = gt * k
b = bt * k    

цикл while потрібен, щоби уникнути ділення на нуль, а виглядає він так гидко й потребує додаткового if тому, що пітоністи колись вирішили обійтися без класичного do … until.

наостанок: мені сподобалося, коли на схемі є кнопка, і коли вона робить «красіво». а на старті варто додати тест трьох основних кольорів.

отже, план розваг:

  • різні кольори, але постійна яскравість;
  • тест основних кольорів на початку;
  • перемикання кольорів кнопкою.

все вже зроблено, трохи спростив:

#!/usr/bin/env python3
########################################################################
# Filename    : ColorfulLED_v2.py
# Description : Random color change ColorfulLED on button press
# Author      : tivasyk (based on the code from www.freenove.com)
# modification: 2022-12-16
########################################################################
import RPi.GPIO as GPIO
import time
import random

pins = [11, 13, 15]                     # діод на виводах R:11,G:13,B:15
buttonPin = 26                          # кнопка на виводі 26
bounceTime = 300                        # затримка на кнопці
tick = 1

brightness = 100                        # фіксована загальна яскравість (0..100)

def setup():
    global pwmRed,pwmGreen,pwmBlue
    GPIO.setmode(GPIO.BOARD)            # use PHYSICAL GPIO Numbering
    GPIO.setup(pins, GPIO.OUT)          # set RGBLED pins to OUTPUT mode
    GPIO.output(pins, GPIO.HIGH)        # make RGBLED pins output HIGH level
    pwmRed = GPIO.PWM(pins[0], 100)     # set PWM Frequence to 100Hz
    pwmGreen = GPIO.PWM(pins[1], 100)
    pwmBlue = GPIO.PWM(pins[2], 100)
    pwmRed.start(0)                     # set initial Duty Cycle to 0
    pwmGreen.start(0)
    pwmBlue.start(0)
    GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    test()

def buttonEvent(channel):               # обробник натискань
    while True:
        r = random.randint(0,2)
        g = random.randint(0,2)
        b = random.randint(0,2)
        sumrgb = r + g + b
        if sumrgb > 0:
            break
    k = int(brightness / sumrgb)
    setColor(r*k,g*k,b*k)               # засвітити новий колір
    print ('r=%d, g=%d, b=%d ' %(r*k,g*k,b*k))

def setColor(r_val,g_val,b_val):            # заповнення pwm для rgb
    pwmRed.ChangeDutyCycle(100 - r_val)
    pwmGreen.ChangeDutyCycle(100 - g_val)
    pwmBlue.ChangeDutyCycle(100 - b_val)

def test():
    print("Testing colors: Red,", end='', flush=True)
    setColor(100,0,0)
    time.sleep(0.5)
    print(" Green,", end='', flush=True)
    setColor(0,100,0)
    time.sleep(0.5)
    print(" Blue")
    setColor(0,0,100)
    time.sleep(0.5)
    setColor(0,0,0)

def loop():
    # реєструємо обробник натискань
    GPIO.add_event_detect(buttonPin, GPIO.FALLING, callback = buttonEvent, bouncetime = bounceTime)
    while True :
        time.sleep(tick)

def destroy():
    pwmRed.stop()
    pwmGreen.stop()
    pwmBlue.stop()
    GPIO.cleanup()

if __name__ == '__main__':     # Program entrance
    print ('Program is starting ... ')
    setup()
    try:
        loop()
    except KeyboardInterrupt:
        destroy()

зображення: експеримент із багатоколірним діодом

(повернутися до змісту)

6.1 дверний дзвоник

перша простенька схема з транзистором: активна «пищалка» (власний резонатор), з кнопкою для вмикання звуку. цікава для пригадування, як практично застосовувається біполярний транзистор. програма для керування — примітивна, подібна до експерименту 2.1, модифікувати її вже нецікаво.

зображення: пищалка на транзисторі з кнопкою

(повернутися до змісту)

6.2 сигналізація

використано ту саму схему з попереднього експерименту, але активну зумер замінено пасивним — вон не має власного резонатора, тож звукова частота подається програмою за допомогою широтно-імпульсної модуляції (майже як в експериментах з керуванням яскравістю діодів): виставляється коефіцієнт заповнення 50%, розраховується синус із кроком 1°, масштабується (x500), накладається на частоту-носій 2 кгц і результат встановлюється що 10 мілісекунд на зумер (це ж, по суті, частотна модуляція у звуковому діапазоні?):

def alertor():
    p.start(50)                                     # коефіцієнт заповнення = 50%
    for x in range(0,361):                          # 
        sinVal = math.sin(x * (math.pi / 180.0))    # генерування синусоїди (° -> радіани)
        toneVal = 2000 + sinVal * 500               # накладання на резонансну частоту-носій
        p.ChangeFrequency(toneVal)                  # перепрограмування модулятора pwm
        time.sleep(0.001)                           # …з кроком 10 мс

ось тут вже є з чим погратися. по-перше, розрахунок синуса з кроком 1° раз на 10 мс — ну, це ж марнотратство! достатньо робити це вдесятеро повільніше, — а примітивний зумер від цього навіть трохи менше прихрипує:

for x in range(0,361,10):                       # крок 10°
    …
    time.sleep(0.001)                           # …з кроком 100 мс

далі цікаво випробувати діапазон частот, які може хоч приблизно відтворювати той «пищик», додавши виклик testFrequecy() наприкінці setup() або на початку loop():

def testFrequency():
    p.start(50)
    for freq in range(100,10000,100):
        print("Frequency: %i" % freq)
        p.ChangeFrequency(freq)
        time.sleep(tick)
    p.stop()

виявляється, що десь від 500 гц до 4 кгц звук прийнятний, як для зумера. гаразд, а чи можна регулювати гучність, змінюючи прогальність? гадаю, що ні: для цього треба було би регулювати напругу на зумері, а за схемою транзистор в режимі насичення, просто відкривається/закривається сигналом з gpio, і практично безінерційний (~100 мгц). тож — ніт, але спробувати ніхто не забороняє:

def testVolume():
    print("Testing volume with DC")
    p.ChangeFrequency(2000)
    for level in range (0,101,10):
        print("Level: %i" % level)
        p.start(level) 
        time.sleep(tick*2)
    p.stop()

ефекту практично жодного — що й треба було довести. гаразд, але навіть без керування гучністю, лише частотою й часом звучання можна ж пропищати «марш імперії»? легко! знадобляться ноти:

# ноти (частоти)
C = 261
D = 294
E = 329
F = 349
G = 391
GS = 415
A = 440
AS = 455
B = 466
CH = 523
CSH = 554
DH = 587
DSH = 622
EH = 659
FH = 698
FSH = 740
GH = 784
GSH = 830
AH = 880

далі — мелодія; особисто мені ведмідь на вухо став у дитинстві, але ось тут я знайшов те, що треба; тільки писати окрему команду на кожну ноту є тупо, тому — список (у форматі (нота, тривалість)):

# мелодія (нота, тривалість)
melody = (
    (A,500),(A,500),(A,500),(F,350),(CH,150),(A,500),(F,350),(CH,150),(A,650),
    (0,500),
    (EH,500),(EH,500),(EH,500),(FH,350),(CH,150),(GS,500),(F,350),(CH,150),(A,650),
    (0,500),
    (AH,500),(A,300),(A,150),(AH,500),(GSH,325),(GH,175),(FSH,125),(FH,125),(FSH,250),
    (0,325),
    (AS,250),(DSH,500),(DH,325),(CSH,175),(CH,125),(B,125),(CH,250),
    (0,350),
    (F,250),(GS,500),(F,350),(A,125),(CH,500),(A,375),(CH,125),(EH,650),
    (0,500),
    (AH,500),(A,300),(A,150),(AH,500),(GSH,325),(GH,175),(FSH,125),(FH,125),(FSH,250),
    (0,325),
    (AS,250),(DSH,500),(DH,325),(CSH,175),(CH,125),(B,125),(CH,250),
    (0,350),
    (F,250),(GS,500),(F,375),(CH,125),(A,500),(F,375),(CH,125),(A,650),
    (0,650)
)

проста функція для відтворення мелодії з такого списку:

def march():                            # марш імперії!
    for note in melody:                 # перебір нот з мелодії
        print("Note: %i" % note[0])     # просто для індикації
        if note[0] > 0:                 # частоту нуль не можна виставити!
            p.start(50)                 # вкл. звук (коеф. заповн. = 50%)
            p.ChangeFrequency(note[0])  # встановити частоту з ноти
        else:
            p.stop()                    # нульова частота — пауза (викл. звук)
        time.sleep(note[1]/1000)        # перевести мс тривалості в с
        p.stop()                        # відзвучала нота — викл. звук
        time.sleep(20/1000)             # «розрив» між нотами

навіть немає сенсу наводити всю програму, бо досить просто замінити alerter() в коді на march() — і кнопка запускає марш імперії.

(повернутися до змісту)

третій бонус: реанімація малинки

на цьому етапі я, здається, ненавмисне «поламав» малинку, намагаючись поремонтувати i2c. на щастя, усі свої «витвори» для експериментів завчасу скопіював на лептоп (бо налаштовував їх віддалений запуск по ssh, про це згодом), тож треба просто перевстановити систему на флешку.

зображення: малинка відмовилась завантажуватися

здається, багато що змінилося, відколи я востаннє готував системні флешки для raspberry pi:

  • noobs вже не торт;
  • офіційний інструмент відтепер — raspberry pi imager;
  • неофіційну справу noobs’а продовжує pinn;
  • найсвіжіша версія noobs на github’і — 3.8.1 (січень 2022, 2,9 гб), в мене збереглася 3.0.1 (жовтень 2018, 1,8 гб).

спробуймо noobs 3.8.1 для початку?

  • завантажити архів noobs;
  • розпакувати zip (можна одразу на форматовану в fat32 флешку);
  • вилучити «зайві» libreelec з теки /os/;
  • у файлі recovery.cmdline додати silentinstall до аргументів runinstaller

незрозуміло, що робити з ssh: раніше можна було додати порожній файл ssh в корінь флешки, щоби після копіювання він опинявся в корені встановленої raspbian, котра автоматично піднімала сервер ssh. але… здається, в якийсь момент меінтейнери raspbian вирішили, що це погана ідея, і прибрали цю функцію?

тиць-тиць, дзуськи: від усіх покращень noobs погладшав на гігабайт і не може розгорнутися на флешку 8 гб!

зображення: noobs віджер пуза й не влазить на флешку 8 гб

я хотів спробувати автовстановлення з pinn: на позір воно зручне, але святий транзисторе, яке ж складне для розуміння й використання саме в автоматичному режимі! в підсумку, довелося підключити монітор, клавіатуру, щоби закінчити процес. додатково:

  • перезавантаження після встановлення (чомусь dietpi не робить це сам?): # systemctl reboot;
  • налаштування розкладки клавіатури;
  • довстановлення необхідних для розваг з набором freenove пакунків: python3, python3-RPi.GPIO, WiringPi, I2C;
  • довстановлення пакунків, без котрих куций dropbear «не вміє» приймати файли по ssh (!): openssh-client, openssh-sftp-server
  • довстановлення vim, screen (бо будь-яке поновлення на малинці займає стільки часу, що запускати його по ssh, ризикуючи обірвати сесію — нерозумно).

після цього, враження від dietpi вельми позитивні: справді дуже компактна, добре налаштована й реактивна система для raspberry pi; сподобалась.

(повернутися до змісту)

7.1 читання напруги на потенціометрі

наступний експеримент використовує ацп, котрий «спілкується» з малинкою через інтерфейс i2c. найперше, треба ввімкнути підтримку i2c (raspi-config або dietpi-config), переконатися, що на малинці необхідні бібліотеки встановлено, модулі ядра завантажено тощо (те, з чим у мене виникли проблеми до перевстановлення системи):

> lsmod | grep i2c
i2c_bcm2835            16384  0
i2c_dev                16384  0
i2c_bcm2708            16384  0
> [sudo] apt list --installed | grep "i2c-tools\|python3-smbus"
i2c-tools/stable,now 4.2-1+b1 armhf [installed]
python3-smbus/stable,now 4.2-1+b1 armhf [installed]

схема проста, програма до неї відносно проста; єдиний нюанс — із модулем ADCDevice.py: інструкція пропонує розгорнути його з архіва ADCDevice-1.0.3.tar.gz і зареєструвати в системі за допомогою setup.py install, але… нмсд, цього не треба робити: копія модуля є в теці проєкту 07.1.1_ADC, і чудово звідти підтягується, треба лише додати користувача (pi, dietpi чи створеного для себе) до групи системної i2c, щоби не доводилося запускати скрипти, що використовують i2c, із sudo (доведеться перелогінитися після цього):

> sudo adduser <user> i2c

простенька модифікація в циклі loop() для виводу інформації про напругу лише коли вона змінюється:

tick = 0.3                      # такт для циклу = 0,3 c 
def loop():
    while True:
        try:                    # запобіжник для першого прогону
            oldValue = value    # …коли value ще не визначена
        except NameError:
            oldValue = 0 
        value = adc.analogRead(0)    # read the ADC value of channel 0
        voltage = value / 255.0 * 3.3  # calculate the voltage value
        if oldValue != value:   # вивід — якщо є зміна
            print ('ADC Value : %d, Voltage : %.2f'%(value,voltage))
        time.sleep(tick)        # затримка циклу

зображення: схема з ацп для вимірювання напруги

(повернутися до змісту)

8.1 регулювання яскравості світлодіода

розвиток попереднього експерименту — програмне регулювання яскравості світлодіода за допомогою потенціометра і ацп; до схеми додається діод і резистор між землею і одним із виводів gpio (11), решту робить програма.

(повернутися до змісту)

четвертий бонус: fritzing

рано чи пізно хочеться трішки модифікувати схему експерименту, або спробувати повністю перезібрати щось своє. власне, для цього й призначений набір з великою кількістю цікавих деталей (їх більше, ніж потрібно для схем в інструкції, а можна докупити ще більше), — але ж перш ніж колупатися в монтажній платі, хочеться/треба накидати електричну схему та якось візуалізувати монтаж?

виявляється, це нескладно зробити з чудовим редактором fritzing. не всі деталі є «з коробки»: приміром, не знайшов ацп ads7830 і підставив щось візуально подібне, але електричну схему вже не можу розвести як треба. але загалом — дуже помічний інструмент; на сайті є підказки з використання. теоретично, fritzing вміє проганяти симуляції електричних схем… але на практиці мені поки що не вдалося цього утнути (треба розбиратися).

зображення: візуалізація експериментів у редакторі fritzing

(повернутися до змісту)

9.1 регулювання кольору світлодіода

попередня схема, продубльована на три канали ацп, для керування кольорами триколірного світлодіода. практично нічого нового-цікавого, тож збираю, щоби «закрити гештальт» (ну не можна ж пропускати експерименти?!)

зображення: схема з ацп для регулювання кольорів світлодіода

як розважитися з цією схемою? наприклад, чи можна залишити тільки один потенціометр, і додати кнопку для вибору каналу (rgb) для регулювання яскравости, показуючи поточний канал одним кольоровим проблиском (див. монтажну схему в попередньому бонусі)? я спробував… зовсім забувши, що звичайний аналоговий потенціометр має «пам’ять» — його не можна «скинути в нуль», перемикаючи канал кнопкою. теоретично, викрутитися можна, додавши перевірку зміни значення на потенціометрі, — і не чіпати канали, якщо зміни нема:

#!/usr/bin/env python3
#############################################################################
# Filename    : ColorfulSoftlight_v2.py
# Description : Control RGBLED with one Potentiometer, and a button channel switch
# Author      : tivasyk (base on the code from www.freenove.com)
# modification: 2022-12-24
########################################################################
import RPi.GPIO as GPIO
import time
from ADCDevice import *

ledPins = [15, 13, 11]                          # виводи до колірних каналів RGB світлодіода
ledLevels = [0, 0, 0]                           # поточні рівні на каналах RGB
ledBlocked = False                              # блокування світлодіода для індикації каналу RGB
buttonPin = 12                                  # вивід кнопки (1 = вкл)
bounceTime = 300                                # затримка на jitter кнопки
adc = ADCDevice()                               # об'єкт для керування ацп
tick = 0.3
channel = 0                                     # канал (R = 0, G = 1, B = 2)
pwmRGB = []                                     # масив pwm'ів для керування ШІМ на 3-х каналах RGB

def setup():
    global adc                                  # детектування ацп
    if(adc.detectI2C(0x48)):                    # …pcf8591.
        adc = PCF8591()
    elif(adc.detectI2C(0x4b)):                  # …ads7830
        adc = ADS7830()
    else:                                       # не знайдено?
        print("No correct I2C address found, \n"
        "Please use command 'i2cdetect -y 1' to check the I2C address! \n"
        "Program Exit. \n");
        exit(-1)
        
    global pwmRGB                               # масив pwm для керування яскравостями RGB
    GPIO.setwarnings(False)                     # вимкнути попередження
    GPIO.setmode(GPIO.BOARD)                    # використовуємо фізичну нумерацію виводів gpio
    for pin in ledPins:                         # налаштування виводів до світлодіода
        GPIO.setup(pin, GPIO.OUT)               # режим OUTPUT
        GPIO.output(pin, GPIO.HIGH)             # схема зі спільним анодом (1 = викл)
        pwmRGB += [GPIO.PWM(pin,100)]           # створити об'єкт pwm для вивода pin (в масиві)
        pwmRGB[len(pwmRGB)-1].start(0)          # …і запустити ШІМ на виводі
    GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)      # налаштування кнопки
    
def buttonEvent(chan):                          # обробник натискань кнопки
    global channel
    global pwmRGB
    global ledBlocked
    if channel < 2:                             # перемкнути канал (0 -> 1 -> 2 -> 0)
        channel += 1
    else:
        channel = 0
    print("Switch to channel %i" % channel)
    ledBlocked = True                           # заблокувати світлодіод
    for pwm in range(0,len(pwmRGB)):
        if pwm == channel:
            pwmRGB[pwm].ChangeDutyCycle(0)      # блимнути поточним каналом
        else:
            pwmRGB[pwm].ChangeDutyCycle(100)    # …вимкнувши інші
    time.sleep(tick*3)
    for pwm in range(0,len(pwmRGB)):
        pwmRGB[pwm].ChangeDutyCycle(100 - ledLevels[pwm]*100/255)   # відновити рівень каналів
    ledBlocked = False                          # розблокувати світлодіод

def loop():
    global channel                              # канал (R = 0, G = 1, B = 2)
    global ledLevels                            # поточні рівні на каналах RGB
    oldLevel = 0
    # зареєструємо обробник натискань кнопки
    GPIO.add_event_detect(buttonPin, GPIO.FALLING, callback = buttonEvent, bouncetime = bounceTime)
    while True:     
        if not ledBlocked:                      # лише якщо не в режимі індикації каналу
            newLevel = adc.analogRead(0)        # прочитати потенціометр, не чіпаючи канали RGB
            if abs(newLevel - oldLevel) > 5:    # допоки немає зміни
                ledLevels[channel] = newLevel
                oldLevel = newLevel
                pwmRGB[channel].ChangeDutyCycle(100 - ledLevels[channel]*100/255)  # …і налаштовуємо ШІМ поточного каналу RGB
                print ('Current channel: %i; RGB values: %d, %d, %d' % (channel, ledLevels[0], ledLevels[1], ledLevels[2]))
        time.sleep(tick)                        # такт циклу

def destroy():
    adc.close()
    for pwm in pwmRGB:  # вимкнути ШІМ на всіх pwm
        pwm.stop()  
    GPIO.cleanup()
    
if __name__ == '__main__': # Program entrance
    print ('Program is starting ... ')
    setup()
    try:
        loop()
    except KeyboardInterrupt: # Press ctrl-c to end the program.
        destroy()

не дуже красиво, але працює. краще можна зробити лише з якимось регулятором без «пам’яті» (цифровий потенціометр?), або з кнопками «+/-».

зображення: схема з ацп для регулювання кольорів світлодіода одним потенціометром

(повернутися до змісту)

(далі буде)