Komunikacja PC->Arduino (ustawianie i pobieranie parametrów)

Arduino to pożyteczne urządzenie, które niewątpliwie przyczyniło się do popularyzacji mikrokontrolerów i choć w większości wykorzystują one przestarzałe Atmegi, wciąż jest to idealne urządzenie do budowy prostych rozwiązań. Na temat Arduino powiedziano już chyba wszystko. W sieci istnieją dziesiątki tysięcy bibliotek i gotowych kodów, które można po prostu dostosować i wykorzystać we własnym projekcie. I w tym waśnie tkwi siła tego projektu. Jednakże nie znalazłem sprawdzonych przykładów dwustronnej komunikacji pomiędzy komputerem a mikrokontrolerem. Owszem, w sieci znaleźć można wiele opisów pobierania i wizualizacji danych poprawnych z kontrolera, jednakże przykładów dwustronnej i jednocześnie stabilnej komunikacji – wiele nie znalazłem. Niniejszy wpis dotyczy właśnie przykładu budowy urządzenia, które zostało przetestowane i działa, a jednocześnie można zmieniać jego parametry, czy też analizować je na bieżąco przy pomocy dedykowanej aplikacji. Ten projekt składa się z dwóch części: urządzenia zbudowanego z Arduino Nano oraz kilku części oraz programu napisanego w C# jako narzędzia, które potrafi analizować na bieżąco dane pobrane z urządzenia oraz pełniącego funkcję konfiguracji kontrolera.

Urządzenie włączające automatycznie światło lub inny odbiornik po zapadnięciu zmierzchu lub za pomocą pilota.

Założenia:

  • Włączenie światła następuje po wykryciu ruchu tylko gdy jest ciemno i wyłącza je po określonym czasie.
  • Jeśli włączono światło za pomocą pilota, zaświeca się na obudowie diona LED i urzadzenie nie reaguje na czujnik ruchu. Światło świeci się do świtu.
  • Parametry urządzenia (poziom światła, histereza, czas włączenia światła) można zmieniać za pomocą przewodu USB przez wirtualny port COM przy pomocy aplikacji GUI.
  • Aplikacja GUI posiada opcję analizatora, który na bieżąco pokazuje parametry odczytane z urządzenia. Dzięki temu można dobrać odpowiednie parametry.

Podzespoły:

  1. Arduino Nano 5V
  2. Czujnik ruchu HC-SR501
  3. Fotorezystor GL5516-5K-10K lub moduł światła KY-018
  4. Moduł przekaźnika z optoizolacją 10A
  5. Moduł pilot/odbiornik SC2272 YK04
  6. Miniaturowa przetwornica STEP-DOWN 230V na 5V typ H6B4
  7. Dioda LED + rezystor 330 om
  8. Obudowa Z23B lub inna. Wymiary: wys. 37mm, szer. 60mm, dł. 84mm
  9. Złacze 3 przewodowe np. T2 lub KF301-3pin

Oto kod źródłowy szkicu *.ino dla Arduino

Wykonanie obustronnej komunikacji pomiędzy PC a Arduinio jest stosunkowo trudne. Testując wiele przykładów z sieci nie udało mi się uzyskać stabilnej komunikacji bez resetowania portu COM. Czasem komunikacja kończyła się koniecznością ponownego podłączenia przewodu USB. W niniejszym przykładzie przedstawiam sposób, który działa stabilnie.

Warto zwrócić uwagę na format parametrów [PARAMETR] lub [PARAMETR:WARTOSC] oraz na funkcję dekodującą parseData(), która dzieli odebrane polecenie na część będącą komendą oraz wartość int oraz decodeComm(), która wykorzystując zmienne zdekodowane przez parseData(), wykonuje polecenia. Oczywiście wszystko można dostosować do własnych potrzeb i utworzyć własny protokół komunikacji. Niezwykle istotna jest funkcja lop(), w której w odpowiedniej kolejności uruchamiana jest komunikacja. To rozwiązanie działa bardzo stabilnie, w porównaniu do przykładów niektórych znalezionych w sieci. Przydatną biblioteką jest Threads i warto ją czasem stosować. Umożliwia ona tworzenie bytów na podobieństwo wątków, uruchamianych w zadanych odstępach czasu i może być przydatna np. do odczytywania parametrów.

