Projekt: Kontrola dostępu cz. 1

Witajcie! Dziś zaczniemy dość pracować nad dość sporym projektem, mianowicie chodzi o system kontroli dostępu wykorzystujący karty oraz tagi zbliżeniowe w standardzie Mifare. System taki można zamontować wszędzie tam, gdzie mamy do dyspozycji elektronicznie sterowany zamek (najczęściej taki elektrozaczep można spotkać w furtkach). W pierwszej części omówimy założenia projektu, ustalimy jakie części będą na pewno potrzebne oraz zbudujemy prototyp w oparciu o platformę Arduino.

Pomysł na projekt zrodził się u mnie dość spontanicznie – zaczęło się od tego, że mieliśmy u dziewczyny zastąpić stary domofon nowym wideodomofonem, a skoro już będziemy grzebali przy furtce to może fajnie by było zrobić coś, aby nie trzeba było za każdym razem otwierać jej kluczem. Po przemyśleniu kilku opcji (jak np. otwieranie kodem, kartą lub SMSem) padło na breloczki Mifare z uwagi na to, że są proste w obsłudze i można je szybko wyjąć z kieszeni, przyłożyć do czytnika i otworzyć w ten sposób furkę.

Założenia projektu

System ma za zadanie odczytać kartę zbliżeniową, sprawdzić jej identyfikator, a następnie podjąć odpowiednie akcje (udzielić dostępu lub nie). Warto zapewnić też jakąś komunikację między użytkownikiem a urządzeniem (np. zapalenie zielonej diody i radosne piknięcie buzzerem w przypadku udzielenia dostępu) oraz możliwość programowania nowych kart i kasowania pamięci. Do tego ostatniego przyda nam się karta administratora, której UID będzie wpisane na stałe w kodzie programu. Niech długie przytrzymanie karty administratora (master card) będzie oznaczało skasowanie całej pamięci, a krótkie zbliżenie wprowadzenie urządzenia w tryb dodawania nowej karty. Ogólnie schemat działania ma prezentować się tak:

Poza tym przewidziałem także funkcję programowania przez UART poprzez podłączenie kablem do np. laptopa z uruchomionym odpowiednim programem (który napiszemy w części drugiej).

Lista potrzebnych części

Lista ta odrobinę się zmieni w ostatecznej wersji projektu, kiedy już zrobimy własną płytkę drukowaną. Podaję tutaj listę części do prototypu montowanego na płytce stykowej.

Schemat połączeń

Wszystkie połączenia ustaliłem już tak, żeby łatwo było wykonać ścieżki na płytce drukowanej.

Musimy przeznaczyć trzy piny PWM na diodę RGB, jeden pin cyfrowy na buzzer (diodę w prototypie), kolejny pin na wysterowanie modułu przekaźnika oraz piny magistrali SPI dla czytnika kart. Pin CS czytnika ustalimy sobie na pinie A0 Arduino.

Należy bezwzględnie pamiętać, że czytnik jest zasilany z 3.3V, a podanie na niego 5V błyskawicznie go uśmierci (kiedyś przypadkiem podłączyłem jeden do 5V). W naszym prototypie zasilanie dla czytnika zapewnimy sobie sami budując stabilizator, aby sprawdzić czy będzie działał dobrze (na płytce drukowanej nie będziemy mieli 3V3 z Arduino ;).

Zaczynając od lewej mamy tam DIY stabilizator. Składa się on z diody Zenera 3V9, tranzystoa NPN (tutaj BC547) oraz rezystora 150 omów. Idąc w prawo spotykamy moduł przekaźnika, następnie pomarańczowa dioda zastępuje nam buzzer, dioda RGB będzie sygnalizowała różne stany urządzenia. Skrajnie po prawej stronie mamy czytnik MIfare. Jego piny od lewej to 3V3, RST, GND, IRQ (nie używany), MISO, MOSI, SCK, CS.

