На хабре предостаточно статей для начинающих о том, какой волшебный и замечательный этот MSP430 LaunchPad от Texas Instruments. Однако дальше стандартной мигалки светодиодом обычно никто не заходит. Пора исправлять эту ситуацию.
Работая в команде, мы пользуемся старым добрым SVN для контроля версий. Казалось бы, причём тут микроконтроллеры?
Как раз для сигнализации очередного коммита в репозиторий я и приспособил эту дивную штуковину.
Идея
Хотелось бы такое устройство, которое будет сигнализировать мелодией об очередном коммите в любой из наших репозиториев. Было бы неплохо, чтобы у каждого разработчика была своя уникальная мелодия, и всем было бы слышно, кто только что сделал очередной коммит.
Проект
В качестве веб фронтенда к SVN прикручен WebSVN, закрытый Basic-авторизацией. Таким образом мы можем получить доступ к списку проектов и их ревизиям. В свою очередь, нам необходим процесс, который по расписанию, например раз в минуту будет проверять репозитории проектов на предмет нового коммита. Так как ближе всего к рукам был PHP, решено было писать на нём. Рабочая система — Windows 7 с Apache 2.1 + PHP 5.3.6.
При обнаружении нового коммита, скрипт посылает на COM-порт управляющую команду устройству, о том что надо проиграть мелодию автора коммита.
Итак, у нас есть WebSVN:
Средствами PHP мы парсим список проектов и заглядываем в каждый проект:
<?
// Забъём на время выполнения
set_time_limit(0);
// Подключим всякие функции и конфиги
require 'functions.php';
require 'config.php';
// Готовим CURL
$s = curl_init();
curl_setopt($s, CURLOPT_URL, $svnURL);
curl_setopt($s, CURLOPT_USERPWD, $authLogin.':'.$authPass);
curl_setopt($s, CURLOPT_USERAGENT, 'CommitBeep 1.0');
curl_setopt($s, CURLOPT_REFERER, $svnURL);
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
// Получаем список проектов
$page = curl_exec($s);
$httpCode = curl_getinfo($s, CURLINFO_HTTP_CODE);
curl_close($s);
// Получим ссылки на проекты
$doc = new DOMDocument();
$doc->loadHTML($page);
$links = $doc->getElementsByTagName('a');
foreach ($links as $link) {
$link = dom2array($link);
if (strpos($link['@attributes']['href'], 'listing.php?repname') !== false) {
$projectName = $link['#text'];
checkSVNCommit($projectName);
}
}
Для того, чтобы авторизоваться через Basic Auth, воспользуемся функциями библиотеки CURL.
Ещё для разбора HTML воспользуемся DOMDocument и DOM.
Т.к. с DOM из PHP работать довольно муторно, а нам достаточно взять все ссылки, воспользуемся функцией dom2array():
/**
* Парсит DOM узел в массив
* @param object $node узел
*/
function dom2array($node) {
$res = array();
if($node->nodeType == XML_TEXT_NODE){
$res = $node->nodeValue;
}
else{
if($node->hasAttributes()){
$attributes = $node->attributes;
if(!is_null($attributes)){
$res['@attributes'] = array();
foreach ($attributes as $index=>$attr) {
$res['@attributes'][$attr->name] = $attr->value;
}
}
}
if($node->hasChildNodes()){
$children = $node->childNodes;
for($i=0;$i<$children->length;$i++){
$child = $children->item($i);
$res[$child->nodeName] = dom2array($child);
}
}
}
return $res;
}
У каждого проекта есть своя страничка, в которой указана последняя ревизия и её автор.
Используя всё тот же CURL, выковыриваем номер ревизии и её автора. Парсинг через explode() и strpos() ужасен, но на скорую руку потянет.
/**
* Проверяем ревизию и вызываем бибикало, если что
* @param string $projectName название проекта
*/
function checkSVNCommit($projectName) {
require 'config.php'; // Конфиг у нас тут подключается локально
$old_rev = @file_get_contents('revisions/'.$projectName.'.txt');
$rev = 0;
$user = 'unknown';
// Готовим CURL
$s = curl_init();
curl_setopt($s, CURLOPT_URL, $svnURL.'/listing.php?repname='.urlencode($projectName).'&path=%2F&sc=0');
curl_setopt($s, CURLOPT_USERPWD, $authLogin.':'.$authPass);
curl_setopt($s, CURLOPT_USERAGENT, 'CommitBeep 1.0');
curl_setopt($s, CURLOPT_REFERER, $svnURL);
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
// Получаем список проектов
$page = curl_exec($s);
$httpCode = curl_getinfo($s, CURLINFO_HTTP_CODE);
curl_close($s);
// Весьма топорная реализация, но зато наглядно и быстро
$page = explode("n", strip_tags($page));
foreach($page as $line) {
if (strpos($line, 'Last modification:') !== false) {
$line = explode(' ', $line);
$rev = $line[3];
$user = $line[5];
}
}
echo "$projectName $rev $user<br/>n";
// Ревизия поменялась? Бибикаем!
if ($rev != $old_rev) {
beep($comNumber, getMelodyByUser($user));
file_put_contents('revisions/'.$projectName.'.txt', $rev);
}
}
Реализация банальная и местами топорная, но прекрасно работает. Читаем из текстового файлика старую ревизию и сравниваем с текущей. Если различаются, записываем и бибикаем.
Первые подводные камни
Я был на 146% уверен, что работа с COM-портами не представляет сложности. Как оказалось, старый способ fopen(«COM6:», «w+») на Windows 7 уже не работает. Не хватает каких-то прав доступа. Кроме того даже если в консоли перенаправить вывод в порт, то опять возникнет ошибка доступа. Так что из cmd (bat) — файлов у нас тоже ничего не получится, и через exec() тоже не прокатит.
Крепкое гугление вывело меня на виндовое расширение — PHP Serial.
Подключается как и все расширения, довольно просто и работает как два пальца:
/**
* Посылает сигнал бибикалу
* @param integer $com номер COM-порта
* @param integer $melody номер мелодии
*/
function beep($com = 6, $melody = 1) {
ser_open("COM".$com, 9600, 8, "None", "1", "None");
ser_write("$melody");
ser_close();
}
Бибикало с микроконтроллером
Настала очередь программировать LaunchPad. Возьмём Energia в качестве среды разработки.
В отличие от Arduino, для MSP430 не так очевидно, как работать с Serial port. Оказывается, сужествует библиотечка TimerSerial, которая является модификацией Arduino-вской базовой библиотеки Serial.
Немного усилий, и можно писать и читать в терминал.
#include <TimerSerial.h>
TimerSerial mySerial;
// Начальные установки
void setup() {
mySerial.begin();
mySerial.println("Welcome to CommitBeep 1.0");
}
// Главный цикл
void loop() {
while (mySerial.available()) {
char inChar = (char)mySerial.read();
mySerial.write(inChar);
delay(100);
}
}
Эта простенькая программка читает символ и тут же пишет его обратно. Как бы эхо.
Подводные камни. Продолжение.
Так, с приёмом-передачей данных разобрались. Теперь бибикало.
Стандартная библиотека Tone, также как-то видоизменена в Energia. И самое неприятное, они никак не дружат с TimerSerial. Вероятно используются общие таймеры или ещё что-то в этом духе, но вместе их использовать не получилось.
Пришлось писать свой велосипед.
#define NOTE_G3 196
#define NOTE_A3 220
#define NOTE_C4 262
int speakerPin;
// Начальные установки
void setup() {
speakerPin = 14; // контакт 1.6 (или 14) у нас подключён к бибикалу
pinMode(speakerPin, OUTPUT);
beep();
}
void playTone(int tone, int duration) {
for (long i = 0; i < duration * 1000L; i += tone * 2) {
digitalWrite(speakerPin, HIGH);
delayMicroseconds(tone);
digitalWrite(speakerPin, LOW);
delayMicroseconds(tone);
}
digitalWrite(speakerPin, LOW);
}
// Бибикало
void beep() {
int melody[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3};
int noteDurations[] = {4,8,8,4,4};
for (int thisNote = 0; thisNote < 4; thisNote++) {
int noteDuration = 1000/noteDurations[thisNote];
playTone(melody[thisNote], noteDuration);
}
}
Все заморочки решены, осталось написать код. Вообщем-то остальное довольно просто.
Объединяем и получаем наш скетч:
#include <TimerSerial.h>
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978
TimerSerial mySerial;
int speakerPin;
// Начальные установки
void setup() {
speakerPin = 14; // контакт 1.6 (или 14) у нас подключён к бибикалу
mySerial.begin();
mySerial.println("Welcome to CommitBeep 1.0");
pinMode(speakerPin, OUTPUT);
}
void playTone(int tone, int duration) {
for (long i = 0; i < duration * 1000L; i += tone * 2) {
digitalWrite(speakerPin, HIGH);
delayMicroseconds(tone);
digitalWrite(speakerPin, LOW);
delayMicroseconds(tone);
}
digitalWrite(speakerPin, LOW);
}
// Бибикало
void beep(int melody) {
switch ((int)melody) {
case '2': {
int mel[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3};
int noteDurations[] = {4,8,8,4,4};
for (int thisNote = 0; thisNote < 4; thisNote++) {
int noteDuration = 1000/noteDurations[thisNote];
playTone(mel[thisNote], noteDuration);
}
break;
}
case '3': {
int mel[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3};
int noteDurations[] = {4,8,8,4,4};
for (int thisNote = 0; thisNote < 4; thisNote++) {
int noteDuration = 1000/noteDurations[thisNote];
playTone(mel[thisNote], noteDuration);
}
break;
}
default: {
int mel[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3};
int noteDurations[] = {4,8,8,4,4};
for (int thisNote = 0; thisNote < 4; thisNote++) {
int noteDuration = 1000/noteDurations[thisNote];
playTone(mel[thisNote], noteDuration);
}
}
}
}
// Главный цикл
void loop() {
while (mySerial.available()) {
char inChar = (char)mySerial.read();
beep(inChar);
delay(100);
}
}
Подключаем, заливаем, открываем окошко терминала, пишем туда 1, и слушаем дефолтную мелодию.
Планировщик задач
Как ни странно, но мы будем использовать именно планировщик задач для фонового выполнения нашего PHP скрипта.
Напишем CMD-файлик, который вызывает PHP и сам скрипт:
D:denwerusrbinphp5.exe D:denwerhometestwwwcommit_beepindex.php
А чтобы это хозяйство не мозолило глаза чёрным окном консоли, воспользуемся нехитрой утилитой hidcon от Андрея Гречкина.
Последние штрихи
Само по себе всё работает и усердно пищит при каждом коммите. Но на столе выглядит не очень красиво. Осталось обернуть нашу новую железку в приятный пластиковый корпус, поработав чуток канцелярским ножом и изолентой:
Итоги
Один день на разработку от идеи до готового устройства. Интересная и увлекательная борьба с подводными камнями и знакомство с Energia и MSP430.
Бюджет проекта ~200 руб.
Автор: pewpew