KNR - Hangwatch

Typ_projektu
Arduino
Zdjecie główne
Krótki opis projektu

Hangwatch to projekt interaktywnego haczyka IoT pomagający śledzić obecność w pomieszczeniach Koła Naukowego Robotyków PW. System składa się z modułu fizycznego opartego na ESP32, dostępnego w wersji podstawowej z dwiema diodami LED, przyciskiem i magnesem oraz w wersji zaawansowanej (pokazanej na obrazku) z dodatkowym wyświetlaczem LCD. Serwer odbiera dane przy pomocy żądań POST/GET od haczyków i udostępnia prostą stronę WWW prezentującą status sali (hanged/empty/offline) wraz z czasem aktywności.

Niezbędne elementy

[przykładowe - prosimy o edycję]

1. Płytka ESP32 - dowolna

2. Wyświetlacz LCD oparty o sterownik ST7735S

3. Krańcówka z drukarki 3D

4. 5 śrubek M2 wraz z nakrętkami

5. Mocna taśma dwustronna lub inny sposób montażu

Sprzęt

1. Drukarka 3D

2. Lutownica

Opis projektu

Hangwatch to kompleksowy system IoT pozwalający w czasie rzeczywistym monitorować obecność osób w salach Koła Naukowego Robotyków PW. Składa się z dwuwarstwowej architektury: inteligentnych wieszaków na klucze (haczyk) wyposażonych w moduł ESP32 oraz diody LED, przycisk i magnes (wariant podstawowy) lub dodatkowy wyświetlacz LCD (wariant zaawansowany), oraz serwera z prostą aplikacją WWW prezentującą statusy wszystkich pomieszczeń.

Moduł haczyka komunikuje się z serwerem przez HTTP, wysyłając żądania POST/GET do endpointu /hooks, które zawierają identyfikator płytki, nazwę sali oraz aktualny stan („hanged”, „empty” lub „offline”) wraz z czasami ostatniej aktywności i zmiany stanu (podawanymi w sekundach) . Serwer napisany  w Pythonie (plik serve.py) przetwarza te dane, aktualizuje wewnętrzną strukturę JSON i udostępnia je klientom przeglądarkowym w formie czytelnego interfejsu, pozwalając na stwierdzenie dane pomieszczenie jest aktualnie otwarte..

Całość można uruchomić lokalnie, instalując zależności z requirements.txt i wywołując python3 serve.py, lub w Dockerze (docker build --tag hangwatch . i uruchamiając docker run -p 8080:80 hangwatch -d --restart=always). Projekt dystrybuowany jest na licencji MIT i umożliwia elastyczne definiowanie dozwolonych haczyków oraz haseł w pliku rooms.json – doskonałe rozwiązanie do prostego systemu rejestracji obecności w przestrzeniach kołowych

kod programu
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <string>
#include <bitmap.h>
#include <WiFiManager.h>
#define BUTTON1_PIN 25
#define BUTTON2_PIN 26
#define BUTTON_WEB_PIN 14


TFT_eSPI tft = TFT_eSPI();

const char* ssid_self = "HACZYK";
const char* password_self = "haczykowanie";

const char* SERVER_ADDRESS = "https://hangwatch.knr.edu.pl/hooks"; 

const char* BOARD_ID = "box";
const char* MIEJSCE = "Boks b2.01";
const char* HASLO = "tajne hasło";

void IRAM_ATTR buttonAction_Falling();
void IRAM_ATTR buttonAction_Rising();
void IRAM_ATTR buttonAction_WebServer();
int get_status();
int send_status_request(bool buttonState);
void loading();
void setupMode();

class Button{
    public:
        bool isPressed;
        uint16_t Pin;
        const char* states[2]={"Student","Piwo"};
};

Button button1;
Button button2;
Button buttonWeb;


