Простите, давно не развлекался с неймингом, как и с периферией для домашней автоматики. Конкретно эта штука — пульт дистанционного управления светом — получилась, потому что я хотел что-то с интерфейсом типа «нажал-покрутил-нажал», а не с обычной россыпью кнопок. Вау-эффект не достигнут: домашние в упор не замечают пульт, но я, по крайней мере, закрыл гештальт.
Краткое ТЗ:
1) Управление тремя группами освещения на кухне
2) Управление тремя группами освещения в комнате
3) Управление всеми источниками света одновременно
4) Разумная продолжительность автономной работы (от недели)
5) Совместимость с кодированием Livolo, SC2260, EV1527
Итого, дальше не нужно читать, если вы не любите Arduino, выключатели Livolo и китайские радиорозетки. Потому что первое — основа для пульта, а второе и третье — периферия.
Концепт
Логика управления представлялась мне следующей:
1) Нажатие на «крутилку» переключает зоны группы освещения по кольцу (кухня — комната — все);
2) Поворот ручки, в зависимости от направления вращения, последовательно включает или выключает освещение выбранной группы;
3) Режим работы (выбранная группа) отображается ненавязчивой светодиодной индикацией.
Так как у меня в ходу радиоуправление по наиболее презираемому варианту, без защиты от помех и обратной связи, то заодно предусмотрена небольшая хитрость на случай пропуска срабатывания.
В случае, если поворот ручки не приводит к желаемому результату, то комбинированное нажатие и поворот в обратную сторону позволяет пропустить команду. Затем команду можно повторить, как обычно.
То есть, если я повернул ручку по часовой стрелке, а главный свет не зажегся, то я могу нажать на ручку, повернуть ее против часовой стрелки, а затем отпустить и снова повернуть по часовой, чтобы повторить включение.
Зачем так сложно? Затем, что кроме неловких протоколов, у меня еще и неловкие периферийные устройства. Например, радиоуправляемые выключатели света Livolo и радиореле, у которых одна и та же команда на включение и выключение вкупе с обычными радиорозетками, у которых команды на включение и выключение — раздельные.
Трюк с пропуском команды позволяет творчески обыграть невключение (невыключение), не ломая общей схемы иллюминации. Кроме того, пропуск команды позволяет перескакивать через источники света, включение или выключение которых не требуется.
Ну и, конечно, чтобы понимать, что вообще происходит с пультом, у него имеется отдельный индикатор, который горит во время посыла команды.
Если пульт некоторое время (настраивается в коде) не трогать, то контроллер отправляется спать. При этом он не сохраняет последнее состояние, и когда просыпается по нажатию на ручку, то начинает жизнь с чистого листа.
Это не ошибка. Повторюсь, у меня выключатели без обратной связи, и пульт физически не в состоянии получить информацию о текущем состоянии каждого управляемого периферийного устройства.
Поэтому сразу после пробуждения поворот ручки начинает либо включать, либо выключать свет с нуля.
Первый подход
Визуальная концепция вида «коробка с крутилкой» требовала, как можно догадаться, двух вещей: коробки и крутилки. В первой версии роль коробки играл тонкий пауэрбанк, использование которого решало сразу две проблемы: у меня был и корпус, и схема зарядки аккумулятора, причем уже с разъемом. Сам аккумулятор, понятно, пришлось заменить на более компактный, иначе начинка уже не помещалась.
С крутилкой вышло затейливее. По ходу поисков я выяснил, что чем симпатичнее ручка потенциометра и чем она больше — тем ближе стоимость ее грамма к стоимости грамма золота. Поэтому приобрел ручку, которая минимально устраивала меня по эстетическим свойствам.
Управляющая часть стала результатом эксперимента с ATmega328P и закономерным продолжением сюжетной линии, задаваемой уже имеющейся домашней автоматикой (на тех же Arduino и примитивных радиопротоколах).
Я не очень дорого купил россыпь упомянутых контроллеров и условно макетных (на самом деле — переходник с мелкого корпуса на крупный шаг) плат с целью попробовать изготовить из них малобюджетную версию Arduino с минимальным (но разумным) количеством элементов.
Эксперимент оказался успешным, и сконфигурированный под среду Arduino контроллер вполне успешно замигал светодиодом, после того, как проглотил классический Blink. Ну а затем по принципу «дорисуйте сову», я добавил к получившейся плате энкодер (с кнопкой), три светодиода и обычный передатчик с амплитудной модуляцией на несущей 433,92 МГц.
Чтобы разместить все элементы в небольшом корпусе, мне пришлось немного помучиться, но пульт все же заработал. И хотя, казалось бы, задача решена, хотелось большего — оригинального корпуса.
Второй подход
Собственно, первую версию (жалуюсь) по внешнему виду группа товарищей разбила в пух и прах, поэтому я отложил ее на неопределенное время. Но разбирать не стал: жалко.
А вот когда появился 3D-принтер, то пообещал себе однажды сделать тот самый оригинальный корпус и таким образом закрыть вопрос с пультом.
Я не знаю, плохо или хорошо у меня получилось — не очень умею оценивать свои штуки. Но на 3DToday коллектив более радушный, чем на MySKU (а это я не жалуюсь — сам не подарок), и оценили корпус выше, чем я сам.
Зато имея полную свободу действий, я отказался от хилых и невнятных китайских аккумуляторов, и взял источником питания старый добрый 18650. И, как несложно видеть, именно его размеры во многом определяют габариты всего корпуса.
Сам же корпус я стал делать модульным, состоящим из множества частей, что позволяло перепечатывать лишь отдельные (ошибочные или не слишком оптимальные) элементы, а не все изделие целиком.
Другой момент в том, что я крайне не люблю делать вырезы под разъемы, которые у меня толком и не получаются. Поэтому в питающей цепи есть еще одна хитрость, известная по ежу Ивлину: беспроводная зарядка.
У меня в загашнике как раз лежал еще один приемник, который я сразу же пустил в дело.
Наконец, последняя хитрость — довольно очевидная, но все же: чтобы пульт не елозил по столу, я приклеил на дно кусочек нескользящего автомобильного коврика. И в итоге эта штука стоит абсолютным монолитом, хотя и переставить на другое место — тоже не проблема.
Что потребуется для повторения
Железка
1) Контроллер ATmega328P — 1 шт. (у меня в корпусе TQFP, но можно любой)
2) Резистор 10 кОм — 5 шт. (4 на подавление дребезга энкодера, 1 — на контроллер)
3) Резистор 100 Ом — 3 шт.
4) Керамические конденсаторы 0,1 мкФ — 4 шт. (на контроллер и подавление дребезга энкодера)
5) Нажимной энкодер (валкодер) — 1 шт. (у меня PEC12-4220F-S0024)
6) Светодиоды — 3 шт. (диаметром 3 мм)
7) Плата зарядки литиевого аккумулятора — 1 шт. (из попавшегося под руку пауэрбанка, по идее, подойдет любая с автоматическим включением под нагрузкой)
8) Приемник беспроводной зарядки Qi — 1 шт.
9) Передатчик с амплитудной модуляцией на 433 МГц — 1 шт. (вроде такого)
10) Немного стеклотекстолита для платы энкодера
11) 3D-принтер
12) Подходящий пластик (я печатал PLA)
13) Винты M4x30 — 4 шт.
Вообще, количество компонентов можно уменьшить. К примеру, в совсем минимальном варианте контроллер вообще не требует обвязки, хотя я решил последовать совету Ника Гаммона и не пожалел пару конденсаторов и резистор.
Точно так же можно не заморачиваться с аппаратным подавлением дребезга контактов, и попробовать обойтись программным. Тогда можно вычеркнуть еще четыре резистора и пару конденсаторов.
Альтернативно можно использовать готовую плату Arduino, вроде Pro Mini, но в этом случае я не могу гарантировать низкий уровень потребления энергии, и вам придется поколдовать над ним самостоятельно. Заодно придется подправить и корпус.
Схема:
Для справки распиновка ATmega328p в корпусе TQFP-32 от Hobby Electronics:
Для своего энкодера я нарисовал небольшую плату:
По хорошему, ее бы надо просверлить для монтажа энкодера, либо придавливать его «брюхом к плате (позаботившись об изоляции, чтобы не было КЗ), чтобы энкодер был смонтирован а) более-менее ровно и б) не шатался. Исторически сложилось, что у меня — второй вариант.
Для корпуса важно, чтобы высота платы с деталями, исключая энкодер, была не более (или не сильно более) 5 мм.
Содержимое архива следует извлечь в папку hardware папки среды Arduino. В дальнейшем я описываю происходящее на примере среды 1.0.3, которой пока пользуюсь.
Когда описания скопированы, следует запустить Arduino и загрузить скетч программатора в Arduino, которая будет использоваться в качестве этого самого программатора. Скетч находится в меню Файл — Примеры — ArduinoISP.
Разумеется, следует выбрать свою плату и порт. Я выбираю Mega, потому что у меня она и есть:
После загрузки скетча программатора необходимо переключиться на целевую плату. Т.е. в нашем случае — ATmega328 с частотой 8 МГц и внутренним задающим генератором. Она будет в списке плат, если описания, о которых говорил выше, скопированы правильно:
Теперь нужно соединить линии MISO, MOSI и SCK платы-программатора и платы с будущей Arduino, а также подключить RESET, GND и VCC. Плюс питания лучше именно в последнюю очередь.
Исходя из приведенной выше инфографики и описания Arduino Mega, вырисовывается следующая картина:
SPI — Arduino Mega — ATmega328p
MISO — 50 — 16
MOSI — 51 — 15
SCK — 52 — 17
SS (RESET) — 53 — 29
Физическое подключение на ваш вкус, я применил исключительно варварский метод — обычные макетные провода прямо в отверстия платы, без пайки и изоляции:
Если все готово — записываем загрузчик. Сначала убеждаемся, что выбран правильный программатор (Сервис — Программатор — Arduino as ISP):
Потом делаем Сервис — Записать загрузчик:
После этого на выходе — минималистическая плата Arduino, для загрузки скетчей в которую можно использовать адаптер USB-Serial или полноценную плату Arduino с таким адаптером на борту. В первом случае нужно соединить крест-накрест RX и TX, и не забыть подключить общую землю. Во втором случае дополнительно необходимо замкнуть на землю RESET Arduino, которая используется в качестве адаптера.
Если у вас, как и у меня, нет цепи автоматического сброса контроллера перед загрузкой скетча, то вариантов два: либо дернуть его сброс, либо просто включить его питание, когда среда Arduino напишет о начале загрузки.
Корпус
Корпус, как я и говорил — модульный. Это значит, что на скрытую от глаз внутреннюю часть можно пустить тот пластик, который залежался и больше никуда не годится. В него можно одеть электронику:
Обращаю внимание на тот факт, что корпус специфичный и рассчитан на то, чтобы в него поместился мой вариант начинки.
Ротор я предлагаю делать прозрачным, чтобы он рассеивал свет индикаторов. Для большей увесистости внутрь ротора можно вложить гайку М16:
Еще понадобится рубашка ротора и крышка к ней. Крышка просто вставляется внутрь и держится на трении. И, конечно, не обойтись без верхней и нижней части внешнего корпуса.
Я печатал ротор с заполнением 10%, остальные элементы — с заполнением 5%. Пластик — PLA. Установленная температура сопла на моем принтере — 200С на первых трех слоях, 185С — на последующих. К сожалению, не могу сказать, какая истинная температура сопла. Стол холодный.
Сборка простая.
Платы размещаются в пазах прочного корпуса, светодиоды — ножками в пазы нижней части прочного корпуса. Антенна передатчика выводится вниз, точно так же вниз выводится приемник беспроводной зарядки — чтобы он был ближе к этой самой зарядке.
Начинка фиксируется промежуточной пластиной, в паз которой проходит жгут проводов энкодера.
Энкодер фиксируется верхней пластиной, все вместе стягивается винтами M4x30, которые сами нарезают себе резьбу в пластике.
Теперь прочный корпус можно заключить в половинки внешнего корпуса. На вал энкодера надевается ротор, на ротор — рубашка. Опционально на дно корпуса клеится нескользящий коврик. Другая опция — декоративная вставка, которая скрывает шов между половинками корпуса.
Это все располагается в секции переменных.
// Код сна: http://donalmorrissey.blogspot.ru/2010/04/sleeping-arduino-part-5-wake-up-via.html
// Библиотека Livolo: http://forum.arduino.cc/index.php?action=dlattach;topic=153525.0;attach=108106
#include <avr/sleep.h>
#include <avr/power.h>
#include <livolo.h>
#define adc_disable() (ADCSRA &= ~(1<<ADEN)) // disable ADC (before power-off)
#define adc_enable() (ADCSRA |= (1<<ADEN)) // re-enable ADC
#define txPin 7 // пин передатчика
Livolo livolo(txPin);
#define PULSESHORT 450
#define PULSELONG 1350
#define PULSESYNC 13950
#define encA 5
#define encB 6 // пины энкодера
#define buttonPin 2 // пин кнопки (прерывание)
#define roomLed 10 // пин индикатора комнаты
#define kitchenLed 9 // пин индикатора кухни
#define switchLed 3 // пин индикатора переключения освещения
#define switchLedTimeOut 150 // время свечения индикатора переключения освещения
#define switchTreshold 4 // количество срабатываний энкодера до переключения
#define offDelay 15000 // таймаут автовыключения
#define rLev 3 // количество уровней света комнаты
#define kLev 3 // количество уровней света кухни
#define glev 2 // количество уровней света всего
#define txPowerPin 8 // пин питания передатчика
#define kitchenBackLightOn1 12
#define kitchenBackLightOn2 34
#define kitchenMainLightOn 56
#define roomBackLightOn 12
#define roomMainLightOn1 34
#define roomMainLightOn2 56
#define mainLightOn 12
#define kitchenBackLightOff1 12
#define kitchenBackLightOff2 34
#define kitchenMainLightOff 56
#define roomBackLightOff 12
#define roomMainLighOtff1 34
#define roomMainLightOff2 56
#define mainLightOff 12
#define LivoloID 8500
volatile byte rotorMode = 0; // режим работы
byte currentMode = 0; // текущий режим работы
int curEncA, prevEncA, curButton, prevButton; // текущее значение энкодера, предыдущее
byte encCountPlus = 0; // счетчик энкодера
byte encCountMinus = 0; // счетчик энкодера
unsigned long offTimeOut = 0; // счетчик таймера автовыключения
unsigned long modeTimeOut = 0; // счетчик таймера короткого нажатия для переключения режима
unsigned long switchLedTime = 0; // счетчик выключения индикатора переключения освещения
unsigned long modeTime = 0; // длительность нажатия
unsigned int modeTreshold = 500; // пороговая длительность короткого нажатия (короче ххх мс)
unsigned int bounceTreshold = 200; // пороговая длительность дребезга контактов
byte rLevState = 0;
byte kLevState = 0;
byte gLevState = 0;
// флаги света k - кухня, r - комната, h - коридор, b - ванная
boolean kBackState = false;
boolean kBackState1 = false;
boolean kMainState = false;
boolean rBackState = false;
boolean rMainState = false;
boolean rMainState1 = false;
boolean hMainState = false;
boolean bMainState = false;
boolean afterSleep = false; // флаг выхода из режима сна
boolean modeTimeOutStart = false;
boolean switchLedOn = false;
boolean allOn = false; // флаг "все включено" для индикатора переключения
boolean allOff = false; // флаг "все выключено" для индикатора переключения
static void ookPulse(int on, int off) {
digitalWrite(txPin, HIGH);
delayMicroseconds(on);
digitalWrite(txPin, LOW);
delayMicroseconds(off);
}
static void rcSend(long remoteCode) {
for (byte reSend = 0; reSend < 8; reSend++) {
for(byte repeat=0; repeat<4; repeat++){
for (byte i = 24; i>0; i--) { // transmit remoteID
byte txPulse=bitRead(remoteCode, i-1); // read bits from remote ID
// Serial.print(txPulse);
switch (txPulse) {
case 0: // 00
ookPulse(PULSESHORT,PULSELONG);
//ookPulse(PULSESHORT,PULSELONG);
break;
case 1: // 11
ookPulse(PULSELONG,PULSESHORT);
//ookPulse(PULSELONG,PULSESHORT);
break;
} // switch
} // for loop
ookPulse(PULSESHORT,PULSESYNC); // S(ync)
// Serial.println();
} // repeat
}
delay(150);
}
void switchLedToggle() {
digitalWrite(switchLed, HIGH);
switchLedTime = millis();
switchLedOn = true;
}
void lightsUp(boolean lightsUpMode) {
// чтобы при включении после сна при "выключении" свет выключался с максимума
// а при "включении" - с минимума
if (afterSleep == true) {
if (lightsUpMode == false) {
gLevState = 1;
rLevState = 3;
kLevState = 3;
} else
{gLevState = 0;
rLevState = 0;
kLevState = 0;
}
afterSleep = false; // сброс признака "после сна"
}
// Все освещение
if (rotorMode == 2) {
if (lightsUpMode == false){
if (allOff == false) {
switchLedToggle();
if (digitalRead(buttonPin) == HIGH) {
// выключить все
gLevState = 0;
rLevState = 0;
kLevState = 0;
rcSend(kitchenBackLightOff1);
rcSend(kitchenBackLightOff2);
rcSend(roomBackLightOff);
livolo.sendButton(LivoloID, mainLightOff);
}
allOff = true;
allOn = false;
}
}
if (lightsUpMode == true){
// включить все, что не включено
if (allOn == false) {
switchLedToggle();
if (digitalRead(buttonPin) == HIGH) {
gLevState = 1;
rLevState = 3;
kLevState = 3;
rcSend(kitchenBackLightOn1);
rcSend(kitchenBackLightOn2);
rcSend(roomBackLightOn);
livolo.sendButton(LivoloID, mainLightOff); // сначала выключим все Livolo
livolo.sendButton(LivoloID, kitchenMainLightOn); //#1 теперь включим
livolo.sendButton(LivoloID, roomMainLightOn1); // #2
livolo.sendButton(LivoloID, roomMainLightOn2); // #3
livolo.sendButton(LivoloID, mainLightOn); // #6
}
allOn = true;
allOff = false;
}
}
}
// Кухня
if (rotorMode == 1) {
if (lightsUpMode == false && kLevState > 0) {
switchLedToggle();
if (digitalRead(buttonPin) == HIGH) {
if (kLevState == 3) {
// выкл верх
livolo.sendButton(LivoloID, kitchenMainLightOff); // #3
}
if (kLevState == 2) {
// выкл фон 2
livolo.sendButton(LivoloID, kitchenBackLightOff2); // #6
}
if (kLevState == 1) {
// выкл фон 1
rcSend(kitchenBackLightOff1);
}
}
if (kLevState!=0) {
kLevState--;}
// Serial.println(kLevState);
}
if (lightsUpMode == true && kLevState < 3) {
switchLedToggle();
kLevState++;
if (digitalRead(buttonPin) == HIGH) {
if (kLevState > 3) {kLevState = 3;}
// Serial.println(kLevState);
if (kLevState == 1) {
// вкл фон 1
rcSend(kitchenBackLightOn1);
}
if (kLevState == 2) {
// вкл фон 2
livolo.sendButton(LivoloID, kitchenBackLightOn2); // #6
}
if (kLevState == 3) {
// вкл верх
livolo.sendButton(LivoloID, kitchenMainLightOn); // #3
}
}
}
}
// Комната
if (rotorMode == 0) {
if (lightsUpMode == false && rLevState > 0) {
switchLedToggle();
if (digitalRead(buttonPin) == HIGH) {
if (rLevState == 3) {
// выкл верх1
livolo.sendButton(LivoloID, roomMainLighOtff1); //#1
}
if (rLevState == 2) {
// выкл верх
livolo.sendButton(LivoloID, roomMainLightOff2); // #2
}
if (rLevState == 1) {
// выкл фон
rcSend(roomBackLightOff);
}
}
if (rLevState != 0) {
rLevState--;
}
}
if (lightsUpMode == true && rLevState < 3) {
switchLedToggle();
rLevState++;
if (digitalRead(buttonPin) == HIGH) {
if (rLevState == 1) {
// вкл фон
rcSend(roomBackLightOn);
}
if (rLevState == 2) {
// вкл верх
livolo.sendButton(LivoloID, roomMainLightOn1); //#1
}
if (rLevState == 3) {
// вкл верх 1
livolo.sendButton(LivoloID, roomMainLightOn2); // #2
}
}
}
}
}
void wakeUp() {
detachInterrupt(0);
}
void setMode() {
if (rotorMode >= 2 ) {
rotorMode = 0;
} else {
rotorMode++;
}
offTimeOut = millis();
}
void ledBlink() {
for (byte iLed = 0; iLed<3; iLed++) {
digitalWrite(kitchenLed, HIGH);
digitalWrite(roomLed, HIGH);
delay(100);
digitalWrite(kitchenLed, LOW);
digitalWrite(roomLed, LOW);
delay(100);
}
}
void setLed() {
if (rotorMode == 0) { // Комната
digitalWrite(roomLed, HIGH);
digitalWrite(kitchenLed, LOW);
}
if (rotorMode == 1) { // Кухня
digitalWrite(roomLed, LOW);
digitalWrite(kitchenLed, HIGH);
}
if (rotorMode == 2) { // Комната и кухня
digitalWrite(roomLed, HIGH);
digitalWrite(kitchenLed, HIGH);
}
}
void enterSleep()
{
// ledBlink();
afterSleep = true;
digitalWrite(txPin, LOW);
digitalWrite(txPowerPin, LOW);
digitalWrite(roomLed, LOW);
digitalWrite(kitchenLed, LOW);
digitalWrite(switchLed, LOW);
pinMode(txPin, INPUT);
pinMode(txPowerPin, INPUT);
pinMode(roomLed, INPUT);
pinMode(kitchenLed, INPUT);
pinMode(switchLed, INPUT);
attachInterrupt(0, wakeUp, LOW);
adc_disable();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode();
sleep_disable();
power_all_enable();
pinMode(txPin, OUTPUT);
pinMode(txPowerPin, OUTPUT);
pinMode(roomLed, OUTPUT);
pinMode(kitchenLed, OUTPUT);
pinMode(switchLed, OUTPUT);
digitalWrite(txPin, LOW);
digitalWrite(txPowerPin, HIGH);
// ledBlink();
setLed();
offTimeOut = millis();
allOn = false;
allOff = false;
}
void setup() {
// Serial.begin(115200);
pinMode(txPin, OUTPUT);
pinMode(txPowerPin, OUTPUT);
pinMode(roomLed, OUTPUT);
pinMode(kitchenLed, OUTPUT);
pinMode(switchLed, OUTPUT);
digitalWrite(txPin, LOW);
digitalWrite(txPowerPin, HIGH);
digitalWrite(roomLed, LOW);
digitalWrite(kitchenLed, LOW);
digitalWrite(switchLed, LOW);
// pinMode(buttonPin, INPUT_PULLUP);
pinMode(buttonPin, INPUT);
pinMode(encA, INPUT);
pinMode(encB, INPUT);
prevEncA = digitalRead(encA);
offTimeOut = millis();
rotorMode = 0;
setLed();
prevButton = digitalRead(buttonPin);
}
void loop() {
if ((millis() - offTimeOut) > offDelay) {
enterSleep();
} else {
// выключение индикации переключения режимов
if (switchLedOn == true) {
if ((millis() - switchLedTime) > switchLedTimeOut) {
digitalWrite(switchLed, LOW);
switchLedOn = false;
}
}
// сброс таймера автовыключения при нажатом энкодере
if (digitalRead(buttonPin) == LOW) {
offTimeOut = millis();
}
// переключение режимов
curButton = digitalRead(buttonPin);
if ((prevButton == HIGH) && (curButton == LOW)) {
if (modeTimeOutStart == false) {
modeTimeOut = millis();
modeTimeOutStart = true;
}
} else {
if (modeTimeOutStart == true) {
modeTime = millis() - modeTimeOut;
if ((modeTime < modeTreshold) && (modeTime > bounceTreshold)) {
setMode();
modeTimeOutStart = false;
prevButton = digitalRead(buttonPin);
} else {
modeTimeOutStart = false;
prevButton = digitalRead(buttonPin);
}
}
}
// переключение индикации режимов
if (currentMode != rotorMode) { // если текущий режим не совпадает с установленным, то устанавливаем текущий режим
currentMode = rotorMode;
// и включаем соответствующую индикацию
setLed();
}
// подсчет импульсов
curEncA = digitalRead(encA);
if ((prevEncA == LOW) && (curEncA == HIGH)) {
offTimeOut = millis();
if (digitalRead(encB) == LOW) {
encCountMinus++;
encCountPlus = 0;
// Serial.println("Encoder Minus");
if (encCountMinus > switchTreshold) {
encCountMinus = 0;
lightsUp(false);
}
} else {
encCountPlus++;
encCountMinus = 0;
// Serial.println("Encoder Plus");
if (encCountPlus > switchTreshold) {
encCountPlus = 0;
lightsUp(true);
}
}
}
prevEncA = curEncA;
}
}
Модель корпуса по ссылке.
Все.
ps. Я постарался ничего не забыть, но мог. Если так — прошу прощения и приложу все силы, чтобы верно ответить на наводящие вопросы и исправить ошибки.
Автор: spc