Skip to content

Commit 6a16a56

Browse files
authored
Merge pull request #47 from int3ll3ct/master
Proposed fix for issue #46
2 parents 1b7527e + eaa7f61 commit 6a16a56

File tree

3 files changed

+87
-40
lines changed

3 files changed

+87
-40
lines changed

.travis.yml

Lines changed: 0 additions & 9 deletions
This file was deleted.

openroast/controllers/recipe.py

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,77 +3,104 @@
33

44
import json
55
import openroast
6-
6+
from multiprocessing import sharedctypes, Array
7+
import ctypes
78

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

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

1524
# Tells if a recipe has been loaded
16-
self.recipeLoaded = False
25+
self.recipeLoaded = sharedctypes.Value('i', 0) # boolean
26+
27+
def _recipe(self):
28+
# retrieve the recipe as a JSON string in shared memory.
29+
# needed to allow freshroastsr700 to access Recipe from
30+
# its child process
31+
if self.recipeLoaded.value:
32+
return json.loads(self.recipe_str.value.decode('utf_8'))
33+
else:
34+
return {}
1735

1836
def load_recipe_json(self, recipeJson):
19-
self.recipe = recipeJson
20-
self.recipeLoaded = True
37+
# recipeJson is actually a dict...
38+
self.recipe_str.value = json.dumps(recipeJson).encode('utf_8')
39+
self.recipeLoaded.value = 1
2140

2241
def load_recipe_file(self, recipeFile):
2342
# Load recipe file
2443
recipeFileHandler = open(recipeFile)
25-
self.recipe = json.load(recipeFileHandler)
44+
recipe_dict = json.load(recipeFileHandler)
2645
recipeFileHandler.close()
27-
self.recipeLoaded = True
46+
self.load_recipe_json(recipe_dict)
2847

2948
def clear_recipe(self):
30-
self.recipeLoaded = False
31-
self.recipe = {}
32-
self.currentRecipeStep = 0
49+
self.recipeLoaded.value = 0
50+
self.recipe_str.value = ''.encode('utf_8')
51+
self.currentRecipeStep.value = 0
3352

3453
def check_recipe_loaded(self):
35-
return self.recipeLoaded
54+
return self.recipeLoaded.value != 0
3655

3756
def get_num_recipe_sections(self):
38-
return len(self.recipe["steps"])
57+
if not self.check_recipe_loaded():
58+
return 0
59+
return len(self._recipe()["steps"])
3960

4061
def get_current_step_number(self):
41-
return self.currentRecipeStep
62+
return self.currentRecipeStep.value
4263

4364
def get_current_fan_speed(self):
44-
return self.recipe["steps"][self.currentRecipeStep]["fanSpeed"]
65+
crnt_step = self.currentRecipeStep.value
66+
return self._recipe()["steps"][crnt_step]["fanSpeed"]
4567

4668
def get_current_target_temp(self):
47-
if(self.recipe["steps"][self.currentRecipeStep].get("targetTemp")):
48-
return self.recipe["steps"][self.currentRecipeStep]["targetTemp"]
69+
crnt_step = self.currentRecipeStep.value
70+
if(self._recipe()["steps"][crnt_step].get("targetTemp")):
71+
return self._recipe()["steps"][crnt_step]["targetTemp"]
4972
else:
5073
return 150
5174

5275
def get_current_section_time(self):
53-
return self.recipe["steps"][self.currentRecipeStep]["sectionTime"]
76+
crnt_step = self.currentRecipeStep.value
77+
return self._recipe()["steps"][crnt_step]["sectionTime"]
5478

5579
def restart_current_recipe(self):
56-
self.currentRecipeStep = 0
80+
self.currentRecipeStep.value = 0
5781
self.load_current_section()
5882

5983
def more_recipe_sections(self):
60-
if(len(self.recipe["steps"]) - self.currentRecipeStep == 0):
84+
if not self.check_recipe_loaded():
85+
return False
86+
if(len(self._recipe()["steps"]) - self.currentRecipeStep.value == 0):
6187
return False
6288
else:
6389
return True
6490

6591
def get_current_cooling_status(self):
66-
if(self.recipe["steps"][self.currentRecipeStep].get("cooling")):
67-
return self.recipe["steps"][self.currentRecipeStep]["cooling"]
92+
crnt_step = self.currentRecipeStep.value
93+
if(self._recipe()["steps"][crnt_step].get("cooling")):
94+
return self._recipe()["steps"][crnt_step]["cooling"]
6895
else:
6996
return False
7097

7198
def get_section_time(self, index):
72-
return self.recipe["steps"][index]["sectionTime"]
99+
return self._recipe()["steps"][index]["sectionTime"]
73100

74101
def get_section_temp(self, index):
75-
if(self.recipe["steps"][index].get("targetTemp")):
76-
return self.recipe["steps"][index]["targetTemp"]
102+
if(self._recipe()["steps"][index].get("targetTemp")):
103+
return self._recipe()["steps"][index]["targetTemp"]
77104
else:
78105
return 150
79106

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

89116
# Prevent the roaster from starting when section time = 0 (ex clear)
90-
if(not cooling and sectionTime > 0 and self.currentRecipeStep > 0):
117+
if(not cooling and sectionTime > 0 and
118+
self.currentRecipeStep.value > 0):
91119
openroast.roaster.roast()
92120

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

103131
def move_to_next_section(self):
132+
# this gets called from freshroastsr700's timer process, which
133+
# is spawned using multiprocessing. Therefore, all things
134+
# accessed in this function must be process-safe!
104135
if self.check_recipe_loaded():
105-
if (self.currentRecipeStep + 1) >= self.get_num_recipe_sections():
136+
if(
137+
(self.currentRecipeStep.value + 1) >=
138+
self.get_num_recipe_sections()):
106139
openroast.roaster.idle()
107140
else:
108-
self.currentRecipeStep += 1
141+
self.currentRecipeStep.value += 1
109142
self.load_current_section()
110-
openroast.window.roast.update_controllers()
143+
openroast.window.roast.schedule_update_controllers()
111144
else:
112145
openroast.roaster.idle()
113146

114147
def get_current_recipe(self):
115-
return self.recipe
148+
return self._recipe()

openroast/views/roasttab.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import math
77
import datetime
88
import openroast
9+
from multiprocessing import sharedctypes
910

1011
from PyQt5 import QtCore
1112
from PyQt5 import QtWidgets
@@ -34,6 +35,10 @@ def __init__(self):
3435
self.timer.timeout.connect(self.update_data)
3536
self.timer.start()
3637

38+
# Create a shared memory flag for scheduling the occasional call to
39+
# update_controllers() from the the timer.
40+
self._schedule_controller_update_flag = sharedctypes.Value('i', 0)
41+
3742
# Set the roast tab diabled when starting.
3843
self.setEnabled(False)
3944

@@ -104,6 +109,12 @@ def update_data(self):
104109
self.connectionStatusLabel.setHidden(False)
105110
self.setEnabled(False)
106111

112+
# if openroast.roaster has moved the recipe to the next section,
113+
# update the controller-related info onscreen.
114+
if(self._schedule_controller_update_flag.value):
115+
self._schedule_controller_update_flag.value=0
116+
self.update_controllers()
117+
107118
def create_right_pane(self):
108119
rightPane = QtWidgets.QVBoxLayout()
109120

@@ -444,5 +455,17 @@ def update_controllers(self):
444455
self.update_target_temp()
445456
self.update_fan_info()
446457

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

0 commit comments

Comments
 (0)