Карта офисной активности

assembled.jpg

Что это такое?

Сегодня мы разберёмся, как объединить дюжину ардуин в одну сеть и не пуститься по миру. Для выполнения этой сложнейшей задачи мы воспользуемся RS485-шилдами, которые позволяют развернуть сеть типа «общая шина». Главным преимуществом такой сети является дешевизна развёртывания: вам не требуется прокладывать кабели к каждому узлу от маршрутизатора, да и сам этот маршрутизатор не требуется.

Что нам понадобится?

Ведущее устройство:

Ведомое устройство:

Также для прокладки сети вам потребуется медный провод сечением 0,5–1 мм.

Как это собрать?

Сборка ведущего устройства

  1. Установите RS485-шилд на Arduino Leonardo.
  2. Подключите импульсный источник питания к Arduino Leonardo.
  3. Подключите линии «A» и «B» сети RS485 к клеммнику.
  4. Подключите линии «GND» и «VIN» сети RS485 к соответствующим выводам Arduino.
  5. Подключите USB-кабелем плату Arduino к ПК.

Сборка ведомого устройства

  1. Установите Troyka-шилд на Arduino Uno.
  2. Установите RS485-шилд на Troyka-шилд.
  3. Подключите дальномеры к аналоговым тройкам A0 и A1.
  4. Подключите линии «A» и «B» сети RS485 к клеммнику.
  5. Подключите линии «GND» и «VIN» сети RS485 к соответствующим выводам Arduino.

Прокладка сети

  1. Воспользуйтесь витой парой или скрутите два провода в витую пару при помощи шуруповёрта.
  2. Протяните витую пару так, чтобы от неё до каждого из ваших устройств было расстояние не более 1 м.
  3. Соедините каждое устройство с линией. Разрывать её не обязательно. Можно воспользоваться таким способом: В итоге у вас должна получиться приблизительно такая сеть:
  4. На всех платах установите переключатель выбора режима работы в ручной режим и включите передатчик (переключатель ON/OFF).

Исходный код

Визуализация на Processing

activitymap.pde
// Нам нужна работа с последовательным портом
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();
} 
 

Ведущее устройство

master.ino
#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;
    }
}

Ведомые устройства

sensor.ino
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-программа и отображает в графическом виде. Она рисует план помещений и отображает в каждой комнате количество находящихся там человек.

Демонстрация работы устройства

Что можно сделать ещё?

  1. В нашей реализации есть изъян: если опрос ведомого устройства будет происходить в момент прохода человека, то проход может остаться незамеченным. Чтобы избавиться от этого изъяна нужно подключать дальномеры через компараторы так, чтобы Arduino получала не аналоговый, а цифровой сигнал. Тогда на него можно будет назначить прерывание и обрабатывать события прохода по прерываниям.