UART & Sensors
This commit is contained in:
+13
-10
@@ -20,9 +20,9 @@ db = client["microwave_network_db"]
|
|||||||
cooking_collection = db["cooking_parameters"]
|
cooking_collection = db["cooking_parameters"]
|
||||||
device_network_collection = db["device_network"]
|
device_network_collection = db["device_network"]
|
||||||
|
|
||||||
# Ensure the photo storage directory exists when the app starts
|
# Ensure the camera image storage directory exists when the app starts
|
||||||
PHOTO_DIR = "storage/dishPhotos"
|
CAMERA_IMAGE_DIR = "storage/dishCameraImages"
|
||||||
os.makedirs(PHOTO_DIR, exist_ok=True)
|
os.makedirs(CAMERA_IMAGE_DIR, exist_ok=True)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Routes
|
# Routes
|
||||||
@@ -40,24 +40,24 @@ def cooking_params():
|
|||||||
if not data:
|
if not data:
|
||||||
return jsonify({"error": "Invalid or missing JSON payload"}), 400
|
return jsonify({"error": "Invalid or missing JSON payload"}), 400
|
||||||
|
|
||||||
# 1. Handle the Photo
|
# 1. Handle the Camera Image
|
||||||
photo_b64 = data.get("photo")
|
camera_image_b64 = data.get("camera_image")
|
||||||
if photo_b64:
|
if camera_image_b64:
|
||||||
# Generate a unique filename using UUID to avoid overwriting
|
# Generate a unique filename using UUID to avoid overwriting
|
||||||
filename = f"dish_{uuid.uuid4().hex}.jpg"
|
filename = f"dish_{uuid.uuid4().hex}.jpg"
|
||||||
filepath = os.path.join(PHOTO_DIR, filename)
|
filepath = os.path.join(CAMERA_IMAGE_DIR, filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Decode the base64 string and save it as a binary file
|
# Decode the base64 string and save it as a binary file
|
||||||
with open(filepath, "wb") as f:
|
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
|
# Replace the giant base64 string in the dictionary with the local file path
|
||||||
# so we don't bloat the MongoDB document
|
# so we don't bloat the MongoDB document
|
||||||
data["photo"] = filepath
|
data["camera_image"] = filepath
|
||||||
|
|
||||||
except Exception as e:
|
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
|
# 2. Save to MongoDB
|
||||||
try:
|
try:
|
||||||
@@ -70,6 +70,9 @@ def cooking_params():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
||||||
|
|
||||||
|
# 3. Returns with the cooking parameters
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/device-network", methods=["POST"])
|
@app.route("/device-network", methods=["POST"])
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ RPI_SYSTEMD_SERVICE="smartwave.service"
|
|||||||
|
|
||||||
# Vérification des arguments
|
# Vérification des arguments
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
echo "Usage: ./deploy.sh [wifi|lora|rpi|all]"
|
echo "Usage: ./deploy.sh [wifi|mqtt|lora|rpi|all]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -79,6 +79,9 @@ case $CIBLE in
|
|||||||
"wifi")
|
"wifi")
|
||||||
deploy_to_esp "micro_ondes/esp_wifi" "$PORT_ESP_WIFI" "ESP-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")
|
"lora")
|
||||||
deploy_to_esp "micro_ondes/esp_lora" "$PORT_ESP_LORA" "ESP-LORA"
|
deploy_to_esp "micro_ondes/esp_lora" "$PORT_ESP_LORA" "ESP-LORA"
|
||||||
;;
|
;;
|
||||||
@@ -92,6 +95,6 @@ case $CIBLE in
|
|||||||
# Ajoute les autres ici
|
# Ajoute les autres ici
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Cible inconnue. Utilise 'wifi', 'lora' ou 'all'."
|
echo "Cible inconnue. Utilise 'wifi', 'lora', 'mqtt' ou 'all'."
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
+12
-1
@@ -1,5 +1,16 @@
|
|||||||
|
|
||||||
# LoRa
|
# 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 |
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ while True:
|
|||||||
command = uart_device.read()
|
command = uart_device.read()
|
||||||
print(f"[Main] Received command from WiFi Board: {command}")
|
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
|
# 2. Send local metrics over the wire to the WiFi board every few seconds
|
||||||
# uart_device.send("Data Pack: LoRa Link RSSI -72dBm")
|
# uart_device.send("Data Pack: LoRa Link RSSI -72dBm")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# This file is executed on every boot (including wake-boot from deepsleep)
|
# This file is executed on every boot (including wake-boot from deepsleep)
|
||||||
#import esp
|
import esp
|
||||||
#esp.osdebug(None)
|
esp.osdebug(True)
|
||||||
#import webrepl
|
#import webrepl
|
||||||
#webrepl.start()
|
#webrepl.start()
|
||||||
|
|
||||||
@@ -16,4 +16,4 @@ def do_connect(ssid, pwd):
|
|||||||
print('network config:', sta_if.ifconfig())
|
print('network config:', sta_if.ifconfig())
|
||||||
|
|
||||||
# Attempt to connect to WiFi network
|
# Attempt to connect to WiFi network
|
||||||
do_connect("Smartwave-1", 'Smartwave-prot-1')
|
# do_connect("Smartwave-1", 'Smartwave-prot-1')
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import _thread
|
import _thread
|
||||||
import select
|
import select
|
||||||
from machine import Pin
|
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 time
|
||||||
|
import ujson as json
|
||||||
|
import sys
|
||||||
|
|
||||||
# Simple thread-safe queue list
|
# Simple thread-safe queue list
|
||||||
msg_queue = []
|
msg_queue = []
|
||||||
@@ -13,17 +16,22 @@ def queue_publish(topic, payload):
|
|||||||
with queue_lock:
|
with queue_lock:
|
||||||
msg_queue.append((topic, payload))
|
msg_queue.append((topic, payload))
|
||||||
|
|
||||||
# --- Hardware & Client Setup ---
|
# --- INITIALIZE CAMERA ---
|
||||||
vext = Pin(19, Pin.OUT)
|
try:
|
||||||
vext.value(0)
|
# Pass your confirmed working SCL and SDA pins here
|
||||||
time.sleep_ms(100)
|
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:
|
try:
|
||||||
with open("device_id.txt", "r") as f:
|
with open("device_id.txt", "r") as f:
|
||||||
DEVICE_ID = f.read().strip()
|
DEVICE_ID = f.read().strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
DEVICE_ID = "ESP32_Inconnu"
|
DEVICE_ID = "ESP32_Inconnu"
|
||||||
|
|
||||||
|
# --- MQTT SETUP ---
|
||||||
MQTT_CA_FILE = "/certs/ca.crt"
|
MQTT_CA_FILE = "/certs/ca.crt"
|
||||||
|
|
||||||
mqtt_client = get_mqtt_client(
|
mqtt_client = get_mqtt_client(
|
||||||
@@ -34,8 +42,28 @@ mqtt_client = get_mqtt_client(
|
|||||||
keepalive=config.MQTT_KEEPALIVE,
|
keepalive=config.MQTT_KEEPALIVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
global orchestrator_id
|
||||||
|
orchestrator_id = None
|
||||||
def on_mqtt_message(message):
|
def on_mqtt_message(message):
|
||||||
print("[MQTT Thread] Received 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)
|
mqtt_client.set_callback(on_mqtt_message)
|
||||||
|
|
||||||
@@ -73,7 +101,7 @@ def mqtt_background_thread():
|
|||||||
|
|
||||||
# 3. Handle Keepalive tracking manually
|
# 3. Handle Keepalive tracking manually
|
||||||
if time.time() - last_check >= 15:
|
if time.time() - last_check >= 15:
|
||||||
print("[Thread] Sending keepalive ping...")
|
# print("[Thread] Sending keepalive ping...")
|
||||||
mqtt_client._client.ping()
|
mqtt_client._client.ping()
|
||||||
last_check = time.time()
|
last_check = time.time()
|
||||||
|
|
||||||
@@ -82,6 +110,7 @@ def mqtt_background_thread():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[Thread] Connection dropped or error encountered:", e)
|
print("[Thread] Connection dropped or error encountered:", e)
|
||||||
|
sys.print_exception(e)
|
||||||
print("[Thread] Cleaning up socket context. Retrying in 5 seconds...")
|
print("[Thread] Cleaning up socket context. Retrying in 5 seconds...")
|
||||||
|
|
||||||
# --- FIX FOR ERROR 23 (SOCKET LEAK) ---
|
# --- FIX FOR ERROR 23 (SOCKET LEAK) ---
|
||||||
@@ -99,29 +128,54 @@ def mqtt_background_thread():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
# --- 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")
|
||||||
|
|
||||||
# UART
|
time.sleep(5) # Fast responsive polling loop for local UART
|
||||||
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
|
|
||||||
|
except Exception as e:
|
||||||
|
print("[Thread] UART error encountered:", e)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
# --- Launch background worker ---
|
# --- 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) ---
|
# --- MAIN APPLICATION THREAD (Core 0) ---
|
||||||
print("[Main] Main execution path active.")
|
print("[Main] Main execution path active.")
|
||||||
time.sleep(2) # Give the thread a moment to initial connect
|
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:
|
while True:
|
||||||
print("[Main] Queueing a test message for MQTT...")
|
# MQTT HELLO sent every x seconds until we get a response from the orchestrator
|
||||||
# Instead of direct publishing, push it to the queue safely
|
if (orchestrator_id == None and -(mqtt_hello_sent_timestamp - time.time()) > config.MQTT_HELLO_INTERVAL):
|
||||||
queue_publish(config.MQTT_TOPIC_SENSOR, "Hello from ESP32!")
|
print("[Main] Attempting to send initial hello to orchestrator...")
|
||||||
|
queue_publish(config.MQTT_TOPIC_HELLO, payloads.mqtt_hello(DEVICE_ID))
|
||||||
# 1. Check if the Heltec V3 sent us something over the wire
|
mqtt_hello_sent_timestamp = time.time()
|
||||||
while uart_device.any():
|
pass
|
||||||
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
|
# 2. Example: Send data to the Heltec board every 5 seconds
|
||||||
# uart_device.send("Status Check: WiFi Active")
|
# uart_device.send("Status Check: WiFi Active")
|
||||||
|
time.sleep(1)
|
||||||
time.sleep_ms(200) # Fast responsive polling loop for local UART
|
|
||||||
@@ -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
|
||||||
+101
-24
@@ -1,7 +1,12 @@
|
|||||||
|
import json
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import time
|
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 ---
|
# --- Read Unique Device ID ---
|
||||||
try:
|
try:
|
||||||
@@ -49,6 +54,7 @@ mqtt_client = get_mqtt_client(
|
|||||||
)
|
)
|
||||||
mqtt_client.connect()
|
mqtt_client.connect()
|
||||||
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS)
|
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}")
|
print(f"Subscribed to topic: {config.MQTT_TOPIC_SENSOR}")
|
||||||
|
|
||||||
# --- THE CRUCIAL PAHO FIX ---
|
# --- THE CRUCIAL PAHO FIX ---
|
||||||
@@ -66,51 +72,122 @@ def mqtt_listener():
|
|||||||
message = mqtt_client.get_message()
|
message = mqtt_client.get_message()
|
||||||
|
|
||||||
if 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}")
|
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.
|
# 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
|
# Launch background monitoring workers
|
||||||
threading.Thread(target=lora_listener, daemon=True).start()
|
# threading.Thread(target=lora_listener, daemon=True).start()
|
||||||
threading.Thread(target=mqtt_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.")
|
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 ---
|
# --- MAIN EXECUTION LOOP ---
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# Check for non-heartbeat data safely
|
# Check for non-heartbeat data
|
||||||
try:
|
try:
|
||||||
msg = data_queue.get(block=False)
|
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:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(3)
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
time.sleep(1) # Prevents rapid error logging in case of persistent issues
|
||||||
|
|
||||||
# Clean termination
|
# Clean termination
|
||||||
if hasattr(mqtt_client._client, "loop_stop"):
|
if hasattr(mqtt_client._client, "loop_stop"):
|
||||||
mqtt_client._client.loop_stop()
|
mqtt_client._client.loop_stop()
|
||||||
mqtt_client.close()
|
mqtt_client.close()
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
paho-mqtt>=1.6,<3
|
paho-mqtt>=1.6,<3
|
||||||
pyserial>=3.5,<4
|
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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import shared.deviceTypes as deviceTypes
|
import shared.deviceTypes as deviceTypes
|
||||||
import shared.config as config
|
import shared.config as config
|
||||||
|
import shared.payloads as payloads
|
||||||
|
|
||||||
def get_lora(*args, **kwargs):
|
def get_lora(*args, **kwargs):
|
||||||
from .lora_device import get_lora_device
|
from .lora_device import get_lora_device
|
||||||
|
|||||||
+6
-3
@@ -1,12 +1,15 @@
|
|||||||
|
DEBUG=True
|
||||||
|
|
||||||
# LoRa
|
# LoRa
|
||||||
HEARTBEAT_INTERVAL = 10
|
HEARTBEAT_INTERVAL = 30
|
||||||
|
|
||||||
# MQTT
|
# MQTT
|
||||||
MQTT_BROKER_HOST = "192.168.50.1"
|
MQTT_BROKER_HOST = "192.168.50.1"
|
||||||
|
MQTT_TOPIC_HELLO = b"smartwave/hello"
|
||||||
MQTT_TOPIC_SENSOR = b"smartwave/sensor"
|
MQTT_TOPIC_SENSOR = b"smartwave/sensor"
|
||||||
MQTT_TOPIC_COOKING = b"smartwave/cooking"
|
MQTT_TOPIC_COOKING = b"smartwave/cooking"
|
||||||
MQTT_KEEPALIVE = 30
|
MQTT_KEEPALIVE = 30
|
||||||
USE_TLS = True
|
USE_TLS = True
|
||||||
MQTT_QOS = 1
|
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
|
||||||
@@ -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}")
|
||||||
+39
-7
@@ -13,13 +13,13 @@ except ImportError:
|
|||||||
from umqtt.simple import MQTTClient as _MQTTClient
|
from umqtt.simple import MQTTClient as _MQTTClient
|
||||||
BACKEND_NAME = "umqtt.simple"
|
BACKEND_NAME = "umqtt.simple"
|
||||||
IS_MICROPYTHON = True
|
IS_MICROPYTHON = True
|
||||||
except ImportError:
|
# except ImportError:
|
||||||
try:
|
# try:
|
||||||
from umqtt.robust import MQTTClient as _MQTTClient
|
# from umqtt.robust import MQTTClient as _MQTTClient
|
||||||
BACKEND_NAME = "umqtt.robust"
|
# BACKEND_NAME = "umqtt.robust"
|
||||||
IS_MICROPYTHON = True
|
# IS_MICROPYTHON = True
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise ImportError("No MQTT client found. Expected paho.mqtt or umqtt.") from exc
|
raise ImportError("No MQTT client found. Expected paho.mqtt or umqtt.") from exc
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PORT = 8884
|
DEFAULT_PORT = 8884
|
||||||
@@ -179,6 +179,38 @@ class BrokerClient:
|
|||||||
topic = topic.decode('utf-8')
|
topic = topic.decode('utf-8')
|
||||||
|
|
||||||
return client.subscribe(topic, qos=qos)
|
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):
|
def _on_micropython_message(self, topic, payload):
|
||||||
self._store_message(topic, payload, None, False)
|
self._store_message(topic, payload, None, False)
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user