
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.
[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
1. Drukarka 3D
2. Lutownica
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
#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;
}
}