13 September 2013

«Achtung!», или Мониторим состояние сборки проекта

Mail.ru Group corporate blogPython

Для сборки проекта, запуска тестов и проверки качества кода мы (в проекте «Календарь Mail.ru») используем Jenkins CI. Запуск сборки происходит сразу же после пуша в репозиторий git (по хуку) и, конечно же, хочется своевременно получать информацию о провалившихся сборках. С одной стороны, уведомления по email вроде бы достаточно, с другой стороны хочется чего-то более заметного и весёлого.

Года три или четыре назад, когда я работал над другим проектом, для схожих целей была куплена мигалка, работающая от сети 220 вольт, для управления ею был приобретён модуль MP709 (USB-реле) небезызвестной фирмы MasterKit. Решение оказалось неудачным: во-первых, поддержки Linux у этого модуля на тот момент не было (пришлось реверсить протокол обмена данными и писать свой «драйвер» под Linux), во-вторых, расположение мигалки ограничивали провода, тянущийся от компьютера и от розетки. Так или иначе, конструкция была заброшена в дальний ящик и ждала своего часа.
Недавно у меня в очередной раз зачесались руки, захотелось сделать что-то интересное, но бесполезное, и я вспомнил про мигалку. Покопавшись в куче хлама я выудил оттуда: Arduino Nano — две штуки, модуль реле — одна штука, приёмник MX-05V — одна штука и передатчик MX-FS-03V — одна штука. Засунув весь остальной хлам в дальний угол на балконе, дожидаться своего следующего часа, я приступил к сборке устройства.
Лирическое отсупление: когда-то, давным-давно, когда я был молодой и у меня было много времени, я бы взял пару микроконтроллеров AVR, вытравил бы плату, спаял бы устройство, запрограммировал бы его на языке ассемблера… И получил бы то же самое на выходе. Удовольствия от процесса больше, результат — тот же.

Hardware


Схема получилась предельно простой, благо в наше время огромное количество различных электронных модулей, конструкторов и блоков позволяет собрать электронное устройство любому человеку, не бравшему ни разу паяльник в руки и не изучавшему схемотехнику и программирование.Передатчик (Arduino Nano + MX-FS-03V)

Приёмник (MX-05V + Arduino Nano + модуль реле):

Принцип работы ещё проще: Arduino подключается к порту USB компьютера и слушает команды через последовательный интерфейс. Если команда валидна, она будет передана через радиопередатчик. Во втором блоке другая Arduino слушает приёмник и при поступлении команды либо включает, либо выключает реле. Всё. Никакой обратной связи, никаких проверок (контрольных сумм), никакой логики.
Программа передатчика выглядит следующим образом:
#include <VirtualWire.h>

int ledPin = 13; // pin with led on it

char incoming_byte; // a buffer to store the incoming messages
char input_data[12]; // the size of the message
int input_size = 0; // counter, just counter

void setup() {
// setup led pin
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);

// initialize serial (9600 bps)
Serial.begin(9600);
Serial.println("Ready for achtung");

// initialize the IO and ISR
vw_set_ptt_inverted(true); // required for DR3100
vw_setup(1200); // bits per sec
}

void loop() {
while (Serial.available() > 0) {
incoming_byte = Serial.read(); // read the incoming byte

if (incoming_byte == 'n') {
input_data[input_size++] = 0;
if (strncmp(input_data, "Achtung ON", 10) == 0 || strncmp(input_data, "Achtung OFF", 11) == 0) {
digitalWrite(ledPin, HIGH);
vw_send((uint8_t *)input_data, input_size);
vw_wait_tx(); // wait until the whole message is gone
digitalWrite(ledPin, LOW);

Serial.println("OK");
} else {
Serial.println("FAIL");
}
while (input_size-- >= 0) {
input_data[input_size] = 0;
}
input_size = 0; // reset counter
} else {
if (input_size <= 11) {
input_data[input_size] = incoming_byte;
}
input_size++;
}
}
}

Программа приёмника:
#include <VirtualWire.h>

int ledPin = 13; // pin with led on it
int relayPin = 3; // pin with relay on it

byte input_data[VW_MAX_MESSAGE_LEN]; // a buffer to store the incoming messages
byte input_size = VW_MAX_MESSAGE_LEN; // the size of the message

void setup() {
// setup led pin
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);

// setup relay pin
pinMode(relayPin, OUTPUT);
digitalWrite(relayPin, HIGH);

// initialize the IO and ISR
vw_set_ptt_inverted(true); // required for DR3100
vw_setup(1200); // bits per sec
vw_rx_start(); // start the receiver
}

void loop() {
// check if we have some data to process
if (vw_get_message(input_data, &input_size)) {
digitalWrite(ledPin, HIGH);
if (strncmp((char*) input_data, "Achtung ON", 10) == 0) {
digitalWrite(relayPin, LOW); // achtung! turn relay on
} else if (strncmp((char*) input_data, "Achtung OFF", 11) == 0) {
digitalWrite(relayPin, HIGH); // turn relay off
} else {
delay(1000);
}
digitalWrite(ledPin, LOW);
}
}

Как видим, всё предельно просто. Подключаем микроконтроллеры, прошиваем, соответственно, приёмник и передатчик с помощью встроенно в программу Arduino загрузчика, запускаем «Serial Monitor», подключенный к передатчику, и передаём команды включения/выключения реле, а так же невалидные команды (для проверки):

