Централизованный пульт контроля источников освещения ЦПКИО-2Д Ротор

image

Простите, давно не развлекался с неймингом, как и с периферией для домашней автоматики. Конкретно эта штука — пульт дистанционного управления светом — получилась, потому что я хотел что-то с интерфейсом типа «нажал-покрутил-нажал», а не с обычной россыпью кнопок. Вау-эффект не достигнут: домашние в упор не замечают пульт, но я, по крайней мере, закрыл гештальт.

Краткое ТЗ:

1) Управление тремя группами освещения на кухне
2) Управление тремя группами освещения в комнате
3) Управление всеми источниками света одновременно
4) Разумная продолжительность автономной работы (от недели)
5) Совместимость с кодированием Livolo, SC2260, EV1527

Итого, дальше не нужно читать, если вы не любите Arduino, выключатели Livolo и китайские радиорозетки. Потому что первое — основа для пульта, а второе и третье — периферия.

Концепт

Логика управления представлялась мне следующей:

1) Нажатие на «крутилку» переключает зоны группы освещения по кольцу (кухня — комната — все);

2) Поворот ручки, в зависимости от направления вращения, последовательно включает или выключает освещение выбранной группы;

3) Режим работы (выбранная группа) отображается ненавязчивой светодиодной индикацией.

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

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

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

Зачем так сложно? Затем, что кроме неловких протоколов, у меня еще и неловкие периферийные устройства. Например, радиоуправляемые выключатели света Livolo и радиореле, у которых одна и та же команда на включение и выключение вкупе с обычными радиорозетками, у которых команды на включение и выключение — раздельные.

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

Ну и, конечно, чтобы понимать, что вообще происходит с пультом, у него имеется отдельный индикатор, который горит во время посыла команды.

Если пульт некоторое время (настраивается в коде) не трогать, то контроллер отправляется спать. При этом он не сохраняет последнее состояние, и когда просыпается по нажатию на ручку, то начинает жизнь с чистого листа.

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

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

Первый подход

Визуальная концепция вида «коробка с крутилкой» требовала, как можно догадаться, двух вещей: коробки и крутилки. В первой версии роль коробки играл тонкий пауэрбанк, использование которого решало сразу две проблемы: у меня был и корпус, и схема зарядки аккумулятора, причем уже с разъемом. Сам аккумулятор, понятно, пришлось заменить на более компактный, иначе начинка уже не помещалась.

image

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

image

Управляющая часть стала результатом эксперимента с ATmega328P и закономерным продолжением сюжетной линии, задаваемой уже имеющейся домашней автоматикой (на тех же Arduino и примитивных радиопротоколах).

Я не очень дорого купил россыпь упомянутых контроллеров и условно макетных (на самом деле — переходник с мелкого корпуса на крупный шаг) плат с целью попробовать изготовить из них малобюджетную версию Arduino с минимальным (но разумным) количеством элементов.

image

image

image

Эксперимент оказался успешным, и сконфигурированный под среду Arduino контроллер вполне успешно замигал светодиодом, после того, как проглотил классический Blink. Ну, а затем по принципу «дорисуйте сову», я добавил к получившейся плате энкодер (с кнопкой), три светодиода и обычный передатчик с амплитудной модуляцией на несущей 433,92 МГц.

Чтобы разместить все элементы в небольшом корпусе, мне пришлось немного помучиться, но пульт все же заработал. И хотя, казалось бы, задача решена, хотелось большего — оригинального корпуса.

image

image

Второй подход

Собственно, первую версию (жалуюсь) по внешнему виду группа товарищей разбила в пух и прах, поэтому я отложил ее на неопределенное время. Но разбирать не стал: жалко.

А вот когда появился 3D-принтер, то пообещал себе однажды сделать тот самый оригинальный корпус и таким образом закрыть вопрос с пультом.

Я не знаю, плохо или хорошо у меня получилось — не очень умею оценивать свои штуки. Но на 3DToday коллектив более радушный, чем на MySKU (а это я не жалуюсь — сам не подарок), и оценили корпус выше, чем я сам.

image

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

Сам же корпус я стал делать модульным, состоящим из множества частей, что позволяло перепечатывать лишь отдельные (ошибочные или не слишком оптимальные) элементы, а не все изделие целиком.

Другой момент в том, что я крайне не люблю делать вырезы под разъемы, которые у меня толком и не получаются. Поэтому в питающей цепи есть еще одна хитрость, известная по ежу Ивлину: беспроводная зарядка.

iezq6m_73cckmk6y-bidsohs9ye.jpeg

У меня в загашнике как раз лежал еще один приемник, который я сразу же пустил в дело.

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

Что потребуется для повторения

Железка

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) Винты M4×30 — 4 шт.

Вообще, количество компонентов можно уменьшить. К примеру, в совсем минимальном варианте контроллер вообще не требует обвязки, хотя я решил последовать совету Ника Гаммона и не пожалел пару конденсаторов и резистор.

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

Альтернативно можно использовать готовую плату Arduino, вроде Pro Mini, но в этом случае я не могу гарантировать низкий уровень потребления энергии, и вам придется поколдовать над ним самостоятельно. Заодно придется подправить и корпус.

Схема:

woxvhhfaucpc3emhiyipfk56m1s.png

