Сегодня мы разберёмся, как объединить дюжину ардуин в одну сеть и не пуститься по миру. Для выполнения этой сложнейшей задачи мы воспользуемся RS485-шилдами, которые позволяют развернуть сеть типа «общая шина». Главным преимуществом такой сети является дешевизна развёртывания: вам не требуется прокладывать кабели к каждому узлу от маршрутизатора, да и сам этот маршрутизатор не требуется.
Ведущее устройство:
Ведомое устройство:
Также для прокладки сети вам потребуется медный провод сечением 0,5–1 мм.
// Нам нужна работа с последовательным портом 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); } } }
Когда человек проходит через дверной проём, он сначала попадает в поле зрения одного датчика, а потом другого. По тому, какой датчик сработал первым, можно определить направление пересечения дверного проёма. Если мы подключимся к аналоговым пинам датчиков, то при проходе человека увидим примерно такую картину:
Всё, что выше Vref, скетч воспринимает как единицу. Остальное — как нули. Vref надо подбирать исходя из размеров дверных косяков и параметров датчиков.
Каждый узел хранит количество проходов. При обнаружении пересечения дверного проёма устройство добавляет «1» к переменной, если человек прошёл в одну сторону, и отнимает «1» в обратном случае.
Ведущее устройство циклически опрашивает все узлы, посылая пакет с номером устройства. Эти пакеты получают все ведомые устройства одновременно, однако отвечает только то устройство, номер которого указан в пакете. Номер устройства задаётся выводами «8», «9», «10».
В ответном пакете устройство сообщает ведущему устройству значение хранимой переменной проходов. После этого устройство обнуляет это значение. Если между двумя опросами одного устройства было два прохода в разные стороны, то эти события могут «ускользнуть» от ведущего. Чтобы этого не произошло опрос производится очень часто — десятки раз в секунду.
Ведомые устройства никогда не начинают передачу по собственному желанию. Они только отвечают на запросы ведущего. Этим решается проблема коллизий на линии.
Ведущее устройство основано на Arduino Leonardo. Это позволяет использовать одновременно RS485-шилд и общение по Serial между Arduino и ПК. Данные, собраные со всех ведомых устройств, передаются по Serial, где их получает Processing-программа и отображает в графическом виде. Она рисует план помещений и отображает в каждой комнате количество находящихся там человек.