Реле включается-выключается, всё работает, всё хорошо.
Самое время разобрать мигалку и посмотреть, куда мы разместим приёмник и реле:

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

Собираем мигалку с нашим приёмником и блоком питания:

Убеждаемся, что всё работает: включаем прибор в сеть и отправляем через «Serial Monitor» в передатчик команду «Achtung ON». Реле щёлкнуло, лампочка загорелась, двигатель закрутился, ура! Отправляем команду «Achtung OFF» — всё остановилось. Мигалка работает. Осталось только прикрепить платы с помощью винтов и китайского термоклея и собрать всю конструкцию в первозданном виде.
С передатчиком я решил не церемониться и просто спаял всё вместе:

В будущем планирую упаковать всё это в красивый корпус.

Software


Для того, чтобы отправлять команды с компьютера в передатчик пишем простой скрипт:
#!/usr/bin/env python
"""
Send 'ON' or 'OFF' command to achtung transmitter.
"""

from __future__ import print_function
import os
import sys
import serial


if __name__ == '__main__':
if len(sys.argv) != 3:
print("Usage: achtung /dev/ttyXX <ON|OFF>")
sys.exit(1)

tty = sys.argv[1]
if not os.path.exists(tty):
print("ERROR: device '%s' is not exists" % tty)
sys.exit(1)

command = sys.argv[2]
if command not in ('ON', 'OFF'):
print("ERROR: unknown command '%s'" % command)
sys.exit(1)

transmitter = serial.Serial(tty, 9600)

if transmitter.readline().strip() != 'Ready for achtung':
print("ERROR: device is not initialized")
sys.exit(1)

transmitter.write("Achtung %sn" % command)

result = transmitter.readline().strip()
if result != 'OK':
print("ERROR: device response is not OK")
sys.exit(1)

Чтобы скрипт работал, необходимо установить зависимость — модуль pyserial:
    pip install pyserial
Запускаем, наслаждаемся включающейся/выключающейся мигалкой:
    python achtung.py /dev/tty.usbserial-A900FYDU ON
    python achtung.py /dev/tty.usbserial-A900FYDU OFF

Для получения статуса сборок в Jenkins CI лучше всего воспользоваться модулем jenkinsapi, в этом случае скрипт получится элементарным. Установка зависимости:
    pip install jenkinsapi
Сам скрипт:
#!/usr/bin/env python
"""
Check for Jenkins jobs status and run alarm if job is not OK.
"""

import os
import sys
import time

import serial
from jenkinsapi.jenkins import Jenkins
from jenkinsapi.exceptions import JenkinsAPIException


URL = 'http://jenkins.example.ru/'
USERNAME = 'jenkins'
PASSWORD = 'secret'
JOBS = ['build', 'test', 'lint']
TTY = '/dev/tty.usbserial-A900FYDU'


def job_failed(connect, job_name):
"""
Returns boolean job status (True - success, False - fail).
"""

if job_name not in connect:
return True

last_build = connect[job_name].get_last_build()

if last_build.is_running():
return False

build_status = last_build.get_status()

if build_status in ('FAIL', 'FAILED', 'FAILURE', 'ERROR', 'REGRESSION'):
return True

return False


def send_to_transmitter(device, command):
"""
Send command to device and die on error.
"""

device.write("Achtung %sn" % command)

result = device.readline().strip()
if result != 'OK':
print("ERROR: device response is not OK")
sys.exit(1)


if __name__ == '__main__':
if not os.path.exists(TTY):
print("ERROR: device '%s' is not exists" % TTY)
sys.exit(1)

try:
connect = Jenkins(URL, USERNAME, PASSWORD)

if any(job_failed(connect, job_name) for job_name in JOBS):
transmitter = serial.Serial(TTY, 9600)

if transmitter.readline().strip() != 'Ready for achtung':
print("ERROR: device is not initialized")
sys.exit(1)

send_to_transmitter(transmitter, 'ON')
time.sleep(5)
send_to_transmitter(transmitter, 'OFF')

except JenkinsAPIException:
sys.exit(1)

Запущенный скрипт подключается к Jenkins, получает статус указанных заданий и, если одно из них провалено, включает мигалку на пять секунд (это сделано для того, чтобы мигание не раздражало, но давало понять, что билд завершён неудачно). Скрипт запускается в кроне каждую минуту с 11:00 до 20:00 в будни (рабочее время наших сотрудников) — нет смысла включать мигалку в ночи или на выходных.
Конечно, можно было не поллить результаты сборок, а настроить Jenkins таким образом, чтобы при неудачной сборке он сам инициировал включение мигалки, но требования безопасности в нашей компании исключают такую возможность: любые коннекты из сети с серверами в офисную сеть категорически запрещены.

Profit!


Всё настроено, работает, и радует всех разработчиков (кроме того, который сфейлил билд) весёлым миганием в самый подходящий момент.
Исходники: https://github.com/dreadatour/jenkins-achtung.

Update: видео работы устройства:
Tags:achtung
Hubs: Mail.ru Group corporate blog Python
+63
24.5k 72
Comments 21
Top of the last 24 hours