forked from GaryDyr/imaxcharger
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathimax_logger.py
426 lines (381 loc) · 18 KB
/
imax_logger.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
"""
Bokah interface for Imax B6 mini charnger
Requires: bokeh - pip install bokuh
but calls imax_0.py; see imax_0.py for additional necessary library packages
"""
import sys
import time
import datetime
from bokeh.models import ColumnDataSource, Slider, DataTable, TableColumn
from bokeh.plotting import Figure, output_file, show
from bokeh.plotting import figure, output_file, show
from bokeh.io import output_file, show
from bokeh.layouts import widgetbox, column, row
from bokeh.models.widgets import Button, RadioButtonGroup, Select, Slider, RadioGroup, Div, TextInput
from bokeh.layouts import gridplot, layout
from bokeh.plotting import curdoc
from bokeh.driving import linear
from bokeh.models.callbacks import CustomJS
#specify varibles for the inputs.
bat_type = ""
chrg_type = ""
nominal_mah = 0
DC_or_CD = "Discharge/Charge"
cycles = 1
cells = 4
run_text = "Enter run information"
time_interval = 10 #seconds
final_read = {'final_mah':"", 'final_t':"", 'final_V':"", 'final_T':""}
#next is set in start_device(), which is obtained from imax_0.start_imax
device_dict = {'device':None, 'EndPt_out':None, 'EndPt_in':None}
text_update = None
settings_dict = {}
read_data = {}
data_out_packet = []
device_started = False
out_data = {}
start_cycle = None
run_status = 0
#create a dictionary of initial values to minimize global calls
settings = {
'bat_type':bat_type,
'cells':cells,
'chrg_type':chrg_type,
'nominal_mah':nominal_mah,
'DC_or_CD':DC_or_CD,
'cycles':cycles,
'device_started':device_started,
'time_interval':time_interval,
'start_cycle':start_cycle,
'run_text':run_text,
'run_status':run_status,
'final_out':final_read,
'data_out_packet':data_out_packet,
'settings_dict':settings_dict,
'device_dict':device_dict
}
#Create the header for page
notice1 = Div(text="""The data input here are for logging identification and saving conditions, which are already manually chosen
on the imax. They do not set or reset the imax.""",
sizing_mode = "scale_width")
select_battype = Select(title = "Battery Type", value ="", options = ['NiMH', 'NiCd', 'LiPO', 'LiFe', 'LiIO', 'LiHV'])
def select_battype_handler(attr, old, new):
settings['bat_type'] = new
select_battype.on_change('value', select_battype_handler)
select_chrg_type = Select(title="Charge Type", value="Charge", options=["Charge", "Discharge", "Cycle", "Re-Peak", "AutoCharge", "Balance Charge", "Fast Charge", "Storage"])
def select_chrg_type_handler(attr, old, new):
settings['chrg_type'] = new
select_chrg_type.on_change('value', select_chrg_type_handler)
maxmah_slider = Slider(start=50, end=24000, value=1, step=50, title="Bat. Specified Capacity, mah")
def maxmah_handler(attr, old, new):
settings['nominal_mah'] = new
maxmah_slider.on_change('value', maxmah_handler)
DC_radio_group = RadioGroup(labels=["Discharge/Charge", "Charge/Discharge"], active=0)
def DC_radio_handler(new):
settings['DC'] = new
DC_radio_group.on_click(DC_radio_handler)
select_cells = Select(title = "No. of Cells", value ="4", options = [str(i) for i in range(1,13)])
def select_cells_handler(attr, old, new):
settings['cells'] = new
select_cells.on_change('value', select_cells_handler)
#imax discharge/charge # of cycles; imax limit is 5
#no way to read this from imax only shows up in set up packet
select_cycles = Select(title = "Cycles", value ="1", options = [str(i) for i in range(1,6)])
def select_cycles_handler(attr, old, new):
settings['cycles'] = new
select_cycles.on_change('value', select_cycles_handler)
text_input = TextInput(value="Enter run information", title="Run Info::")
def text_input_handler(attr, old, new):
settings['run_text'] = new
text_input.on_change("value", text_input_handler)
textsource = ColumnDataSource(data=dict(time = [],msg = []))
columns = [
TableColumn(field="time", title="Time"),
TableColumn(field="msg", title="Msg", width = 600)]
data_table = DataTable(source=textsource, columns=columns, width=600)
button_save = Button(label="Save Run", button_type = 'warning')
def button_save_handler():
global read_data
print('button save worked')
run_modes= {'bat_type':settings['bat_type'], 'chrg_type':settings['chrg_type'], 'nominal_mah':settings['nominal_mah'],
'DC_or_CD':settings['DC_or_CD'], 'cycles':settings['cycles'], 'cells':settings['cells'], 'run_text':settings['run_text']}
excel_out = imax_0.write_excel_file(run_modes, settings['final_read'], read_data, settings['settings_dict'])
msg = 'Data saved to: ' + excel_out
print(msg)
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), msg)
button_save.on_click(button_save_handler)
def text_update(t, msg):
global data_table
print('time and msg: ', t, msg)
new_data = dict(time=[t], msg=[msg],)
textsource.stream(new_data, 20) #adding the value is a scrolloff lines
data_table.update()
import imax_0
def check_device_status():
global device_dict
#used by startstop btn handler to determine if imax start btn was pressed, or imax running.
#global device_dict
EndPt_out = device_dict['EndPt_out']
EndPt_in = device_dict['EndPt_in']
device = device_dict['device']
#send host->imax packet to trigger imax to do imax->host transfer
#make sure device is still connected
if device:
w_out = device.write(EndPt_out, settings['data_out_packet'])
data = device.read(EndPt_in.bEndpointAddress,EndPt_in.wMaxPacketSize)
settings['run_status'] = int(str(data[4]))
return settings['run_status']
else:
print('Check device failed.')
return None
def get_final_data():
global device_dict
device = device_dict['device']
EndPt_out = device_dict['EndPt_out']
EndPt_in = device_dict['EndPt_in']
final_out = [0x0f, 0x03, 0xfe, 0x000, 0xfe, 0xff, 0xff] + [0]*57
w_out = device.write(EndPt_out, final_out)
final_data = device.read(EndPt_in.bEndpointAddress,EndPt_in.wMaxPacketSize)
if final_data:
final_mah = str(final_data[5] * 256 + final_data[6]) #energy
final_t = str(final_data[7] * 256 + final_data[8]) #timer sec
final_V = str((final_data[9] * 256 + final_data[10]) / 1000.0) #voltage, V
final_T = str(final_data[14]) #Temperature, deg C, if F???
#int temp
settings['final_read'] = {'final_mah':final_mah, 'final_t':final_t, 'final_V':final_V, 'final_T':final_T}
msg = 'Run completed. Final values: ' + final_mah + ' mah; '+ final_V + ' mV; ' + final_t + ' s.'
print(msg)
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), msg)
def start_device():
global read_data
global device_dict
#sets up device if connected, returns imax settings, configrs, and sets data dictionary.
#check for device, if not there wait for connection.
device_str = imax_0.find_my_device() #returns True if device found
if "No" in device_str:
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), device_str)
print(device_str)
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'Imax offline. Cycling for one minute.')
nowtime = datetime.datetime.now()
futuretime = datetime.datetime.now() + datetime.timedelta(minutes = 1)
while "No" in device_str:
device_str = imax_0.find_my_device() #returns msg which has "No" in it ,if device not found
if "No" in device_str:
if datetime.datetime.now()> futuretime:
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'Could not find device; check device and connection.')
return False
time.sleep(1)
print('device found')
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), device_str)
#device was found, engage device, get parameters and dictionaries
device_dict, read_data, settings_dict, data_out_packet = imax_0.start_imax()
print('Loading settings)
settings['settings_dict'] = settings_dict
settings['data_out_packet'] = data_out_packet
#Determine if device is idling, or already running: run_status = 2 or 3, or is running = 1
settings['run_status'] = check_device_status()
nowtime = datetime.datetime.now()
futuretime = datetime.datetime.now() + datetime.timedelta(minutes = 1)
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'Waiting for Imax button press for one minute.')
while settings['run_status'] > 1:
settings['run_status'] = check_device_status()
if datetime.datetime.now()> futuretime:
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'Imax button not pressed in 1 min...aborting start.')
return False
time.sleep(1)
print('out of loop run_status is: ', settings['run_status'])
return True
def add_lines(plot, source, cells_num = 0):
#called from button_startstop_handler if bat_type LiPO, note cells must be > 1
color_list = ['orange', 'yellow', 'green', 'blue', 'violet', 'darkmagenta']
if cells_num > 1:
for i in range(cells_num):
p1.line(x = 'timer', y = 'cell'+ str(i+1), source = source, color = color_list[i], line_width = 2)
button_startstop = Button(label = "Start", button_type = 'success')
def button_startstop_handler():
global button_startstop
#read btn label and isas driver for condition
#label = "Start": and device_started = False is initial start up condition
if button_startstop.label == "Start":
button_startstop.label = "Connecting"
settings['device_started'] = start_device()
#returns True if device found,connected and started
if settings['device_started']:
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'Imax found & running.')
button_startstop.label = "Stop"
settings['start_cycle'] = curdoc().add_periodic_callback(update, 10000)
print('device found')
else:
if not settings['device_started']:
button_startstop.label = "Start"
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'Imax start failed. Check everything')
else: #deal with stop conditions; user pressed app stop button, or run_status > 1 (imax buttons pressed.)
if button_startstop.label == "Stop":
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'Run stopped.')
curdoc().remove_periodic_callback(settings['start_cycle'])
get_final_data()
button_startstop.label = "Start"
if "Li" in settings['bat_type']:
add_lines(p1, source, cells_num = int(settings['cells']))
button_startstop.on_click(button_startstop_handler)
def read_imax():
global read_data
global device_dict
# see above call to start_imax() for globals set from imax.py
#global data_out_packet
global out_data
#global run_status
device = device_dict['device']
EndPt_out = device_dict['EndPt_out']
EndPt_in = device_dict['EndPt_in']
#send the host->imax packet to trigger imax to fill buffer and do imax->host transfer
#make sure device is still connected
w_out = device.write(EndPt_out, settings['data_out_packet'])
try:
data = device.read(EndPt_in.bEndpointAddress,EndPt_in.wMaxPacketSize) #using more general form of Endpoint_IN attributes
except Exception as e:
print('Something went wrong: no data incoming; error is: ', e)
sys.exit()
#Parse the hex data
out_data['mah'] = [int(str(data[5]*256 + data[6]))] #capacity, mah
out_data['timer'] = [int(str(data[7]*256 + data[8]))] #seconds
out_data['volts'] = [int(str(data[9]*256 + data[10]))] #volts
out_data['current'] = [int(str(data[11]*256 + data[12]))] #amps
out_data['ext_T'] = [int(str(data[13]))]
out_data['internal_T'] = [int(str(data[14]))] #deg. C?
out_data['cell1'] = [int(str(data[17]*256 + data[18]))] #cell 1 etc.
out_data['cell2'] = [int(str(data[19]*256 + data[20]))]
out_data['cell3'] = [int(str(data[21]*256 + data[22]))]
out_data['cell4'] = [int(str(data[23]*256 + data[24]))]
out_data['cell5'] = [int(str(data[25]*256 + data[26]))]
out_data['cell6'] = [int(str(data[27]*256 + data[28]))]
read_data['mah'].append(int(str(data[5]*256 + data[6]))) #capacity, mah
read_data['timer'].append(int(str(data[7]*256 + data[8]))) #seconds
read_data['volts'].append(int(str(data[9]*256 + data[10]))) #volts
read_data['current'].append(int(str(data[11]*256 + data[12]))) #amps
read_data['ext_T'].append(int(str(data[13])))
read_data['internal_T'].append(int(str(data[14]))) #deg. C?
read_data['cell1'].append(int(str(data[17]*256 + data[18])))
read_data['cell2'].append(int(str(data[19]*256 + data[20])))
read_data['cell3'].append(int(str(data[21]*256 + data[22])))
read_data['cell4'].append(int(str(data[23]*256 + data[24])))
read_data['cell5'].append(int(str(data[25]*256 + data[26])))
read_data['cell6'].append(int(str(data[27]*256 + data[28])))
#print the data (same sequence Milek7 used with hidapi; much appreciated effort.
print(
str(data[4]) + ", " + #state
str(data[5] * 256 + data[6]) + ", " + #energy
str(data[7] * 256 + data[8]) + ", " + #timer
str((data[9] * 256 + data[10]) / 1000.0) + ", " + #voltage
str((data[11] * 256 + data[12]) / 1000.0) + ", " + #current
str(data[13]) + ", " + #ext temp
str(data[14]) + ", " + #int temp
str((data[17] * 256 + data[18]) / 1000.0) + ", " + #cell 1
str((data[19] * 256 + data[20]) / 1000.0) + ", " + #cels 2
str((data[21] * 256 + data[22]) / 1000.0) + ", " + #cell 3
str((data[23] * 256 + data[24]) / 1000.0) + ", " + #cell 4
str((data[25] * 256 + data[26]) / 1000.0) + ", " + #cell 5
str((data[27] * 256 + data[28]) / 1000.0) #cell 6
)
#Has the charger been stopped by pressing the charger Stop button?
settings['run_status'] = int(str(data[4]))
return settings['run_status'], out_data
#initialize read_data dictionary for plots
out_data = {'mah':[0], 'timer':[0], 'volts':[0], 'current':[0],
'ext_T':[0], 'internal_T':[0], 'cell1':[0], 'cell2':[0],
'cell3':[0],'cell4':[0], 'cell5':[0], 'cell6':[0]}
#time_interval = 5 #seconds
source = ColumnDataSource(data = out_data)
#Generate two plots, for capacity and voltage
p = figure(plot_width=400, plot_height=400)
p.title.text = "Capactiy Input vs. Charge Time"
p.title.text_color = "black"
p.title.text_font = "arial"
p.title.text_font_style = "bold"
p.yaxis.minor_tick_line_color = "black"
p.xaxis.axis_label = "Time, s"
p.yaxis.axis_label = "Capacity Added(mah)"
r_cap = p.line(x = 'timer', y = 'mah', source = source, color="red", line_width=2)
#Set the voltabe plot; complicated a bit by battery type
p1 = figure(plot_width=400, plot_height=400)
p1.title.text = "Voltage vs. Charge Time"
p1.title.text_color = "black"
p1.title.text_font = "arial"
p1.title.text_font_style = "bold"
p1.yaxis.minor_tick_line_color = "black"
p1.xaxis.axis_label = "Time, s"
p1.yaxis.axis_label = "Voltage. mV"
rx = p1.line(x = 'timer', y = 'volts', source = source, color = "firebrick", line_width=2)
"""
#test block for adding lines
if "Li" in bat_type:
r1 = p1.line(x ='timer', y = 'cell1', source = source, color="orange", line_width=2)
r2 = p1.line(x ='timer', y = 'cell2', source = source, color="yellow", line_width=2)
r3 = p1.line(x ='timer', y = 'cell3', source = source, color="green", line_width=2)
r4 = p1.line(x ='timer', y = 'cell4', source = source, color="blue", line_width=2)
r5 = p1.line(x ='timer', y = 'cell5', source = source, color="violet", line_width=2)
r6 = p1.line(x ='timer', y = 'cell6', source = source, color="darkmagenta", line_width=2)
"""
def update():
global out_data
global source
global device_dict
device = device_dict['device']
if device:
#note "new_data" is bokeh specific to only add to source
settings['run_status'], new_data = read_imax()
#print('read_imax returned run status: ', settings['run_status'])
if settings['run_status'] < 2 and button_startstop.label == "Stop":
source.stream(new_data) # 20)
else:
msg = '; User stopped run from imax stop button.'
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), msg)
button_startstop_handler()
else:
msg = '; Device no longer connected.'
text_update(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), msg)
button_startstop_handler()
"""
class Cycler(update()):
msg = "Message from Cycler"
start_cycle = staticmethod(curdoc().add_periodic_callback(update, 10000))
stop_cycle = staticmethod(curdoc().remove_periodic_callback(start_cycle))
# create the widgets
"""
#start_cycle = curdoc().add_periodic_callback(update, 10000) #time in milliseconds
#w1 = row(select_battype, select_chrg_type) #, width = 300) #sizing_mode = 'fixed')
w1 = widgetbox(select_battype, select_chrg_type, select_cells) #, width = 300) #sizing_mode = 'fixed')
w2 = widgetbox(maxmah_slider) #, sizing_mode='fixed')
w3 = widgetbox(text_input) #, sizing_mode = 'fixed')
w4 = widgetbox(button_startstop, button_save )#, sizing_mode ='fixed')
w5 = widgetbox(DC_radio_group, select_cycles)
w6 = widgetbox(data_table)
"""
Layit = layout([row(column(
[notice1],
[w1],
[w2],
[w3],
[w4],
[w5],
[data_table]),
column(p, p1))], sizing_mode='fixed')
"""
#Layit = gridplot([[column([p, p1]), widgetbox(w1.children+w2.children+w3.children+w4.children+w5.children)]])
curdoc().add_root(notice1)
Layit = gridplot([[widgetbox(w1.children+w2.children+w3.children+w4.children+w5.children,), column([p, p1])]])
curdoc().add_root(Layit)
curdoc().add_root(data_table)
#curdoc().add_root(p)
#curdoc().add_root(p1)
#interval is not constant,
#based on derivative of capacity
# Add a periodic callback to be run every 500 milliseconds
#curdoc().add_periodic_callback(update, 500)
#for running textarea update following is one way to do it with js
"""
textare udating w/ jquery and textarea
var txt = document.getElementById('log');
setInterval(function(){txt.value += '\ntest';},2000);
<textarea id='log' rows=50 cols=60 autofocus></textarea>
"""