====== Карта офисной активности ======
{{ :projects:activitymap:assembled.jpg?direct&700 }}
===== Что это такое? =====
Сегодня мы разберёмся, как объединить дюжину ардуин в одну сеть и не пуститься по миру. Для выполнения этой сложнейшей задачи мы воспользуемся RS485-шилдами, которые позволяют развернуть сеть типа [[wpru>Шина_(топология_компьютерной_сети) | «общая шина»]]. Главным преимуществом такой сети является дешевизна развёртывания: вам не требуется прокладывать кабели к каждому узлу от маршрутизатора, да и сам этот маршрутизатор не требуется.
===== Что нам понадобится? =====
{{ :projects:activitymap:whats_need.jpg?direct&700 |}}
Ведущее устройство:
- [[amp>product/arduino-leonardo?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Arduino Leonardo]]
- [[amp>product/arduino-rs485-shield?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | DF Robot RS485-шилд]]
- [[amp>product/power-supply-adapter-robiton-tn1000s?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Импульсный блок питания]]
- [[amp>product/usb-cable-micro?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Кабель Micro USB]]
Ведомое устройство:
- [[amp>product/arduino-uno?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Arduino Uno]]
- [[amp>product/arduino-rs485-shield?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | DFRobot RS485-шилд]]
- [[amp>product/breadboard-mini?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Breadboard Mini]]
- [[amp>product/wire-mm?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Провода «папа-папа»]]
- [[amp>product/infrared-range-meter-150?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Инфракрасный дальномер Sharp (20-150 см)]] × 2 шт
Также для прокладки сети вам потребуется медный провод сечением 0,5–1 мм.
===== Как это собрать? =====
=== Сборка ведущего устройства ===
{{ :projects:activitymap:master-assembled.jpg?direct&700 |}}
- Установите RS485-шилд на Arduino Leonardo.
- Подключите импульсный источник питания к Arduino Leonardo.
- Подключите линии «A» и «B» сети RS485 к клеммнику.
- Подключите линии «GND» и «VIN» сети RS485 к соответствующим выводам Arduino.
- Подключите USB-кабелем плату Arduino к ПК.
=== Сборка ведомого устройства ===
{{ :projects:slave-assembled.jpg?direct&700 |}}
- Установите Troyka-шилд на Arduino Uno.
- Установите RS485-шилд на Troyka-шилд.
- Подключите дальномеры к аналоговым тройкам A0 и A1.
- Подключите линии «A» и «B» сети RS485 к клеммнику.
- Подключите линии «GND» и «VIN» сети RS485 к соответствующим выводам Arduino.
=== Прокладка сети ===
- Воспользуйтесь витой парой или скрутите два провода в витую пару при помощи шуруповёрта. {{ :projects:activitymap:do-a-wire.jpg?direct&700 |}}
- Протяните витую пару так, чтобы от неё до каждого из ваших устройств было расстояние не более 1 м.
- Соедините каждое устройство с линией. Разрывать её не обязательно. Можно воспользоваться таким способом: {{ :projects:activitymap:connect-wires.jpg?direct&300 |}} В итоге у вас должна получиться приблизительно такая сеть:{{ :projects:activitymap:scheme.png?direct&700 |}}
- На всех платах установите переключатель выбора режима работы в ручной режим и включите передатчик (переключатель ON/OFF).
===== Исходный код =====
==== Визуализация на Processing ====
// Нам нужна работа с последовательным портом
import processing.serial.*;
Serial port;
// Множитель размеров комнат. Нужен для простого масштабирования размеров
int roomScale = 3;
// Координата начала рисования по X и по Y
int drawStart = 75;
// ширина дверного проёма
int doorwayWidth = 15 * roomScale;
// массив ширин комнат
int[] roomWidth = {
130 * roomScale, 60 * roomScale, 25 * roomScale, 85 * roomScale, 130 * roomScale
};
// массив высот комнат
int[] roomHeight = {
100 * roomScale, 90 * roomScale, 90 * roomScale, 85 * roomScale, 75 * roomScale
};
// Количество человек в комнате хранится в этом массиве
Integer[] persons = {
0, 0, 0, 0
};
// У нас три дверных проёма. Текущее состояние каждого проёма
// хранится здесь. 0 - без изменения, 1 - кто-то вышел, 2 - кто-то вошёл
int[] doorwayState = {
0, 0, 0
};
// Эта функция выполняется один раз при старте скетча
void setup()
{
// размер окна
size(800, 700);
// создаём последовательное соединение с Arduino на 17-м COM-порту
port = new Serial(this, "COM17", 9600);
// Данные из COM-порта буферизируются до тех пор, пока не придёт '\n'
port.bufferUntil('\n');
// Координаты текстового поля задаются относительно центра текста
textAlign(CENTER, CENTER);
// размер текста
textSize(50);
}
// Эта функция вызывается каждый раз, когда пришло время рисовать кадр
// Аналог loop() в Arduino
void draw()
{
// Закрашиваем всё окно чёрным цветом
background(0);
// Рисуем комнаты
drawRooms();
}
void drawRooms()
{
// задаём белый цвет линий
stroke(255);
// ширина линий 10 px
strokeWeight(10);
// заливка объектов - серая
fill(150);
// рисуем прямоугольники комнат
rect(drawStart, drawStart, roomWidth[0], roomHeight[0]);
rect(drawStart + roomWidth[0], drawStart, roomWidth[1], roomHeight[1]);
rect(drawStart + roomWidth[0] + roomWidth[1], drawStart, roomWidth[2], roomHeight[2]);
rect(drawStart + roomWidth[0], drawStart + roomHeight[1], roomWidth[3], roomHeight[3]);
rect(drawStart, drawStart + roomHeight[0], roomWidth[4], roomHeight[4]);
// проверка состояния дверных проёмов в функции checkDoorway()
// если кто-то прошёл
if (checkDoorway(0)) {
//дверной проём будет красным
stroke(255, 0, 0);
} else {
// иначе - тёмно-серым
stroke(50);
}
// рисуем проём
line(drawStart + roomWidth[0],
drawStart + roomHeight[0] * 0.6,
drawStart + roomWidth[0],
doorwayWidth + drawStart + roomHeight[0] * 0.6);
// Далее всё то же самое.
// Можно было это сделать в цикле,
// но этот код писала торопливая мартышка.
// Так делать - плохо:)
if (checkDoorway(1)) {
stroke(255, 0, 0);
} else {
stroke(50);
}
line(drawStart + roomWidth[0] + roomWidth[1],
drawStart + roomHeight[0] * 0.15,
drawStart + roomWidth[0] + roomWidth[1],
doorwayWidth + drawStart + roomHeight[0] * 0.15);
if (checkDoorway(2)) {
stroke(255, 0, 0);
} else {
stroke(50);
}
line(drawStart + roomWidth[0] + roomWidth[1] + (roomWidth[2] - doorwayWidth)/2,
drawStart + roomHeight[2],
drawStart + roomWidth[0] + roomWidth[1] + doorwayWidth + (roomWidth[2] - doorwayWidth)/2,
drawStart + roomHeight[2]);
// Вот здесь цикл должен был закончится :)
// Рисуем дверные проёмы, которые не снабжены датчиками
stroke(50);
line(drawStart + roomWidth[0] + roomWidth[1] + roomWidth[2],
drawStart + roomHeight[0] * 0.1,
drawStart + roomWidth[0] + roomWidth[1] + roomWidth[2],
2 * doorwayWidth + drawStart + roomHeight[0] * 0.1);
line(drawStart + roomWidth[0],
drawStart + roomHeight[0] + roomHeight[4] * 0.4,
drawStart + roomWidth[0],
doorwayWidth + drawStart + roomHeight[0] + roomHeight[4] * 0.4);
// В центрах комнат нарисуем количество человек в комнате
fill(255);
text(persons[0].toString(), drawStart + roomWidth[0]/2,
drawStart + roomHeight[0]/2);
text(persons[1].toString(), drawStart + roomWidth[0]+roomWidth[1]/2,
drawStart + roomHeight[1]/2);
text(persons[2].toString(),
drawStart + roomWidth[0] + roomWidth[1]+roomWidth[2]/2,
drawStart + roomHeight[2]/2);
text(persons[3].toString(),
drawStart + roomWidth[0]+roomWidth[3]/2,
drawStart + roomHeight[1] + roomWidth[3]/2);
}
// Проверка состояния дверного проёма под номером currentDoorway
boolean checkDoorway(int currentDoorway)
{
// Если никто не проходил - вернём false
boolean result = false;
switch(doorwayState[currentDoorway]) {
//Если кто-то вышел из комнаты
case 1:
result = true; // состояние изменилось
// если в комнате было больше 0 человек
if (persons[currentDoorway] > 0) {
// отнимем одного человека из этой комнаты
persons[currentDoorway]--;
}
// и добавим его в соседнюю комнату
persons[currentDoorway+1]++;
// изменение состояния отработано, запомним это
doorwayState[currentDoorway] = 0;
break;
// Если кто-то вошел в комнату
case 2:
result = true; // состояние изменилось
// если в соседней комнате больше 0 человек
if (persons[currentDoorway+1] > 0) {
// отнимем одного человека из соседней комнаты
persons[currentDoorway+1]--;
}
// и добавим в другую комнату
persons[currentDoorway]++;
// изменение состояния отработано, запомним это
doorwayState[currentDoorway] = 0;
break;
}
// Возвращаем состояние дверного проёма
return result;
}
// Эта функция работает при получении байта из последовательного порта
void serialEvent (Serial port)
{
try {
// Читаем данные, пришедшие от Arduino
String data = port.readStringUntil('\n');
// формат посылки x:y, где x-номер дверного проёма,
// y - его статус. Разбиваем строку data на несколько строк,
// используя ':' как разделитель
String[] list = split(data, ':');
// номер проёма будет содержаться в первой строке. Сразу переводим строку в число
Integer doorwayNumber = int(list[0]);
// а статус проёма - во второй строке. Сразу переводим строку в число
// HARDCODE! substring(0, 1) - копирование подстроки размером в 1 символ из строки,
// начиная с 0-го символа. Просто у торопливой мартышки день не задался...
Integer state = int(list[1].substring(0, 1));
// Сохраняем состояние дверного проёма
doorwayState[doorwayNumber] = state;
// пишем в консоль пришедшую информацию для отладки
println(data);
println(list[0]);
println(list[1]);
}
// Если что-то пошло не так
catch (Exception e) {
// скорее всего нет связи с Arduino
println("Connection...");
}
}
// Освободим последовательный порт при закрытии программы.
void stop() {
port.clear();
port.stop();
}
==== Ведущее устройство ====
#define STATE_BEGIN 0
#define STATE_SENDED 1
#define STATE_RECEIVED 2
void setup()
{
Serial.begin(9600);
Serial1.begin(1200);
while (!Serial) ;
pinMode(2, OUTPUT);
}
int curDev = 0;
int state = STATE_BEGIN;
void loop()
{
static unsigned long t;
byte msg[4] = { 0x03, 0x20, 0x00, 0x05 };
if(state == STATE_BEGIN)
{
msg[1] = 0x20 + curDev;
digitalWrite(2, HIGH);
Serial1.write((const byte*)msg, 4);
Serial1.flush();
digitalWrite(2, LOW);
state = STATE_SENDED;
t = millis();
}
else if(state == STATE_SENDED)
{
if(Serial1.available())
{
static byte imsg[4];
imsg[0] = imsg[1];
imsg[1] = imsg[2];
imsg[2] = imsg[3];
imsg[3] = Serial1.read();
if(imsg[0] == 0x03 && imsg[3] == 0x05)
{
Serial.print(curDev);
Serial.print(':');
Serial.println((int)(imsg[1]));
Serial.flush();
delay(1);
state = STATE_RECEIVED;
}
}
else if(millis()-t > 50) {
state = STATE_RECEIVED;
}
}
else if(state == STATE_RECEIVED)
{
if(++curDev == 15)
{
curDev = 0;
}
state = STATE_BEGIN;
}
}
==== Ведомые устройства ====
signed long t1 = 0, t2 = 0;
unsigned char dir = 0;
#define BASE_ADDR 0x20
#define MSG_LEN 4
void sensor1()
{
t1 = millis();
check_motion();
}
void sensor2()
{
t2 = millis();
check_motion();
}
void check_motion()
{
if(t1 == 0 || t2 == 0) return;
if(t2-t1 > 0)
{
tone(9, 100, 1000);
dir = 1;
}
else if(t1-t2 > 0)
{
tone(9, 300, 1000);
dir = 2;
}
t1 = t2 = 0;
}
void dispatch_msg(byte* msg)
{
if(msg[1] != BASE_ADDR+addr()) return;
msg[0] = 0x03;
msg[1] = dir;
msg[2] = 0x00;
msg[3] = 0x05;
digitalWrite(2, HIGH);
Serial.write(msg, 4);
Serial.flush();
digitalWrite(2, LOW);
dir = 0;
}
void setup(void)
{
Serial.begin(1200);
digitalWrite(13, LOW);
digitalWrite(4, HIGH);
pinMode(8, INPUT);
pinMode(9, INPUT);
pinMode(10, INPUT);
pinMode(2, OUTPUT);
digitalWrite(2, LOW);
analogReference(INTERNAL);
}
unsigned char addr(void)
{
unsigned char a = 0x00;
if(digitalRead(8)) a |= 0x01;
if(digitalRead(9)) a |= 0x02;
if(digitalRead(10)) a |= 0x04;
return a;
}
byte msg[4];
void loop(void)
{
static unsigned long n1 = 0, n2 = 0;
if(analogRead(A0) > 500)
{
if(++n1 >= 20)
{
if(n1 > 20+1) n1--;
if(n1 == 20) sensor1();
}
}
else n1 = 0;
if(analogRead(A1) > 500)
{
if(++n2 >= 20)
{
if(n2 > 20+1) n2--;
if(n2 == 20) sensor2();
}
}
else n2 = 0;
if(Serial.available())
{
int i;
byte b;
for(i = 0; i < MSG_LEN-1; i++)
msg[i] = msg[i+1];
msg[MSG_LEN-1] = Serial.read();
if(msg[0] == 0x03 && msg[3] == 0x05)
{
dispatch_msg(msg);
}
}
}
===== Как это работает? =====
Когда человек проходит через дверной проём, он сначала попадает в поле зрения одного датчика, а потом другого. По тому, какой датчик сработал первым, можно определить направление пересечения дверного проёма. Если мы подключимся к аналоговым пинам датчиков, то при проходе человека увидим примерно такую картину:
{{ :projects:activitymap:signals.png?direct}}
Всё, что выше Vref, скетч воспринимает как единицу. Остальное — как нули. Vref надо подбирать исходя из размеров дверных косяков и параметров датчиков.
Каждый узел хранит количество проходов. При обнаружении пересечения дверного проёма устройство добавляет «1» к переменной, если человек прошёл в одну сторону, и отнимает «1» в обратном случае.
Ведущее устройство циклически опрашивает все узлы, посылая пакет с номером устройства. Эти пакеты получают все ведомые устройства одновременно, однако отвечает только то устройство, номер которого указан в пакете. Номер устройства задаётся выводами «8», «9», «10».
В ответном пакете устройство сообщает ведущему устройству значение хранимой переменной проходов. После этого устройство обнуляет это значение. Если между двумя опросами одного устройства было два прохода в разные стороны, то эти события могут «ускользнуть» от ведущего. Чтобы этого не произошло опрос производится очень часто — десятки раз в секунду.
Ведомые устройства никогда не начинают передачу по собственному желанию. Они только отвечают на запросы ведущего. Этим решается проблема [[wpru>Коллизия_кадров | коллизий]] на линии.
Ведущее устройство основано на [[amp>product/arduino-leonardo?utm_source=proj&utm_campaign=activitymap&utm_medium=wiki | Arduino Leonardo]]. Это позволяет использовать одновременно RS485-шилд и общение по Serial между Arduino и ПК. Данные, собраные со всех ведомых устройств, передаются по Serial, где их получает Processing-программа и отображает в графическом виде. Она рисует план помещений и отображает в каждой комнате количество находящихся там человек.
{{ :projects:activitymap:host_window.png?direct&600 |}}
===== Демонстрация работы устройства =====
{{youtube>GJa3rkl3Lxg?large}}
===== Что можно сделать ещё? =====
- В нашей реализации есть изъян: если опрос ведомого устройства будет происходить в момент прохода человека, то проход может остаться незамеченным. Чтобы избавиться от этого изъяна нужно подключать дальномеры через компараторы так, чтобы Arduino получала не аналоговый, а цифровой сигнал. Тогда на него можно будет назначить прерывание и обрабатывать события прохода по прерываниям.