#include <Thread.h>
#include <EEPROM.h>
 
#define RF 5     // Odbiornik RF
#define LED 7     // Odbiornik RF
#define PHOTO 1  // Fotorezystor
#define RELAY 3  // Przekaźnik
#define PIR 2    // PIR
 
#define IDNT 30021       // Znacznik urządzenia
#define VER 1.2          // Wersja firmware
byte debug = false;      // Czy tryb debugowania? Informacje w monitorze portu. 
 
volatile byte ruch = LOW;          // Czy wykryto ruch?
volatile byte dzien = LOW;         // Czy jest dzień?
volatile byte wlacz = LOW;         // Czy światło jest aktualnie włączone?
unsigned histereza = 10;           // histereza przałączania dzień/noc
volatile unsigned int tik = 1;     // Odliczany czas włączenia światła
volatile unsigned int czas = 0;    // Domyślny czas włączenia światła
volatile unsigned int swiatlo = 0; // Odczyt z fotorezystora
bool rfClick = false;              // Czy wciśnięty przycisk A w pilocie?
bool PERMANENTBUTTON = LOW;        // Stan przycisku pilota - włącza światło na stałe
unsigned int minswiatlo = 90;      // Wartość światła przełączana dzień/noc
 
const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars];     
boolean newData = false;      
char commandPC[numChars] = {0};
int valuePC = 0;
 
Thread pseudoTh = Thread();
 
void setup(){
  Serial.begin(9600);   
  initEprom();
  pinMode(PIR, INPUT);   
  pinMode(RELAY, OUTPUT);
  pinMode(PHOTO, INPUT);
  pinMode(LED, OUTPUT);
  pinMode(RF, INPUT);
  digitalWrite(RELAY,HIGH); 
  digitalWrite(LED,LOW); 
  attachInterrupt(digitalPinToInterrupt(PIR),setAlarm,RISING);
 
  pseudoTh.onRun(incTik);
  pseudoTh.setInterval(1000);   
}
 
void initEprom(){
 if(EEPROM.read(0) == 255) EEPROM.write(0,15);  // czas włączenia światła = 15 sekund
 if(EEPROM.read(1) == 255) EEPROM.write(1,170); // światło przejścia dzień/noc = 170
 if(EEPROM.read(2) == 255) EEPROM.write(2,0);   // debuger domyślnie na 0
 if(EEPROM.read(3) == 255) EEPROM.write(3,10);  // histereza domyślnie na 10
  czas = EEPROM.read(0);
  minswiatlo = EEPROM.read(1);
  debug = EEPROM.read(2);
  histereza = EEPROM.read(3);
}
 
void setAlarm(){
  ruch = HIGH;
}
 
void runDebug(String co){
 Serial.println(co);
 Serial.print(F("move = ")); Serial.println(String(ruch));
 Serial.print(F("tik = ")); Serial.print(tik); Serial.print(F(" z ")); Serial.println(czas);
 Serial.print("light = "); Serial.print(swiatlo); Serial.print(" level = "); Serial.print(minswiatlo); Serial.print(" his = "); Serial.println(histereza);
 Serial.print("permenant = "); Serial.println(PERMANENTBUTTON);
 Serial.print("clicked = "); Serial.println(rfClick);
 Serial.print("day = "); if(dzien) Serial.println("yes"); else Serial.println("no");
 Serial.println(F("OK"));
}
 
void disLight(){
 digitalWrite(RELAY,HIGH);
 digitalWrite(LED,LOW);
 PERMANENTBUTTON = LOW; 
 wlacz = LOW;
  if (debug) runDebug("OFF");
 delay(200);
}
 
void enLight(){
 digitalWrite(RELAY,LOW); 
 wlacz = HIGH;
 if (debug) runDebug("ON");
 delay(200);
}
 
