Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposed fix for issue #46 #47

Merged
merged 19 commits into from
Mar 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
10ef530
Recipe object is now largely process-safe. On Ubuntu systems, Openro…
Feb 8, 2017
644351e
Comment spelling review.
Feb 8, 2017
7269a81
Fixed target temp and fan level updates at recipe section boundaries …
Feb 10, 2017
4228c86
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
6b3853b
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
d1cc167
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
f9a889f
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
61d8885
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
f98720a
Revert "Proposing a change to .travis.yml to deliberately include PyQ…
Feb 10, 2017
8759b3c
Revert "Proposing a change to .travis.yml to deliberately include PyQ…
Feb 10, 2017
358b412
Revert "Proposing a change to .travis.yml to deliberately include PyQ…
Feb 10, 2017
0f02c8b
Revert "Proposing a change to .travis.yml to deliberately include PyQ…
Feb 10, 2017
72ccdab
Revert "Proposing a change to .travis.yml to deliberately include PyQ…
Feb 10, 2017
83e6b81
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
911e92c
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
d7d4c72
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
935b710
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
e6bd4e4
Proposing a change to .travis.yml to deliberately include PyQt5 for t…
Feb 10, 2017
eaa7f61
Removed .travis.yml, as Travis CI cannot easily deal with python3-qt5…
Feb 10, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .travis.yml

This file was deleted.

95 changes: 64 additions & 31 deletions openroast/controllers/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,104 @@

import json
import openroast

from multiprocessing import sharedctypes, Array
import ctypes

class Recipe(object):
def __init__(self):
self.currentRecipeStep = 0
def __init__(self, max_recipe_size_bytes=64*1024):
# this object is accessed by multiple processes, in part because
# freshroastsr700 calls Recipe.move_to_next_section() from a
# child process. Therefore, all data handling must be process-safe.

# recipe step currently being applied
self.currentRecipeStep = sharedctypes.Value('i', 0)
# Stores recipe
self.recipe = {}
# Here, we need to use shared memory to store the recipe.
# Tried multiprocessing.Manager, wasn't very successful with that,
# resorting to allocating a fixed-size, large buffer to store a JSON
# string. This Array needs to live for the lifetime of the object.
self.recipe_str = Array(ctypes.c_char, max_recipe_size_bytes)

# Tells if a recipe has been loaded
self.recipeLoaded = False
self.recipeLoaded = sharedctypes.Value('i', 0) # boolean

def _recipe(self):
# retrieve the recipe as a JSON string in shared memory.
# needed to allow freshroastsr700 to access Recipe from
# its child process
if self.recipeLoaded.value:
return json.loads(self.recipe_str.value.decode('utf_8'))
else:
return {}

def load_recipe_json(self, recipeJson):
self.recipe = recipeJson
self.recipeLoaded = True
# recipeJson is actually a dict...
self.recipe_str.value = json.dumps(recipeJson).encode('utf_8')
self.recipeLoaded.value = 1

def load_recipe_file(self, recipeFile):
# Load recipe file
recipeFileHandler = open(recipeFile)
self.recipe = json.load(recipeFileHandler)
recipe_dict = json.load(recipeFileHandler)
recipeFileHandler.close()
self.recipeLoaded = True
self.load_recipe_json(recipe_dict)

def clear_recipe(self):
self.recipeLoaded = False
self.recipe = {}
self.currentRecipeStep = 0
self.recipeLoaded.value = 0
self.recipe_str.value = ''.encode('utf_8')
self.currentRecipeStep.value = 0

def check_recipe_loaded(self):
return self.recipeLoaded
return self.recipeLoaded.value != 0

def get_num_recipe_sections(self):
return len(self.recipe["steps"])
if not self.check_recipe_loaded():
return 0
return len(self._recipe()["steps"])

def get_current_step_number(self):
return self.currentRecipeStep
return self.currentRecipeStep.value

def get_current_fan_speed(self):
return self.recipe["steps"][self.currentRecipeStep]["fanSpeed"]
crnt_step = self.currentRecipeStep.value
return self._recipe()["steps"][crnt_step]["fanSpeed"]

def get_current_target_temp(self):
if(self.recipe["steps"][self.currentRecipeStep].get("targetTemp")):
return self.recipe["steps"][self.currentRecipeStep]["targetTemp"]
crnt_step = self.currentRecipeStep.value
if(self._recipe()["steps"][crnt_step].get("targetTemp")):
return self._recipe()["steps"][crnt_step]["targetTemp"]
else:
return 150

