Compare commits

..

9 Commits

Author SHA1 Message Date
Ninluc dc30f6ce6c Higher debug cooking params
Build, push image, and notify Watchtower / build-image (push) Successful in 57s
Build, push image, and notify Watchtower / notify (push) Successful in 11s
2026-08-07 16:51:08 +02:00
Ninluc 16e3325f8f Better estimated remaining time 2026-08-07 16:51:00 +02:00
Ninluc 988cb7958c Better cooking_state_temperature_provider 2026-08-07 16:50:14 +02:00
Ninluc 053964e56c UART commands for getting dish temperature 2026-08-07 16:14:43 +02:00
Ninluc ec9be8ac30 cooking state logs at interval 2026-08-07 16:13:47 +02:00
Ninluc f30b29be69 More Screen updates, icon and screens correction 2026-08-07 14:32:02 +02:00
Ninluc 46acbb88c2 Fix RGB lights 2026-08-06 19:42:34 +02:00
Ninluc 6ab2a2bea3 Fix Lora ? 2026-08-06 19:38:15 +02:00
Ninluc 35a330e7ae Microwave Screen + defrost mode 2026-08-06 19:27:35 +02:00
9 changed files with 328 additions and 77 deletions
+132
View File
@@ -0,0 +1,132 @@
import time
from shared.cookingState import CookingState, CookingStates
from shared import config
import framebuf
# Icon and bitmaps
image_weather_frost_bits = bytearray(b'\x01\x00\x13\x901\x18s\x9c\x09 \x05@S\x94\xfe\xfeS\x94\x05@\x09 s\x9c1\x18\x13\x90\x01\x00\x00\x00')
image_Alert_bits = bytearray(b'\xf7\x80\xe3\x80\xeb\x80\xc9\x80\xc9\x80\x80\x80\x88\x80\x00\x00')
image_CoolHiSmall_bits = bytearray(b'\x1f\xff\xc0?\xff\xe0\x7f\xff\xf0\xff\xdf\xf8\xffW\xf8\xfd\x8d\xf8\xf9\xdc\xf8\xfe\xdb\xf8\xf7Wx\xfb\x8e\xf8\xe0\x008\xfb\x8e\xf8\xf7Wx\xfe\xdb\xf8\xf9\xdc\xf8\xfd\x8d\xf8\xffW\xf8\xff\xdf\xf8\x7f\xff\xf0?\xff\xe0\x1f\xff\xc0')
image_CoolLoSmall_bits = bytearray(b'\x1f\xff\xc0 \x00 @\x00\x10\x80 \x08\x80\xa8\x08\x82r\x08\x86#\x08\x81$\x08\x88\xa8\x88\x84q\x08\x9f\xff\xc8\x84q\x08\x88\xa8\x88\x81$\x08\x86#\x08\x82r\x08\x80\xa8\x08\x80 \x08@\x00\x10 \x00 \x1f\xff\xc0')
image_music_sound_wave_bits = bytearray(b'\x00\x00\x00\x02\x00\x00\x02 \x00\x02 \x00\x02 \x00\x0a(\x00\x0a\xa8\x00*\xa8\x00\xaa\xaa\x80\x0a\xaa\x00\x0a\xa8\x00\x0a \x00\x02 \x00\x02 \x00\x02\x00\x00\x00\x00\x00')
image_weather_temperature_bits = bytearray(b'\x1c\x00\x22\x00+\x00*\x00+\x00*\x00+\x00*\x00*\x00I\x00\x9c\x80\xae\x80\xbe\x80\x9c\x80A\x00>\x00')
fb_image_weather_frost_bits = framebuf.FrameBuffer(image_weather_frost_bits, 15, 16, framebuf.MONO_HLSB)
fb_image_CoolHiSmall_bits = framebuf.FrameBuffer(image_CoolHiSmall_bits, 21, 21, framebuf.MONO_HLSB)
fb_image_weather_temperature_bits = framebuf.FrameBuffer(image_weather_temperature_bits, 9, 16, framebuf.MONO_HLSB)
fb_image_Alert_bits = framebuf.FrameBuffer(image_Alert_bits, 9, 8, framebuf.MONO_HLSB)
fb_image_music_sound_wave_bits = framebuf.FrameBuffer(image_music_sound_wave_bits, 17, 16, framebuf.MONO_HLSB)
fb_image_CoolLoSmall_bits = framebuf.FrameBuffer(image_CoolLoSmall_bits, 21, 21, framebuf.MONO_HLSB)
# Constants
CHARACTER_WIDTH = 6
CHARACTER_GAP = 2
CHARACTER_FULL_WIDTH = CHARACTER_WIDTH + CHARACTER_GAP
CHARACTER_HEIGHT = 7
class MicrowaveScreen:
# Used for animation
# lastUpdateTime = 0 # Not used right now
def __init__(self, display):
self.display = display
self.width = display.width
self.height = display.height
def h_centered_text(self, text, y, color=1):
"""Centers text horizontally
Args:
text (str): _description_
y (int): _description_
color (int, optional): _description_. Defaults to 1.
"""
text = str(text)
text_width = len(text) * CHARACTER_FULL_WIDTH
if config.DEBUG:
if text_width > self.width:
print(f"[MicrowaveScreen] Warning: Text '{text}' is too long to fit on the screen.")
x = (self.width - text_width) // 2
self.display.text(text, x, y, color)
def flick(self):
self.display.fill(0)
def show(self):
self.display.show()
def bootScreen(self):
self.display.text("SmartWave", 28, 28, 1)
self.show()
def _progressBar(self, progress, x, y, width=87, height=13, color=1):
# Box outline
self.display.rect(x, y, width, height, color)
# Fill the progress bar based on the progress value (0.0 to 1.0)
fill_width = int(progress * (width)) # Subtract 2
self.display.fill_rect(x, y, fill_width, height, color)
def update(self, cooking_state: CookingState | None, defrost_mode: bool = False):
self.flick()
# self.lastUpdateTime = time.time() # Not Used right now
if not cooking_state or cooking_state.state == CookingStates.IDLE: # Is not currently cooking
self.h_centered_text("Ready !", 28, 1)
else:
# Screen center
if cooking_state.state == CookingStates.STIRRING_REQUIRED:
self._message("Pls Stir", True)
elif cooking_state.state == CookingStates.ALERT:
self._message("Alert !", True)
elif cooking_state.state == CookingStates.DONE:
self._message("Done !", False)
else: # Progress bar
elapsed_time = cooking_state.get_elapsed_time()
estimated_progress = elapsed_time / (elapsed_time + cooking_state.estimated_remaining_time)
self._progressBar(estimated_progress, 21, 25)
# Estimated remaining time
if cooking_state.paused:
self.h_centered_text("Paused", 40, 1)
elif cooking_state.state != CookingStates.DONE:
remaining_time = int(cooking_state.estimated_remaining_time)
minutes = remaining_time // 60
seconds = remaining_time % 60
self.h_centered_text(f"{minutes:02}:{seconds:02}", 40, 1)
# Current dish temperature
if cooking_state.current_dish_temp is not None and cooking_state.current_dish_temp is not 0.0:
self.display.blit(fb_image_weather_temperature_bits, 27, 2)
self.display.text(str(int(cooking_state.current_dish_temp)), 37, 6, 1)
self.display.ellipse(54, 6, 1, 1, 1, False)
self.display.text("C", 56, 6, 1)
# Power level
if cooking_state.power_level is not None:
self.display.blit(fb_image_music_sound_wave_bits, 74, 2)
self.display.text(f"{cooking_state.power_level}W", 92, 6, 1)
if defrost_mode:
self.display.blit(fb_image_CoolHiSmall_bits, 2, 2)
else:
self.display.blit(fb_image_CoolLoSmall_bits, 2, 2)
self.show()
pass
def _message(self, text, alert):
self.display.fill_rect(0, 27, self.width, CHARACTER_HEIGHT + 4, 1)
if alert:
self.display.blit(fb_image_Alert_bits, 10, 29)
self.display.blit(fb_image_Alert_bits, self.width - 10, 29)
self.h_centered_text(text, 29, 0)
def message(self, text, alert):
self.flick()
self._message(text, alert)
self.show()
pass
+91 -27
View File
@@ -2,10 +2,11 @@ import gc
import sys
import time
import _thread
from lib.microwaveScreen import MicrowaveScreen
import uasyncio as asyncio
from machine import Pin, SoftI2C
import framebuf
import ssd1306
import ujson
# Clean memory immediately
gc.collect()
@@ -30,7 +31,13 @@ data_queue = SafeQueue()
lora = None
uart_device = None
magnetron_led = None
display = None
microwave_screen = None
defrost_mode = False
current_temp = [None, None]
last_temp = [None, None]
temperature_asked = False
last_temp_request_time = 0
PING_PAYLOAD = {
"id": DEVICE_ID,
@@ -39,7 +46,7 @@ PING_PAYLOAD = {
def init_hardware():
"""Initializes all hardware components."""
global lora, uart_device, magnetron_led, display
global lora, uart_device, magnetron_led, microwave_screen
print("[Main] Initializing hardware peripherals...")
@@ -48,6 +55,14 @@ def init_hardware():
vext.value(0)
time.sleep_ms(100)
# Init OLED Display
scl_pin = Pin(18, Pin.OUT, pull=Pin.PULL_UP)
sda_pin = Pin(17, Pin.OUT, pull=Pin.PULL_UP)
display_i2c = SoftI2C(scl=scl_pin, sda=sda_pin, freq=100000)
display = ssd1306.SSD1306_I2C(128, 64, display_i2c, addr=0x3C)
microwave_screen = MicrowaveScreen(display)
microwave_screen.bootScreen()
# Init LoRa
lora = get_lora()
lora.configure(freq=868.1, sf=7)
@@ -57,14 +72,6 @@ def init_hardware():
magnetron_led.color = RGBLED.WHITE_YELLOW
magnetron_led.off()
# Init OLED Display
scl_pin = Pin(18, Pin.OUT, pull=Pin.PULL_UP)
sda_pin = Pin(17, Pin.OUT, pull=Pin.PULL_UP)
display_i2c = SoftI2C(scl=scl_pin, sda=sda_pin, freq=100000)
display = ssd1306.SSD1306_I2C(128, 64, display_i2c, addr=0x3C)
display.text("Booting...", 1, 2, 1)
display.show()
# Init UART
uart_device = get_uart(uart_id=1, tx_pin=46, rx_pin=45)
@@ -82,7 +89,7 @@ def lora_hardware_thread():
# 1. Send periodic heartbeat
if now - last_heartbeat_time >= config.LORA_HEARTBEAT_INTERVAL:
last_heartbeat_time = now
print("\n[LoRa Thread] Sending Heartbeat...")
log("\n[LoRa Thread] Sending Heartbeat...")
if lora:
lora.send(PING_PAYLOAD)
@@ -98,7 +105,44 @@ def lora_hardware_thread():
# --- COOKING STATE CALLBACKS ---
def cooking_state_temperature_provider():
return 22.0, 29.0 # TODO: Replace with real temperature reading
global current_temp, last_temp, temperature_asked, last_temp_request_time, uart_device
def temp_is_invalid(temp):
return (
temp is None
or not isinstance(temp, (list, tuple))
or len(temp) < 2
or temp[0] is None
or temp[1] is None
)
# 1. Check for UART request timeout (reset lock if 3 seconds pass without a response)
now = time.time()
if temperature_asked and (now - last_temp_request_time > 3):
print("[CookingState] Temperature request timed out. Retrying UART request...")
temperature_asked = False
# 2. Trigger new UART request if idle
if not temperature_asked and uart_device:
temperature_asked = True
last_temp_request_time = now
uart_device.send_as_command(UARTCommand(UARTCommandType.TEMPERATURE_REQUEST, {}))
# 3. Handle fresh incoming reading
if not temp_is_invalid(current_temp):
temps = [float(current_temp[0]), float(current_temp[1])]
last_temp = [temps[0], temps[1]] # Keep a safe reference copy
# Reset current_temp buffer to consume the value
current_temp = [None, None]
return temps
# 4. Fallback: Use last valid reading
if not temp_is_invalid(last_temp):
return [float(last_temp[0]), float(last_temp[1])]
# 5. Default fallback if no data has ever arrived
return (0.0, 0.0)
def cooking_state_on_state_change(state):
print(f"[CookingState] State changed to: {state.state}")
@@ -114,15 +158,12 @@ def cooking_state_on_state_change(state):
if lora:
lora.send_reliable({"id": DEVICE_ID, "new_cooking_state": state.state})
# Update OLED display
if display:
display.fill(0)
display.text(cookingState.CookingStates.get_state_name(state.state), 1, 2, 1)
display.show()
microwave_screen.update(cooking_state, defrost_mode)
def cooking_state_on_refresh(state):
# TODO: Show screen information
pass
# Update OLED display
if microwave_screen:
microwave_screen.update(state, defrost_mode)
def cooking_state_on_pause(state):
# If the cooking is unpaused and was in STIRRING_REQUIRED or ALERT state, we set the state back to COOKING.
@@ -135,17 +176,19 @@ def cooking_state_on_pause(state):
async def uart_polling_task():
"""Polls UART for incoming messages from the WiFi board."""
global cooking_state
global cooking_state, temperature_asked
while True:
if uart_device and uart_device.any():
command = uart_device.read_as_command()
if command:
print(f"[UART Task] Received command from WiFi Board: {command.command_type}")
log(f"[UART Task] Received command from WiFi Board: {command.command_type}")
if command.command_type == UARTCommandType.COOKING_PARAMS:
params = command.payload
print(f"[UART Task] Cooking parameters received: {params}")
uart_device.send_as_command(UARTCommand(UARTCommandType.TEMPERATURE_REQUEST, {}))
cooking_state = cookingState.CookingState(
cook_time=params["cook_time"],
power_level=params["power_level"],
@@ -156,8 +199,18 @@ async def uart_polling_task():
cooking_state.set_refresh_callback(cooking_state_on_refresh)
cooking_state.set_pause_callback(cooking_state_on_pause)
await asyncio.sleep_ms(20)
await asyncio.sleep_ms(200)
cooking_state_on_state_change(cooking_state)
elif command.command_type == UARTCommandType.TEMPERATURE_RESPONSE:
try:
# Check if payload is already a dict or needs JSON decoding
payload = ujson.loads(command.payload) if isinstance(command.payload, str) else command.payload
current_temp[0] = payload.get("dish_temp", 0.0)
current_temp[1] = payload.get("ambient_temp", 0.0)
except Exception as e:
print(f"[UART Task] Error parsing temperature payload: {e}")
finally:
temperature_asked = False
else:
print(f"[UART Task] Unknown command type received: {command.command_type}")
@@ -166,7 +219,7 @@ async def uart_polling_task():
async def lora_process_task():
"""Consumes packets pushed to data_queue by the LoRa hardware thread."""
global cooking_state
global cooking_state, defrost_mode, microwave_screen
while True:
while not data_queue.empty():
@@ -190,18 +243,26 @@ async def lora_process_task():
print("[LoRa Process] Cooking resumed via orchestrator command.")
else:
log("[LoRa Process] No active cooking state to toggle pause/resume.")
elif data["action"] == LoraCommands.TOGGLE_DEFROST:
print("[LoRa Process] Toggling defrost mode via orchestrator command.")
defrost_mode = data["defrost_state"]
microwave_screen.update(cooking_state, defrost_mode)
await asyncio.sleep_ms(50)
async def cooking_loop_task():
"""Ticks the cooking state and logs information periodically."""
"""Ticks the cooking state and logs information periodically without flooding output."""
log_counter = 0
while True:
if cooking_state is not None:
cooking_state.update_tick()
log_counter += 1
# Print log output every 5 seconds (10 ticks x 500ms)
if log_counter % 10 == 0:
print(f"[Cooking Task] State: {cooking_state.state}, Temp: {cooking_state.current_dish_temp}, "
f"Paused: {cooking_state.paused}, Remaining: {cooking_state.get_remaining_time():.2f}s, "
f"Est. Remaining: {cooking_state.get_remaining_time_estimation():.2f}s")
f"Paused: {cooking_state.paused}, Remaining: {cooking_state.get_remaining_time():.2f}s")
await asyncio.sleep_ms(500)
@@ -215,6 +276,7 @@ async def memory_cleanup_task():
# --- BOOTSTRAP ---
async def main():
global cooking_state, defrost_mode, microwave_screen
print("[Main] Starting application...")
init_hardware()
@@ -235,6 +297,8 @@ async def main():
print("[Main] All async tasks running concurrently!")
microwave_screen.update(None, defrost_mode)
# Keep main task alive indefinitely
while True:
await asyncio.sleep(3600)
+7 -5
View File
@@ -195,14 +195,16 @@ async def uart_task():
try:
cmd = uart_device.read_as_command()
if cmd:
if cmd and hasattr(cmd, "command_type"):
print("[UART] Command received from LoRa board:", cmd.command_type, cmd.payload)
if (
hasattr(cmd, "command_type")
and cmd.command_type == UARTCommandType.COOKING_STATE_UPDATE
):
if cmd.command_type == UARTCommandType.COOKING_STATE_UPDATE:
if cooking_state:
cooking_state.set_state(cmd.payload.get("state"))
elif cmd.command_type == UARTCommandType.TEMPERATURE_REQUEST:
uart_device.send_as_command(UARTCommand(UARTCommandType.TEMPERATURE_RESPONSE, payloads.lora_sensor_data(
mlx_temperature_sensor.read_object_temp() if mlx_temperature_sensor else 0,
mlx_temperature_sensor.read_ambient_temp() if mlx_temperature_sensor else 0
)))
except Exception as e:
print("[UART Task] Error reading command:", e)
+7 -7
View File
@@ -127,12 +127,13 @@ async def mqtt_listener_task():
def button_callback():
"""Button physical interrupt callback."""
global button_state
if microwave_states.get("2") == MicrowaveState.COOKING:
if microwave_states.get("2") == MicrowaveState.COOKING or microwave_states.get("2") == MicrowaveState.DONE:
print("[Button] Toggling pause/resume for microwave '2'.")
lora.send_reliable({"id": DEVICE_ID, "microwave_id": "2", "action": LoraCommands.TOGGLE_PAUSE})
else:
button_state = not button_state
print(f"[Button] Defrost state toggled to: {button_state}")
lora.send_reliable({"id": DEVICE_ID, "microwave_id": "2", "action": LoraCommands.TOGGLE_DEFROST, "defrost_state": button_state})
button.set_callback(button_callback)
button.start_button_monitoring_thread()
@@ -199,13 +200,15 @@ async def handle_new_dish(microwave_id, detected_height):
ir_data_events.pop(microwave_id, None)
return
# 5. Wait for MQTT IR data (if it already arrived, event.wait() returns instantly)
# 5. Wait for MQTT IR data
try:
await asyncio.wait_for(event.wait(), timeout=10.0)
ir_payload = ir_data_cache.get(microwave_id, {})
sensors_data["ir_initial_temp"] = ir_payload.get("dish_temp")
sensors_data["ir_ambient_temp"] = ir_payload.get("ambient_temp")
print(f"[{microwave_id}] IR data synchronized successfully: {ir_payload}")
# 6. Dispatch cloud request task
asyncio.create_task(request_cloud_cooking_plan(microwave_id, sensors_data))
except asyncio.TimeoutError:
print(f"[{microwave_id}] ⚠️ Timeout waiting for MQTT IR data from ESP32.")
sensors_data["ir_initial_temp"] = None
@@ -213,9 +216,6 @@ async def handle_new_dish(microwave_id, detected_height):
finally:
ir_data_events.pop(microwave_id, None)
# 6. Dispatch cloud request task
asyncio.create_task(request_cloud_cooking_plan(microwave_id, sensors_data))
async def request_cloud_cooking_plan(microwave_id, sensors_data):
"""Sends all data to the cloud with up to 3 retries (30s interval)."""
global cloud_alert
@@ -259,8 +259,8 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
cloud_alert = False # Reset alert flag on success
microwave_states[microwave_id] = MicrowaveState.COOKING
if config.DEBUG:
c_time = min(c_time, 10) # Limit to 10s for debug
c_temp = min(c_temp, 50) # Limit to 50°C for debug
c_time = 20 # Set to 20s for debug
c_temp = 50 # Set to 50°C for debug
mqtt_client.publish(
config.MQTT_TOPIC_COOKING,
payloads.mqtt_cooking_config(microwave_id, c_time, c_power, c_temp),
+69 -27
View File
@@ -3,6 +3,7 @@ import time
class CookingState:
TEMPERATURE_TOLERANCE = 1.0
MIN_SIGNIFICANT_HEATING_RATE = 0.05 # °C/s threshold to consider valid heating
def __init__(self, cook_time: int, power_level: int, target_temp: float, temperature_provider=None, on_state_change=None, on_refresh=None):
self.cook_time = cook_time
@@ -27,6 +28,9 @@ class CookingState:
self._last_refresh_signature = None
self._stirred = False
# Moving average filter for heating rate (°C / sec)
self._smoothed_heating_rate = 0.0
def set_temperature_provider(self, temperature_provider):
self.temperature_provider = temperature_provider
@@ -45,9 +49,9 @@ class CookingState:
self.paused = True
self._pause_started_at = time.time()
# self._notify_refresh(force=True)
if self.on_pause:
self.on_pause(self)
def unpause(self):
if not self.paused:
return
@@ -56,15 +60,14 @@ class CookingState:
if self._pause_started_at is not None:
self._paused_duration += now - self._pause_started_at
# self._pause_started_at = None
self.paused = False
# self._notify_refresh(force=True)
def toggle_pause(self):
if self.paused:
self.unpause()
else:
self.pause()
if self.on_pause:
self.on_pause(self)
def set_state(self, state):
@@ -90,20 +93,56 @@ class CookingState:
def get_remaining_time_estimation(self) -> float:
elapsed_time = self.get_elapsed_time()
# 1. Base timer remaining based on standard cook time
timer_remaining = max(0.0, float(self.cook_time) - elapsed_time)
if self.current_dish_temp is None:
# If temperature is unavailable or already met target, rely on standard timer
if self.current_dish_temp is None or self.current_dish_temp >= (self.target_temp - self.TEMPERATURE_TOLERANCE):
return timer_remaining
if self.current_dish_temp >= self.target_temp:
return timer_remaining
# 2. Prevent hitting 00:00 before stirring trigger:
# If we passed half cook_time and temp is far from target, extend expected base time to 1.25x cook_time
temp_progress = max(0.0, self.current_dish_temp) / max(1.0, self.target_temp)
if elapsed_time > (self.cook_time * 0.5) and temp_progress < 0.8:
adjusted_cook_time = self.cook_time * 1.25
timer_remaining = max(0.0, adjusted_cook_time - elapsed_time)
# 3. Estimate using heating rate
heating_rate = self._estimate_heating_rate()
if heating_rate <= 0:
return timer_remaining
temp_needed = self.target_temp - self.current_dish_temp
target_remaining = (self.target_temp - self.current_dish_temp) / heating_rate
return max(timer_remaining, max(0.0, target_remaining))
if heating_rate > 0.01:
rate_based_remaining = temp_needed / heating_rate
else:
# If flat/slow, project remaining time based on remaining missing temperature fraction
temp_ratio = max(0.1, temp_needed / self.target_temp)
rate_based_remaining = max(timer_remaining, self.cook_time * temp_ratio * 1.25)
# 4. Strict dynamic cap: Never exceed maximum possible execution window (2.0x cook_time total)
max_possible_remaining = max(0.0, (self.cook_time * 2.0) - elapsed_time)
bounded_remaining = min(rate_based_remaining, max_possible_remaining)
# Return the larger of the adjusted timer or the bounded prediction
return max(timer_remaining, bounded_remaining)
def _estimate_heating_rate(self) -> float:
"""Calculates heating rate in °C/sec over time interval."""
if self._last_temperature_sample is None or self.current_dish_temp is None:
return 0.0
last_time, last_temp = self._last_temperature_sample
now = time.time()
delta_time = now - last_time
if delta_time < 0.8: # Skip micro-ticks
return 0.0
delta_temp = self.current_dish_temp - last_temp
if delta_temp <= 0:
return 0.0
return delta_temp / delta_time
def _read_temperatures(self):
if self.temperature_provider is None:
@@ -118,27 +157,29 @@ class CookingState:
raise ValueError("temperature_provider must return a pair: (dish_temp, ambient_temp)")
def _estimate_heating_rate(self):
def _update_heating_rate(self):
"""Calculates instantaneous rate and updates the Exponential Moving Average."""
now = time.time()
if self._last_temperature_sample is None:
return 0.0
self._last_temperature_sample = (now, self.current_dish_temp)
return
last_time, last_temp = self._last_temperature_sample
now = time.time()
current_temp = self.current_dish_temp
if current_temp is None:
return 0.0
delta_time = now - last_time
if delta_time <= 0:
return 0.0
return (current_temp - last_temp) / delta_time
if delta_time <= 0.5 or self.current_dish_temp is None:
return
instant_rate = (self.current_dish_temp - last_temp) / delta_time
self._last_temperature_sample = (now, self.current_dish_temp)
# Exponential Moving Average (EMA) - alpha smooths out sensor noise
alpha = 0.2
self._smoothed_heating_rate = (alpha * instant_rate) + ((1.0 - alpha) * self._smoothed_heating_rate)
def _notify_state_change(self):
if self.on_state_change is None:
return
self.on_state_change(self)
def _notify_refresh(self, force=False):
@@ -177,21 +218,22 @@ class CookingState:
now = time.time()
elapsed_time = self.get_elapsed_time()
self.estimated_remaining_time = self.get_remaining_time_estimation()
if self.current_dish_temp is not None:
if elapsed_time < (self.cook_time / 2.0) and self.current_dish_temp >= self.target_temp and (self._paused_duration == None or self._paused_duration < 5): # If the dish is heating too fast, we require stirring
self._update_heating_rate()
if elapsed_time < (self.cook_time / 2.0) and self.current_dish_temp >= self.target_temp and (self._paused_duration is None or self._paused_duration < 5):
self.state = CookingStates.STIRRING_REQUIRED
self.pause()
elif elapsed_time >= self.cook_time and self.current_dish_temp >= (self.target_temp - self.TEMPERATURE_TOLERANCE):
self.state = CookingStates.DONE
elif elapsed_time >= self.cook_time * 1.25 and (self._paused_duration == None or self._paused_duration < 5): # If the dish is not heating up
elif elapsed_time >= self.cook_time * 1.25 and (self._paused_duration is None or self._paused_duration < 5):
self.state = CookingStates.STIRRING_REQUIRED
self.pause()
elif self._pause_started_at != None and (self._pause_started_at + self._paused_duration) < (now - (self.cook_time * 0.75)): # If the dish had to be pause and it's been a long time, we stop the cooking
elif self._pause_started_at is not None and (self._pause_started_at + self._paused_duration) < (now - (self.cook_time * 0.75)):
self.state = CookingStates.DONE
self._last_temperature_sample = (now, self.current_dish_temp)
self.estimated_remaining_time = self.get_remaining_time_estimation()
if self.state != previous_state:
self._notify_state_change()
+3 -3
View File
@@ -33,10 +33,9 @@ class BaseLoraDevice:
"""Sends an immediate acknowledgement packet back to the sender."""
print(f"[ReliableLoRa] -> Triggering ACK send for msg_id: {ack_id}")
if IS_MICROPYTHON:
time.sleep_ms(100)
time.sleep_ms(50)
else:
# Give LA66 chip time to finish logging RSSI and reset RF frontend
time.sleep(0.25)
time.sleep(0.05) # Reduced delay to fit ESP32 RX window
ack_payload = {"_type": "_ack", "_ack_id": ack_id}
self.send(ack_payload)
@@ -425,3 +424,4 @@ class LoraCommands:
PING = "ping"
COOKING_STATE_UPDATE = "cooking_state_update"
TOGGLE_PAUSE = "toggle_pause"
TOGGLE_DEFROST = "toggle_defrost"
+6
View File
@@ -39,6 +39,12 @@ def mqtt_sensor_data(id_microwave, dish_temp, ambient_temp):
"ambient_temp": ambient_temp
})
def lora_sensor_data(dish_temp, ambient_temp):
return as_json({
"dish_temp": dish_temp,
"ambient_temp": ambient_temp
})
def mqtt_cooking_config(id_microwave, cook_time, power_level, target_temp):
return as_json({
"id_microwave": id_microwave,
+3
View File
@@ -75,6 +75,8 @@ class RGBLED:
"""Sets the RGB color tuple (e.g., (255, 128, 0))."""
if isinstance(rgb_tuple, (tuple, list)) and len(rgb_tuple) == 3:
self._color = tuple(rgb_tuple)
# Automatically turn LED on unless color is OFF (0,0,0)
self._is_on = (self._color != self.OFF)
self._apply()
else:
raise ValueError("Color must be a tuple of 3 integers: (R, G, B)")
@@ -144,6 +146,7 @@ class RGBLED:
if self._is_blinking:
self._timer.deinit()
self._is_blinking = False
self.on() # Ensures LED remains ON steadily after blinking stops
def blink_toggle(self, interval_ms=500):
"""Toggles blinking state (starts if stopped, stops if active)."""
+2
View File
@@ -98,3 +98,5 @@ class UARTCommandType:
"""Enumeration of known UART command types."""
COOKING_PARAMS = "COOKING_PARAMS"
COOKING_STATE_UPDATE = "COOKING_STATE_UPDATE"
TEMPERATURE_REQUEST = "TEMPERATURE_REQUEST"
TEMPERATURE_RESPONSE = "TEMPERATURE_RESPONSE"