|
|
@@ -0,0 +1,249 @@
|
|
|
+import sensor, image, time, math, socket, network
|
|
|
+from pyb import Pin
|
|
|
+
|
|
|
+SSID = "EkoSlupek"
|
|
|
+KEY = "1234567890"
|
|
|
+HOST = ""
|
|
|
+PORT = 8080
|
|
|
+
|
|
|
+threshold_list = [(190, 255)]
|
|
|
+min_temp_in_celsius = 20.0
|
|
|
+max_temp_in_celsius = 42.0
|
|
|
+treshold_temperature = 38.0
|
|
|
+# Korekta offsetu temperatury (dodaj/subtrahuj stopnie aby dopasować do referencyjnego termometru)
|
|
|
+temperature_offset = -2.0 # Przykład: jeśli kamera pokazuje 38°C a Flir 33°C, ustaw -5.0
|
|
|
+
|
|
|
+pin1 = Pin('P4', Pin.OUT_PP, Pin.PULL_NONE)
|
|
|
+
|
|
|
+# --- sensor setup ---
|
|
|
+sensor.reset()
|
|
|
+sensor.ioctl(sensor.IOCTL_LEPTON_SET_MODE, True, False)
|
|
|
+sensor.ioctl(sensor.IOCTL_LEPTON_SET_RANGE, min_temp_in_celsius, max_temp_in_celsius)
|
|
|
+sensor.set_pixformat(sensor.GRAYSCALE)
|
|
|
+sensor.set_framesize(sensor.QQVGA)
|
|
|
+sensor.skip_frames(time=2000)
|
|
|
+
|
|
|
+# --- WiFi jako Access Point ---
|
|
|
+try:
|
|
|
+ wlan = network.WLAN(network.AP_IF)
|
|
|
+ wlan.config(ssid=SSID, key=KEY, channel=2)
|
|
|
+ wlan.active(True)
|
|
|
+ print("AP started: {} IP: {}".format(SSID, wlan.ifconfig()[0]))
|
|
|
+except Exception as e:
|
|
|
+ print("WiFi error:", e)
|
|
|
+ wlan = None
|
|
|
+
|
|
|
+def map_g_to_temp(g):
|
|
|
+ temp = ((g * (max_temp_in_celsius - min_temp_in_celsius)) / 255.0) + min_temp_in_celsius
|
|
|
+ return temp + temperature_offset # Dodaj korekcję offsetu
|
|
|
+
|
|
|
+def temp_to_g(temp):
|
|
|
+ """Konwertuj temperaturę na wartość grayscale"""
|
|
|
+ return int(((temp - min_temp_in_celsius) / (max_temp_in_celsius - min_temp_in_celsius)) * 255.0)
|
|
|
+
|
|
|
+# Próg temperatury - pokazuj tylko bloby powyżej tej temperatury
|
|
|
+min_display_temp = 35.0
|
|
|
+min_display_g = temp_to_g(min_display_temp) # wartość grayscale dla 35°C
|
|
|
+
|
|
|
+# Próg dla liczenia średniej - liczymy średnią tylko z pikseli powyżej tej temperatury
|
|
|
+# To eliminuje problem z chłodnym otoczeniem w prostokącie bloba
|
|
|
+mean_threshold_temp = 36.5
|
|
|
+mean_threshold_g = temp_to_g(mean_threshold_temp) # wartość grayscale dla 36.5°C
|
|
|
+
|
|
|
+# Opcja: użyj maksimum zamiast średniej (bardziej wrażliwe na gorączkę, ale może być podatne na zakłócenia)
|
|
|
+# False = użyj średniej z gorących pikseli (ZALECANE)
|
|
|
+# True = użyj maksimum (najgorętszy piksel w blobie)
|
|
|
+use_max_instead_of_mean = False
|
|
|
+
|
|
|
+# --- Funkcja restartująca serwer (TYLKO socket, bez restartu AP) ---
|
|
|
+def restart_server():
|
|
|
+ global server
|
|
|
+ try:
|
|
|
+ # close old server if exists
|
|
|
+ try:
|
|
|
+ if server:
|
|
|
+ server.close()
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # create new server socket
|
|
|
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
|
|
|
+ server.bind([HOST, PORT])
|
|
|
+ server.listen(1) # jedno połączenie naraz
|
|
|
+ server.setblocking(False)
|
|
|
+ print("Server ready on port", PORT)
|
|
|
+ except Exception as e:
|
|
|
+ print("Server restart error:", e)
|
|
|
+ server = None
|
|
|
+
|
|
|
+# --- inicjalizacja ---
|
|
|
+server = None
|
|
|
+client = None
|
|
|
+last_client_activity = time.ticks_ms() # aktualizacja przy każdym udanym send/accept
|
|
|
+restart_cooldown = time.ticks_ms() # do ograniczenia częstotliwości restartów
|
|
|
+clock = time.clock()
|
|
|
+
|
|
|
+# Czas wykrycia gorączki - dla redukcji zakłóceń (opóźnienie 1 sekunda)
|
|
|
+fever_start_time = None
|
|
|
+fever_detection_delay_ms = 1000 # Czas opóźnienia w milisekundach (1 sekunda)
|
|
|
+
|
|
|
+restart_server()
|
|
|
+
|
|
|
+while True:
|
|
|
+ clock.tick()
|
|
|
+ img = sensor.snapshot()
|
|
|
+
|
|
|
+ # ---- analiza temperatury ----
|
|
|
+ blob_stats = []
|
|
|
+ blobs = img.find_blobs(threshold_list, pixels_threshold=200, area_threshold=200, merge=True)
|
|
|
+
|
|
|
+ for blob in blobs:
|
|
|
+ # Liczymy średnią tylko z pikseli powyżej 36.5°C w obszarze bloba
|
|
|
+ # To eliminuje problem z chłodnym otoczeniem w prostokącie bloba
|
|
|
+ hot_threshold = [(mean_threshold_g, 255)]
|
|
|
+ stats = img.get_statistics(thresholds=hot_threshold, roi=blob.rect())
|
|
|
+
|
|
|
+ # Sprawdź czy są jakieś piksele powyżej 36.5°C
|
|
|
+ if stats.mean() >= mean_threshold_g:
|
|
|
+ if use_max_instead_of_mean:
|
|
|
+ # Użyj maksimum (najgorętszy piksel) - bardziej wrażliwe
|
|
|
+ temp_g = stats.max()
|
|
|
+ else:
|
|
|
+ # Użyj średniej z gorących pikseli - bardziej stabilne (ZALECANE)
|
|
|
+ temp_g = stats.mean()
|
|
|
+
|
|
|
+ temp = map_g_to_temp(temp_g)
|
|
|
+
|
|
|
+ # Pokazuj tylko bloby z temperaturą > 35°C
|
|
|
+ if temp > min_display_temp:
|
|
|
+ blob_stats.append((blob.x(), blob.y(), temp, blob))
|
|
|
+
|
|
|
+ img.to_rainbow(color_palette=image.PALETTE_IRONBOW)
|
|
|
+
|
|
|
+ # Sprawdź czy którykolwiek blob ma przekroczoną temperaturę
|
|
|
+ has_fever = False
|
|
|
+
|
|
|
+ # Rysuj tylko bloby które mają mean > 35°C
|
|
|
+ for blob_stat in blob_stats:
|
|
|
+ blob = blob_stat[3] # ostatni element to obiekt blob
|
|
|
+
|
|
|
+ # Rysuj elipsę zamiast prostokąta - lepiej pasuje do kształtu
|
|
|
+ try:
|
|
|
+ img.draw_ellipse(blob.cx(), blob.cy(), int(blob.w()//2), int(blob.h()//2), 0, color=(0,255,0))
|
|
|
+ except:
|
|
|
+ # Fallback: prostokąt jeśli elipsa nie działa
|
|
|
+ img.draw_rectangle(blob.rect(), color=(0,255,0))
|
|
|
+
|
|
|
+ img.draw_cross(blob.cx(), blob.cy(), color=(0,255,0))
|
|
|
+ img.draw_string(blob_stat[0]+2, blob_stat[1]+1, "%.2fC" % blob_stat[2], mono_space=False, color=(0,255,0))
|
|
|
+
|
|
|
+ if blob_stat[2] > treshold_temperature:
|
|
|
+ has_fever = True
|
|
|
+
|
|
|
+ # Sterowanie pinem z opóźnieniem 1 sekunda (redukcja zakłóceń)
|
|
|
+ current_time = time.ticks_ms()
|
|
|
+
|
|
|
+ if has_fever:
|
|
|
+ # Gorączka wykryta - sprawdź czy jest wykrywana wystarczająco długo
|
|
|
+ if fever_start_time is None:
|
|
|
+ # Pierwsze wykrycie - zapisz czas
|
|
|
+ fever_start_time = current_time
|
|
|
+ else:
|
|
|
+ # Sprawdź czy minęło wystarczająco dużo czasu (>1 sekunda)
|
|
|
+ if time.ticks_diff(current_time, fever_start_time) >= fever_detection_delay_ms:
|
|
|
+ # Gorączka wykrywana dłużej niż 1 sekunda - włącz pin
|
|
|
+ pin1.value(1)
|
|
|
+ # Jeśli jeszcze nie minęła sekunda, pin pozostaje w poprzednim stanie
|
|
|
+ else:
|
|
|
+ # Brak gorączki - resetuj timer i wyłącz pin
|
|
|
+ fever_start_time = None
|
|
|
+ pin1.value(0)
|
|
|
+
|
|
|
+ # ---- obsługa WiFi: jeśli interfejs się zawiesi -> wypisz i spróbuj ponownie (ale nie zamykamy AP bez potrzeby) ----
|
|
|
+ if wlan and not wlan.active():
|
|
|
+ print("WiFi down — trying to re-enable AP...")
|
|
|
+ try:
|
|
|
+ wlan.active(False)
|
|
|
+ time.sleep_ms(500)
|
|
|
+ wlan.active(True)
|
|
|
+ wlan.config(ssid=SSID, key=KEY, channel=2)
|
|
|
+ print("AP restarted: {} IP: {}".format(SSID, wlan.ifconfig()[0]))
|
|
|
+ # po restarcie interfejsu trzeba odtworzyć server socket
|
|
|
+ restart_server()
|
|
|
+ except Exception as e:
|
|
|
+ print("WiFi restart error:", e)
|
|
|
+ # przejdź dalej w pętli
|
|
|
+ continue
|
|
|
+
|
|
|
+ # ---- obsługa połączeń (server socket) ----
|
|
|
+ if server:
|
|
|
+ if not client:
|
|
|
+ # spróbuj acceptować (non-blocking)
|
|
|
+ try:
|
|
|
+ client, addr = server.accept()
|
|
|
+ print("New client:", addr)
|
|
|
+ client.settimeout(0.1)
|
|
|
+ client.send(
|
|
|
+ "HTTP/1.1 200 OK\r\n"
|
|
|
+ "Server: OpenMV\r\n"
|
|
|
+ "Content-Type: multipart/x-mixed-replace;boundary=openmv\r\n\r\n"
|
|
|
+ )
|
|
|
+ # zaktualizuj timestamp aktywności przy udanym połączeniu
|
|
|
+ last_client_activity = time.ticks_ms()
|
|
|
+ except OSError:
|
|
|
+ # brak nowego klienta — nic nie rób
|
|
|
+ client = None
|
|
|
+
|
|
|
+ elif client:
|
|
|
+ # jeśli mamy klienta, wysyłaj ramki
|
|
|
+ try:
|
|
|
+ cframe = img.to_jpeg(quality=70, copy=True)
|
|
|
+ header = "\r\n--openmv\r\nContent-Type: image/jpeg\r\nContent-Length:{}\r\n\r\n".format(cframe.size())
|
|
|
+ client.send(header)
|
|
|
+ client.send(cframe)
|
|
|
+ # update ostatniej udanej wysyłki
|
|
|
+ last_client_activity = time.ticks_ms()
|
|
|
+ except OSError as e:
|
|
|
+ # klient rozłączył się lub socket padł -> zamykamy klienta i odtwarzamy server socket
|
|
|
+ print("⚠️ Client disconnected or socket error:", e)
|
|
|
+ try:
|
|
|
+ client.close()
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ client = None
|
|
|
+
|
|
|
+ # restartujemy TYLKO server socket (workaround WINC bug)
|
|
|
+ if time.ticks_diff(time.ticks_ms(), restart_cooldown) > 2000:
|
|
|
+ restart_cooldown = time.ticks_ms()
|
|
|
+ print("♻️ Recreating server socket (socket-only restart)...")
|
|
|
+ try:
|
|
|
+ if server:
|
|
|
+ try:
|
|
|
+ server.close()
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ restart_server()
|
|
|
+ except Exception as e2:
|
|
|
+ print("Error recreating server socket:", e2)
|
|
|
+ # krótka pauza, żeby nie próbować od razu accept w tej samej iteracji
|
|
|
+ time.sleep_ms(200)
|
|
|
+
|
|
|
+ # ---- watchdog: jeśli przez >10 s brak udanej wysyłki/aktywności -> odtwórz server socket (bez restartu AP) ----
|
|
|
+ if time.ticks_diff(time.ticks_ms(), last_client_activity) > 10000:
|
|
|
+ # ochronna blokada przed zbyt częstymi restartami
|
|
|
+ if time.ticks_diff(time.ticks_ms(), restart_cooldown) > 2000:
|
|
|
+ print("⏰ No client activity for >10s — recreating server socket (watchdog).")
|
|
|
+ try:
|
|
|
+ if server:
|
|
|
+ try:
|
|
|
+ server.close()
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ restart_server()
|
|
|
+ except Exception as e:
|
|
|
+ print("Watchdog server recreate error:", e)
|
|
|
+ client = None
|
|
|
+ last_client_activity = time.ticks_ms()
|
|
|
+ restart_cooldown = time.ticks_ms()
|
|
|
+ time.sleep_ms(200)
|