Bogusław Kempny

Serwer kamer

×
Autor English
Początek HC-SR04 LCD Kamera fork() sms strfry() GPIO impulsy Klawiatura Brama GPIO PWM SG90 RFID Grafolog RCP Shutdown Temperatura ....











Robimy zdjęcia.

Kupujemy za 100 złotych kamerę internetową USB.

Instalujemy v4l-utils, fswebcam, podłączamy kamerę do portu USB Raspberry.

W /dev powinno się pojawić urządzenie video0.

Wydajemy komendę:
fswebcam -d /dev/video0 -r 640x480 --jpeg 85 /home/rcp/zdjecia/zdjecie.jpg

Kamerka mrugnie do nas zieloną diodą, ptaszek wyleci z obiektywu i w kartotece /home/rcp/zdjecia pojawi się plik zdjęcie.jpg.

Takie to proste. Można by na tym zakończyć, fotka zrobiona.

Opiszemy jednak, jak z Raspberry zrobić serwer kamer, obsługujący ich kilka, robiący zdjęcia na polecenie innego programu, działającego gdzieś tam w sieci, na innym serwerze.

Rozwiązanie to zastosowaliśmy w naszym systemie kontroli trzeźwości.

Po włączeniu alkomatu system robi trzy zdjęcia osoby stojącej przed alkomatem, aby wyeliminować przypadki dmuchania w imieniu kolegi, który jest dziś w nie najlepszej formie.

Na początek istotna, według mnie, uwaga.

W Raspberry zamiast dysku mamy kartę SD. Dobre rozwiązanie, takie karty są szybkie, pojemne...

Mają jednak jedną istotną wadę, "zużywają" się podczas zapisu. Liczba zapisów którą karta wytrzyma liczona jest w dziesiątkach, a nawet setkach tysięcy.

W telefonie komórkowym powinna działać wiele lat.

My jednak planujemy zrobic serwer kamer, robiący może i kilkadziesiąt tysięcy zdjęć dziennie.

Przy tak dużej liczbie zapisów po tygodniu może się okazać, że karta nie wytrzymała.

Poradzimy sobie z tym problemem tworząc niewielki filesystem w pamięci RAM.

W pliku /etc/fstab dodajemy linijkę:

none /home/rcp/zdjecia tmpfs size=1M,noatime 0 0

Po uruchomieniu systemu (lub komendzie mount -a) w kartotece /home/rcp/zdjęcia zamontowany zostanie niewielki, bo rozmiaru 1MB filesystem w pamięci RAM.

Tu możemy pisać ile dusza zapragnie nie obawiając się zniszczenia karty.

Oczywiście punkt montowania, czyli kartotekę /home/rcp/zdjęcia musimy wcześniej utworzyć!

Podłączamy kolejne kamery do Raspberry, w /dev pojawią się kolejne urządzenia video1, video2...

Pięknie, ale jak rozpoznać, która kamera fotografuje drzwi, a która jest w oknie?

Tę z okna podłączyliśmy do USB4, tę skierowaną na drzwi do USB2. Pomocna będzie komenda z pakietu v4l-utils:

root@mobile3:~# v4l2-ctl --list-devices
UVC Camera (046d:081b) (usb-3f980000.usb-1.4):
/dev/video0
UVC Camera (046d:081b) (usb-3f980000.usb-1.5):
/dev/video1

Już wiemy, które urządzenie video z /dev do którego portu USB jest podłączone.

Wystarczy prościutki program zamieniający numer portu USB na nazwę urządzenia video i możemy pstrykać zdjęcia.

W tym, co v4l2-ctl wyprodukuje, wyszukuje numer portu USB i zwraca nazwę własciwego urządzenia video

 sprintf(wzor,"usb-1.%1d" , atoi(argv[1])); 
 while( fgets(buf, 50, stdin) != NULL)
  {
   if ( strstr(buf, wzor) != NULL) 
    {
     fgets(buf, 50, stdin);
     printf("%s", buf);
     exit(0);
    }
  } 
} //od main

No to teraz trochę starego, dobrego shell'a.

Skrypt pstryk :


#!/bin/sh
cd /home/rcp
DEV=`v4l2-ctl --list-devices|./znajdz $1`
/usr/bin/fswebcam -d $DEV -r 640x480 --jpeg 85  /home/rcp/zdjecia/`echo $2|sed -e 's/[[:space:]]//'`.jpg >/dev/null 2>&1
/usr/bin/fswebcam -d $DEV -r 640x480 --jpeg 85  /home/rcp/zdjecia/`echo $2|sed -e 's/[[:space:]]//'`a.jpg >/dev/null 2>&1
/usr/bin/fswebcam -d $DEV -r 640x480 --jpeg 85  /home/rcp/zdjecia/`echo $2|sed -e 's/[[:space:]]//'`b.jpg >/dev/null 2>&1