W pomarańczowych kabelkach mamy 3V3, a czerwonymi płynie 5V (poza czerwonym dochodzącym do diody RGB, tam mamy stan z pinu sterującego kolorem czerwonym).

Kod programu

Cały kod programu można pobrać tutaj. Przystąpimy teraz do omawiania go krok po kroku (a będzie co omawiać ;).

Aby program w ogóle się skompilował musimy mieć zainstalowane dwie biblioteki:

Mając taką bazę uruchamiamy IDE i przystępujemy do analizy kodu.

Na początku jak zwykle załączamy potrzebne biblioteki oraz definiujemy stałe z numerami pinów do których podłączone są peryferia. Tworzymy obiekt klasy MFRC522, ustalamy sobie UID karty administratora (masterCard).

enum MODE_ENUM {READY, MASTER_ADD, MASTER_CLEAR, ACCESS_GRANTED, ACCESS_DENIED, NEED_AUTH, OTHER};

Tutaj tworzymy sobie typ wyliczeniowy, który będzie informował nas w jakim stanie znajduje się aktualnie urządzenie.

MODE_ENUM currentMode = READY;
bool isMaster = false, isLastReadMaster = false, adminAccess = false;
elapsedMillis masterTimeout = 0, actionTime = 0, loginTime = 0;

byte cardNotPresentCount = 0;

Teraz definiujemy sobie kilka przydatnych zmiennych. Ustalamy, że nasze urządzenie po włączeniu będzie w stanie gotowości, tworzymy zmienne informujące nas, czy odczytana karta jest masterem oraz kilka zmiennych odliczających czas. Zmiennymi adminAccess oraz loginTime zajmiemy się bardziej szczegółowo w drugiej części.

Zmienna cardNotPresentCount będzie pomocna przy sprawdzaniu długiego przytrzymania mastera przy czytniku, albowiem przy standardowym kodzie biblioteka pokazuje, że karta na zmianę jest przykładana i odejmowana od czytnika.

Zanim omówimy funkcję setup zajmiemy się dwiema funkcjami, które są używane w setup().

Pierwsza z nich to isInUse:

bool isInUse(int count, int currentPin, ...) {
  va_list ap;
  va_start(ap, count);
  for(int x = 0; x < count; x++) {
    if(va_arg(ap, int) == currentPin) {
      return true;
    }
  }
  
  va_end(ap);
  return false;
}

Jest to funkcja o zmiennej liczbie argumentów. Pierwsze dwa argumenty to count – ilość dodatkowych argumentów oraz currentPin – pin, który chcemy sprawdzić, czy nie jest przypadkiem w użyciu.

Jako dodatkowe argumenty podajemy numery pinów, które używamy w programie. Funkcja zwróci false, jeżeli currentPin nie znajduje się na tej liście oraz true w przeciwnym wypadku.

Kolejna funkcja to setLedColor:

void setLedColor(byte r, byte g, byte b) {
  analogWrite(redPin, r);
  analogWrite(greenPin, g);
  analogWrite(bluePin, b);
}

Tutaj nic wielkiego nie ma. Po prostu ustalamy kolor dla diody RGB. Każdą składową podajemy w zakresie 0-255.

Skoro omówiliśmy już obie funkcje to przejdźmy do setup:

void setup() {
  Serial.begin(9600);
  
  for(int x = 0; x <= 19; x++) {
    if(isInUse(12, x, 0, 1, redPin, greenPin, bluePin, relayPin, buzzerPin, rstPin, ssPin, 11, 12, 13)) continue;
    
    pinMode(x, INPUT_PULLUP);
  }
  
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  pinMode(buzzerPin, OUTPUT);

  digitalWrite(redPin, LOW);
  digitalWrite(greenPin, LOW);
  digitalWrite(bluePin, LOW);
  digitalWrite(relayPin, !RELAY_ON);
  digitalWrite(buzzerPin, LOW);
  
  SPI.begin();
  reader.PCD_Init();
  if(DEBUG) reader.PCD_DumpVersionToSerial();

  if(DEBUG) {
    Serial.println("EEPROM DUMP:");
    Serial.print("Cards count: ");
    Serial.println(EEPROM.read(0));
    for(int x = 1; x < EEPROM.length(); x++) {
      if((x - 1) % 10 == 0) {
        Serial.print((x - 1) / 10);
        Serial.print(":\t\t");
      }
      Serial.print(EEPROM.read(x), HEX);
      if(x % 10 == 0) {
        Serial.println();
      } else {
        Serial.print("\t");
      }
    }
    Serial.println();
  }

  for(int x = 0; x < 6; x++) {
    if(x%2 == 0) setLedColor(0,255,255);
    else setLedColor(0,0,0);
    delay(150);
  }

  setLedColor(0,0,0);
}

Na samym początku odpalamy port szeregowy, ustawiamy piny, których nie używamy na INPUT_PULLUP, aby nie ściągały zakłóceń z otoczenia, ustalamy tryb pracy dla pinów, które używamy oraz przypisujemy im stan. Następnie inicjujemy magistralę SPI oraz czytnik kart podłączony do niej. Jeżeli stała DEBUG jest true to wyświetlamy zawartość pamięci EEPROM mikrokontrolera.

Następnie radośnie migamy diodą na przywitanie po czym mikrokontroler wchodzi do funkcji loop.

Omówmy pierw kilka innych ważnych funkcji.

bool compareUids(byte * uid1, byte * uid2) {
  for(byte x = 0; x < 10; x++) {
    if(uid1[x] != uid2[x]) {
      return false;
    }
  }
  return true;
}

bool checkIfMaster(byte * uid) {
  return compareUids(uid, masterCard);
}

Te dwie proste funkcje porównują ze sobą 2 identyfikatory i sprawdzają, czy są takie same. Zwracają przy tym prawdę lub fałsz. Druga funkcja jak widać wywołuje pierwszą z tym, że drugim parametrem jest zawsze UID karty administratora.

bool readUid(byte * result, MFRC522::Uid uid) {
  if(!uid.sak) return false;
  
  for(byte x = 0; x < 10; x++) {
    if(x < uid.size) {
      result[x] = uid.uidByte[x];
    } else {
      result[x] = 0;
    }

    if(DEBUG) {
      Serial.print(result[x], HEX);
      Serial.print(" ");
    }
  }

  if(DEBUG) Serial.println();
  return true;
}

Funkcja readUid ma za zadanie zapisać do tablicy result podanej jako argument identyfikator odczytany z czytnika w odpowiednim formacie.

bool checkIfValidCard(byte * uid) {
  int cardsCount = EEPROM.read(0);
  byte currentCard[10] = {0};
  for(int x = 0; x < cardsCount; x++) {
    readCardFromEEPROM(x, currentCard);
    if(compareUids(currentCard, uid)) return true;
  }

  return false;
}

Tą funkcją sprawdzamy, czy podana karta znajduje się w pamięci EEPROM.

W dalszej analizie napotykamy na szereg funkcji perform* (np. performAccessGranted). Są to głównie zależności czasowe, więc omówię tutaj tylko najbardziej rozbudowaną – performMasterAdd:

void performMasterAdd() {
  if(actionTime <= 10000) {
    for(int x = 1; x <= 50; x++) {
      if(actionTime < 200 * x && actionTime >= 200 * (x - 1)) {
        digitalWrite(greenPin, !(x%2));
      }
    }

    if(reader.PICC_IsNewCardPresent() && reader.PICC_ReadCardSerial()) {
      byte cardId[10] = {0};
      if(readUid(cardId, reader.uid) && !checkIfMaster(cardId)) {
        currentMode = READY;
        digitalWrite(buzzerPin, HIGH);
        digitalWrite(greenPin, LOW);

        int responseLed;
        if(addToEEPROM(cardId)) { 
           responseLed = greenPin;
        } else {
           responseLed = redPin;
        }

        for(int x = 0; x < 10; x++) {
          digitalWrite(responseLed, !digitalRead(responseLed));
          delay(100);
        }
        digitalWrite(buzzerPin, LOW);
        digitalWrite(responseLed, LOW); 
      }
    }
    
  } else {
    setLedColor(0, 0, 0);
    currentMode = READY;
  }
}

