diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d4326cb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: python -python: - - "3.4" -# command to install dependencies -sudo: required -install: - - "pip install -r requirements.txt" -# command to run tests -script: nosetests diff --git a/openroast/controllers/recipe.py b/openroast/controllers/recipe.py index 5f151f7..e8596c9 100644 --- a/openroast/controllers/recipe.py +++ b/openroast/controllers/recipe.py @@ -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 @@ -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 @@ -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() diff --git a/openroast/views/roasttab.py b/openroast/views/roasttab.py index 18e3490..1a37c05 100644 --- a/openroast/views/roasttab.py +++ b/openroast/views/roasttab.py @@ -6,6 +6,7 @@ import math import datetime import openroast +from multiprocessing import sharedctypes from PyQt5 import QtCore from PyQt5 import QtWidgets @@ -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) @@ -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() @@ -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