Для справки распиновка ATmega328p в корпусе TQFP-32 от Hobby Electronics:

1yeasp0veriyrgvhrafu_inlwlm.jpeg

Для своего энкодера я нарисовал небольшую плату:

i2jgd7p7jlv1kyhbyjhoayd3d4k.jpeg

kecrytksno5yhwsjjdpcbwgc9-c.jpeg

6yvb8jl1h6zvpm-z-atgntumnsw.jpeg

По хорошему, ее бы надо просверлить для монтажа энкодера, либо придавливать его «брюхом к плате (позаботившись об изоляции, чтобы не было КЗ), чтобы энкодер был смонтирован а) более-менее ровно и б) не шатался. Исторически сложилось, что у меня — второй вариант.

Для корпуса важно, чтобы высота платы с деталями, исключая энкодер, была не более (или не сильно более) 5 мм.

Если под руками не готовая плата Arduino, то для того, чтобы все заработало, в контроллер ATmega328P нужно для начала записать загрузчик Arduino.
Для этого необходимо, во-первых, добавить в среду Arduino описание контроллера. Для этого идем на официальный сайт Arduino и скачиваем оттуда подходящий для установленной у вас версии среды архив описаний (для 1.6, для 1.5, для 1.0).

Содержимое архива следует извлечь в папку hardware папки среды Arduino. В дальнейшем я описываю происходящее на примере среды 1.0.3, которой пока пользуюсь.

Когда описания скопированы, следует запустить Arduino и загрузить скетч программатора в Arduino, которая будет использоваться в качестве этого самого программатора. Скетч находится в меню Файл — Примеры — ArduinoISP.

image

Разумеется, следует выбрать свою плату и порт. Я выбираю Mega, потому что у меня она и есть:

image

После загрузки скетча программатора необходимо переключиться на целевую плату. Т.е. в нашем случае — ATmega328 с частотой 8 МГц и внутренним задающим генератором. Она будет в списке плат, если описания, о которых говорил выше, скопированы правильно:

image

Теперь нужно соединить линии 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

Физическое подключение на ваш вкус, я применил исключительно варварский метод — обычные макетные провода прямо в отверстия платы, без пайки и изоляции:

image

Если все готово — записываем загрузчик. Сначала убеждаемся, что выбран правильный программатор (Сервис — Программатор — Arduino as ISP):

image

Потом делаем Сервис — Записать загрузчик:

image

После этого на выходе — минималистическая плата Arduino, для загрузки скетчей в которую можно использовать адаптер USB-Serial или полноценную плату Arduino с таким адаптером на борту. В первом случае нужно соединить крест-накрест RX и TX, и не забыть подключить общую землю. Во втором случае дополнительно необходимо замкнуть на землю RESET Arduino, которая используется в качестве адаптера.

Если у вас, как и у меня, нет цепи автоматического сброса контроллера перед загрузкой скетча, то вариантов два: либо дернуть его сброс, либо просто включить его питание, когда среда Arduino напишет о начале загрузки.

Корпус

Корпус, как я и говорил — модульный. Это значит, что на скрытую от глаз внутреннюю часть можно пустить тот пластик, который залежался и больше никуда не годится. В него можно одеть электронику:

image

Обращаю внимание на тот факт, что корпус специфичный и рассчитан на то, чтобы в него поместился мой вариант начинки.

Ротор я предлагаю делать прозрачным, чтобы он рассеивал свет индикаторов. Для большей увесистости внутрь ротора можно вложить гайку М16:

image

Еще понадобится рубашка ротора и крышка к ней. Крышка просто вставляется внутрь и держится на трении. И, конечно, не обойтись без верхней и нижней части внешнего корпуса.

Я печатал ротор с заполнением 10%, остальные элементы — с заполнением 5%. Пластик — PLA. Установленная температура сопла на моем принтере — 200С на первых трех слоях, 185С — на последующих. К сожалению, не могу сказать, какая истинная температура сопла. Стол холодный.

Сборка простая.

kacq2-crn1cictkqxhpb0sok15u.jpeg

Платы размещаются в пазах прочного корпуса, светодиоды — ножками в пазы нижней части прочного корпуса. Антенна передатчика выводится вниз, точно так же вниз выводится приемник беспроводной зарядки — чтобы он был ближе к этой самой зарядке.

d9zuextrjywuvjbj4ve68-c2rgg.png

Начинка фиксируется промежуточной пластиной, в паз которой проходит жгут проводов энкодера.

Энкодер фиксируется верхней пластиной, все вместе стягивается винтами M4×30, которые сами нарезают себе резьбу в пластике.

Теперь прочный корпус можно заключить в половинки внешнего корпуса. На вал энкодера надевается ротор, на ротор — рубашка. Опционально на дно корпуса клеится нескользящий коврик. Другая опция — декоративная вставка, которая скрывает шов между половинками корпуса.

Код
В коде нужно задать команды на включение и выключение своих периферийных устройств. Опционально — поменять таймаут автовыключения.

Это все располагается в секции переменных.

// Код сна: 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 
#include 
#include 

#define adc_disable() (ADCSRA &= ~(1<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. Я постарался ничего не забыть, но мог. Если так — прошу прощения и приложу все силы, чтобы верно ответить на наводящие вопросы и исправить ошибки.

© Geektimes