void setup()
{
    Serial.begin(115200);
    delay(1000);
    button1.Pin = BUTTON1_PIN; //przycisk do wykrywanai kluczyka
    button2.Pin = BUTTON2_PIN; 
    buttonWeb.Pin = BUTTON_WEB_PIN; //przycisk do wchodzenia w tryb setupu
    buttonWeb.isPressed = false; //tryb setupu musi byc wylaczony przy bootowaniu
    button1.isPressed = false;

    tft.init(); //wlaczenie wysweitalcza
    tft.textsize=2;
    tft.fillScreen(TFT_BLACK);
    
    pinMode(button1.Pin,INPUT_PULLUP);
    attachInterrupt(button1.Pin,buttonAction_Falling,FALLING);
    attachInterrupt(button2.Pin,buttonAction_Rising,RISING);
    attachInterrupt(buttonWeb.Pin,buttonAction_WebServer,FALLING); 
    
    setupMode();
}
void loop() 
{   
    int httpResponseCode;
    uint64_t timeElapsed;
    if(buttonWeb.isPressed == true){
        setupMode();
    }
    
    if(button1.isPressed == true){
        int status027 = get_status();
        tft.fillRect(0,20,128,40,TFT_BLACK);
        tft.drawBitmap(39,60,logo,50,53,TFT_BLACK,TFT_CYAN);
        httpResponseCode = send_status_request(button1.isPressed);
        while(httpResponseCode != 200){
        httpResponseCode = send_status_request(button1.isPressed);
        
        }
        timeElapsed = millis();
        while(button1.isPressed==true){
        tft.drawBitmap(39,60,logo,50,53,TFT_BLACK,TFT_WHITE);
        
        tft.setCursor(10,10);
        tft.textsize = 1; 
        tft.setTextColor(TFT_WHITE,TFT_BLACK);
        tft.print("STATUS STUDENTA");
        tft.setCursor(10,20);
        if(status027== 0 ){
            tft.print("027 zamkniete");
        }
        else if(status027 == 1){
            tft.print("027 otwarte");
        }
        else if(status027 == 2){
            tft.print("027 offline");
        }
        tft.textsize = 2;
        tft.setCursor(10,40);
        tft.setTextColor(TFT_RED,TFT_BLACK);
        tft.print(button1.states[1]);
        if(millis() - timeElapsed >= 100000){
        timeElapsed = millis();
        httpResponseCode = send_status_request(button1.isPressed);
        }
        }
        
        
    }
    else{
       tft.fillRect(0,20,128,40,TFT_BLACK);
        tft.drawBitmap(39,60,logo,50,53,TFT_BLACK,TFT_CYAN);
        httpResponseCode = send_status_request(button1.isPressed);
        int status027 = get_status();
        while(httpResponseCode != 200){
        tft.fillRect(0,10,128,40,TFT_BLACK);
        tft.setCursor(100,10);
        

        httpResponseCode = send_status_request(button1.isPressed);
        
        }
        
        timeElapsed = millis();
        while(button1.isPressed==false){
        
        tft.drawBitmap(39,60,logo,50,53,TFT_BLACK,TFT_WHITE);
        tft.setCursor(10,10);
        tft.textsize = 1; 
        tft.setTextColor(TFT_WHITE,TFT_BLACK);
        tft.print("STATUS STUDENTA");
        tft.setCursor(10,20);
        if(status027 == 0 ){
            tft.print("027 zamkniete");
        }
        else if(status027 == 1){
            tft.print("027 otwarte");
        }
        else if(status027 == 2){
            tft.print("027 offline");
        }
        tft.textsize = 2;
        tft.setCursor(10,40);
        tft.setTextColor(TFT_GREEN);
        tft.print(button1.states[0]);
        if(millis() - timeElapsed >= 100000){
        timeElapsed = millis();
        httpResponseCode = send_status_request(button1.isPressed);
        }
        }
        
       
    }
   
}


void IRAM_ATTR buttonAction_Falling(){
 button1.isPressed = false; 
}
void IRAM_ATTR buttonAction_Rising(){
 button1.isPressed = true;
}
void IRAM_ATTR buttonAction_WebServer(){
buttonWeb.isPressed = !buttonWeb.isPressed;
}
int send_status_request(bool buttonState)
{
    
    DynamicJsonDocument jsonDoc(200);
    jsonDoc["place"] =  MIEJSCE;
    if(buttonState==false)
    {
        jsonDoc["state"] = "hanged";
    }
    else
    {
        jsonDoc["state"]="empty";
    }
    jsonDoc["password"]=HASLO;
    jsonDoc["board_id"] = BOARD_ID;
    String payload;
    serializeJson(jsonDoc, payload);
    // Wyślij żądanie POST na serwer Flask
    HTTPClient http;
    http.begin(SERVER_ADDRESS);
    http.addHeader("Content-Type", "application/json");
    int httpResponseCode = http.sendRequest("POST", payload);
    http.end();
    loading();
    return httpResponseCode;
}
void loading(){
    int whiteRect_Y = 150;
    int whiteRect_X = 0;
    int whiteRect_Height = 4;
    int whiteRect_Width = 128;
    int rectSpace = 1;
    int barWidth = 1;
    int barHeight = 2;
    tft.setTextColor(TFT_WHITE);
    tft.drawRect(whiteRect_X,whiteRect_Y,whiteRect_Width,whiteRect_Height,TFT_WHITE);
    for(int i = whiteRect_X + rectSpace; i<=whiteRect_Width-rectSpace;i++){
        tft.fillRect(i,whiteRect_Y + rectSpace,barWidth,barHeight,TFT_GREEN);
        delay(6);
    }
    tft.fillRect(whiteRect_X,whiteRect_Y,whiteRect_Width, whiteRect_Height,TFT_BLACK);
    
}
void setupMode(){
    tft.fillRect(0,10,158,90,TFT_BLACK);
    tft.drawBitmap(39,60,logo,50,53,TFT_BLACK,TFT_LIGHTGREY);
    tft.setCursor(10,10);
    tft.textsize = 1; 
    tft.setTextColor(TFT_WHITE,TFT_BLACK);
    tft.print("TRYB SETUPU");
    tft.setTextColor(TFT_CYAN,TFT_BLACK);
    tft.setCursor(10,30);
    tft.print(ssid_self);
    tft.setCursor(10,40);
    tft.print(password_self);

    WiFiManager wm;
    wm.setConfigPortalTimeout(180);
    if(!wm.startConfigPortal(ssid_self,password_self)){
        tft.fillRect(0,10,128,40,TFT_BLACK);
        tft.setCursor(10,10);
        tft.print("timeout");
        delay(3000);
        
        
    }
    tft.fillRect(0,10,128,40,TFT_BLACK);
    tft.textsize = 2;
    buttonWeb.isPressed = false;
}
int get_status(){
    HTTPClient http;
    http.begin(SERVER_ADDRESS);
    DynamicJsonDocument jsonDoc(200);
    int responseCode = http.GET();
    String payload = "{}" ;
    jsonDoc["board_id"] = "1234";
    serializeJson(jsonDoc, payload);
    if(responseCode > 0){
        payload = http.getString();
    }
    http.end();
    if(payload = "empty"){
        return 0;
    }
    else if(payload = "hanged"){
        return 1;
    }
    else{
        return 2;
    }
    }
Tagi
IoT,ESP32,LCD,HTTP.PYTHON