scp -p32 /home/rcp/zdjecia/`echo $2|sed -e 's/[[:space:]]//'`*.jpg bk@192.168.0.100:/var/www/html/skt/kamera >/home/rcp/kamera.log 2>&1
rm /home/rcp/zdjecia/`echo $2|sed -e 's/[[:space:]]//'`*.jpg

Jak działa?

Wywołujemy go z dwoma parametrami, numer kamery i nazwa z jaką zapisać zdjęcia, a więc na przykład

./pstryk 3 zdjęcie2

Komenda scp kopiuje pliki na zdalny serwer, żeby jednak nie prosiła o podanie hasła, musimy skonfigurować serwer i klienta ssh (na zdalnym i lokalnym komputerze) do autentyfikacji za pomocą kluczy publicznych, jak to zostało opisane w man ssh .

No to umiemy już zrobić zdjęcia, przesłać na dysk "dorosłego"serwera, czas na program dzięki któremu nasz dzielny Raspberry zamieni się w serwer kamer.

Program kamera.c za chwilę omówimy, ale na wypadek, gdybym tym omawianiem zanudził na śmierć, już teraz mówię, że wystarczy go ściągnąć, skompilować, uruchomić i gotowe:

cc kamera.c -o kamera
./kamera 5095&

Teraz już możemy zdalnie zrobić zdjęcie, łącząc się z naszym Raspberry na przykład protokołem telnet:

telnet 192.168.0.33 5095
Trying 192.168.0.33...
Connected to 192.168.0.33.
Escape character is '^]'.
5 
KUKU

5 to numer portu USB do którego wpięliśmy kamerę, "KUKU" to nazwa pod jaką zdjęcie zapisać.

Jeśli nie skonfigurowaliśmy jeszcze kluczy dla scp, to dobrze jest opatrzyć znakiem komentarza linię skryptu pstryk kasującą zrobione zdjęcie:

#rm /home/rcp/zdjecia/`echo $2|sed -e 's/[[:space:]]//'`*.jpg

W kartotece na zdjęcia powinne znaleźć się nasze fotografie:

root@mobile3:/home/rcp/zdjecia# ls -l /home/rcp/zdjecia/
total 36
-rw-r--r-- 1 root root 12124 Jun 30 12:57 KUKUa.jpg
-rw-r--r-- 1 root root 11975 Jun 30 12:57 KUKUb.jpg
-rw-r--r-- 1 root root 11133 Jun 30 12:57 KUKU.jpg

Oczywiście w prawdziwym środowisku numer kamery i nazwę zdjęcia wyśle naszemu serwerowi inny program działający na jakimś komputerze w sieci.

No to teraz program serwera. Kompletny program źródłowy znaleźć można w linku na poprzedniej stronie, pominiemy więc tu te jego fragmenty, które nie wymagają objaśnienia, jak pliki nagłówkowe, definicje zmiennych.

Nasz program, jak każdy przyzwoity serwer powinien nieustannie odbierać od programów klienckich prośby o zrobienie zdjęcia. Sam proces robienia zdjęcia trwa jednak kilka sekund, kamera musi ocenić jakość oświetlenia, wyostrzyć obraz itp.

W tym czasie serwerowi trudno byłoby równocześnie obsługiwać prośby klientów, dlatego w celu zrobienia zdjęcia za każdym razem uruchomimy proces potomny, który się tym zajmie.

Po skończeniu pracy procesu potomnego pozostaje on jednak nadal w systemie jako tak zwany proces zombie. Nie cały, tylko pewne jego elementy, ale zostaje.

Chodzi o to, że proces nadrzędny może chcieć się na przykład dowiedzieć, czy proces potomny nie zakończył się błędnie.

Proces zombie zostanie usunięty po skończeniu pracy procesu, który go uruchomił, jeśli jednak, jak w naszym przypadku, proces nadrzędny pracuje cały czas, musimy sami zadbać o jego usunięcie.
Po skończeniu procesu potomnego do procesu nadrzędnego wysyłany jest sygnał SIGCHLD, obsługujemy go, co powoduje usunięcie procesu zombie.
Zapamietanie statusu zakończenia procesu w zmiennej globalnej nie jest w tym programie potrzebne, można go użyć na przykład do obsługi przypadków, że zdjęcie się nie zrobilo.
A więc procedura obsługi przerwania:
sig_atomic_t child_exit_status;

