Skip to content

Commit c7c2da6

Browse files
committed
initial version
1 parent 4616e36 commit c7c2da6

File tree

10 files changed

+1882
-0
lines changed

10 files changed

+1882
-0
lines changed

Control-Panel.png

17.7 KB
Loading

DataModel.py

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
from kivy.adapters.dictadapter import DictAdapter
2+
from kivy.uix.listview import ListItemButton, ListItemLabel, \
3+
CompositeListItem, ListView, SelectableView
4+
from kivy.uix.gridlayout import GridLayout
5+
6+
7+
from kivy.uix.label import Label
8+
from kivy.uix.textinput import TextInput
9+
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
10+
from kivy.uix.popup import Popup
11+
from kivy.uix.button import Button
12+
from kivy.uix.boxlayout import BoxLayout
13+
from kivy.clock import Clock
14+
from kivy.lang import Builder
15+
from kivy.event import EventDispatcher
16+
from kivy.logger import Logger
17+
from backgroundJob import BackgroundJob
18+
19+
from random import randint
20+
Builder.load_file("DataModel.kv")
21+
22+
integers_dict = {}
23+
24+
25+
class ErrorPopup(Popup):
26+
"""
27+
Popup class to display error messages
28+
"""
29+
def __init__(self, **kwargs):
30+
print kwargs
31+
super(ErrorPopup, self).__init__(**kwargs)
32+
content = BoxLayout(orientation="vertical")
33+
content.add_widget(Label(text=kwargs['text'], font_size=20))
34+
mybutton = Button(text="Dismiss", size_hint=(1,.20), font_size=20)
35+
content.add_widget(mybutton)
36+
self.content = content
37+
self.title = kwargs["title"]
38+
self.auto_dismiss = False
39+
self.size_hint = .7, .5
40+
self.font_size = 20
41+
mybutton.bind(on_release=self.exit_popup)
42+
self.open()
43+
44+
def exit_popup(self, *args):
45+
self.dismiss()
46+
47+
48+
class ListItemReprMixin(Label):
49+
"""
50+
repr class for ListItem Composite class
51+
"""
52+
def __repr__(self):
53+
text = self.text.encode('utf-8') if isinstance(self.text, unicode) \
54+
else self.text
55+
return '<%s text=%s>' % (self.__class__.__name__, text)
56+
57+
58+
class NumericTextInput(SelectableView, TextInput):
59+
"""
60+
:class:`~kivy.uix.listview.NumericTextInput` mixes
61+
:class:`~kivy.uix.listview.SelectableView` with
62+
:class:`~kivy.uix.label.TextInput` to produce a label suitable for use in
63+
:class:`~kivy.uix.listview.ListView`.
64+
"""
65+
edit = BooleanProperty(False)
66+
67+
def __init__(self, data_model, minval, maxval, **kwargs):
68+
self.minval = minval
69+
self.maxval = maxval
70+
71+
super(NumericTextInput, self).__init__(**kwargs)
72+
try:
73+
self.val = int(self.text)
74+
except ValueError:
75+
error = "Only numeric value in range {0}-{1} to be used".format(minval, maxval)
76+
self.hint_text = error
77+
self.padding_x = self.width
78+
self.disabled = True
79+
self.data_model = data_model
80+
81+
def on_touch_down(self, touch):
82+
if self.collide_point(*touch.pos) and not self.edit:
83+
self.edit = True
84+
self.select()
85+
return super(NumericTextInput, self).on_touch_down(touch)
86+
87+
def select(self, *args):
88+
self.disabled = False
89+
self.bold = True
90+
if isinstance(self.parent, CompositeListItem):
91+
for child in self.parent.children:
92+
print child.children
93+
self.parent.select_from_child(self, *args)
94+
95+
def deselect(self, *args):
96+
self.bold = False
97+
self.disabled = True
98+
if isinstance(self.parent, CompositeListItem):
99+
self.parent.deselect_from_child(self, *args)
100+
101+
def select_from_composite(self, *args):
102+
self.bold = True
103+
104+
def deselect_from_composite(self, *args):
105+
self.bold = False
106+
107+
def on_text_validate(self, *args):
108+
109+
try:
110+
int(self.text)
111+
112+
if not(self.minval <= int(self.text) <= self.maxval):
113+
raise ValueError
114+
self.edit = False
115+
self.data_model.on_data_update(self.index, self.text)
116+
self.deselect()
117+
except ValueError:
118+
error_text = ("Only numeric value "
119+
"in range {0}-{1} to be used".format(self.minval,
120+
self.maxval))
121+
ErrorPopup(title="Error", text=error_text)
122+
self.text = ""
123+
self.hint_text = error_text
124+
return
125+
126+
def on_text_focus(self, instance, focus):
127+
if focus is False:
128+
self.text = instance.text
129+
self.edit = False
130+
self.deselect()
131+
132+
133+
class UpdateEventDispatcher(EventDispatcher):
134+
'''
135+
Event dispatcher for updates in Data Model
136+
'''
137+
def __init__(self, **kwargs):
138+
self.register_event_type('on_update')
139+
super(UpdateEventDispatcher, self).__init__(**kwargs)
140+
141+
def on_update(self, _parent, blockname, data):
142+
Logger.debug("In UpdateEventDispatcher "
143+
"on_update {parent:%s,"
144+
" blockname: %s, data:%s,}" % (_parent, blockname, data))
145+
_parent.sync_data_callback(blockname, data)
146+
147+
148+
class DataModel(GridLayout):
149+
"""
150+
Uses :class:`CompositeListItem` for list item views comprised by two
151+
:class:`ListItemButton`s and one :class:`ListItemLabel`. Illustrates how
152+
to construct the fairly involved args_converter used with
153+
:class:`CompositeListItem`.
154+
"""
155+
minval = NumericProperty(0)
156+
maxval = NumericProperty(0)
157+
simulate = False
158+
time_interval = 1
159+
dirty_thread = False
160+
dirty_model = False
161+
simulate_timer = None
162+
simulate = False
163+
dispatcher = None
164+
list_view = None
165+
_parent = None
166+
is_simulating = False
167+
blockname = "<BLOCK_NAME_NOT_SET>"
168+
169+
def __init__(self, **kwargs):
170+
kwargs['cols'] = 2
171+
kwargs['size_hint'] = (1.0, 1.0)
172+
super(DataModel, self).__init__(**kwargs)
173+
self.init()
174+
175+
def init(self, simulate=False, time_interval=1, **kwargs):
176+
"""
177+
Initializes Datamodel
178+
179+
"""
180+
self.minval = kwargs.get("minval", self.minval)
181+
self.maxval = kwargs.get("maxval", self.maxval)
182+
self.blockname = kwargs.get("blockname", self.blockname)
183+
self.clear_widgets()
184+
self.simulate = simulate
185+
self.time_interval = time_interval
186+
dict_adapter = DictAdapter(data={},
187+
args_converter=self.arg_converter,
188+
selection_mode='single',
189+
allow_empty_selection=True,
190+
cls=CompositeListItem
191+
)
192+
193+
# Use the adapter in our ListView:
194+
self.list_view = ListView(adapter=dict_adapter)
195+
self.add_widget(self.list_view)
196+
self.dispatcher = UpdateEventDispatcher()
197+
self._parent = kwargs.get('_parent', None)
198+
self.simulate_timer = BackgroundJob(
199+
"simulation",
200+
self.time_interval,
201+
self._simulate_block_values
202+
)
203+
204+
def clear_widgets(self, make_dirty=False, **kwargs):
205+
"""
206+
Overidden Clear widget function used while deselecting/deleting slave
207+
:param make_dirty:
208+
:param kwargs:
209+
:return:
210+
"""
211+
if make_dirty:
212+
self.dirty_model = True
213+
super(DataModel, self).clear_widgets(**kwargs)
214+
215+
def reinit(self, **kwargs):
216+
"""
217+
Re-initializes Datamodel on change in model configuration from settings
218+
:param kwargs:
219+
:return:
220+
"""
221+
self.minval = kwargs.get("minval", self.minval)
222+
self.maxval = kwargs.get("maxval", self.maxval)
223+
time_interval = kwargs.get("time_interval", None)
224+
try:
225+
if time_interval and int(time_interval) != self.time_interval:
226+
self.time_interval = time_interval
227+
if self.is_simulating:
228+
self.simulate_timer.cancel()
229+
self.simulate_timer = BackgroundJob(self.time_interval,
230+
self._simulate_block_values)
231+
self.dirty_thread = False
232+
self.start_stop_simulation(self.simulate)
233+
except ValueError:
234+
Logger.debug("Error while reinitializing DataModel %s" % kwargs)
235+
236+
def update_view(self):
237+
"""
238+
Updates view with listview again
239+
:return:
240+
"""
241+
if self.dirty_model:
242+
self.add_widget(self.list_view)
243+
self.dirty_model = False
244+
245+
def arg_converter(self, index, data):
246+
"""
247+
arg converter to convert data to list view
248+
:param index:
249+
:param data:
250+
:return:
251+
"""
252+
_id = self.list_view.adapter.sorted_keys[index]
253+
return {
254+
'text': str(_id),
255+
'size_hint_y': None,
256+
'height': 30,
257+
'cls_dicts': [
258+
{
259+
'cls': ListItemButton,
260+
'kwargs': {'text': str(_id)}
261+
},
262+
{
263+
'cls': NumericTextInput,
264+
'kwargs': {
265+
'data_model': self,
266+
'minval': self.minval,
267+
'maxval': self.maxval,
268+
'text': str(data),
269+
'multiline': False,
270+
'is_representing_cls': True,
271+
272+
273+
}
274+
},
275+
]
276+
}
277+
278+
def add_data(self, data, item_strings):
279+
"""
280+
Adds data to the Data model
281+
:param data:
282+
:param item_strings:
283+
:return:
284+
"""
285+
self.update_view()
286+
last_index = len(item_strings)
287+
if last_index in item_strings:
288+
last_index = int(item_strings[-1]) + 1
289+
item_strings.append(last_index)
290+
self.list_view.adapter.data.update({last_index: data})
291+
self.list_view._trigger_reset_populate()
292+
return self.list_view.adapter.data, item_strings
293+
294+
def delete_data(self, item_strings):
295+
"""
296+
Delete data from data model
297+
:param item_strings:
298+
:return:
299+
"""
300+
selections = self.list_view.adapter.selection
301+
items_popped = []
302+
for item in selections:
303+
index_popped = item_strings.pop(item_strings.index(int(item.text)))
304+
data_popped = self.list_view.adapter.data.pop(int(item.text), None)
305+
self.list_view.adapter.update_for_new_data()
306+
self.list_view._trigger_reset_populate()
307+
items_popped.append(index_popped)
308+
return items_popped, self.list_view.adapter.data
309+
310+
def on_selection_change(self, item):
311+
pass
312+
313+
def on_data_update(self, index, data):
314+
"""
315+
Call back function to update data when data is changed in the list view
316+
:param index:
317+
:param data:
318+
:return:
319+
"""
320+
self.list_view.adapter.data.update({index: data})
321+
self.list_view._trigger_reset_populate()
322+
self.dispatcher.dispatch('on_update', self._parent, self.blockname,
323+
self.list_view.adapter.data)
324+
325+
def refresh(self, data={}):
326+
"""
327+
Data model refresh function to update when the view when slave is
328+
selected
329+
:param data:
330+
:return:
331+
"""
332+
self.update_view()
333+
self.list_view.adapter.data = data
334+
self.list_view.disabled = False
335+
self.list_view._trigger_reset_populate()
336+
337+
def start_stop_simulation(self, simulate):
338+
"""
339+
Starts or stops simulating data
340+
:param simulate:
341+
:return:
342+
"""
343+
self.simulate = simulate
344+
345+
if self.simulate:
346+
if self.dirty_thread:
347+
self.simulate_timer = BackgroundJob(
348+
self.time_interval,
349+
self._simulate_block_values
350+
)
351+
self.simulate_timer.start()
352+
self.dirty_thread = False
353+
self.is_simulating = True
354+
else:
355+
self.simulate_timer.cancel()
356+
self.dirty_thread = True
357+
self.is_simulating = False
358+
359+
def _simulate_block_values(self):
360+
if self.simulate:
361+
data = self.list_view.adapter.data
362+
if data:
363+
for index, value in data.items():
364+
data[index] = randint(self.minval, self.maxval)
365+
print self.minval, self.maxval, data[index]
366+
self.refresh(data)
367+
self.dispatcher.dispatch('on_update',
368+
self._parent,
369+
self.blockname,
370+
self.list_view.adapter.data)
371+
372+
373+
374+

backgroundJob.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import threading
2+
import logging
3+
4+
5+
class BackgroundJob(threading.Thread):
6+
def __init__(self, name, interval, function):
7+
threading.Thread.__init__(self)
8+
self._name = name
9+
self._logger = logging.getLogger("modbus_tk")
10+
self.interval = interval
11+
self.simulate_func = function
12+
self.stop_timer = threading.Event()
13+
14+
def run(self):
15+
self._logger.info("Start %s thread" % self._name)
16+
while not self.stop_timer.is_set():
17+
if not self.stop_timer.is_set():
18+
self.simulate_func()
19+
self.stop_timer.wait(self.interval)
20+
self._logger.info("Stop %s thread" % self._name)
21+
22+
def cancel(self):
23+
self.stop_timer.set()
24+

0 commit comments

Comments
 (0)