Очередной CI светофор. На этот раз attiny2313 и Node.js

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



Под катом светофор из цветомузыки и пластиковых бутылок, USB модуль управления светофором на attiny2313 за доллар, а так же софт для опроса Jenkins и управления USB модулем на Node.js.

Проект был разделен на 3 части

  1. Светофор. Снимать светофор с перекрестка или пытаться купить его мы не собирались. Мы не ставили перед собой цель повесить в офисе настоящий светофор, нам вполне по силам сделать его из подручных материалов.
  2. Электроника. Я сразу отбросил Rasbery Pi и Arduino как избыточные и дорогие решения. Мне нужен был доступ к устройству по USB, я планироал использовать напряжение питания USB в качестве источника питания ламп светофора, то есть избавить схему от дополнительного питания.
  3. Софт для работы с USB на стороне компьютера. Задача: опрос Jenkins текущего статуса определенного билда и отправка соответствующего сообщения по USB.


Светофор


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

Вместо ламп я использовал светодиоды, которые предварительно выпаял из светодиодной ленты. Для большей яркости решил сделать сборки по 4 светодиода, соединенных параллельно и ограничил протекающий через них ток резисторами по 100ом, порты attiny2313 нормально справляются с нагрузкой около 20мА.
Картинка


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


Светодиодные сборки я прикрепил к крышкам от бутылок при помощи болтов М3 и трубок с внутренним диаметром 2мм, сделал это для того, чтобы была возможность отрегулировать расстояние для большей яркости.
Картинка


Еще картинки



Электроника


В наличии у меня были два AVR микроконтроллера: более дешевый attiny13 с 1kB памяти и чуть дороже attiny2313 c 2kB памяти. Для того, чтобы устройство могло работать с USB протоколом, я не собирался усложнять схему дополнительными USB модулями так как это можно сделать прямо на MK при помощи прекрасной библиотеки V-USB.

Минимальный размер памяти, необходимый этой библиотеке, равен около 1.3kB — поэтому выбор пал на attiny2313. Цена этого микроконтроллера 1-2 доллара, купить его тоже не составит никакого труда, так как это очень популярная модель.

Схема электроники

Схема устройства очень простая, для наглядности покажу свою, но в качестве руководства советую изучить возможные варианты, так как здесь предложены 3 варианта схем, описаны их достоинства и недостатки:



Основная часть схемы — это обвязка, необходимая для корректного определения компьютером устройства как USB. Она необходима для изменения напряжения на контактах D+ и D- USB. Дело в том, что напряжение на контактах питания USB и питание моей схемы равно 5в, но для сигналов на D+ и D- напряжение должно быть 3.3в, для этого на них стоят стабилитроны. Подключение светодиодов у меня расположено так только потому, что я паял без предварительной трассировки, по этой же причине я не выкладываю чертежи платы. Так же на схеме присутствует динамик, он необходим для воспроизведения мелодии из этой статьи, когда падает билд.
Готовый USB модуль



Принцип работы:

USB модуль умеет совсем чуть-чуть. Либо светить одним светодиодом, либо моргать одним из них. Звучание мелодии на упавший билд не было задумано изначально, а было добавлено в уже работающее устройство модификацией прошивки.

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

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

Прошивка

Код прошивки я всегда пишу на C в Eclipse, компилятор gcc из набора WinAVR, прошивку заливаю программатором USBasp, купленном на алиекспресс за 4.5$, прошиваю фьюзы программой Khazama.

Начать стоит с прошивки фьюзов. В схеме используется 12MHz кварц и для его использования фьюзы должны быть установлены на кварц с частотой больше 8MHz (CKSEL=1110). Кроме того, нужно не забыть убрать деление частоты на 8 (CKDIV8=1), если я не ошибаюсь, в attiny2313 делитель установлен по дефолту, повторюсь, его нужно отключить (установить единицу). Кварц на 12MHz — не единственный возможный вариант, о возможных вариантах почитайте внизу этой страницы. С таким малым количеством памяти мне стоило выбрать 16MHz, но я решил остановиться на 12MHz.