void incTik(){
  swiatlo = analogRead(PHOTO); 
    if(swiatlo > minswiatlo+histereza) { 
      dzien = HIGH; 
      PERMANENTBUTTON = LOW; 
      } else 
      if(swiatlo < minswiatlo-histereza) 
        dzien = LOW;   
 
  if((!PERMANENTBUTTON) && (!dzien)) tik++;
  if (tik > czas) {
    if (wlacz) disLight();
    tik = 1;
  }  
}
 
// Funkcja resetująca Arduino
void(* resetFunc) (void) = 0;
 
String decodeComm(){
  if(String(commandPC) == "ABO" && valuePC == 0) Serial.println("Leszek Klich 2019"); else
  if(String(commandPC) == "VER" && valuePC == 0) Serial.println(VER); else
  if(String(commandPC) == "IDE" && valuePC == 0) Serial.println(IDNT); else
  if(String(commandPC) == "HIS" && valuePC == 0) Serial.println(histereza); else
  if(String(commandPC) == "TIM" && valuePC == 0) Serial.println(czas); else
  if(String(commandPC) == "LIG" && valuePC == 0) Serial.println(swiatlo); else
  if(String(commandPC) == "DAY" && valuePC == 0) Serial.println(dzien); else
  if(String(commandPC) == "LEV" && valuePC == 0) Serial.println(minswiatlo); else
  if(String(commandPC) == "REL" && valuePC == 0) Serial.println(wlacz); else
  if(String(commandPC) == "DEB" && valuePC == 0) Serial.println(debug); else
  if(String(commandPC) == "MOV" && valuePC == 0) Serial.println(ruch); 
  else
  if(String(commandPC) == "RES" && valuePC == 0) resetFunc(); else
  if(String(commandPC) == "PAR" && valuePC == 0) { Serial.print("S:"); Serial.print(czas); Serial.print(" T:"); Serial.println(minswiatlo); Serial.print(" H:"); Serial.println(histereza); } else
  if(String(commandPC) == "TIM" && valuePC > 0 && valuePC < 255) { EEPROM.write(0, valuePC); initEprom(); } else // czas
  if(String(commandPC) == "LEV" && valuePC > 0 && valuePC < 255) { EEPROM.write(1, valuePC); initEprom(); } else // minimalny poziom światła do przełączania trybu  dzien/noc
  if(String(commandPC) == "DEB" && valuePC == 0 && valuePC == 1) { EEPROM.write(2, valuePC); initEprom(); } else // tryb debugowania
  if(String(commandPC) == "HIS" && valuePC > 0 && valuePC < 200) { EEPROM.write(3, valuePC); initEprom(); } else // histereza
  if(String(commandPC) == "DEF" && valuePC == 0) { EEPROM.write(0,15); EEPROM.write(1,170); EEPROM.write(2,0); EEPROM.write(3,10); initEprom(); } else // reset do domyślnych ustawień
  Serial.println("ERR");
  //resetFunc();
}
 
// Dzieli przychodzące z portu PC dane na komendę oraz wartość
void parseData() {     
    char *strIndex; 
    valuePC = 0;
    strcpy(commandPC, "0");
    strIndex = strtok(tempChars,":");      // Pobierz pierwszą wartość
    strcpy(commandPC, strIndex); 
 
    strIndex = strtok(NULL, ":"); // this continues where the previous call left off
    valuePC = atoi(strIndex);     // konwersja do integer
    if (valuePC > 255) valuePC = 0;
    //strIndex = strtok(NULL, ":");     // Jeśli kolejne parametry
    //floatFromPC = atof(strIndex);     // Można także do float...
}
 
