"Умный дом" на Arduino - часть вторая
Продолжение «Умного дома» на базе Arduino.
Перед прочтением необходимо ознакомиться с первой частью.
В этой части будет описано, плавное управление освещением (диммер, далее ШИМ) и сохранение значений в энергонезависимую память EEPROM.
То есть, после отключения питания и последующего включения, включится то, что работало до обесточивания.
Использование EEPROM полезно там, где существуют перебои с энергоснабжением (например дача).
Посмотреть, понажимать и подвигать можно здесь...
Видео
Управлял с ipadа, было крайне не удобно. И почему-то не видно зелёных кругов, а они есть )))
Нажатие на кнопки будет включать/отключать соответствующие пины, а управление ползунками будет увеличивать/уменьшать ШИМ на D5 и D6.
Внутри индикаторов расположены полукруглые кнопки с помощью которых можно мгновенно отключить и включить ШИМ. При включении вернётся то значение ШИМа, которое было установлено при отключении.
По ходу статьи, будет даваться более детальное описание.
На всякий случай:
Установка OpenWrt на роутер.
Установка сервера Lighttpd.
Подключение Ардуино к роутеру.
Все действия справедливы для любого роутера с OpenWrt или компьютера с
Ардуина...
В первую очередь нужно обнулить EEPROM. Прошейте в ардуино этот скетч:
#include <EEPROM.h>
void setup()
{
// write a 0 to all 512 bytes of the EEPROM
for (int i = 0; i < 512; i++)
EEPROM.write(i, 0);
// turn the LED on when we're done
digitalWrite(13, HIGH);
}
void loop()
{
}
Теперь все ячейки EEPROM равны нулю.
Залейте в ардуину основной скетч:
#include <EEPROM.h>
byte d2 = EEPROM.read(2); // флаги (состояние пинов) хранится в EEPROM, считываем их
byte d3 = EEPROM.read(3);
byte d4 = EEPROM.read(4);
int shim1 = EEPROM.read(5); // значение ШИМ хранится в EEPROM, считываем их
int shim2 = EEPROM.read(6);
byte d11 = EEPROM.read(11);
byte d12 = EEPROM.read(12);
byte d13 = EEPROM.read(13);
byte descript[5]; // массив
void setup()
{
Serial.begin(57600);
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
pinMode(6, OUTPUT);
pinMode(11, OUTPUT);
pinMode(12, OUTPUT);
pinMode(13, OUTPUT);
if(d2) digitalWrite(2, HIGH); else digitalWrite(2, LOW); // если до перезагрузки d2 была включена, то включаем, если нет, то нет
delay(500); // чтобы не включалось всё сразу, делаем паузы
if(d3) digitalWrite(3, HIGH); else digitalWrite(3, LOW);
delay(500);
if(d4) digitalWrite(4, HIGH); else digitalWrite(4, LOW);
delay(500);
analogWrite(5, shim1 * 2.55); // включаем ШИМ d5
delay(500);
analogWrite(6, shim2 * 2.55); // включаем ШИМ d6
delay(500);
if(d11) digitalWrite(11, HIGH); else digitalWrite(11, LOW);
delay(500);
if(d12) digitalWrite(12, HIGH); else digitalWrite(12, LOW);
delay(500);
if(d13) digitalWrite(13, HIGH); else digitalWrite(13, LOW);
}
void loop()
{
if (Serial.available()>4) // ждём дескриптор и нужный символ
{
if (Serial.read()=='Y') // проверяем первый символ, если это 'Y', то продолжаем принимать, если нет, то выходим из цикла чтения
{
for (byte i=0; i < 5; i++)
{
descript[i] = Serial.read(); // добавляем символы в массив
}
if((descript[0] =='+') && (descript[1] =='=') && (descript[2] =='Z')) // проверяем дескриптор
{
switch (descript[3])
{
case 'o': // обновление
glavnaia(); // отправка ответа
break;
case 'A': // d2 вкл
digitalWrite(2, HIGH); // вкл d2
d2 = 1; // ставим флаг в единицу (вкл)
EEPROM.write(2, d2); // записываем состояние d2 в ячейку №2 EEPROM
glavnaia(); // отправка ответа
break;
case 'a': // d2 откл
digitalWrite(2, LOW); // откл d2
d2 = 0; // ставим флаг в ноль (откл)
EEPROM.write(2, d2); // записываем состояние d2 в ячейку №2 EEPROM
glavnaia(); // отправка ответа
break;
case 'B': // d3
digitalWrite(3, HIGH);
d3 = 1;
EEPROM.write(3, d3);
glavnaia();
break;
case 'b': // d3
digitalWrite(3, LOW);
d3 = 0;
EEPROM.write(3, d3);
glavnaia();
break;
case 'C': // d4
digitalWrite(4, HIGH);
d4 = 1;
EEPROM.write(4, d4);
glavnaia();
break;
case 'c': // d4
digitalWrite(4, LOW);
d4 = 0;
EEPROM.write(4, d4);
glavnaia();
break;
case 'D': // d5 прибавляем shim1
shim1++; // прибавляем
if(shim1 > 100) shim1 = 100; // если больше ста, то будет сто
EEPROM.write(5, shim1); // записываем значение в ячейку №5 EEPROM
analogWrite(5, shim1 * 2.55); // включаем ШИМ D5
glavnaia(); // функция отправки ответа
break;
case 'd': // d5 убавляем shim1
shim1--;
if(shim1 < 1) shim1 = 0;
EEPROM.write(5, shim1);
analogWrite(5, shim1 * 2.55);
glavnaia();
break;
case 'E': // d6 прибавляем shim2
shim2++;
if(shim2 > 100) shim2 = 100;
EEPROM.write(6, shim2);
analogWrite(6, shim2 * 2.55);
glavnaia();
break;
case 'e': // d6 убавляем shim2
shim2--;
if(shim2 < 1) shim2 = 0;
EEPROM.write(6, shim2);
analogWrite(6, shim2 * 2.55);
glavnaia();
break;
case 'F': // мгновенное включение ШИМ на D5
shim1 = EEPROM.read(5); // считываем значение ШИМ из EEPROM
analogWrite(5, shim1 * 2.55); // включаем ШИМ D5
glavnaia();
break;
case 'f': // мгновенное отключение ШИМ на D5
shim1 = 0;
analogWrite(5, shim1); // отключаем ШИМ D5, но НЕ записываем в EEPROM
glavnaia();
break;
case 'G': // мгновенное включение ШИМ на D6
shim2 = EEPROM.read(6); // считываем значение ШИМ из EEPROM
analogWrite(6, shim2 * 2.55); // включаем ШИМ D6
glavnaia();
break;
case 'g': // мгновенное отключение ШИМ на D6
shim2 = 0;
analogWrite(6, shim2); // отключаем ШИМ D6, но НЕ записываем в EEPROM
glavnaia();
break;
case 'J': // d11
digitalWrite(11, HIGH);
d11 = 1;
EEPROM.write(11, d11);
glavnaia();
break;
case 'j': // d11
digitalWrite(11, LOW);
d11 = 0;
EEPROM.write(11, d11);
glavnaia();
break;
case 'K': // d12
digitalWrite(12, HIGH);
d12 = 1;
EEPROM.write(12, d12);
glavnaia();
break;
case 'k': // d12
digitalWrite(12, LOW);
d12 = 0;
EEPROM.write(12, d12);
glavnaia();
break;
case 'M': // d13
digitalWrite(13, HIGH);
d13 = 1;
EEPROM.write(13, d13);
glavnaia();
break;
case 'm': // d13
digitalWrite(13, LOW);
d13 = 0;
EEPROM.write(13, d13);
glavnaia();
break;
default:
glavnaia();
}
}
else // если дескриптор ложный, то очищаем буфер
{
for(byte i=0; i < 255; i++)
{
Serial.read();
}
}
} // конец if (Serial.read()=='Y')
} // конец чтение порта
} // конец loop
void glavnaia() // отправка данных
{
Serial.print(d2);//0
Serial.print(",");
Serial.print(d3);//1
Serial.print(",");
Serial.print(d4);//2
Serial.print(",");
Serial.print(0);//3 // пока отключаем, потом пригодится
Serial.print(",");
Serial.print(0);//4 // пока отключаем, потом пригодится
Serial.print(",");
Serial.print(0);//5 // пока отключаем, потом пригодится
Serial.print(",");
Serial.print(0);//6 // пока отключаем, потом пригодится
Serial.print(",");
Serial.print(0);//7 // пока отключаем, потом пригодится
Serial.print(",");
Serial.print(0);//8 // пока отключаем, потом пригодится
Serial.print(",");
Serial.print(d11);//9
Serial.print(",");
Serial.print(d12);//10
Serial.print(",");
Serial.print(d13);//11
Serial.print(",");
Serial.print(shim1); // 12 отсылается
Serial.print(",");
Serial.println(shim2); // 13 отсылается 14 значений разделённых запятой
}
Протокол обмена данными с ардуиной описан здесь.
Как работает
Кнопки:
Нажатие на кнопку (например D13) включит светодиод и запишет в EEPROM единицу. В веб-интерфейс отправится флаг 1 сообщающий о том, что включение выполнено. Кнопка подсветится.
При повторном нажатии, светодиод погаснет и в EEPROM запишется ноль. В веб-интерфейс отправится флаг 0 сообщающий о том, что отключение выполнено. Кнопка поменяет цвет.
Говоря иными словами, в веб-интерфейсе отображается только гарантированно выполненная команда.
...
case 'M': // d13
digitalWrite(13, HIGH); // включили
d13 = 1; // установили флаг
EEPROM.write(13, d13); // записали его в память
glavnaia(); // функция отправки ответа
break;
case 'm': // d13
digitalWrite(13, LOW);
d13 = 0;
EEPROM.write(13, d13);
glavnaia();
break;
...
Если включить D13 и обесточить ардуину, то при последующем включении ардуина прочитает соответствующую ячейку памяти:
...
byte d13 = EEPROM.read(13);
...
И если там была единица, то в блоке void setup(), автоматически включится светодиод:
...
delay(500);
if(d13) digitalWrite(13, HIGH); else digitalWrite(13, LOW);
Задержка перед включением нужна для того, чтоб не включались все потребители одновременно (например это дача, и ардуиной управляются обогреватели в разных комнатах).
Чтобы отключить «автоматическое» управление, например для пина d2, надо в начале кода переправить это:
byte d2 = EEPROM.read(2);
на это:
byte d2 = 0;
В void setup() убрать строки:
...
if(d2) digitalWrite(2, HIGH); else digitalWrite(2, LOW);
delay(500);
...
И в блоке switch (descript[3]) закомментировать запись в EEPROM, вот так:
...
////////////// Кнопки ///////////////////
case 'A': // d2 вкл
digitalWrite(2, HIGH); // вкл d2
d2 = 1; // ставим флаг в единицу (вкл)
//EEPROM.write(2, d2); // записываем состояние d2 в ячейку №2 EEPROM
glavnaia(); // отправка ответа
break;
case 'a': // d2 откл
digitalWrite(2, LOW); // откл d2
d2 = 0; // ставим флаг в ноль (откл)
//EEPROM.write(2, d2); // записываем состояние d2 в ячейку №2 EEPROM
glavnaia(); // отправка ответа
break;
...
Диммер:
Диапазон значений ШИМ от 0 до 255, ардуина получает (от клиента) значения в диапазоне от 0 до 100, которые внутри программы умножаются на 2.55 и выводятся на «ножку».
case 'D': // d5 прибавляем shim1
shim1++; // прибавили
if(shim1 > 100) shim1 = 100; // если больше ста, то будет сто
EEPROM.write(5, shim1); // записали значение в память
analogWrite(5, shim1 * 2.55); // зажгли лампочку
glavnaia(); // функция отправки ответа
break;
case 'd': // d5 убавляем shim1
shim1--;
if(shim1 < 1) shim1 = 0;
EEPROM.write(5, shim1);
analogWrite(5, shim1 * 2.55);
glavnaia();
break;
Когда Вы сдвигаете ползунок в веб-интерфейсе, то в ардуину отсылается команда увеличить/уменьшить ШИМ на единицу (и так далее пока двигаете ползунок). Переменная shim1++; увеличивается, её значение помещается в память, и на пин подаётся shim1 умноженый на 2.55.
После этого значение shim1 отсылается обратно в веб-интерфейс и присваиваются индикатору и ползунку.
То есть, индикатору и ползунку будет присвоено то значение, которое гарантированно выполнено ардуиной.
Если часть данных потеряется, ползунок сам отодвинется.
Если нажать на кнопку внутри индикатора:
То в ардуину отправится команда обнуляющая значение shim1
case 'F': // мгновенное включение ШИМ на D5
shim1 = EEPROM.read(5); // считываем значение ШИМ из EEPROM
analogWrite(5, shim1 * 2.55); // включаем ШИМ D5
glavnaia();
break;
case 'f': // мгновенное отключение ШИМ на D5
shim1 = 0;
analogWrite(5, shim1); // отключаем ШИМ D5, но НЕ записываем в EEPROM
glavnaia();
break;
При этом, в память ничего НЕ записывается, а нажатие на соседнюю кнопку вернёт значение из памяти.
(так удобнее отключать свет, нежели ползунок сдвигать)
Веб...
Скачайте архив и распакуйте его в рабочую папку сервера (по умолчанию это /var/www), как то так – /var/www/knopki_shimpolz (у вас может быть своя папка).
В браузере зайдите по адресу ваш_роутер/knopki_shimpolz/. Должна появиться вот такая картинка:
Как это работает
Для наглядности откройте файл index.html из архива, и почитайте комментарии.
Работа кнопок описана в предыдущей части.
Диммер:
При первой загрузки страницы, срабатывает функция обновления — show(); (в дальнейшем она работает с установленым интервалом) и у ардуины вместе с другими данными запрашиваются значения ШИМ:
/*обновление*/
show();
setInterval(show,2000); /* частота обновления в милисекундах */
function show(){ /* функция обновления */
if(flagobnov == 1) { /* это флаг нужен для временного отключения обновления */
$.ajax({
type: "GET",
url: "box2.php?df=o", /* отправка символа о */
timeout:200, /* время (мс), в течении которого функция будет ждать ответа от сервера */
cache: false,
success: function(data){
var vars = data.split(","); /* разбор строки принятой от ардуино */
if(vars.length == dlina){ /* проверка длины данных (количество блоков разделённых запятой) */
/*d2*/
if(vars[0] == 1) { $(".d2otkl").show(); $(".d2vkl").hide(); } /* в зависимости от принятого флага скрывает/показавыет кнопку вкл или откл */
else if(vars[0] == 0) { $(".d2otkl").hide(); $(".d2vkl").show(); }
/*d3*/
if(vars[1] == 1) { $(".d3otkl").show(); $(".d3vkl").hide(); }
else if(vars[1] == 0) { $(".d3otkl").hide(); $(".d3vkl").show(); }
...
shim1 = vars[12]; /* получаем значение ШИМ */
sh1(); /* и выводим его на первый индикатор */
shim2 = vars[13];
sh2();
...
После получения значения shim1, программа переходит в функцию sh1();
function sh1(){ /* рисование первого индикатора */
var $ppc = $('.progress-pie-chart'),
percent = shim1,
deg = 360*percent/100;
if (percent > 50) {
$ppc.addClass('gt-50');
}
else $ppc.removeClass('gt-50');
$('.ppc-progress-fill').css('transform','rotate('+ deg +'deg)');
$('.ppc-percents span').html(percent+' % D5 '); /* название на кнопке - D5 */
sl1();
}
Значение shim1 выводится на индикатор (зелёный кружок) и работа передаётся в функцию sl1();
Функция sl1(); устанавливает ползунок в соответствии со значением shim1
function sl1(){ /* первый слайдер */
$( "#slider" ).slider({
value : shim1,
min : 0,
max : 100,
step : 1,
slide: function( event, ui ) {
...
Теперь функция slide: function( event, ui ) ожидает движения ползунка.
Когда ползунок будет сдвинут в ту или иную сторону на одно деление, сработает следующий алгоритм:
Отключается обновление ⇨
flagobnov = 0;
Проверяется в какую сторону сдвинут ползунок (в большую или меньшую) ⇨
if( ui.value > shim1 ){
else if( ui.value < shim1 ){
В ардуину отправляется символ указывающий увеличить (уменьшить) ШИМ на единицу ⇨
$.ajax({
type: "GET",
url: "box2.php?df=D", /* говорим ардуине что надо увеличить ШИМ на единицу */
Получаем новое значение ШИМ от ардуины и вызывает функцию отрисовки индикатора (sh1();) с новым значением ⇨
shim1 = vars[12]; /* получаем от ардуины ответ с новым значением ШИМ */
sh1(); /* и выводим значение ШИМа на индикатор */
Включаем обновление ⇨
flagobnov = 1;
Функция (sh1();) в свою очередь отрисовывает индикатор и передаёт управление в функцию (sl1();).
Функция (sl1();) устанавливает ползунок в соответствии с новым значением ШИМ и ожидает очередного движение ползунка.
Код целиком:
...
function sh1(){ /* рисование первого индикатора */
var $ppc = $('.progress-pie-chart'),
percent = shim1,
deg = 360*percent/100;
if (percent > 50) {
$ppc.addClass('gt-50');
}
else $ppc.removeClass('gt-50');
$('.ppc-progress-fill').css('transform','rotate('+ deg +'deg)');
$('.ppc-percents span').html(percent+' % D5 '); /* название на кнопке - D5 */
sl1();
}
function sl1(){ /* первый слайдер */
$( "#slider" ).slider({
value : shim1,
min : 0,
max : 100,
step : 1,
slide: function( event, ui ) { /* отправили новое значение в арду, получили его обратно, отправили на индикатор и отдуда вернули сюда чтоб установить слайдер */
flagobnov = 0; /* пока таскаем ползунок отключаем обновление, чтоб не засорять "эфир" */
if( ui.value > shim1 ){ /* если потащили ползунок в большую сторону, то увеличиваем ШИМ */
$.ajax({
type: "GET",
url: "box2.php?df=D", /* говорим ардуине что надо увеличить ШИМ на единицу */
timeout:200,
cache: false,
success: function(data){
var vars = data.split(",");
if(vars.length == dlina)
{
shim1 = vars[12]; /* получаем от ардуины ответ с новым значением ШИМ */
sh1(); /* и выводим значение ШИМа на индикатор */
}
}
});
}
else if( ui.value < shim1 ){ /* если потащили ползунок в меньшую сторону, то уменьшаем ШИМ */
$.ajax({
type: "GET",
url: "box2.php?df=d", /* говорим ардуине что надо уменьшить ШИМ на единицу */
timeout:200,
cache: false,
success: function(data){
var vars = data.split(",");
if(vars.length == dlina)
{
shim1 = vars[12]; /* получаем от ардуины ответ с новым значением ШИМ */
sh1(); /* и выводим значение ШИМа на индикатор */
}
}
});
}
flagobnov = 1; /* включаем обновление */
}
});
}
...
Значение индикатора и позиция ползунка, гарантированно соответствуют значению в ардуине.
Нажатие на кнопку «Мгновенное отключение ШИМ» отправляет в ардуину команду обнулить ШИМ, а кнопка «Мгновенное включение ШИМ» запросит у ардуины значение ШИМ, которое было до обнуления.
/* d5 ШИМ */
/*мгновенное включение ШИМ на D5*/
$(".d5shimvkl").click(function(){
$.ajax({
type: "GET",
url: "box2.php?df=F",
timeout:200,
cache: false,
success: function(data)
{
var vars = data.split(",");
if(vars.length == dlina)
{
shim1 = vars[12]; /* получаем от ардуины ответ с новым значением ШИМ */
sh1(); /* и выводим значение ШИМа на индикатор */
}
}
});
return false;
});
/*мгновенное отключение ШИМ на D5*/
$(".d5shimotkl").click(function(){
$.ajax({
type: "GET",
url: "box2.php?df=f",
timeout:200,
cache: false,
success: function(data)
{
var vars = data.split(",");
if(vars.length == dlina)
{
shim1 = vars[12]; /* получаем от ардуины ответ с новым значением ШИМ */
sh1(); /* и выводим значение ШИМа на индикатор */
}
}
});
return false;
});
Дизайн
Позиция индикаторов, их цвет и шрифт меняется в файле shim.css
/* первый кружок */
.progress-pie-chart {
width: 200px;
height: 200px;
top: 90px; /* позиция */
left: 80px; /* позиция */
border-radius: 50%;
background-color: #E5E5E5;
position: absolute;
}
.ppc-percents span {
display: block;
font-size: 26px; /*размер текста на кружках*/
font-weight: 600; /*ширина текста на кружках*/
font-family: Arial, Helvetica, sans-serif; /*шрифт*/
color: #161616; /*цвет текста на кружках*/
text-shadow: 0px 1px 2px #7c7c7c; /*цвет и размер тени текста*/
}
Размер и позицию ползунков можно менять в файле slai.css
.ui-slider {
position: relative;
width: 200px; /* ширина ползунка */
text-align: left;
outline: none;
}
...
/* кнопка ползунка */
.ui-slider-horizontal .ui-slider-handle {
width: 50px; /*размер кнопки ползунка*/
height: 50px;
margin-left: -25px;
outline: none;
box-shadow: 0 0 10px 3px rgba(0,0,0,0.3);
border-radius: 4px;
border: 1px solid #2b2c2b;
cursor: pointer;
}
...
/*первый ползунок*/
s1 {
position: absolute;
top: 360px;
left: 80px;
font-size: 26px; /*размер текста на кнопках*/
font-weight: 600; /*ширина текста на кнопках*/
font-family: Arial, Helvetica, sans-serif; /*шрифт*/
color: #161616; /*цвет текста на кнопках*/
text-shadow: 0px 1px 2px #7c7c7c; /*цвет и размер тени текста*/
}
/*второй ползунок*/
s2 {
position: absolute;
top: 360px;
left: 420px;
font-size: 26px; /*размер текста на кнопках*/
font-weight: 600; /*ширина текста на кнопках*/
font-family: Arial, Helvetica, sans-serif; /*шрифт*/
color: #161616; /*цвет текста на кнопках*/
text-shadow: 0px 1px 2px #7c7c7c; /*цвет и размер тени текста*/
}
На этом вторая часть закончена, в следующей части рассмотренно подключение температурных датчиков и включение/отключение различных устройств по температуре, а так же добавлен спящий режим для веб-интерфейса.
Вот тут можно скачать библиотеку для разгона Arduino.
Телеграм-чат istarik
- +761
- stD
19599
Поддержать автора
Комментарии (13)
Вопрос, отключили вечером эл-во, все погасло, прошло 4-5 часов, все легли спать и тут загорается свет, все проснулись.
Или свет дали на следующий день, когда все разошлись и он будет гореть до вечера.
Чтобы отключить «автоматическое» управление, например пина d2 надо:
В начале кода, переправить это:
на это:
В void setup() убрать строки:
И в блоке switch (descript[3]) закомментировать запись в EEPROM, вот так:
Поскольку это шаблон, я решил добавлять функционал поэтапно, чтоб код с самого начала не выглядел громоздким. Позже, включение/отключение "автоматики" можно будет делать из интерфейса.
Делаю все по инструкции, и столкнулся с такой проблемой: при вызове любой функции с ШИМ, вешается почти наглухо роутер.
Вводу через SSH команду «top» и вижу 100% загрузку процессора. При этом появляется множество соединений к Lighttpd.
Подскажите, куда копать?
Получается вот что, когда Вы двигаете ползунок, в роутер очень часто летят пакеты (прибавление/убавление на единицу — это очередной пакет) и если ардуина не успела обработать один пакет и отдать ответ файлу box2.php, то он ждёт ответа (таймаут то в 150мс не сработал), а следующий пакет открывает ещё одно соединение, которое упирается в то, что открыто предыдущим и т.д. Количество соединений начинает расти как снежный ком и всё виснет.
/etc/php.ini у Вас отредактирован как здесь описано?
Минимальный таймаут 1 сек. — меньше не получится.
На полноценном компе такое не наблюдается.
В общем, «ШИМ» надо переделывать. Как только всё исправлю, так сразу выложу.
Спасибо за столь подробные и нужные инструкции!
Пытался забороть веб сервер и что-то никак( Не могу найти причину по которой веб сервер отказывается отображать кнопки.
вводная
open wrt — CHAOS CALMER (15.05, r46767)
Вот так это выглядит
…
Рекомендую Вам обратить внимание на эту статью.
Статью я видел, но у меня не устанавливаются пакеты arduread и arduserver. Ругается на несоответствие архитектуры (роутер Asus RT-N16). Так как возможностей железа хватает чтобы развернуть lighhtpd c плюшками не стал особо упираться.
Зато на uhttpd все заработало)))
единственное… подскажите как работать с файлами arduserver? чем их редактировать и обирать?
И еще раз спасибо за проделанную работу!
…
arduserver и arduread — это бинарники, исходники выложены в статье. Редактировать в любом текстовом редакторе, собирать компилятором.
…
Пожалуйста.
firefox — Ie — lunascape