UART & Sensors
This commit is contained in:
+13
-10
@@ -20,9 +20,9 @@ db = client["microwave_network_db"]
|
||||
cooking_collection = db["cooking_parameters"]
|
||||
device_network_collection = db["device_network"]
|
||||
|
||||
# Ensure the photo storage directory exists when the app starts
|
||||
PHOTO_DIR = "storage/dishPhotos"
|
||||
os.makedirs(PHOTO_DIR, exist_ok=True)
|
||||
# Ensure the camera image storage directory exists when the app starts
|
||||
CAMERA_IMAGE_DIR = "storage/dishCameraImages"
|
||||
os.makedirs(CAMERA_IMAGE_DIR, exist_ok=True)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Routes
|
||||
@@ -40,24 +40,24 @@ def cooking_params():
|
||||
if not data:
|
||||
return jsonify({"error": "Invalid or missing JSON payload"}), 400
|
||||
|
||||
# 1. Handle the Photo
|
||||
photo_b64 = data.get("photo")
|
||||
if photo_b64:
|
||||
# 1. Handle the Camera Image
|
||||
camera_image_b64 = data.get("camera_image")
|
||||
if camera_image_b64:
|
||||
# Generate a unique filename using UUID to avoid overwriting
|
||||
filename = f"dish_{uuid.uuid4().hex}.jpg"
|
||||
filepath = os.path.join(PHOTO_DIR, filename)
|
||||
filepath = os.path.join(CAMERA_IMAGE_DIR, filename)
|
||||
|
||||
try:
|
||||
# Decode the base64 string and save it as a binary file
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(base64.b64decode(photo_b64))
|
||||
f.write(base64.b64decode(camera_image_b64))
|
||||
|
||||
# Replace the giant base64 string in the dictionary with the local file path
|
||||
# so we don't bloat the MongoDB document
|
||||
data["photo"] = filepath
|
||||
data["camera_image"] = filepath
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Failed to save photo: {str(e)}"}), 500
|
||||
return jsonify({"error": f"Failed to save camera image: {str(e)}"}), 500
|
||||
|
||||
# 2. Save to MongoDB
|
||||
try:
|
||||
@@ -71,6 +71,9 @@ def cooking_params():
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
||||
|
||||
# 3. Returns with the cooking parameters
|
||||
|
||||
|
||||
|
||||
@app.route("/device-network", methods=["POST"])
|
||||
def device_network():
|
||||
|
||||
@@ -14,7 +14,7 @@ RPI_SYSTEMD_SERVICE="smartwave.service"
|
||||
|
||||
# Vérification des arguments
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: ./deploy.sh [wifi|lora|rpi|all]"
|
||||
echo "Usage: ./deploy.sh [wifi|mqtt|lora|rpi|all]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -79,6 +79,9 @@ case $CIBLE in
|
||||
"wifi")
|
||||
deploy_to_esp "micro_ondes/esp_wifi" "$PORT_ESP_WIFI" "ESP-WIFI"
|
||||
;;
|
||||
"mqtt")
|
||||
deploy_to_esp "micro_ondes/esp_wifi" "$PORT_ESP_WIFI" "ESP-WIFI"
|
||||
;;
|
||||
"lora")
|
||||
deploy_to_esp "micro_ondes/esp_lora" "$PORT_ESP_LORA" "ESP-LORA"
|
||||
;;
|
||||
@@ -92,6 +95,6 @@ case $CIBLE in
|
||||
# Ajoute les autres ici
|
||||
;;
|
||||
*)
|
||||
echo "Cible inconnue. Utilise 'wifi', 'lora' ou 'all'."
|
||||
echo "Cible inconnue. Utilise 'wifi', 'lora', 'mqtt' ou 'all'."
|
||||
;;
|
||||
esac
|
||||
+12
-1
@@ -1,5 +1,16 @@
|
||||
|
||||
# LoRa
|
||||
|
||||
`mpremote connect /dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0 repl`
|
||||
`mpremote connect /dev/serial/by-path/pci-0000:00:14.0-usb-0:6.1:1.0-port0 repl`
|
||||
|
||||
# MQTT
|
||||
|
||||
`mpremote connect /dev/serial/by-path/pci-0000:00:14.0-usb-0:6.2:1.0-port0 repl`
|
||||
|
||||
# UART
|
||||
|
||||
| LoRa | MQTT |
|
||||
| --- | --- |
|
||||
| 45 | P17 |
|
||||
| 46 | P16 |
|
||||
| GND | GND |
|
||||
|
||||
@@ -61,7 +61,7 @@ while True:
|
||||
command = uart_device.read()
|
||||
print(f"[Main] Received command from WiFi Board: {command}")
|
||||
|
||||
uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}")
|
||||
# uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}")
|
||||
|
||||
# 2. Send local metrics over the wire to the WiFi board every few seconds
|
||||
# uart_device.send("Data Pack: LoRa Link RSSI -72dBm")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file is executed on every boot (including wake-boot from deepsleep)
|
||||
#import esp
|
||||
#esp.osdebug(None)
|
||||
import esp
|
||||
esp.osdebug(True)
|
||||
#import webrepl
|
||||
#webrepl.start()
|
||||
|
||||
@@ -16,4 +16,4 @@ def do_connect(ssid, pwd):
|
||||
print('network config:', sta_if.ifconfig())
|
||||
|
||||
# Attempt to connect to WiFi network
|
||||
do_connect("Smartwave-1", 'Smartwave-prot-1')
|
||||
# do_connect("Smartwave-1", 'Smartwave-prot-1')
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import _thread
|
||||
import select
|
||||
from machine import Pin
|
||||
from shared import get_mqtt_client, get_uart, config
|
||||
from sensors import temperature_gun
|
||||
from shared import get_mqtt_client, get_uart, config, payloads
|
||||
import time
|
||||
import ujson as json
|
||||
import sys
|
||||
|
||||
# Simple thread-safe queue list
|
||||
msg_queue = []
|
||||
@@ -13,17 +16,22 @@ def queue_publish(topic, payload):
|
||||
with queue_lock:
|
||||
msg_queue.append((topic, payload))
|
||||
|
||||
# --- Hardware & Client Setup ---
|
||||
vext = Pin(19, Pin.OUT)
|
||||
vext.value(0)
|
||||
time.sleep_ms(100)
|
||||
# --- INITIALIZE CAMERA ---
|
||||
try:
|
||||
# Pass your confirmed working SCL and SDA pins here
|
||||
temperature_gun.init_camera(scl_pin=21, sda_pin=22, freq=100000)
|
||||
except Exception as e:
|
||||
print("[Main] Critical: Camera setup failed!")
|
||||
sys.print_exception(e)
|
||||
|
||||
# --- READ DEVICE ID ---
|
||||
try:
|
||||
with open("device_id.txt", "r") as f:
|
||||
DEVICE_ID = f.read().strip()
|
||||
except Exception:
|
||||
DEVICE_ID = "ESP32_Inconnu"
|
||||
|
||||
# --- MQTT SETUP ---
|
||||
MQTT_CA_FILE = "/certs/ca.crt"
|
||||
|
||||
mqtt_client = get_mqtt_client(
|
||||
@@ -34,9 +42,29 @@ mqtt_client = get_mqtt_client(
|
||||
keepalive=config.MQTT_KEEPALIVE,
|
||||
)
|
||||
|
||||
global orchestrator_id
|
||||
orchestrator_id = None
|
||||
def on_mqtt_message(message):
|
||||
print("[MQTT Thread] Received message:", message)
|
||||
|
||||
# Try and parse the payload as json, but if it fails, just print the raw payload
|
||||
payload_data=None
|
||||
try:
|
||||
payload_data = json.loads(message['payload'])
|
||||
except Exception as e:
|
||||
print("[MQTT Thread] Error parsing JSON:", e)
|
||||
sys.print_exception(e)
|
||||
pass # Maybe it's not JSON
|
||||
|
||||
if message['topic'] == config.MQTT_TOPIC_HELLO and payload_data and "id_orchestrator" in payload_data and payload_data["id_microwave"] == DEVICE_ID:
|
||||
print("[MQTT Thread] Hello response received from orchestrator:", payload_data["id_orchestrator"])
|
||||
global orchestrator_id
|
||||
orchestrator_id = payload_data["id_orchestrator"]
|
||||
# Unsubscribe from the hello topic since we got a response
|
||||
mqtt_client.unsubscribe(config.MQTT_TOPIC_HELLO)
|
||||
print("[MQTT Thread] Unsubscribed from topic:", config.MQTT_TOPIC_HELLO)
|
||||
print("[MQTT Thread] Message processing complete.")
|
||||
|
||||
mqtt_client.set_callback(on_mqtt_message)
|
||||
|
||||
|
||||
@@ -73,7 +101,7 @@ def mqtt_background_thread():
|
||||
|
||||
# 3. Handle Keepalive tracking manually
|
||||
if time.time() - last_check >= 15:
|
||||
print("[Thread] Sending keepalive ping...")
|
||||
# print("[Thread] Sending keepalive ping...")
|
||||
mqtt_client._client.ping()
|
||||
last_check = time.time()
|
||||
|
||||
@@ -82,6 +110,7 @@ def mqtt_background_thread():
|
||||
|
||||
except Exception as e:
|
||||
print("[Thread] Connection dropped or error encountered:", e)
|
||||
sys.print_exception(e)
|
||||
print("[Thread] Cleaning up socket context. Retrying in 5 seconds...")
|
||||
|
||||
# --- FIX FOR ERROR 23 (SOCKET LEAK) ---
|
||||
@@ -100,28 +129,53 @@ def mqtt_background_thread():
|
||||
pass
|
||||
time.sleep(5)
|
||||
|
||||
# UART
|
||||
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
|
||||
# --- UART BACKGROUND THREAD ---
|
||||
def uart_background_thread():
|
||||
"""Background UART worker handling all serial operations safely."""
|
||||
print("[Thread] Background UART worker started.")
|
||||
|
||||
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# 1. Check for incoming messages from the Heltec board
|
||||
while uart_device.any():
|
||||
incoming_msg = uart_device.read()
|
||||
print(f"[Thread] Received from esp-lora over UART: {incoming_msg}")
|
||||
|
||||
# 2. Example: Send data to the Heltec board every 5 seconds
|
||||
# uart_device.send("Status Check: WiFi Active")
|
||||
|
||||
time.sleep(5) # Fast responsive polling loop for local UART
|
||||
|
||||
except Exception as e:
|
||||
print("[Thread] UART error encountered:", e)
|
||||
time.sleep(5)
|
||||
|
||||
# --- Launch background worker ---
|
||||
_thread.start_new_thread(mqtt_background_thread, ())
|
||||
# _thread.start_new_thread(mqtt_background_thread, ())
|
||||
# _thread.start_new_thread(uart_background_thread, ())
|
||||
|
||||
|
||||
# --- MAIN APPLICATION THREAD (Core 0) ---
|
||||
print("[Main] Main execution path active.")
|
||||
time.sleep(2) # Give the thread a moment to initial connect
|
||||
mqtt_hello_sent_timestamp = -config.MQTT_HELLO_INTERVAL
|
||||
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
||||
|
||||
while True:
|
||||
print("[Main] Queueing a test message for MQTT...")
|
||||
# Instead of direct publishing, push it to the queue safely
|
||||
queue_publish(config.MQTT_TOPIC_SENSOR, "Hello from ESP32!")
|
||||
# MQTT HELLO sent every x seconds until we get a response from the orchestrator
|
||||
if (orchestrator_id == None and -(mqtt_hello_sent_timestamp - time.time()) > config.MQTT_HELLO_INTERVAL):
|
||||
print("[Main] Attempting to send initial hello to orchestrator...")
|
||||
queue_publish(config.MQTT_TOPIC_HELLO, payloads.mqtt_hello(DEVICE_ID))
|
||||
mqtt_hello_sent_timestamp = time.time()
|
||||
pass
|
||||
|
||||
# 1. Check if the Heltec V3 sent us something over the wire
|
||||
while uart_device.any():
|
||||
incoming_msg = uart_device.read()
|
||||
print(f"[Main] Received from esp-lora over UART: {incoming_msg}")
|
||||
# Sensors
|
||||
print(f"[Main] Reading temperature from the gun sensor...")
|
||||
temp = temperature_gun.read_temperature()
|
||||
print(f"[Main] Temperature read: {temp}°C")
|
||||
|
||||
# 2. Example: Send data to the Heltec board every 5 seconds
|
||||
# uart_device.send("Status Check: WiFi Active")
|
||||
|
||||
time.sleep_ms(200) # Fast responsive polling loop for local UART
|
||||
time.sleep(1)
|
||||
@@ -0,0 +1 @@
|
||||
import sensors.temperature_gun as temperature_gun
|
||||
@@ -0,0 +1,969 @@
|
||||
"""
|
||||
Temperatue gun sensor module
|
||||
using the MLX90640-D55/D110 sensor. This module provides a function to read the temperature from the gun sensor.
|
||||
Resolution of 32x24 pixels,
|
||||
I2C interface
|
||||
Noise Equivalent Temperature difference (NETD) is 0.1K RMS @ 1Hz refresh rate
|
||||
"""
|
||||
|
||||
import machine # type: ignore
|
||||
import math
|
||||
import struct
|
||||
import time
|
||||
from micropython import const# Some libraries that we will use
|
||||
import time
|
||||
|
||||
|
||||
class RefreshRate: # pylint: disable=too-few-public-methods
|
||||
""" Enum-like class for MLX90640's refresh rate """
|
||||
REFRESH_0_5_HZ = const(0b000) # 0.5Hz
|
||||
REFRESH_1_HZ = const(0b001) # 1Hz
|
||||
REFRESH_2_HZ = const(0b010) # 2Hz
|
||||
REFRESH_4_HZ = const(0b011) # 4Hz
|
||||
REFRESH_8_HZ = const(0b100) # 8Hz
|
||||
REFRESH_16_HZ = const(0b101) # 16Hz
|
||||
REFRESH_32_HZ = const(0b110) # 32Hz
|
||||
REFRESH_64_HZ = const(0b111) # 64Hz
|
||||
|
||||
class ContextManaged:
|
||||
"""An object that automatically deinitializes hardware with a context manager."""
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.deinit()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def deinit(self):
|
||||
"""Free any hardware used by the object."""
|
||||
return
|
||||
|
||||
class Lockable(ContextManaged):
|
||||
"""An object that must be locked to prevent collisions on a microcontroller resource."""
|
||||
|
||||
_locked = False
|
||||
|
||||
def try_lock(self):
|
||||
"""Attempt to grab the lock. Return True on success, False if the lock is already taken."""
|
||||
if self._locked:
|
||||
return False
|
||||
self._locked = True
|
||||
return True
|
||||
|
||||
def unlock(self):
|
||||
"""Release the lock so others may use the resource."""
|
||||
if self._locked:
|
||||
self._locked = False
|
||||
else:
|
||||
raise ValueError("Not locked")
|
||||
|
||||
class I2C(Lockable):
|
||||
def __init__(self, pins=(21, 22), frequency=100000):
|
||||
self.init(pins, frequency)
|
||||
|
||||
def init(self, pins, frequency):
|
||||
self.deinit()
|
||||
|
||||
# 1. Force the ESP32 to activate its internal pull-up resistors on these pins
|
||||
self._pins = (
|
||||
machine.Pin(int(pins[0]), machine.Pin.IN, machine.Pin.PULL_UP),
|
||||
machine.Pin(int(pins[1]), machine.Pin.IN, machine.Pin.PULL_UP)
|
||||
)
|
||||
|
||||
try:
|
||||
# 2. Bypasses the glitchy ESP32 hardware block using SoftI2C
|
||||
# (Note: SoftI2C does not take a bus ID number like '0')
|
||||
self._i2c = machine.SoftI2C(scl=self._pins[0], sda=self._pins[1], freq=frequency)
|
||||
except RuntimeError:
|
||||
raise
|
||||
print(f"Created resilient SoftI2C: {self._i2c}")
|
||||
|
||||
def deinit(self):
|
||||
try:
|
||||
del self._i2c
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def scan(self):
|
||||
return self._i2c.scan()
|
||||
|
||||
def readfrom_into(self, address, buffer, *, start=0, end=None):
|
||||
if start is not 0 or end is not None:
|
||||
if end is None:
|
||||
end = len(buffer)
|
||||
buffer = memoryview(buffer)[start:end]
|
||||
stop = True # remove for efficiency later
|
||||
return self._i2c.readfrom_into(address, buffer)
|
||||
|
||||
def writeto(self, address, buffer, *, start=0, end=None, stop=True):
|
||||
if isinstance(buffer, str):
|
||||
buffer = bytes([ord(x) for x in buffer])
|
||||
if start is not 0 or end is not None:
|
||||
if end is None:
|
||||
return self._i2c.writeto(address, memoryview(buffer)[start:], stop)
|
||||
else:
|
||||
return self._i2c.writeto(address, memoryview(buffer)[start:end], stop)
|
||||
return self._i2c.writeto(address, buffer, stop)
|
||||
|
||||
class I2CDevice:
|
||||
def __init__(self, i2c, device_address, probe=True):
|
||||
self.i2c = i2c
|
||||
self._has_write_read = False # hasattr(self.i2c, "writeto_then_readfrom") --> has been turned to False
|
||||
self.device_address = device_address
|
||||
|
||||
if probe:
|
||||
self.__probe_for_device()
|
||||
|
||||
def readinto(self, buf, *, start=0, end=None):
|
||||
if end is None:
|
||||
end = len(buf)
|
||||
self.i2c.readfrom_into(self.device_address, buf, start=start, end=end)
|
||||
|
||||
def write(self, buf, *, start=0, end=None, stop=True):
|
||||
if end is None:
|
||||
end = len(buf)
|
||||
self.i2c.writeto(self.device_address, buf, start=start, end=end, stop=stop)
|
||||
|
||||
# pylint: disable-msg=too-many-arguments
|
||||
def write_then_readinto(
|
||||
self,
|
||||
out_buffer,
|
||||
in_buffer,
|
||||
*,
|
||||
out_start=0,
|
||||
out_end=None,
|
||||
in_start=0,
|
||||
in_end=None,
|
||||
stop=False
|
||||
):
|
||||
if out_end is None:
|
||||
out_end = len(out_buffer)
|
||||
if in_end is None:
|
||||
in_end = len(in_buffer)
|
||||
if stop:
|
||||
raise ValueError("Stop must be False. Use writeto instead.")
|
||||
if self._has_write_read:
|
||||
#print("c",dir(self.i2c))
|
||||
# In linux, at least, this is a special kernel function call
|
||||
self.i2c.writeto_then_readfrom(
|
||||
self.device_address,
|
||||
out_buffer,
|
||||
in_buffer,
|
||||
out_start=out_start,
|
||||
out_end=out_end,
|
||||
in_start=in_start,
|
||||
in_end=in_end,
|
||||
)
|
||||
|
||||
else:
|
||||
# If we don't have a special implementation, we can fake it with two calls
|
||||
self.i2c.writeto(self.device_address, out_buffer, stop=False) # These lines have been changed to make it work with wipy micropython I2C module
|
||||
#self.write(out_buffer, start=out_start, end=out_end, stop=False)
|
||||
#self.readinto(in_buffer, start=in_start, end=in_end)
|
||||
self.i2c.readfrom_into(self.device_address, in_buffer) # These lines have been changed to make it work with wipy micropython I2C module
|
||||
|
||||
|
||||
# pylint: enable-msg=too-many-arguments
|
||||
|
||||
def __enter__(self):
|
||||
while not self.i2c.try_lock():
|
||||
pass
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.i2c.unlock()
|
||||
return False
|
||||
|
||||
def __probe_for_device(self):
|
||||
"""
|
||||
Try to read a byte from an address,
|
||||
if you get an OSError it means the device is not there
|
||||
or that the device does not support these means of probing
|
||||
"""
|
||||
while not self.i2c.try_lock():
|
||||
pass
|
||||
try:
|
||||
self.i2c.writeto(self.device_address, b"")
|
||||
except OSError:
|
||||
# some OS's dont like writing an empty bytesting...
|
||||
# Retry by reading a byte
|
||||
try:
|
||||
result = bytearray(1)
|
||||
self.i2c.readfrom_into(self.device_address, result)
|
||||
except OSError:
|
||||
raise ValueError("No I2C device at address: %x" % self.device_address)
|
||||
finally:
|
||||
self.i2c.unlock()
|
||||
|
||||
eeData = [0] * const(832)
|
||||
I2C_READ_LEN = const(2048)
|
||||
SCALEALPHA = const(0.000001)
|
||||
MLX90640_DEVICEID1 = const(0x2407)
|
||||
OPENAIR_TA_SHIFT = const(8)
|
||||
|
||||
class MLX90640: # pylint: disable=too-many-instance-attributes
|
||||
"""Interface to the MLX90640 temperature sensor."""
|
||||
|
||||
kVdd = 0
|
||||
vdd25 = 0
|
||||
KvPTAT = 0
|
||||
KtPTAT = 0
|
||||
vPTAT25 = 0
|
||||
alphaPTAT = 0
|
||||
gainEE = 0
|
||||
tgc = 0
|
||||
KsTa = 0
|
||||
resolutionEE = 0
|
||||
calibrationModeEE = 0
|
||||
ksTo = [0] * 5
|
||||
ct = [0] * 5
|
||||
alpha = [0] * 768
|
||||
alphaScale = 0
|
||||
offset = [0] * 768
|
||||
kta = [0] * 768
|
||||
ktaScale = 0
|
||||
kv = [0] * 768
|
||||
kvScale = 0
|
||||
cpAlpha = [0] * 2
|
||||
cpOffset = [0] * 2
|
||||
ilChessC = [0] * 3
|
||||
brokenPixels = [0xFFFF] * 5
|
||||
outlierPixels = [0xFFFF] * 5
|
||||
cpKta = 0
|
||||
cpKv = 0
|
||||
|
||||
def __init__(self, i2c_bus, address=0x33):
|
||||
self.i2c_device = I2CDevice(i2c_bus, address)
|
||||
self._I2CReadWords(0x2400, eeData)
|
||||
# print(eeData)
|
||||
self._ExtractParameters()
|
||||
|
||||
@property
|
||||
def serial_number(self):
|
||||
""" 3-item tuple of hex values that are unique to each MLX90640 """
|
||||
serialWords = [0, 0, 0]
|
||||
self._I2CReadWords(MLX90640_DEVICEID1, serialWords)
|
||||
return serialWords
|
||||
|
||||
@property
|
||||
def refresh_rate(self):
|
||||
""" How fast the MLX90640 will spit out data. Start at lowest speed in
|
||||
RefreshRate and then slowly increase I2C clock rate and rate until you
|
||||
max out. The sensor does not like it if the I2C host cannot 'keep up'!"""
|
||||
controlRegister = [0]
|
||||
self._I2CReadWords(0x800D, controlRegister)
|
||||
return (controlRegister[0] >> 7) & 0x07
|
||||
|
||||
@refresh_rate.setter
|
||||
def refresh_rate(self, rate):
|
||||
controlRegister = [0]
|
||||
value = (rate & 0x7) << 7
|
||||
self._I2CReadWords(0x800D, controlRegister)
|
||||
value |= controlRegister[0] & 0xFC7F
|
||||
self._I2CWriteWord(0x800D, value)
|
||||
|
||||
def getFrame(self, framebuf):
|
||||
""" Request both 'halves' of a frame from the sensor, merge them
|
||||
and calculate the temperature in C for each of 32x24 pixels. Placed
|
||||
into the 768-element array passed in! """
|
||||
emissivity = 0.95
|
||||
tr = 23.15
|
||||
mlx90640Frame = [0] * 834
|
||||
|
||||
for _ in range(2):
|
||||
status = self._GetFrameData(mlx90640Frame)
|
||||
if status < 0:
|
||||
raise RuntimeError("Frame data error")
|
||||
# For a MLX90640 in the open air the shift is -8 degC.
|
||||
tr = self._GetTa(mlx90640Frame) - OPENAIR_TA_SHIFT
|
||||
self._CalculateTo(mlx90640Frame, emissivity, tr, framebuf)
|
||||
|
||||
def _GetFrameData(self, frameData):
|
||||
dataReady = 0
|
||||
cnt = 0
|
||||
statusRegister = [0]
|
||||
controlRegister = [0]
|
||||
|
||||
while dataReady == 0:
|
||||
self._I2CReadWords(0x8000, statusRegister)
|
||||
dataReady = statusRegister[0] & 0x0008
|
||||
# print("ready status: 0x%x" % dataReady)
|
||||
|
||||
while (dataReady != 0) and (cnt < 5):
|
||||
self._I2CWriteWord(0x8000, 0x0030)
|
||||
# print("Read frame", cnt)
|
||||
self._I2CReadWords(0x0400, frameData, end=832)
|
||||
|
||||
self._I2CReadWords(0x8000, statusRegister)
|
||||
dataReady = statusRegister[0] & 0x0008
|
||||
# print("frame ready: 0x%x" % dataReady)
|
||||
cnt += 1
|
||||
|
||||
if cnt > 4:
|
||||
raise RuntimeError("Too many retries")
|
||||
|
||||
self._I2CReadWords(0x800D, controlRegister)
|
||||
frameData[832] = controlRegister[0]
|
||||
frameData[833] = statusRegister[0] & 0x0001
|
||||
return frameData[833]
|
||||
|
||||
def _GetTa(self, frameData):
|
||||
vdd = self._GetVdd(frameData)
|
||||
|
||||
ptat = frameData[800]
|
||||
if ptat > 32767:
|
||||
ptat -= 65536
|
||||
|
||||
ptatArt = frameData[768]
|
||||
if ptatArt > 32767:
|
||||
ptatArt -= 65536
|
||||
ptatArt = (ptat / (ptat * self.alphaPTAT + ptatArt)) * math.pow(2, 18)
|
||||
|
||||
ta = ptatArt / (1 + self.KvPTAT * (vdd - 3.3)) - self.vPTAT25
|
||||
ta = ta / self.KtPTAT + 25
|
||||
return ta
|
||||
|
||||
def _GetVdd(self, frameData):
|
||||
vdd = frameData[810]
|
||||
if vdd > 32767:
|
||||
vdd -= 65536
|
||||
|
||||
resolutionRAM = (frameData[832] & 0x0C00) >> 10
|
||||
resolutionCorrection = math.pow(2, self.resolutionEE) / math.pow(
|
||||
2, resolutionRAM
|
||||
)
|
||||
vdd = (resolutionCorrection * vdd - self.vdd25) / self.kVdd + 3.3
|
||||
|
||||
return vdd
|
||||
|
||||
def _CalculateTo(self, frameData, emissivity, tr, result):
|
||||
# pylint: disable=too-many-locals, too-many-branches, too-many-statements
|
||||
subPage = frameData[833]
|
||||
alphaCorrR = [0] * 4
|
||||
irDataCP = [0, 0]
|
||||
|
||||
vdd = self._GetVdd(frameData)
|
||||
ta = self._GetTa(frameData)
|
||||
|
||||
ta4 = ta + 273.15
|
||||
ta4 = ta4 * ta4
|
||||
ta4 = ta4 * ta4
|
||||
tr4 = tr + 273.15
|
||||
tr4 = tr4 * tr4
|
||||
tr4 = tr4 * tr4
|
||||
taTr = tr4 - (tr4 - ta4) / emissivity
|
||||
|
||||
ktaScale = math.pow(2, self.ktaScale)
|
||||
kvScale = math.pow(2, self.kvScale)
|
||||
alphaScale = math.pow(2, self.alphaScale)
|
||||
|
||||
alphaCorrR[0] = 1 / (1 + self.ksTo[0] * 40)
|
||||
alphaCorrR[1] = 1
|
||||
alphaCorrR[2] = 1 + self.ksTo[1] * self.ct[2]
|
||||
alphaCorrR[3] = alphaCorrR[2] * (1 + self.ksTo[2] * (self.ct[3] - self.ct[2]))
|
||||
|
||||
# --------- Gain calculation -----------------------------------
|
||||
gain = frameData[778]
|
||||
if gain > 32767:
|
||||
gain -= 65536
|
||||
gain = self.gainEE / gain
|
||||
|
||||
# --------- To calculation -------------------------------------
|
||||
mode = (frameData[832] & 0x1000) >> 5
|
||||
|
||||
irDataCP[0] = frameData[776]
|
||||
irDataCP[1] = frameData[808]
|
||||
for i in range(2):
|
||||
if irDataCP[i] > 32767:
|
||||
irDataCP[i] -= 65536
|
||||
irDataCP[i] *= gain
|
||||
|
||||
irDataCP[0] -= (
|
||||
self.cpOffset[0]
|
||||
* (1 + self.cpKta * (ta - 25))
|
||||
* (1 + self.cpKv * (vdd - 3.3))
|
||||
)
|
||||
if mode == self.calibrationModeEE:
|
||||
irDataCP[1] -= (
|
||||
self.cpOffset[1]
|
||||
* (1 + self.cpKta * (ta - 25))
|
||||
* (1 + self.cpKv * (vdd - 3.3))
|
||||
)
|
||||
else:
|
||||
irDataCP[1] -= (
|
||||
(self.cpOffset[1] + self.ilChessC[0])
|
||||
* (1 + self.cpKta * (ta - 25))
|
||||
* (1 + self.cpKv * (vdd - 3.3))
|
||||
)
|
||||
|
||||
for pixelNumber in range(768):
|
||||
ilPattern = pixelNumber // 32 - (pixelNumber // 64) * 2
|
||||
chessPattern = ilPattern ^ (pixelNumber - (pixelNumber // 2) * 2)
|
||||
conversionPattern = (
|
||||
(pixelNumber + 2) // 4
|
||||
- (pixelNumber + 3) // 4
|
||||
+ (pixelNumber + 1) // 4
|
||||
- pixelNumber // 4
|
||||
) * (1 - 2 * ilPattern)
|
||||
|
||||
if mode == 0:
|
||||
pattern = ilPattern
|
||||
else:
|
||||
pattern = chessPattern
|
||||
|
||||
if pattern == frameData[833]:
|
||||
irData = frameData[pixelNumber]
|
||||
if irData > 32767:
|
||||
irData -= 65536
|
||||
irData *= gain
|
||||
|
||||
kta = self.kta[pixelNumber] / ktaScale
|
||||
kv = self.kv[pixelNumber] / kvScale
|
||||
irData -= (
|
||||
self.offset[pixelNumber]
|
||||
* (1 + kta * (ta - 25))
|
||||
* (1 + kv * (vdd - 3.3))
|
||||
)
|
||||
|
||||
if mode != self.calibrationModeEE:
|
||||
irData += (
|
||||
self.ilChessC[2] * (2 * ilPattern - 1)
|
||||
- self.ilChessC[1] * conversionPattern
|
||||
)
|
||||
|
||||
irData = irData - self.tgc * irDataCP[subPage]
|
||||
irData /= emissivity
|
||||
|
||||
alphaCompensated = SCALEALPHA * alphaScale / self.alpha[pixelNumber]
|
||||
alphaCompensated *= 1 + self.KsTa * (ta - 25)
|
||||
|
||||
Sx = (
|
||||
alphaCompensated
|
||||
* alphaCompensated
|
||||
* alphaCompensated
|
||||
* (irData + alphaCompensated * taTr)
|
||||
)
|
||||
Sx = math.sqrt(math.sqrt(Sx)) * self.ksTo[1]
|
||||
|
||||
To = (
|
||||
math.sqrt(
|
||||
math.sqrt(
|
||||
irData
|
||||
/ (alphaCompensated * (1 - self.ksTo[1] * 273.15) + Sx)
|
||||
+ taTr
|
||||
)
|
||||
)
|
||||
- 273.15
|
||||
)
|
||||
|
||||
if To < self.ct[1]:
|
||||
torange = 0
|
||||
elif To < self.ct[2]:
|
||||
torange = 1
|
||||
elif To < self.ct[3]:
|
||||
torange = 2
|
||||
else:
|
||||
torange = 3
|
||||
|
||||
To = (
|
||||
math.sqrt(
|
||||
math.sqrt(
|
||||
irData
|
||||
/ (
|
||||
alphaCompensated
|
||||
* alphaCorrR[torange]
|
||||
* (1 + self.ksTo[torange] * (To - self.ct[torange]))
|
||||
)
|
||||
+ taTr
|
||||
)
|
||||
)
|
||||
- 273.15
|
||||
)
|
||||
|
||||
result[pixelNumber] = To
|
||||
|
||||
# pylint: enable=too-many-locals, too-many-branches, too-many-statements
|
||||
|
||||
def _ExtractParameters(self):
|
||||
self._ExtractVDDParameters()
|
||||
self._ExtractPTATParameters()
|
||||
self._ExtractGainParameters()
|
||||
self._ExtractTgcParameters()
|
||||
self._ExtractResolutionParameters()
|
||||
self._ExtractKsTaParameters()
|
||||
self._ExtractKsToParameters()
|
||||
self._ExtractCPParameters()
|
||||
self._ExtractAlphaParameters()
|
||||
self._ExtractOffsetParameters()
|
||||
self._ExtractKtaPixelParameters()
|
||||
self._ExtractKvPixelParameters()
|
||||
self._ExtractCILCParameters()
|
||||
self._ExtractDeviatingPixels()
|
||||
|
||||
def _ExtractVDDParameters(self):
|
||||
# extract VDD
|
||||
self.kVdd = (eeData[51] & 0xFF00) >> 8
|
||||
if self.kVdd > 127:
|
||||
self.kVdd -= 256 # convert to signed
|
||||
self.kVdd *= 32
|
||||
self.vdd25 = eeData[51] & 0x00FF
|
||||
self.vdd25 = ((self.vdd25 - 256) << 5) - 8192
|
||||
|
||||
def _ExtractPTATParameters(self):
|
||||
# extract PTAT
|
||||
self.KvPTAT = (eeData[50] & 0xFC00) >> 10
|
||||
if self.KvPTAT > 31:
|
||||
self.KvPTAT -= 64
|
||||
self.KvPTAT /= 4096
|
||||
self.KtPTAT = eeData[50] & 0x03FF
|
||||
if self.KtPTAT > 511:
|
||||
self.KtPTAT -= 1024
|
||||
self.KtPTAT /= 8
|
||||
self.vPTAT25 = eeData[49]
|
||||
self.alphaPTAT = (eeData[16] & 0xF000) / math.pow(2, 14) + 8
|
||||
|
||||
def _ExtractGainParameters(self):
|
||||
# extract Gain
|
||||
self.gainEE = eeData[48]
|
||||
if self.gainEE > 32767:
|
||||
self.gainEE -= 65536
|
||||
|
||||
def _ExtractTgcParameters(self):
|
||||
# extract Tgc
|
||||
#print(eeData[60])
|
||||
self.tgc = eeData[60] & 0x00FF
|
||||
#print(self.tgc)
|
||||
if self.tgc > 127:
|
||||
self.tgc -= 256
|
||||
self.tgc /= 32
|
||||
#print(self.tgc)
|
||||
|
||||
def _ExtractResolutionParameters(self):
|
||||
# extract resolution
|
||||
self.resolutionEE = (eeData[56] & 0x3000) >> 12
|
||||
|
||||
def _ExtractKsTaParameters(self):
|
||||
# extract KsTa
|
||||
self.KsTa = (eeData[60] & 0xFF00) >> 8
|
||||
if self.KsTa > 127:
|
||||
self.KsTa -= 256
|
||||
self.KsTa /= 8192
|
||||
|
||||
def _ExtractKsToParameters(self):
|
||||
# extract ksTo
|
||||
step = ((eeData[63] & 0x3000) >> 12) * 10
|
||||
self.ct[0] = -40
|
||||
self.ct[1] = 0
|
||||
self.ct[2] = (eeData[63] & 0x00F0) >> 4
|
||||
self.ct[3] = (eeData[63] & 0x0F00) >> 8
|
||||
self.ct[2] *= step
|
||||
self.ct[3] = self.ct[2] + self.ct[3] * step
|
||||
|
||||
KsToScale = (eeData[63] & 0x000F) + 8
|
||||
KsToScale = 1 << KsToScale
|
||||
|
||||
self.ksTo[0] = eeData[61] & 0x00FF
|
||||
self.ksTo[1] = (eeData[61] & 0xFF00) >> 8
|
||||
self.ksTo[2] = eeData[62] & 0x00FF
|
||||
self.ksTo[3] = (eeData[62] & 0xFF00) >> 8
|
||||
|
||||
for i in range(4):
|
||||
if self.ksTo[i] > 127:
|
||||
self.ksTo[i] -= 256
|
||||
self.ksTo[i] /= KsToScale
|
||||
self.ksTo[4] = -0.0002
|
||||
|
||||
def _ExtractCPParameters(self):
|
||||
# extract CP
|
||||
offsetSP = [0] * 2
|
||||
alphaSP = [0] * 2
|
||||
|
||||
alphaScale = ((eeData[32] & 0xF000) >> 12) + 27
|
||||
|
||||
offsetSP[0] = eeData[58] & 0x03FF
|
||||
if offsetSP[0] > 511:
|
||||
offsetSP[0] -= 1024
|
||||
|
||||
offsetSP[1] = (eeData[58] & 0xFC00) >> 10
|
||||
if offsetSP[1] > 31:
|
||||
offsetSP[1] -= 64
|
||||
offsetSP[1] += offsetSP[0]
|
||||
|
||||
alphaSP[0] = eeData[57] & 0x03FF
|
||||
if alphaSP[0] > 511:
|
||||
alphaSP[0] -= 1024
|
||||
alphaSP[0] /= math.pow(2, alphaScale)
|
||||
|
||||
alphaSP[1] = (eeData[57] & 0xFC00) >> 10
|
||||
if alphaSP[1] > 31:
|
||||
alphaSP[1] -= 64
|
||||
alphaSP[1] = (1 + alphaSP[1] / 128) * alphaSP[0]
|
||||
|
||||
cpKta = eeData[59] & 0x00FF
|
||||
if cpKta > 127:
|
||||
cpKta -= 256
|
||||
ktaScale1 = ((eeData[56] & 0x00F0) >> 4) + 8
|
||||
self.cpKta = cpKta / math.pow(2, ktaScale1)
|
||||
|
||||
cpKv = (eeData[59] & 0xFF00) >> 8
|
||||
if cpKv > 127:
|
||||
cpKv -= 256
|
||||
kvScale = (eeData[56] & 0x0F00) >> 8
|
||||
self.cpKv = cpKv / math.pow(2, kvScale)
|
||||
|
||||
self.cpAlpha[0] = alphaSP[0]
|
||||
self.cpAlpha[1] = alphaSP[1]
|
||||
self.cpOffset[0] = offsetSP[0]
|
||||
self.cpOffset[1] = offsetSP[1]
|
||||
#print(self.cpAlpha[0])
|
||||
#print(self.cpAlpha[1])
|
||||
|
||||
def _ExtractAlphaParameters(self):
|
||||
# extract alpha
|
||||
accRemScale = eeData[32] & 0x000F
|
||||
accColumnScale = (eeData[32] & 0x00F0) >> 4
|
||||
accRowScale = (eeData[32] & 0x0F00) >> 8
|
||||
alphaScale = ((eeData[32] & 0xF000) >> 12) + 30
|
||||
alphaRef = eeData[33]
|
||||
accRow = [0] * 24
|
||||
accColumn = [0] * 32
|
||||
alphaTemp = [0] * 768
|
||||
|
||||
for i in range(6):
|
||||
p = i * 4
|
||||
accRow[p + 0] = eeData[34 + i] & 0x000F
|
||||
accRow[p + 1] = (eeData[34 + i] & 0x00F0) >> 4
|
||||
accRow[p + 2] = (eeData[34 + i] & 0x0F00) >> 8
|
||||
accRow[p + 3] = (eeData[34 + i] & 0xF000) >> 12
|
||||
|
||||
for i in range(24):
|
||||
if accRow[i] > 7:
|
||||
accRow[i] -= 16
|
||||
|
||||
for i in range(8):
|
||||
p = i * 4
|
||||
accColumn[p + 0] = eeData[40 + i] & 0x000F
|
||||
accColumn[p + 1] = (eeData[40 + i] & 0x00F0) >> 4
|
||||
accColumn[p + 2] = (eeData[40 + i] & 0x0F00) >> 8
|
||||
accColumn[p + 3] = (eeData[40 + i] & 0xF000) >> 12
|
||||
|
||||
for i in range(32):
|
||||
if accColumn[i] > 7:
|
||||
accColumn[i] -= 16
|
||||
for i in range(24):
|
||||
for j in range(32):
|
||||
p = 32 * i + j
|
||||
alphaTemp[p] = (eeData[64 + p] & 0x03F0) >> 4
|
||||
if alphaTemp[p] > 31:
|
||||
alphaTemp[p] -= 64
|
||||
alphaTemp[p] *= 1 << accRemScale
|
||||
alphaTemp[p] += (
|
||||
alphaRef
|
||||
+ (accRow[i] << accRowScale)
|
||||
+ (accColumn[j] << accColumnScale)
|
||||
)
|
||||
alphaTemp[p] /= math.pow(2, alphaScale)
|
||||
alphaTemp[p] -= self.tgc * (self.cpAlpha[0] + self.cpAlpha[1]) / 2
|
||||
alphaTemp[p] = SCALEALPHA / alphaTemp[p]
|
||||
# print("alphaTemp: ", alphaTemp)
|
||||
|
||||
temp = max(alphaTemp)
|
||||
#print("temp", temp)
|
||||
|
||||
alphaScale = 0
|
||||
while temp < 32768:
|
||||
temp *= 2
|
||||
alphaScale += 1
|
||||
|
||||
for i in range(768):
|
||||
temp = alphaTemp[i] * math.pow(2, alphaScale)
|
||||
self.alpha[i] = int(temp + 0.5)
|
||||
|
||||
self.alphaScale = alphaScale
|
||||
|
||||
def _ExtractOffsetParameters(self):
|
||||
# extract offset
|
||||
occRow = [0] * 24
|
||||
occColumn = [0] * 32
|
||||
|
||||
occRemScale = eeData[16] & 0x000F
|
||||
occColumnScale = (eeData[16] & 0x00F0) >> 4
|
||||
occRowScale = (eeData[16] & 0x0F00) >> 8
|
||||
offsetRef = eeData[17]
|
||||
if offsetRef > 32767:
|
||||
offsetRef -= 65536
|
||||
|
||||
for i in range(6):
|
||||
p = i * 4
|
||||
occRow[p + 0] = eeData[18 + i] & 0x000F
|
||||
occRow[p + 1] = (eeData[18 + i] & 0x00F0) >> 4
|
||||
occRow[p + 2] = (eeData[18 + i] & 0x0F00) >> 8
|
||||
occRow[p + 3] = (eeData[18 + i] & 0xF000) >> 12
|
||||
|
||||
for i in range(24):
|
||||
if occRow[i] > 7:
|
||||
occRow[i] -= 16
|
||||
|
||||
for i in range(8):
|
||||
p = i * 4
|
||||
occColumn[p + 0] = eeData[24 + i] & 0x000F
|
||||
occColumn[p + 1] = (eeData[24 + i] & 0x00F0) >> 4
|
||||
occColumn[p + 2] = (eeData[24 + i] & 0x0F00) >> 8
|
||||
occColumn[p + 3] = (eeData[24 + i] & 0xF000) >> 12
|
||||
|
||||
for i in range(32):
|
||||
if occColumn[i] > 7:
|
||||
occColumn[i] -= 16
|
||||
|
||||
for i in range(24):
|
||||
for j in range(32):
|
||||
p = 32 * i + j
|
||||
self.offset[p] = (eeData[64 + p] & 0xFC00) >> 10
|
||||
if self.offset[p] > 31:
|
||||
self.offset[p] -= 64
|
||||
self.offset[p] *= 1 << occRemScale
|
||||
self.offset[p] += (
|
||||
offsetRef
|
||||
+ (occRow[i] << occRowScale)
|
||||
+ (occColumn[j] << occColumnScale)
|
||||
)
|
||||
|
||||
def _ExtractKtaPixelParameters(self): # pylint: disable=too-many-locals
|
||||
# extract KtaPixel
|
||||
KtaRC = [0] * 4
|
||||
ktaTemp = [0] * 768
|
||||
|
||||
KtaRoCo = (eeData[54] & 0xFF00) >> 8
|
||||
if KtaRoCo > 127:
|
||||
KtaRoCo -= 256
|
||||
KtaRC[0] = KtaRoCo
|
||||
|
||||
KtaReCo = eeData[54] & 0x00FF
|
||||
if KtaReCo > 127:
|
||||
KtaReCo -= 256
|
||||
KtaRC[2] = KtaReCo
|
||||
|
||||
KtaRoCe = (eeData[55] & 0xFF00) >> 8
|
||||
if KtaRoCe > 127:
|
||||
KtaRoCe -= 256
|
||||
KtaRC[1] = KtaRoCe
|
||||
|
||||
KtaReCe = eeData[55] & 0x00FF
|
||||
if KtaReCe > 127:
|
||||
KtaReCe -= 256
|
||||
KtaRC[3] = KtaReCe
|
||||
|
||||
ktaScale1 = ((eeData[56] & 0x00F0) >> 4) + 8
|
||||
ktaScale2 = eeData[56] & 0x000F
|
||||
|
||||
for i in range(24):
|
||||
for j in range(32):
|
||||
p = 32 * i + j
|
||||
split = 2 * (p // 32 - (p // 64) * 2) + p % 2
|
||||
ktaTemp[p] = (eeData[64 + p] & 0x000E) >> 1
|
||||
if ktaTemp[p] > 3:
|
||||
ktaTemp[p] -= 8
|
||||
ktaTemp[p] *= 1 << ktaScale2
|
||||
ktaTemp[p] += KtaRC[split]
|
||||
ktaTemp[p] /= math.pow(2, ktaScale1)
|
||||
# ktaTemp[p] = ktaTemp[p] * mlx90640->offset[p];
|
||||
|
||||
temp = abs(ktaTemp[0])
|
||||
for kta in ktaTemp:
|
||||
temp = max(temp, abs(kta))
|
||||
|
||||
ktaScale1 = 0
|
||||
while temp < 64:
|
||||
temp *= 2
|
||||
ktaScale1 += 1
|
||||
|
||||
for i in range(768):
|
||||
temp = ktaTemp[i] * math.pow(2, ktaScale1)
|
||||
if temp < 0:
|
||||
self.kta[i] = int(temp - 0.5)
|
||||
else:
|
||||
self.kta[i] = int(temp + 0.5)
|
||||
self.ktaScale = ktaScale1
|
||||
|
||||
def _ExtractKvPixelParameters(self):
|
||||
KvT = [0] * 4
|
||||
kvTemp = [0] * 768
|
||||
|
||||
KvRoCo = (eeData[52] & 0xF000) >> 12
|
||||
if KvRoCo > 7:
|
||||
KvRoCo -= 16
|
||||
KvT[0] = KvRoCo
|
||||
|
||||
KvReCo = (eeData[52] & 0x0F00) >> 8
|
||||
if KvReCo > 7:
|
||||
KvReCo -= 16
|
||||
KvT[2] = KvReCo
|
||||
|
||||
KvRoCe = (eeData[52] & 0x00F0) >> 4
|
||||
if KvRoCe > 7:
|
||||
KvRoCe -= 16
|
||||
KvT[1] = KvRoCe
|
||||
|
||||
KvReCe = eeData[52] & 0x000F
|
||||
if KvReCe > 7:
|
||||
KvReCe -= 16
|
||||
KvT[3] = KvReCe
|
||||
|
||||
kvScale = (eeData[56] & 0x0F00) >> 8
|
||||
|
||||
for i in range(24):
|
||||
for j in range(32):
|
||||
p = 32 * i + j
|
||||
split = 2 * (p // 32 - (p // 64) * 2) + p % 2
|
||||
kvTemp[p] = KvT[split]
|
||||
kvTemp[p] /= math.pow(2, kvScale)
|
||||
# kvTemp[p] = kvTemp[p] * mlx90640->offset[p];
|
||||
|
||||
temp = abs(kvTemp[0])
|
||||
for kv in kvTemp:
|
||||
temp = max(temp, abs(kv))
|
||||
|
||||
kvScale = 0
|
||||
while temp < 64:
|
||||
temp *= 2
|
||||
kvScale += 1
|
||||
|
||||
for i in range(768):
|
||||
temp = kvTemp[i] * math.pow(2, kvScale)
|
||||
if temp < 0:
|
||||
self.kv[i] = int(temp - 0.5)
|
||||
else:
|
||||
self.kv[i] = int(temp + 0.5)
|
||||
self.kvScale = kvScale
|
||||
|
||||
def _ExtractCILCParameters(self):
|
||||
ilChessC = [0] * 3
|
||||
|
||||
self.calibrationModeEE = (eeData[10] & 0x0800) >> 4
|
||||
self.calibrationModeEE = self.calibrationModeEE ^ 0x80
|
||||
|
||||
ilChessC[0] = eeData[53] & 0x003F
|
||||
if ilChessC[0] > 31:
|
||||
ilChessC[0] -= 64
|
||||
ilChessC[0] /= 16.0
|
||||
|
||||
ilChessC[1] = (eeData[53] & 0x07C0) >> 6
|
||||
if ilChessC[1] > 15:
|
||||
ilChessC[1] -= 32
|
||||
ilChessC[1] /= 2.0
|
||||
|
||||
ilChessC[2] = (eeData[53] & 0xF800) >> 11
|
||||
if ilChessC[2] > 15:
|
||||
ilChessC[2] -= 32
|
||||
ilChessC[2] /= 8.0
|
||||
|
||||
self.ilChessC = ilChessC
|
||||
|
||||
def _ExtractDeviatingPixels(self):
|
||||
self.brokenPixels = [0xFFFF] * 5
|
||||
self.outlierPixels = [0xFFFF] * 5
|
||||
|
||||
pixCnt = 0
|
||||
brokenPixCnt = 0
|
||||
outlierPixCnt = 0
|
||||
|
||||
while (pixCnt < 768) and (brokenPixCnt < 5) and (outlierPixCnt < 5):
|
||||
if eeData[pixCnt + 64] == 0:
|
||||
self.brokenPixels[brokenPixCnt] = pixCnt
|
||||
brokenPixCnt += 1
|
||||
elif (eeData[pixCnt + 64] & 0x0001) != 0:
|
||||
self.outlierPixels[outlierPixCnt] = pixCnt
|
||||
outlierPixCnt += 1
|
||||
pixCnt += 1
|
||||
|
||||
if brokenPixCnt > 4:
|
||||
raise RuntimeError("More than 4 broken pixels")
|
||||
if outlierPixCnt > 4:
|
||||
raise RuntimeError("More than 4 outlier pixels")
|
||||
if (brokenPixCnt + outlierPixCnt) > 4:
|
||||
raise RuntimeError("More than 4 faulty pixels")
|
||||
# print("Found %d broken pixels, %d outliers" % (brokenPixCnt, outlierPixCnt))
|
||||
# TODO INCOMPLETE
|
||||
|
||||
def _I2CWriteWord(self, writeAddress, data):
|
||||
cmd = bytearray(4)
|
||||
cmd[0] = writeAddress >> 8
|
||||
cmd[1] = writeAddress & 0x00FF
|
||||
cmd[2] = data >> 8
|
||||
cmd[3] = data & 0x00FF
|
||||
dataCheck = [0]
|
||||
|
||||
with self.i2c_device as i2c:
|
||||
i2c.write(cmd)
|
||||
# print("Wrote:", [hex(i) for i in cmd])
|
||||
time.sleep(0.001)
|
||||
self._I2CReadWords(writeAddress, dataCheck)
|
||||
# print("dataCheck: 0x%x" % dataCheck[0])
|
||||
# if (dataCheck != data):
|
||||
# return -2
|
||||
|
||||
_inbuf = bytearray(2 * I2C_READ_LEN)
|
||||
|
||||
def _I2CReadWords(self, addr, buffer, *, end=None):
|
||||
# stamp = time.monotonic()
|
||||
if end is None:
|
||||
remainingWords = len(buffer)
|
||||
else:
|
||||
remainingWords = end
|
||||
offset = 0
|
||||
addrbuf = bytearray(2)
|
||||
# inbuf = bytearray(2 * I2C_READ_LEN)
|
||||
inbuf = self._inbuf
|
||||
|
||||
with self.i2c_device as i2c:
|
||||
while remainingWords:
|
||||
addrbuf[0] = addr >> 8 # MSB
|
||||
addrbuf[1] = addr & 0xFF # LSB
|
||||
read_words = min(remainingWords, I2C_READ_LEN)
|
||||
i2c.write_then_readinto(
|
||||
addrbuf, inbuf, in_end=read_words * 2
|
||||
) # in bytes
|
||||
# print("-> ", [hex(i) for i in addrbuf])
|
||||
|
||||
outwords = struct.unpack(
|
||||
">" + "H" * read_words, inbuf[0 : read_words * 2]
|
||||
)
|
||||
# print("<- (", read_words, ")", [hex(i) for i in outwords])
|
||||
for i, w in enumerate(outwords):
|
||||
buffer[offset + i] = w
|
||||
offset += read_words
|
||||
remainingWords -= read_words
|
||||
addr += read_words
|
||||
|
||||
ixc = None
|
||||
mlx = None
|
||||
frame = None
|
||||
|
||||
def init_camera(scl_pin=22, sda_pin=21, freq=100000):
|
||||
"""Explicitly initializes the I2C bus and camera after power is stable."""
|
||||
global ixc, mlx, frame
|
||||
|
||||
print(f"[Camera] Initializing I2C on SCL:{scl_pin}, SDA:{sda_pin} at {freq}Hz...")
|
||||
ixc = I2C(pins=(scl_pin, sda_pin), frequency=freq)
|
||||
|
||||
print("[Camera] Probing for MLX90640...")
|
||||
mlx = MLX90640(ixc)
|
||||
|
||||
# Bonus: Your wiki snapshot recommends 16Hz for smooth images!
|
||||
mlx.refresh_rate = RefreshRate.REFRESH_16_HZ
|
||||
|
||||
frame = [0] * 768
|
||||
print("[Camera] Setup successful!")
|
||||
|
||||
def read_temperature():
|
||||
if mlx is None:
|
||||
print("[Camera] Error: Camera not initialized. Call init_camera() first.")
|
||||
return None
|
||||
try:
|
||||
print("Querying camera...")
|
||||
mlx.getFrame(frame)
|
||||
return frame
|
||||
except Exception as e:
|
||||
print(f"[Camera] Read error: {e}")
|
||||
return None
|
||||
+99
-22
@@ -1,7 +1,12 @@
|
||||
import json
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
from shared import get_lora, get_mqtt_client, deviceTypes, config
|
||||
import traceback
|
||||
from orchestrateur.sensors import gps
|
||||
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads
|
||||
from shared.logging import log
|
||||
from sensors import ultrasonicRanger, temp_hum, button, camera
|
||||
|
||||
# --- Read Unique Device ID ---
|
||||
try:
|
||||
@@ -49,6 +54,7 @@ mqtt_client = get_mqtt_client(
|
||||
)
|
||||
mqtt_client.connect()
|
||||
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS)
|
||||
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
||||
print(f"Subscribed to topic: {config.MQTT_TOPIC_SENSOR}")
|
||||
|
||||
# --- THE CRUCIAL PAHO FIX ---
|
||||
@@ -66,49 +72,120 @@ def mqtt_listener():
|
||||
message = mqtt_client.get_message()
|
||||
|
||||
if message:
|
||||
# Try to parse the payload as a python dictionary, but if it fails, just print the raw payload
|
||||
try:
|
||||
payload = json.loads(message['payload'])
|
||||
except Exception as e:
|
||||
print(f"Error parsing MQTT payload: {e}")
|
||||
payload = message['payload'] # Fallback to raw payload if parsing fails
|
||||
|
||||
print(f"\n[Thread MQTT] Message reçu : {message}")
|
||||
data_queue.put({"source": "MQTT", "data": message})
|
||||
data_queue.put({"source": "MQTT", "topic": message['topic'] ,"data": payload})
|
||||
|
||||
# --- THE CPU FIX ---
|
||||
# Sleep for 100ms. Prevents the thread from turning into an infinite 100% CPU hog.
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.2)
|
||||
|
||||
# Button
|
||||
button_state = False
|
||||
def button_callback():
|
||||
global button_state
|
||||
button_state = not button_state
|
||||
print(f"\n[Thread Button] Button state changed to: {button_state}")
|
||||
|
||||
button.set_callback(button_callback)
|
||||
|
||||
# Launch background monitoring workers
|
||||
threading.Thread(target=lora_listener, daemon=True).start()
|
||||
threading.Thread(target=mqtt_listener, daemon=True).start()
|
||||
# threading.Thread(target=lora_listener, daemon=True).start()
|
||||
# threading.Thread(target=mqtt_listener, daemon=True).start()
|
||||
# Launch button monitoring thread
|
||||
button.start_button_monitoring_thread()
|
||||
|
||||
print("Orchestrateur prêt. Le main loop est libre.")
|
||||
|
||||
# Sensor reading
|
||||
def read_sensors():
|
||||
"""Read all sensors and return a dictionary of their values."""
|
||||
log("\nLecture des capteurs...")
|
||||
sensor_data = {}
|
||||
|
||||
# Read Ultrasonic Ranger
|
||||
distance = ultrasonicRanger.get_dish_height()
|
||||
if distance is not None:
|
||||
log(f"\nLecture du capteur Ultrason : {distance}")
|
||||
sensor_data["ultrasonic_distance"] = distance
|
||||
|
||||
# Read Temperature and Humidity
|
||||
temperature, humidity = temp_hum.get_temperature_and_humidity()
|
||||
if temperature is not None and humidity is not None:
|
||||
log(f"\nLecture du capteur Temp/Hum : {temperature}, {humidity}")
|
||||
sensor_data["temperature"] = temperature
|
||||
sensor_data["humidity"] = humidity
|
||||
|
||||
|
||||
# Read GPS Data
|
||||
gps_data = gps.get_gps_data()
|
||||
if gps_data:
|
||||
log(f"\nLecture du capteur GPS : {gps_data}")
|
||||
sensor_data["gps"] = gps_data
|
||||
|
||||
# Camera
|
||||
picture_bytes = None
|
||||
try:
|
||||
picture_bytes = camera.get_picture()
|
||||
log(f"\nLecture du capteur Caméra : {len(picture_bytes)} bytes")
|
||||
sensor_data["camera_image"] = picture_bytes
|
||||
except Exception as e:
|
||||
log(f"Error reading camera data: {e}")
|
||||
|
||||
# Read Button State (last because he can still change state while reading other sensors)
|
||||
sensor_data["button_state"] = button_state
|
||||
|
||||
return sensor_data
|
||||
|
||||
# --- MAIN EXECUTION LOOP ---
|
||||
while True:
|
||||
try:
|
||||
# Check for non-heartbeat data safely
|
||||
# Check for non-heartbeat data
|
||||
try:
|
||||
msg = data_queue.get(block=False)
|
||||
print(f"\n[Main Loop] Données traitées : {msg['data']}")
|
||||
|
||||
# print(msg)
|
||||
|
||||
if msg["source"] == "LoRa":
|
||||
print(f"\n[Main Loop] LoRa : Données traitées : {msg['data']}")
|
||||
elif msg["source"] == "MQTT":
|
||||
if (msg["topic"] == config.MQTT_TOPIC_HELLO.decode('utf-8')):
|
||||
if ("id_orchestrator" in msg["data"] and msg["data"]["id_orchestrator"] == DEVICE_ID):
|
||||
# Do not answer to messages coming from me
|
||||
continue
|
||||
microwave_id = msg["data"]["id_microwave"]
|
||||
print(f"\n[Main Loop] MQTT : Hello reçu de {microwave_id}.")
|
||||
# Responds
|
||||
mqtt_client.publish(config.MQTT_TOPIC_HELLO, payloads.mqtt_hello_ack(DEVICE_ID, microwave_id), qos=config.MQTT_QOS)
|
||||
print(f"[Main Loop] MQTT : Réponse Hello envoyée à {microwave_id}.")
|
||||
# TODO : Save in database
|
||||
|
||||
|
||||
print(f"\n[Main Loop] MQTT : Données traitées : {msg['data']}")
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
time.sleep(1)
|
||||
# DEBUG : Read sensors
|
||||
sensor_values = read_sensors()
|
||||
if sensor_values:
|
||||
sensor_values_print = sensor_values.copy()
|
||||
if "camera_image" in sensor_values_print:
|
||||
sensor_values_print["camera_image"] = f"<{len(sensor_values_print['camera_image'])} bytes>"
|
||||
print(f"\nCapteurs Données lues : {sensor_values_print}")
|
||||
|
||||
# Publish debug telemetry message
|
||||
print("[Main Loop] Envoi d'un message de debug sur MQTT...")
|
||||
response = mqtt_client.publish(
|
||||
config.MQTT_TOPIC_COOKING,
|
||||
f"Orchestrateur actif, ID: {DEVICE_ID}",
|
||||
qos=config.MQTT_QOS
|
||||
)
|
||||
|
||||
# This will now unblock instantly because loop_start() handles the delivery confirmation!
|
||||
response.wait_for_publish()
|
||||
print("[Main Loop] Message de debug publié avec succès.")
|
||||
|
||||
time.sleep(9)
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
time.sleep(1) # Prevents rapid error logging in case of persistent issues
|
||||
|
||||
# Clean termination
|
||||
if hasattr(mqtt_client._client, "loop_stop"):
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
paho-mqtt>=1.6,<3
|
||||
pyserial>=3.5,<4
|
||||
# picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2
|
||||
# OpenCV
|
||||
# sudo apt install -y python3-opencv
|
||||
# sudo apt install -y opencv-data
|
||||
@@ -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.config as config
|
||||
import shared.payloads as payloads
|
||||
|
||||
def get_lora(*args, **kwargs):
|
||||
from .lora_device import get_lora_device
|
||||
|
||||
+5
-2
@@ -1,12 +1,15 @@
|
||||
|
||||
DEBUG=True
|
||||
|
||||
# LoRa
|
||||
HEARTBEAT_INTERVAL = 10
|
||||
HEARTBEAT_INTERVAL = 30
|
||||
|
||||
# MQTT
|
||||
MQTT_BROKER_HOST = "192.168.50.1"
|
||||
MQTT_TOPIC_HELLO = b"smartwave/hello"
|
||||
MQTT_TOPIC_SENSOR = b"smartwave/sensor"
|
||||
MQTT_TOPIC_COOKING = b"smartwave/cooking"
|
||||
MQTT_KEEPALIVE = 30
|
||||
USE_TLS = True
|
||||
MQTT_QOS = 1
|
||||
# Long because messages are stored into the broker and will be sent when the orchestrator is back online.
|
||||
MQTT_HELLO_INTERVAL = 30
|
||||
@@ -0,0 +1,6 @@
|
||||
from shared.config import DEBUG
|
||||
|
||||
def log(message):
|
||||
"""Log a message to the console if DEBUG is enabled."""
|
||||
if DEBUG:
|
||||
print(f"\n{message}")
|
||||
+37
-5
@@ -13,11 +13,11 @@ except ImportError:
|
||||
from umqtt.simple import MQTTClient as _MQTTClient
|
||||
BACKEND_NAME = "umqtt.simple"
|
||||
IS_MICROPYTHON = True
|
||||
except ImportError:
|
||||
try:
|
||||
from umqtt.robust import MQTTClient as _MQTTClient
|
||||
BACKEND_NAME = "umqtt.robust"
|
||||
IS_MICROPYTHON = True
|
||||
# except ImportError:
|
||||
# try:
|
||||
# from umqtt.robust import MQTTClient as _MQTTClient
|
||||
# BACKEND_NAME = "umqtt.robust"
|
||||
# IS_MICROPYTHON = True
|
||||
except ImportError as exc:
|
||||
raise ImportError("No MQTT client found. Expected paho.mqtt or umqtt.") from exc
|
||||
|
||||
@@ -180,6 +180,38 @@ class BrokerClient:
|
||||
|
||||
return client.subscribe(topic, qos=qos)
|
||||
|
||||
def unsubscribe(self, topic):
|
||||
client = self.open()
|
||||
if IS_MICROPYTHON:
|
||||
import struct
|
||||
# Ensure the topic is bytes for writing to the socket
|
||||
topic_bytes = topic if isinstance(topic, bytes) else topic.encode('utf-8')
|
||||
|
||||
# 1. Build the MQTT unsubscribe packet header
|
||||
pkt = bytearray(b"\xa2\0\0\0")
|
||||
client.pid += 1
|
||||
|
||||
# Packet length is: 2 bytes (PID) + 2 bytes (topic length indicator) + topic string length
|
||||
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic_bytes), client.pid)
|
||||
|
||||
# 2. Write the packet to the socket
|
||||
client.sock.write(pkt)
|
||||
client._send_str(topic_bytes)
|
||||
|
||||
# 3. Wait for the UNSUBACK confirmation frame (0xB0) from the broker
|
||||
while True:
|
||||
op = client.wait_msg()
|
||||
if op == 0xB0:
|
||||
resp = client.sock.read(3)
|
||||
assert resp[1] == pkt[2] and resp[2] == pkt[3]
|
||||
return client
|
||||
return client
|
||||
|
||||
if isinstance(topic, bytes):
|
||||
topic = topic.decode('utf-8')
|
||||
|
||||
return client.unsubscribe(topic)
|
||||
|
||||
def _on_micropython_message(self, topic, payload):
|
||||
self._store_message(topic, payload, None, False)
|
||||
|
||||
|
||||
@@ -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