· 5 years ago · Mar 25, 2020, 05:14 PM
1#!/usr/bin/env python
2
3import logging
4import json
5# logging.basicConfig has to be before astm import, otherwise logs don't appear
6logging.basicConfig(format='%(asctime)s %(levelname)s [%(name)s] %(message)s', level=logging.WARNING)
7# a nasty workaround on missing hidapi.dll on my windows (allows testing from saved files, but not download of pump)
8try:
9 import hid # pip install hidapi - Platform independant
10except WindowsError:
11 pass
12import astm # pip install astm
13import struct
14import binascii
15import datetime
16import crc16 # pip install crc16
17import Crypto.Cipher.AES # pip install PyCrypto
18import sqlite3
19import hashlib
20import re
21import pickle # needed for local history export
22import lzo # pip install python-lzo
23from .pump_history_parser import NGPHistoryEvent, BloodGlucoseReadingEvent
24from .helpers import DateTimeHelper
25import time
26from dateutil import tz
27import paho.mqtt.client as mqtt
28
29logger = logging.getLogger(__name__)
30
31ascii= {
32 'ACK' : 0x06,
33 'CR' : 0x0D,
34 'ENQ' : 0x05,
35 'EOT' : 0x04,
36 'ETB' : 0x17,
37 'ETX' : 0x03,
38 'LF' : 0x0A,
39 'NAK' : 0x15,
40 'STX' : 0x02
41}
42
43def ord_hack(char_or_byte):
44 return char_or_byte if isinstance(char_or_byte, int) else ord(char_or_byte)
45
46class COM_D_COMMAND:
47 HIGH_SPEED_MODE_COMMAND = 0x0412
48 TIME_REQUEST = 0x0403
49 TIME_RESPONSE = 0x0407
50 READ_PUMP_STATUS_REQUEST = 0x0112
51 READ_PUMP_STATUS_RESPONSE = 0x013C
52 READ_BASAL_PATTERN_REQUEST = 0x0116
53 READ_BASAL_PATTERN_RESPONSE = 0x0123
54 READ_BOLUS_WIZARD_CARB_RATIOS_REQUEST = 0x012B
55 READ_BOLUS_WIZARD_CARB_RATIOS_RESPONSE = 0x012C
56 READ_BOLUS_WIZARD_SENSITIVITY_FACTORS_REQUEST = 0x012E
57 READ_BOLUS_WIZARD_SENSITIVITY_FACTORS_RESPONSE = 0x012F
58 READ_BOLUS_WIZARD_BG_TARGETS_REQUEST = 0x0131
59 READ_BOLUS_WIZARD_BG_TARGETS_RESPONSE = 0x0132
60 DEVICE_STRING_REQUEST = 0x013A
61 DEVICE_STRING_RESPONSE = 0x013B
62 DEVICE_CHARACTERISTICS_REQUEST = 0x0200
63 DEVICE_CHARACTERISTICS_RESPONSE = 0x0201
64 READ_HISTORY_REQUEST = 0x0304
65 READ_HISTORY_RESPONSE = 0x0305
66 END_HISTORY_TRANSMISSION = 0x030A
67 READ_HISTORY_INFO_REQUEST = 0x030C
68 READ_HISTORY_INFO_RESPONSE = 0x030D
69 UNMERGED_HISTORY_RESPONSE = 0x030E
70 INITIATE_MULTIPACKET_TRANSFER = 0xFF00
71 MULTIPACKET_SEGMENT_TRANSMISSION = 0xFF01
72 MULTIPACKET_RESEND_PACKETS = 0xFF02
73 ACK_MULTIPACKET_COMMAND = 0x00FE # TODO ACK_COMMAND
74 NAK_COMMAND = 0x00FF
75 BOLUSES_REQUEST = 0x0114
76 REMOTE_BOLUS_REQUEST = 0x0100
77 REQUEST_0x0124 = 0x0124
78 REQUEST_0x0405 = 0x0405
79 TEMP_BASAL_REQUEST = 0x0115
80 SUSPEND_RESUME_REQUEST = 0x0107
81 NGP_PARAMETER_REQUEST = 0x0138
82
83class HISTORY_DATA_TYPE:
84 PUMP_DATA = 0x02
85 SENSOR_DATA = 0x03
86
87class TimeoutException( Exception ):
88 pass
89
90class ChecksumException( Exception ):
91 pass
92
93class UnexpectedMessageException( Exception ):
94 pass
95
96class UnexpectedStateException( Exception ):
97 pass
98
99class NegotiationException( Exception ):
100 pass
101
102class InvalidMessageError( Exception ):
103 pass
104
105class ChecksumError( Exception ):
106 pass
107
108class DataIncompleteError( Exception ):
109 pass
110
111class Config( object ):
112 def __init__( self, stickSerial ):
113 self.conn = sqlite3.connect( 'read_minimed.db' )
114 self.c = self.conn.cursor()
115 self.c.execute( '''CREATE TABLE IF NOT EXISTS
116 config ( stick_serial TEXT PRIMARY KEY, hmac TEXT, key TEXT, last_radio_channel INTEGER )''' )
117 self.c.execute( "INSERT OR IGNORE INTO config VALUES ( ?, ?, ?, ? )", ( stickSerial, '', '', 0x14 ) )
118 self.conn.commit()
119
120 self.loadConfig( stickSerial )
121
122 def loadConfig( self, stickSerial ):
123 self.c.execute( 'SELECT * FROM config WHERE stick_serial = ?', ( stickSerial, ) )
124 self.data = self.c.fetchone()
125
126 @property
127 def stickSerial( self ):
128 return self.data[0]
129
130 @property
131 def lastRadioChannel( self ):
132 return self.data[3]
133
134 @lastRadioChannel.setter
135 def lastRadioChannel( self, value ):
136 self.c.execute( "UPDATE config SET last_radio_channel = ? WHERE stick_serial = ?", ( value, self.stickSerial ) )
137 self.conn.commit()
138 self.loadConfig( self.stickSerial )
139
140 @property
141 def hmac( self ):
142 return self.data[1]
143
144 @hmac.setter
145 def hmac( self, value ):
146 self.c.execute( "UPDATE config SET hmac = ? WHERE stick_serial = ?", ( value, self.stickSerial ) )
147 self.conn.commit()
148 self.loadConfig( self.stickSerial )
149
150 @property
151 def key( self ):
152 return self.data[2]
153
154 @key.setter
155 def key( self, value ):
156 self.c.execute( "UPDATE config SET key = ? WHERE stick_serial = ?", ( value, self.stickSerial ) )
157 self.conn.commit()
158 self.loadConfig( self.stickSerial )
159
160class MedtronicSession( object ):
161 radioChannel = None
162 bayerSequenceNumber = 1
163 minimedSequenceNumber = 1
164 sendSequenceNumber = 0
165
166 @property
167 def HMAC( self ):
168 serial = bytearray( re.sub( r"\d+-", "", self.stickSerial ), 'ascii' )
169 paddingKey = b"A4BD6CED9A42602564F413123"
170 digest = hashlib.sha256(serial + paddingKey).hexdigest()
171 return "".join(reversed([digest[i:i+2] for i in range(0, len(digest), 2)]))
172
173 @property
174 def hexKey( self ):
175 if self.config.key == "":
176 raise Exception( "Key not found in config database. Run get_hmac_and_key.py to get populate HMAC and key." )
177 return self.config.key
178
179 @property
180 def stickSerial( self ):
181 return self._stickSerial
182
183 @stickSerial.setter
184 def stickSerial( self, value ):
185 self._stickSerial = value
186 self.config = Config( self.stickSerial )
187 self.radioChannel = self.config.lastRadioChannel
188
189 @property
190 def linkMAC( self ):
191 return self._linkMAC
192
193 @linkMAC.setter
194 def linkMAC( self, value ):
195 self._linkMAC = value
196
197 @property
198 def pumpMAC( self ):
199 return self._pumpMAC
200
201 @pumpMAC.setter
202 def pumpMAC( self, value ):
203 self._pumpMAC = value
204
205 @property
206 def linkSerial( self ):
207 return self.linkMAC & 0xffffff
208
209 @property
210 def pumpSerial( self ):
211 return self.pumpMAC & 0xffffff
212
213 @property
214 def KEY( self ):
215 return self._key
216
217 @KEY.setter
218 def KEY( self, value ):
219 self._key = value
220
221 @property
222 def IV( self ):
223 tmp = bytearray()
224 tmp.append(self.radioChannel)
225 tmp += self.KEY[1:]
226 return bytes(tmp)
227
228class MedtronicMessage( object ):
229 ENVELOPE_SIZE = 2
230
231 def __init__( self, commandAction=None, session=None, payload=None ):
232 self.commandAction = commandAction
233 self.session = session
234 if payload:
235 self.setPayload( payload )
236
237 def setPayload( self, payload ):
238 self.payload = payload
239 self.envelope = struct.pack( '<BB', self.commandAction,
240 len( self.payload ) + self.ENVELOPE_SIZE )
241
242 @classmethod
243 def calculateCcitt( self, data ):
244 crc = crc16.crc16xmodem( bytes(data), 0xffff )
245 return crc & 0xffff
246
247 def pad( self, x, n = 16 ):
248 p = n - ( len( x ) % n )
249 return x + bytes(bytearray(p))#chr(p) * p
250
251 # Encrpytion equivalent to Java's AES/CFB/NoPadding mode
252 def encrypt( self, clear ):
253 cipher = Crypto.Cipher.AES.new(
254 key=self.session.KEY,
255 mode=Crypto.Cipher.AES.MODE_CFB,
256 IV=self.session.IV,
257 segment_size=128
258 )
259
260 encrypted = cipher.encrypt(self.pad(clear))[0:len(clear)]
261 return encrypted
262
263 # Decryption equivalent to Java's AES/CFB/NoPadding mode
264 def decrypt( self, encrypted ):
265 cipher = Crypto.Cipher.AES.new(
266 key=self.session.KEY,
267 mode=Crypto.Cipher.AES.MODE_CFB,
268 IV=self.session.IV,
269 segment_size=128
270 )
271
272 decrypted = cipher.decrypt(self.pad(encrypted))[0:len(encrypted)]
273 return decrypted
274
275 def encode( self ):
276 # Increment the Minimed Sequence Number
277 self.session.minimedSequenceNumber += 1
278 message = self.envelope + self.payload
279 crc = struct.pack( '<H', crc16.crc16xmodem( message, 0xffff ) & 0xffff )
280 return message + crc
281
282 @classmethod
283 def decode( cls, message, session ):
284 response = cls()
285 response.session = session
286 response.envelope = message[0:2]
287 response.payload = message[2:-2]
288 response.originalMessage = message;
289
290 checksum = struct.unpack( '<H', message[-2:] )[0]
291 calcChecksum = MedtronicMessage.calculateCcitt( response.envelope + response.payload )
292 if( checksum != calcChecksum ):
293 raise ChecksumException( 'Expected to get {0}. Got {1}'.format( calcChecksum, checksum ) )
294
295 return response
296
297class ChannelNegotiateMessage( MedtronicMessage ):
298 def __init__( self, session ):
299 MedtronicMessage.__init__( self, 0x03, session )
300
301 # The minimedSequenceNumber is always sent as 1 for this message,
302 # even though the sequence should keep incrementing as normal
303 payload = struct.pack( '<BB8s', 1, session.radioChannel,
304 b'\x00\x00\x00\x07\x07\x00\x00\x02' )
305 payload += struct.pack( '<Q', session.linkMAC )
306 payload += struct.pack( '<Q', session.pumpMAC )
307
308 self.setPayload( payload )
309
310class MedtronicSendMessage( MedtronicMessage ):
311 def __init__( self, messageType, session, payload=None ):
312 MedtronicMessage.__init__( self, 0x05, session )
313
314 # FIXME - make this not be hard coded
315 if messageType == COM_D_COMMAND.HIGH_SPEED_MODE_COMMAND:
316 seqNo = self.session.sendSequenceNumber | 0x80
317 else:
318 seqNo = self.session.sendSequenceNumber
319
320 encryptedPayload = struct.pack( '>BH', seqNo, messageType )
321 if payload:
322 encryptedPayload += payload
323 crc = crc16.crc16xmodem( encryptedPayload, 0xffff )
324 encryptedPayload += struct.pack( '>H', crc & 0xffff )
325 # logger.debug("### PAYLOAD")
326 # logger.debug(binascii.hexlify( encryptedPayload ))
327
328 mmPayload = struct.pack( '<QBBB',
329 self.session.pumpMAC,
330 self.session.minimedSequenceNumber,
331 0x11, # Mode flags
332 len( encryptedPayload )
333 )
334 mmPayload += self.encrypt( encryptedPayload )
335
336 self.setPayload( mmPayload )
337 self.session.sendSequenceNumber += 1
338
339class MedtronicReceiveMessage( MedtronicMessage ):
340 @classmethod
341 def decode( cls, message, session ):
342 response = MedtronicMessage.decode( message, session )
343
344 # TODO - check validity of the envelope
345 response.responseEnvelope = response.payload[0:22]
346 decryptedResponsePayload = response.decrypt( bytes(response.payload[22:]) )
347
348 response.responsePayload = decryptedResponsePayload[0:-2]
349
350 # logger.debug("### DECRYPTED PAYLOAD:")
351 # logger.debug(binascii.hexlify( response.responsePayload ))
352
353 if len( response.responsePayload ) > 2:
354 checksum = struct.unpack( '>H', decryptedResponsePayload[-2:])[0]
355 calcChecksum = MedtronicMessage.calculateCcitt( response.responsePayload )
356 if( checksum != calcChecksum ):
357 raise ChecksumException( 'Expected to get {0}. Got {1}'.format( calcChecksum, checksum ) )
358
359 response.__class__ = MedtronicReceiveMessage
360
361 if response.messageType == COM_D_COMMAND.TIME_RESPONSE:
362 response.__class__ = PumpTimeResponseMessage
363 elif response.messageType == COM_D_COMMAND.READ_HISTORY_INFO_RESPONSE:
364 response.__class__ = PumpHistoryInfoResponseMessage
365 elif response.messageType == COM_D_COMMAND.READ_PUMP_STATUS_RESPONSE:
366 response.__class__ = PumpStatusResponseMessage
367 elif response.messageType == COM_D_COMMAND.INITIATE_MULTIPACKET_TRANSFER:
368 response.__class__ = MultiPacketSegment
369 elif response.messageType == COM_D_COMMAND.MULTIPACKET_SEGMENT_TRANSMISSION:
370 response.__class__ = MultiPacketSegment
371 elif response.messageType == COM_D_COMMAND.END_HISTORY_TRANSMISSION:
372 response.__class__ = MultiPacketSegment
373
374 return response
375
376 @property
377 def messageType( self ):
378 return struct.unpack( '>H', self.responsePayload[1:3] )[0]
379
380
381class ReadInfoResponseMessage( object ):
382 @classmethod
383 def decode( cls, message ):
384 response = cls()
385 response.responsePayload = message
386 return response
387
388 @property
389 def linkMAC( self ):
390 return struct.unpack( '>Q', self.responsePayload[0:8] )[0]
391
392 @property
393 def pumpMAC( self ):
394 return struct.unpack( '>Q', self.responsePayload[8:16] )[0]
395
396class ReadLinkKeyResponseMessage( object ):
397 @classmethod
398 def decode( cls, message ):
399 response = cls()
400 response.responsePayload = message
401 return response
402
403 @property
404 def packedLinkKey( self ):
405 return struct.unpack( '>55s', self.responsePayload[0:55] )[0]
406
407 def linkKey( self, serialNumber ):
408 key = bytearray(b"")
409 pos = ord_hack( serialNumber[-1:] ) & 7
410
411 for it in range(16):
412 if ( ord_hack( self.packedLinkKey[pos + 1] ) & 1) == 1:
413 key.append(~ord_hack( self.packedLinkKey[pos] ) & 0xff)
414 else:
415 key.append(self.packedLinkKey[pos])
416
417 if (( ord_hack( self.packedLinkKey[pos + 1] ) >> 1 ) & 1 ) == 0:
418 pos += 3
419 else:
420 pos += 2
421
422 return key
423
424class PumpTimeResponseMessage( MedtronicReceiveMessage ):
425 @classmethod
426 def decode( cls, message, session ):
427 response = MedtronicReceiveMessage.decode( message, session )
428 if response.messageType != COM_D_COMMAND.TIME_RESPONSE:
429 raise UnexpectedMessageException( "Expected to get a Time Response message '{0}'. Got {1}.".format( COM_D_COMMAND.TIME_RESPONSE, response.messageType ) )
430
431 # Since we only add behaviour, we can cast this class to ourselves
432 response.__class__ = PumpTimeResponseMessage
433 return response
434
435 @property
436 def timeSet( self ):
437 if self.responsePayload[3] == 0:
438 return False
439 else:
440 return True
441
442 @property
443 def encodedDatetime( self ):
444 return struct.unpack( '>Q', self.responsePayload[4:] )[0]
445
446 @property
447 def datetime( self ):
448 dateTimeData = self.encodedDatetime
449 return DateTimeHelper.decodeDateTime( dateTimeData )
450
451 @property
452 def offset( self ):
453 dateTimeData = self.encodedDatetime
454 return DateTimeHelper.decodeDateTimeOffset( dateTimeData )
455
456
457class PumpHistoryInfoResponseMessage( MedtronicReceiveMessage ):
458 @classmethod
459 def decode( cls, message, session ):
460 response = MedtronicReceiveMessage.decode( message, session )
461 if response.messageType != COM_D_COMMAND.READ_HISTORY_INFO_RESPONSE:
462 raise UnexpectedMessageException( "Expected to get a Time Response message '{0}'. Got {1}.".format( COM_D_COMMAND.READ_HISTORY_INFO_RESPONSE, response.messageType ) )
463 # Since we only add behaviour, we can cast this class to ourselves
464 response.__class__ = PumpHistoryInfoResponseMessage
465 return response
466
467 @property
468 def historySize( self ):
469 return struct.unpack( '>I', self.responsePayload[4:8] )[0]
470
471 @property
472 def encodedDatetimeStart( self ):
473 return struct.unpack( '>Q', self.responsePayload[8:16] )[0]
474
475 @property
476 def encodedDatetimeEnd( self ):
477 return struct.unpack( '>Q', self.responsePayload[16:24] )[0]
478
479 @property
480 def datetimeStart( self ):
481 dateTimeData = self.encodedDatetimeStart
482 return DateTimeHelper.decodeDateTime( dateTimeData )
483
484 @property
485 def datetimeEnd( self ):
486 dateTimeData = self.encodedDatetimeEnd
487 return DateTimeHelper.decodeDateTime( dateTimeData )
488
489class MultiPacketSegment( MedtronicReceiveMessage ):
490 @classmethod
491 def decode( cls, message, session ):
492 response = MedtronicReceiveMessage.decode( message, session )
493 # Since we only add behaviour, we can cast this class to ourselves
494 response.__class__ = MultiPacketSegment
495 return response
496
497 @property
498 def packetNumber( self ):
499 return struct.unpack( '>H', self.responsePayload[3:5] )[0]
500
501 @property
502 def payload( self ):
503 return self.responsePayload[5:]
504
505 @property
506 def segmentSize( self ):
507 return struct.unpack( '>I', self.responsePayload[3:7] )[0]
508
509 @property
510 def packetSize( self ):
511 return struct.unpack( '>H', self.responsePayload[7:9] )[0]
512
513 @property
514 def lastPacketSize( self ):
515 return struct.unpack( '>H', self.responsePayload[9:11] )[0]
516
517 @property
518 def packetsToFetch( self ):
519 return struct.unpack( '>H', self.responsePayload[11:13] )[0]
520
521class PumpStatusResponseMessage( MedtronicReceiveMessage ):
522 MMOL = 1
523 MGDL = 2
524
525 @classmethod
526 def decode( cls, message, session ):
527 response = MedtronicReceiveMessage.decode( message, session )
528 if response.messageType != COM_D_COMMAND.READ_PUMP_STATUS_RESPONSE:
529 raise UnexpectedMessageException( "Expected to get a Status Response message '{0}'. Got {1}.".format( COM_D_COMMAND.READ_PUMP_STATUS_RESPONSE, response.messageType ) )
530
531 # Since we only add behaviour, we can cast this class to ourselves
532 response.__class__ = PumpStatusResponseMessage
533 return response
534
535 @property
536 def currentBasalRate( self ):
537 return float( struct.unpack( '>I', self.responsePayload[0x1b:0x1f] )[0] ) / 10000
538
539 @property
540 def tempBasalRate( self ):
541 return float( struct.unpack( '>H', self.responsePayload[0x21:0x23] )[0] ) / 10000
542
543 @property
544 def tempBasalPercentage( self ):
545 return int( struct.unpack( '>B', self.responsePayload[0x23:0x24] )[0] )
546
547 @property
548 def tempBasalMinutesRemaining( self ):
549 return int( struct.unpack( '>H', self.responsePayload[0x24:0x26] )[0] )
550
551 @property
552 def batteryLevelPercentage( self ):
553 return int( struct.unpack( '>B', self.responsePayload[0x2a:0x2b] )[0] )
554
555 @property
556 def insulinUnitsRemaining( self ):
557 return int( struct.unpack( '>I', self.responsePayload[0x2b:0x2f] )[0] ) / 10000
558
559 @property
560 def activeInsulin( self ):
561 return float( struct.unpack( '>H', self.responsePayload[51:53] )[0] ) / 10000
562
563 @property
564 def sensorBGL( self ):
565 return int( struct.unpack( '>H', self.responsePayload[53:55] )[0] )
566
567 @property
568 def trendArrow( self ):
569 status = int( struct.unpack( '>B', self.responsePayload[0x40:0x41] )[0] )
570 if status == 0x60:
571 return "No arrows"
572 elif status == 0xc0:
573 return "3 arrows up"
574 elif status == 0xa0:
575 return "2 arrows up"
576 elif status == 0x80:
577 return "1 arrow up"
578 elif status == 0x40:
579 return "1 arrow down"
580 elif status == 0x20:
581 return "2 arrows down"
582 elif status == 0x00:
583 return "3 arrows down"
584 else:
585 return "Unknown trend"
586
587 @property
588 def sensorBGLTimestamp( self ):
589 dateTimeData = struct.unpack( '>Q', self.responsePayload[55:63] )[0]
590 return DateTimeHelper.decodeDateTime( dateTimeData )
591
592 @property
593 def recentBolusWizard( self ):
594 if self.responsePayload[72] == 0:
595 return False
596 else:
597 return True
598
599 @property
600 def bolusWizardBGL( self ):
601 return struct.unpack( '>H', self.responsePayload[73:75] )[0]
602
603class BeginEHSMMessage( MedtronicSendMessage ):
604 def __init__( self, session ):
605 payload = struct.pack( '>B', 0x00 )
606 MedtronicSendMessage.__init__( self, COM_D_COMMAND.HIGH_SPEED_MODE_COMMAND, session, payload )
607
608class FinishEHSMMessage( MedtronicSendMessage ):
609 def __init__( self, session ):
610 payload = struct.pack( '>B', 0x01 )
611 MedtronicSendMessage.__init__( self, COM_D_COMMAND.HIGH_SPEED_MODE_COMMAND, session, payload )
612
613class PumpTimeRequestMessage( MedtronicSendMessage ):
614 def __init__( self, session ):
615 MedtronicSendMessage.__init__( self, COM_D_COMMAND.TIME_REQUEST, session )
616
617class PumpStatusRequestMessage( MedtronicSendMessage ):
618 def __init__( self, session ):
619 MedtronicSendMessage.__init__( self, COM_D_COMMAND.READ_PUMP_STATUS_REQUEST, session )
620
621class PumpHistoryInfoRequestMessage( MedtronicSendMessage ):
622 def __init__( self, session, dateStart, dateEnd, dateOffset, requestType = HISTORY_DATA_TYPE.PUMP_DATA):
623 histDataType_PumpData = requestType
624 fromRtc = DateTimeHelper.rtcFromDate(dateStart, dateOffset)
625 toRtc = DateTimeHelper.rtcFromDate(dateEnd, dateOffset)
626 payload = struct.pack( '>BBIIH', histDataType_PumpData, 0x04, fromRtc, toRtc, 0x00 )
627 MedtronicSendMessage.__init__( self, COM_D_COMMAND.READ_HISTORY_INFO_REQUEST, session, payload )
628
629class PumpHistoryRequestMessage( MedtronicSendMessage ):
630 def __init__( self, session, dateStart, dateEnd, dateOffset, requestType = HISTORY_DATA_TYPE.PUMP_DATA ):
631 histDataType_PumpData = requestType
632 fromRtc = DateTimeHelper.rtcFromDate(dateStart, dateOffset)
633 toRtc = DateTimeHelper.rtcFromDate(dateEnd, dateOffset)
634 payload = struct.pack( '>BBIIH', histDataType_PumpData, 0x04, fromRtc, toRtc, 0x00 )
635 MedtronicSendMessage.__init__( self, COM_D_COMMAND.READ_HISTORY_REQUEST, session, payload )
636
637class AckMultipacketRequestMessage( MedtronicSendMessage ):
638 SEGMENT_COMMAND__INITIATE_TRANSFER = COM_D_COMMAND.INITIATE_MULTIPACKET_TRANSFER
639 SEGMENT_COMMAND__SEND_NEXT_SEGMENT = COM_D_COMMAND.MULTIPACKET_SEGMENT_TRANSMISSION
640
641 def __init__( self, session, segmentCommand ):
642 payload = struct.pack( '>H', segmentCommand )
643 MedtronicSendMessage.__init__( self, COM_D_COMMAND.ACK_MULTIPACKET_COMMAND, session, payload )
644
645class BasicNgpParametersRequestMessage( MedtronicSendMessage ):
646 def __init__( self, session ):
647 MedtronicSendMessage.__init__( self, COM_D_COMMAND.NGP_PARAMETER_REQUEST, session )
648
649class DeviceCharacteristicsRequestMessage( MedtronicSendMessage ):
650 def __init__( self, session ):
651 MedtronicSendMessage.__init__( self, COM_D_COMMAND.DEVICE_CHARACTERISTICS_REQUEST, session )
652
653class SuspendResumeRequestMessage( MedtronicSendMessage ):
654 def __init__( self, session ):
655 # TODO: Bug? Shall the payload be passed to the message, or not needed?
656 payload = struct.pack( '>B', 0x01 )
657 MedtronicSendMessage.__init__( self, COM_D_COMMAND.SUSPEND_RESUME_REQUEST, session )
658
659class PumpTempBasalRequestMessage( MedtronicSendMessage ):
660 def __init__( self, session ):
661 MedtronicSendMessage.__init__( self, COM_D_COMMAND.TEMP_BASAL_REQUEST, session )
662
663class PumpBolusesRequestMessage( MedtronicSendMessage ):
664 def __init__( self, session ):
665 MedtronicSendMessage.__init__( self, COM_D_COMMAND.BOLUSES_REQUEST, session )
666
667class PumpRemoteBolusRequestMessage( MedtronicSendMessage ):
668 def __init__( self, session, bolusID, amount, execute ):
669 unknown1 = 0 # ??
670 unknown2 = 0 # Square Wave amount?
671 unknown3 = 0 # Square Wave length?
672 payload = struct.pack( '>BBHHBH', bolusID, execute, unknown1, amount * 10000, unknown2, unknown3 )
673 MedtronicSendMessage.__init__( self, COM_D_COMMAND.REMOTE_BOLUS_REQUEST, session, payload )
674
675class Type405RequestMessage( MedtronicSendMessage ):
676 def __init__( self, session, pumpDateTime ):
677 payload = struct.pack( '>BQ', 0x01, pumpDateTime )
678 MedtronicSendMessage.__init__( self, COM_D_COMMAND.REQUEST_0x0405, session, payload )
679
680class Type124RequestMessage( MedtronicSendMessage ):
681 def __init__( self, session, pumpDateTime ):
682 payload = struct.pack( '>QBB', pumpDateTime, 0x00, 0xFF )
683 MedtronicSendMessage.__init__( self, COM_D_COMMAND.REQUEST_0x0124, session, payload )
684
685class BayerBinaryMessage( object ):
686 def __init__( self, messageType=None, session=None, payload=None ):
687 self.payload = payload
688 self.session = session
689 if messageType and self.session:
690 self.envelope = struct.pack( '<BB6s10sBI5sI', 0x51, 3, b'000000', b'\x00' * 10,
691 messageType, self.session.bayerSequenceNumber, b'\x00' * 5,
692 len( self.payload ) if self.payload else 0 )
693 self.envelope += struct.pack( 'B', self.makeMessageCrc() )
694
695 def makeMessageCrc( self ):
696 checksum = 0
697 for x in self.envelope[0:32]:
698 checksum += ord_hack(x)
699 #checksum = sum( bytearray(self.envelope[0:32], 'utf-8') )
700
701 if self.payload:
702 checksum += sum( bytearray( self.payload ) )
703
704 return checksum & 0xff
705
706 def encode( self ):
707 # Increment the Bayer Sequence Number
708 self.session.bayerSequenceNumber += 1
709 if self.payload:
710 return self.envelope + self.payload
711 else:
712 return self.envelope
713
714 @classmethod
715 def decode( cls, message ):
716 response = cls()
717 response.envelope = message[0:33]
718 response.payload = message[33:]
719
720 checksum = message[32]
721 calcChecksum = response.makeMessageCrc()
722 if( checksum != calcChecksum ):
723 logger.error('ChecksumException: Expected to get {0}. Got {1}'.format( calcChecksum, checksum ))
724 raise ChecksumException( 'Expected to get {0}. Got {1}'.format( calcChecksum, checksum ) )
725
726 return response
727
728 @property
729 def linkDeviceOperation( self ):
730 return ord_hack(self.envelope[18])
731
732 # HACK: This is just a debug try, session param shall not be there
733 def checkLinkDeviceOperation( self, expectedValue, session = None ):
734 if self.linkDeviceOperation != expectedValue:
735 logger.debug("### checkLinkDeviceOperation BayerBinaryMessage.envelope: {0}".format(binascii.hexlify(self.envelope)))
736 logger.debug("### checkLinkDeviceOperation BayerBinaryMessage.payload: {0}".format(binascii.hexlify(self.payload)))
737 # HACK: This is just a debug try
738 if self.linkDeviceOperation == 0x80:
739 response = MedtronicReceiveMessage.decode( self.payload, session )
740 logger.warning("#### Message type of caught 0x80: 0x{0:x}".format(response.messageType))
741 raise UnexpectedMessageException( "Expected to get linkDeviceOperation {0:x}. Got {1:x}".format( expectedValue, self.linkDeviceOperation ) )
742
743class Medtronic600SeriesDriver( object ):
744 USB_BLOCKSIZE = 64
745 USB_VID = 0x1a79
746 USB_PID = 0x6210
747 MAGIC_HEADER = b'ABC'
748
749 CHANNELS = [ 0x14, 0x11, 0x0e, 0x17, 0x1a ] # In the order that the CareLink applet requests them
750
751 session = None
752 offset = -1592387759; # Just read out of my pump. Shall be overwritten by reading date/time from pump
753
754 def __init__( self ):
755 self.session = MedtronicSession()
756 self.device = None
757
758 self.deviceInfo = None
759
760 def openDevice( self ):
761 logger.info("# Opening device")
762 self.device = hid.device()
763 self.device.open( self.USB_VID, self.USB_PID )
764
765 logger.info("Manufacturer: %s" % self.device.get_manufacturer_string())
766 logger.info("Product: %s" % self.device.get_product_string())
767 logger.info("Serial No: %s" % self.device.get_serial_number_string())
768
769 def closeDevice( self ):
770 logger.info("# Closing device")
771 self.device.close()
772
773 def readMessage( self ):
774 payload = bytearray()
775 while True:
776 data = self.device.read( self.USB_BLOCKSIZE, timeout_ms = 10000 )
777 if data:
778 if( bytearray( data[0:3] ) != self.MAGIC_HEADER ):
779 logger.error('Recieved invalid USB packet')
780 raise RuntimeError( 'Recieved invalid USB packet')
781 payload.extend( data[4:data[3] + 4] )
782 # TODO - how to deal with messages that finish on the boundary?
783 if data[3] != self.USB_BLOCKSIZE - 4:
784 break
785 else:
786 logger.warning('Timeout waiting for message')
787 raise TimeoutException( 'Timeout waiting for message' )
788
789 # logger.debug("READ: " + binascii.hexlify( payload )) # Debugging
790 return payload
791
792 def sendMessage( self, payload ):
793 # Split the message into 60 byte chunks
794 for packet in [ payload[ i: i+60 ] for i in range( 0, len( payload ), 60 ) ]:
795 message = struct.pack( '>3sB', self.MAGIC_HEADER, len( packet ) ) + packet
796 self.device.write( bytearray( message ) )
797 # logger.debug("SEND: " + binascii.hexlify( message )) # Debugging
798
799 @property
800 def deviceSerial( self ):
801 if not self.deviceInfo:
802 return None
803 else:
804 return self.deviceInfo[0][4][3][1]
805
806 def getDeviceInfo( self ):
807 logger.info("# Read Device Info")
808 self.sendMessage( struct.pack( '>B', 0x58 ) )
809
810 try:
811 msg = self.readMessage()
812
813 if not astm.codec.is_chunked_message( msg ):
814 logger.error('readDeviceInfo: Expected to get an ASTM message, but got {0} instead'.format( binascii.hexlify( msg ) ))
815 raise RuntimeError( 'Expected to get an ASTM message, but got {0} instead'.format( binascii.hexlify( msg ) ) )
816
817 self.deviceInfo = astm.codec.decode( bytes( msg ) )
818 self.session.stickSerial = self.deviceSerial
819 self.checkControlMessage( ascii['ENQ'] )
820
821 except TimeoutException:
822 self.sendMessage( struct.pack( '>B', ascii['EOT'] ) )
823 self.checkControlMessage( ascii['ENQ'] )
824 self.getDeviceInfo()
825
826 def checkControlMessage( self, controlChar ):
827 msg = self.readMessage()
828 if len( msg ) > 0 and msg[0] != controlChar:
829 logger.error(' ### checkControlMessage: Expected to get an 0x{0:x} control character, got message with length {1} and control char 0x{1:x}'.format( controlChar, len( msg ), msg[0] ))
830 raise RuntimeError( 'Expected to get an 0x{0:x} control character, got message with length {1} and control char 0x{1:x}'.format( controlChar, len( msg ), msg[0] ) )
831
832 def enterControlMode( self ):
833 logger.info("# enterControlMode")
834 self.sendMessage( struct.pack( '>B', ascii['NAK'] ) )
835 self.checkControlMessage( ascii['EOT'] )
836 self.sendMessage( struct.pack( '>B', ascii['ENQ'] ) )
837 self.checkControlMessage( ascii['ACK'] )
838
839 def exitControlMode( self ):
840 logger.info("# exitControlMode")
841 try:
842 self.sendMessage( struct.pack( '>B', ascii['EOT'] ) )
843 self.checkControlMessage( ascii['ENQ'] )
844 except Exception:
845 logger.warning("Unexpected error by exitControlMode, ignoring", exc_info = True);
846
847 def enterPassthroughMode( self ):
848 logger.info("# enterPassthroughMode")
849 self.sendMessage( struct.pack( '>2s', b'W|' ) )
850 self.checkControlMessage( ascii['ACK'] )
851 self.sendMessage( struct.pack( '>2s', b'Q|' ) )
852 self.checkControlMessage( ascii['ACK'] )
853 self.sendMessage( struct.pack( '>2s', b'1|' ) )
854 self.checkControlMessage( ascii['ACK'] )
855
856 def exitPassthroughMode( self ):
857 logger.info("# exitPassthroughMode")
858 try:
859 self.sendMessage( struct.pack( '>2s', b'W|' ) )
860 self.checkControlMessage( ascii['ACK'] )
861 self.sendMessage( struct.pack( '>2s', b'Q|' ) )
862 self.checkControlMessage( ascii['ACK'] )
863 self.sendMessage( struct.pack( '>2s', b'0|' ) )
864 self.checkControlMessage( ascii['ACK'] )
865 except Exception:
866 logger.warning("Unexpected error by exitPassthroughMode, ignoring", exc_info = True);
867
868 def openConnection( self ):
869 logger.info("# Request Open Connection")
870
871 mtMessage = binascii.unhexlify( self.session.HMAC )
872 bayerMessage = BayerBinaryMessage( 0x10, self.session, mtMessage )
873 self.sendMessage( bayerMessage.encode() )
874 self.readMessage()
875
876 def closeConnection( self ):
877 logger.info("# Request Close Connection")
878 try:
879 mtMessage = binascii.unhexlify( self.session.HMAC )
880 bayerMessage = BayerBinaryMessage( 0x11, self.session, mtMessage )
881 self.sendMessage( bayerMessage.encode() )
882 self.readMessage()
883 except Exception:
884 logger.warning("Unexpected error by requestCloseConnection, ignoring", exc_info = True);
885
886 def readInfo( self ):
887 logger.info("# Request Read Info")
888 bayerMessage = BayerBinaryMessage( 0x14, self.session )
889 self.sendMessage( bayerMessage.encode() )
890 response = BayerBinaryMessage.decode( self.readMessage() ) # The response is a 0x14 as well
891 info = ReadInfoResponseMessage.decode( response.payload )
892 self.session.linkMAC = info.linkMAC
893 self.session.pumpMAC = info.pumpMAC
894
895 def readLinkKey( self ):
896 logger.info("# Request Read Link Key")
897 bayerMessage = BayerBinaryMessage( 0x16, self.session )
898 self.sendMessage( bayerMessage.encode() )
899 response = BayerBinaryMessage.decode( self.readMessage() ) # The response is a 0x14 as well
900 keyRequest = ReadLinkKeyResponseMessage.decode( response.payload )
901 self.session.KEY = bytes(keyRequest.linkKey( self.session.stickSerial ))
902 logger.debug("LINK KEY: {0}".format(binascii.hexlify(self.session.KEY)))
903
904
905 def negotiateChannel( self ):
906 logger.info("# Negotiate pump comms channel")
907
908 # Scan the last successfully connected channel first, since this could save us negotiating time
909 for self.session.radioChannel in [ self.session.config.lastRadioChannel ] + self.CHANNELS:
910 logger.debug("Negotiating on channel {0}".format( self.session.radioChannel ))
911
912 mtMessage = ChannelNegotiateMessage( self.session )
913
914 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
915 self.sendMessage( bayerMessage.encode() )
916 self.getBayerBinaryMessage(0x81) # Read the 0x81
917 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
918 if len( response.payload ) > 13:
919 # Check that the channel ID matches
920 responseChannel = response.payload[43]
921 if self.session.radioChannel == responseChannel:
922 break
923 else:
924 raise UnexpectedMessageException( "Expected to get a message for channel {0}. Got {1}".format( self.session.radioChannel, responseChannel ) )
925 else:
926 self.session.radioChannel = None
927
928 if not self.session.radioChannel:
929 raise NegotiationException( 'Could not negotiate a comms channel with the pump. Are you near to the pump?' )
930 else:
931 self.session.config.lastRadioChannel = self.session.radioChannel
932
933 def beginEHSM( self ):
934 logger.info("# Begin Extended High Speed Mode Session")
935 mtMessage = BeginEHSMMessage( self.session )
936
937 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
938 self.sendMessage( bayerMessage.encode() )
939 self.getBayerBinaryMessage(0x81) # The Begin EHSM only has an 0x81 response.
940
941 def finishEHSM( self ):
942 logger.info("# Finish Extended High Speed Mode Session")
943 try:
944 mtMessage = FinishEHSMMessage( self.session )
945
946 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
947 self.sendMessage( bayerMessage.encode() )
948 try:
949 self.getBayerBinaryMessage(0x81) # The Finish EHSM only has an 0x81 response.
950 except:
951 # if does not come, ignore...
952 pass
953 except Exception:
954 logger.warning("Unexpected error by finishEHSM, ignoring", exc_info = True);
955
956 def getBayerBinaryMessage(self, expectedLinkDeviceOperation):
957 messageReceived = False
958 message = None
959 while messageReceived == False:
960 message = BayerBinaryMessage.decode(self.readMessage())
961 if message.linkDeviceOperation == expectedLinkDeviceOperation:
962 messageReceived = True
963 else:
964 logger.warning("## getBayerBinaryMessage: waiting for message 0x{0:x}, got 0x{1:x}".format(expectedLinkDeviceOperation, message.linkDeviceOperation))
965 return message
966
967 def getMedtronicMessage(self, expectedMessageTypes):
968 messageReceived = False
969 medMessage = None
970 while messageReceived == False:
971 message = self.getBayerBinaryMessage(0x80)
972 medMessage = MedtronicReceiveMessage.decode(message.payload, self.session)
973 if medMessage.messageType in expectedMessageTypes:
974 messageReceived = True
975 else:
976 logger.warning("## getMedtronicMessage: waiting for message of [{0}], got 0x{1:x}".format(''.join('%04x '%i for i in expectedMessageTypes) , medMessage.messageType))
977 return medMessage
978
979 def getPumpTime( self ):
980 logger.info("# Get Pump Time")
981 mtMessage = PumpTimeRequestMessage( self.session )
982
983 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
984 self.sendMessage( bayerMessage.encode() )
985 self.getBayerBinaryMessage(0x81)
986 result = self.getMedtronicMessage([COM_D_COMMAND.TIME_RESPONSE])
987 self.offset = result.offset;
988 return result
989
990 def getPumpStatus( self ):
991 logger.info("# Get Pump Status")
992 mtMessage = PumpStatusRequestMessage( self.session )
993
994 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
995 self.sendMessage( bayerMessage.encode() )
996 self.getBayerBinaryMessage(0x81) # Read the 0x81
997 response = self.getMedtronicMessage([COM_D_COMMAND.READ_PUMP_STATUS_RESPONSE])
998 return response
999
1000 def getPumpHistoryInfo( self, dateStart, dateEnd, requestType = HISTORY_DATA_TYPE.PUMP_DATA ):
1001 logger.info("# Get Pump History Info")
1002 mtMessage = PumpHistoryInfoRequestMessage( self.session, dateStart, dateEnd, self.offset, requestType )
1003
1004 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1005 self.sendMessage( bayerMessage.encode() )
1006 self.getBayerBinaryMessage(0x81) # Read the 0x81
1007 response = self.getMedtronicMessage([COM_D_COMMAND.READ_HISTORY_INFO_RESPONSE])
1008 return response
1009
1010 def getPumpHistory( self, expectedSize, dateStart, dateEnd, requestType = HISTORY_DATA_TYPE.PUMP_DATA ):
1011 logger.info("# Get Pump History")
1012 allSegments = []
1013 mtMessage = PumpHistoryRequestMessage( self.session, dateStart, dateEnd, self.offset, requestType )
1014
1015 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1016 self.sendMessage( bayerMessage.encode() )
1017 self.getBayerBinaryMessage(0x81) # Read the 0x81
1018
1019 transmissionCompleted = False
1020 while transmissionCompleted != True:
1021 responseSegment = self.getMedtronicMessage([COM_D_COMMAND.HIGH_SPEED_MODE_COMMAND, COM_D_COMMAND.INITIATE_MULTIPACKET_TRANSFER, COM_D_COMMAND.MULTIPACKET_SEGMENT_TRANSMISSION, COM_D_COMMAND.END_HISTORY_TRANSMISSION])
1022
1023 if responseSegment.messageType == COM_D_COMMAND.HIGH_SPEED_MODE_COMMAND:
1024 logger.debug("## getPumpHistory consumed HIGH_SPEED_MODE_COMMAND")
1025 pass
1026 elif responseSegment.messageType == COM_D_COMMAND.INITIATE_MULTIPACKET_TRANSFER:
1027 logger.debug("## getPumpHistory got INITIATE_MULTIPACKET_TRANSFER")
1028 logger.debug("## getPumpHistory INITIATE_MULTIPACKET_TRANSFER.segmentSize: {0}".format(responseSegment.segmentSize))
1029 logger.debug("## getPumpHistory INITIATE_MULTIPACKET_TRANSFER.packetSize: {0}".format(responseSegment.packetSize))
1030 logger.debug("## getPumpHistory INITIATE_MULTIPACKET_TRANSFER.lastPacketSize: {0}".format(responseSegment.lastPacketSize))
1031 logger.debug("## getPumpHistory INITIATE_MULTIPACKET_TRANSFER.packetsToFetch: {0}".format(responseSegment.packetsToFetch))
1032 segmentParams = responseSegment
1033 packets = [None] * responseSegment.packetsToFetch
1034 numPackets = 0
1035 ackMessage = AckMultipacketRequestMessage(self.session, AckMultipacketRequestMessage.SEGMENT_COMMAND__INITIATE_TRANSFER)
1036 bayerAckMessage = BayerBinaryMessage( 0x12, self.session, ackMessage.encode() )
1037 self.sendMessage( bayerAckMessage.encode() )
1038 self.getBayerBinaryMessage(0x81) # Read the 0x81
1039 elif responseSegment.messageType == COM_D_COMMAND.MULTIPACKET_SEGMENT_TRANSMISSION:
1040 logger.debug("## getPumpHistory got MULTIPACKET_SEGMENT_TRANSMISSION")
1041 logger.debug("## getPumpHistory responseSegment.packetNumber: {0}".format(responseSegment.packetNumber))
1042 if responseSegment.packetNumber != (segmentParams.packetsToFetch - 1) and len(responseSegment.payload) != segmentParams.packetSize:
1043 logger.warning("## WARNING - packet length invalid, skipping. Expected {0}, got {1}, for packet {2}/{3}".format(segmentParams.packetSize, len(responseSegment.payload), responseSegment.packetNumber, responseSegment.segmentParams))
1044 continue
1045 if responseSegment.packetNumber == segmentParams.packetsToFetch - 1 and len(responseSegment.payload) != segmentParams.lastPacketSize:
1046 logger.warning("## WARNING - last packet length invalid, skipping. Expected {0}, got {1}, for packet {2}/{3}".format(segmentParams.lastPacketSize, len(responseSegment.payload), responseSegment.packetNumber, responseSegment.segmentParams))
1047 continue
1048 if responseSegment.packetNumber < 0 or responseSegment.packetNumber >= segmentParams.packetsToFetch:
1049 logger.warning("## WARNING - received packed out of expected range. Packet {2}/{3}".format(responseSegment.packetNumber, responseSegment.segmentParams))
1050 continue
1051 if packets[responseSegment.packetNumber] == None:
1052 numPackets = numPackets + 1
1053 packets[responseSegment.packetNumber] = responseSegment.payload
1054 else:
1055 logger.warning("## WARNING - packet duplicated")
1056
1057 if numPackets == segmentParams.packetsToFetch:
1058 logger.debug("## All packets there")
1059 logger.debug("## Requesting next segment")
1060 allSegments.append(packets)
1061
1062 #request next segment
1063 ackMessage = AckMultipacketRequestMessage(self.session, AckMultipacketRequestMessage.SEGMENT_COMMAND__SEND_NEXT_SEGMENT)
1064 bayerAckMessage = BayerBinaryMessage( 0x12, self.session, ackMessage.encode() )
1065 self.sendMessage( bayerAckMessage.encode() )
1066 self.getBayerBinaryMessage(0x81) # Read the 0x81
1067 elif responseSegment.messageType == COM_D_COMMAND.END_HISTORY_TRANSMISSION:
1068 logger.debug("## getPumpHistory got END_HISTORY_TRANSMISSION")
1069 transmissionCompleted = True
1070 else:
1071 logger.warning("## getPumpHistory !!! UNKNOWN MESSAGE !!!")
1072 logger.warning("## getPumpHistory response.messageType: {0:x}".format(responseSegment.messageType))
1073
1074 if transmissionCompleted:
1075 return allSegments
1076 else:
1077 logger.error("Transmission finished, but END_HISTORY_TRANSMISSION did not arrive")
1078 raise DataIncompleteError("Transmission finished, but END_HISTORY_TRANSMISSION did not arrive")
1079
1080 def decodePumpSegment(self, encodedFragmentedSegment, historyType = HISTORY_DATA_TYPE.PUMP_DATA):
1081 decodedBlocks = []
1082 segmentPayload = encodedFragmentedSegment[0]
1083
1084 for idx in range(1, len(encodedFragmentedSegment)):
1085 segmentPayload+= encodedFragmentedSegment[idx]
1086
1087 # Decompress the message
1088 if struct.unpack( '>H', segmentPayload[0:2])[0] == 0x030E:
1089 HEADER_SIZE = 12
1090 BLOCK_SIZE = 2048
1091 # It's an UnmergedHistoryUpdateCompressed response. We need to decompress it
1092 dataType = struct.unpack('>B', segmentPayload[2:3])[0] # Returns a HISTORY_DATA_TYPE
1093 historySizeCompressed = struct.unpack( '>I', segmentPayload[3:7])[0] #segmentPayload.readUInt32BE(0x03)
1094 logger.debug("Compressed: {0}".format(historySizeCompressed))
1095 historySizeUncompressed = struct.unpack( '>I', segmentPayload[7:11])[0] #segmentPayload.readUInt32BE(0x07)
1096 logger.debug("Uncompressed: {0}".format(historySizeUncompressed))
1097 historyCompressed = struct.unpack('>B', segmentPayload[11:12])[0]
1098 logger.debug("IsCompressed: {0}".format(historyCompressed))
1099
1100 if dataType != historyType: # Check HISTORY_DATA_TYPE (PUMP_DATA: 2, SENSOR_DATA: 3)
1101 logger.error('History type in response: {0} {1}'.format(type(dataType), dataType))
1102 raise InvalidMessageError('Unexpected history type in response')
1103
1104 # Check that we have the correct number of bytes in this message
1105 if len(segmentPayload) - HEADER_SIZE != historySizeCompressed:
1106 raise InvalidMessageError('Unexpected message size')
1107
1108
1109 blockPayload = None
1110 if historyCompressed > 0:
1111 blockPayload = lzo.decompress(segmentPayload[HEADER_SIZE:], False, historySizeUncompressed)
1112 else:
1113 blockPayload = segmentPayload[HEADER_SIZE:]
1114
1115
1116 if len(blockPayload) % BLOCK_SIZE != 0:
1117 raise InvalidMessageError('Block payload size is not a multiple of 2048')
1118
1119
1120 for i in range (0, len(blockPayload) // BLOCK_SIZE):
1121 blockSize = struct.unpack('>H', blockPayload[(i + 1) * BLOCK_SIZE - 4 : (i + 1) * BLOCK_SIZE - 2])[0] #blockPayload.readUInt16BE(((i + 1) * ReadHistoryCommand.BLOCK_SIZE) - 4)
1122 blockChecksum = struct.unpack('>H', blockPayload[(i + 1) * BLOCK_SIZE - 2 : (i + 1) * BLOCK_SIZE])[0] #blockPayload.readUInt16BE(((i + 1) * ReadHistoryCommand.BLOCK_SIZE) - 2)
1123
1124 blockStart = i * BLOCK_SIZE
1125 blockData = blockPayload[blockStart : blockStart + blockSize]
1126 calculatedChecksum = MedtronicMessage.calculateCcitt(blockData)
1127 if blockChecksum != calculatedChecksum:
1128 raise ChecksumError('Unexpected checksum in block')
1129 else:
1130 decodedBlocks.append(blockData)
1131 else:
1132 raise InvalidMessageError('Unknown history response message type')
1133
1134 return decodedBlocks
1135
1136 def decodeEvents(self, decodedBlocks):
1137 eventList = []
1138 for page in decodedBlocks:
1139 pos = 0;
1140
1141 while pos < len(page):
1142 eventSize = struct.unpack('>B', page[pos + 2 : pos + 3])[0] # page[pos + 2];
1143 eventData = page[pos : pos + eventSize] # page.slice(pos, pos + eventSize);
1144 pos += eventSize
1145 eventList.extend(NGPHistoryEvent(eventData).eventInstance().allNestedEvents())
1146 return eventList
1147
1148 def processPumpHistory( self, historySegments, historyType = HISTORY_DATA_TYPE.PUMP_DATA):
1149 historyEvents = []
1150 for segment in historySegments:
1151 decodedBlocks = self.decodePumpSegment(segment, historyType)
1152 historyEvents += self.decodeEvents(decodedBlocks)
1153 for event in historyEvents:
1154 event.postProcess(historyEvents)
1155 return historyEvents
1156
1157 def getTempBasalStatus( self ):
1158 logger.info("# Get Temp Basal Status")
1159 mtMessage = PumpTempBasalRequestMessage( self.session )
1160
1161 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1162 self.sendMessage( bayerMessage.encode() )
1163 self.getBayerBinaryMessage(0x81) # Read the 0x81
1164 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
1165 return MedtronicReceiveMessage.decode( response.payload, self.session )
1166
1167 def getBolusesStatus( self ):
1168 logger.info("# Get Boluses Status")
1169 mtMessage = PumpBolusesRequestMessage( self.session )
1170
1171 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1172 self.sendMessage( bayerMessage.encode() )
1173 self.getBayerBinaryMessage(0x81) # Read the 0x81
1174 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
1175 return MedtronicReceiveMessage.decode( response.payload, self.session )
1176
1177 def getBasicParameters( self ):
1178 logger.info("# Get Basic Parameters")
1179 mtMessage = BasicNgpParametersRequestMessage( self.session )
1180
1181 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1182 self.sendMessage( bayerMessage.encode() )
1183 self.getBayerBinaryMessage(0x81) # Read the 0x81
1184 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
1185 return MedtronicReceiveMessage.decode( response.payload, self.session )
1186
1187 def do405Message( self, pumpDateTime ):
1188 logger.info("# Send Message Type 405")
1189 mtMessage = Type405RequestMessage( self.session, pumpDateTime )
1190
1191 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1192 self.sendMessage( bayerMessage.encode() )
1193 self.getBayerBinaryMessage(0x81) # Read the 0x81
1194 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
1195 return MedtronicReceiveMessage.decode( response.payload, self.session )
1196
1197 def do124Message( self, pumpDateTime ):
1198 logger.info("# Send Message Type 124")
1199 mtMessage = Type124RequestMessage( self.session, pumpDateTime )
1200
1201 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1202 self.sendMessage( bayerMessage.encode() )
1203 self.getBayerBinaryMessage(0x81) # Read the 0x81
1204 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
1205 return MedtronicReceiveMessage.decode( response.payload, self.session )
1206
1207 def doRemoteBolus( self, bolusID, amount, execute ):
1208 logger.info("# Execute Remote Bolus")
1209 mtMessage = PumpRemoteBolusRequestMessage( self.session, bolusID, amount, execute )
1210
1211 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1212 self.sendMessage( bayerMessage.encode() )
1213 self.getBayerBinaryMessage(0x81) # Read the 0x81
1214 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
1215 return MedtronicReceiveMessage.decode( response.payload, self.session )
1216
1217 def doRemoteSuspend( self ):
1218 logger.info("# Execute Remote Suspend")
1219 mtMessage = SuspendResumeRequestMessage( self.session )
1220
1221 bayerMessage = BayerBinaryMessage( 0x12, self.session, mtMessage.encode() )
1222 self.sendMessage( bayerMessage.encode() )
1223 response81 = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x81
1224 logger.debug(binascii.hexlify( response81.payload ))
1225
1226 response = BayerBinaryMessage.decode( self.readMessage() ) # Read the 0x80
1227 return MedtronicReceiveMessage.decode( response.payload, self.session )
1228
1229nextRead = 0
1230
1231def downloadPumpSession(downloadOperations):
1232 global nextRead
1233
1234 while True:
1235
1236
1237 while int(round(time.time() * 1000))<nextRead:
1238 time.sleep(1)
1239
1240
1241 mt = Medtronic600SeriesDriver()
1242 mt.openDevice()
1243 try:
1244 mt.getDeviceInfo()
1245 logger.info("Device serial: {0}".format(mt.deviceSerial))
1246 mt.enterControlMode()
1247 try:
1248 mt.enterPassthroughMode()
1249 try:
1250 mt.openConnection()
1251 try:
1252 mt.readInfo()
1253 mt.readLinkKey()
1254 try:
1255 mt.negotiateChannel()
1256 except:
1257 logger.error("downloadPumpSession: Cannot connect to the pump. Retrying in 30 seconds")
1258 time.sleep(30)
1259 continue
1260 mt.beginEHSM()
1261 try:
1262 # We need to read always the pump time to store the offset for later messeging
1263 mt.getPumpTime()
1264 try:
1265 downloadOperations(mt)
1266 except Exception:
1267 logger.error("Unexpected error in client downloadOperations", exc_info = True)
1268 raise
1269 finally:
1270 mt.finishEHSM()
1271 finally:
1272 mt.closeConnection()
1273 finally:
1274 mt.exitPassthroughMode()
1275 finally:
1276 mt.exitControlMode()
1277 finally:
1278 mt.closeDevice()
1279
1280def pumpDownload(mt):
1281 global nextRead
1282
1283 nextRead=int(round(time.time() * 1000))
1284 epoch = datetime.datetime.fromtimestamp(0, tz.tzlocal())
1285
1286 status = mt.getPumpStatus()
1287 #print (binascii.hexlify( status.responsePayload ))
1288
1289 #newStatus = object()
1290 #newStatus.activeInsulin=status.activeInsulin;
1291 #print (json.dumps(newStatus))
1292 print ("Active Insulin: {0:.3f}U".format( status.activeInsulin ))
1293 bgl = status.sensorBGL
1294 print ("Sensor BGL: {0} mg/dL at {1}".format( status.sensorBGL,
1295 status.sensorBGLTimestamp.strftime( "%c" ) ))
1296
1297 print(status.sensorBGLTimestamp)
1298 lastReadEpoch = int(round((status.sensorBGLTimestamp - epoch).total_seconds() *1000))
1299 newRead = lastReadEpoch + (60*1000*5)
1300 if newRead < nextRead:
1301 bgl = -1
1302 nextRead = nextRead+(60*1000*5)
1303 else:
1304 nextRead = newRead
1305 print("New read at: {0}", nextRead)
1306 print ("BGL trend: {0}".format( status.trendArrow ))
1307 print ("Current basal rate: {0:.3f}U".format( status.currentBasalRate ))
1308 print ("Temp basal rate: {0:.3f}U".format( status.tempBasalRate ))
1309 print ("Temp basal percentage: {0}%".format( status.tempBasalPercentage ))
1310 print ("Units remaining: {0:.3f}U".format( status.insulinUnitsRemaining ))
1311 print ("Battery remaining: {0}%".format( status.batteryLevelPercentage ))
1312
1313 try:
1314 broker_address="casacloud.mynetgear.com"
1315 #broker_address="iot.eclipse.org" #use external broker
1316 client = mqtt.Client("pi-gbl") #create new instance
1317 client.username_pw_set("arduino", "!")
1318 client.connect(broker_address) #connect to broker
1319 client.publish("glicemia", str(bgl), retain=True)#publish
1320 client.disconnect()
1321 except:
1322 print ("Error while sending data to MQTT!")
1323## print ("Getting Pump history info")
1324## start_date = datetime.datetime.now() - datetime.timedelta(days=1)
1325## historyInfo = mt.getPumpHistoryInfo(start_date, datetime.datetime.max, HISTORY_DATA_TYPE.PUMP_DATA)
1326## # print (binascii.hexlify( historyInfo.responsePayload, ))
1327## print (" Pump Start: {0}".format(historyInfo.datetimeStart))
1328## print (" Pump End: {0}".format(historyInfo.datetimeEnd));
1329## print (" Pump Size: {0}".format(historyInfo.historySize));
1330##
1331## print ("Getting Pump history")
1332## history_pages = mt.getPumpHistory(historyInfo.historySize, start_date, datetime.datetime.max, HISTORY_DATA_TYPE.PUMP_DATA)
1333##
1334## # Uncomment to save events for testing without Pump (use: tests/process_saved_history.py)
1335## #with open('history_data.dat', 'wb') as output:
1336## # pickle.dump(history_pages, output)
1337##
1338## events = mt.processPumpHistory(history_pages, HISTORY_DATA_TYPE.PUMP_DATA)
1339## print ("# All Pump events:")
1340## for ev in events:
1341## print (" Pump: ", ev)
1342## print ("# End Pump events")
1343
1344## print ("Getting sensor history info")
1345## sensHistoryInfo = mt.getPumpHistoryInfo(start_date, datetime.datetime.max, HISTORY_DATA_TYPE.SENSOR_DATA)
1346## # print (binascii.hexlify( historyInfo.responsePayload, ))
1347## print (" Sensor Start: {0}".format(sensHistoryInfo.datetimeStart))
1348## print (" Sensor End: {0}".format(sensHistoryInfo.datetimeEnd));
1349## print (" Sensor Size: {0}".format(sensHistoryInfo.historySize));
1350##
1351## print ("Getting Sensor history")
1352## sensor_history_pages = mt.getPumpHistory(sensHistoryInfo.historySize, start_date, datetime.datetime.max, HISTORY_DATA_TYPE.SENSOR_DATA)
1353##
1354## # Uncomment to save events for testing without Pump (use: tests/process_saved_history.py)
1355## #with open('sensor_history_data.dat', 'wb') as output:
1356## # pickle.dump(sensor_history_pages, output)
1357##
1358## sensorEvents = mt.processPumpHistory(sensor_history_pages, HISTORY_DATA_TYPE.SENSOR_DATA)
1359## print ("# All Sensor events:")
1360## for ev in sensorEvents:
1361## print (" Sensor", ev)
1362## print ("# End Sensor events")
1363##
1364##
1365## print (binascii.hexlify( mt.doRemoteSuspend().responsePayload ))
1366##
1367### Commented code to try remote bolusing...
1368### print (binascii.hexlify( mt.do405Message( pumpDatetime.encodedDatetime ).responsePayload ))
1369### print (binascii.hexlify( mt.do124Message( pumpDatetime.encodedDatetime ).responsePayload ))
1370## print (binascii.hexlify( mt.getBasicParameters().responsePayload ))
1371## print (binascii.hexlify( mt.getTempBasalStatus().responsePayload ))
1372## print (binascii.hexlify( mt.getBolusesStatus().responsePayload ))
1373## print (binascii.hexlify( mt.doRemoteBolus( 1, 0.1, 0 ).responsePayload ))
1374## print (binascii.hexlify( mt.doRemoteBolus( 1, 0.1, 1 ).responsePayload ))
1375##
1376
1377if __name__ == '__main__':
1378 downloadPumpSession(pumpDownload)