Первым делом необходимо воспользоваться шаблонным кодом USB библиотеки. Папка usbdrv библиотеки должна быть в папке проекта.

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/wdt.h>
#include <util/delay.h>

#include "usbdrv/usbconfig.h"
#include "usbdrv/usbdrv.h"

PROGMEM const char usbHidReportDescriptor[22] = {
    0x06, 0x00, 0xff,
    0x09, 0x01,
    0xa1, 0x01,
    0x15, 0x00,
    0x26, 0xff, 0x00,
    0x75, 0x08,
    0x95, sizeof(uchar),
    0x09, 0x00,
    0xb2, 0x02, 0x01,
    0xc0
};

int main(void) {
    wdt_enable(WDTO_1S);
    usbInit();
    usbDeviceDisconnect();

    for(uchar i = 0; i<250; i++) { // wait 500 ms
        wdt_reset();
        _delay_ms(2);
    }

    usbDeviceConnect();

    sei();

    while(1) {
        wdt_reset();
        usbPoll();
        _delay_us(100);
    }

    return 0;
}


Этот код необходим для работы USB библиотеки. Кроме этого кода необходимо так же настроить несколько параметров, для этого нужно файл usbconfig-prototype.h в папке usbdrv переименовать в usbconfig.h и подправить необходимые параметры, в моем случае — это:

#define USB_CFG_IOPORTNAME      D
#define USB_CFG_DMINUS_BIT      3
#define USB_CFG_DPLUS_BIT       2
#define USB_CFG_INTERFACE_CLASS     3
#define USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH    22


Здесь стоит отметить, что есть возможность использовать любые ноги МК, но USB_CFG_DPLUS_BIT должен указывать на ногу, поддерживающую внешние прерывания, так как по этой ноге будут приходить сигналы с USB порта.

Метод, обрабатывающий пришедшие данные, выглядит так:

#define PORT_D_THRESHOLD 40 // с какого числа начинать использование порта D
#define SERIALIZE_DEVIDER 10 // 51 / 10 = пин 5 и моргание 1
#define PLAY_ON_STATUS 30 // 30 - это горящий красный, начать воспроизведение мелодии

volatile uint8_t *blinkPort = &PORTB;
uint8_t pin;
uint8_t isBlink;
uint8_t play;

USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
    usbRequest_t *rq = (void *)data; // cast data to correct type
    uint8_t status = rq->wValue.bytes[0]; //40/41 - 20/21 - 30/31 (возможные значения)
    // пишу 0 во все используемые мною пины очищая предыдущие состояния
    // чтобы избежать одновременного свечения нескольких светодиодов
    PORTD &=~ ((1 << PD5) | (1 << PD4));
    PORTB &=~ ((1 << PD2) | (1 << PD3));

    // если в сообщении пины 4 или 5, то работать будем с портом D
    // иначе с портом B
    if(status < PORT_D_THRESHOLD) {
        blinkPort = &PORTB;
    } else {
         blinkPort = &PORTD;
    }

    // десериализация 
    isBlink = status % SERIALIZE_DEVIDER;
    pin = status / SERIALIZE_DEVIDER;
    play = status % PLAY_ON_STATUS == 0;

    return 0;
}


Этот метод отрабатывает при получении устройством данных по USB, здесь я меняю переменные состояния в зависимости от пришедших данных. Дальше эти переменные будут использоваться в главном цикле программы.

Чтение данных происходит из значения wValue, не самый правильный вариант передачи данных, но я посчитал его вполне уместным в моем случае.

В моей схеме для зеленого сигнала светофора используется порт D, а для желтого и красного порт B, поэтому мне пришлось объявить указатель на используемый порт и добавить условие для использования порта D, если пришел сигнал об успешном билде. Если для всех сигналов использовать разные пины одного порта, то от этого условия можно отказаться.

Метод main():

Кроме инициализации USB библиотеки необходимо настроить порты на output, в моем случае нельзя настраивать весь порт D, т.к. порт D будет использоваться USB библиотекой. Я выставил единички только на пинах, которые буду использовать:

DDRD = 0x30; //пины 5 и 4 порта D
DDRB = 0xC; //пины 2 и 3 порта B


Main loop:

Главный цикл начинается с необходимых для работы USB:

wdt_reset();
usbPoll();


Дальше идет логика, отвечающая за свечение/моргание одного из светодиодов:

blinkDelay++;
if(blinkDelay > BLINK_DELAY) {
    blinkDelay = -BLINK_DELAY;
}

if(isBlink == 0) {
    *blinkPort |= (1 << pin);
} else {
    if(blinkDelay == 0) {
        *blinkPort ^= (1 << pin);
    }
}


Если флаг моргания не установлен, то просто пишется единица в необходимый бит
*blinkPort |= (1 << pin)
. Иначе необходимый бит инвертируется
*blinkPort ^= (1 << pin)
, таким образом получается моргание светодиодом. Задержка в цикле всего 100us, поэтому для более редкого моргания добавлена искусственная задержка в виде переменной-счетчика blinkDelay.

Дальше идет блок отвечающий, за воспроизведение мелодии:

if(play == 1) {
    if(soundDelay < duration) {
        if(soundDelay % frequency <= FREQUECY_EDGE) {
            PORTD ^= (1 << PD5);
        }
    } else {
        soundDelay = 0;
        if(note == NOTES_COUNT) {
            note = 0;
            play = 0;
        }

        duration = pgm_read_word_near(durations + note) * DURATION_MULTIPLIER;
        frequency = FREQUENCY_DEV / pgm_read_word_near(frequences + note);

        note++;
    }

    soundDelay++;
}


Из публикации «Музыкальный дверной звонок в стиле Star Wars на Arduino» были взяты только массивы частот и длина нот, как я уже упоминал, памяти оставалось очень мало по этому процесс воспроизведения мелодии интегрирован в общий цикл, чтобы не нарушить работу USB библиотеки. Я решил не делать кастомных задержек для задания нужной частоты и длинны ноты.

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

duration = pgm_read_word_near(durations + note) * DURATION_MULTIPLIER;
frequency = FREQUENCY_DEV / pgm_read_word_near(frequences + note);


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

Софт


После того, как схема собрана, а микроконтроллер прошит (как и чем прошивать ищите в интернетах вашего города), можно перейти к компьютерному софту.

Вариантов здесь предостаточно. Для работы с DIY USB большой популярностью пользуется виртуальный COM порт, та же Arduino создает виртуальный COM порт, работать с которым довольно просто, с ним можно работать даже обычным bat файлом. Но мне очень хотелось попробовать работу именно с USB, поэтому моя прошивка настроена как HID устройство, и софт должен уметь работать именно с USB.

Пишем в USB

После небольшого расследования оказалось, что один из самых простых вариантов — это пакет USB для Node.js. Для того, чтобы отправить сигнал об упавшем билде, достаточно написать 4 строчки:

var usb = require('usb');
var device = usb.findByIds(5824, 1500); // VID и PID нашего устройства (эти стоят по умолчанию в V-USB конфиге)
device.open();
device.controlTransfer(usb.LIBUSB_REQUEST_TYPE_RESERVED, usb.LIBUSB_TRANSFER_TYPE_CONTROL, 30, 0, new Buffer(0), function(error, data){});


Здесь число 30 отсылается параметром wValue на устройство, а то в свою очередь подаст напряжение на третий пин порта B и начнет проигрывание мелодии.

Узнаем статус джобы у Jenkins

С опросом Jenkins у Node.js тоже никаких проблем, подключаем пакет jenkins-api и пишем:

var jenkinsapi = require('jenkins-api');
var JOB_NAME = 'JOB';
var JENKINS_URL = 'URL';
var jenkins = jenkinsapi.init(JENKINS_URL);

jenkins.last_build_info(JOB_NAME, function(err, data){
    console.log('Статус билда', data.result);
});


В консоли увидим: SUCCESS, FAILURE или null (в процессе сборки).

Драйвер для Windows

Для работы с USB модулем необходимо установить драйвер libUSB, это можно сделать либо вручную, либо воспользоваться программой zadig, взятой со страницы пакета USB из пункта Installation; за подробностями — туда, там же инструкции для Linux и OSX.