def get_current_section_time(self):
return self.recipe["steps"][self.currentRecipeStep]["sectionTime"]
crnt_step = self.currentRecipeStep.value
return self._recipe()["steps"][crnt_step]["sectionTime"]

def restart_current_recipe(self):
self.currentRecipeStep = 0
self.currentRecipeStep.value = 0
self.load_current_section()

def more_recipe_sections(self):
if(len(self.recipe["steps"]) - self.currentRecipeStep == 0):
if not self.check_recipe_loaded():
return False
if(len(self._recipe()["steps"]) - self.currentRecipeStep.value == 0):
return False
else:
return True

def get_current_cooling_status(self):
if(self.recipe["steps"][self.currentRecipeStep].get("cooling")):
return self.recipe["steps"][self.currentRecipeStep]["cooling"]
crnt_step = self.currentRecipeStep.value
if(self._recipe()["steps"][crnt_step].get("cooling")):
return self._recipe()["steps"][crnt_step]["cooling"]
else:
return False

def get_section_time(self, index):
return self.recipe["steps"][index]["sectionTime"]
return self._recipe()["steps"][index]["sectionTime"]

def get_section_temp(self, index):
if(self.recipe["steps"][index].get("targetTemp")):
return self.recipe["steps"][index]["targetTemp"]
if(self._recipe()["steps"][index].get("targetTemp")):
return self._recipe()["steps"][index]["targetTemp"]
else:
return 150

Expand All @@ -87,7 +114,8 @@ def set_roaster_settings(self, targetTemp, fanSpeed, sectionTime, cooling):
openroast.roaster.cool()

# Prevent the roaster from starting when section time = 0 (ex clear)
if(not cooling and sectionTime > 0 and self.currentRecipeStep > 0):
if(not cooling and sectionTime > 0 and
self.currentRecipeStep.value > 0):
openroast.roaster.roast()

openroast.roaster.target_temp = targetTemp
Expand All @@ -101,15 +129,20 @@ def load_current_section(self):
self.get_current_cooling_status())

def move_to_next_section(self):
# this gets called from freshroastsr700's timer process, which
# is spawned using multiprocessing. Therefore, all things
# accessed in this function must be process-safe!
if self.check_recipe_loaded():
if (self.currentRecipeStep + 1) >= self.get_num_recipe_sections():
if(
(self.currentRecipeStep.value + 1) >=
self.get_num_recipe_sections()):
openroast.roaster.idle()
else:
self.currentRecipeStep += 1
self.currentRecipeStep.value += 1
self.load_current_section()
openroast.window.roast.update_controllers()
openroast.window.roast.schedule_update_controllers()
else:
openroast.roaster.idle()

def get_current_recipe(self):
return self.recipe
return self._recipe()
23 changes: 23 additions & 0 deletions openroast/views/roasttab.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import math
import datetime
import openroast
from multiprocessing import sharedctypes

from PyQt5 import QtCore
from PyQt5 import QtWidgets
Expand Down Expand Up @@ -34,6 +35,10 @@ def __init__(self):
self.timer.timeout.connect(self.update_data)
self.timer.start()

# Create a shared memory flag for scheduling the occasional call to
# update_controllers() from the the timer.
self._schedule_controller_update_flag = sharedctypes.Value('i', 0)

# Set the roast tab diabled when starting.
self.setEnabled(False)

Expand Down Expand Up @@ -104,6 +109,12 @@ def update_data(self):
self.connectionStatusLabel.setHidden(False)
self.setEnabled(False)

# if openroast.roaster has moved the recipe to the next section,
# update the controller-related info onscreen.
if(self._schedule_controller_update_flag.value):
self._schedule_controller_update_flag.value=0
self.update_controllers()

def create_right_pane(self):
rightPane = QtWidgets.QVBoxLayout()

Expand Down Expand Up @@ -444,5 +455,17 @@ def update_controllers(self):
self.update_target_temp()
self.update_fan_info()

def schedule_update_controllers(self):
"""This is designed to be called from other processes. Currently,
the openroast.roaster instance calls this function from a
child process. This object's timer routine (which periodically
calls update_data()) will pick up this flag at the next timer tick
and call update_controllers() at that time.
Alternately, we could have set up a complicated system to
support calling into the Pyqt app from a separate process - this
is easier, at the expense of being not quite immediate, graphically.
"""
self._schedule_controller_update_flag.value = 1

def get_recipe_object(self):
return openroast.recipes