void clean_up_child_process(int signal_num)
{
/* remove child process */
wait (&status);

/* save exit status in a global variable */
child_exit_status = status;
}

I już w procedurze main jej użycie:

main (argc, argv)
     int argc;
     char *argv[];
{

/* handle SIGCHLD by calling clean_up_child_process */
    struct sigaction sigchild_action;
    memset(&sigchild_action, 0, sizeof(sigchild_action));
    sigchild_action.sa_handler = &clean_up_child_process;
    sigaction(SIGCHLD, &sigchild_action, NULL);

Teraz zrobimy serwer nasłuchujący na wybranym porcie (przekazywanym jako parametr przy uruchomieniu naszego programu) ewentualnych próśb o zrobienie zdjęcia.

    int sockfd, newsockfd, portno, clilen;
    struct sockaddr_in serv_addr, cli_addr;
Tworzymy socket:
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) 
    {
        perror("ERROR opening socket");
        exit(1);
    }
    bzero((char *) &serv_addr, sizeof(serv_addr));

W zmiennej portno zapisujemy numer portu na którym nasz serwer ma nasłuchiwać:
    portno = atoi(argv[1]); //arbitralny zakres 5090 - 5099
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(portno);
 
Adresujemy nasz socket:
    if (bind(sockfd, (struct sockaddr *) &serv_addr,
                          sizeof(serv_addr)) < 0)
    {
         perror("ERROR on binding");
         exit(1);
    }



I uruchamiamy nasz serwer, na wskazanym przez nas porcie bedzie sluchał próśb klientów:
Liczba 5 to długość kolejki klientów oczekujących na połączenie.
  listen(sockfd,5);
  clilen = sizeof(cli_addr);
    
  while (TRUE)
   {
I czekamy na klienta. Jak się pojawi, accept utworzy nowy socket (newsockfd), który będzie używany do komunikacji z tym klientem:
restart:
    newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, 
                                &clilen);

    if (newsockfd < 0) 
    {
        if (errno == EINTR) goto restart;
        perror("ERROR on accept");
        exit(1);
    }

Mamy już połączenie z klientem, odbieramy numer kamery które ma zrobic zdjęcie i nazwę pod jaką je zapisac:

    /* czytaj rozkazy z serwera  */
     bzero(buf,10);
// Odczytaj numer portu usb kamery
     time (&czas1);
     time (&czas2);
     m=0;
     while (czas2 - czas1 < 5) // 5 sekund na odczyt, potem timeout
     {
      if ((read (newsockfd, &buf[m], 1)) != -1)

        {
         if( m>100)  break; // padl port 
         if (buf[m] == 0x0a)
          {
            buf[m+1]=0x0;
            wynik=1;
            break;
          }
         m++;
        }
      time (&czas2);
     }

// Odczytaj numer zdjecia
     time (&czas1);
     time (&czas2);
     m=0;
     while (czas2 - czas1 < 5) // 5 sekund na odczyt, potem timeout
     {
      if ((read (newsockfd, &buf1[m], 1)) != -1)
        {
         if( m>100)  break; // padl port 
         if (buf1[m] == 0x0a)
          {
            buf1[m+1]=0x0;
            wynik=1;
            break;
          }
         m++;
        }
      time (&czas2);
     }
I robimy zdjęcia.
Skryptem w shell który wcześniej opisaliśmy.
Uruchamiamy go funkcją execl podając jako parametry numer kamery i nazwę zdjęcia.

Użycie funkcji fork() spowoduje utworzenie procesu będącego kopią procesu który jej użył.
Czy po jej użyciu jesteśmy w procesie który ją wywołał, czy w procesie potomnym, rozpoznać możemy po tym, jaką wartość zwróciła funkcja fork().
W procesie nadrzędnym będzie to wartość PID procesu potomnego, w procesie potomnym wartość zero.
Dlatego execl wykona się tylko w procesie potomnym, który nie spiesząc się może robić zdjęcia.
Proces nadrzędny zamyka połączenie sieciowe z klientem i czeka na obsłużenie kolejnego.

     if (! (m = fork()) )
       execl("/home/rcp/pstryk", "/home/rcp/pstryk", buf, buf1, (char *)0);

     shutdown(newsockfd, 2);
     close(newsockfd);
   } //od while true