Всё вместе


Короткое видео:



Полные исходники здесь.

Спасибо за внимание.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 8

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

    Извините, а почему вы не стали использовать аппаратный ШИМ? Это было бы гораздо проще.

    Если верить даташиту, их у вашей тини аж 4 штуки.
    www.atmel.com/images/doc2543.pdf
      0
      О наличии ШИМ знаю но никогда им не пользовался по неопытности. Ваш комментарий заставил серьезно задуматься, возможно я переделаю эту часть на ШИМ ведь звук мелодии получился, мягко говоря, не очень.
        0
        Аппаратный шим это очень хорошее освобождение ресурсов.
        Смысл в том, что когда надо дергать ногой с определёной частотой, вы вынуждены заботится о том, чтобы к нужному времени процессор был свободен.
        Шим же дёргает ногой сам, вы ему лишь задаёте параметры, как дёргать, и ваше внимание нужно только для изменения режима работы.

          0
          Спасибо, про это я уже почитал. Сначала подумал, что на переделки не хватит памяти, ведь сейчас прошивка занимает 2016 байт, любое изменение просто не поместится, но судя по необходимому коду, вариант на PWM должен освободить не только память но и размер программы.
      0
      Мой комментарий относится к статье косвенно, статья интересная и вообще DIY это классно!
      НО:
      У меня при виде Jenkins и подобных утилит возникает только один вопрос: Это как так на «мастер» можно закоммитить/пушить некомпилируемый код?
      Я по работе с таким сталкивался, в основном, в смежных с моим проектом, и каждый раз это выливается в своеобразный ад. У нас есть специальный человек именуемый по шапке раздаватель CM инжернер, который, помимо своих прямых обязаностей по настройке общего окружения для сборки, следит чтобы инженеры, как минимум, не отправляли на мастер/релиз бранч некомпилируемый код. В целом, это какое-то халатное отношение — сабмитить, то что даже скомпилировать нельзя. Может стоит задуматься ЧЯДНТ, если такое происходит, и исправиться? Если не вышло с N+1 попытки, задуматься, а нужен ли такой инженер в команде, который даже компилируемый код написать/заинтегрировать не в состоянии?
      Критика весьма субъективная, но имеет место быть.
        0
        Во-первых, билд валится не только по ошибке компиляции, а еще и вследствие невыполненных тестов, предупреждений от Style Cop (если включена опция «Считать предупреждения ошибками»).

        Во-вторых, когда работаешь с распределенным проектом, в котором несколько модулей делаются разными командами, да еще есть ядро / фреймворк от еще одной команды, то репозиторий кода имеет очень непростой жизненный цикл. Разных стадий бранчи, например. Бывает так, что твой последний код по какой-то причине ссылается на то, что не положено в транк фреймворка или на твоей локальной машине состояние репозитория отличается от оного на машине сборки. Такие ошибки могут случиться и твой локальный билд не свалится, а на сервере свалится.
          0
          У нас в проекте таже ситуация, много подсистем начиная от компилятора, коначая UI и мое сообщение скорее навеяно печаным опытом нежели какими-то здравыми ± самого Jenkins или OBS например. У нас был товарищ, который ломал корневые зависимости для 3 других подсистем по 5 раз на дню. Jenkins красиво показывал красную табличку, для нас означавшую, только одно — делать fetch/merge нельзя. В итоге после недели повторений человеку нашли другое занятие. И красная табличка в Jenkins больше не появлялась. Потому что остальные инженеры выполняют сборку с имеющимися подсистемами в т.ч. unit-тесты до отправки изменений на ревью. Я уже не говорю про сабмит на релизный бранч.
          0
          Все просто, наш проект компилируется в облаке, а мерж и коммит в SVN не всегда происходит без ошибок. Бывает, когда создаются новые классы вылетает из головы добавить их в package.xml, вот билды и падают. Бывает и по другой причине, например, разница в версиях метадаты. Да и светофор — это just for fun.

        Only users with full accounts can post comments. Log in, please.