void receivePort() {
    static boolean inProgress = false;
    static byte index = 0;
    char fromChar = '[';
    char toChar = ']';
    char getting;
 
    while (Serial.available() > 0 && newData == false) {
        getting = Serial.read();
 
        if (inProgress == true) {
            if (getting != toChar) {
                receivedChars[index] = getting;
                index++;
                if (index >= numChars) {
                    index = numChars - 1;
                }
            }
            else {
                receivedChars[index] = '\0'; 
                inProgress = false;
                index = 0;
                newData = true;
            }
        }
 
        else if (getting == fromChar) {
            inProgress = true;
        }
    }
}
 
 
void loop(){
  ruch = digitalRead(PIR);    
  rfClick = digitalRead(RF);
 
  if(rfClick) {
    if(PERMANENTBUTTON == LOW) {
     PERMANENTBUTTON = HIGH; 
     digitalWrite(LED,HIGH);
     if (!wlacz) enLight(); 
     } else {
     if (wlacz) disLight();
    }
    delay(1000); // Opóźnienie ponownego przyciśnięcia
  }
 
   if (dzien) {
     tik = czas - 2000;
   } 
 
   if ((!dzien) && (ruch)) { 
       tik = 1; 
       if (!wlacz) enLight();
   }
 
  if (pseudoTh.shouldRun()) 
     pseudoTh.run();
 
    receivePort();
 
    if (newData == true) {
        strcpy(tempChars, receivedChars);
        parseData();
        newData = false;
        decodeComm();
    }
}

Lista parametrów urządzenia (wysyłane przez port COM z komputera – szybkość:9600)
[VER] – aktualna wersja firmware w urządzeniu
[IDE] – zwraca identyfikator zdefinowany jako IDNT. Służy do identyfikacji urządzenia
[HIS] – zwraca wartość histerezy
[TIM] – zwraca czas, na jaki włacza się światło po wykryciu ruchu
[LIG] – zwraca 1 gdy jest włączone światło oraz 0, gdy wyłączone
[DAY] – zwraca 1 gdy urządzenie jest w trybie dzień lub 0, gdy noc
[LEV] – zwraca wartość, która decyduje czy jest dzień czy noc (tę wartośc należy dobrać)
[REL] – zwraca czy przekaźnik jest włączony 1- włączony, 0 – wyłączony
[DEB] – zwraca informajcę czy włączony jest tryb debugowania. Jeśli 1-tak, 0-nie
[MOV] – zwraca 1 gdy wykryto ruch oraz 0 gdy brak ruchu z PIR
[RES] – resetuje urządzenie
[PAR] – wyświwetla w terminalu wszystkie parametry z EEPROM
[TIM:X] – ustawia czas włączenia świata w sekundach, gdzie X – ilość sekund (1-254)
[LEV:x] – ustawia wartość przełaczania trybu dzień/noc, gdzie X – wartość (1-254)
[DEB:X] – włącza lub wyłącza tryb debugowania
[HIS:X] – ustawia poziom histerezy dla identyfikacji dzień/noc, gdzie x z zakresu 1-199
[DEF:0] – ustawia parametry domyślne: czas:15 sekund, poziom przłączania170 (dla tego typu fotorezystora), debug:0, histereza:10

Program konfiguracyjny dla Windows

Kod źródłowy Visual Studio (tylko plik mWindow.cs) – pełny kod na GitHub

using System;
using System.IO.Ports;
using System.Threading;
using System.Windows.Forms;
 
namespace KLTool
{
    public partial class mWindow : Form
    {
        private static string VER = "30021"; //identyfikator urządzenia zapisany w Arduino
 
        public mWindow()
        {
            InitializeComponent();
        }
 
        private void mWindow_Load(object sender, System.EventArgs e)
        {
            var ports = SerialPort.GetPortNames();
            cbPort.DataSource = ports;
            sPort.Close();
        }
 
        private void cbPort_DropDown(object sender, System.EventArgs e)
        {
            var ports = SerialPort.GetPortNames();
            cbPort.DataSource = ports;
        }
 
        private void btnConnect_Click(object sender, System.EventArgs e)
        {
            SerialOpen();            
        }
 
 
        private void SerialOpen()
        {
            if (!sPort.IsOpen)
            {
                sPort.PortName = cbPort.Text;
                sPort.BaudRate = 9600;
                sPort.Parity = Parity.None;
                sPort.DataBits = 8;
                sPort.ReadTimeout = 2000;
                sPort.WriteTimeout = 2000;
                sPort.Handshake = Handshake.None;
 
                try
                {
                    sPort.Open();
                }
                catch (UnauthorizedAccessException)
                {
                    LogW("Port " + sPort.PortName + " jest już w użyciu. Zrestartuj komputer i spróbuj ponownie.");
                }
                catch (Exception ex)
                {
                    LogW("Błąd połączenia z urządzeniem. " + ex.Message + " Zrestartuj komputer i spróbuj ponownie.");
                }
 
                if (IsValidDevice())
                {
                    btnConnect.Text = "Rozłącz";
                    cbPort.Enabled = false;
                    btnConfig.Enabled = true;
                    btnDebug.Enabled = true;
                    LogW("Połączono");
                    GetConfig();
                }
                else SerialClose();
            }
            else
            {
                SerialClose();
            }
        }
 