Na samym początku napotykamy na warunek. Jeżeli czas od rozpoczęcia akcji jest mniejszy niż 10 sekund to wykonujemy pewne czynności. W przeciwnym wypadku gasimy diodę i przełączamy kontroler w tryb gotowości.

W ciągu tych 10 sekund wykonujemy miganie zieloną diodą sygnalizujące, że należy przyłożyć kartę, którą chcemy dodać do systemu.

Następnie kolejnym if’em sprawdzamy, czy została przyłożona karta. Inicjujemy sobie 10 bajtową tablicę cardId, a następnie wpisujemy do niej odczytany UID i od razu sprawdzamy, czy ta karta nie jest masterem (po co mielibyśmy dodawać mastera do pamięci EEPROM?). Jeżeli wszystko jest ok to przełączamy kontroler w stan gotowości, włączamy buzzer sygnalizujący zakończenie dodawania oraz migamy kilkukrotnie czerwoną lub zieloną diodą w zależności od tego, czy dodawanie zakończyło się sukcesem, czy też nie.

Na samym końcu mamy funkcje odpowiedzialne za wymianę danych z pamięcią EEPROM tj. dodawanie karty, odczyt oraz wymazanie pamięci.

Podczas dodawania musimy pamiętać, że mamy ograniczoną ilość pamięci i nie możemy dodać do niej karty, jeżeli nie mamy wystarczająco dużo miejsca.

Odczytując kartę także musimy pamiętać o tym samym.

Podczas wymazywania pamięci możemy najpierw odczytywać wszystkie komórki, a następnie nadpisywać zerami tylko te, w których nie ma zer (zwiększy to trwałość tych komórek).

Funkcję processSerialCmds napiszemy w kolejnej części artykułu.

Omówmy teraz funkcję loop:

Na początku mamy warunek związany z adminAccess, więc go pomijamy w tej części poradnika ;)

Następnie jeżeli urządzenie nie jest w stanie gotowości to sprawdzamy w jakim jest i wykonujemy odpowiednie czynności (jedną z funkcji perform*).

Kolejną czynnością jest sprawdzenie, czy nastąpiło przyłożenie karty master krótsze niż 2 sekundy. Jeżeli tak to oznacza to, iż należy wprowadzić kontroler w stan MASTER_ADD i wyjść z funkcji. W następnym wywołaniu loop() zostanie wykonana funkcja performMasterAdd.

Jeżeli nic z powyższych się nie wydarzyło to przystępujemy do sprawdzania co zrobić z odczytaną kartą.

Jeżeli karta nie jest kartą master to sprawdzamy, czy jest w EEPROMie. Jeżeli tak to przyznajemy dostęp, w przeciwnym wypadku nie przyznajemy dostępu.

Teraz mamy serię warunków:

Jeżeli karta jest masterem, ostatnia karta była masterem i była przyłożona przynajmniej 2 sekundy to wejdź w tryb czyszczenia pamięci.

W przeciwnym wypadku jeżeli karta nie jest masterem, a była to wejdź w tryb dodawania nowej karty.

W przeciwnym wypadku jeżeli karta jest masterem, a nie była w poprzedniej iteracji to zacznij liczyć czas od przyłożenia.

To tyle w tej części artykułu. Po wgraniu tego kodu oraz złożeniu urządzenia zgodnie ze schematem powinniśmy otrzymać działający prototyp :)

Spis treści

  1. Program dla arduino (cz. 1) (tutaj jesteś)
  2. GUI do programowania w C# (cz. 2)
  3. Schemat oraz projektowanie PCB (cz. 3)