UART & Sensors
Build, push image, and notify Watchtower / build-image (push) Successful in 1m39s
Build, push image, and notify Watchtower / notify (push) Successful in 1m43s

This commit is contained in:
2026-07-23 15:50:43 +02:00
parent 1ec3ee7abf
commit 2dd664c4b4
25 changed files with 2333 additions and 71 deletions
+13 -10
View File
@@ -20,9 +20,9 @@ db = client["microwave_network_db"]
cooking_collection = db["cooking_parameters"]
device_network_collection = db["device_network"]
# Ensure the photo storage directory exists when the app starts
PHOTO_DIR = "storage/dishPhotos"
os.makedirs(PHOTO_DIR, exist_ok=True)
# Ensure the camera image storage directory exists when the app starts
CAMERA_IMAGE_DIR = "storage/dishCameraImages"
os.makedirs(CAMERA_IMAGE_DIR, exist_ok=True)
# ---------------------------------------------------------
# Routes
@@ -40,24 +40,24 @@ def cooking_params():
if not data:
return jsonify({"error": "Invalid or missing JSON payload"}), 400
# 1. Handle the Photo
photo_b64 = data.get("photo")
if photo_b64:
# 1. Handle the Camera Image
camera_image_b64 = data.get("camera_image")
if camera_image_b64:
# Generate a unique filename using UUID to avoid overwriting
filename = f"dish_{uuid.uuid4().hex}.jpg"
filepath = os.path.join(PHOTO_DIR, filename)
filepath = os.path.join(CAMERA_IMAGE_DIR, filename)
try:
# Decode the base64 string and save it as a binary file
with open(filepath, "wb") as f:
f.write(base64.b64decode(photo_b64))
f.write(base64.b64decode(camera_image_b64))
# Replace the giant base64 string in the dictionary with the local file path
# so we don't bloat the MongoDB document
data["photo"] = filepath
data["camera_image"] = filepath
except Exception as e:
return jsonify({"error": f"Failed to save photo: {str(e)}"}), 500
return jsonify({"error": f"Failed to save camera image: {str(e)}"}), 500
# 2. Save to MongoDB
try:
@@ -71,6 +71,9 @@ def cooking_params():
except Exception as e:
return jsonify({"error": f"Database error: {str(e)}"}), 500
# 3. Returns with the cooking parameters
@app.route("/device-network", methods=["POST"])
def device_network():
+5 -2
View File
@@ -14,7 +14,7 @@ RPI_SYSTEMD_SERVICE="smartwave.service"
# Vérification des arguments
if [ -z "$1" ]; then
echo "Usage: ./deploy.sh [wifi|lora|rpi|all]"
echo "Usage: ./deploy.sh [wifi|mqtt|lora|rpi|all]"
exit 1
fi
@@ -79,6 +79,9 @@ case $CIBLE in
"wifi")
deploy_to_esp "micro_ondes/esp_wifi" "$PORT_ESP_WIFI" "ESP-WIFI"
;;
"mqtt")
deploy_to_esp "micro_ondes/esp_wifi" "$PORT_ESP_WIFI" "ESP-WIFI"
;;
"lora")
deploy_to_esp "micro_ondes/esp_lora" "$PORT_ESP_LORA" "ESP-LORA"
;;
@@ -92,6 +95,6 @@ case $CIBLE in
# Ajoute les autres ici
;;
*)
echo "Cible inconnue. Utilise 'wifi', 'lora' ou 'all'."
echo "Cible inconnue. Utilise 'wifi', 'lora', 'mqtt' ou 'all'."
;;
esac
+12 -1
View File
@@ -1,5 +1,16 @@
# LoRa
`mpremote connect /dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0 repl`
`mpremote connect /dev/serial/by-path/pci-0000:00:14.0-usb-0:6.1:1.0-port0 repl`
# MQTT
`mpremote connect /dev/serial/by-path/pci-0000:00:14.0-usb-0:6.2:1.0-port0 repl`
# UART
| LoRa | MQTT |
| --- | --- |
| 45 | P17 |
| 46 | P16 |
| GND | GND |
+1 -1
View File
@@ -61,7 +61,7 @@ while True:
command = uart_device.read()
print(f"[Main] Received command from WiFi Board: {command}")
uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}")
# uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}")
# 2. Send local metrics over the wire to the WiFi board every few seconds
# uart_device.send("Data Pack: LoRa Link RSSI -72dBm")
+3 -3
View File
@@ -1,6 +1,6 @@
# This file is executed on every boot (including wake-boot from deepsleep)
#import esp
#esp.osdebug(None)
import esp
esp.osdebug(True)
#import webrepl
#webrepl.start()
@@ -16,4 +16,4 @@ def do_connect(ssid, pwd):
print('network config:', sta_if.ifconfig())
# Attempt to connect to WiFi network
do_connect("Smartwave-1", 'Smartwave-prot-1')
# do_connect("Smartwave-1", 'Smartwave-prot-1')
+72 -18
View File
@@ -1,8 +1,11 @@
import _thread
import select
from machine import Pin
from shared import get_mqtt_client, get_uart, config
from sensors import temperature_gun
from shared import get_mqtt_client, get_uart, config, payloads
import time
import ujson as json
import sys
# Simple thread-safe queue list
msg_queue = []
@@ -13,17 +16,22 @@ def queue_publish(topic, payload):
with queue_lock:
msg_queue.append((topic, payload))
# --- Hardware & Client Setup ---
vext = Pin(19, Pin.OUT)
vext.value(0)
time.sleep_ms(100)
# --- INITIALIZE CAMERA ---
try:
# Pass your confirmed working SCL and SDA pins here
temperature_gun.init_camera(scl_pin=21, sda_pin=22, freq=100000)
except Exception as e:
print("[Main] Critical: Camera setup failed!")
sys.print_exception(e)
# --- READ DEVICE ID ---
try:
with open("device_id.txt", "r") as f:
DEVICE_ID = f.read().strip()
except Exception:
DEVICE_ID = "ESP32_Inconnu"
# --- MQTT SETUP ---
MQTT_CA_FILE = "/certs/ca.crt"
mqtt_client = get_mqtt_client(
@@ -34,9 +42,29 @@ mqtt_client = get_mqtt_client(
keepalive=config.MQTT_KEEPALIVE,
)
global orchestrator_id
orchestrator_id = None
def on_mqtt_message(message):
print("[MQTT Thread] Received message:", message)
# Try and parse the payload as json, but if it fails, just print the raw payload
payload_data=None
try:
payload_data = json.loads(message['payload'])
except Exception as e:
print("[MQTT Thread] Error parsing JSON:", e)
sys.print_exception(e)
pass # Maybe it's not JSON
if message['topic'] == config.MQTT_TOPIC_HELLO and payload_data and "id_orchestrator" in payload_data and payload_data["id_microwave"] == DEVICE_ID:
print("[MQTT Thread] Hello response received from orchestrator:", payload_data["id_orchestrator"])
global orchestrator_id
orchestrator_id = payload_data["id_orchestrator"]
# Unsubscribe from the hello topic since we got a response
mqtt_client.unsubscribe(config.MQTT_TOPIC_HELLO)
print("[MQTT Thread] Unsubscribed from topic:", config.MQTT_TOPIC_HELLO)
print("[MQTT Thread] Message processing complete.")
mqtt_client.set_callback(on_mqtt_message)
@@ -73,7 +101,7 @@ def mqtt_background_thread():
# 3. Handle Keepalive tracking manually
if time.time() - last_check >= 15:
print("[Thread] Sending keepalive ping...")
# print("[Thread] Sending keepalive ping...")
mqtt_client._client.ping()
last_check = time.time()
@@ -82,6 +110,7 @@ def mqtt_background_thread():
except Exception as e:
print("[Thread] Connection dropped or error encountered:", e)
sys.print_exception(e)
print("[Thread] Cleaning up socket context. Retrying in 5 seconds...")
# --- FIX FOR ERROR 23 (SOCKET LEAK) ---
@@ -100,28 +129,53 @@ def mqtt_background_thread():
pass
time.sleep(5)
# UART
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
# --- UART BACKGROUND THREAD ---
def uart_background_thread():
"""Background UART worker handling all serial operations safely."""
print("[Thread] Background UART worker started.")
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
while True:
try:
# 1. Check for incoming messages from the Heltec board
while uart_device.any():
incoming_msg = uart_device.read()
print(f"[Thread] Received from esp-lora over UART: {incoming_msg}")
# 2. Example: Send data to the Heltec board every 5 seconds
# uart_device.send("Status Check: WiFi Active")
time.sleep(5) # Fast responsive polling loop for local UART
except Exception as e:
print("[Thread] UART error encountered:", e)
time.sleep(5)
# --- Launch background worker ---
_thread.start_new_thread(mqtt_background_thread, ())
# _thread.start_new_thread(mqtt_background_thread, ())
# _thread.start_new_thread(uart_background_thread, ())
# --- MAIN APPLICATION THREAD (Core 0) ---
print("[Main] Main execution path active.")
time.sleep(2) # Give the thread a moment to initial connect
mqtt_hello_sent_timestamp = -config.MQTT_HELLO_INTERVAL
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
while True:
print("[Main] Queueing a test message for MQTT...")
# Instead of direct publishing, push it to the queue safely
queue_publish(config.MQTT_TOPIC_SENSOR, "Hello from ESP32!")
# MQTT HELLO sent every x seconds until we get a response from the orchestrator
if (orchestrator_id == None and -(mqtt_hello_sent_timestamp - time.time()) > config.MQTT_HELLO_INTERVAL):
print("[Main] Attempting to send initial hello to orchestrator...")
queue_publish(config.MQTT_TOPIC_HELLO, payloads.mqtt_hello(DEVICE_ID))
mqtt_hello_sent_timestamp = time.time()
pass
# 1. Check if the Heltec V3 sent us something over the wire
while uart_device.any():
incoming_msg = uart_device.read()
print(f"[Main] Received from esp-lora over UART: {incoming_msg}")
# Sensors
print(f"[Main] Reading temperature from the gun sensor...")
temp = temperature_gun.read_temperature()
print(f"[Main] Temperature read: {temp}°C")
# 2. Example: Send data to the Heltec board every 5 seconds
# uart_device.send("Status Check: WiFi Active")
time.sleep_ms(200) # Fast responsive polling loop for local UART
time.sleep(1)
+1
View File
@@ -0,0 +1 @@
import sensors.temperature_gun as temperature_gun
@@ -0,0 +1,969 @@
"""
Temperatue gun sensor module
using the MLX90640-D55/D110 sensor. This module provides a function to read the temperature from the gun sensor.
Resolution of 32x24 pixels,
I2C interface
Noise Equivalent Temperature difference (NETD) is 0.1K RMS @ 1Hz refresh rate
"""
import machine # type: ignore
import math
import struct
import time
from micropython import const# Some libraries that we will use
import time
class RefreshRate: # pylint: disable=too-few-public-methods
""" Enum-like class for MLX90640's refresh rate """
REFRESH_0_5_HZ = const(0b000) # 0.5Hz
REFRESH_1_HZ = const(0b001) # 1Hz
REFRESH_2_HZ = const(0b010) # 2Hz
REFRESH_4_HZ = const(0b011) # 4Hz
REFRESH_8_HZ = const(0b100) # 8Hz
REFRESH_16_HZ = const(0b101) # 16Hz
REFRESH_32_HZ = const(0b110) # 32Hz
REFRESH_64_HZ = const(0b111) # 64Hz
class ContextManaged:
"""An object that automatically deinitializes hardware with a context manager."""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.deinit()
# pylint: disable=no-self-use
def deinit(self):
"""Free any hardware used by the object."""
return
class Lockable(ContextManaged):
"""An object that must be locked to prevent collisions on a microcontroller resource."""
_locked = False
def try_lock(self):
"""Attempt to grab the lock. Return True on success, False if the lock is already taken."""
if self._locked:
return False
self._locked = True
return True
def unlock(self):
"""Release the lock so others may use the resource."""
if self._locked:
self._locked = False
else:
raise ValueError("Not locked")
class I2C(Lockable):
def __init__(self, pins=(21, 22), frequency=100000):
self.init(pins, frequency)
def init(self, pins, frequency):
self.deinit()
# 1. Force the ESP32 to activate its internal pull-up resistors on these pins
self._pins = (
machine.Pin(int(pins[0]), machine.Pin.IN, machine.Pin.PULL_UP),
machine.Pin(int(pins[1]), machine.Pin.IN, machine.Pin.PULL_UP)
)
try:
# 2. Bypasses the glitchy ESP32 hardware block using SoftI2C
# (Note: SoftI2C does not take a bus ID number like '0')
self._i2c = machine.SoftI2C(scl=self._pins[0], sda=self._pins[1], freq=frequency)
except RuntimeError:
raise
print(f"Created resilient SoftI2C: {self._i2c}")
def deinit(self):
try:
del self._i2c
except AttributeError:
pass
def scan(self):
return self._i2c.scan()
def readfrom_into(self, address, buffer, *, start=0, end=None):
if start is not 0 or end is not None:
if end is None:
end = len(buffer)
buffer = memoryview(buffer)[start:end]
stop = True # remove for efficiency later
return self._i2c.readfrom_into(address, buffer)
def writeto(self, address, buffer, *, start=0, end=None, stop=True):
if isinstance(buffer, str):
buffer = bytes([ord(x) for x in buffer])
if start is not 0 or end is not None:
if end is None:
return self._i2c.writeto(address, memoryview(buffer)[start:], stop)
else:
return self._i2c.writeto(address, memoryview(buffer)[start:end], stop)
return self._i2c.writeto(address, buffer, stop)
class I2CDevice:
def __init__(self, i2c, device_address, probe=True):
self.i2c = i2c
self._has_write_read = False # hasattr(self.i2c, "writeto_then_readfrom") --> has been turned to False
self.device_address = device_address
if probe:
self.__probe_for_device()
def readinto(self, buf, *, start=0, end=None):
if end is None:
end = len(buf)
self.i2c.readfrom_into(self.device_address, buf, start=start, end=end)
def write(self, buf, *, start=0, end=None, stop=True):
if end is None:
end = len(buf)
self.i2c.writeto(self.device_address, buf, start=start, end=end, stop=stop)
# pylint: disable-msg=too-many-arguments
def write_then_readinto(
self,
out_buffer,
in_buffer,
*,
out_start=0,
out_end=None,
in_start=0,
in_end=None,
stop=False
):
if out_end is None:
out_end = len(out_buffer)
if in_end is None:
in_end = len(in_buffer)
if stop:
raise ValueError("Stop must be False. Use writeto instead.")
if self._has_write_read:
#print("c",dir(self.i2c))
# In linux, at least, this is a special kernel function call
self.i2c.writeto_then_readfrom(
self.device_address,
out_buffer,
in_buffer,
out_start=out_start,
out_end=out_end,
in_start=in_start,
in_end=in_end,
)
else:
# If we don't have a special implementation, we can fake it with two calls
self.i2c.writeto(self.device_address, out_buffer, stop=False) # These lines have been changed to make it work with wipy micropython I2C module
#self.write(out_buffer, start=out_start, end=out_end, stop=False)
#self.readinto(in_buffer, start=in_start, end=in_end)
self.i2c.readfrom_into(self.device_address, in_buffer) # These lines have been changed to make it work with wipy micropython I2C module
# pylint: enable-msg=too-many-arguments
def __enter__(self):
while not self.i2c.try_lock():
pass
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.i2c.unlock()
return False
def __probe_for_device(self):
"""
Try to read a byte from an address,
if you get an OSError it means the device is not there
or that the device does not support these means of probing
"""
while not self.i2c.try_lock():
pass
try:
self.i2c.writeto(self.device_address, b"")
except OSError:
# some OS's dont like writing an empty bytesting...
# Retry by reading a byte
try:
result = bytearray(1)
self.i2c.readfrom_into(self.device_address, result)
except OSError:
raise ValueError("No I2C device at address: %x" % self.device_address)
finally:
self.i2c.unlock()
eeData = [0] * const(832)
I2C_READ_LEN = const(2048)
SCALEALPHA = const(0.000001)
MLX90640_DEVICEID1 = const(0x2407)
OPENAIR_TA_SHIFT = const(8)
class MLX90640: # pylint: disable=too-many-instance-attributes
"""Interface to the MLX90640 temperature sensor."""
kVdd = 0
vdd25 = 0
KvPTAT = 0
KtPTAT = 0
vPTAT25 = 0
alphaPTAT = 0
gainEE = 0
tgc = 0
KsTa = 0
resolutionEE = 0
calibrationModeEE = 0
ksTo = [0] * 5
ct = [0] * 5
alpha = [0] * 768
alphaScale = 0
offset = [0] * 768
kta = [0] * 768
ktaScale = 0
kv = [0] * 768
kvScale = 0
cpAlpha = [0] * 2
cpOffset = [0] * 2
ilChessC = [0] * 3
brokenPixels = [0xFFFF] * 5
outlierPixels = [0xFFFF] * 5
cpKta = 0
cpKv = 0
def __init__(self, i2c_bus, address=0x33):
self.i2c_device = I2CDevice(i2c_bus, address)
self._I2CReadWords(0x2400, eeData)
# print(eeData)
self._ExtractParameters()
@property
def serial_number(self):
""" 3-item tuple of hex values that are unique to each MLX90640 """
serialWords = [0, 0, 0]
self._I2CReadWords(MLX90640_DEVICEID1, serialWords)
return serialWords
@property
def refresh_rate(self):
""" How fast the MLX90640 will spit out data. Start at lowest speed in
RefreshRate and then slowly increase I2C clock rate and rate until you
max out. The sensor does not like it if the I2C host cannot 'keep up'!"""
controlRegister = [0]
self._I2CReadWords(0x800D, controlRegister)
return (controlRegister[0] >> 7) & 0x07
@refresh_rate.setter
def refresh_rate(self, rate):
controlRegister = [0]
value = (rate & 0x7) << 7
self._I2CReadWords(0x800D, controlRegister)
value |= controlRegister[0] & 0xFC7F
self._I2CWriteWord(0x800D, value)
def getFrame(self, framebuf):
""" Request both 'halves' of a frame from the sensor, merge them
and calculate the temperature in C for each of 32x24 pixels. Placed
into the 768-element array passed in! """
emissivity = 0.95
tr = 23.15
mlx90640Frame = [0] * 834
for _ in range(2):
status = self._GetFrameData(mlx90640Frame)
if status < 0:
raise RuntimeError("Frame data error")
# For a MLX90640 in the open air the shift is -8 degC.
tr = self._GetTa(mlx90640Frame) - OPENAIR_TA_SHIFT
self._CalculateTo(mlx90640Frame, emissivity, tr, framebuf)
def _GetFrameData(self, frameData):
dataReady = 0
cnt = 0
statusRegister = [0]
controlRegister = [0]
while dataReady == 0:
self._I2CReadWords(0x8000, statusRegister)
dataReady = statusRegister[0] & 0x0008
# print("ready status: 0x%x" % dataReady)
while (dataReady != 0) and (cnt < 5):
self._I2CWriteWord(0x8000, 0x0030)
# print("Read frame", cnt)
self._I2CReadWords(0x0400, frameData, end=832)
self._I2CReadWords(0x8000, statusRegister)
dataReady = statusRegister[0] & 0x0008
# print("frame ready: 0x%x" % dataReady)
cnt += 1
if cnt > 4:
raise RuntimeError("Too many retries")
self._I2CReadWords(0x800D, controlRegister)
frameData[832] = controlRegister[0]
frameData[833] = statusRegister[0] & 0x0001
return frameData[833]
def _GetTa(self, frameData):
vdd = self._GetVdd(frameData)
ptat = frameData[800]
if ptat > 32767:
ptat -= 65536
ptatArt = frameData[768]
if ptatArt > 32767:
ptatArt -= 65536
ptatArt = (ptat / (ptat * self.alphaPTAT + ptatArt)) * math.pow(2, 18)
ta = ptatArt / (1 + self.KvPTAT * (vdd - 3.3)) - self.vPTAT25
ta = ta / self.KtPTAT + 25
return ta
def _GetVdd(self, frameData):
vdd = frameData[810]
if vdd > 32767:
vdd -= 65536
resolutionRAM = (frameData[832] & 0x0C00) >> 10
resolutionCorrection = math.pow(2, self.resolutionEE) / math.pow(
2, resolutionRAM
)
vdd = (resolutionCorrection * vdd - self.vdd25) / self.kVdd + 3.3
return vdd
def _CalculateTo(self, frameData, emissivity, tr, result):
# pylint: disable=too-many-locals, too-many-branches, too-many-statements
subPage = frameData[833]
alphaCorrR = [0] * 4
irDataCP = [0, 0]
vdd = self._GetVdd(frameData)
ta = self._GetTa(frameData)
ta4 = ta + 273.15
ta4 = ta4 * ta4
ta4 = ta4 * ta4
tr4 = tr + 273.15
tr4 = tr4 * tr4
tr4 = tr4 * tr4
taTr = tr4 - (tr4 - ta4) / emissivity
ktaScale = math.pow(2, self.ktaScale)
kvScale = math.pow(2, self.kvScale)
alphaScale = math.pow(2, self.alphaScale)
alphaCorrR[0] = 1 / (1 + self.ksTo[0] * 40)
alphaCorrR[1] = 1
alphaCorrR[2] = 1 + self.ksTo[1] * self.ct[2]
alphaCorrR[3] = alphaCorrR[2] * (1 + self.ksTo[2] * (self.ct[3] - self.ct[2]))
# --------- Gain calculation -----------------------------------
gain = frameData[778]
if gain > 32767:
gain -= 65536
gain = self.gainEE / gain
# --------- To calculation -------------------------------------
mode = (frameData[832] & 0x1000) >> 5
irDataCP[0] = frameData[776]
irDataCP[1] = frameData[808]
for i in range(2):
if irDataCP[i] > 32767:
irDataCP[i] -= 65536
irDataCP[i] *= gain
irDataCP[0] -= (
self.cpOffset[0]
* (1 + self.cpKta * (ta - 25))
* (1 + self.cpKv * (vdd - 3.3))
)
if mode == self.calibrationModeEE:
irDataCP[1] -= (
self.cpOffset[1]
* (1 + self.cpKta * (ta - 25))
* (1 + self.cpKv * (vdd - 3.3))
)
else:
irDataCP[1] -= (
(self.cpOffset[1] + self.ilChessC[0])
* (1 + self.cpKta * (ta - 25))
* (1 + self.cpKv * (vdd - 3.3))
)
for pixelNumber in range(768):
ilPattern = pixelNumber // 32 - (pixelNumber // 64) * 2
chessPattern = ilPattern ^ (pixelNumber - (pixelNumber // 2) * 2)
conversionPattern = (
(pixelNumber + 2) // 4
- (pixelNumber + 3) // 4
+ (pixelNumber + 1) // 4
- pixelNumber // 4
) * (1 - 2 * ilPattern)
if mode == 0:
pattern = ilPattern
else:
pattern = chessPattern
if pattern == frameData[833]:
irData = frameData[pixelNumber]
if irData > 32767:
irData -= 65536
irData *= gain
kta = self.kta[pixelNumber] / ktaScale
kv = self.kv[pixelNumber] / kvScale
irData -= (
self.offset[pixelNumber]
* (1 + kta * (ta - 25))
* (1 + kv * (vdd - 3.3))
)
if mode != self.calibrationModeEE:
irData += (
self.ilChessC[2] * (2 * ilPattern - 1)
- self.ilChessC[1] * conversionPattern
)
irData = irData - self.tgc * irDataCP[subPage]
irData /= emissivity
alphaCompensated = SCALEALPHA * alphaScale / self.alpha[pixelNumber]
alphaCompensated *= 1 + self.KsTa * (ta - 25)
Sx = (
alphaCompensated
* alphaCompensated
* alphaCompensated
* (irData + alphaCompensated * taTr)
)
Sx = math.sqrt(math.sqrt(Sx)) * self.ksTo[1]
To = (
math.sqrt(
math.sqrt(
irData
/ (alphaCompensated * (1 - self.ksTo[1] * 273.15) + Sx)
+ taTr
)
)
- 273.15
)
if To < self.ct[1]:
torange = 0
elif To < self.ct[2]:
torange = 1
elif To < self.ct[3]:
torange = 2
else:
torange = 3
To = (
math.sqrt(
math.sqrt(
irData
/ (
alphaCompensated
* alphaCorrR[torange]
* (1 + self.ksTo[torange] * (To - self.ct[torange]))
)
+ taTr
)
)
- 273.15
)
result[pixelNumber] = To
# pylint: enable=too-many-locals, too-many-branches, too-many-statements
def _ExtractParameters(self):
self._ExtractVDDParameters()
self._ExtractPTATParameters()
self._ExtractGainParameters()
self._ExtractTgcParameters()
self._ExtractResolutionParameters()
self._ExtractKsTaParameters()
self._ExtractKsToParameters()
self._ExtractCPParameters()
self._ExtractAlphaParameters()
self._ExtractOffsetParameters()
self._ExtractKtaPixelParameters()
self._ExtractKvPixelParameters()
self._ExtractCILCParameters()
self._ExtractDeviatingPixels()
def _ExtractVDDParameters(self):
# extract VDD
self.kVdd = (eeData[51] & 0xFF00) >> 8
if self.kVdd > 127:
self.kVdd -= 256 # convert to signed
self.kVdd *= 32
self.vdd25 = eeData[51] & 0x00FF
self.vdd25 = ((self.vdd25 - 256) << 5) - 8192
def _ExtractPTATParameters(self):
# extract PTAT
self.KvPTAT = (eeData[50] & 0xFC00) >> 10
if self.KvPTAT > 31:
self.KvPTAT -= 64
self.KvPTAT /= 4096
self.KtPTAT = eeData[50] & 0x03FF
if self.KtPTAT > 511:
self.KtPTAT -= 1024
self.KtPTAT /= 8
self.vPTAT25 = eeData[49]
self.alphaPTAT = (eeData[16] & 0xF000) / math.pow(2, 14) + 8
def _ExtractGainParameters(self):
# extract Gain
self.gainEE = eeData[48]
if self.gainEE > 32767:
self.gainEE -= 65536
def _ExtractTgcParameters(self):
# extract Tgc
#print(eeData[60])
self.tgc = eeData[60] & 0x00FF
#print(self.tgc)
if self.tgc > 127:
self.tgc -= 256
self.tgc /= 32
#print(self.tgc)
def _ExtractResolutionParameters(self):
# extract resolution
self.resolutionEE = (eeData[56] & 0x3000) >> 12
def _ExtractKsTaParameters(self):
# extract KsTa
self.KsTa = (eeData[60] & 0xFF00) >> 8
if self.KsTa > 127:
self.KsTa -= 256
self.KsTa /= 8192
def _ExtractKsToParameters(self):
# extract ksTo
step = ((eeData[63] & 0x3000) >> 12) * 10
self.ct[0] = -40
self.ct[1] = 0
self.ct[2] = (eeData[63] & 0x00F0) >> 4
self.ct[3] = (eeData[63] & 0x0F00) >> 8
self.ct[2] *= step
self.ct[3] = self.ct[2] + self.ct[3] * step
KsToScale = (eeData[63] & 0x000F) + 8
KsToScale = 1 << KsToScale
self.ksTo[0] = eeData[61] & 0x00FF
self.ksTo[1] = (eeData[61] & 0xFF00) >> 8
self.ksTo[2] = eeData[62] & 0x00FF
self.ksTo[3] = (eeData[62] & 0xFF00) >> 8
for i in range(4):
if self.ksTo[i] > 127:
self.ksTo[i] -= 256
self.ksTo[i] /= KsToScale
self.ksTo[4] = -0.0002
def _ExtractCPParameters(self):
# extract CP
offsetSP = [0] * 2
alphaSP = [0] * 2
alphaScale = ((eeData[32] & 0xF000) >> 12) + 27
offsetSP[0] = eeData[58] & 0x03FF
if offsetSP[0] > 511:
offsetSP[0] -= 1024
offsetSP[1] = (eeData[58] & 0xFC00) >> 10
if offsetSP[1] > 31:
offsetSP[1] -= 64
offsetSP[1] += offsetSP[0]
alphaSP[0] = eeData[57] & 0x03FF
if alphaSP[0] > 511:
alphaSP[0] -= 1024
alphaSP[0] /= math.pow(2, alphaScale)
alphaSP[1] = (eeData[57] & 0xFC00) >> 10
if alphaSP[1] > 31:
alphaSP[1] -= 64
alphaSP[1] = (1 + alphaSP[1] / 128) * alphaSP[0]
cpKta = eeData[59] & 0x00FF
if cpKta > 127:
cpKta -= 256
ktaScale1 = ((eeData[56] & 0x00F0) >> 4) + 8
self.cpKta = cpKta / math.pow(2, ktaScale1)
cpKv = (eeData[59] & 0xFF00) >> 8
if cpKv > 127:
cpKv -= 256
kvScale = (eeData[56] & 0x0F00) >> 8
self.cpKv = cpKv / math.pow(2, kvScale)
self.cpAlpha[0] = alphaSP[0]
self.cpAlpha[1] = alphaSP[1]
self.cpOffset[0] = offsetSP[0]
self.cpOffset[1] = offsetSP[1]
#print(self.cpAlpha[0])
#print(self.cpAlpha[1])
def _ExtractAlphaParameters(self):
# extract alpha
accRemScale = eeData[32] & 0x000F
accColumnScale = (eeData[32] & 0x00F0) >> 4
accRowScale = (eeData[32] & 0x0F00) >> 8
alphaScale = ((eeData[32] & 0xF000) >> 12) + 30
alphaRef = eeData[33]
accRow = [0] * 24
accColumn = [0] * 32
alphaTemp = [0] * 768
for i in range(6):
p = i * 4
accRow[p + 0] = eeData[34 + i] & 0x000F
accRow[p + 1] = (eeData[34 + i] & 0x00F0) >> 4
accRow[p + 2] = (eeData[34 + i] & 0x0F00) >> 8
accRow[p + 3] = (eeData[34 + i] & 0xF000) >> 12
for i in range(24):
if accRow[i] > 7:
accRow[i] -= 16
for i in range(8):
p = i * 4
accColumn[p + 0] = eeData[40 + i] & 0x000F
accColumn[p + 1] = (eeData[40 + i] & 0x00F0) >> 4
accColumn[p + 2] = (eeData[40 + i] & 0x0F00) >> 8
accColumn[p + 3] = (eeData[40 + i] & 0xF000) >> 12
for i in range(32):
if accColumn[i] > 7:
accColumn[i] -= 16
for i in range(24):
for j in range(32):
p = 32 * i + j
alphaTemp[p] = (eeData[64 + p] & 0x03F0) >> 4
if alphaTemp[p] > 31:
alphaTemp[p] -= 64
alphaTemp[p] *= 1 << accRemScale
alphaTemp[p] += (
alphaRef
+ (accRow[i] << accRowScale)
+ (accColumn[j] << accColumnScale)
)
alphaTemp[p] /= math.pow(2, alphaScale)
alphaTemp[p] -= self.tgc * (self.cpAlpha[0] + self.cpAlpha[1]) / 2
alphaTemp[p] = SCALEALPHA / alphaTemp[p]
# print("alphaTemp: ", alphaTemp)
temp = max(alphaTemp)
#print("temp", temp)
alphaScale = 0
while temp < 32768:
temp *= 2
alphaScale += 1
for i in range(768):
temp = alphaTemp[i] * math.pow(2, alphaScale)
self.alpha[i] = int(temp + 0.5)
self.alphaScale = alphaScale
def _ExtractOffsetParameters(self):
# extract offset
occRow = [0] * 24
occColumn = [0] * 32
occRemScale = eeData[16] & 0x000F
occColumnScale = (eeData[16] & 0x00F0) >> 4
occRowScale = (eeData[16] & 0x0F00) >> 8
offsetRef = eeData[17]
if offsetRef > 32767:
offsetRef -= 65536
for i in range(6):
p = i * 4
occRow[p + 0] = eeData[18 + i] & 0x000F
occRow[p + 1] = (eeData[18 + i] & 0x00F0) >> 4
occRow[p + 2] = (eeData[18 + i] & 0x0F00) >> 8
occRow[p + 3] = (eeData[18 + i] & 0xF000) >> 12
for i in range(24):
if occRow[i] > 7:
occRow[i] -= 16
for i in range(8):
p = i * 4
occColumn[p + 0] = eeData[24 + i] & 0x000F
occColumn[p + 1] = (eeData[24 + i] & 0x00F0) >> 4
occColumn[p + 2] = (eeData[24 + i] & 0x0F00) >> 8
occColumn[p + 3] = (eeData[24 + i] & 0xF000) >> 12
for i in range(32):
if occColumn[i] > 7:
occColumn[i] -= 16
for i in range(24):
for j in range(32):
p = 32 * i + j
self.offset[p] = (eeData[64 + p] & 0xFC00) >> 10
if self.offset[p] > 31:
self.offset[p] -= 64
self.offset[p] *= 1 << occRemScale
self.offset[p] += (
offsetRef
+ (occRow[i] << occRowScale)
+ (occColumn[j] << occColumnScale)
)
def _ExtractKtaPixelParameters(self): # pylint: disable=too-many-locals
# extract KtaPixel
KtaRC = [0] * 4
ktaTemp = [0] * 768
KtaRoCo = (eeData[54] & 0xFF00) >> 8
if KtaRoCo > 127:
KtaRoCo -= 256
KtaRC[0] = KtaRoCo
KtaReCo = eeData[54] & 0x00FF
if KtaReCo > 127:
KtaReCo -= 256
KtaRC[2] = KtaReCo
KtaRoCe = (eeData[55] & 0xFF00) >> 8
if KtaRoCe > 127:
KtaRoCe -= 256
KtaRC[1] = KtaRoCe
KtaReCe = eeData[55] & 0x00FF
if KtaReCe > 127:
KtaReCe -= 256
KtaRC[3] = KtaReCe
ktaScale1 = ((eeData[56] & 0x00F0) >> 4) + 8
ktaScale2 = eeData[56] & 0x000F
for i in range(24):
for j in range(32):
p = 32 * i + j
split = 2 * (p // 32 - (p // 64) * 2) + p % 2
ktaTemp[p] = (eeData[64 + p] & 0x000E) >> 1
if ktaTemp[p] > 3:
ktaTemp[p] -= 8
ktaTemp[p] *= 1 << ktaScale2
ktaTemp[p] += KtaRC[split]
ktaTemp[p] /= math.pow(2, ktaScale1)
# ktaTemp[p] = ktaTemp[p] * mlx90640->offset[p];
temp = abs(ktaTemp[0])
for kta in ktaTemp:
temp = max(temp, abs(kta))
ktaScale1 = 0
while temp < 64:
temp *= 2
ktaScale1 += 1
for i in range(768):
temp = ktaTemp[i] * math.pow(2, ktaScale1)
if temp < 0:
self.kta[i] = int(temp - 0.5)
else:
self.kta[i] = int(temp + 0.5)
self.ktaScale = ktaScale1
def _ExtractKvPixelParameters(self):
KvT = [0] * 4
kvTemp = [0] * 768
KvRoCo = (eeData[52] & 0xF000) >> 12
if KvRoCo > 7:
KvRoCo -= 16
KvT[0] = KvRoCo
KvReCo = (eeData[52] & 0x0F00) >> 8
if KvReCo > 7:
KvReCo -= 16
KvT[2] = KvReCo
KvRoCe = (eeData[52] & 0x00F0) >> 4
if KvRoCe > 7:
KvRoCe -= 16
KvT[1] = KvRoCe
KvReCe = eeData[52] & 0x000F
if KvReCe > 7:
KvReCe -= 16
KvT[3] = KvReCe
kvScale = (eeData[56] & 0x0F00) >> 8
for i in range(24):
for j in range(32):
p = 32 * i + j
split = 2 * (p // 32 - (p // 64) * 2) + p % 2
kvTemp[p] = KvT[split]
kvTemp[p] /= math.pow(2, kvScale)
# kvTemp[p] = kvTemp[p] * mlx90640->offset[p];
temp = abs(kvTemp[0])
for kv in kvTemp:
temp = max(temp, abs(kv))
kvScale = 0
while temp < 64:
temp *= 2
kvScale += 1
for i in range(768):
temp = kvTemp[i] * math.pow(2, kvScale)
if temp < 0:
self.kv[i] = int(temp - 0.5)
else:
self.kv[i] = int(temp + 0.5)
self.kvScale = kvScale
def _ExtractCILCParameters(self):
ilChessC = [0] * 3
self.calibrationModeEE = (eeData[10] & 0x0800) >> 4
self.calibrationModeEE = self.calibrationModeEE ^ 0x80
ilChessC[0] = eeData[53] & 0x003F
if ilChessC[0] > 31:
ilChessC[0] -= 64
ilChessC[0] /= 16.0
ilChessC[1] = (eeData[53] & 0x07C0) >> 6
if ilChessC[1] > 15:
ilChessC[1] -= 32
ilChessC[1] /= 2.0
ilChessC[2] = (eeData[53] & 0xF800) >> 11
if ilChessC[2] > 15:
ilChessC[2] -= 32
ilChessC[2] /= 8.0
self.ilChessC = ilChessC
def _ExtractDeviatingPixels(self):
self.brokenPixels = [0xFFFF] * 5
self.outlierPixels = [0xFFFF] * 5
pixCnt = 0
brokenPixCnt = 0
outlierPixCnt = 0
while (pixCnt < 768) and (brokenPixCnt < 5) and (outlierPixCnt < 5):
if eeData[pixCnt + 64] == 0:
self.brokenPixels[brokenPixCnt] = pixCnt
brokenPixCnt += 1
elif (eeData[pixCnt + 64] & 0x0001) != 0:
self.outlierPixels[outlierPixCnt] = pixCnt
outlierPixCnt += 1
pixCnt += 1
if brokenPixCnt > 4:
raise RuntimeError("More than 4 broken pixels")
if outlierPixCnt > 4:
raise RuntimeError("More than 4 outlier pixels")
if (brokenPixCnt + outlierPixCnt) > 4:
raise RuntimeError("More than 4 faulty pixels")
# print("Found %d broken pixels, %d outliers" % (brokenPixCnt, outlierPixCnt))
# TODO INCOMPLETE
def _I2CWriteWord(self, writeAddress, data):
cmd = bytearray(4)
cmd[0] = writeAddress >> 8
cmd[1] = writeAddress & 0x00FF
cmd[2] = data >> 8
cmd[3] = data & 0x00FF
dataCheck = [0]
with self.i2c_device as i2c:
i2c.write(cmd)
# print("Wrote:", [hex(i) for i in cmd])
time.sleep(0.001)
self._I2CReadWords(writeAddress, dataCheck)
# print("dataCheck: 0x%x" % dataCheck[0])
# if (dataCheck != data):
# return -2
_inbuf = bytearray(2 * I2C_READ_LEN)
def _I2CReadWords(self, addr, buffer, *, end=None):
# stamp = time.monotonic()
if end is None:
remainingWords = len(buffer)
else:
remainingWords = end
offset = 0
addrbuf = bytearray(2)
# inbuf = bytearray(2 * I2C_READ_LEN)
inbuf = self._inbuf
with self.i2c_device as i2c:
while remainingWords:
addrbuf[0] = addr >> 8 # MSB
addrbuf[1] = addr & 0xFF # LSB
read_words = min(remainingWords, I2C_READ_LEN)
i2c.write_then_readinto(
addrbuf, inbuf, in_end=read_words * 2
) # in bytes
# print("-> ", [hex(i) for i in addrbuf])
outwords = struct.unpack(
">" + "H" * read_words, inbuf[0 : read_words * 2]
)
# print("<- (", read_words, ")", [hex(i) for i in outwords])
for i, w in enumerate(outwords):
buffer[offset + i] = w
offset += read_words
remainingWords -= read_words
addr += read_words
ixc = None
mlx = None
frame = None
def init_camera(scl_pin=22, sda_pin=21, freq=100000):
"""Explicitly initializes the I2C bus and camera after power is stable."""
global ixc, mlx, frame
print(f"[Camera] Initializing I2C on SCL:{scl_pin}, SDA:{sda_pin} at {freq}Hz...")
ixc = I2C(pins=(scl_pin, sda_pin), frequency=freq)
print("[Camera] Probing for MLX90640...")
mlx = MLX90640(ixc)
# Bonus: Your wiki snapshot recommends 16Hz for smooth images!
mlx.refresh_rate = RefreshRate.REFRESH_16_HZ
frame = [0] * 768
print("[Camera] Setup successful!")
def read_temperature():
if mlx is None:
print("[Camera] Error: Camera not initialized. Call init_camera() first.")
return None
try:
print("Querying camera...")
mlx.getFrame(frame)
return frame
except Exception as e:
print(f"[Camera] Read error: {e}")
return None
+99 -22
View File
@@ -1,7 +1,12 @@
import json
import threading
import queue
import time
from shared import get_lora, get_mqtt_client, deviceTypes, config
import traceback
from orchestrateur.sensors import gps
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads
from shared.logging import log
from sensors import ultrasonicRanger, temp_hum, button, camera
# --- Read Unique Device ID ---
try:
@@ -49,6 +54,7 @@ mqtt_client = get_mqtt_client(
)
mqtt_client.connect()
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS)
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
print(f"Subscribed to topic: {config.MQTT_TOPIC_SENSOR}")
# --- THE CRUCIAL PAHO FIX ---
@@ -66,49 +72,120 @@ def mqtt_listener():
message = mqtt_client.get_message()
if message:
# Try to parse the payload as a python dictionary, but if it fails, just print the raw payload
try:
payload = json.loads(message['payload'])
except Exception as e:
print(f"Error parsing MQTT payload: {e}")
payload = message['payload'] # Fallback to raw payload if parsing fails
print(f"\n[Thread MQTT] Message reçu : {message}")
data_queue.put({"source": "MQTT", "data": message})
data_queue.put({"source": "MQTT", "topic": message['topic'] ,"data": payload})
# --- THE CPU FIX ---
# Sleep for 100ms. Prevents the thread from turning into an infinite 100% CPU hog.
time.sleep(0.1)
time.sleep(0.2)
# Button
button_state = False
def button_callback():
global button_state
button_state = not button_state
print(f"\n[Thread Button] Button state changed to: {button_state}")
button.set_callback(button_callback)
# Launch background monitoring workers
threading.Thread(target=lora_listener, daemon=True).start()
threading.Thread(target=mqtt_listener, daemon=True).start()
# threading.Thread(target=lora_listener, daemon=True).start()
# threading.Thread(target=mqtt_listener, daemon=True).start()
# Launch button monitoring thread
button.start_button_monitoring_thread()
print("Orchestrateur prêt. Le main loop est libre.")
# Sensor reading
def read_sensors():
"""Read all sensors and return a dictionary of their values."""
log("\nLecture des capteurs...")
sensor_data = {}
# Read Ultrasonic Ranger
distance = ultrasonicRanger.get_dish_height()
if distance is not None:
log(f"\nLecture du capteur Ultrason : {distance}")
sensor_data["ultrasonic_distance"] = distance
# Read Temperature and Humidity
temperature, humidity = temp_hum.get_temperature_and_humidity()
if temperature is not None and humidity is not None:
log(f"\nLecture du capteur Temp/Hum : {temperature}, {humidity}")
sensor_data["temperature"] = temperature
sensor_data["humidity"] = humidity
# Read GPS Data
gps_data = gps.get_gps_data()
if gps_data:
log(f"\nLecture du capteur GPS : {gps_data}")
sensor_data["gps"] = gps_data
# Camera
picture_bytes = None
try:
picture_bytes = camera.get_picture()
log(f"\nLecture du capteur Caméra : {len(picture_bytes)} bytes")
sensor_data["camera_image"] = picture_bytes
except Exception as e:
log(f"Error reading camera data: {e}")
# Read Button State (last because he can still change state while reading other sensors)
sensor_data["button_state"] = button_state
return sensor_data
# --- MAIN EXECUTION LOOP ---
while True:
try:
# Check for non-heartbeat data safely
# Check for non-heartbeat data
try:
msg = data_queue.get(block=False)
print(f"\n[Main Loop] Données traitées : {msg['data']}")
# print(msg)
if msg["source"] == "LoRa":
print(f"\n[Main Loop] LoRa : Données traitées : {msg['data']}")
elif msg["source"] == "MQTT":
if (msg["topic"] == config.MQTT_TOPIC_HELLO.decode('utf-8')):
if ("id_orchestrator" in msg["data"] and msg["data"]["id_orchestrator"] == DEVICE_ID):
# Do not answer to messages coming from me
continue
microwave_id = msg["data"]["id_microwave"]
print(f"\n[Main Loop] MQTT : Hello reçu de {microwave_id}.")
# Responds
mqtt_client.publish(config.MQTT_TOPIC_HELLO, payloads.mqtt_hello_ack(DEVICE_ID, microwave_id), qos=config.MQTT_QOS)
print(f"[Main Loop] MQTT : Réponse Hello envoyée à {microwave_id}.")
# TODO : Save in database
print(f"\n[Main Loop] MQTT : Données traitées : {msg['data']}")
except queue.Empty:
pass
time.sleep(1)
# DEBUG : Read sensors
sensor_values = read_sensors()
if sensor_values:
sensor_values_print = sensor_values.copy()
if "camera_image" in sensor_values_print:
sensor_values_print["camera_image"] = f"<{len(sensor_values_print['camera_image'])} bytes>"
print(f"\nCapteurs Données lues : {sensor_values_print}")
# Publish debug telemetry message
print("[Main Loop] Envoi d'un message de debug sur MQTT...")
response = mqtt_client.publish(
config.MQTT_TOPIC_COOKING,
f"Orchestrateur actif, ID: {DEVICE_ID}",
qos=config.MQTT_QOS
)
# This will now unblock instantly because loop_start() handles the delivery confirmation!
response.wait_for_publish()
print("[Main Loop] Message de debug publié avec succès.")
time.sleep(9)
time.sleep(3)
except KeyboardInterrupt:
break
except Exception as e:
traceback.print_exc()
time.sleep(1) # Prevents rapid error logging in case of persistent issues
# Clean termination
if hasattr(mqtt_client._client, "loop_stop"):
+4
View File
@@ -1,2 +1,6 @@
paho-mqtt>=1.6,<3
pyserial>=3.5,<4
# picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2
# OpenCV
# sudo apt install -y python3-opencv
# sudo apt install -y opencv-data
+7
View File
@@ -0,0 +1,7 @@
# import grovepi
import sensors.ultrasonicRanger as ultrasonicRanger
import sensors.temp_hum as temp_hum
import sensors.button as button
import sensors.gps as gps
import sensors.camera as camera
+44
View File
@@ -0,0 +1,44 @@
import grovepi
import time
import threading
from sensors.lock import grove_lock
from shared.logging import log
button = 2
button_switch_state = 0
grovepi.pinMode(button, "INPUT")
button_callback = None
def read_button_state():
if not grove_lock.acquire(timeout=0.05):
return None
try:
return grovepi.digitalRead(button)
except Exception as e:
log(f"BTN Error: {e}")
return None
finally:
grove_lock.release()
def monitor_button():
global button_switch_state
last_button_state = button_switch_state
while True:
time.sleep(0.04)
current_state = read_button_state()
if current_state is not None:
if current_state == 1 and last_button_state == 0:
if button_callback:
button_callback()
last_button_state = current_state
def start_button_monitoring_thread():
threading.Thread(target=monitor_button, daemon=True).start()
def set_callback(callback):
global button_callback
button_callback = callback
+33
View File
@@ -0,0 +1,33 @@
import grovepi
import math
from sensors.lock import grove_lock
from picamera2 import Picamera2, Preview
import time
picam2 = Picamera2()
camera_config = picam2.create_still_configuration()
picam2.configure(camera_config)
picam2.start()
time.sleep(2)
def preview_camera():
picam2.start_preview(Preview.DRM)
def stop_preview_camera():
picam2.stop_preview()
def take_picture():
"""Takes a picture and saves it to the file system"""
picam2.capture_file("test.jpg")
return "test.jpg"
def get_picture():
"""Returns the image bytes as base64
"""
file_path = take_picture()
with open(file_path, "rb") as f:
image_bytes = f.read()
return image_bytes
+122
View File
@@ -0,0 +1,122 @@
import serial
import time
import threading
from shared.logging import log
from sensors.lock import serial_lock
def calculate_nmea_checksum(line: str) -> bool:
"""Validates standard NMEA 0183 sentence checksum ($...*HH)."""
if not line.startswith('$') or '*' not in line:
return False
try:
content, checksum_str = line[1:].split('*', 1)
calculated_checksum = 0
for char in content:
calculated_checksum ^= ord(char)
return calculated_checksum == int(checksum_str[:2], 16)
except Exception:
return False
class GROVEGPS:
def __init__(self, port='/dev/ttyAMA0', baud=9600, timeout=1):
self.ser = serial.Serial(port, baud, timeout=timeout)
self.clean_data()
def clean_data(self):
self.timestamp = ""
self.quality = 0
self.satellites = 0
self.altitude = -1.0
self.latitude = -1.0
self.longitude = -1.0
def read(self):
"""Reads the latest GGA sentence from serial, thread-safely."""
with serial_lock:
# 1. Flush accumulated stale data in the UART buffer
if self.ser.in_waiting > 0:
self.ser.reset_input_buffer()
# 2. Try reading up to 15 lines to catch the freshest GGA sentence
for _ in range(5):
raw_bytes = self.ser.readline()
try:
line = raw_bytes.decode('utf-8', errors='ignore').strip()
# log(f"GPS: Read line: {line}")
except Exception:
continue
# Supports both $GPGGA and modern $GNGGA sentences
if (line.startswith('$GPGGA') or line.startswith('$GNGGA')) and calculate_nmea_checksum(line):
if self.parse_gga(line):
return True
return False
def parse_gga(self, line):
self.clean_data()
gga = line.split(',')
if len(gga) < 10:
return False
try:
self.timestamp = gga[1]
self.quality = int(gga[6]) if gga[6] != "" else 0
self.satellites = int(gga[7]) if gga[7] != "" else 0
# If quality > 0 and coordinates exist, convert NMEA DDDMM.MMMM to decimal degrees
if self.quality > 0 and gga[2] != "" and gga[4] != "":
lat_raw = float(gga[2])
ns = gga[3]
lon_raw = float(gga[4])
ew = gga[5]
# Latitude calculation
lat_deg = lat_raw // 100
lat_min = lat_raw % 100
self.latitude = lat_deg + (lat_min / 60.0)
if ns == 'S':
self.latitude = -self.latitude
# Longitude calculation
lon_deg = lon_raw // 100
lon_min = lon_raw % 100
self.longitude = lon_deg + (lon_min / 60.0)
if ew == 'W':
self.longitude = -self.longitude
self.altitude = float(gga[9]) if gga[9] != "" else -1.0
return True
else:
# No lock on this line
return True
except (ValueError, IndexError):
return False
# Shared instance
gps = GROVEGPS()
def get_gps_data():
"""Returns GPS dictionary if fix is valid, otherwise returns None."""
has_data = gps.read()
# Strictly check that we have a valid GPS lock (quality > 0 and valid coordinates)
if has_data and gps.quality > 0 and gps.latitude != -1.0:
return {
"timestamp": gps.timestamp,
"latitude": round(gps.latitude, 6),
"longitude": round(gps.longitude, 6),
"altitude": gps.altitude,
"quality": gps.quality,
"satellites": gps.satellites
}
else:
log(f"GPS: No valid fix or data available. Satellites: {gps.satellites}, Quality: {gps.quality}")
# Return None so main.py doesn't process or log empty GPS data
return None
+2
View File
@@ -0,0 +1,2 @@
# import orchestrateur.sensors.lib.grovepi_old as grovepi_old
# import sensors.lib.grove_i2c_temp_hum_mini as grove_i2c_temp_hum_mini
@@ -0,0 +1,87 @@
#!/usr/bin/env python
#
# GrovePi Library for using the Grove - Temperature&Humidity Sensor (http://www.seeedstudio.com/depot/Grove-TemperatureHumidity-Sensor-HighAccuracy-Mini-p-1921.html)
#
# The GrovePi connects the Raspberry Pi and Grove sensors. You can learn more about GrovePi here: http://www.dexterindustries.com/GrovePi
#
# Have a question about this library? Ask on the forums here: http://forum.dexterindustries.com/c/grovepi
#
# Released under the MIT license (http://choosealicense.com/licenses/mit/).
# For more information see https://github.com/DexterInd/GrovePi/blob/master/LICENSE
#################################################################################################################################################
# NOTE:
# The software for this sensor is still in development and might make your GrovePi unuable as long as this sensor is connected with the GrovePi
#################################################################################################################################################
import time,sys
import RPi.GPIO as GPIO
import smbus
debug = 0
# use the bus that matches your raspi version
rev = GPIO.RPI_REVISION
if rev == 2 or rev == 3:
bus = smbus.SMBus(1)
else:
bus = smbus.SMBus(0)
class th02:
ADDRESS = 0x40
TH02_REG_STATUS = 0x00
TH02_REG_DATA_H = 0x01
TH02_REG_DATA_L = 0x02
TH02_REG_CONFIG = 0x03
TH02_REG_ID = 0x11
TH02_STATUS_RDY_MASK = 0x01
TH02_CMD_MEASURE_HUMI = [0x01]
TH02_CMD_MEASURE_TEMP = [0x11]
SUCCESS = 0
def getTemperature(self):
bus.write_i2c_block_data(self.ADDRESS, self.TH02_REG_CONFIG, self.TH02_CMD_MEASURE_TEMP)
while 1:
status=self.getStatus()
if debug:
print("st:",status)
if status:
break
t_raw=bus.read_i2c_block_data(self.ADDRESS, self.TH02_REG_DATA_H,3)
if debug:
print(t_raw)
temperature = (t_raw[1]<<8|t_raw[2])>>2
return (temperature/32.0)-50.0
def getHumidity(self):
bus.write_i2c_block_data(self.ADDRESS, self.TH02_REG_CONFIG, self.TH02_CMD_MEASURE_HUMI)
while 1:
status=self.getStatus()
if debug:
print("st:",status)
if status:
break
t_raw=bus.read_i2c_block_data(self.ADDRESS, self.TH02_REG_DATA_H,3)
if debug:
print(t_raw)
temperature = (t_raw[1]<<8|t_raw[2])>>4
return (temperature/16.0)-24.0
def getStatus(self):
status=bus.read_i2c_block_data(self.ADDRESS, self.TH02_REG_STATUS,1)
if debug:
print(status)
if status[0] & self.TH02_STATUS_RDY_MASK != 1:
return 1
else:
return 0
if __name__ == "__main__":
t= th02()
while True:
print(t.getTemperature(),t.getHumidity())
time.sleep(.5)
+691
View File
@@ -0,0 +1,691 @@
#!/usr/bin/env python
#
# GrovePi Python library
# v1.4
#
# This file provides the basic functions for using the GrovePi
#
# The GrovePi connects the Raspberry Pi and Grove sensors. You can learn more about GrovePi here: http://www.dexterindustries.com/GrovePi
#
# Have a question about this example? Ask on the forums here: http://forum.dexterindustries.com/c/grovepi
#
'''
## License
The MIT License (MIT)
GrovePi for the Raspberry Pi: an open source platform for connecting Grove Sensors to the Raspberry Pi.
Copyright (C) 2017 Dexter Industries
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
'''
# Initial Date: 13 Feb 2014
# Last Updated: 11 Nov 2016
# http://www.dexterindustries.com/
# Author Date Comments
# Karan 13 Feb 2014 Initial Authoring
# 11 Nov 2016 I2C retries added for faster IO
# DHT function updated to look for nan's
__version__ = '1.4.1'
import sys
import time
import math
import struct
import numpy
import di_i2c
def set_bus(bus):
global i2c
i2c = di_i2c.DI_I2C(bus = bus, address = address)
address = 0x04
max_recv_size = 10
set_bus("RPI_1SW")
if sys.version_info<(3,0):
p_version = 2
else:
p_version = 3
# Earliest version of the firmware to work with
works_with_firmware = [
"1.4.0"
]
# interrupt operations
COUNT_CHANGES = 0
COUNT_LOW_DURATION = 1
# interrupt trigger mode
CHANGE = 1
FALLING = 2
RISING = 3
# This allows us to be more specific about which commands contain unused bytes
unused = 0
retries = 10
additional_waiting = 0
# Get firmware version
version_cmd = [8]
# No data is available from the GrovePi
data_not_available_cmd = [23]
# Command Format
# digitalRead() command format header
dRead_cmd = [1]
# digitalWrite() command format header
dWrite_cmd = [2]
# analogRead() command format header
aRead_cmd = [3]
# analogWrite() command format header
aWrite_cmd = [4]
# pinMode() command format header
pMode_cmd = [5]
# Ultrasonic read
uRead_cmd = [7]
# Accelerometer (+/- 1.5g) read
acc_xyz_cmd = [20]
# RTC get time
rtc_getTime_cmd = [30]
# DHT Pro sensor temperature
dht_temp_cmd = [40]
# Grove LED Bar commands
# Initialise
ledBarInit_cmd = [50]
# Set orientation
ledBarOrient_cmd = [51]
# Set level
ledBarLevel_cmd = [52]
# Set single LED
ledBarSetOne_cmd = [53]
# Toggle single LED
ledBarToggleOne_cmd = [54]
# Set all LEDs
ledBarSet_cmd = [55]
# Get current state
ledBarGet_cmd = [56]
# Grove 4 Digit Display commands
# Initialise
fourDigitInit_cmd = [70]
# Set brightness, not visible until next cmd
fourDigitBrightness_cmd = [71]
# Set numeric value without leading zeros
fourDigitValue_cmd = [72]
# Set numeric value with leading zeros
fourDigitValueZeros_cmd = [73]
# Set individual digit
fourDigitIndividualDigit_cmd = [74]
# Set individual leds of a segment
fourDigitIndividualLeds_cmd = [75]
# Set left and right values with colon
fourDigitScore_cmd = [76]
# Analog read for n seconds
fourDigitAnalogRead_cmd = [77]
# Entire display on
fourDigitAllOn_cmd = [78]
# Entire display off
fourDigitAllOff_cmd = [79]
# Grove Chainable RGB LED commands
# Store color for later use
storeColor_cmd = [90]
# Initialise
chainableRgbLedInit_cmd = [91]
# Initialise and test with a simple color
chainableRgbLedTest_cmd = [92]
# Set one or more leds to the stored color by pattern
chainableRgbLedSetPattern_cmd = [93]
# set one or more leds to the stored color by modulo
chainableRgbLedSetModulo_cmd = [94]
# sets leds similar to a bar graph, reversible
chainableRgbLedSetLevel_cmd = [95]
# Read the button from IR sensor
ir_read_cmd = [21]
# Set pin for the IR receiver
ir_recv_pin_cmd = [22]
# Check if there's data coming from the IR receiver
ir_read_isdata = [24]
# Interrupt-based devices
isr_set_cmd = [6]
isr_unset_cmd = [9]
isr_read_cmd = [10]
isr_clear_cmd = [11]
isr_active_cmd = [12]
# Grove Encoders
encoder_read_cmd = [13]
encoder_en_cmd = [14]
encoder_dis_cmd = [15]
# Dust, Encoder & Flow Sensor commands
# dust_sensor_read_cmd=[10]
# dust_sensor_en_cmd=[14]
# dust_sensor_dis_cmd=[15]
# dust_sensor_int_cmd=[9]
# dust_sensor_read_int_cmd=[6]
# flow_read_cmd=[12]
# flow_disable_cmd=[13]
# flow_en_cmd=[18]
# Function declarations of the various functions used for encoding and sending
# data from RPi to Arduino
# Write I2C block to the GrovePi
def write_i2c_block(block, custom_timing = None):
'''
Now catches and raises Keyboard Interrupt that the user is responsible to catch.
'''
counter = 0
reg = block[0]
data = block[1:]
while counter < 3:
try:
i2c.write_reg_list(reg, data)
time.sleep(0.002 + additional_waiting)
return
except KeyboardInterrupt:
raise KeyboardInterrupt
except:
counter += 1
time.sleep(0.003)
continue
# Read I2C block from the GrovePi
def read_i2c_block(no_bytes = max_recv_size):
'''
Now catches and raises Keyboard Interrupt that the user is responsible to catch.
'''
data = data_not_available_cmd
counter = 0
while data[0] in [data_not_available_cmd[0], 255] and counter < 3:
try:
data = i2c.read_list(reg = None, len = no_bytes)
time.sleep(0.002 + additional_waiting)
if counter > 0:
counter = 0
except KeyboardInterrupt:
raise KeyboardInterrupt
except:
counter += 1
time.sleep(0.003)
return data
def read_identified_i2c_block(read_command_id, no_bytes):
data = [-1]
while len(data) <= 1:
data = read_i2c_block(no_bytes + 1)
return data[1:]
# Arduino Digital Read
def digitalRead(pin):
write_i2c_block(dRead_cmd + [pin, unused, unused])
data = read_identified_i2c_block( dRead_cmd, no_bytes = 1)[0]
return data
# Arduino Digital Write
def digitalWrite(pin, value):
write_i2c_block(dWrite_cmd + [pin, value, unused])
read_i2c_block(no_bytes = 1)
return 1
# Read analog value from Pin
def analogRead(pin):
write_i2c_block(aRead_cmd + [pin, unused, unused])
number = read_identified_i2c_block(aRead_cmd, no_bytes = 2)
return number[0] * 256 + number[1]
# Write PWM
def analogWrite(pin, value):
write_i2c_block(aWrite_cmd + [pin, value, unused])
read_i2c_block(no_bytes = 1)
return 1
# Setting Up Pin mode on Arduino
def pinMode(pin, mode):
if mode == "OUTPUT":
write_i2c_block(pMode_cmd + [pin, 1, unused])
elif mode == "INPUT":
write_i2c_block(pMode_cmd + [pin, 0, unused])
read_i2c_block(no_bytes = 1)
return 1
# Read temp in Celsius from Grove Temperature Sensor
def temp(pin, model = '1.0'):
# each of the sensor revisions use different thermistors, each with their own B value constant
if model == '1.2':
bValue = 4250 # sensor v1.2 uses thermistor ??? (assuming NCP18WF104F03RC until SeeedStudio clarifies)
elif model == '1.1':
bValue = 4250 # sensor v1.1 uses thermistor NCP18WF104F03RC
else:
bValue = 3975 # sensor v1.0 uses thermistor TTC3A103*39H
a = analogRead(pin)
resistance = (float)(1023 - a) * 10000 / a
t = (float)(1 / (math.log(resistance / 10000) / bValue + 1 / 298.15) - 273.15)
return t
# Read value from Grove Ultrasonic
def ultrasonicRead(pin):
write_i2c_block(uRead_cmd + [pin, unused, unused])
number = read_identified_i2c_block(uRead_cmd, no_bytes = 2)
return (number[0] * 256 + number[1])
# Read the firmware version
def version():
write_i2c_block(version_cmd + [unused, unused, unused])
number = read_identified_i2c_block(version_cmd, no_bytes = 3)
return "%s.%s.%s" % (number[0], number[1], number[2])
# Read Grove Accelerometer (+/- 1.5g) XYZ value
# Need to investigate why this reports what was read with the previous command
# Doesn't look to be implemented on the GrovePi
def acc_xyz():
write_i2c_block(acc_xyz_cmd + [unused, unused, unused])
number = read_identified_i2c_block(acc_xyz_cmd, no_bytes = 3)
if number[1] > 32:
number[1] = - (number[1] - 224)
if number[2] > 32:
number[2] = - (number[2] - 224)
if number[3] > 32:
number[3] = - (number[3] - 224)
return (number[0], number[1], number[2])
# Read from Grove RTC
# Doesn't look to be implemented on the GrovePi
def rtc_getTime():
write_i2c_block(rtc_getTime_cmd + [unused, unused, unused])
number = read_i2c_block()
return number
# Read and return temperature and humidity from Grove DHT Pro
def dht(pin, module_type):
write_i2c_block(dht_temp_cmd + [pin, module_type, unused])
number = read_identified_i2c_block(dht_temp_cmd, no_bytes = 8)
if p_version==2:
h=''
for element in (number[0:4]):
h+=chr(element)
t_val=struct.unpack('f', h)
t = round(t_val[0], 2)
h = ''
for element in (number[4:8]):
h+=chr(element)
hum_val=struct.unpack('f',h)
hum = round(hum_val[0], 2)
else:
t_val=bytearray(number[0:4])
h_val=bytearray(number[4:8])
t=round(struct.unpack('f',t_val)[0],2)
hum=round(struct.unpack('f',h_val)[0],2)
if t > -100.0 and t <150.0 and hum >= 0.0 and hum<=100.0:
return [t, hum]
else:
return [float('nan'),float('nan')]
# Grove - Infrared Receiver - get the commands received from the Grove IR sensor
def ir_read_signal():
write_i2c_block(ir_read_cmd + [unused, unused, unused])
data_back = read_identified_i2c_block(ir_read_cmd, no_bytes = 7)
return (data_back[0],
data_back[1] + data_back[2] * 256,
data_back[3] + data_back[4] * 256 + data_back[5] * (256 ** 2) + data_back[6] * (256 ** 3))
# Grove - Infrared Receiver - set the pin on which the Grove IR sensor is connected
def ir_recv_pin(pin):
write_i2c_block(ir_recv_pin_cmd + [pin, unused, unused])
read_i2c_block(no_bytes = 1)
# Grove - Infrared Receiver - check if there's any data that hasn't been read so far
def ir_is_data():
write_i2c_block(ir_read_isdata + 3 * [unused])
number = read_identified_i2c_block(ir_read_isdata, no_bytes = 1)
return number[0] != 0
# after a list of numerical values is provided
# the function returns a list with the outlier(or extreme) values removed
# make the std_factor_threshold bigger so that filtering becomes less strict
# and make the std_factor_threshold smaller to get the opposite
def statisticalNoiseReduction(values, std_factor_threshold = 2):
if len(values) == 0:
return []
mean = numpy.mean(values)
standard_deviation = numpy.std(values)
if standard_deviation == 0:
return values
filtered_values = [element for element in values if element > mean - std_factor_threshold * standard_deviation]
filtered_values = [element for element in filtered_values if element < mean + std_factor_threshold * standard_deviation]
return filtered_values
# Grove LED Bar - initialise
# orientation: (0 = red to green, 1 = green to red)
def ledBar_init(pin, orientation):
write_i2c_block(ledBarInit_cmd + [pin, orientation, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove LED Bar - set orientation
# orientation: (0 = red to green, 1 = green to red)
def ledBar_orientation(pin, orientation):
write_i2c_block(ledBarOrient_cmd + [pin, orientation, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove LED Bar - set level
# level: (0-10)
def ledBar_setLevel(pin, level):
write_i2c_block(ledBarLevel_cmd + [pin, level, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove LED Bar - set single led
# led: which led (1-10)
# state: off or on (0-1)
def ledBar_setLed(pin, led, state):
write_i2c_block(ledBarSetOne_cmd + [pin, led, state])
read_i2c_block(no_bytes = 1)
return 1
# Grove LED Bar - toggle single led
# led: which led (1-10)
def ledBar_toggleLed(pin, led):
write_i2c_block(ledBarToggleOne_cmd + [pin, led, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove LED Bar - set all leds
# state: (0-1023) or (0x00-0x3FF) or (0b0000000000-0b1111111111) or (int('0000000000',2)-int('1111111111',2))
def ledBar_setBits(pin, state):
byte1 = state & 255
byte2 = state >> 8
write_i2c_block(ledBarSet_cmd + [pin, byte1, byte2])
read_i2c_block(no_bytes = 1)
return 1
# Grove LED Bar - get current state
# state: (0-1023) a bit for each of the 10 LEDs
def ledBar_getBits(pin):
write_i2c_block(ledBarGet_cmd + [pin, unused, unused])
block = read_identified_i2c_block(ledBarGet_cmd, no_bytes = 2)
return block[0] ^ (block[1] << 8)
# Grove 4 Digit Display - initialise
def fourDigit_init(pin):
write_i2c_block(fourDigitInit_cmd + [pin, unused, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove 4 Digit Display - set numeric value with or without leading zeros
# value: (0-65535) or (0000-FFFF)
def fourDigit_number(pin, value, leading_zero):
# split the value into two bytes so we can render 0000-FFFF on the display
byte1 = value & 255
byte2 = value >> 8
# separate commands to overcome current 4 bytes per command limitation
if (leading_zero):
write_i2c_block(fourDigitValue_cmd + [pin, byte1, byte2])
else:
write_i2c_block(fourDigitValueZeros_cmd + [pin, byte1, byte2])
read_i2c_block(no_bytes = 1)
return 1
# Grove 4 Digit Display - set brightness
# brightness: (0-7)
def fourDigit_brightness(pin, brightness):
# not actually visible until next command is executed
write_i2c_block(fourDigitBrightness_cmd + [pin, brightness, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove 4 Digit Display - set individual segment (0-9,A-F)
# segment: (0-3)
# value: (0-15) or (0-F)
def fourDigit_digit(pin, segment, value):
write_i2c_block(fourDigitIndividualDigit_cmd + [pin, segment, value])
read_i2c_block(no_bytes = 1)
return 1
# Grove 4 Digit Display - set 7 individual leds of a segment
# segment: (0-3)
# leds: (0-255) or (0-0xFF) one bit per led, segment 2 is special, 8th bit is the colon
def fourDigit_segment(pin, segment, leds):
write_i2c_block(fourDigitIndividualLeds_cmd + [pin, segment, leds])
read_i2c_block(no_bytes = 1)
return 1
# Grove 4 Digit Display - set left and right values (0-99), with leading zeros and a colon
# left: (0-255) or (0-FF)
# right: (0-255) or (0-FF)
# colon will be lit
def fourDigit_score(pin, left, right):
write_i2c_block(fourDigitScore_cmd + [pin, left, right])
read_i2c_block(no_bytes = 1)
return 1
# Grove 4 Digit Display - display analogRead value for n seconds, 4 samples per second
# analog: analog pin to read
# duration: analog read for this many seconds
def fourDigit_monitor(pin, analog, duration):
write_i2c_block(fourDigitAnalogRead_cmd + [pin, analog, duration])
read_i2c_block(no_bytes = 1)
time.sleep(duration)
return 1
# Grove 4 Digit Display - turn entire display on (88:88)
def fourDigit_on(pin):
write_i2c_block(fourDigitAllOn_cmd + [pin, unused, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove 4 Digit Display - turn entire display off
def fourDigit_off(pin):
write_i2c_block(fourDigitAllOff_cmd + [pin, unused, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove Chainable RGB LED - store a color for later use
# red: 0-255
# green: 0-255
# blue: 0-255
def storeColor(red, green, blue):
write_i2c_block(storeColor_cmd + [red, green, blue])
read_i2c_block(no_bytes = 1)
return 1
# Grove Chainable RGB LED - initialise
# numLeds: how many leds do you have in the chain
def chainableRgbLed_init(pin, numLeds):
write_i2c_block(chainableRgbLedInit_cmd + [pin, numLeds, unused])
read_i2c_block(no_bytes = 1)
return 1
# Grove Chainable RGB LED - initialise and test with a simple color
# numLeds: how many leds do you have in the chain
# testColor: (0-7) 3 bits in total - a bit for red, green and blue, eg. 0x04 == 0b100 (0bRGB) == rgb(255, 0, 0) == #FF0000 == red
# ie. 0 black, 1 blue, 2 green, 3 cyan, 4 red, 5 magenta, 6 yellow, 7 white
def chainableRgbLed_test(pin, numLeds, testColor):
write_i2c_block(chainableRgbLedTest_cmd + [pin, numLeds, testColor])
read_i2c_block(no_bytes = 1)
return 1
# Grove Chainable RGB LED - set one or more leds to the stored color by pattern
# pattern: (0-3) 0 = this led only, 1 all leds except this led, 2 this led and all leds inwards, 3 this led and all leds outwards
# whichLed: index of led you wish to set counting outwards from the GrovePi, 0 = led closest to the GrovePi
def chainableRgbLed_pattern(pin, pattern, whichLed):
write_i2c_block(chainableRgbLedSetPattern_cmd + [pin, pattern, whichLed])
read_i2c_block(no_bytes = 1)
return 1
# Grove Chainable RGB LED - set one or more leds to the stored color by modulo
# offset: index of led you wish to start at, 0 = led closest to the GrovePi, counting outwards
# divisor: when 1 (default) sets stored color on all leds >= offset, when 2 sets every 2nd led >= offset and so on
def chainableRgbLed_modulo(pin, offset, divisor):
write_i2c_block(chainableRgbLedSetModulo_cmd + [pin, offset, divisor])
read_i2c_block(no_bytes = 1)
return 1
# Grove Chainable RGB LED - sets leds similar to a bar graph, reversible
# level: (0-10) the number of leds you wish to set to the stored color
# reversible (0-1) when 0 counting outwards from GrovePi, 0 = led closest to the GrovePi, otherwise counting inwards
def chainableRgbLed_setLevel(pin, level, reverse):
write_i2c_block(chainableRgbLedSetLevel_cmd + [pin, level, reverse])
read_i2c_block(no_bytes = 1)
return 1
def set_pin_interrupt(pin, ftype, interrupt_mode, period):
'''
Attach an interrupt to a pin.
pin - D2-D8 pins
ftype - 0 for COUNT_CHANGES, 1 for COUNT_LOW_DURATION
interrupt_mode - 1 for CHANGE, 2 for FALLING, 3 for RISING
period - as measured in ms (max 65535 ms)
'''
period_high = period >> 8
period_low = period & 0xff
combined_params = (pin & 0x0f) + ((ftype & 0x03) << 4) + ((interrupt_mode & 0x03) << 6)
write_i2c_block(isr_set_cmd + [combined_params, period_high, period_low])
read_i2c_block(no_bytes = 1)
def unset_pin_interrupt(pin):
'''
Detach an interrupt from a pin.
pin - D2-D8 pins
'''
write_i2c_block(isr_unset_cmd + [pin, unused, unused])
read_i2c_block(no_bytes = 1)
def unset_all_interrupts():
'''
Detach all attached interrupts from all D2-D8 pins.
pin - D2-D8 pins
'''
write_i2c_block(isr_clear_cmd + 3 * [unused])
read_i2c_block(no_bytes = 1)
def is_interrupt_active(pin):
write_i2c_block(isr_active_cmd + [pin, unused, unused])
data = read_identified_i2c_block(isr_active_cmd, no_bytes = 2)
value = data[1] >> pin
return value != 0
def get_active_interrupts():
'''
Get list of attached interrupts for a given pin or all of them.
pin - D2-D8 pins; if it's 255 return the state of all pins
'''
pin = 255
write_i2c_block(isr_active_cmd + [pin, unused, unused])
data = read_identified_i2c_block(isr_active_cmd, no_bytes = 2)
value = data[0] + (data[1] << 8)
active_interrupts = [i for i in range(2 * 8) if ((value >> i) & 0x01)]
return active_interrupts
def read_interrupt_state(pin):
'''
Read number of pulses/changes on given port that occurred within a time period.
pin - D2-D8 pins
'''
write_i2c_block(isr_read_cmd + [pin, unused, unused])
data = read_identified_i2c_block(isr_read_cmd, no_bytes = 4)
value = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24)
return value
def dust_sensor_en(pin = 2, period = 30000):
set_pin_interrupt(pin, ftype=COUNT_LOW_DURATION, interrupt_mode=CHANGE, period=period)
def dust_sensor_dis(pin = 2):
unset_pin_interrupt(pin)
def dust_sensor_read(pin = 2, period = 30000):
'''
By default, the sample rate is set to 1 at every 30 seconds and this
function was written only for that interval.
If you wish to use a different
interval, then use dust_sensor_read_more function. To set a
different interval, use set_dust_sensor_interval function.
'''
lpo = read_interrupt_state(pin)
percentage = 100.0 * lpo / period
concentration = 1.1 * percentage ** 3 - 3.8 * percentage ** 2 + 520 * percentage + 0.62
return lpo, percentage, concentration
def encoder_en(pin = 2, steps = 32):
write_i2c_block(encoder_en_cmd + [pin, steps, unused])
read_i2c_block(no_bytes = 1)
def encoder_dis(pin = 2):
write_i2c_block(encoder_dis_cmd + [pin, unused, unused])
read_i2c_block(no_bytes = 1)
def encoderRead(pin = 2):
write_i2c_block(encoder_read_cmd + [pin, unused, unused])
data = read_identified_i2c_block(encoder_read_cmd, no_bytes = 4)
value = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24)
return value
def flowEnable(pin = 2, period = 2000):
set_pin_interrupt(pin, ftype=COUNT_CHANGES, interrupt_mode=RISING, period=period)
def flowDisable(pin = 2):
unset_pin_interrupt(pin)
def flowRead(pin = 2):
val = read_interrupt_state(pin)
return val
def main():
print("library supports this fw versions: " +
" ".join('{}'.format(k[1]) for k in enumerate(works_with_firmware)))
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
import threading
# Dedicated lock for I2C bus access (used by GrovePi sensors)
grove_lock = threading.Lock()
# Dedicated lock for UART/Serial port access
serial_lock = threading.Lock()
+52
View File
@@ -0,0 +1,52 @@
# from sensors.lib import grove_i2c_temp_hum_mini
# t= grove_i2c_temp_hum_mini.th02()
# def get_temperature():
# """Get the temperature in Celsius from the TH02 sensor."""
# # try:
# return t.getTemperature()
# # except Exception as e:
# # print(f"Error reading temperature: {e}")
# # return None
# def get_humidity():
# """Get the humidity in percentage from the TH02 sensor."""
# # try:
# return t.getHumidity()
# # except Exception as e:
# # print(f"Error reading humidity: {e}")
# # return None
# import seeed_dht
# sensor = seeed_dht.DHT("11", 4) # DHT11 sensor on GPIO pin 4
# def get_humidity_and_temperature():
# humi, temp = sensor.read()
# return humi, temp
# import sensors.lib.grovepi as grovepi
import grovepi
import math
from sensors.lock import grove_lock
# Connect the Grove Temperature & Humidity Sensor Pro to digital port D3
# This example uses the blue colored sensor.
# SIG,NC,VCC,GND
sensor = 3 # The Sensor goes on digital port 3.
# temp_humidity_sensor_type
# Grove Base Kit comes with the blue sensor.
blue = 0 # The Blue colored sensor.
white = 1 # The White colored sensor.
def get_temperature_and_humidity():
with grove_lock:
[temp,humidity] = grovepi.dht(sensor,blue)
if math.isnan(temp) == False and math.isnan(humidity) == False:
return temp, humidity
else:
print("Error reading from DHT sensor")
return None, None
+32
View File
@@ -0,0 +1,32 @@
import grovepi
from sensors.lock import grove_lock
# Connect the Grove Ultrasonic Ranger to digital port D4
# SIG,NC,VCC,GND
ULTRASONIC_RANGER_PORT = 4
def read_ultrasonic_ranger(ultrasonic_ranger=ULTRASONIC_RANGER_PORT):
if not grove_lock.acquire(timeout=1.0):
print("Ultrasonic: Lock acquisition timed out")
return None
try:
return grovepi.ultrasonicRead(ultrasonic_ranger)
except Exception as e:
print(f"Error: {e}")
return None
finally:
grove_lock.release()
def get_dish_height():
"""Returns the height of the dish in centimeters."""
distance = read_ultrasonic_ranger()
if distance is not None:
# Assuming the ultrasonic sensor is mounted at a fixed height above the dish
# and pointing downwards, we can calculate the height of the dish.
# For example, if the sensor is 30 cm above the dish when it's empty:
SENSOR_HEIGHT = 30 # cm
dish_height = SENSOR_HEIGHT - distance
return max(dish_height, 0) # Ensure height is not negative
else:
return None
+1
View File
@@ -3,6 +3,7 @@
import shared.deviceTypes as deviceTypes
import shared.config as config
import shared.payloads as payloads
def get_lora(*args, **kwargs):
from .lora_device import get_lora_device
+5 -2
View File
@@ -1,12 +1,15 @@
DEBUG=True
# LoRa
HEARTBEAT_INTERVAL = 10
HEARTBEAT_INTERVAL = 30
# MQTT
MQTT_BROKER_HOST = "192.168.50.1"
MQTT_TOPIC_HELLO = b"smartwave/hello"
MQTT_TOPIC_SENSOR = b"smartwave/sensor"
MQTT_TOPIC_COOKING = b"smartwave/cooking"
MQTT_KEEPALIVE = 30
USE_TLS = True
MQTT_QOS = 1
# Long because messages are stored into the broker and will be sent when the orchestrator is back online.
MQTT_HELLO_INTERVAL = 30
+6
View File
@@ -0,0 +1,6 @@
from shared.config import DEBUG
def log(message):
"""Log a message to the console if DEBUG is enabled."""
if DEBUG:
print(f"\n{message}")
+37 -5
View File
@@ -13,11 +13,11 @@ except ImportError:
from umqtt.simple import MQTTClient as _MQTTClient
BACKEND_NAME = "umqtt.simple"
IS_MICROPYTHON = True
except ImportError:
try:
from umqtt.robust import MQTTClient as _MQTTClient
BACKEND_NAME = "umqtt.robust"
IS_MICROPYTHON = True
# except ImportError:
# try:
# from umqtt.robust import MQTTClient as _MQTTClient
# BACKEND_NAME = "umqtt.robust"
# IS_MICROPYTHON = True
except ImportError as exc:
raise ImportError("No MQTT client found. Expected paho.mqtt or umqtt.") from exc
@@ -180,6 +180,38 @@ class BrokerClient:
return client.subscribe(topic, qos=qos)
def unsubscribe(self, topic):
client = self.open()
if IS_MICROPYTHON:
import struct
# Ensure the topic is bytes for writing to the socket
topic_bytes = topic if isinstance(topic, bytes) else topic.encode('utf-8')
# 1. Build the MQTT unsubscribe packet header
pkt = bytearray(b"\xa2\0\0\0")
client.pid += 1
# Packet length is: 2 bytes (PID) + 2 bytes (topic length indicator) + topic string length
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic_bytes), client.pid)
# 2. Write the packet to the socket
client.sock.write(pkt)
client._send_str(topic_bytes)
# 3. Wait for the UNSUBACK confirmation frame (0xB0) from the broker
while True:
op = client.wait_msg()
if op == 0xB0:
resp = client.sock.read(3)
assert resp[1] == pkt[2] and resp[2] == pkt[3]
return client
return client
if isinstance(topic, bytes):
topic = topic.decode('utf-8')
return client.unsubscribe(topic)
def _on_micropython_message(self, topic, payload):
self._store_message(topic, payload, None, False)
+23
View File
@@ -0,0 +1,23 @@
try:
import ujson as json
except ImportError:
import json
def as_json(data):
"""Convert a dictionary to a JSON string."""
try:
return json.dumps(data)
except Exception as e:
print("[Payloads] Error converting to JSON:", e)
return "{}" # Return an empty JSON object on error
def mqtt_hello(id_microwave):
return as_json({
"id_microwave": id_microwave
})
def mqtt_hello_ack(id_orchestrator, id_microwave):
return as_json({
"id_microwave": id_microwave,
"id_orchestrator": id_orchestrator
})