        private bool IsValidDevice()
        {
            if ((sPort.IsOpen) && (PortWrite("[IDE]").Trim() == VER))
            {
                grConfig.Enabled = true;
                return true;
            }
            else
            {
                LogW("Nierozpozmnane urządzenie!");
                SerialClose();
                return false;
            }
        }
 
        private void SetConfig()
        {
            if (sPort.IsOpen)
            {
                LogW("Zapis parametrów...");
                ChangeParam("[TIM:" + numCzas.Value.ToString() + "]");
                ChangeParam("[HIS:" + numHist.Value.ToString() + "]");
                ChangeParam("[HIS:" + numHist.Value.ToString() + "]");
                ChangeParam("[LEV:" + minLight.Value.ToString() + "]");
                if (chDebug.Checked) ChangeParam("[DEB:1]"); else ChangeParam("[DEB:0]");
                LogW("Gotowe.");
            }
            else LogW("Błąd! Port jest zamknięty!");
        }
 
 
        private void GetConfig()
        {
            if (sPort.IsOpen)
            {               
                mWindow.ActiveForm.Text = "LK Sterownik v:" + PortWrite("[VER]");
                numCzas.Value = Convert.ToInt32(PortWrite("[TIM]"));
                numHist.Value = Convert.ToInt32(PortWrite("[HIS]"));
                minLight.Value = Convert.ToInt32(PortWrite("[LEV]"));
                if (PortWrite("[DEB]") == "1") chDebug.Checked = true; else chDebug.Checked = false;
                grConfig.Enabled = true;
            }
            else LogW("Błąd! Port jest zamknięty!");
        }
 
        private void LogW(string mess)
        {
            txtLog.AppendText(mess + Environment.NewLine);
        }
 
        private void SerialClose()
        {
            if (sPort.IsOpen)
            {
                sPort.Close();
                grConfig.Enabled = false;
                pAnalyze.Visible = false;
                btnConnect.Text = "Połącz";
                btnConfig.Enabled = false;
                btnDebug.Enabled = false;
                cbPort.Enabled = true;
                LogW("Rozłączono");
            }
            else LogW("Port jest zamknięty!");
        }
 
 
        private string PortWrite(string message)
        {
            string ret = "";
            sPort.WriteLine(message);
            Thread.Sleep(100);
            try
            {
                    ret = sPort.ReadLine().Trim();
                }
                catch (TimeoutException)
                {
                    MessageBox.Show("Błąd połączenia. Upłynął czas połączenia...");
                }
            return ret;
        }
 
        private bool ChangeParam(string message)
        {
            try
            {
                sPort.WriteLine(message);
            }
            catch (UnauthorizedAccessException)
            {
                MessageBox.Show("Błąd połączenia z urządzeniem.");
                return false;
            }
            return true;
        }
 
        private void mWindow_FormClosing(object sender, FormClosingEventArgs e)
        {
            SerialClose();
        }
 
 
        private void btnConfig_Click(object sender, EventArgs e)
        {
            SetConfig();
        }
 
        private void sPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
        {
            MessageBox.Show("Błąd komunikacji z portem COM!");
        }
 
        private void btnDebug_Click(object sender, EventArgs e)
        {
            if (pAnalyze.Visible == false)
            {
                pAnalyze.Visible = true;
                btnConfig.Enabled = false;
                timer1.Start();
            }
            else
            {
                pAnalyze.Visible = false;
                btnConfig.Enabled = true;
                timer1.Stop();
            }
        }
 
