====== Карта офисной активности ====== {{ :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 получала не аналоговый, а цифровой сигнал. Тогда на него можно будет назначить прерывание и обрабатывать события прохода по прерываниям.