        private void btnExit_Click(object sender, EventArgs e)
        {
            SerialClose();
            Application.Exit();
        }
 
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (sPort.IsOpen)
            {
                lLev.Text = PortWrite("[LEV]");
                lMov.Text = (Convert.ToInt16(PortWrite("[MOV]")) == 1) ? "Brak ruchu" : "Ruch wykryty";
                lDay.Text = (Convert.ToInt16(PortWrite("[DAY]")) == 0) ? "Noc" : "Dzień";
                lRelay.Text = (Convert.ToInt16(PortWrite("[REL]")) == 0) ? "Wyłączony" : "Włączony";
            }
            else
            {
                SerialClose();
                LogW("Błąd! Port jest zamknięty!");
            }
        }
 
        private void btnDefault_Click(object sender, EventArgs e)
        {
            DialogResult dialogResult = MessageBox.Show("Zresetować urządzenie do wartości domyślnych?", "Potwierdź", MessageBoxButtons.YesNo);
            if (dialogResult == DialogResult.Yes)
            {
                if (sPort.IsOpen)
                {
                    LogW("Przywracanie parametrów domyślnych...");
                    ChangeParam("[DEF:0]");
                    Thread.Sleep(500);
                    GetConfig();
                    LogW("Gotowe.");
                }
                else LogW("Błąd! Port jest zamknięty!");
            }          
        }
    }
 
}

Jeśli nie zamierzacie budować urządzenia, lecz interesuje Was jedynie idea odczytu/wysyłania parametrów do EEPROM Arduino, wystarczy wgrać szkic i testować. Oczywiście wówczas nie będzie można analizować parametrów, ale to wystarczy, by zrozumieć ideę pobierania danych za pomoca komend oraz przesyłania do urządzenia danych w formacie komenda:wartość. Aplikacja rozpoznaje, czy ma do czynienia z konkretnym modelem urządzenia, dzięki odczytaniu jego identyfikatora.

Program konfiguracyjny urządzenia

Po kliknięciu przycisku Analizator, można na bieżąco podglądać odczytane parametry. Poniżej widać bzdurne parametry (aktualny poziom światła), ponieważ zrzut ekranu wykonany został na samym Arduino, bez podłączonych czujników.


Analizator bieżących parametrów

Program działa stabilnie i poprawnie analizuje parametry. Pozwala także na wygodne konfigurowanie parametrów zaszytych w EEPROM Arduino. Dzięki temu urządzenie jest o wiele bardziej elastyczne. Trudno jest bowiem odpowiednio dobrać parametry do warunków rzeczywistych. Trudno jest także teoretycznie dobrać histerezę. Jednakże analizując na bieżąco parametry, staje się to bardzo proste. Podczas budowy urządzenia należy pamiętać, aby fotorezysotr nie był skierowany na źródło światła, które włącza urządzenie, bowiem system się zapętli. W moim urządzeniu fotorezystor znajduje się na dole obudowy, bowiem założyłem, że sterowana lampa będzie znajdować się powyżej urządzenia. Zdalne sterowanie za pomocą modułu bezprzewodowego wymaga dostrojenia odbiornika. W tym celu należy złożyć antenę w pilocie i poprosić inną osobę o wciskanie przycisku. W tym samym czasie należy dostroić kondensator na częstotliwość pilota, aby uzyskać najlepszy zasięg. Warto dokonać tego plastykowym śrubokrętem, bowiem metalowe przedmioty mogą negatywnie wpływać na dokładność strojenia. Należy także dodać antenkę w postaci przewodu dolutowanego do płytki PCB odbiornika. Długość przewodu – anteny to 23,8cm licząc od płytki lub można dokupić lub wykonać dedykowaną antenę wewnętrzną. Ja skróciłem antenę, zatem nie jest ona dobrana optymalnie, a mimo to zasięg wyniósł około 20-30m.

Odbiornik 315Mhz – widoczny kondensator do strojenia

Pełne kody źródłowe można znaleźć na: https://github.com/lklich/ArduinoSerialConfigTool

Total Page Visits: 596 - Today Page Visits: 3
Tagi , .Dodaj do zakładek Link.

Dodaj komentarz

Twój adres email nie zostanie opublikowany.

5 + 5 =

This site uses Akismet to reduce spam. Learn how your comment data is processed.